rustc_expand/mbe/
diagnostics.rs

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