about summary refs log tree commit diff
diff options
context:
space:
mode:
authorTrevor Gross <t.gross35@gmail.com>2025-07-10 03:23:53 -0400
committerGitHub <noreply@github.com>2025-07-10 03:23:53 -0400
commit643efdaa823b21e0423c6614f74a8b014c67520b (patch)
tree4f6158fd60dcfc3957916b2b425aedf35a7b729e
parent73d3adc67b700ab3a41c72791f0ca5cbaef51425 (diff)
parent87e981996fe47826a8f3a69e3d3cc73ec68dbf8d (diff)
downloadrust-643efdaa823b21e0423c6614f74a8b014c67520b.tar.gz
rust-643efdaa823b21e0423c6614f74a8b014c67520b.zip
Rollup merge of #142950 - tgross35:metavariable-expr-rework, r=petrochenkov
mbe: Rework diagnostics for metavariable expressions

Make the diagnostics for metavariable expressions more user-friendly. This mostly addresses syntactic errors; I will be following up with improvements to `concat(..)`.
-rw-r--r--compiler/rustc_expand/messages.ftl26
-rw-r--r--compiler/rustc_expand/src/errors.rs44
-rw-r--r--compiler/rustc_expand/src/mbe/metavar_expr.rs98
-rw-r--r--tests/ui/macros/metavar-expressions/syntax-errors.rs33
-rw-r--r--tests/ui/macros/metavar-expressions/syntax-errors.stderr192
5 files changed, 259 insertions, 134 deletions
diff --git a/compiler/rustc_expand/messages.ftl b/compiler/rustc_expand/messages.ftl
index 3fc0fa06191..89d6e62834d 100644
--- a/compiler/rustc_expand/messages.ftl
+++ b/compiler/rustc_expand/messages.ftl
@@ -133,6 +133,32 @@ expand_module_multiple_candidates =
 expand_must_repeat_once =
     this must repeat at least once
 
+expand_mve_extra_tokens =
+    unexpected trailing tokens
+    .label = for this metavariable expression
+    .range = the `{$name}` metavariable expression takes between {$min_or_exact_args} and {$max_args} arguments
+    .exact = the `{$name}` metavariable expression takes {$min_or_exact_args ->
+        [zero] no arguments
+        [one] a single argument
+        *[other] {$min_or_exact_args} arguments
+    }
+    .suggestion = try removing {$extra_count ->
+        [one] this token
+        *[other] these tokens
+    }
+
+expand_mve_missing_paren =
+    expected `(`
+    .label = for this this metavariable expression
+    .unexpected = unexpected token
+    .note = metavariable expressions use function-like parentheses syntax
+    .suggestion = try adding parentheses
+
+expand_mve_unrecognized_expr =
+    unrecognized metavariable expression
+    .label = not a valid metavariable expression
+    .note = valid metavariable expressions are {$valid_expr_list}
+
 expand_mve_unrecognized_var =
     variable `{$key}` is not recognized in meta-variable expression
 
