| use std::ops::Range; |
| |
| use rustc_errors::E0232; |
| use rustc_hir::AttrPath; |
| use rustc_hir::attrs::diagnostic::{ |
| Directive, FilterFormatString, Flag, FormatArg, FormatString, LitOrArg, Name, NameValue, |
| OnUnimplementedCondition, Piece, Predicate, |
| }; |
| use rustc_hir::lints::{AttributeLintKind, FormatWarning}; |
| use rustc_macros::Diagnostic; |
| use rustc_parse_format::{ |
| Argument, FormatSpec, ParseError, ParseMode, Parser, Piece as RpfPiece, Position, |
| }; |
| use rustc_session::lint::builtin::{ |
| MALFORMED_DIAGNOSTIC_ATTRIBUTES, MALFORMED_DIAGNOSTIC_FORMAT_LITERALS, |
| }; |
| use rustc_span::{Ident, InnerSpan, Span, Symbol, kw, sym}; |
| use thin_vec::{ThinVec, thin_vec}; |
| |
| use crate::context::{AcceptContext, Stage}; |
| use crate::parser::{ArgParser, MetaItemListParser, MetaItemOrLitParser, MetaItemParser}; |
| |
| pub(crate) mod do_not_recommend; |
| pub(crate) mod on_const; |
| pub(crate) mod on_move; |
| pub(crate) mod on_unimplemented; |
| pub(crate) mod on_unknown; |
| |
| #[derive(Copy, Clone)] |
| pub(crate) enum Mode { |
| /// `#[rustc_on_unimplemented]` |
| RustcOnUnimplemented, |
| /// `#[diagnostic::on_unimplemented]` |
| DiagnosticOnUnimplemented, |
| /// `#[diagnostic::on_const]` |
| DiagnosticOnConst, |
| /// `#[diagnostic::on_move]` |
| DiagnosticOnMove, |
| /// `#[diagnostic::on_unknown]` |
| DiagnosticOnUnknown, |
| } |
| |
| fn merge_directives<S: Stage>( |
| cx: &mut AcceptContext<'_, '_, S>, |
| first: &mut Option<(Span, Directive)>, |
| later: (Span, Directive), |
| ) { |
| if let Some((_, first)) = first { |
| if first.is_rustc_attr || later.1.is_rustc_attr { |
| cx.emit_err(DupesNotAllowed); |
| } |
| |
| merge(cx, &mut first.message, later.1.message, sym::message); |
| merge(cx, &mut first.label, later.1.label, sym::label); |
| first.notes.extend(later.1.notes); |
| } else { |
| *first = Some(later); |
| } |
| } |
| |
| fn merge<T, S: Stage>( |
| cx: &mut AcceptContext<'_, '_, S>, |
| first: &mut Option<(Span, T)>, |
| later: Option<(Span, T)>, |
| option_name: Symbol, |
| ) { |
| match (first, later) { |
| (Some(_) | None, None) => {} |
| (Some((first_span, _)), Some((later_span, _))) => { |
| cx.emit_lint( |
| MALFORMED_DIAGNOSTIC_ATTRIBUTES, |
| AttributeLintKind::IgnoredDiagnosticOption { |
| first_span: *first_span, |
| later_span, |
| option_name, |
| }, |
| later_span, |
| ); |
| } |
| (first @ None, Some(later)) => { |
| first.get_or_insert(later); |
| } |
| } |
| } |
| |
| fn parse_directive_items<'p, S: Stage>( |
| cx: &mut AcceptContext<'_, '_, S>, |
| mode: Mode, |
| items: impl Iterator<Item = &'p MetaItemOrLitParser>, |
| is_root: bool, |
| ) -> Option<Directive> { |
| let condition = None; |
| let mut message: Option<(Span, _)> = None; |
| let mut label: Option<(Span, _)> = None; |
| let mut notes = ThinVec::new(); |
| let mut parent_label = None; |
| let mut subcommands = ThinVec::new(); |
| |
| for item in items { |
| let span = item.span(); |
| |
| macro malformed() {{ |
| match mode { |
| Mode::RustcOnUnimplemented => { |
| cx.emit_err(NoValueInOnUnimplemented { span: item.span() }); |
| } |
| Mode::DiagnosticOnUnimplemented => { |
| cx.emit_lint( |
| MALFORMED_DIAGNOSTIC_ATTRIBUTES, |
| AttributeLintKind::MalformedOnUnimplementedAttr { span }, |
| span, |
| ); |
| } |
| Mode::DiagnosticOnConst => { |
| cx.emit_lint( |
| MALFORMED_DIAGNOSTIC_ATTRIBUTES, |
| AttributeLintKind::MalformedOnConstAttr { span }, |
| span, |
| ); |
| } |
| Mode::DiagnosticOnMove => { |
| cx.emit_lint( |
| MALFORMED_DIAGNOSTIC_ATTRIBUTES, |
| AttributeLintKind::MalformedOnMoveAttr { span }, |
| span, |
| ); |
| } |
| Mode::DiagnosticOnUnknown => { |
| cx.emit_lint( |
| MALFORMED_DIAGNOSTIC_ATTRIBUTES, |
| AttributeLintKind::MalformedOnUnknownAttr { span }, |
| span, |
| ); |
| } |
| } |
| continue; |
| }} |
| |
| macro or_malformed($($code:tt)*) {{ |
| let Some(ret) = (||{ |
| Some($($code)*) |
| })() else { |
| malformed!() |
| }; |
| ret |
| }} |
| |
| macro duplicate($name: ident, $($first_span:tt)*) {{ |
| match mode { |
| Mode::RustcOnUnimplemented => { |
| cx.emit_err(NoValueInOnUnimplemented { span: item.span() }); |
| } |
| Mode::DiagnosticOnUnimplemented |Mode::DiagnosticOnConst | Mode::DiagnosticOnMove | Mode::DiagnosticOnUnknown => { |
| cx.emit_lint( |
| MALFORMED_DIAGNOSTIC_ATTRIBUTES, |
| AttributeLintKind::IgnoredDiagnosticOption { |
| first_span: $($first_span)*, |
| later_span: span, |
| option_name: $name, |
| }, |
| span, |
| ); |
| } |
| } |
| }} |
| |
| let item: &MetaItemParser = or_malformed!(item.meta_item()?); |
| let name = or_malformed!(item.ident()?).name; |
| |
| // Currently, as of April 2026, all arguments of all diagnostic attrs |
| // must have a value, like `message = "message"`. Thus in a well-formed |
| // diagnostic attribute this is never `None`. |
| // |
| // But we don't assert its presence yet because we don't want to mention it |
| // if someone does something like `#[diagnostic::on_unimplemented(doesnt_exist)]`. |
| // That happens in the big `match` below. |
| let value: Option<Ident> = match item.args().name_value() { |
| Some(nv) => Some(or_malformed!(nv.value_as_ident()?)), |
| None => None, |
| }; |
| |
| let mut parse_format = |input: Ident| { |
| let snippet = cx.sess.source_map().span_to_snippet(input.span).ok(); |
| let is_snippet = snippet.is_some(); |
| match parse_format_string(input.name, snippet, input.span, mode) { |
| Ok((f, warnings)) => { |
| for warning in warnings { |
| let (FormatWarning::InvalidSpecifier { span, .. } |
| | FormatWarning::PositionalArgument { span, .. } |
| | FormatWarning::DisallowedPlaceholder { span }) = warning; |
| cx.emit_lint( |
| MALFORMED_DIAGNOSTIC_FORMAT_LITERALS, |
| AttributeLintKind::MalformedDiagnosticFormat { warning }, |
| span, |
| ); |
| } |
| |
| f |
| } |
| Err(e) => { |
| cx.emit_lint( |
| MALFORMED_DIAGNOSTIC_FORMAT_LITERALS, |
| AttributeLintKind::DiagnosticWrappedParserError { |
| description: e.description, |
| label: e.label, |
| span: slice_span(input.span, e.span, is_snippet), |
| }, |
| input.span, |
| ); |
| // We could not parse the input, just use it as-is. |
| FormatString { |
| input: input.name, |
| span: input.span, |
| pieces: thin_vec![Piece::Lit(input.name)], |
| } |
| } |
| } |
| }; |
| match (mode, name) { |
| (_, sym::message) => { |
| let value = or_malformed!(value?); |
| if let Some(message) = &message { |
| duplicate!(name, message.0) |
| } else { |
| message = Some((item.span(), parse_format(value))); |
| } |
| } |
| (_, sym::label) => { |
| let value = or_malformed!(value?); |
| if let Some(label) = &label { |
| duplicate!(name, label.0) |
| } else { |
| label = Some((item.span(), parse_format(value))); |
| } |
| } |
| (_, sym::note) => { |
| let value = or_malformed!(value?); |
| notes.push(parse_format(value)) |
| } |
| (Mode::RustcOnUnimplemented, sym::parent_label) => { |
| let value = or_malformed!(value?); |
| if parent_label.is_none() { |
| parent_label = Some(parse_format(value)); |
| } else { |
| duplicate!(name, span) |
| } |
| } |
| (Mode::RustcOnUnimplemented, sym::on) => { |
| if is_root { |
| let items = or_malformed!(item.args().list()?); |
| let mut iter = items.mixed(); |
| let condition: &MetaItemOrLitParser = match iter.next() { |
| Some(c) => c, |
| None => { |
| cx.emit_err(InvalidOnClause::Empty { span }); |
| continue; |
| } |
| }; |
| |
| let condition = parse_condition(condition); |
| |
| if items.len() < 2 { |
| // Something like `#[rustc_on_unimplemented(on(.., /* nothing */))]` |
| // There's a condition but no directive behind it, this is a mistake. |
| malformed!(); |
| } |
| |
| let mut directive = |
| or_malformed!(parse_directive_items(cx, mode, iter, false)?); |
| |
| match condition { |
| Ok(c) => { |
| directive.condition = Some(c); |
| subcommands.push(directive); |
| } |
| Err(e) => { |
| cx.emit_err(e); |
| } |
| } |
| } else { |
| malformed!(); |
| } |
| } |
| |
| _other => { |
| malformed!(); |
| } |
| } |
| } |
| |
| Some(Directive { |
| is_rustc_attr: matches!(mode, Mode::RustcOnUnimplemented), |
| condition, |
| subcommands, |
| message, |
| label, |
| notes, |
| parent_label, |
| }) |
| } |
| |
| pub(crate) fn parse_format_string( |
| input: Symbol, |
| snippet: Option<String>, |
| span: Span, |
| mode: Mode, |
| ) -> Result<(FormatString, Vec<FormatWarning>), ParseError> { |
| let s = input.as_str(); |
| let mut parser = Parser::new(s, None, snippet, false, ParseMode::Diagnostic); |
| let pieces: Vec<_> = parser.by_ref().collect(); |
| |
| if let Some(err) = parser.errors.into_iter().next() { |
| return Err(err); |
| } |
| let mut warnings = Vec::new(); |
| |
| let pieces = pieces |
| .into_iter() |
| .map(|piece| match piece { |
| RpfPiece::Lit(lit) => Piece::Lit(Symbol::intern(lit)), |
| RpfPiece::NextArgument(arg) => { |
| warn_on_format_spec(&arg.format, &mut warnings, span, parser.is_source_literal); |
| let arg = parse_arg(&arg, mode, &mut warnings, span, parser.is_source_literal); |
| Piece::Arg(arg) |
| } |
| }) |
| .collect(); |
| |
| Ok((FormatString { input, pieces, span }, warnings)) |
| } |
| |
| fn parse_arg( |
| arg: &Argument<'_>, |
| mode: Mode, |
| warnings: &mut Vec<FormatWarning>, |
| input_span: Span, |
| is_source_literal: bool, |
| ) -> FormatArg { |
| let span = slice_span(input_span, arg.position_span.clone(), is_source_literal); |
| if matches!(mode, Mode::DiagnosticOnUnknown) { |
| warnings.push(FormatWarning::DisallowedPlaceholder { span }); |
| return FormatArg::AsIs(sym::empty_braces); |
| } |
| |
| match arg.position { |
| // Something like "hello {name}" |
| Position::ArgumentNamed(name) => match (mode, Symbol::intern(name)) { |
| // Only `#[rustc_on_unimplemented]` can use these |
| (Mode::RustcOnUnimplemented { .. }, sym::ItemContext) => FormatArg::ItemContext, |
| (Mode::RustcOnUnimplemented { .. }, sym::This) => FormatArg::This, |
| (Mode::RustcOnUnimplemented { .. }, sym::Trait) => FormatArg::Trait, |
| // Any attribute can use these |
| (_, kw::SelfUpper) => FormatArg::SelfUpper, |
| (_, generic_param) => FormatArg::GenericParam { generic_param, span }, |
| }, |
| |
| // `{:1}` and `{}` are ignored |
| Position::ArgumentIs(idx) => { |
| warnings.push(FormatWarning::PositionalArgument { |
| span, |
| help: format!("use `{{{idx}}}` to print a number in braces"), |
| }); |
| FormatArg::AsIs(Symbol::intern(&format!("{{{idx}}}"))) |
| } |
| Position::ArgumentImplicitlyIs(_) => { |
| warnings.push(FormatWarning::PositionalArgument { |
| span, |
| help: String::from("use `{{}}` to print empty braces"), |
| }); |
| FormatArg::AsIs(sym::empty_braces) |
| } |
| } |
| } |
| |
| /// `#[rustc_on_unimplemented]` and `#[diagnostic::...]` don't actually do anything |
| /// with specifiers, so emit a warning if they are used. |
| fn warn_on_format_spec( |
| spec: &FormatSpec<'_>, |
| warnings: &mut Vec<FormatWarning>, |
| input_span: Span, |
| is_source_literal: bool, |
| ) { |
| if spec.ty != "" { |
| let span = spec |
| .ty_span |
| .as_ref() |
| .map(|inner| slice_span(input_span, inner.clone(), is_source_literal)) |
| .unwrap_or(input_span); |
| warnings.push(FormatWarning::InvalidSpecifier { span, name: spec.ty.into() }) |
| } |
| } |
| |
| fn slice_span(input: Span, Range { start, end }: Range<usize>, is_source_literal: bool) -> Span { |
| if is_source_literal { input.from_inner(InnerSpan { start, end }) } else { input } |
| } |
| |
| pub(crate) fn parse_condition( |
| input: &MetaItemOrLitParser, |
| ) -> Result<OnUnimplementedCondition, InvalidOnClause> { |
| let span = input.span(); |
| let pred = parse_predicate(input)?; |
| Ok(OnUnimplementedCondition { span, pred }) |
| } |
| |
| fn parse_predicate(input: &MetaItemOrLitParser) -> Result<Predicate, InvalidOnClause> { |
| let Some(meta_item) = input.meta_item() else { |
| return Err(InvalidOnClause::UnsupportedLiteral { span: input.span() }); |
| }; |
| |
| let Some(predicate) = meta_item.ident() else { |
| return Err(InvalidOnClause::ExpectedIdentifier { |
| span: meta_item.path().span(), |
| path: meta_item.path().get_attribute_path(), |
| }); |
| }; |
| |
| match meta_item.args() { |
| ArgParser::List(mis) => match predicate.name { |
| sym::any => Ok(Predicate::Any(parse_predicate_sequence(mis)?)), |
| sym::all => Ok(Predicate::All(parse_predicate_sequence(mis)?)), |
| sym::not => { |
| if let Some(single) = mis.single() { |
| Ok(Predicate::Not(Box::new(parse_predicate(single)?))) |
| } else { |
| Err(InvalidOnClause::ExpectedOnePredInNot { span: mis.span }) |
| } |
| } |
| invalid_pred => { |
| Err(InvalidOnClause::InvalidPredicate { span: predicate.span, invalid_pred }) |
| } |
| }, |
| ArgParser::NameValue(p) => { |
| let Some(value) = p.value_as_ident() else { |
| return Err(InvalidOnClause::UnsupportedLiteral { span: p.args_span() }); |
| }; |
| let name = parse_name(predicate.name); |
| let value = parse_filter(value.name); |
| let kv = NameValue { name, value }; |
| Ok(Predicate::Match(kv)) |
| } |
| ArgParser::NoArgs => { |
| let flag = parse_flag(predicate)?; |
| Ok(Predicate::Flag(flag)) |
| } |
| } |
| } |
| |
| fn parse_predicate_sequence( |
| sequence: &MetaItemListParser, |
| ) -> Result<ThinVec<Predicate>, InvalidOnClause> { |
| sequence.mixed().map(parse_predicate).collect() |
| } |
| |
| fn parse_flag(Ident { name, span }: Ident) -> Result<Flag, InvalidOnClause> { |
| match name { |
| sym::crate_local => Ok(Flag::CrateLocal), |
| sym::direct => Ok(Flag::Direct), |
| sym::from_desugaring => Ok(Flag::FromDesugaring), |
| invalid_flag => Err(InvalidOnClause::InvalidFlag { invalid_flag, span }), |
| } |
| } |
| |
| fn parse_name(name: Symbol) -> Name { |
| match name { |
| kw::SelfUpper => Name::SelfUpper, |
| sym::from_desugaring => Name::FromDesugaring, |
| sym::cause => Name::Cause, |
| generic => Name::GenericArg(generic), |
| } |
| } |
| |
| fn parse_filter(input: Symbol) -> FilterFormatString { |
| let pieces = Parser::new(input.as_str(), None, None, false, ParseMode::Diagnostic) |
| .map(|p| match p { |
| RpfPiece::Lit(s) => LitOrArg::Lit(Symbol::intern(s)), |
| // We just ignore formatspecs here |
| RpfPiece::NextArgument(a) => match a.position { |
| // In `TypeErrCtxt::on_unimplemented_note` we substitute `"{integral}"` even |
| // if the integer type has been resolved, to allow targeting all integers. |
| // `"{integer}"` and `"{float}"` come from numerics that haven't been inferred yet, |
| // from the `Display` impl of `InferTy` to be precise. |
| // `"{union|enum|struct}"` is used as a special selector for ADTs. |
| // |
| // Don't try to format these later! |
| Position::ArgumentNamed( |
| arg @ ("integer" | "integral" | "float" | "union" | "enum" | "struct"), |
| ) => LitOrArg::Lit(Symbol::intern(&format!("{{{arg}}}"))), |
| |
| Position::ArgumentNamed(arg) => LitOrArg::Arg(Symbol::intern(arg)), |
| Position::ArgumentImplicitlyIs(_) => LitOrArg::Lit(sym::empty_braces), |
| Position::ArgumentIs(idx) => LitOrArg::Lit(Symbol::intern(&format!("{{{idx}}}"))), |
| }, |
| }) |
| .collect(); |
| FilterFormatString { pieces } |
| } |
| |
| #[derive(Diagnostic)] |
| pub(crate) enum InvalidOnClause { |
| #[diag("empty `on`-clause in `#[rustc_on_unimplemented]`", code = E0232)] |
| Empty { |
| #[primary_span] |
| #[label("empty `on`-clause here")] |
| span: Span, |
| }, |
| #[diag("expected a single predicate in `not(..)`", code = E0232)] |
| ExpectedOnePredInNot { |
| #[primary_span] |
| #[label("unexpected quantity of predicates here")] |
| span: Span, |
| }, |
| #[diag("literals inside `on`-clauses are not supported", code = E0232)] |
| UnsupportedLiteral { |
| #[primary_span] |
| #[label("unexpected literal here")] |
| span: Span, |
| }, |
| #[diag("expected an identifier inside this `on`-clause", code = E0232)] |
| ExpectedIdentifier { |
| #[primary_span] |
| #[label("expected an identifier here, not `{$path}`")] |
| span: Span, |
| path: AttrPath, |
| }, |
| #[diag("this predicate is invalid", code = E0232)] |
| InvalidPredicate { |
| #[primary_span] |
| #[label("expected one of `any`, `all` or `not` here, not `{$invalid_pred}`")] |
| span: Span, |
| invalid_pred: Symbol, |
| }, |
| #[diag("invalid flag in `on`-clause", code = E0232)] |
| InvalidFlag { |
| #[primary_span] |
| #[label( |
| "expected one of the `crate_local`, `direct` or `from_desugaring` flags, not `{$invalid_flag}`" |
| )] |
| span: Span, |
| invalid_flag: Symbol, |
| }, |
| } |
| |
| #[derive(Diagnostic)] |
| #[diag("this attribute must have a value", code = E0232)] |
| #[note("e.g. `#[rustc_on_unimplemented(message=\"foo\")]`")] |
| pub(crate) struct NoValueInOnUnimplemented { |
| #[primary_span] |
| #[label("expected value here")] |
| pub span: Span, |
| } |
| |
| #[derive(Diagnostic)] |
| #[diag( |
| "using multiple `rustc_on_unimplemented` (or mixing it with `diagnostic::on_unimplemented`) is not supported" |
| )] |
| pub(crate) struct DupesNotAllowed; |