Skip to main content

rustc_expand/mbe/
diagnostics.rs

1use std::borrow::Cow;
2
3use rustc_ast::token::{self, Token};
4use rustc_ast::tokenstream::TokenStream;
5use rustc_errors::{Applicability, Diag, DiagCtxtHandle, DiagMessage};
6use rustc_hir::attrs::diagnostic::{CustomDiagnostic, Directive, FormatArgs};
7use rustc_macros::Subdiagnostic;
8use rustc_parse::parser::{Parser, Recovery, token_descr};
9use rustc_session::parse::ParseSess;
10use rustc_span::source_map::SourceMap;
11use rustc_span::{DUMMY_SP, ErrorGuaranteed, Ident, Span};
12use tracing::debug;
13
14use super::macro_rules::{MacroRule, NoopTracker, parser_from_cx};
15use crate::expand::{AstFragmentKind, parse_ast_fragment};
16use crate::mbe::macro_parser::ParseResult::*;
17use crate::mbe::macro_parser::{MatcherLoc, NamedParseResult, TtParser};
18use crate::mbe::macro_rules::{
19    Tracker, try_match_macro, try_match_macro_attr, try_match_macro_derive,
20};
21
22pub(super) enum FailedMacro<'a> {
23    Func,
24    Attr(&'a TokenStream),
25    Derive,
26}
27
28pub(super) fn failed_to_match_macro(
29    psess: &ParseSess,
30    sp: Span,
31    def_span: Span,
32    name: Ident,
33    args: FailedMacro<'_>,
34    body: &TokenStream,
35    rules: &[MacroRule],
36    on_unmatch_args: Option<&Directive>,
37) -> (Span, ErrorGuaranteed) {
38    {
    use ::tracing::__macro_support::Callsite as _;
    static __CALLSITE: ::tracing::callsite::DefaultCallsite =
        {
            static META: ::tracing::Metadata<'static> =
                {
                    ::tracing_core::metadata::Metadata::new("event compiler/rustc_expand/src/mbe/diagnostics.rs:38",
                        "rustc_expand::mbe::diagnostics", ::tracing::Level::DEBUG,
                        ::tracing_core::__macro_support::Option::Some("compiler/rustc_expand/src/mbe/diagnostics.rs"),
                        ::tracing_core::__macro_support::Option::Some(38u32),
                        ::tracing_core::__macro_support::Option::Some("rustc_expand::mbe::diagnostics"),
                        ::tracing_core::field::FieldSet::new(&["message"],
                            ::tracing_core::callsite::Identifier(&__CALLSITE)),
                        ::tracing::metadata::Kind::EVENT)
                };
            ::tracing::callsite::DefaultCallsite::new(&META)
        };
    let enabled =
        ::tracing::Level::DEBUG <= ::tracing::level_filters::STATIC_MAX_LEVEL
                &&
                ::tracing::Level::DEBUG <=
                    ::tracing::level_filters::LevelFilter::current() &&
            {
                let interest = __CALLSITE.interest();
                !interest.is_never() &&
                    ::tracing::__macro_support::__is_enabled(__CALLSITE.metadata(),
                        interest)
            };
    if enabled {
        (|value_set: ::tracing::field::ValueSet|
                    {
                        let meta = __CALLSITE.metadata();
                        ::tracing::Event::dispatch(meta, &value_set);
                        ;
                    })({
                #[allow(unused_imports)]
                use ::tracing::field::{debug, display, Value};
                let mut iter = __CALLSITE.metadata().fields().iter();
                __CALLSITE.metadata().fields().value_set(&[(&::tracing::__macro_support::Iterator::next(&mut iter).expect("FieldSet corrupted (this is a bug)"),
                                    ::tracing::__macro_support::Option::Some(&format_args!("failed to match macro")
                                            as &dyn Value))])
            });
    } else { ; }
};debug!("failed to match macro");
39    let def_head_span = if !def_span.is_dummy() && !psess.source_map().is_imported(def_span) {
40        psess.source_map().guess_head_span(def_span)
41    } else {
42        DUMMY_SP
43    };
44
45    // An error occurred, try the expansion again, tracking the expansion closely for better
46    // diagnostics.
47    let mut tracker = CollectTrackerAndEmitter::new(psess.dcx(), sp);
48
49    let try_success_result = match args {
50        FailedMacro::Func => try_match_macro(psess, name, body, rules, &mut tracker),
51        FailedMacro::Attr(attr_args) => {
52            try_match_macro_attr(psess, name, attr_args, body, rules, &mut tracker)
53        }
54        FailedMacro::Derive => try_match_macro_derive(psess, name, body, rules, &mut tracker),
55    };
56
57    if try_success_result.is_ok() {
58        // Nonterminal parser recovery might turn failed matches into successful ones,
59        // but for that it must have emitted an error already
60        if !tracker.dcx.has_errors().is_some() {
    {
        ::core::panicking::panic_fmt(format_args!("Macro matching returned a success on the second try"));
    }
};assert!(
61            tracker.dcx.has_errors().is_some(),
62            "Macro matching returned a success on the second try"
63        );
64    }
65
66    if let Some(result) = tracker.result {
67        // An irrecoverable error occurred and has been emitted.
68        return result;
69    }
70
71    let Some(BestFailure { token, msg: label, remaining_matcher, .. }) = tracker.best_failure
72    else {
73        return (sp, psess.dcx().span_delayed_bug(sp, "failed to match a macro"));
74    };
75
76    let span = token.span.substitute_dummy(sp);
77    let CustomDiagnostic {
78        message: custom_message, label: custom_label, notes: custom_notes, ..
79    } = {
80        on_unmatch_args
81            .map(|directive| directive.eval(None, &FormatArgs { this: name.to_string(), .. }))
82            .unwrap_or_default()
83    };
84
85    let mut err = match custom_message {
86        Some(message) => psess.dcx().struct_span_err(span, message),
87        None => psess.dcx().struct_span_err(span, parse_failure_msg(&token, None)),
88    };
89    err.span_label(span, custom_label.unwrap_or_else(|| label.to_string()));
90    if !def_head_span.is_dummy() {
91        err.span_label(def_head_span, "when calling this macro");
92    }
93
94    annotate_doc_comment(&mut err, psess.source_map(), span);
95
96    if let Some(span) = remaining_matcher.span() {
97        err.span_note(span, ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("while trying to match {0}",
                remaining_matcher))
    })format!("while trying to match {remaining_matcher}"));
