From c3118bb2b94c8fd7dd4795704aae8bd2d72a82e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ozan=20Kenan=20G=C3=BCng=C3=B6r?= Date: Sat, 14 Mar 2026 07:01:42 +0300 Subject: [PATCH 1/3] rustc_expand: suggest `:` for malformed macro matcher fragment --- compiler/rustc_expand/src/errors.rs | 2 ++ compiler/rustc_expand/src/mbe/quoted.rs | 33 +++++++++++++++++-- tests/ui/macros/macro-semicolon-for-colon.rs | 9 +++++ .../macros/macro-semicolon-for-colon.stderr | 19 +++++++++++ 4 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 tests/ui/macros/macro-semicolon-for-colon.rs create mode 100644 tests/ui/macros/macro-semicolon-for-colon.stderr diff --git a/compiler/rustc_expand/src/errors.rs b/compiler/rustc_expand/src/errors.rs index 6c5732f497f8a..499c532701702 100644 --- a/compiler/rustc_expand/src/errors.rs +++ b/compiler/rustc_expand/src/errors.rs @@ -415,6 +415,8 @@ pub(crate) struct MissingFragmentSpecifier { )] pub add_span: Span, pub valid: &'static str, + #[suggestion("use `:` instead of `;`", code = ":", applicability = "maybe-incorrect")] + pub semi_span: Option, } #[derive(Diagnostic)] diff --git a/compiler/rustc_expand/src/mbe/quoted.rs b/compiler/rustc_expand/src/mbe/quoted.rs index eb874a27cece5..be16148647528 100644 --- a/compiler/rustc_expand/src/mbe/quoted.rs +++ b/compiler/rustc_expand/src/mbe/quoted.rs @@ -94,6 +94,7 @@ fn parse( span, add_span: span.shrink_to_hi(), valid: VALID_FRAGMENT_NAMES_MSG, + semi_span: None, }); // Fall back to a `TokenTree` since that will match anything if we continue expanding. @@ -144,9 +145,35 @@ fn parse( }); result.push(TokenTree::MetaVarDecl { span, name: ident, kind }); } else { - // Whether it's none or some other tree, it doesn't belong to - // the current meta variable, returning the original span. - missing_fragment_specifier(start_sp); + // Check for typo: `;` instead of `:` before a valid fragment specifier. + let semi_span = { + let mut clone = iter.clone(); + if let Some(tokenstream::TokenTree::Token(semi_token, _)) = clone.next() + && semi_token.kind == token::Semi + && let Some(tokenstream::TokenTree::Token(frag_token, _)) = clone.next() + && let Some((fragment, _)) = frag_token.ident() + && NonterminalKind::from_symbol(fragment.name, || edition).is_some() + { + Some(semi_token.span) + } else { + None + } + }; + if semi_span.is_some() { + sess.dcx().emit_err(errors::MissingFragmentSpecifier { + span: start_sp, + add_span: start_sp.shrink_to_hi(), + valid: VALID_FRAGMENT_NAMES_MSG, + semi_span, + }); + result.push(TokenTree::MetaVarDecl { + span: start_sp, + name: ident, + kind: NonterminalKind::TT, + }); + } else { + missing_fragment_specifier(start_sp); + } } } result diff --git a/tests/ui/macros/macro-semicolon-for-colon.rs b/tests/ui/macros/macro-semicolon-for-colon.rs new file mode 100644 index 0000000000000..f5d8f3eb62247 --- /dev/null +++ b/tests/ui/macros/macro-semicolon-for-colon.rs @@ -0,0 +1,9 @@ +// Ensure that using `;` instead of `:` in a macro fragment specifier +// produces a helpful suggestion. + +macro_rules! m { + ($x;tt) => {}; + //~^ ERROR missing fragment specifier +} + +fn main() {} diff --git a/tests/ui/macros/macro-semicolon-for-colon.stderr b/tests/ui/macros/macro-semicolon-for-colon.stderr new file mode 100644 index 0000000000000..49309af4dd95a --- /dev/null +++ b/tests/ui/macros/macro-semicolon-for-colon.stderr @@ -0,0 +1,19 @@ +error: missing fragment specifier + --> $DIR/macro-semicolon-for-colon.rs:5:6 + | +LL | ($x;tt) => {}; + | ^^ + | + = note: fragment specifiers must be provided + = help: valid fragment specifiers are `ident`, `block`, `stmt`, `expr`, `pat`, `ty`, `lifetime`, `literal`, `path`, `meta`, `tt`, `item` and `vis`, along with `expr_2021` and `pat_param` for edition compatibility +help: try adding a specifier here + | +LL | ($x:spec;tt) => {}; + | +++++ +help: use `:` instead of `;` + | +LL - ($x;tt) => {}; +LL + ($x:tt) => {}; + | + +error: aborting due to 1 previous error From 6c1bf681c75bab72c22ca1cff45e451395630f4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ozan=20Kenan=20G=C3=BCng=C3=B6r?= Date: Sat, 14 Mar 2026 09:05:40 +0300 Subject: [PATCH 2/3] tests: update blessed output for macro fragment suggestion --- tests/ui/macros/macro-semicolon-for-colon.stderr | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/ui/macros/macro-semicolon-for-colon.stderr b/tests/ui/macros/macro-semicolon-for-colon.stderr index 49309af4dd95a..038c0a6121bce 100644 --- a/tests/ui/macros/macro-semicolon-for-colon.stderr +++ b/tests/ui/macros/macro-semicolon-for-colon.stderr @@ -17,3 +17,4 @@ LL + ($x:tt) => {}; | error: aborting due to 1 previous error + From 2fd6acdbddba337bf53f3397e8f24560083c4b1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ozan=20Kenan=20G=C3=BCng=C3=B6r?= Date: Mon, 16 Mar 2026 23:31:49 +0300 Subject: [PATCH 3/3] rustc_expand: refine recovery for fragment typo suggestion --- compiler/rustc_expand/src/errors.rs | 9 ++++-- compiler/rustc_expand/src/mbe/quoted.rs | 32 ++++++++++++------- tests/ui/macros/macro-semicolon-for-colon.rs | 3 +- .../macros/macro-semicolon-for-colon.stderr | 10 ++---- 4 files changed, 31 insertions(+), 23 deletions(-) diff --git a/compiler/rustc_expand/src/errors.rs b/compiler/rustc_expand/src/errors.rs index 499c532701702..d49d2e964f8fe 100644 --- a/compiler/rustc_expand/src/errors.rs +++ b/compiler/rustc_expand/src/errors.rs @@ -413,9 +413,14 @@ pub(crate) struct MissingFragmentSpecifier { code = ":spec", applicability = "maybe-incorrect" )] - pub add_span: Span, + pub add_span: Option, pub valid: &'static str, - #[suggestion("use `:` instead of `;`", code = ":", applicability = "maybe-incorrect")] + #[suggestion( + "use `:` instead of `;`", + style = "verbose", + code = ":", + applicability = "maybe-incorrect" + )] pub semi_span: Option, } diff --git a/compiler/rustc_expand/src/mbe/quoted.rs b/compiler/rustc_expand/src/mbe/quoted.rs index be16148647528..224e2427978a9 100644 --- a/compiler/rustc_expand/src/mbe/quoted.rs +++ b/compiler/rustc_expand/src/mbe/quoted.rs @@ -92,7 +92,7 @@ fn parse( let mut missing_fragment_specifier = |span| { sess.dcx().emit_err(errors::MissingFragmentSpecifier { span, - add_span: span.shrink_to_hi(), + add_span: Some(span.shrink_to_hi()), valid: VALID_FRAGMENT_NAMES_MSG, semi_span: None, }); @@ -146,31 +146,39 @@ fn parse( result.push(TokenTree::MetaVarDecl { span, name: ident, kind }); } else { // Check for typo: `;` instead of `:` before a valid fragment specifier. - let semi_span = { + let typo_recovery = { let mut clone = iter.clone(); if let Some(tokenstream::TokenTree::Token(semi_token, _)) = clone.next() && semi_token.kind == token::Semi && let Some(tokenstream::TokenTree::Token(frag_token, _)) = clone.next() && let Some((fragment, _)) = frag_token.ident() - && NonterminalKind::from_symbol(fragment.name, || edition).is_some() { - Some(semi_token.span) + let span = frag_token.span.with_lo(start_sp.lo()); + let edition = || { + // FIXME(#85708) - once we properly decode a foreign + // crate's `SyntaxContext::root`, then we can replace + // this with just `span.edition()`. A + // `SyntaxContext::root()` from the current crate will + // have the edition of the current crate, and a + // `SyntaxContext::root()` from a foreign crate will + // have the edition of that crate (which we manually + // retrieve via the `edition` parameter). + if !span.from_expansion() { edition } else { span.edition() } + }; + NonterminalKind::from_symbol(fragment.name, edition) + .map(|kind| (semi_token.span, span, kind)) } else { None } }; - if semi_span.is_some() { + if let Some((semi_span, span, kind)) = typo_recovery { sess.dcx().emit_err(errors::MissingFragmentSpecifier { span: start_sp, - add_span: start_sp.shrink_to_hi(), + add_span: None, valid: VALID_FRAGMENT_NAMES_MSG, - semi_span, - }); - result.push(TokenTree::MetaVarDecl { - span: start_sp, - name: ident, - kind: NonterminalKind::TT, + semi_span: Some(semi_span), }); + result.push(TokenTree::MetaVarDecl { span, name: ident, kind }); } else { missing_fragment_specifier(start_sp); } diff --git a/tests/ui/macros/macro-semicolon-for-colon.rs b/tests/ui/macros/macro-semicolon-for-colon.rs index f5d8f3eb62247..a6badc6147182 100644 --- a/tests/ui/macros/macro-semicolon-for-colon.rs +++ b/tests/ui/macros/macro-semicolon-for-colon.rs @@ -2,8 +2,7 @@ // produces a helpful suggestion. macro_rules! m { - ($x;tt) => {}; - //~^ ERROR missing fragment specifier + ($x;ty) => {}; //~ ERROR missing fragment specifier } fn main() {} diff --git a/tests/ui/macros/macro-semicolon-for-colon.stderr b/tests/ui/macros/macro-semicolon-for-colon.stderr index 038c0a6121bce..694c378e14806 100644 --- a/tests/ui/macros/macro-semicolon-for-colon.stderr +++ b/tests/ui/macros/macro-semicolon-for-colon.stderr @@ -1,19 +1,15 @@ error: missing fragment specifier --> $DIR/macro-semicolon-for-colon.rs:5:6 | -LL | ($x;tt) => {}; +LL | ($x;ty) => {}; | ^^ | = note: fragment specifiers must be provided = help: valid fragment specifiers are `ident`, `block`, `stmt`, `expr`, `pat`, `ty`, `lifetime`, `literal`, `path`, `meta`, `tt`, `item` and `vis`, along with `expr_2021` and `pat_param` for edition compatibility -help: try adding a specifier here - | -LL | ($x:spec;tt) => {}; - | +++++ help: use `:` instead of `;` | -LL - ($x;tt) => {}; -LL + ($x:tt) => {}; +LL - ($x;ty) => {}; +LL + ($x:ty) => {}; | error: aborting due to 1 previous error