diff --git a/compiler/rustc_expand/src/errors.rs b/compiler/rustc_expand/src/errors.rs index 6c5732f497f8a..d49d2e964f8fe 100644 --- a/compiler/rustc_expand/src/errors.rs +++ b/compiler/rustc_expand/src/errors.rs @@ -413,8 +413,15 @@ 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 `;`", + style = "verbose", + 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..224e2427978a9 100644 --- a/compiler/rustc_expand/src/mbe/quoted.rs +++ b/compiler/rustc_expand/src/mbe/quoted.rs @@ -92,8 +92,9 @@ 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, }); // Fall back to a `TokenTree` since that will match anything if we continue expanding. @@ -144,9 +145,43 @@ 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 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() + { + 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 let Some((semi_span, span, kind)) = typo_recovery { + sess.dcx().emit_err(errors::MissingFragmentSpecifier { + span: start_sp, + add_span: None, + valid: VALID_FRAGMENT_NAMES_MSG, + semi_span: Some(semi_span), + }); + result.push(TokenTree::MetaVarDecl { span, name: ident, kind }); + } 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..a6badc6147182 --- /dev/null +++ b/tests/ui/macros/macro-semicolon-for-colon.rs @@ -0,0 +1,8 @@ +// Ensure that using `;` instead of `:` in a macro fragment specifier +// produces a helpful suggestion. + +macro_rules! m { + ($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 new file mode 100644 index 0000000000000..694c378e14806 --- /dev/null +++ b/tests/ui/macros/macro-semicolon-for-colon.stderr @@ -0,0 +1,16 @@ +error: missing fragment specifier + --> $DIR/macro-semicolon-for-colon.rs:5:6 + | +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: use `:` instead of `;` + | +LL - ($x;ty) => {}; +LL + ($x:ty) => {}; + | + +error: aborting due to 1 previous error +