98    } else {
99        err.note(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("while trying to match {0}",
                remaining_matcher))
    })format!("while trying to match {remaining_matcher}"));
100    }
101    for note in custom_notes {
102        err.note(note);
103    }
104
105    if let MatcherLoc::Token { token: expected_token } = &remaining_matcher
106        && (#[allow(non_exhaustive_omitted_patterns)] match expected_token.kind {
    token::OpenInvisible(_) => true,
    _ => false,
}matches!(expected_token.kind, token::OpenInvisible(_))
107            || #[allow(non_exhaustive_omitted_patterns)] match token.kind {
    token::OpenInvisible(_) => true,
    _ => false,
}matches!(token.kind, token::OpenInvisible(_)))
108    {
109        err.note("captured metavariables except for `:tt`, `:ident` and `:lifetime` cannot be compared to other tokens");
110        err.note("see <https://doc.rust-lang.org/nightly/reference/macros-by-example.html#forwarding-a-matched-fragment> for more information");
111
112        if !def_span.is_dummy() && !psess.source_map().is_imported(def_span) {
113            err.help("try using `:tt` instead in the macro definition");
114        }
115    }
116
117    // Check whether there's a missing comma in this macro call, like `println!("{}" a);`
118    if let FailedMacro::Func = args
119        && let Some((body, comma_span)) = body.add_comma()
120    {
121        for rule in rules {
122            let MacroRule::Func { lhs, .. } = rule else { continue };
123            let parser = parser_from_cx(psess, body.clone(), Recovery::Allowed);
124            let mut tt_parser = TtParser::new(name);
125
126            if let Success(_) =
127                tt_parser.parse_tt(&mut Cow::Borrowed(&parser), lhs, &mut NoopTracker)
128            {
129                if comma_span.is_dummy() {
130                    err.note("you might be missing a comma");
131                } else {
132                    err.span_suggestion_short(
133                        comma_span,
134                        "missing comma here",
135                        ", ",
136                        Applicability::MachineApplicable,
137                    );
138                }
139            }
140        }
141    }
142    let guar = err.emit();
143    (sp, guar)
144}
145
146/// The tracker used for the slow error path that collects useful info for diagnostics.
147struct CollectTrackerAndEmitter<'dcx, 'matcher> {
148    dcx: DiagCtxtHandle<'dcx>,
149    remaining_matcher: Option<&'matcher MatcherLoc>,
150    /// Which arm's failure should we report? (the one furthest along)
151    best_failure: Option<BestFailure>,
152    root_span: Span,
153    result: Option<(Span, ErrorGuaranteed)>,
154}
155
156struct BestFailure {
157    token: Token,
158    position_in_tokenstream: (bool, u32),
159    msg: &'static str,
160    remaining_matcher: MatcherLoc,
161}
162
163impl BestFailure {
164    fn is_better_position(&self, position: (bool, u32)) -> bool {
165        position > self.position_in_tokenstream
166    }
167}
168
169impl<'dcx, 'matcher> Tracker<'matcher> for CollectTrackerAndEmitter<'dcx, 'matcher> {
170    type Failure = (Token, u32, &'static str);
171
172    fn build_failure(tok: Token, position: u32, msg: &'static str) -> Self::Failure {
173        (tok, position, msg)
174    }
175
176    fn before_match_loc(&mut self, parser: &TtParser, matcher: &'matcher MatcherLoc) {
177        if self.remaining_matcher.is_none()
178            || (parser.has_no_remaining_items_for_step() && *matcher != MatcherLoc::Eof)
179        {
180            self.remaining_matcher = Some(matcher);
181        }
182    }
183
184    fn after_arm(&mut self, in_body: bool, result: &NamedParseResult<Self::Failure>) {
185        match result {
186            Success(_) => {
187                // Nonterminal parser recovery might turn failed matches into successful ones,
188                // but for that it must have emitted an error already
189                self.dcx.span_delayed_bug(
190                    self.root_span,
191                    "should not collect detailed info for successful macro match",
192                );
193            }
194            Failure((token, approx_position, msg)) => {
195                {
    use ::tracing::__macro_support::Callsite as _;
    static __CALLSITE: ::tracing::callsite::DefaultCallsite =
        {
            static META: ::tracing::Metadata<'static> =
                {
                    ::tracing_core::metadata::Metadata::new("event compiler/rustc_expand/src/mbe/diagnostics.rs:195",
                        "rustc_expand::mbe::diagnostics", ::tracing::Level::DEBUG,
                        ::tracing_core::__macro_support::Option::Some("compiler/rustc_expand/src/mbe/diagnostics.rs"),
                        ::tracing_core::__macro_support::Option::Some(195u32),
                        ::tracing_core::__macro_support::Option::Some("rustc_expand::mbe::diagnostics"),
                        ::tracing_core::field::FieldSet::new(&["message", "token",
                                        "msg"], ::tracing_core::callsite::Identifier(&__CALLSITE)),
                        ::tracing::metadata::Kind::EVENT)
                };
            ::tracing::callsite::DefaultCallsite::new(&META)
        };
    let enabled =
        ::tracing::Level::DEBUG <= ::tracing::level_filters::STATIC_MAX_LEVEL
                &&
                ::tracing::Level::DEBUG <=
                    ::tracing::level_filters::LevelFilter::current() &&
            {
                let interest = __CALLSITE.interest();
                !interest.is_never() &&
                    ::tracing::__macro_support::__is_enabled(__CALLSITE.metadata(),
                        interest)
            };
    if enabled {
        (|value_set: ::tracing::field::ValueSet|
                    {
                        let meta = __CALLSITE.metadata();
                        ::tracing::Event::dispatch(meta, &value_set);
                        ;
                    })({
                #[allow(unused_imports)]
                use ::tracing::field::{debug, display, Value};
                let mut iter = __CALLSITE.metadata().fields().iter();
                __CALLSITE.metadata().fields().value_set(&[(&::tracing::__macro_support::Iterator::next(&mut iter).expect("FieldSet corrupted (this is a bug)"),
                                    ::tracing::__macro_support::Option::Some(&format_args!("a new failure of an arm")
                                            as &dyn Value)),
                                (&::tracing::__macro_support::Iterator::next(&mut iter).expect("FieldSet corrupted (this is a bug)"),
                                    ::tracing::__macro_support::Option::Some(&debug(&token) as
                                            &dyn Value)),
                                (&::tracing::__macro_support::Iterator::next(&mut iter).expect("FieldSet corrupted (this is a bug)"),
                                    ::tracing::__macro_support::Option::Some(&debug(&msg) as
                                            &dyn Value))])
            });
    } else { ; }
};debug!(?token, ?msg, "a new failure of an arm");
196
197                let position_in_tokenstream = (in_body, *approx_position);
198                if self
199                    .best_failure
200                    .as_ref()
201                    .is_none_or(|failure| failure.is_better_position(position_in_tokenstream))
202                {
203                    self.best_failure = Some(BestFailure {
204                        token: *token,
205                        position_in_tokenstream,
206                        msg,
207                        remaining_matcher: self
208                            .remaining_matcher
209                            .expect("must have collected matcher already")
210                            .clone(),
211                    })
212                }
213            }
214            Error(err_sp, msg) => {
215                let span = err_sp.substitute_dummy(self.root_span);
216                let guar = self.dcx.span_err(span, msg.clone());
217                self.result = Some((span, guar));
218            }
219            ErrorReported(guar) => self.result = Some((self.root_span, *guar)),
220        }
221    }
222
223    fn description() -> &'static str {
224        "detailed"
225    }
226
227    fn recovery() -> Recovery {
228        Recovery::Allowed
229    }
230}
231
232impl<'dcx> CollectTrackerAndEmitter<'dcx, '_> {
233    fn new(dcx: DiagCtxtHandle<'dcx>, root_span: Span) -> Self {
234        Self { dcx, remaining_matcher: None, best_failure: None, root_span, result: None }
235    }
236}
237
238pub(super) fn emit_frag_parse_err(
239    mut e: Diag<'_>,
240    parser: &mut Parser<'_>,
241    orig_parser: &mut Parser<'_>,
242    site_span: Span,
243    arm_span: Span,
244    kind: AstFragmentKind,
245    bindings: &[MacroRule],
246    matched_rule_bindings: &[MatcherLoc],
247) -> ErrorGuaranteed {
248    // FIXME(davidtwco): avoid depending on the error message text
249    if parser.token == token::Eof
250        && let DiagMessage::Str(message) = &e.messages[0].0
251        && message.ends_with(", found `<eof>`")
252    {
253        let msg = &e.messages[0];
254        e.messages[0] = (
255            DiagMessage::from(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("macro expansion ends with an incomplete expression: {0}",
                message.replace(", found `<eof>`", "")))
    })format!(
256                "macro expansion ends with an incomplete expression: {}",
257                message.replace(", found `<eof>`", ""),
258            )),
259            msg.1,
260        );
261        if !e.span.is_dummy() {
262            // early end of macro arm (#52866)
263            e.replace_span_with(parser.token.span.shrink_to_hi(), true);
264        }
265    }
266    if e.span.is_dummy() {
267        // Get around lack of span in error (#30128)
268        e.replace_span_with(site_span, true);
269        if !parser.psess.source_map().is_imported(arm_span) {
270            e.span_label(arm_span, "in this macro arm");
271        }
272    } else if parser.psess.source_map().is_imported(parser.token.span) {
273        e.span_label(site_span, "in this macro invocation");
274    }
275    match kind {
276        // Try a statement if an expression is wanted but failed and suggest adding `;` to call.
277        AstFragmentKind::Expr => match parse_ast_fragment(orig_parser, AstFragmentKind::Stmts) {
278            Err(err) => err.cancel(),
279            Ok(_) => {
280                e.note(
281                    "the macro call doesn't expand to an expression, but it can expand to a statement",
282                );
283
284                if parser.token == token::Semi {
285                    if let Ok(snippet) = parser.psess.source_map().span_to_snippet(site_span) {
286                        e.span_suggestion_verbose(
287                            site_span,
288                            "surround the macro invocation with `{}` to interpret the expansion as a statement",
289                            ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{{ {0}; }}", snippet))
    })format!("{{ {snippet}; }}"),
290                            Applicability::MaybeIncorrect,
291                        );
292                    }
293                } else {
294                    e.span_suggestion_verbose(
295                        site_span.shrink_to_hi(),
296                        "add `;` to interpret the expansion as a statement",
297                        ";",
298                        Applicability::MaybeIncorrect,
299                    );
300                }
301            }
302        },
303        _ => annotate_err_with_kind(&mut e, kind, site_span),
304    };
305
306    if parser.token.kind == token::Dollar {
307        let dollar_span = parser.token.span;
308        parser.bump();
309        if let token::Ident(name, _) = parser.token.kind {
310            let metavar_span = dollar_span.to(parser.token.span);
311            let mut bindings_names = ::alloc::vec::Vec::new()vec![];
312            for rule in bindings {
313                let MacroRule::Func { lhs, .. } = rule else { continue };
314                for param in lhs {
315                    let MatcherLoc::MetaVarDecl { bind, .. } = param else { continue };
316                    bindings_names.push(bind.name);
317                }
318            }
319
320            let mut matched_rule_bindings_names = ::alloc::vec::Vec::new()vec![];
321            for param in matched_rule_bindings {
322                let MatcherLoc::MetaVarDecl { bind, .. } = param else { continue };
323                matched_rule_bindings_names.push(bind.name);
324            }
325
326            // Report the unbound metavariable as the primary error up front, so every
327            // case is consistent regardless of which suggestion (if any) we attach below.
328            e.primary_message(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("cannot find macro parameter `${0}` in this scope",
                name))
    })format!("cannot find macro parameter `${name}` in this scope"));
