| use std::borrow::Cow; | |
| use rustc_ast::token::{self, Token}; | |
| use rustc_ast::tokenstream::TokenStream; | |
| use rustc_errors::{Applicability, Diag, DiagCtxtHandle, DiagMessage}; | |
| use rustc_macros::Subdiagnostic; | |
| use rustc_parse::parser::{Parser, Recovery, token_descr}; | |
| use rustc_session::parse::ParseSess; | |
| use rustc_span::source_map::SourceMap; | |
| use rustc_span::{DUMMY_SP, ErrorGuaranteed, Ident, Span}; | |
| use tracing::debug; | |
| use super::macro_rules::{MacroRule, NoopTracker, parser_from_cx}; | |
| use crate::expand::{AstFragmentKind, parse_ast_fragment}; | |
| use crate::mbe::macro_parser::ParseResult::*; | |
| use crate::mbe::macro_parser::{MatcherLoc, NamedParseResult, TtParser}; | |
| use crate::mbe::macro_rules::{ | |
| Tracker, try_match_macro, try_match_macro_attr, try_match_macro_derive, | |
| }; | |
| pub(super) enum FailedMacro<'a> { | |
| Func, | |
| Attr(&'a TokenStream), | |
| Derive, | |
| } | |
| pub(super) fn failed_to_match_macro( | |
| psess: &ParseSess, | |
| sp: Span, | |
| def_span: Span, | |
| name: Ident, | |
| args: FailedMacro<'_>, | |
| body: &TokenStream, | |
| rules: &[MacroRule], | |
| ) -> (Span, ErrorGuaranteed) { | |
| debug!("failed to match macro"); | |
| let def_head_span = if !def_span.is_dummy() && !psess.source_map().is_imported(def_span) { | |
| psess.source_map().guess_head_span(def_span) | |
| } else { | |
| DUMMY_SP | |
| }; | |
| // An error occurred, try the expansion again, tracking the expansion closely for better | |
| // diagnostics. | |
| let mut tracker = CollectTrackerAndEmitter::new(psess.dcx(), sp); | |
| let try_success_result = match args { | |
| FailedMacro::Func => try_match_macro(psess, name, body, rules, &mut tracker), | |
| FailedMacro::Attr(attr_args) => { | |
| try_match_macro_attr(psess, name, attr_args, body, rules, &mut tracker) | |
| } | |
| FailedMacro::Derive => try_match_macro_derive(psess, name, body, rules, &mut tracker), | |
| }; | |
| if try_success_result.is_ok() { | |
| // Nonterminal parser recovery might turn failed matches into successful ones, | |
| // but for that it must have emitted an error already | |
| assert!( | |
| tracker.dcx.has_errors().is_some(), | |
| "Macro matching returned a success on the second try" | |
| ); | |
| } | |
| if let Some(result) = tracker.result { | |
| // An irrecoverable error occurred and has been emitted. | |
| return result; | |
| } | |
| let Some(BestFailure { token, msg: label, remaining_matcher, .. }) = tracker.best_failure | |
| else { | |
| return (sp, psess.dcx().span_delayed_bug(sp, "failed to match a macro")); | |
| }; | |
| let span = token.span.substitute_dummy(sp); | |
| let mut err = psess.dcx().struct_span_err(span, parse_failure_msg(&token, None)); | |
| err.span_label(span, label); | |
| if !def_head_span.is_dummy() { | |
| err.span_label(def_head_span, "when calling this macro"); | |
| } | |
| annotate_doc_comment(&mut err, psess.source_map(), span); | |
| if let Some(span) = remaining_matcher.span() { | |
| err.span_note(span, format!("while trying to match {remaining_matcher}")); | |
| } else { | |
| err.note(format!("while trying to match {remaining_matcher}")); | |
| } | |
| if let MatcherLoc::Token { token: expected_token } = &remaining_matcher | |
| && (matches!(expected_token.kind, token::OpenInvisible(_)) | |
| || matches!(token.kind, token::OpenInvisible(_))) | |
| { | |
| err.note("captured metavariables except for `:tt`, `:ident` and `:lifetime` cannot be compared to other tokens"); | |
| err.note("see <https://doc.rust-lang.org/nightly/reference/macros-by-example.html#forwarding-a-matched-fragment> for more information"); | |
| if !def_span.is_dummy() && !psess.source_map().is_imported(def_span) { | |
| err.help("try using `:tt` instead in the macro definition"); | |
| } | |
| } | |
| // Check whether there's a missing comma in this macro call, like `println!("{}" a);` | |
| if let FailedMacro::Func = args | |
| && let Some((body, comma_span)) = body.add_comma() | |
| { | |
| for rule in rules { | |
| let MacroRule::Func { lhs, .. } = rule else { continue }; | |
| let parser = parser_from_cx(psess, body.clone(), Recovery::Allowed); | |
| let mut tt_parser = TtParser::new(name); | |
| if let Success(_) = | |
| tt_parser.parse_tt(&mut Cow::Borrowed(&parser), lhs, &mut NoopTracker) | |
| { | |
| if comma_span.is_dummy() { | |
| err.note("you might be missing a comma"); | |
| } else { | |
| err.span_suggestion_short( | |
| comma_span, | |
| "missing comma here", | |
| ", ", | |
| Applicability::MachineApplicable, | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| let guar = err.emit(); | |
| (sp, guar) | |
| } | |
| /// The tracker used for the slow error path that collects useful info for diagnostics. | |
| struct CollectTrackerAndEmitter<'dcx, 'matcher> { | |
| dcx: DiagCtxtHandle<'dcx>, | |
| remaining_matcher: Option<&'matcher MatcherLoc>, | |
| /// Which arm's failure should we report? (the one furthest along) | |
| best_failure: Option<BestFailure>, | |
| root_span: Span, | |
| result: Option<(Span, ErrorGuaranteed)>, | |
| } | |
| struct BestFailure { | |
| token: Token, | |
| position_in_tokenstream: (bool, u32), | |
| msg: &'static str, | |
| remaining_matcher: MatcherLoc, | |
| } | |
| impl BestFailure { | |
| fn is_better_position(&self, position: (bool, u32)) -> bool { | |
| position > self.position_in_tokenstream | |
| } | |
| } | |
| impl<'dcx, 'matcher> Tracker<'matcher> for CollectTrackerAndEmitter<'dcx, 'matcher> { | |
| type Failure = (Token, u32, &'static str); | |
| fn build_failure(tok: Token, position: u32, msg: &'static str) -> Self::Failure { | |
| (tok, position, msg) | |
| } | |
| fn before_match_loc(&mut self, parser: &TtParser, matcher: &'matcher MatcherLoc) { | |
| if self.remaining_matcher.is_none() | |
| || (parser.has_no_remaining_items_for_step() && *matcher != MatcherLoc::Eof) | |
| { | |
| self.remaining_matcher = Some(matcher); | |
| } | |
| } | |
| fn after_arm(&mut self, in_body: bool, result: &NamedParseResult<Self::Failure>) { | |
| match result { | |
| Success(_) => { | |
| // Nonterminal parser recovery might turn failed matches into successful ones, | |
| // but for that it must have emitted an error already | |
| self.dcx.span_delayed_bug( | |
| self.root_span, | |
| "should not collect detailed info for successful macro match", | |
| ); | |
| } | |
| Failure((token, approx_position, msg)) => { | |
| debug!(?token, ?msg, "a new failure of an arm"); | |
| let position_in_tokenstream = (in_body, *approx_position); | |
| if self | |
| .best_failure | |
| .as_ref() | |
| .is_none_or(|failure| failure.is_better_position(position_in_tokenstream)) | |
| { | |
| self.best_failure = Some(BestFailure { | |
| token: *token, | |
| position_in_tokenstream, | |
| msg, | |
| remaining_matcher: self | |
| .remaining_matcher | |
| .expect("must have collected matcher already") | |
| .clone(), | |
| }) | |
| } | |
| } | |
| Error(err_sp, msg) => { | |
| let span = err_sp.substitute_dummy(self.root_span); | |
| let guar = self.dcx.span_err(span, msg.clone()); | |
| self.result = Some((span, guar)); | |
| } | |
| ErrorReported(guar) => self.result = Some((self.root_span, *guar)), | |
| } | |
| } | |
| fn description() -> &'static str { | |
| "detailed" | |
| } | |
| fn recovery() -> Recovery { | |
| Recovery::Allowed | |
| } | |
| } | |
| impl<'dcx> CollectTrackerAndEmitter<'dcx, '_> { | |
| fn new(dcx: DiagCtxtHandle<'dcx>, root_span: Span) -> Self { | |
| Self { dcx, remaining_matcher: None, best_failure: None, root_span, result: None } | |
| } | |
| } | |
| pub(super) fn emit_frag_parse_err( | |
| mut e: Diag<'_>, | |
| parser: &Parser<'_>, | |
| orig_parser: &mut Parser<'_>, | |
| site_span: Span, | |
| arm_span: Span, | |
| kind: AstFragmentKind, | |
| ) -> ErrorGuaranteed { | |
| // FIXME(davidtwco): avoid depending on the error message text | |
| if parser.token == token::Eof | |
| && let DiagMessage::Str(message) = &e.messages[0].0 | |
| && message.ends_with(", found `<eof>`") | |
| { | |
| let msg = &e.messages[0]; | |
| e.messages[0] = ( | |
| DiagMessage::from(format!( | |
| "macro expansion ends with an incomplete expression: {}", | |
| message.replace(", found `<eof>`", ""), | |
| )), | |
| msg.1, | |
| ); | |
| if !e.span.is_dummy() { | |
| // early end of macro arm (#52866) | |
| e.replace_span_with(parser.token.span.shrink_to_hi(), true); | |
| } | |
| } | |
| if e.span.is_dummy() { | |
| // Get around lack of span in error (#30128) | |
| e.replace_span_with(site_span, true); | |
| if !parser.psess.source_map().is_imported(arm_span) { | |
| e.span_label(arm_span, "in this macro arm"); | |
| } | |
| } else if parser.psess.source_map().is_imported(parser.token.span) { | |
| e.span_label(site_span, "in this macro invocation"); | |
| } | |
| match kind { | |
| // Try a statement if an expression is wanted but failed and suggest adding `;` to call. | |
| AstFragmentKind::Expr => match parse_ast_fragment(orig_parser, AstFragmentKind::Stmts) { | |
| Err(err) => err.cancel(), | |
| Ok(_) => { | |
| e.note( | |
| "the macro call doesn't expand to an expression, but it can expand to a statement", | |
| ); | |
| if parser.token == token::Semi { | |
| if let Ok(snippet) = parser.psess.source_map().span_to_snippet(site_span) { | |
| e.span_suggestion_verbose( | |
| site_span, | |
| "surround the macro invocation with `{}` to interpret the expansion as a statement", | |
| format!("{{ {snippet}; }}"), | |
| Applicability::MaybeIncorrect, | |
| ); | |
| } | |
| } else { | |
| e.span_suggestion_verbose( | |
| site_span.shrink_to_hi(), | |
| "add `;` to interpret the expansion as a statement", | |
| ";", | |
| Applicability::MaybeIncorrect, | |
| ); | |
| } | |
| } | |
| }, | |
| _ => annotate_err_with_kind(&mut e, kind, site_span), | |
| }; | |
| e.emit() | |
| } | |
| pub(crate) fn annotate_err_with_kind(err: &mut Diag<'_>, kind: AstFragmentKind, span: Span) { | |
| match kind { | |
| AstFragmentKind::Ty => { | |
| err.span_label(span, "this macro call doesn't expand to a type"); | |
| } | |
| AstFragmentKind::Pat => { | |
| err.span_label(span, "this macro call doesn't expand to a pattern"); | |
| } | |
| _ => {} | |
| }; | |
| } | |
| #[derive(Subdiagnostic)] | |
| enum ExplainDocComment { | |
| #[label( | |
| "inner doc comments expand to `#![doc = \"...\"]`, which is what this macro attempted to match" | |
| )] | |
| Inner { | |
| #[primary_span] | |
| span: Span, | |
| }, | |
| #[label( | |
| "outer doc comments expand to `#[doc = \"...\"]`, which is what this macro attempted to match" | |
| )] | |
| Outer { | |
| #[primary_span] | |
| span: Span, | |
| }, | |
| } | |
| fn annotate_doc_comment(err: &mut Diag<'_>, sm: &SourceMap, span: Span) { | |
| if let Ok(src) = sm.span_to_snippet(span) { | |
| if src.starts_with("///") || src.starts_with("/**") { | |
| err.subdiagnostic(ExplainDocComment::Outer { span }); | |
| } else if src.starts_with("//!") || src.starts_with("/*!") { | |
| err.subdiagnostic(ExplainDocComment::Inner { span }); | |
| } | |
| } | |
| } | |
| /// Generates an appropriate parsing failure message. For EOF, this is "unexpected end...". For | |
| /// other tokens, this is "unexpected token...". | |
| fn parse_failure_msg(tok: &Token, expected_token: Option<&Token>) -> Cow<'static, str> { | |
| if let Some(expected_token) = expected_token { | |
| Cow::from(format!("expected {}, found {}", token_descr(expected_token), token_descr(tok))) | |
| } else { | |
| match tok.kind { | |
| token::Eof => Cow::from("unexpected end of macro invocation"), | |
| _ => Cow::from(format!("no rules expected {}", token_descr(tok))), | |
| } | |
| } | |
| } |