feat: add skip_macro_invocations option (#5347)

* feat: add skip_macro_names option

* [review] update configuration documentation

* [review] fix docstring

* [feat] implement wildcard macro invocation skip

* commit missed files

* [review] test override skip macro names

* [review] skip_macro_names -> skip_macro_invocations

* [review] expand doc configuration

* [review] add lots more tests

* [review] add use alias test examples

* [review] add link to standard macro behaviour
This commit is contained in:
Tom Milligan 2022-07-13 01:31:19 +01:00 committed by GitHub
parent 2403f827bf
commit c240f3a6b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 459 additions and 4 deletions

View File

@ -1014,6 +1014,62 @@ macro_rules! foo {
See also [`format_macro_matchers`](#format_macro_matchers).
## `skip_macro_invocations`
Skip formatting the bodies of macro invocations with the following names.
rustfmt will not format any macro invocation for macros with names set in this list.
Including the special value "*" will prevent any macro invocations from being formatted.
Note: This option does not have any impact on how rustfmt formats macro definitions.
- **Default value**: `[]`
- **Possible values**: a list of macro name idents, `["name_0", "name_1", ..., "*"]`
- **Stable**: No (tracking issue: [#5346](https://github.com/rust-lang/rustfmt/issues/5346))
#### `[]` (default):
rustfmt will follow its standard approach to formatting macro invocations.
No macro invocations will be skipped based on their name. More information about rustfmt's standard macro invocation formatting behavior can be found in [#5437](https://github.com/rust-lang/rustfmt/discussions/5437).
```rust
lorem!(
const _: u8 = 0;
);
ipsum!(
const _: u8 = 0;
);
```
#### `["lorem"]`:
The named macro invocations will be skipped.
```rust
lorem!(
const _: u8 = 0;
);
ipsum!(
const _: u8 = 0;
);
```
#### `["*"]`:
The special selector `*` will skip all macro invocations.
```rust
lorem!(
const _: u8 = 0;
);
ipsum!(
const _: u8 = 0;
);
```
## `format_strings`

View File

@ -1,4 +1,5 @@
use crate::config::file_lines::FileLines;
use crate::config::macro_names::MacroSelectors;
use crate::config::options::{IgnoreList, WidthHeuristics};
/// Trait for types that can be used in `Config`.
@ -46,6 +47,12 @@ impl ConfigType for FileLines {
}
}
impl ConfigType for MacroSelectors {
fn doc_hint() -> String {
String::from("[<string>, ...]")
}
}
impl ConfigType for WidthHeuristics {
fn doc_hint() -> String {
String::new()

118
src/config/macro_names.rs Normal file
View File

@ -0,0 +1,118 @@
//! This module contains types and functions to support formatting specific macros.
use itertools::Itertools;
use std::{fmt, str};
use serde::{Deserialize, Serialize};
use serde_json as json;
use thiserror::Error;
/// Defines the name of a macro.
#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Deserialize, Serialize)]
pub struct MacroName(String);
impl MacroName {
pub fn new(other: String) -> Self {
Self(other)
}
}
impl fmt::Display for MacroName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl From<MacroName> for String {
fn from(other: MacroName) -> Self {
other.0
}
}
/// Defines a selector to match against a macro.
#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Deserialize, Serialize)]
pub enum MacroSelector {
Name(MacroName),
All,
}
impl fmt::Display for MacroSelector {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Name(name) => name.fmt(f),
Self::All => write!(f, "*"),
}
}
}
impl str::FromStr for MacroSelector {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"*" => MacroSelector::All,
name => MacroSelector::Name(MacroName(name.to_owned())),
})
}
}
/// A set of macro selectors.
#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
pub struct MacroSelectors(pub Vec<MacroSelector>);
impl fmt::Display for MacroSelectors {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0.iter().format(", "))
}
}
#[derive(Error, Debug)]
pub enum MacroSelectorsError {
#[error("{0}")]
Json(json::Error),
}
// This impl is needed for `Config::override_value` to work for use in tests.
impl str::FromStr for MacroSelectors {
type Err = MacroSelectorsError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let raw: Vec<&str> = json::from_str(s).map_err(MacroSelectorsError::Json)?;
Ok(Self(
raw.into_iter()
.map(|raw| {
MacroSelector::from_str(raw).expect("MacroSelector from_str is infallible")
})
.collect(),
))
}
}
#[cfg(test)]
mod test {
use super::*;
use std::str::FromStr;
#[test]
fn macro_names_from_str() {
let macro_names = MacroSelectors::from_str(r#"["foo", "*", "bar"]"#).unwrap();
assert_eq!(
macro_names,
MacroSelectors(
[
MacroSelector::Name(MacroName("foo".to_owned())),
MacroSelector::All,
MacroSelector::Name(MacroName("bar".to_owned()))
]
.into_iter()
.collect()
)
);
}
#[test]
fn macro_names_display() {
let macro_names = MacroSelectors::from_str(r#"["foo", "*", "bar"]"#).unwrap();
assert_eq!(format!("{}", macro_names), "foo, *, bar");
}
}

View File

@ -13,6 +13,8 @@ pub use crate::config::file_lines::{FileLines, FileName, Range};
#[allow(unreachable_pub)]
pub use crate::config::lists::*;
#[allow(unreachable_pub)]
pub use crate::config::macro_names::{MacroSelector, MacroSelectors};
#[allow(unreachable_pub)]
pub use crate::config::options::*;
#[macro_use]
@ -22,6 +24,7 @@ pub(crate) mod options;
pub(crate) mod file_lines;
pub(crate) mod lists;
pub(crate) mod macro_names;
// This macro defines configuration options used in rustfmt. Each option
// is defined as follows:
@ -67,6 +70,8 @@ create_config! {
format_macro_matchers: bool, false, false,
"Format the metavariable matching patterns in macros";
format_macro_bodies: bool, true, false, "Format the bodies of macros";
skip_macro_invocations: MacroSelectors, MacroSelectors::default(), false,
"Skip formatting the bodies of macros invoked with the following names.";
hex_literal_case: HexLiteralCase, HexLiteralCase::Preserve, false,
"Format hexadecimal integer literals";
@ -403,6 +408,7 @@ mod test {
use super::*;
use std::str;
use crate::config::macro_names::MacroName;
use rustfmt_config_proc_macro::{nightly_only_test, stable_only_test};
#[allow(dead_code)]
@ -611,6 +617,7 @@ normalize_doc_attributes = false
format_strings = false
format_macro_matchers = false
format_macro_bodies = true
skip_macro_invocations = []
hex_literal_case = "Preserve"
empty_item_single_line = true
struct_lit_single_line = true
@ -1019,4 +1026,17 @@ make_backup = false
);
}
}
#[test]
fn test_override_skip_macro_invocations() {
let mut config = Config::default();
config.override_value("skip_macro_invocations", r#"["*", "println"]"#);
assert_eq!(
config.skip_macro_invocations(),
MacroSelectors(vec![
MacroSelector::All,
MacroSelector::Name(MacroName::new("println".to_owned()))
])
);
}
}

View File

@ -7,6 +7,7 @@ use rustc_ast_pretty::pprust;
/// by other context. Query this context to know if you need skip a block.
#[derive(Default, Clone)]
pub(crate) struct SkipContext {
pub(crate) all_macros: bool,
macros: Vec<String>,
attributes: Vec<String>,
}
@ -23,8 +24,15 @@ impl SkipContext {
self.attributes.append(&mut other.attributes);
}
pub(crate) fn update_macros<T>(&mut self, other: T)
where
T: IntoIterator<Item = String>,
{
self.macros.extend(other.into_iter());
}
pub(crate) fn skip_macro(&self, name: &str) -> bool {
self.macros.iter().any(|n| n == name)
self.all_macros || self.macros.iter().any(|n| n == name)
}
pub(crate) fn skip_attribute(&self, name: &str) -> bool {

View File

@ -27,8 +27,13 @@ impl ConfigurationSection {
lazy_static! {
static ref CONFIG_NAME_REGEX: regex::Regex =
regex::Regex::new(r"^## `([^`]+)`").expect("failed creating configuration pattern");
// Configuration values, which will be passed to `from_str`:
//
// - must be prefixed with `####`
// - must be wrapped in backticks
// - may by wrapped in double quotes (which will be stripped)
static ref CONFIG_VALUE_REGEX: regex::Regex =
regex::Regex::new(r#"^#### `"?([^`"]+)"?`"#)
regex::Regex::new(r#"^#### `"?([^`]+?)"?`"#)
.expect("failed creating configuration value pattern");
}

View File

@ -8,7 +8,7 @@ use rustc_span::{symbol, BytePos, Pos, Span};
use crate::attr::*;
use crate::comment::{contains_comment, rewrite_comment, CodeCharKind, CommentCodeSlices};
use crate::config::Version;
use crate::config::{BraceStyle, Config};
use crate::config::{BraceStyle, Config, MacroSelector};
use crate::coverage::transform_missing_snippet;
use crate::items::{
format_impl, format_trait, format_trait_alias, is_mod_decl, is_use_item, rewrite_extern_crate,
@ -770,6 +770,15 @@ impl<'b, 'a: 'b> FmtVisitor<'a> {
snippet_provider: &'a SnippetProvider,
report: FormatReport,
) -> FmtVisitor<'a> {
let mut skip_context = SkipContext::default();
let mut macro_names = Vec::new();
for macro_selector in config.skip_macro_invocations().0 {
match macro_selector {
MacroSelector::Name(name) => macro_names.push(name.to_string()),
MacroSelector::All => skip_context.all_macros = true,
}
}
skip_context.update_macros(macro_names);
FmtVisitor {
parent_context: None,
parse_sess: parse_session,
@ -784,7 +793,7 @@ impl<'b, 'a: 'b> FmtVisitor<'a> {
is_macro_def: false,
macro_rewrite_failure: false,
report,
skip_context: Default::default(),
skip_context,
}
}

View File

@ -0,0 +1,11 @@
// rustfmt-skip_macro_invocations: ["*"]
// Should skip this invocation
items!(
const _: u8 = 0;
);
// Should skip this invocation
renamed_items!(
const _: u8 = 0;
);

View File

@ -0,0 +1,11 @@
// rustfmt-skip_macro_invocations: ["*","items"]
// Should skip this invocation
items!(
const _: u8 = 0;
);
// Should also skip this invocation, as the wildcard covers it
renamed_items!(
const _: u8 = 0;
);

View File

@ -0,0 +1,11 @@
// rustfmt-skip_macro_invocations: []
// Should not skip this invocation
items!(
const _: u8 = 0;
);
// Should not skip this invocation
renamed_items!(
const _: u8 = 0;
);

View File

@ -0,0 +1,11 @@
// rustfmt-skip_macro_invocations: ["items"]
// Should skip this invocation
items!(
const _: u8 = 0;
);
// Should not skip this invocation
renamed_items!(
const _: u8 = 0;
);

View File

@ -0,0 +1,6 @@
// rustfmt-skip_macro_invocations: ["unknown"]
// Should not skip this invocation
items!(
const _: u8 = 0;
);

View File

@ -0,0 +1,16 @@
// rustfmt-skip_macro_invocations: ["foo","bar"]
// Should skip this invocation
foo!(
const _: u8 = 0;
);
// Should skip this invocation
bar!(
const _: u8 = 0;
);
// Should not skip this invocation
baz!(
const _: u8 = 0;
);

View File

@ -0,0 +1,6 @@
// rustfmt-skip_macro_invocations: ["items"]
// Should not skip this invocation
self::items!(
const _: u8 = 0;
);

View File

@ -0,0 +1,6 @@
// rustfmt-skip_macro_invocations: ["self::items"]
// Should skip this invocation
self::items!(
const _: u8 = 0;
);

View File

@ -0,0 +1,6 @@
// rustfmt-skip_macro_invocations: ["self::items"]
// Should not skip this invocation
items!(
const _: u8 = 0;
);

View File

@ -0,0 +1,32 @@
// rustfmt-skip_macro_invocations: ["aaa","ccc"]
// These tests demonstrate a realistic use case with use aliases.
// The use statements should not impact functionality in any way.
use crate::{aaa, bbb, ddd};
// No use alias, invocation in list
// Should skip this invocation
aaa!(
const _: u8 = 0;
);
// Use alias, invocation in list
// Should skip this invocation
use crate::bbb as ccc;
ccc!(
const _: u8 = 0;
);
// Use alias, invocation not in list
// Should not skip this invocation
use crate::ddd as eee;
eee!(
const _: u8 = 0;
);
// No use alias, invocation not in list
// Should not skip this invocation
fff!(
const _: u8 = 0;
);

View File

@ -0,0 +1,11 @@
// rustfmt-skip_macro_invocations: ["*"]
// Should skip this invocation
items!(
const _: u8 = 0;
);
// Should skip this invocation
renamed_items!(
const _: u8 = 0;
);

View File

@ -0,0 +1,11 @@
// rustfmt-skip_macro_invocations: ["*","items"]
// Should skip this invocation
items!(
const _: u8 = 0;
);
// Should also skip this invocation, as the wildcard covers it
renamed_items!(
const _: u8 = 0;
);

View File

@ -0,0 +1,11 @@
// rustfmt-skip_macro_invocations: []
// Should not skip this invocation
items!(
const _: u8 = 0;
);
// Should not skip this invocation
renamed_items!(
const _: u8 = 0;
);

View File

@ -0,0 +1,11 @@
// rustfmt-skip_macro_invocations: ["items"]
// Should skip this invocation
items!(
const _: u8 = 0;
);
// Should not skip this invocation
renamed_items!(
const _: u8 = 0;
);

View File

@ -0,0 +1,6 @@
// rustfmt-skip_macro_invocations: ["unknown"]
// Should not skip this invocation
items!(
const _: u8 = 0;
);

View File

@ -0,0 +1,16 @@
// rustfmt-skip_macro_invocations: ["foo","bar"]
// Should skip this invocation
foo!(
const _: u8 = 0;
);
// Should skip this invocation
bar!(
const _: u8 = 0;
);
// Should not skip this invocation
baz!(
const _: u8 = 0;
);

View File

@ -0,0 +1,6 @@
// rustfmt-skip_macro_invocations: ["items"]
// Should not skip this invocation
self::items!(
const _: u8 = 0;
);

View File

@ -0,0 +1,6 @@
// rustfmt-skip_macro_invocations: ["self::items"]
// Should skip this invocation
self::items!(
const _: u8 = 0;
);

View File

@ -0,0 +1,6 @@
// rustfmt-skip_macro_invocations: ["self::items"]
// Should not skip this invocation
items!(
const _: u8 = 0;
);

View File

@ -0,0 +1,32 @@
// rustfmt-skip_macro_invocations: ["aaa","ccc"]
// These tests demonstrate a realistic use case with use aliases.
// The use statements should not impact functionality in any way.
use crate::{aaa, bbb, ddd};
// No use alias, invocation in list
// Should skip this invocation
aaa!(
const _: u8 = 0;
);
// Use alias, invocation in list
// Should skip this invocation
use crate::bbb as ccc;
ccc!(
const _: u8 = 0;
);
// Use alias, invocation not in list
// Should not skip this invocation
use crate::ddd as eee;
eee!(
const _: u8 = 0;
);
// No use alias, invocation not in list
// Should not skip this invocation
fff!(
const _: u8 = 0;
);