From 62dd37131f6f43d0dab3e26cf125cd841a08e003 Mon Sep 17 00:00:00 2001 From: Jonathan Brouwer Date: Fri, 6 Feb 2026 19:04:18 +0100 Subject: [PATCH] Remove slugs from the `#[derive(Diagnostic)]` macro --- .../src/diagnostics/diagnostic.rs | 27 +---- .../src/diagnostics/diagnostic_builder.rs | 98 +++++-------------- .../rustc_macros/src/diagnostics/message.rs | 75 ++------------ .../src/diagnostics/subdiagnostic.rs | 22 ++--- .../rustc_macros/src/diagnostics/utils.rs | 43 +++----- 5 files changed, 60 insertions(+), 205 deletions(-) diff --git a/compiler/rustc_macros/src/diagnostics/diagnostic.rs b/compiler/rustc_macros/src/diagnostics/diagnostic.rs index e8356655dd9f..dc8231e5f0b0 100644 --- a/compiler/rustc_macros/src/diagnostics/diagnostic.rs +++ b/compiler/rustc_macros/src/diagnostics/diagnostic.rs @@ -1,7 +1,5 @@ #![deny(unused_must_use)] -use std::cell::RefCell; - use proc_macro2::TokenStream; use quote::quote; use synstructure::Structure; @@ -22,7 +20,6 @@ pub(crate) fn new(structure: Structure<'a>) -> Self { pub(crate) fn into_tokens(self) -> TokenStream { let DiagnosticDerive { mut structure } = self; let kind = DiagnosticDeriveKind::Diagnostic; - let messages = RefCell::new(Vec::new()); let implementation = kind.each_variant(&mut structure, |mut builder, variant| { let preamble = builder.preamble(variant); let body = builder.body(variant); @@ -30,7 +27,6 @@ pub(crate) fn into_tokens(self) -> TokenStream { let Some(message) = builder.primary_message() else { return DiagnosticDeriveError::ErrorHandled.to_compile_error(); }; - messages.borrow_mut().push(message.clone()); let message = message.diag_message(Some(variant)); let init = quote! { @@ -52,9 +48,7 @@ pub(crate) fn into_tokens(self) -> TokenStream { }); // A lifetime of `'a` causes conflicts, but `_sess` is fine. - // FIXME(edition_2024): Fix the `keyword_idents_2024` lint to not trigger here? - #[allow(keyword_idents_2024)] - let mut imp = structure.gen_impl(quote! { + structure.gen_impl(quote! { gen impl<'_sess, G> rustc_errors::Diagnostic<'_sess, G> for @Self where G: rustc_errors::EmissionGuarantee { @@ -67,11 +61,7 @@ fn into_diag( #implementation } } - }); - for test in messages.borrow().iter().map(|s| s.generate_test(&structure)) { - imp.extend(test); - } - imp + }) } } @@ -88,7 +78,6 @@ pub(crate) fn new(structure: Structure<'a>) -> Self { pub(crate) fn into_tokens(self) -> TokenStream { let LintDiagnosticDerive { mut structure } = self; let kind = DiagnosticDeriveKind::LintDiagnostic; - let messages = RefCell::new(Vec::new()); let implementation = kind.each_variant(&mut structure, |mut builder, variant| { let preamble = builder.preamble(variant); let body = builder.body(variant); @@ -96,7 +85,6 @@ pub(crate) fn into_tokens(self) -> TokenStream { let Some(message) = builder.primary_message() else { return DiagnosticDeriveError::ErrorHandled.to_compile_error(); }; - messages.borrow_mut().push(message.clone()); let message = message.diag_message(Some(variant)); let primary_message = quote! { diag.primary_message(#message); @@ -112,9 +100,7 @@ pub(crate) fn into_tokens(self) -> TokenStream { } }); - // FIXME(edition_2024): Fix the `keyword_idents_2024` lint to not trigger here? - #[allow(keyword_idents_2024)] - let mut imp = structure.gen_impl(quote! { + structure.gen_impl(quote! { gen impl<'__a> rustc_errors::LintDiagnostic<'__a, ()> for @Self { #[track_caller] fn decorate_lint<'__b>( @@ -124,11 +110,6 @@ fn decorate_lint<'__b>( #implementation; } } - }); - for test in messages.borrow().iter().map(|s| s.generate_test(&structure)) { - imp.extend(test); - } - - imp + }) } } diff --git a/compiler/rustc_macros/src/diagnostics/diagnostic_builder.rs b/compiler/rustc_macros/src/diagnostics/diagnostic_builder.rs index 6107b181eea2..b386408a1918 100644 --- a/compiler/rustc_macros/src/diagnostics/diagnostic_builder.rs +++ b/compiler/rustc_macros/src/diagnostics/diagnostic_builder.rs @@ -4,7 +4,7 @@ use quote::{format_ident, quote, quote_spanned}; use syn::parse::ParseStream; use syn::spanned::Spanned; -use syn::{Attribute, LitStr, Meta, Path, Token, Type, parse_quote}; +use syn::{Attribute, LitStr, Meta, Path, Token, Type}; use synstructure::{BindingInfo, Structure, VariantInfo}; use super::utils::SubdiagnosticVariant; @@ -109,24 +109,14 @@ impl DiagnosticDeriveVariantBuilder { pub(crate) fn primary_message(&self) -> Option<&Message> { match self.message.as_ref() { None => { - span_err(self.span, "diagnostic slug not specified") + span_err(self.span, "diagnostic message not specified") .help( - "specify the slug as the first argument to the `#[diag(...)]` \ - attribute, such as `#[diag(hir_analysis_example_error)]`", + "specify the message as the first argument to the `#[diag(...)]` \ + attribute, such as `#[diag(\"Example error\")]`", ) .emit(); None } - Some(Message::Slug(slug)) - if let Some(Mismatch { slug_name, crate_name, slug_prefix }) = - Mismatch::check(slug) => - { - span_err(slug.span().unwrap(), "diagnostic slug and crate name do not match") - .note(format!("slug is `{slug_name}` but the crate name is `{crate_name}`")) - .help(format!("expected a slug starting with `{slug_prefix}_...`")) - .emit(); - None - } Some(msg) => Some(msg), } } @@ -177,25 +167,15 @@ fn parse_subdiag_attribute( .help("consider creating a `Subdiagnostic` instead")); } - // For subdiagnostics without a message specified, insert a placeholder slug - let slug = subdiag.slug.unwrap_or_else(|| { - Message::Slug(match subdiag.kind { - SubdiagnosticKind::Label => parse_quote! { _subdiag::label }, - SubdiagnosticKind::Note => parse_quote! { _subdiag::note }, - SubdiagnosticKind::NoteOnce => parse_quote! { _subdiag::note_once }, - SubdiagnosticKind::Help => parse_quote! { _subdiag::help }, - SubdiagnosticKind::HelpOnce => parse_quote! { _subdiag::help_once }, - SubdiagnosticKind::Warn => parse_quote! { _subdiag::warn }, - SubdiagnosticKind::Suggestion { .. } => parse_quote! { _subdiag::suggestion }, - SubdiagnosticKind::MultipartSuggestion { .. } => unreachable!(), - }) - }); + let Some(message) = subdiag.message else { + throw_invalid_attr!(attr, |diag| diag.help("subdiagnostic message is missing")) + }; - Ok(Some((subdiag.kind, slug, false))) + Ok(Some((subdiag.kind, message, false))) } /// Establishes state in the `DiagnosticDeriveBuilder` resulting from the struct - /// attributes like `#[diag(..)]`, such as the slug and error code. Generates + /// attributes like `#[diag(..)]`, such as the message and error code. Generates /// diagnostic builder calls for setting error code and creating note/help messages. fn generate_structure_code_for_attr( &mut self, @@ -213,9 +193,6 @@ fn generate_structure_code_for_attr( if name == "diag" { let mut tokens = TokenStream::new(); attr.parse_args_with(|input: ParseStream<'_>| { - let mut input = &*input; - let slug_recovery_point = input.fork(); - if input.peek(LitStr) { // Parse an inline message let message = input.parse::()?; @@ -226,15 +203,8 @@ fn generate_structure_code_for_attr( ) .emit(); } - self.message = Some(Message::Inline(message.span(), message.value())); - } else { - // Parse a slug - let slug = input.parse::()?; - if input.is_empty() || input.peek(Token![,]) { - self.message = Some(Message::Slug(slug)); - } else { - input = &slug_recovery_point; - } + self.message = + Some(Message { message_span: message.span(), value: message.value() }); } // Parse arguments @@ -248,7 +218,7 @@ fn generate_structure_code_for_attr( if input.peek(Token![,]) { span_err( arg_name.span().unwrap(), - "diagnostic slug must be the first argument", + "diagnostic message must be the first argument", ) .emit(); continue; @@ -265,7 +235,7 @@ fn generate_structure_code_for_attr( } _ => { span_err(arg_name.span().unwrap(), "unknown argument") - .note("only the `code` parameter is valid after the slug") + .note("only the `code` parameter is valid after the message") .emit(); } } @@ -276,7 +246,7 @@ fn generate_structure_code_for_attr( return Ok(tokens); } - let Some((subdiag, slug, _no_span)) = self.parse_subdiag_attribute(attr)? else { + let Some((subdiag, message, _no_span)) = self.parse_subdiag_attribute(attr)? else { // Some attributes aren't errors - like documentation comments - but also aren't // subdiagnostics. return Ok(quote! {}); @@ -287,7 +257,7 @@ fn generate_structure_code_for_attr( | SubdiagnosticKind::NoteOnce | SubdiagnosticKind::Help | SubdiagnosticKind::HelpOnce - | SubdiagnosticKind::Warn => Ok(self.add_subdiagnostic(&fn_ident, slug, variant)), + | SubdiagnosticKind::Warn => Ok(self.add_subdiagnostic(&fn_ident, message, variant)), SubdiagnosticKind::Label | SubdiagnosticKind::Suggestion { .. } => { throw_invalid_attr!(attr, |diag| diag .help("`#[label]` and `#[suggestion]` can only be applied to fields")); @@ -406,7 +376,7 @@ fn generate_inner_field_code( _ => (), } - let Some((subdiag, slug, _no_span)) = self.parse_subdiag_attribute(attr)? else { + let Some((subdiag, message, _no_span)) = self.parse_subdiag_attribute(attr)? else { // Some attributes aren't errors - like documentation comments - but also aren't // subdiagnostics. return Ok(quote! {}); @@ -415,7 +385,7 @@ fn generate_inner_field_code( match subdiag { SubdiagnosticKind::Label => { report_error_if_not_applied_to_span(attr, &info)?; - Ok(self.add_spanned_subdiagnostic(binding, &fn_ident, slug, variant)) + Ok(self.add_spanned_subdiagnostic(binding, &fn_ident, message, variant)) } SubdiagnosticKind::Note | SubdiagnosticKind::NoteOnce @@ -426,11 +396,11 @@ fn generate_inner_field_code( if type_matches_path(inner, &["rustc_span", "Span"]) || type_matches_path(inner, &["rustc_span", "MultiSpan"]) { - Ok(self.add_spanned_subdiagnostic(binding, &fn_ident, slug, variant)) + Ok(self.add_spanned_subdiagnostic(binding, &fn_ident, message, variant)) } else if type_is_unit(inner) || (matches!(info.ty, FieldInnerTy::Plain(_)) && type_is_bool(inner)) { - Ok(self.add_subdiagnostic(&fn_ident, slug, variant)) + Ok(self.add_subdiagnostic(&fn_ident, message, variant)) } else { report_type_error(attr, "`Span`, `MultiSpan`, `bool` or `()`")? } @@ -456,7 +426,7 @@ fn generate_inner_field_code( applicability.set_once(quote! { #static_applicability }, span); } - let message = slug.diag_message(Some(variant)); + let message = message.diag_message(Some(variant)); let applicability = applicability .value() .unwrap_or_else(|| quote! { rustc_errors::Applicability::Unspecified }); @@ -477,7 +447,7 @@ fn generate_inner_field_code( } } - /// Adds a spanned subdiagnostic by generating a `diag.span_$kind` call with the current slug + /// Adds a spanned subdiagnostic by generating a `diag.span_$kind` call with the current message /// and `fluent_attr_identifier`. fn add_spanned_subdiagnostic( &self, @@ -496,7 +466,7 @@ fn add_spanned_subdiagnostic( } } - /// Adds a subdiagnostic by generating a `diag.span_$kind` call with the current slug + /// Adds a subdiagnostic by generating a `diag.span_$kind` call with the current message /// and `fluent_attr_identifier`. fn add_subdiagnostic( &self, @@ -567,27 +537,3 @@ fn type_err(span: &Span) -> Result { } } } - -struct Mismatch { - slug_name: String, - crate_name: String, - slug_prefix: String, -} - -impl Mismatch { - /// Checks whether the slug starts with the crate name it's in. - fn check(slug: &syn::Path) -> Option { - // If this is missing we're probably in a test, so bail. - let crate_name = std::env::var("CARGO_CRATE_NAME").ok()?; - - // If we're not in a "rustc_" crate, bail. - let Some(("rustc", slug_prefix)) = crate_name.split_once('_') else { return None }; - - let slug_name = slug.segments.first()?.ident.to_string(); - if slug_name.starts_with(slug_prefix) { - return None; - } - - Some(Mismatch { slug_name, slug_prefix: slug_prefix.to_string(), crate_name }) - } -} diff --git a/compiler/rustc_macros/src/diagnostics/message.rs b/compiler/rustc_macros/src/diagnostics/message.rs index 2db8df2f69ae..18d4d60dde3e 100644 --- a/compiler/rustc_macros/src/diagnostics/message.rs +++ b/compiler/rustc_macros/src/diagnostics/message.rs @@ -2,16 +2,15 @@ use fluent_syntax::ast::{Expression, InlineExpression, Pattern, PatternElement}; use proc_macro2::{Span, TokenStream}; use quote::quote; -use syn::Path; use syn::ext::IdentExt; -use synstructure::{Structure, VariantInfo}; +use synstructure::VariantInfo; use crate::diagnostics::error::span_err; #[derive(Clone)] -pub(crate) enum Message { - Slug(Path), - Inline(Span, String), +pub(crate) struct Message { + pub message_span: Span, + pub value: String, } impl Message { @@ -19,69 +18,9 @@ impl Message { /// The passed `variant` is used to check whether all variables in the message are used. /// For subdiagnostics, we cannot check this. pub(crate) fn diag_message(&self, variant: Option<&VariantInfo<'_>>) -> TokenStream { - match self { - Message::Slug(slug) => { - quote! { crate::fluent_generated::#slug } - } - Message::Inline(message_span, message) => { - verify_fluent_message(*message_span, &message, variant); - quote! { rustc_errors::DiagMessage::Inline(std::borrow::Cow::Borrowed(#message)) } - } - } - } - - /// Generates a `#[test]` that verifies that all referenced variables - /// exist on this structure. - pub(crate) fn generate_test(&self, structure: &Structure<'_>) -> TokenStream { - match self { - Message::Slug(slug) => { - // FIXME: We can't identify variables in a subdiagnostic - for field in structure.variants().iter().flat_map(|v| v.ast().fields.iter()) { - for attr_name in field.attrs.iter().filter_map(|at| at.path().get_ident()) { - if attr_name == "subdiagnostic" { - return quote!(); - } - } - } - use std::sync::atomic::{AtomicUsize, Ordering}; - // We need to make sure that the same diagnostic slug can be used multiple times without - // causing an error, so just have a global counter here. - static COUNTER: AtomicUsize = AtomicUsize::new(0); - let slug = slug.get_ident().unwrap(); - let ident = quote::format_ident!( - "verify_{slug}_{}", - COUNTER.fetch_add(1, Ordering::Relaxed) - ); - let ref_slug = quote::format_ident!("{slug}_refs"); - let struct_name = &structure.ast().ident; - let variables: Vec<_> = structure - .variants() - .iter() - .flat_map(|v| { - v.ast() - .fields - .iter() - .filter_map(|f| f.ident.as_ref().map(|i| i.to_string())) - }) - .collect(); - // tidy errors on `#[test]` outside of test files, so we use `#[test ]` to work around this - quote! { - #[cfg(test)] - #[test ] - fn #ident() { - let variables = [#(#variables),*]; - for vref in crate::fluent_generated::#ref_slug { - assert!(variables.contains(vref), "{}: variable `{vref}` not found ({})", stringify!(#struct_name), stringify!(#slug)); - } - } - } - } - Message::Inline(..) => { - // We don't generate a test for inline diagnostics, we can verify these at compile-time! - // This verification is done in the `diag_message` function above - quote! {} - } - } + let message = &self.value; + verify_fluent_message(self.message_span, &message, variant); + quote! { rustc_errors::DiagMessage::Inline(std::borrow::Cow::Borrowed(#message)) } } } diff --git a/compiler/rustc_macros/src/diagnostics/subdiagnostic.rs b/compiler/rustc_macros/src/diagnostics/subdiagnostic.rs index ac1fa984664c..7eb170ec236a 100644 --- a/compiler/rustc_macros/src/diagnostics/subdiagnostic.rs +++ b/compiler/rustc_macros/src/diagnostics/subdiagnostic.rs @@ -186,10 +186,10 @@ impl<'parent, 'a> SubdiagnosticDeriveVariantBuilder<'parent, 'a> { fn identify_kind( &mut self, ) -> Result, DiagnosticDeriveError> { - let mut kind_slugs = vec![]; + let mut kind_messages = vec![]; for attr in self.variant.ast().attrs { - let Some(SubdiagnosticVariant { kind, slug }) = + let Some(SubdiagnosticVariant { kind, message }) = SubdiagnosticVariant::from_attr(attr, &self.fields)? else { // Some attributes aren't errors - like documentation comments - but also aren't @@ -197,22 +197,22 @@ fn identify_kind( continue; }; - let Some(slug) = slug else { + let Some(message) = message else { let name = attr.path().segments.last().unwrap().ident.to_string(); let name = name.as_str(); throw_span_err!( attr.span().unwrap(), format!( - "diagnostic slug must be first argument of a `#[{name}(...)]` attribute" + "diagnostic message must be first argument of a `#[{name}(...)]` attribute" ) ); }; - kind_slugs.push((kind, slug)); + kind_messages.push((kind, message)); } - Ok(kind_slugs) + Ok(kind_messages) } /// Generates the code for a field with no attributes. @@ -498,9 +498,9 @@ fn generate_field_code_inner_list( } pub(crate) fn into_tokens(&mut self) -> Result { - let kind_slugs = self.identify_kind()?; + let kind_messages = self.identify_kind()?; - let kind_stats: KindsStatistics = kind_slugs.iter().map(|(kind, _slug)| kind).collect(); + let kind_stats: KindsStatistics = kind_messages.iter().map(|(kind, _msg)| kind).collect(); let init = if kind_stats.has_multipart_suggestion { quote! { let mut suggestions = Vec::new(); } @@ -516,7 +516,7 @@ pub(crate) fn into_tokens(&mut self) -> Result Result Option<&T> { /// /// ```ignore (not-usage-example) /// /// Suggest `==` when users wrote `===`. -/// #[suggestion(slug = "parser-not-javascript-eq", code = "{lhs} == {rhs}")] +/// #[suggestion("example message", code = "{lhs} == {rhs}")] /// struct NotJavaScriptEq { /// #[primary_span] /// span: Span, @@ -588,13 +588,13 @@ pub(super) enum SubdiagnosticKind { pub(super) struct SubdiagnosticVariant { pub(super) kind: SubdiagnosticKind, - pub(super) slug: Option, + pub(super) message: Option, } impl SubdiagnosticVariant { /// Constructs a `SubdiagnosticVariant` from a field or type attribute such as `#[note]`, - /// `#[error(parser::add_paren)]` or `#[suggestion(code = "...")]`. Returns the - /// `SubdiagnosticKind` and the diagnostic slug, if specified. + /// `#[error("add parenthesis")]` or `#[suggestion(code = "...")]`. Returns the + /// `SubdiagnosticKind` and the diagnostic message, if specified. pub(super) fn from_attr( attr: &Attribute, fields: &FieldMap, @@ -660,11 +660,11 @@ pub(super) fn from_attr( let list = match &attr.meta { Meta::List(list) => { // An attribute with properties, such as `#[suggestion(code = "...")]` or - // `#[error(some::slug)]` + // `#[error("message")]` list } Meta::Path(_) => { - // An attribute without a slug or other properties, such as `#[note]` - return + // An attribute without a message or other properties, such as `#[note]` - return // without further processing. // // Only allow this if there are no mandatory properties, such as `code = "..."` in @@ -677,7 +677,7 @@ pub(super) fn from_attr( | SubdiagnosticKind::HelpOnce | SubdiagnosticKind::Warn | SubdiagnosticKind::MultipartSuggestion { .. } => { - return Ok(Some(SubdiagnosticVariant { kind, slug: None })); + return Ok(Some(SubdiagnosticVariant { kind, message: None })); } SubdiagnosticKind::Suggestion { .. } => { throw_span_err!(span, "suggestion without `code = \"...\"`") @@ -692,45 +692,34 @@ pub(super) fn from_attr( let mut code = None; let mut suggestion_kind = None; - let mut slug = None; + let mut message = None; list.parse_args_with(|input: ParseStream<'_>| { let mut is_first = true; while !input.is_empty() { // Try to parse an inline diagnostic message if input.peek(LitStr) { - let message = input.parse::()?; - if !message.suffix().is_empty() { + let inline_message = input.parse::()?; + if !inline_message.suffix().is_empty() { span_err( - message.span().unwrap(), + inline_message.span().unwrap(), "Inline message is not allowed to have a suffix", ).emit(); } if !input.is_empty() { input.parse::()?; } if is_first { - slug = Some(Message::Inline(message.span(), message.value())); + message = Some(Message { message_span: inline_message.span(), value: inline_message.value() }); is_first = false; } else { - span_err(message.span().unwrap(), "a diagnostic message must be the first argument to the attribute").emit(); - } - continue - } - - // Try to parse a slug instead - let arg_name: Path = input.parse::()?; - let arg_name_span = arg_name.span().unwrap(); - if input.is_empty() || input.parse::().is_ok() { - if is_first { - slug = Some(Message::Slug(arg_name)); - is_first = false; - } else { - span_err(arg_name_span, "a diagnostic slug must be the first argument to the attribute").emit(); + span_err(inline_message.span().unwrap(), "a diagnostic message must be the first argument to the attribute").emit(); } continue } is_first = false; // Try to parse an argument + let arg_name: Path = input.parse::()?; + let arg_name_span = arg_name.span().unwrap(); match (arg_name.require_ident()?.to_string().as_str(), &mut kind) { ("code", SubdiagnosticKind::Suggestion { code_field, .. }) => { let code_init = build_suggestion_code( @@ -836,7 +825,7 @@ pub(super) fn from_attr( | SubdiagnosticKind::Warn => {} } - Ok(Some(SubdiagnosticVariant { kind, slug })) + Ok(Some(SubdiagnosticVariant { kind, message })) } }