From 239c27d9c8a84ddb8c848892e4f228e33abc337e Mon Sep 17 00:00:00 2001 From: Max Dexheimer Date: Thu, 4 Sep 2025 19:21:45 +0200 Subject: [PATCH 1/4] Implement generalized formatting of meta-style attributes --- src/attr.rs | 3 +- src/attr/meta2.rs | 243 ++++++++++++++++++++++++++++++++++++++++++++++ src/overflow.rs | 22 ++++- 3 files changed, 266 insertions(+), 2 deletions(-) create mode 100644 src/attr/meta2.rs diff --git a/src/attr.rs b/src/attr.rs index e2104617fdc..ebef4487c6f 100644 --- a/src/attr.rs +++ b/src/attr.rs @@ -322,6 +322,7 @@ impl Rewrite for ast::MetaItem { } } +pub(crate) mod meta2; impl Rewrite for ast::Attribute { fn rewrite(&self, context: &RewriteContext<'_>, shape: Shape) -> Option { self.rewrite_result(context, shape).ok() @@ -342,7 +343,7 @@ impl Rewrite for ast::Attribute { return Ok(snippet.to_owned()); } - if let Some(ref meta) = self.meta() { + if let Some(meta) = meta2::MetaItem2::from_attr(self, context) { // This attribute is possibly a doc attribute needing normalization to a doc comment if context.config.normalize_doc_attributes() && meta.has_name(sym::doc) { if let Some(ref literal) = meta.value_str() { diff --git a/src/attr/meta2.rs b/src/attr/meta2.rs new file mode 100644 index 00000000000..295af58179b --- /dev/null +++ b/src/attr/meta2.rs @@ -0,0 +1,243 @@ +use rustc_ast::ptr::P; +use rustc_ast::tokenstream::{TokenStream, TokenTree}; +use rustc_ast::{ast, token}; +use rustc_parse::exp; +use rustc_parse::parser::Parser; +use rustc_session::parse::ParseSess; +use rustc_span::Span; + +use crate::config::lists::SeparatorTactic; +use crate::expr::rewrite_literal; +use crate::overflow; +use crate::rewrite::{Rewrite, RewriteContext, RewriteResult}; +use crate::shape::Shape; +use crate::spanned::Spanned; +use crate::types::{PathContext, rewrite_path}; + +fn is_eof(token: &token::Token) -> bool { + matches!( + token, + rustc_ast::token::Token { + kind: rustc_ast::token::TokenKind::Eof, + .. + } + ) +} +fn try_parse<'a, T>( + parser: &mut Parser<'a>, + parse: impl FnOnce(&mut Parser<'a>) -> rustc_errors::PResult<'a, Option>, +) -> Option { + let mut fork = parser.clone(); + match parse(&mut fork) { + Ok(x) => match parser.psess.dcx().has_errors() { + Some(_) => { + parser.psess.dcx().reset_err_count(); + None + } + None => match x { + Some(x) => { + *parser = fork; + Some(x) + } + None => None, + }, + }, + Err(e) => { + e.cancel(); + parser.psess.dcx().reset_err_count(); + None + } + } +} + +#[derive(Debug)] +pub(crate) struct MetaItem2 { + #[allow(dead_code)] // not used here, but part of the ast copied over from rustc + pub unsafety: ast::Safety, + pub path: ast::Path, + pub kind: MetaItemKind2, + pub span: Span, +} +impl MetaItem2 { + pub(crate) fn from_attr(attr: &ast::Attribute, context: &RewriteContext<'_>) -> Option { + match &attr.kind { + ast::AttrKind::Normal(normal) => { + let _guard = context.enter_macro(); + Self::from_attr_item(&normal.item, context.psess.inner()) + } + ast::AttrKind::DocComment(..) => None, + } + } + fn from_attr_item(attr: &ast::AttrItem, sess: &ParseSess) -> Option { + Some(Self { + kind: MetaItemKind2::from_attr_args(&attr.args, sess)?, + unsafety: attr.unsafety, + path: attr.path.clone(), + span: attr.span(), + }) + } + pub(crate) fn has_name(&self, name: rustc_span::Symbol) -> bool { + self.path == name + } + pub(crate) fn value_str(&self) -> Option { + if let MetaItemKind2::NameValue(expr) = &self.kind { + if let ast::Expr { + kind: ast::ExprKind::Lit(token_lit), + .. + } = &**expr + { + return ast::LitKind::from_token_lit(*token_lit) + .ok() + .and_then(|it| it.str()); + } + } + None + } +} + +#[derive(Debug)] +pub(crate) enum MetaItemKind2 { + Word, + List(Vec), + NameValue(P), +} +impl MetaItemKind2 { + fn from_attr_args(args: &ast::AttrArgs, sess: &ParseSess) -> Option { + match args { + ast::AttrArgs::Empty => Some(Self::Word), + ast::AttrArgs::Delimited(ast::DelimArgs { + dspan: _, + delim: token::Delimiter::Parenthesis, + tokens, + }) => Self::list_from_tokens(tokens.clone(), sess).map(Self::List), + ast::AttrArgs::Delimited(..) => None, + ast::AttrArgs::Eq { expr, .. } => Some(Self::NameValue(expr.clone())), + } + } + + fn list_from_tokens(tokens: TokenStream, sess: &ParseSess) -> Option> { + let mut parser = Parser::new(sess, tokens, None); + let mut result = Vec::new(); + while !is_eof(&parser.token) { + let eat_opt_comma = + |parser: &mut Parser<'_>| parser.eat(exp!(Eof)) || parser.eat(exp!(Comma)); + let inner = try_parse(&mut parser, |parser| { + Ok(MetaItemInner2::parse_noexpr(parser).take_if(|_| eat_opt_comma(parser))) + }) + .or_else(|| { + try_parse(&mut parser, |parser| { + let expr = MetaItemInner2::Expr(parser.parse_expr()?); + Ok(eat_opt_comma(parser).then_some(expr)) + }) + }); + result.push(inner?); + } + Some(result) + } +} + +#[derive(Debug)] +pub(crate) enum MetaItemInner2 { + MetaItem(MetaItem2), + Lit(ast::MetaItemLit), + Expr(P), +} + +impl MetaItemInner2 { + fn parse_noexpr(parser: &mut Parser<'_>) -> Option { + if let Some(lit) = ast::MetaItemLit::from_token(&parser.token) { + parser.bump(); + Some(Self::Lit(lit)) + } else if let token::TokenKind::OpenDelim(token::Delimiter::Invisible(_)) = + &parser.token.kind + { + if let TokenTree::Delimited(.., token::Delimiter::Invisible(_), inner) = + parser.parse_token_tree() + { + Self::parse_noexpr(&mut Parser::new(parser.psess, inner, None)) + } else { + None + } + } else { + try_parse(parser, |fork| { + let item = fork.parse_attr_item(rustc_parse::parser::ForceCollect::No)?; + Ok(MetaItem2::from_attr_item(&item, parser.psess).map(Self::MetaItem)) + }) + } + } +} + +impl Rewrite for MetaItem2 { + fn rewrite(&self, context: &RewriteContext<'_>, shape: Shape) -> Option { + self.rewrite_result(context, shape).ok() + } + + fn rewrite_result(&self, context: &RewriteContext<'_>, shape: Shape) -> RewriteResult { + Ok(match self.kind { + MetaItemKind2::Word => { + rewrite_path(context, PathContext::Type, &None, &self.path, shape)? + } + MetaItemKind2::List(ref list) => { + let path = rewrite_path(context, PathContext::Type, &None, &self.path, shape)?; + let has_trailing_comma = crate::expr::span_ends_with_comma(context, self.span); + overflow::rewrite_with_parens( + context, + &path, + list.iter(), + // 1 = "]" + shape.sub_width(1, self.span)?, + self.span, + context.config.attr_fn_like_width(), + Some(if has_trailing_comma { + SeparatorTactic::Always + } else { + SeparatorTactic::Never + }), + )? + } + MetaItemKind2::NameValue(ref expr) => { + let path = rewrite_path(context, PathContext::Type, &None, &self.path, shape)?; + // 3 = ` = ` + let lit_shape = shape.shrink_left(path.len() + 3, self.span)?; + let value = match expr.kind { + ast::ExprKind::Lit(ref lit) => { + // `rewrite_literal` returns `None` when `lit` exceeds max + // width. Since a literal is basically unformattable unless it + // is a string literal (and only if `format_strings` is set), + // we might be better off ignoring the fact that the attribute + // is longer than the max width and continue on formatting. + // See #2479 for example. + rewrite_literal(context, *lit, expr.span, lit_shape) + .unwrap_or_else(|_| context.snippet(expr.span).to_owned()) + } + _ => expr.rewrite_result(context, lit_shape)?, + }; + format!("{path} = {value}") + } + }) + } +} + +impl Spanned for MetaItemInner2 { + fn span(&self) -> Span { + match self { + Self::MetaItem(meta) => meta.span, + Self::Lit(lit) => lit.span, + Self::Expr(expr) => expr.span(), + } + } +} + +impl Rewrite for MetaItemInner2 { + fn rewrite(&self, context: &RewriteContext<'_>, shape: Shape) -> Option { + self.rewrite_result(context, shape).ok() + } + + fn rewrite_result(&self, context: &RewriteContext<'_>, shape: Shape) -> RewriteResult { + match self { + Self::MetaItem(ref meta_item) => meta_item.rewrite_result(context, shape), + Self::Lit(ref l) => rewrite_literal(context, l.as_token_lit(), l.span, shape), + Self::Expr(ref expr) => expr.rewrite_result(context, shape), + } + } +} diff --git a/src/overflow.rs b/src/overflow.rs index 19f7b06f8a3..fe21c210e9f 100644 --- a/src/overflow.rs +++ b/src/overflow.rs @@ -27,6 +27,8 @@ use crate::spanned::Spanned; use crate::types::{SegmentParam, can_be_overflowed_type}; use crate::utils::{count_newlines, extra_offset, first_line_width, last_line_width, mk_sp}; +use crate::attr::meta2::MetaItemInner2; + /// A list of `format!`-like macros, that take a long format string and a list of arguments to /// format. /// @@ -79,6 +81,7 @@ pub(crate) enum OverflowableItem<'a> { GenericParam(&'a ast::GenericParam), MacroArg(&'a MacroArg), MetaItemInner(&'a ast::MetaItemInner), + MetaItemInner2(&'a crate::attr::meta2::MetaItemInner2), SegmentParam(&'a SegmentParam<'a>), FieldDef(&'a ast::FieldDef), TuplePatField(&'a TuplePatField<'a>), @@ -124,6 +127,7 @@ impl<'a> OverflowableItem<'a> { OverflowableItem::GenericParam(gp) => f(*gp), OverflowableItem::MacroArg(macro_arg) => f(*macro_arg), OverflowableItem::MetaItemInner(nmi) => f(*nmi), + OverflowableItem::MetaItemInner2(nmi) => f(*nmi), OverflowableItem::SegmentParam(sp) => f(*sp), OverflowableItem::FieldDef(sf) => f(*sf), OverflowableItem::TuplePatField(pat) => f(*pat), @@ -144,6 +148,12 @@ impl<'a> OverflowableItem<'a> { matches!(meta_item.kind, ast::MetaItemKind::Word) } }, + OverflowableItem::MetaItemInner2(meta_item_inner) => match meta_item_inner { + MetaItemInner2::Lit(..) | MetaItemInner2::Expr(..) => true, + MetaItemInner2::MetaItem(ref meta_item) => { + matches!(meta_item.kind, crate::attr::meta2::MetaItemKind2::Word) + } + }, // FIXME: Why don't we consider `SegmentParam` to be simple? // FIXME: If we also fix `SegmentParam`, then we should apply the same // heuristic to `PreciseCapturingArg`. @@ -188,6 +198,12 @@ impl<'a> OverflowableItem<'a> { ast::MetaItemInner::Lit(..) => false, ast::MetaItemInner::MetaItem(..) => true, }, + OverflowableItem::MetaItemInner2(meta_item_inner) if len == 1 => { + match meta_item_inner { + MetaItemInner2::Lit(..) | MetaItemInner2::Expr(..) => false, + MetaItemInner2::MetaItem(..) => true, + } + } OverflowableItem::SegmentParam(SegmentParam::Type(ty)) => { can_be_overflowed_type(context, ty, len) } @@ -201,6 +217,7 @@ impl<'a> OverflowableItem<'a> { let base_cases = match self { OverflowableItem::MacroArg(..) => SPECIAL_CASE_MACROS, OverflowableItem::MetaItemInner(..) => SPECIAL_CASE_ATTR, + OverflowableItem::MetaItemInner2(..) => SPECIAL_CASE_ATTR, _ => &[], }; let additional_cases = match self { @@ -265,7 +282,10 @@ impl_into_overflowable_item_for_ast_node!( Pat, PreciseCapturingArg ); -impl_into_overflowable_item_for_rustfmt_types!([MacroArg], [SegmentParam, TuplePatField]); +impl_into_overflowable_item_for_rustfmt_types!( + [MacroArg, MetaItemInner2], + [SegmentParam, TuplePatField] +); pub(crate) fn into_overflowable_list<'a, T>( iter: impl Iterator, From 2746d3b8597f3027131955056d8ad1e82c59937b Mon Sep 17 00:00:00 2001 From: Max Dexheimer Date: Fri, 5 Sep 2025 12:26:12 +0200 Subject: [PATCH 2/4] Fix tests by formatting attrs --- src/cargo-fmt/main.rs | 7 +------ tests/target/macro_rules.rs | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/cargo-fmt/main.rs b/src/cargo-fmt/main.rs index 9b4adc41a8b..efe608aa207 100644 --- a/src/cargo-fmt/main.rs +++ b/src/cargo-fmt/main.rs @@ -42,12 +42,7 @@ pub struct Opts { version: bool, /// Specify package to format - #[arg( - short = 'p', - long = "package", - value_name = "package", - num_args = 1.. - )] + #[arg(short = 'p', long = "package", value_name = "package", num_args = 1..)] packages: Vec, /// Specify path to Cargo.toml diff --git a/tests/target/macro_rules.rs b/tests/target/macro_rules.rs index 97444aef404..1ccae3a5a18 100644 --- a/tests/target/macro_rules.rs +++ b/tests/target/macro_rules.rs @@ -174,7 +174,7 @@ macro_rules! m [ // #2470 macro foo($type_name:ident, $docs:expr) { #[allow(non_camel_case_types)] - #[doc=$docs] + #[doc = $docs] #[derive(Debug, Clone, Copy)] pub struct $type_name; } From feb82746c388ab7277069b9b1a57502df812ca5b Mon Sep 17 00:00:00 2001 From: Max Dexheimer Date: Fri, 5 Sep 2025 12:28:37 +0200 Subject: [PATCH 3/4] Ensure that long line isn't broken up in long line message test --- tests/cargo-fmt/source/issue_3164/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/cargo-fmt/source/issue_3164/src/main.rs b/tests/cargo-fmt/source/issue_3164/src/main.rs index 9330107ac8d..7f3c2276ff7 100644 --- a/tests/cargo-fmt/source/issue_3164/src/main.rs +++ b/tests/cargo-fmt/source/issue_3164/src/main.rs @@ -3,7 +3,7 @@ macro_rules! foo { ($id:ident) => { macro_rules! bar { ($id2:tt) => { - #[cfg(any(target_feature = $id2, target_feature = $id2, target_feature = $id2, target_feature = $id2, target_feature = $id2))] + #[cfg(any(target_feature = $id2, target_feature = $id2, target_feature = $id2, target_feature = $id2, target_feature = $id2 /**/))] fn $id() {} }; } From e129e9816b0f148a75c5017d4e6429b17537bbc6 Mon Sep 17 00:00:00 2001 From: Max Dexheimer Date: Fri, 5 Sep 2025 14:08:04 +0200 Subject: [PATCH 4/4] Add tests --- tests/source/attrib.rs | 12 ++++++++++++ tests/target/attrib.rs | 25 +++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/tests/source/attrib.rs b/tests/source/attrib.rs index d45fba55224..4a873c43dad 100644 --- a/tests/source/attrib.rs +++ b/tests/source/attrib.rs @@ -232,3 +232,15 @@ fn issue3509() { 1, } } + +// #3781 +enum E { + #[error(display = "invalid max keyframe interval {} (expected > 0, < {})", _0, i32::max_value() as u64)] + InvalidMaxKeyFrameInterval(u64), + #[error(display = "invalid max keyframe interval {} (expected > 0, < {})", _0, i32::max_value())] + InvalidMaxKeyFrameInterval(u64), +} + +// #6374 +#[nutype(validate(len_char_min = 5, len_char_max = 20, regex = EMAIL_REGEX, extra_args = make_sure_this_line_is_split,))] +struct Email(String); diff --git a/tests/target/attrib.rs b/tests/target/attrib.rs index 7e61f68d76a..41dfc0b54e4 100644 --- a/tests/target/attrib.rs +++ b/tests/target/attrib.rs @@ -269,3 +269,28 @@ fn issue3509() { } } } + +// #3781 +enum E { + #[error( + display = "invalid max keyframe interval {} (expected > 0, < {})", + _0, + i32::max_value() as u64 + )] + InvalidMaxKeyFrameInterval(u64), + #[error( + display = "invalid max keyframe interval {} (expected > 0, < {})", + _0, + i32::max_value() + )] + InvalidMaxKeyFrameInterval(u64), +} + +// #6374 +#[nutype(validate( + len_char_min = 5, + len_char_max = 20, + regex = EMAIL_REGEX, + extra_args = make_sure_this_line_is_split, +))] +struct Email(String);