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