diff --git a/compiler/rustc_expand/src/errors.rs b/compiler/rustc_expand/src/errors.rs
index fdbc65aff68..3ac5d213053 100644
--- a/compiler/rustc_expand/src/errors.rs
+++ b/compiler/rustc_expand/src/errors.rs
@@ -496,6 +496,50 @@ pub(crate) use metavar_exprs::*;
 mod metavar_exprs {
     use super::*;
 
+    #[derive(Diagnostic, Default)]
+    #[diag(expand_mve_extra_tokens)]
+    pub(crate) struct MveExtraTokens {
+        #[primary_span]
+        #[suggestion(code = "", applicability = "machine-applicable")]
+        pub span: Span,
+        #[label]
+        pub ident_span: Span,
+        pub extra_count: usize,
+
+        // The rest is only used for specific diagnostics and can be default if neither
+        // `note` is `Some`.
+        #[note(expand_exact)]
+        pub exact_args_note: Option<()>,
+        #[note(expand_range)]
+        pub range_args_note: Option<()>,
+        pub min_or_exact_args: usize,
+        pub max_args: usize,
+        pub name: String,
+    }
+
+    #[derive(Diagnostic)]
+    #[note]
+    #[diag(expand_mve_missing_paren)]
+    pub(crate) struct MveMissingParen {
+        #[primary_span]
+        #[label]
+        pub ident_span: Span,
+        #[label(expand_unexpected)]
+        pub unexpected_span: Option<Span>,
+        #[suggestion(code = "( /* ... */ )", applicability = "has-placeholders")]
+        pub insert_span: Option<Span>,
+    }
+
+    #[derive(Diagnostic)]
+    #[note]
+    #[diag(expand_mve_unrecognized_expr)]
+    pub(crate) struct MveUnrecognizedExpr {
+        #[primary_span]
+        #[label]
+        pub span: Span,
+        pub valid_expr_list: &'static str,
+    }
+
     #[derive(Diagnostic)]
     #[diag(expand_mve_unrecognized_var)]
     pub(crate) struct MveUnrecognizedVar {
diff --git a/compiler/rustc_expand/src/mbe/metavar_expr.rs b/compiler/rustc_expand/src/mbe/metavar_expr.rs
index ffd3548019a..d2b275ad20a 100644
--- a/compiler/rustc_expand/src/mbe/metavar_expr.rs
+++ b/compiler/rustc_expand/src/mbe/metavar_expr.rs
@@ -7,6 +7,8 @@ use rustc_macros::{Decodable, Encodable};
 use rustc_session::parse::ParseSess;
 use rustc_span::{Ident, Span, Symbol};
 
+use crate::errors;
+
 pub(crate) const RAW_IDENT_ERR: &str = "`${concat(..)}` currently does not support raw identifiers";
 pub(crate) const UNSUPPORTED_CONCAT_ELEM_ERR: &str = "expected identifier or string literal";
 
@@ -40,11 +42,32 @@ impl MetaVarExpr {
     ) -> PResult<'psess, MetaVarExpr> {
         let mut iter = input.iter();
         let ident = parse_ident(&mut iter, psess, outer_span)?;
-        let Some(TokenTree::Delimited(.., Delimiter::Parenthesis, args)) = iter.next() else {
-            let msg = "meta-variable expression parameter must be wrapped in parentheses";
-            return Err(psess.dcx().struct_span_err(ident.span, msg));
+        let next = iter.next();
+        let Some(TokenTree::Delimited(.., Delimiter::Parenthesis, args)) = next else {
+            // No `()`; wrong or no delimiters. Point at a problematic span or a place to
+            // add parens if it makes sense.
+            let (unexpected_span, insert_span) = match next {
+                Some(TokenTree::Delimited(..)) => (None, None),
+                Some(tt) => (Some(tt.span()), None),
+                None => (None, Some(ident.span.shrink_to_hi())),
+            };
+            let err =
+                errors::MveMissingParen { ident_span: ident.span, unexpected_span, insert_span };
+            return Err(psess.dcx().create_err(err));
         };
-        check_trailing_token(&mut iter, psess)?;
+
+        // Ensure there are no trailing tokens in the braces, e.g. `${foo() extra}`
+        if iter.peek().is_some() {
+            let span = iter_span(&iter).expect("checked is_some above");
+            let err = errors::MveExtraTokens {
+                span,
+                ident_span: ident.span,
+                extra_count: iter.count(),
+                ..Default::default()
+            };
+            return Err(psess.dcx().create_err(err));
+        }
+
         let mut iter = args.iter();
         let rslt = match ident.as_str() {
             "concat" => parse_concat(&mut iter, psess, outer_span, ident.span)?,
@@ -56,18 +79,14 @@ impl MetaVarExpr {
             "index" => MetaVarExpr::Index(parse_depth(&mut iter, psess, ident.span)?),
             "len" => MetaVarExpr::Len(parse_depth(&mut iter, psess, ident.span)?),
             _ => {
-                let err_msg = "unrecognized meta-variable expression";
-                let mut err = psess.dcx().struct_span_err(ident.span, err_msg);
-                err.span_suggestion(
-                    ident.span,
-                    "supported expressions are count, ignore, index and len",
-                    "",
-                    Applicability::MachineApplicable,
-                );
-                return Err(err);
+                let err = errors::MveUnrecognizedExpr {
+                    span: ident.span,
+                    valid_expr_list: "`count`, `ignore`, `index`, `len`, and `concat`",
+                };
+                return Err(psess.dcx().create_err(err));
             }
         };
-        check_trailing_token(&mut iter, psess)?;
+        check_trailing_tokens(&mut iter, psess, ident)?;
         Ok(rslt)
     }
 
@@ -87,20 +106,51 @@ impl MetaVarExpr {
     }
 }
 
-// Checks if there are any remaining tokens. For example, `${ignore(ident ... a b c ...)}`
-fn check_trailing_token<'psess>(
+/// Checks if there are any remaining tokens (for example, `${ignore($valid, extra)}`) and create
+/// a diag with the correct arg count if so.
+fn check_trailing_tokens<'psess>(
     iter: &mut TokenStreamIter<'_>,
     psess: &'psess ParseSess,
+    ident: Ident,
 ) -> PResult<'psess, ()> {
-    if let Some(tt) = iter.next() {
-        let mut diag = psess
-            .dcx()
-            .struct_span_err(tt.span(), format!("unexpected token: {}", pprust::tt_to_string(tt)));
-        diag.span_note(tt.span(), "meta-variable expression must not have trailing tokens");
-        Err(diag)
-    } else {
-        Ok(())
+    if iter.peek().is_none() {
+        // All tokens consumed, as expected
+        return Ok(());
     }
+
+    // `None` for max indicates the arg count must be exact, `Some` indicates a range is accepted.
+    let (min_or_exact_args, max_args) = match ident.as_str() {
+        "concat" => panic!("concat takes unlimited tokens but didn't eat them all"),
+        "ignore" => (1, None),
+        // 1 or 2 args
+        "count" => (1, Some(2)),
+        // 0 or 1 arg
+        "index" => (0, Some(1)),
+        "len" => (0, Some(1)),
+        other => unreachable!("unknown MVEs should be rejected earlier (got `{other}`)"),
+    };
+
+    let err = errors::MveExtraTokens {
+        span: iter_span(iter).expect("checked is_none above"),
+        ident_span: ident.span,
+        extra_count: iter.count(),
+
+        exact_args_note: if max_args.is_some() { None } else { Some(()) },
+        range_args_note: if max_args.is_some() { Some(()) } else { None },
+        min_or_exact_args,
+        max_args: max_args.unwrap_or_default(),
+        name: ident.to_string(),
+    };
+    Err(psess.dcx().create_err(err))
+}
+
+/// Returns a span encompassing all tokens in the iterator if there is at least one item.
+fn iter_span(iter: &TokenStreamIter<'_>) -> Option<Span> {
+    let mut iter = iter.clone(); // cloning is cheap
+    let first_sp = iter.next()?.span();
+    let last_sp = iter.last().map(TokenTree::span).unwrap_or(first_sp);
+    let span = first_sp.with_hi(last_sp.hi());
+    Some(span)
 }
 
 /// Indicates what is placed in a `concat` parameter. For example, literals
diff --git a/tests/ui/macros/metavar-expressions/syntax-errors.rs b/tests/ui/macros/metavar-expressions/syntax-errors.rs
index 8fc76a74baa..585ea4d5979 100644
--- a/tests/ui/macros/metavar-expressions/syntax-errors.rs
+++ b/tests/ui/macros/metavar-expressions/syntax-errors.rs
@@ -30,7 +30,7 @@ macro_rules! metavar_with_literal_suffix {
 
 macro_rules! mve_without_parens {
     ( $( $i:ident ),* ) => { ${ count } };
-    //~^ ERROR meta-variable expression parameter must be wrapped in parentheses
+    //~^ ERROR expected `(`
 }
 
 #[rustfmt::skip]
@@ -45,9 +45,14 @@ macro_rules! open_brackets_with_lit {
      //~^ ERROR expected identifier
  }
 
+macro_rules! mvs_missing_paren {
+    ( $( $i:ident ),* ) => { ${ count $i ($i) } };
+    //~^ ERROR expected `(`
+}
+
 macro_rules! mve_wrong_delim {
     ( $( $i:ident ),* ) => { ${ count{i} } };
-    //~^ ERROR meta-variable expression parameter must be wrapped in parentheses
+    //~^ ERROR expected `(`
 }
 
 macro_rules! invalid_metavar {
@@ -64,28 +69,30 @@ macro_rules! open_brackets_with_group {
 macro_rules! extra_garbage_after_metavar {
     ( $( $i:ident ),* ) => {
         ${count() a b c}
-        //~^ ERROR unexpected token: a
+        //~^ ERROR unexpected trailing tokens
         ${count($i a b c)}
-        //~^ ERROR unexpected token: a
+        //~^ ERROR unexpected trailing tokens
         ${count($i, 1 a b c)}
-        //~^ ERROR unexpected token: a
+        //~^ ERROR unexpected trailing tokens
         ${count($i) a b c}
-        //~^ ERROR unexpected token: a
+        //~^ ERROR unexpected trailing tokens
 
         ${ignore($i) a b c}
-        //~^ ERROR unexpected token: a
+        //~^ ERROR unexpected trailing tokens
         ${ignore($i a b c)}
-        //~^ ERROR unexpected token: a
+        //~^ ERROR unexpected trailing tokens
 
         ${index() a b c}
-        //~^ ERROR unexpected token: a
+        //~^ ERROR unexpected trailing tokens
         ${index(1 a b c)}
-        //~^ ERROR unexpected token: a
+        //~^ ERROR unexpected trailing tokens
 
         ${index() a b c}
-        //~^ ERROR unexpected token: a
+        //~^ ERROR unexpected trailing tokens
         ${index(1 a b c)}
-        //~^ ERROR unexpected token: a
+        //~^ ERROR unexpected trailing tokens
+        ${index(1, a b c)}
+        //~^ ERROR unexpected trailing tokens
     };
 }
 
@@ -111,7 +118,7 @@ macro_rules! unknown_ignore_ident {
 
 macro_rules! unknown_metavar {
     ( $( $i:ident ),* ) => { ${ aaaaaaaaaaaaaa(i) } };
-    //~^ ERROR unrecognized meta-variable expression
+    //~^ ERROR unrecognized metavariable expression
 }
 
 fn main() {}
diff --git a/tests/ui/macros/metavar-expressions/syntax-errors.stderr b/tests/ui/macros/metavar-expressions/syntax-errors.stderr
index 20d2358facc..bf1c7673a6c 100644
--- a/tests/ui/macros/metavar-expressions/syntax-errors.stderr
+++ b/tests/ui/macros/metavar-expressions/syntax-errors.stderr
@@ -40,167 +40,165 @@ error: only unsuffixes integer literals are supported in meta-variable expressio
 LL |     ( $( $i:ident ),* ) => { ${ index(1u32) } };
    |                                 ^^^^^
 
-error: meta-variable expression parameter must be wrapped in parentheses
+error: expected `(`
   --> $DIR/syntax-errors.rs:32:33
    |
 LL |     ( $( $i:ident ),* ) => { ${ count } };
-   |                                 ^^^^^
+   |                                 ^^^^^- help: try adding parentheses: `( /* ... */ )`
+   |                                 |
+   |                                 for this this metavariable expression
+   |
+   = note: metavariable expressions use function-like parentheses syntax
 
-error: meta-variable expression parameter must be wrapped in parentheses
+error: expected `(`
   --> $DIR/syntax-errors.rs:49:33
    |
+LL |     ( $( $i:ident ),* ) => { ${ count $i ($i) } };
+   |                                 ^^^^^ - unexpected token
+   |                                 |
+   |                                 for this this metavariable expression
+   |
+   = note: metavariable expressions use function-like parentheses syntax
+
+error: expected `(`
+  --> $DIR/syntax-errors.rs:54:33
+   |
 LL |     ( $( $i:ident ),* ) => { ${ count{i} } };
-   |                                 ^^^^^
+   |                                 ^^^^^ for this this metavariable expression
+   |
+   = note: metavariable expressions use function-like parentheses syntax
 
 error: expected identifier, found `123`
-  --> $DIR/syntax-errors.rs:54:23
+  --> $DIR/syntax-errors.rs:59:23
    |
 LL |     () => { ${ignore($123)} }
    |                       ^^^ help: try removing `123`
 
-error: unexpected token: a
-  --> $DIR/syntax-errors.rs:66:19
-   |
-LL |         ${count() a b c}
-   |                   ^
-   |
-note: meta-variable expression must not have trailing tokens
-  --> $DIR/syntax-errors.rs:66:19
+error: unexpected trailing tokens
+  --> $DIR/syntax-errors.rs:71:19
    |
 LL |         ${count() a b c}
-   |                   ^
+   |           -----   ^^^^^ help: try removing these tokens
+   |           |
+   |           for this metavariable expression
 
-error: unexpected token: a
-  --> $DIR/syntax-errors.rs:68:20
+error: unexpected trailing tokens
+  --> $DIR/syntax-errors.rs:73:20
    |
 LL |         ${count($i a b c)}
-   |                    ^
-   |
-note: meta-variable expression must not have trailing tokens
-  --> $DIR/syntax-errors.rs:68:20
+   |           -----    ^^^^^ help: try removing these tokens
+   |           |
+   |           for this metavariable expression
    |
-LL |         ${count($i a b c)}
-   |                    ^
+   = note: the `count` metavariable expression takes between 1 and 2 arguments
 
-error: unexpected token: a
-  --> $DIR/syntax-errors.rs:70:23
+error: unexpected trailing tokens
+  --> $DIR/syntax-errors.rs:75:23
    |
 LL |         ${count($i, 1 a b c)}
-   |                       ^
+   |           -----       ^^^^^ help: try removing these tokens
+   |           |
+   |           for this metavariable expression
    |
-note: meta-variable expression must not have trailing tokens
-  --> $DIR/syntax-errors.rs:70:23
-   |
-LL |         ${count($i, 1 a b c)}
-   |                       ^
+   = note: the `count` metavariable expression takes between 1 and 2 arguments
 
-error: unexpected token: a
-  --> $DIR/syntax-errors.rs:72:21
-   |
-LL |         ${count($i) a b c}
-   |                     ^
-   |
-note: meta-variable expression must not have trailing tokens
-  --> $DIR/syntax-errors.rs:72:21
+error: unexpected trailing tokens
+  --> $DIR/syntax-errors.rs:77:21
    |
 LL |         ${count($i) a b c}
-   |                     ^
+   |           -----     ^^^^^ help: try removing these tokens
+   |           |
+   |           for this metavariable expression
 
-error: unexpected token: a
-  --> $DIR/syntax-errors.rs:75:22
+error: unexpected trailing tokens
+  --> $DIR/syntax-errors.rs:80:22
    |
 LL |         ${ignore($i) a b c}
-   |                      ^
-   |
-note: meta-variable expression must not have trailing tokens
-  --> $DIR/syntax-errors.rs:75:22
-   |
-LL |         ${ignore($i) a b c}
-   |                      ^
+   |           ------     ^^^^^ help: try removing these tokens
+   |           |
+   |           for this metavariable expression
 
-error: unexpected token: a
-  --> $DIR/syntax-errors.rs:77:21
+error: unexpected trailing tokens
+  --> $DIR/syntax-errors.rs:82:21
    |
 LL |         ${ignore($i a b c)}
-   |                     ^
+   |           ------    ^^^^^ help: try removing these tokens
+   |           |
+   |           for this metavariable expression
    |
-note: meta-variable expression must not have trailing tokens
-  --> $DIR/syntax-errors.rs:77:21
-   |
-LL |         ${ignore($i a b c)}
-   |                     ^
+   = note: the `ignore` metavariable expression takes a single argument
 
-error: unexpected token: a
-  --> $DIR/syntax-errors.rs:80:19
-   |
-LL |         ${index() a b c}
-   |                   ^
-   |
-note: meta-variable expression must not have trailing tokens
-  --> $DIR/syntax-errors.rs:80:19
+error: unexpected trailing tokens
+  --> $DIR/syntax-errors.rs:85:19
    |
 LL |         ${index() a b c}
-   |                   ^
+   |           -----   ^^^^^ help: try removing these tokens
+   |           |
+   |           for this metavariable expression
 
-error: unexpected token: a
-  --> $DIR/syntax-errors.rs:82:19
+error: unexpected trailing tokens
+  --> $DIR/syntax-errors.rs:87:19
    |
 LL |         ${index(1 a b c)}
-   |                   ^
-   |
-note: meta-variable expression must not have trailing tokens
-  --> $DIR/syntax-errors.rs:82:19
+   |           -----   ^^^^^ help: try removing these tokens
+   |           |
+   |           for this metavariable expression
    |
-LL |         ${index(1 a b c)}
-   |                   ^
+   = note: the `index` metavariable expression takes between 0 and 1 arguments
 
-error: unexpected token: a
-  --> $DIR/syntax-errors.rs:85:19
-   |
-LL |         ${index() a b c}
-   |                   ^
-   |
-note: meta-variable expression must not have trailing tokens
-  --> $DIR/syntax-errors.rs:85:19
+error: unexpected trailing tokens
+  --> $DIR/syntax-errors.rs:90:19
    |
 LL |         ${index() a b c}
-   |                   ^
+   |           -----   ^^^^^ help: try removing these tokens
+   |           |
+   |           for this metavariable expression
 
-error: unexpected token: a
-  --> $DIR/syntax-errors.rs:87:19
+error: unexpected trailing tokens
+  --> $DIR/syntax-errors.rs:92:19
    |
 LL |         ${index(1 a b c)}
-   |                   ^
+   |           -----   ^^^^^ help: try removing these tokens
+   |           |
+   |           for this metavariable expression
    |
-note: meta-variable expression must not have trailing tokens
-  --> $DIR/syntax-errors.rs:87:19
+   = note: the `index` metavariable expression takes between 0 and 1 arguments
+
+error: unexpected trailing tokens
+  --> $DIR/syntax-errors.rs:94:18
    |
-LL |         ${index(1 a b c)}
-   |                   ^
+LL |         ${index(1, a b c)}
+   |           -----  ^^^^^^^ help: try removing these tokens
+   |           |
+   |           for this metavariable expression
+   |
+   = note: the `index` metavariable expression takes between 0 and 1 arguments
 
 error: meta-variable expression depth must be a literal
-  --> $DIR/syntax-errors.rs:94:33
+  --> $DIR/syntax-errors.rs:101:33
    |
 LL |     ( $( $i:ident ),* ) => { ${ index(IDX) } };
    |                                 ^^^^^
 
 error: meta-variables within meta-variable expressions must be referenced using a dollar sign
-  --> $DIR/syntax-errors.rs:100:11
+  --> $DIR/syntax-errors.rs:107:11
    |
 LL |         ${count(foo)}
    |           ^^^^^
 
 error: meta-variables within meta-variable expressions must be referenced using a dollar sign
-  --> $DIR/syntax-errors.rs:107:11
+  --> $DIR/syntax-errors.rs:114:11
    |
 LL |         ${ignore(bar)}
    |           ^^^^^^
 
-error: unrecognized meta-variable expression
-  --> $DIR/syntax-errors.rs:113:33
+error: unrecognized metavariable expression
+  --> $DIR/syntax-errors.rs:120:33
    |
 LL |     ( $( $i:ident ),* ) => { ${ aaaaaaaaaaaaaa(i) } };
-   |                                 ^^^^^^^^^^^^^^ help: supported expressions are count, ignore, index and len
+   |                                 ^^^^^^^^^^^^^^ not a valid metavariable expression
+   |
+   = note: valid metavariable expressions are `count`, `ignore`, `index`, `len`, and `concat`
 
 error: expected identifier or string literal
   --> $DIR/syntax-errors.rs:38:14
@@ -215,10 +213,10 @@ LL |      () => { ${ "hi" } };
    |                 ^^^^ help: try removing `"hi"`
 
 error: expected identifier or string literal
-  --> $DIR/syntax-errors.rs:60:33
+  --> $DIR/syntax-errors.rs:65:33
    |
 LL |     ( $( $i:ident ),* ) => { ${ {} } };
    |                                 ^^
 
-error: aborting due to 25 previous errors
+error: aborting due to 27 previous errors