329            e.span(metavar_span);
330            e.span_label(metavar_span, "not found in this scope");
331            if parser.psess.source_map().is_imported(metavar_span) {
332                e.span_label(site_span, "in this macro invocation");
333            }
334
335            if let Some(matched_name) = rustc_span::edit_distance::find_best_match_for_name(
336                &matched_rule_bindings_names[..],
337                name,
338                None,
339            ) {
340                e.span_suggestion_verbose(
341                    parser.token.span,
342                    "there is a macro metavariable with a similar name",
343                    matched_name,
344                    Applicability::MaybeIncorrect,
345                );
346            } else if bindings_names.contains(&name) {
347                e.span_label(
348                    parser.token.span,
349                    "there is an macro metavariable with this name in another macro matcher",
350                );
351            } else if let Some(matched_name) =
352                rustc_span::edit_distance::find_best_match_for_name(&bindings_names[..], name, None)
353            {
354                e.span_suggestion_verbose(
355                    parser.token.span,
356                    "there is a macro metavariable with a similar name in another macro matcher",
357                    matched_name,
358                    Applicability::MaybeIncorrect,
359                );
360            } else if !matched_rule_bindings_names.is_empty() {
361                let msg = matched_rule_bindings_names
362                    .iter()
363                    .map(|sym| ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("${0}", sym))
    })format!("${}", sym))
