From 1bfa30d03270f5eb4e7822df8a3874cacc40676d Mon Sep 17 00:00:00 2001 From: Usman Akinyemi Date: Mon, 16 Feb 2026 01:43:17 +0530 Subject: [PATCH] rustc_expand: improve diagnostics for non-repeatable metavars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Esteban Küber Signed-off-by: Usman Akinyemi --- compiler/rustc_expand/src/base.rs | 3 +- compiler/rustc_expand/src/mbe/diagnostics.rs | 69 ++++++++++++++++++- compiler/rustc_expand/src/mbe/macro_rules.rs | 39 +++++++++-- tests/ui/macros/issue-6596-1.stderr | 6 +- tests/ui/macros/typo-in-norepeat-expr-2.rs | 42 +++++++++++ .../ui/macros/typo-in-norepeat-expr-2.stderr | 46 +++++++++++++ tests/ui/macros/typo-in-norepeat-expr.fixed | 12 ++++ tests/ui/macros/typo-in-norepeat-expr.rs | 12 ++++ tests/ui/macros/typo-in-norepeat-expr.stderr | 18 +++++ 9 files changed, 239 insertions(+), 8 deletions(-) create mode 100644 tests/ui/macros/typo-in-norepeat-expr-2.rs create mode 100644 tests/ui/macros/typo-in-norepeat-expr-2.stderr create mode 100644 tests/ui/macros/typo-in-norepeat-expr.fixed create mode 100644 tests/ui/macros/typo-in-norepeat-expr.rs create mode 100644 tests/ui/macros/typo-in-norepeat-expr.stderr diff --git a/compiler/rustc_expand/src/base.rs b/compiler/rustc_expand/src/base.rs index 225906dfba2de..0014521e2754d 100644 --- a/compiler/rustc_expand/src/base.rs +++ b/compiler/rustc_expand/src/base.rs @@ -277,7 +277,8 @@ impl<'cx> MacroExpanderResult<'cx> { // Emit the SEMICOLON_IN_EXPRESSIONS_FROM_MACROS deprecation lint. let is_local = true; - let parser = ParserAnyMacro::from_tts(cx, tts, site_span, arm_span, is_local, macro_ident); + let parser = + ParserAnyMacro::from_tts(cx, tts, site_span, arm_span, is_local, macro_ident, &[], &[]); ExpandResult::Ready(Box::new(parser)) } } diff --git a/compiler/rustc_expand/src/mbe/diagnostics.rs b/compiler/rustc_expand/src/mbe/diagnostics.rs index df6903dc4937f..45fc891ce8e84 100644 --- a/compiler/rustc_expand/src/mbe/diagnostics.rs +++ b/compiler/rustc_expand/src/mbe/diagnostics.rs @@ -220,13 +220,15 @@ impl<'dcx> CollectTrackerAndEmitter<'dcx, '_> { } } -pub(super) fn emit_frag_parse_err( +pub(super) fn emit_frag_parse_err<'a>( mut e: Diag<'_>, - parser: &Parser<'_>, + parser: &mut Parser<'_>, orig_parser: &mut Parser<'_>, site_span: Span, arm_span: Span, kind: AstFragmentKind, + bindings: &'a [MacroRule], + matched_rule_bindings: &'a [MatcherLoc], ) -> ErrorGuaranteed { // FIXME(davidtwco): avoid depending on the error message text if parser.token == token::Eof @@ -285,6 +287,69 @@ pub(super) fn emit_frag_parse_err( }, _ => annotate_err_with_kind(&mut e, kind, site_span), }; + + let mut bindings_rules = vec![]; + for rule in bindings { + let MacroRule::Func { lhs, .. } = rule else { continue }; + for param in lhs { + let MatcherLoc::MetaVarDecl { bind, .. } = param else { continue }; + bindings_rules.push(*bind); + } + } + + let mut matched_rule_bindings_rules = vec![]; + for param in matched_rule_bindings { + let MatcherLoc::MetaVarDecl { bind, .. } = param else { continue }; + matched_rule_bindings_rules.push(*bind); + } + + let matched_rule_bindings_names: Vec<_> = + matched_rule_bindings_rules.iter().map(|bind| bind.name).collect(); + let bindings_name: Vec<_> = bindings_rules.iter().map(|bind| bind.name).collect(); + if parser.token.kind == token::Dollar { + parser.bump(); + if let token::Ident(name, _) = parser.token.kind { + if let Some(matched_name) = rustc_span::edit_distance::find_best_match_for_name( + &matched_rule_bindings_names[..], + name, + None, + ) { + e.span_suggestion_verbose( + parser.token.span, + "there is a macro metavariable with similar name", + format!("{matched_name}"), + Applicability::MaybeIncorrect, + ); + } else if bindings_name.contains(&name) { + e.span_label( + parser.token.span, + format!( + "there is an macro metavariable with this name in another macro matcher" + ), + ); + } else if let Some(matched_name) = + rustc_span::edit_distance::find_best_match_for_name(&bindings_name[..], name, None) + { + e.span_suggestion_verbose( + parser.token.span, + "there is a macro metavariable with a similar name in another macro matcher", + format!("{matched_name}"), + Applicability::MaybeIncorrect, + ); + } else { + let msg = matched_rule_bindings_names + .iter() + .map(|sym| format!("${}", sym)) + .collect::>() + .join(", "); + + e.span_label(parser.token.span, format!("macro metavariable not found")); + if !matched_rule_bindings_names.is_empty() { + e.note(format!("available metavariable names are: {msg}")); + } + } + } + } e.emit() } diff --git a/compiler/rustc_expand/src/mbe/macro_rules.rs b/compiler/rustc_expand/src/mbe/macro_rules.rs index 7ff49e040f6f3..1f9f1262a2696 100644 --- a/compiler/rustc_expand/src/mbe/macro_rules.rs +++ b/compiler/rustc_expand/src/mbe/macro_rules.rs @@ -55,6 +55,8 @@ pub(crate) struct ParserAnyMacro<'a> { arm_span: Span, /// Whether or not this macro is defined in the current crate is_local: bool, + bindings: &'a [MacroRule], + matched_rule_bindings: &'a [MatcherLoc], } impl<'a> ParserAnyMacro<'a> { @@ -67,13 +69,22 @@ impl<'a> ParserAnyMacro<'a> { arm_span, is_trailing_mac, is_local, + bindings, + matched_rule_bindings, } = *self; let snapshot = &mut parser.create_snapshot_for_diagnostic(); let fragment = match parse_ast_fragment(parser, kind) { Ok(f) => f, Err(err) => { let guar = diagnostics::emit_frag_parse_err( - err, parser, snapshot, site_span, arm_span, kind, + err, + parser, + snapshot, + site_span, + arm_span, + kind, + bindings, + matched_rule_bindings, ); return kind.dummy(site_span, guar); } @@ -100,7 +111,7 @@ impl<'a> ParserAnyMacro<'a> { fragment } - #[instrument(skip(cx, tts))] + #[instrument(skip(cx, tts, bindings, matched_rule_bindings))] pub(crate) fn from_tts<'cx>( cx: &'cx mut ExtCtxt<'a>, tts: TokenStream, @@ -108,6 +119,9 @@ impl<'a> ParserAnyMacro<'a> { arm_span: Span, is_local: bool, macro_ident: Ident, + // bindings and lhs is for diagnostics + bindings: &'a [MacroRule], + matched_rule_bindings: &'a [MatcherLoc], ) -> Self { Self { parser: Parser::new(&cx.sess.psess, tts, None), @@ -121,6 +135,8 @@ impl<'a> ParserAnyMacro<'a> { is_trailing_mac: cx.current_expansion.is_trailing_mac, arm_span, is_local, + bindings, + matched_rule_bindings, } } } @@ -359,7 +375,7 @@ fn expand_macro<'cx>( match try_success_result { Ok((rule_index, rule, named_matches)) => { - let MacroRule::Func { rhs, .. } = rule else { + let MacroRule::Func { lhs, rhs, .. } = rule else { panic!("try_match_macro returned non-func rule"); }; let mbe::TokenTree::Delimited(rhs_span, _, rhs) = rhs else { @@ -387,8 +403,23 @@ fn expand_macro<'cx>( cx.resolver.record_macro_rule_usage(node_id, rule_index); } + // let mut bindings = vec![]; + // for rule in rules { + // let MacroRule::Func { lhs, .. } = rule else { continue }; + // for param in lhs { + // let MatcherLoc::MetaVarDecl { bind, .. } = param else { continue }; + // bindings.push(*bind); + // } + // } + // + // let mut matched_rule_bindings = vec![]; + // for param in lhs { + // let MatcherLoc::MetaVarDecl { bind, .. } = param else { continue }; + // matched_rule_bindings.push(*bind); + // } + // Let the context choose how to interpret the result. Weird, but useful for X-macros. - Box::new(ParserAnyMacro::from_tts(cx, tts, sp, arm_span, is_local, name)) + Box::new(ParserAnyMacro::from_tts(cx, tts, sp, arm_span, is_local, name, rules, lhs)) } Err(CanRetry::No(guar)) => { debug!("Will not retry matching as an error was emitted already"); diff --git a/tests/ui/macros/issue-6596-1.stderr b/tests/ui/macros/issue-6596-1.stderr index f20d67329dbe7..cb66dcc6ea890 100644 --- a/tests/ui/macros/issue-6596-1.stderr +++ b/tests/ui/macros/issue-6596-1.stderr @@ -2,11 +2,15 @@ error: expected expression, found `$` --> $DIR/issue-6596-1.rs:3:9 | LL | $nonexistent - | ^^^^^^^^^^^^ expected expression + | ^----------- + | || + | |macro metavariable not found + | expected expression ... LL | e!(foo); | ------- in this macro invocation | + = note: available metavariable names are: $inp = note: this error originates in the macro `e` (in Nightly builds, run with -Z macro-backtrace for more info) error: aborting due to 1 previous error diff --git a/tests/ui/macros/typo-in-norepeat-expr-2.rs b/tests/ui/macros/typo-in-norepeat-expr-2.rs new file mode 100644 index 0000000000000..48b64c248d637 --- /dev/null +++ b/tests/ui/macros/typo-in-norepeat-expr-2.rs @@ -0,0 +1,42 @@ +macro_rules! err { + (begin $follow:ident end $arg:expr) => { + [$arg] + }; + (begin1 $arg1:ident end $agr2:expr) => { + [$follow] //~ ERROR: expected expression, found `$` + //~^ NOTE: there is an macro metavariable with this name in another macro matcher + //~| NOTE: expected expression + }; +} + +macro_rules! err1 { + (begin $follow:ident end $arg:expr) => { + [$arg] + }; + (begin1 $arg1:ident end) => { + [$follo] //~ ERROR: expected expression, found `$` + //~| NOTE: expected expression + //~| HELP: there is a macro metavariable with a similar name in another macro matcher + }; +} + +macro_rules! err2 { + (begin $follow:ident end $arg:expr) => { + [$arg] + }; + (begin1 $arg1:ident end) => { + [$xyz] //~ ERROR: expected expression, found `$` + //~^ NOTE: expected expression + //~| NOTE available metavariable names are: $arg1 + //~| NOTE: macro metavariable not found + }; +} + +fn main () { + let _ = err![begin1 x end ig]; //~ NOTE: in this expansion of err! + let _ = err1![begin1 x end]; //~ NOTE: in this expansion of err1! + //~| NOTE: in this expansion of err1! + + let _ = err2![begin1 x end]; //~ NOTE: in this expansion of err2! + //~| NOTE in this expansion of err2! +} diff --git a/tests/ui/macros/typo-in-norepeat-expr-2.stderr b/tests/ui/macros/typo-in-norepeat-expr-2.stderr new file mode 100644 index 0000000000000..20390cccc9a74 --- /dev/null +++ b/tests/ui/macros/typo-in-norepeat-expr-2.stderr @@ -0,0 +1,46 @@ +error: expected expression, found `$` + --> $DIR/typo-in-norepeat-expr-2.rs:6:10 + | +LL | [$follow] + | ^------ + | || + | |there is an macro metavariable with this name in another macro matcher + | expected expression +... +LL | let _ = err![begin1 x end ig]; + | ---------------------- in this macro invocation + | + = note: this error originates in the macro `err` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: expected expression, found `$` + --> $DIR/typo-in-norepeat-expr-2.rs:17:10 + | +LL | [$follo] + | ^^^^^^ expected expression +... +LL | let _ = err1![begin1 x end]; + | -------------------- in this macro invocation + | + = note: this error originates in the macro `err1` (in Nightly builds, run with -Z macro-backtrace for more info) +help: there is a macro metavariable with a similar name in another macro matcher + | +LL | [$follow] + | + + +error: expected expression, found `$` + --> $DIR/typo-in-norepeat-expr-2.rs:28:10 + | +LL | [$xyz] + | ^--- + | || + | |macro metavariable not found + | expected expression +... +LL | let _ = err2![begin1 x end]; + | -------------------- in this macro invocation + | + = note: available metavariable names are: $arg1 + = note: this error originates in the macro `err2` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: aborting due to 3 previous errors + diff --git a/tests/ui/macros/typo-in-norepeat-expr.fixed b/tests/ui/macros/typo-in-norepeat-expr.fixed new file mode 100644 index 0000000000000..a59f461e63120 --- /dev/null +++ b/tests/ui/macros/typo-in-norepeat-expr.fixed @@ -0,0 +1,12 @@ +//@ run-rustfix +macro_rules! m { + (begin $ard:ident end) => { + [$ard] //~ ERROR: expected expression, found `$` + //~^ HELP: there is a macro metavariable with similar name + }; +} + +fn main() { + let x = 1; + let _ = m![begin x end]; +} diff --git a/tests/ui/macros/typo-in-norepeat-expr.rs b/tests/ui/macros/typo-in-norepeat-expr.rs new file mode 100644 index 0000000000000..fe554f07e7552 --- /dev/null +++ b/tests/ui/macros/typo-in-norepeat-expr.rs @@ -0,0 +1,12 @@ +//@ run-rustfix +macro_rules! m { + (begin $ard:ident end) => { + [$arg] //~ ERROR: expected expression, found `$` + //~^ HELP: there is a macro metavariable with similar name + }; +} + +fn main() { + let x = 1; + let _ = m![begin x end]; +} diff --git a/tests/ui/macros/typo-in-norepeat-expr.stderr b/tests/ui/macros/typo-in-norepeat-expr.stderr new file mode 100644 index 0000000000000..8f37957ea98e0 --- /dev/null +++ b/tests/ui/macros/typo-in-norepeat-expr.stderr @@ -0,0 +1,18 @@ +error: expected expression, found `$` + --> $DIR/typo-in-norepeat-expr.rs:4:10 + | +LL | [$arg] + | ^^^^ expected expression +... +LL | let _ = m![begin x end]; + | --------------- in this macro invocation + | + = note: this error originates in the macro `m` (in Nightly builds, run with -Z macro-backtrace for more info) +help: there is a macro metavariable with similar name + | +LL - [$arg] +LL + [$ard] + | + +error: aborting due to 1 previous error +