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