| use std::ops::Range; |
| |
| use rustc_errors::E0232; |
| use rustc_hir::AttrPath; |
| use rustc_hir::attrs::diagnostic::{ |
| Directive, Filter, FilterFormatString, Flag, FormatArg, FormatString, LitOrArg, Name, |
| NameValue, Piece, Predicate, |
| }; |
| 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; |
| use crate::errors::{ |
| FormatWarning, IgnoredDiagnosticOption, MalFormedDiagnosticAttributeLint, |
| MissingOptionsForDiagnosticAttribute, NonMetaItemDiagnosticAttribute, WrappedParserError, |
| }; |
| 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; |
| pub(crate) mod on_unmatch_args; |
| |
| #[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, |
| /// `#[diagnostic::on_unmatch_args]` |
| DiagnosticOnUnmatchArgs, |
| } |
| |
| impl Mode { |
| fn as_str(&self) -> &'static str { |
| match self { |
| Self::RustcOnUnimplemented => "rustc_on_unimplemented", |
| Self::DiagnosticOnUnimplemented => "diagnostic::on_unimplemented", |
| Self::DiagnosticOnConst => "diagnostic::on_const", |
| Self::DiagnosticOnMove => "diagnostic::on_move", |
| Self::DiagnosticOnUnknown => "diagnostic::on_unknown", |
| Self::DiagnosticOnUnmatchArgs => "diagnostic::on_unmatch_args", |
| } |
| } |
| |
| fn expected_options(&self) -> &'static str { |
| const DEFAULT: &str = |
| "at least one of the `message`, `note` and `label` options are expected"; |
| match self { |
| Self::RustcOnUnimplemented => { |
| "see <https://rustc-dev-guide.rust-lang.org/diagnostics.html#rustc_on_unimplemented>" |
| } |
| Self::DiagnosticOnUnimplemented => DEFAULT, |
| Self::DiagnosticOnConst => DEFAULT, |
| Self::DiagnosticOnMove => DEFAULT, |
| Self::DiagnosticOnUnknown => DEFAULT, |
| Self::DiagnosticOnUnmatchArgs => DEFAULT, |
| } |
| } |
| |
| fn allowed_options(&self) -> &'static str { |
| const DEFAULT: &str = "only `message`, `note` and `label` are allowed as options"; |
| match self { |
| Self::RustcOnUnimplemented => { |
| "see <https://rustc-dev-guide.rust-lang.org/diagnostics.html#rustc_on_unimplemented>" |
| } |
| Self::DiagnosticOnUnimplemented => DEFAULT, |
| Self::DiagnosticOnConst => DEFAULT, |
| Self::DiagnosticOnMove => DEFAULT, |
| Self::DiagnosticOnUnknown => DEFAULT, |
| Self::DiagnosticOnUnmatchArgs => DEFAULT, |
| } |
| } |
| |
| fn allowed_format_arguments(&self) -> &'static str { |
| match self { |
| Self::RustcOnUnimplemented => { |
| "see <https://rustc-dev-guide.rust-lang.org/diagnostics.html#rustc_on_unimplemented> for allowed format arguments" |
| } |
| Self::DiagnosticOnUnimplemented => { |
| "only `Self` and generics of the trait are allowed as a format argument" |
| } |
| Self::DiagnosticOnConst => { |
| "only `Self` and generics of the implementation are allowed as a format argument" |
| } |
| Self::DiagnosticOnMove => { |
| "only `This`, `Self` and generics of the type are allowed as a format argument" |
| } |
| Self::DiagnosticOnUnknown => { |
| "only `This` is allowed as a format argument, referring to the failed import" |
| } |
| Self::DiagnosticOnUnmatchArgs => { |
| "only `This` is allowed as a format argument, referring to the macro's name" |
| } |
| } |
| } |
| } |
| |
| fn merge_directives( |
| cx: &mut AcceptContext<'_, '_>, |
| 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>( |
| cx: &mut AcceptContext<'_, '_>, |
| first: &mut Option<(Span, T)>, |
| later: Option<(Span, T)>, |
| option_name: Symbol, |
| ) { |
| match (first, later) { |
| (Some(_) | None, None) => {} |
| (Some((first_span, _)), Some((later_span, _))) => { |
| let first_span = *first_span; |
| cx.emit_lint( |
| MALFORMED_DIAGNOSTIC_ATTRIBUTES, |
| IgnoredDiagnosticOption { first_span, later_span, option_name }, |
| later_span, |
| ); |
| } |
| (first @ None, Some(later)) => { |
| first.get_or_insert(later); |
| } |
| } |
| } |
| |
| fn parse_list<'p>( |
| cx: &mut AcceptContext<'_, '_>, |
| args: &'p ArgParser, |
| mode: Mode, |
| ) -> Option<&'p MetaItemListParser> { |
| let span = cx.attr_span; |
| match args { |
| ArgParser::List(items) if items.len() != 0 => return Some(items), |
| ArgParser::List(list) => { |
| // We're dealing with `#[diagnostic::attr()]`. |
| // This can be because that is what the user typed, but that's also what we'd see |
| // if the user used non-metaitem syntax. See `ArgParser::from_attr_args`. |
| cx.emit_lint( |
| MALFORMED_DIAGNOSTIC_ATTRIBUTES, |
| NonMetaItemDiagnosticAttribute, |
| list.span, |
| ); |
| } |
| ArgParser::NoArgs => { |
| cx.emit_lint( |
| MALFORMED_DIAGNOSTIC_ATTRIBUTES, |
| MissingOptionsForDiagnosticAttribute { |
| attribute: mode.as_str(), |
| options: mode.expected_options(), |
| }, |
| span, |
| ); |
| } |
| ArgParser::NameValue(_) => { |
| cx.emit_lint( |
| MALFORMED_DIAGNOSTIC_ATTRIBUTES, |
| MalFormedDiagnosticAttributeLint { |
| attribute: mode.as_str(), |
| options: mode.allowed_options(), |
| span, |
| }, |
| span, |
| ); |
| } |
| } |
| None |
| } |
| |
| fn parse_directive_items<'p>( |
| cx: &mut AcceptContext<'_, '_>, |
| mode: Mode, |
| items: impl Iterator<Item = &'p MetaItemOrLitParser>, |
| is_root: bool, |
| ) -> Option<Directive> { |
| let mut message: Option<(Span, _)> = None; |
| let mut label: Option<(Span, _)> = None; |
| let mut notes = ThinVec::new(); |
| let mut parent_label = None; |
| let mut filters = ThinVec::new(); |
| |
| for item in items { |
| let span = item.span(); |
| |
| macro malformed() {{ |
| cx.emit_lint( |
| MALFORMED_DIAGNOSTIC_ATTRIBUTES, |
| MalFormedDiagnosticAttributeLint { |
| attribute: mode.as_str(), |
| options: mode.allowed_options(), |
| span, |
| }, |
| span, |
| ); |
| continue; |
| }} |
| |
| macro or_malformed($($code:tt)*) {{ |
| let Some(ret) = ( |
| try { |
| $($code)* |
| } |
| ) else { |
| malformed!() |
| }; |
| ret |
| }} |
| |
| macro duplicate($name: ident, $($first_span:tt)*) {{ |
| let first_span = $($first_span)*; |
| cx.emit_lint( |
| MALFORMED_DIAGNOSTIC_ATTRIBUTES, |
| IgnoredDiagnosticOption { |
| 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().as_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::IndexedArgument { span } |
| | FormatWarning::DisallowedPlaceholder { span, .. }) = warning; |
| cx.emit_lint(MALFORMED_DIAGNOSTIC_FORMAT_LITERALS, warning, span); |
| } |
| |
| f |
| } |
| Err(e) => { |
| cx.emit_lint( |
| MALFORMED_DIAGNOSTIC_FORMAT_LITERALS, |
| WrappedParserError { |
| description: e.description, |
| label: e.label, |
| span: slice_span(input.span, e.span.clone(), 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().as_list()?); |
| let mut iter = items.mixed(); |
| let filter: &MetaItemOrLitParser = match iter.next() { |
| Some(c) => c, |
| None => { |
| cx.emit_err(InvalidOnClause::Empty { span }); |
| continue; |
| } |
| }; |
| |
| let filter = parse_filter(filter); |
| |
| if items.len() < 2 { |
| // Something like `#[rustc_on_unimplemented(on(.., /* nothing */))]` |
| // There's a filter but no directive behind it, this is a mistake. |
| malformed!(); |
| } |
| |
| match filter { |
| Ok(filter) => { |
| let directive = |
| or_malformed!(parse_directive_items(cx, mode, iter, false)?); |
| filters.push((filter, directive)); |
| } |
| Err(e) => { |
| cx.emit_err(e); |
| } |
| } |
| } else { |
| malformed!(); |
| } |
| } |
| |
| _other => { |
| malformed!(); |
| } |
| } |
| } |
| |
| Some(Directive { |
| is_rustc_attr: matches!(mode, Mode::RustcOnUnimplemented), |
| filters, |
| 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) => { |
| Piece::Arg(parse_arg(&arg, mode, &mut warnings, span, parser.is_source_literal)) |
| } |
| }) |
| .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); |
| |
| let mut check_format = true; |
| |
| let ret = match arg.position { |
| // Something like "hello {name}" |
| Position::ArgumentNamed(name) => match (mode, Symbol::intern(name)) { |
| (Mode::RustcOnUnimplemented, sym::ItemContext) => FormatArg::ItemContext, |
| |
| // `{This:ty}` |
| (Mode::RustcOnUnimplemented, sym::This) => match arg.format.ty { |
| "resolved" => { |
| check_format = false; |
| FormatArg::ThisResolved |
| } |
| "path" => { |
| check_format = false; |
| FormatArg::ThisPath |
| } |
| _ => FormatArg::This, |
| }, |
| |
| // Some diagnostic attributes can use `{This}` to refer to the annotated item. |
| // For those that don't, we continue and maybe use it as a generic parameter. |
| // |
| // FIXME(mejrs) `DiagnosticOnUnimplemented` is intentionally not here; |
| // that requires lang approval which is best kept for a standalone PR. |
| ( |
| Mode::DiagnosticOnUnknown | Mode::DiagnosticOnMove | Mode::DiagnosticOnUnmatchArgs, |
| sym::This, |
| ) => FormatArg::This, |
| |
| // `{Self}`; the self type. |
| // - For trait declaration attributes that's the type that does not implement it. |
| // - for trait impl attributes, the implemented for type. |
| // - For ADT attributes, that's the type (which will be identical to `{This}`) |
| // - For everything else it doesn't make sense. |
| ( |
| Mode::RustcOnUnimplemented |
| | Mode::DiagnosticOnUnimplemented |
| | Mode::DiagnosticOnMove |
| | Mode::DiagnosticOnConst, |
| kw::SelfUpper, |
| ) => FormatArg::SelfUpper, |
| |
| // Generic parameters. |
| // FIXME(mejrs) unfortunately, all the "special" symbols above might fall through, |
| // but at this time we are not aware of what generic parameters the trait actually has. |
| // If we find `ItemContext` or something we have to assume that's a generic parameter. |
| // We lint against that in `check_attr.rs` though. |
| ( |
| Mode::RustcOnUnimplemented |
| | Mode::DiagnosticOnUnimplemented |
| | Mode::DiagnosticOnMove |
| | Mode::DiagnosticOnConst, |
| generic_param, |
| ) => FormatArg::GenericParam { generic_param, span }, |
| |
| // Generics are explicitly not allowed, we print those back as is. |
| (Mode::DiagnosticOnUnknown | Mode::DiagnosticOnUnmatchArgs, as_is) => { |
| warnings.push(FormatWarning::DisallowedPlaceholder { |
| span, |
| attr: mode.as_str(), |
| allowed: mode.allowed_format_arguments(), |
| }); |
| FormatArg::AsIs(Symbol::intern(&format!("{{{as_is}}}"))) |
| } |
| }, |
| |
| // `{1}` and `{}` are ignored |
| Position::ArgumentIs(idx) => { |
| warnings.push(FormatWarning::IndexedArgument { span }); |
| FormatArg::AsIs(Symbol::intern(&format!("{{{idx}}}"))) |
| } |
| Position::ArgumentImplicitlyIs(_) => { |
| warnings.push(FormatWarning::PositionalArgument { span }); |
| FormatArg::AsIs(sym::empty_braces) |
| } |
| }; |
| if check_format { |
| warn_on_format_spec(&arg.format, warnings, input_span, is_source_literal); |
| } |
| ret |
| } |
| |
| /// `#[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 let Some(ty_span) = &spec.ty_span { |
| let span = slice_span(input_span, ty_span.clone(), is_source_literal); |
| warnings.push(FormatWarning::InvalidSpecifier { span }) |
| } |
| } |
| |
| 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_filter(input: &MetaItemOrLitParser) -> Result<Filter, InvalidOnClause> { |
| let span = input.span(); |
| let pred = parse_predicate(input)?; |
| Ok(Filter { 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.as_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_format(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_format(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( |
| "using multiple `rustc_on_unimplemented` (or mixing it with `diagnostic::on_unimplemented`) is not supported" |
| )] |
| pub(crate) struct DupesNotAllowed; |