Remove slugs from the #[derive(Diagnostic)] macro

This commit is contained in:
Jonathan Brouwer
2026-02-06 19:04:18 +01:00
parent ea8733133c
commit 62dd37131f
5 changed files with 60 additions and 205 deletions
@@ -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
})
}
}
@@ -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::<LitStr>()?;
@@ -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::<Path>()?;
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<!, DiagnosticDeriveError> {
}
}
}
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<Mismatch> {
// 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 })
}
}
@@ -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)) }
}
}
@@ -186,10 +186,10 @@ impl<'parent, 'a> SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
fn identify_kind(
&mut self,
) -> Result<Vec<(SubdiagnosticKind, Message)>, 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<TokenStream, DiagnosticDeriveError> {
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<TokenStream, DiagnosticDeriveErro
.map(|binding| self.generate_field_attr_code(binding, kind_stats))
.collect();
if kind_slugs.is_empty() && !self.has_subdiagnostic {
if kind_messages.is_empty() && !self.has_subdiagnostic {
if self.is_enum {
// It's okay for a variant to not be a subdiagnostic at all..
return Ok(quote! {});
@@ -533,9 +533,9 @@ pub(crate) fn into_tokens(&mut self) -> Result<TokenStream, DiagnosticDeriveErro
let diag = &self.parent.diag;
let mut calls = TokenStream::new();
for (kind, slug) in kind_slugs {
for (kind, messages) in kind_messages {
let message = format_ident!("__message");
let message_stream = slug.diag_message(None);
let message_stream = messages.diag_message(None);
calls.extend(quote! { let #message = #diag.eagerly_translate(#message_stream); });
let name = format_ident!("{}{}", if span_field.is_some() { "span_" } else { "" }, kind);
+16 -27
View File
@@ -267,7 +267,7 @@ fn value_ref(&self) -> 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<Message>,
pub(super) message: Option<Message>,
}
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::<LitStr>()?;
if !message.suffix().is_empty() {
let inline_message = input.parse::<LitStr>()?;
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::<Token![,]>()?; }
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::<Path>()?;
let arg_name_span = arg_name.span().unwrap();
if input.is_empty() || input.parse::<Token![,]>().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::<Path>()?;
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 }))
}
}