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_macros::Subdiagnostic;
7use rustc_parse::parser::{Parser, Recovery, token_descr};
8use rustc_session::parse::ParseSess;
9use rustc_span::source_map::SourceMap;
10use rustc_span::{DUMMY_SP, ErrorGuaranteed, Ident, Span};
11use tracing::debug;
12
13use super::macro_rules::{MacroRule, NoopTracker, parser_from_cx};
14use crate::expand::{AstFragmentKind, parse_ast_fragment};
15use crate::mbe::macro_parser::ParseResult::*;
16use crate::mbe::macro_parser::{MatcherLoc, NamedParseResult, TtParser};
17use crate::mbe::macro_rules::{
18    Tracker, try_match_macro, try_match_macro_attr, try_match_macro_derive,
19};
20
21pub(super) enum FailedMacro<'a> {
22    Func,
23    Attr(&'a TokenStream),
24    Derive,
25}
26
27pub(super) fn failed_to_match_macro(
28    psess: &ParseSess,
29    sp: Span,
30    def_span: Span,
31    name: Ident,
32    args: FailedMacro<'_>,
33    body: &TokenStream,
34    rules: &[MacroRule],
35) -> (Span, ErrorGuaranteed) {
36    debug!("failed to match macro");
37    let def_head_span = if !def_span.is_dummy() && !psess.source_map().is_imported(def_span) {
38        psess.source_map().guess_head_span(def_span)
39    } else {
40        DUMMY_SP
41    };
42
43    // An error occurred, try the expansion again, tracking the expansion closely for better
44    // diagnostics.
45    let mut tracker = CollectTrackerAndEmitter::new(psess.dcx(), sp);
46
47    let try_success_result = match args {
48        FailedMacro::Func => try_match_macro(psess, name, body, rules, &mut tracker),
49        FailedMacro::Attr(attr_args) => {
50            try_match_macro_attr(psess, name, attr_args, body, rules, &mut tracker)
51        }
52        FailedMacro::Derive => try_match_macro_derive(psess, name, body, rules, &mut tracker),
53    };
54
55    if try_success_result.is_ok() {
56        // Nonterminal parser recovery might turn failed matches into successful ones,
57        // but for that it must have emitted an error already
58        assert!(
59            tracker.dcx.has_errors().is_some(),
60            "Macro matching returned a success on the second try"
61        );
62    }
63
64    if let Some(result) = tracker.result {
65        // An irrecoverable error occurred and has been emitted.
66        return result;
67    }
68
69    let Some(BestFailure { token, msg: label, remaining_matcher, .. }) = tracker.best_failure
70    else {
71        return (sp, psess.dcx().span_delayed_bug(sp, "failed to match a macro"));
72    };
73
74    let span = token.span.substitute_dummy(sp);
75
76    let mut err = psess.dcx().struct_span_err(span, parse_failure_msg(&token, None));
77    err.span_label(span, label);
78    if !def_head_span.is_dummy() {
79        err.span_label(def_head_span, "when calling this macro");
80    }
81
82    annotate_doc_comment(&mut err, psess.source_map(), span);
83
84    if let Some(span) = remaining_matcher.span() {
85        err.span_note(span, format!("while trying to match {remaining_matcher}"));
86    } else {
87        err.note(format!("while trying to match {remaining_matcher}"));
88    }
89
90    if let MatcherLoc::Token { token: expected_token } = &remaining_matcher
91        && (matches!(expected_token.kind, token::OpenInvisible(_))
92            || matches!(token.kind, token::OpenInvisible(_)))
93    {
94        err.note("captured metavariables except for `:tt`, `:ident` and `:lifetime` cannot be compared to other tokens");
95        err.note("see <https://doc.rust-lang.org/nightly/reference/macros-by-example.html#forwarding-a-matched-fragment> for more information");
96
97        if !def_span.is_dummy() && !psess.source_map().is_imported(def_span) {
98            err.help("try using `:tt` instead in the macro definition");
99        }
100    }
101
102    // Check whether there's a missing comma in this macro call, like `println!("{}" a);`
103    if let FailedMacro::Func = args
104        && let Some((body, comma_span)) = body.add_comma()
105    {
106        for rule in rules {
107            let MacroRule::Func { lhs, .. } = rule else { continue };
108            let parser = parser_from_cx(psess, body.clone(), Recovery::Allowed);
109            let mut tt_parser = TtParser::new(name);
110
111            if let Success(_) =
112                tt_parser.parse_tt(&mut Cow::Borrowed(&parser), lhs, &mut NoopTracker)
113            {
114                if comma_span.is_dummy() {
115                    err.note("you might be missing a comma");
116                } else {
117                    err.span_suggestion_short(
118                        comma_span,
119                        "missing comma here",
120                        ", ",
121                        Applicability::MachineApplicable,
122                    );
123                }
124            }
125        }
126    }
127    let guar = err.emit();
128    (sp, guar)
129}
130
131/// The tracker used for the slow error path that collects useful info for diagnostics.
132struct CollectTrackerAndEmitter<'dcx, 'matcher> {
133    dcx: DiagCtxtHandle<'dcx>,
134    remaining_matcher: Option<&'matcher MatcherLoc>,
135    /// Which arm's failure should we report? (the one furthest along)
136    best_failure: Option<BestFailure>,
137    root_span: Span,
138    result: Option<(Span, ErrorGuaranteed)>,
139}
140
141struct BestFailure {
142    token: Token,
143    position_in_tokenstream: (bool, u32),
144    msg: &'static str,
145    remaining_matcher: MatcherLoc,
146}
147
148impl BestFailure {
149    fn is_better_position(&self, position: (bool, u32)) -> bool {
150        position > self.position_in_tokenstream
151    }
152}
153
154impl<'dcx, 'matcher> Tracker<'matcher> for CollectTrackerAndEmitter<'dcx, 'matcher> {
155    type Failure = (Token, u32, &'static str);
156
157    fn build_failure(tok: Token, position: u32, msg: &'static str) -> Self::Failure {
158        (tok, position, msg)
159    }
160
161    fn before_match_loc(&mut self, parser: &TtParser, matcher: &'matcher MatcherLoc) {
162        if self.remaining_matcher.is_none()
163            || (parser.has_no_remaining_items_for_step() && *matcher != MatcherLoc::Eof)
164        {
165            self.remaining_matcher = Some(matcher);
166        }
167    }
168
169    fn after_arm(&mut self, in_body: bool, result: &NamedParseResult<Self::Failure>) {
170        match result {
171            Success(_) => {
172                // Nonterminal parser recovery might turn failed matches into successful ones,
173                // but for that it must have emitted an error already
174                self.dcx.span_delayed_bug(
175                    self.root_span,
176                    "should not collect detailed info for successful macro match",
177                );
178            }
179            Failure((token, approx_position, msg)) => {
180                debug!(?token, ?msg, "a new failure of an arm");
181
182                let position_in_tokenstream = (in_body, *approx_position);
183                if self
184                    .best_failure
185                    .as_ref()
186                    .is_none_or(|failure| failure.is_better_position(position_in_tokenstream))
187                {
188                    self.best_failure = Some(BestFailure {
189                        token: *token,
190                        position_in_tokenstream,
191                        msg,
192                        remaining_matcher: self
193                            .remaining_matcher
194                            .expect("must have collected matcher already")
195                            .clone(),
196                    })
197                }
198            }
199            Error(err_sp, msg) => {
200                let span = err_sp.substitute_dummy(self.root_span);
201                let guar = self.dcx.span_err(span, msg.clone());
202                self.result = Some((span, guar));
203            }
204            ErrorReported(guar) => self.result = Some((self.root_span, *guar)),
205        }
206    }
207
208    fn description() -> &'static str {
209        "detailed"
210    }
211
212    fn recovery() -> Recovery {
213        Recovery::Allowed
214    }
215}
216
217impl<'dcx> CollectTrackerAndEmitter<'dcx, '_> {
218    fn new(dcx: DiagCtxtHandle<'dcx>, root_span: Span) -> Self {
219        Self { dcx, remaining_matcher: None, best_failure: None, root_span, result: None }
220    }
221}
222
223pub(super) fn emit_frag_parse_err(
224    mut e: Diag<'_>,
225    parser: &Parser<'_>,
226    orig_parser: &mut Parser<'_>,
227    site_span: Span,
228    arm_span: Span,
229    kind: AstFragmentKind,
230) -> ErrorGuaranteed {
231    // FIXME(davidtwco): avoid depending on the error message text
232    if parser.token == token::Eof
233        && let DiagMessage::Str(message) = &e.messages[0].0
234        && message.ends_with(", found `<eof>`")
235    {
236        let msg = &e.messages[0];
237        e.messages[0] = (
238            DiagMessage::from(format!(
239                "macro expansion ends with an incomplete expression: {}",
240                message.replace(", found `<eof>`", ""),
241            )),
242            msg.1,
243        );
244        if !e.span.is_dummy() {
245            // early end of macro arm (#52866)
246            e.replace_span_with(parser.token.span.shrink_to_hi(), true);
247        }
248    }
249    if e.span.is_dummy() {
250        // Get around lack of span in error (#30128)
251        e.replace_span_with(site_span, true);
252        if !parser.psess.source_map().is_imported(arm_span) {
253            e.span_label(arm_span, "in this macro arm");
254        }
255    } else if parser.psess.source_map().is_imported(parser.token.span) {
256        e.span_label(site_span, "in this macro invocation");
257    }
258    match kind {
259        // Try a statement if an expression is wanted but failed and suggest adding `;` to call.
260        AstFragmentKind::Expr => match parse_ast_fragment(orig_parser, AstFragmentKind::Stmts) {
261            Err(err) => err.cancel(),
262            Ok(_) => {
263                e.note(
264                    "the macro call doesn't expand to an expression, but it can expand to a statement",
265                );
266
267                if parser.token == token::Semi {
268                    if let Ok(snippet) = parser.psess.source_map().span_to_snippet(site_span) {
269                        e.span_suggestion_verbose(
270                            site_span,
271                            "surround the macro invocation with `{}` to interpret the expansion as a statement",
272                            format!("{{ {snippet}; }}"),
273                            Applicability::MaybeIncorrect,
274                        );
275                    }
276                } else {
277                    e.span_suggestion_verbose(
278                        site_span.shrink_to_hi(),
279                        "add `;` to interpret the expansion as a statement",
280                        ";",
281                        Applicability::MaybeIncorrect,
282                    );
283                }
284            }
285        },
286        _ => annotate_err_with_kind(&mut e, kind, site_span),
287    };
288    e.emit()
289}
290
291pub(crate) fn annotate_err_with_kind(err: &mut Diag<'_>, kind: AstFragmentKind, span: Span) {
292    match kind {
293        AstFragmentKind::Ty => {
294            err.span_label(span, "this macro call doesn't expand to a type");
295        }
296        AstFragmentKind::Pat => {
297            err.span_label(span, "this macro call doesn't expand to a pattern");
298        }
299        _ => {}
300    };
301}
302
303#[derive(Subdiagnostic)]
304enum ExplainDocComment {
305    #[label(expand_explain_doc_comment_inner)]
306    Inner {
307        #[primary_span]
308        span: Span,
309    },
310    #[label(expand_explain_doc_comment_outer)]
311    Outer {
312        #[primary_span]
313        span: Span,
314    },
315}
316
317fn annotate_doc_comment(err: &mut Diag<'_>, sm: &SourceMap, span: Span) {
318    if let Ok(src) = sm.span_to_snippet(span) {
319        if src.starts_with("///") || src.starts_with("/**") {
320            err.subdiagnostic(ExplainDocComment::Outer { span });
321        } else if src.starts_with("//!") || src.starts_with("/*!") {
322            err.subdiagnostic(ExplainDocComment::Inner { span });
323        }
324    }
325}
326
327/// Generates an appropriate parsing failure message. For EOF, this is "unexpected end...". For
328/// other tokens, this is "unexpected token...".
329fn parse_failure_msg(tok: &Token, expected_token: Option<&Token>) -> Cow<'static, str> {
330    if let Some(expected_token) = expected_token {
331        Cow::from(format!("expected {}, found {}", token_descr(expected_token), token_descr(tok)))
332    } else {
333        match tok.kind {
334            token::Eof => Cow::from("unexpected end of macro invocation"),
335            _ => Cow::from(format!("no rules expected {}", token_descr(tok))),
336        }
337    }
338}