364                    .collect::<Vec<_>>()
365                    .join(", ");
366                e.note(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("available metavariable names are: {0}",
                msg))
    })format!("available metavariable names are: {msg}"));
367            }
368        }
369    }
370    e.emit()
371}
372
373pub(crate) fn annotate_err_with_kind(err: &mut Diag<'_>, kind: AstFragmentKind, span: Span) {
374    match kind {
375        AstFragmentKind::Ty => {
376            err.span_label(span, "this macro call doesn't expand to a type");
377        }
378        AstFragmentKind::Pat => {
379            err.span_label(span, "this macro call doesn't expand to a pattern");
380        }
381        _ => {}
382    };
383}
384
385#[derive(const _: () =
    {
        impl rustc_errors::Subdiagnostic for ExplainDocComment {
            fn add_to_diag<__G>(self, diag: &mut rustc_errors::Diag<'_, __G>)
                where __G: rustc_errors::EmissionGuarantee {
                match self {
                    ExplainDocComment::Inner { span: __binding_0 } => {
                        let mut sub_args = rustc_errors::DiagArgMap::default();
                        let __message =
                            rustc_errors::format_diag_message(&rustc_errors::DiagMessage::Inline(std::borrow::Cow::Borrowed("inner doc comments expand to `#![doc = \"...\"]`, which is what this macro attempted to match")),
                                &sub_args);
                        diag.span_label(__binding_0, __message);
                    }
                    ExplainDocComment::Outer { span: __binding_0 } => {
                        let mut sub_args = rustc_errors::DiagArgMap::default();
                        let __message =
                            rustc_errors::format_diag_message(&rustc_errors::DiagMessage::Inline(std::borrow::Cow::Borrowed("outer doc comments expand to `#[doc = \"...\"]`, which is what this macro attempted to match")),
                                &sub_args);
                        diag.span_label(__binding_0, __message);
                    }
                }
            }
        }
    };Subdiagnostic)]
386enum ExplainDocComment {
387    #[label(
388        "inner doc comments expand to `#![doc = \"...\"]`, which is what this macro attempted to match"
389    )]
390    Inner {
391        #[primary_span]
392        span: Span,
393    },
394    #[label(
395        "outer doc comments expand to `#[doc = \"...\"]`, which is what this macro attempted to match"
396    )]
397    Outer {
398        #[primary_span]
399        span: Span,
400    },
401}
402
403fn annotate_doc_comment(err: &mut Diag<'_>, sm: &SourceMap, span: Span) {
404    if let Ok(src) = sm.span_to_snippet(span) {
405        if src.starts_with("///") || src.starts_with("/**") {
406            err.subdiagnostic(ExplainDocComment::Outer { span });
407        } else if src.starts_with("//!") || src.starts_with("/*!") {
408            err.subdiagnostic(ExplainDocComment::Inner { span });
409        }
410    }
411}
412
413/// Generates an appropriate parsing failure message. For EOF, this is "unexpected end...". For
414/// other tokens, this is "unexpected token...".
415fn parse_failure_msg(tok: &Token, expected_token: Option<&Token>) -> Cow<'static, str> {
416    if let Some(expected_token) = expected_token {
417        Cow::from(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("expected {0}, found {1}",
                token_descr(expected_token), token_descr(tok)))
    })format!("expected {}, found {}", token_descr(expected_token), token_descr(tok)))
418    } else {
419        match tok.kind {
420            token::Eof => Cow::from("unexpected end of macro invocation"),
421            _ => Cow::from(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("no rules expected {0}",
                token_descr(tok)))
    })format!("no rules expected {}", token_descr(tok))),
422        }
423    }
424}