Add inline syntax for diagnostic messages

This commit is contained in:
Jonathan Brouwer 2026-01-30 17:46:36 +01:00
parent 7d8ebe3128
commit 8927aa5738
No known key found for this signature in database
GPG key ID: F13E55D38C971DEF
8 changed files with 151 additions and 42 deletions

View file

@ -247,6 +247,9 @@ pub enum SubdiagMessage {
/// Identifier of a Fluent message. Instances of this variant are generated by the
/// `Subdiagnostic` derive.
FluentIdentifier(FluentId),
/// An inline Fluent message. Instances of this variant are generated by the
/// `Subdiagnostic` derive.
Inline(Cow<'static, str>),
/// Attribute of a Fluent message. Needs to be combined with a Fluent identifier to produce an
/// actual translated message. Instances of this variant are generated by the `fluent_messages`
/// macro.
@ -291,6 +294,8 @@ pub enum DiagMessage {
/// <https://projectfluent.org/fluent/guide/hello.html>
/// <https://projectfluent.org/fluent/guide/attributes.html>
FluentIdentifier(FluentId, Option<FluentId>),
/// An inline Fluent message, containing the to be translated diagnostic message.
Inline(Cow<'static, str>),
}
impl DiagMessage {
@ -305,21 +310,22 @@ impl DiagMessage {
SubdiagMessage::FluentIdentifier(id) => {
return DiagMessage::FluentIdentifier(id, None);
}
SubdiagMessage::Inline(s) => return DiagMessage::Inline(s),
SubdiagMessage::FluentAttr(attr) => attr,
};
match self {
DiagMessage::Str(s) => DiagMessage::Str(s.clone()),
DiagMessage::FluentIdentifier(id, _) => {
DiagMessage::FluentIdentifier(id.clone(), Some(attr))
}
_ => panic!("Tried to add a subdiagnostic to a message without a fluent identifier"),
}
}
pub fn as_str(&self) -> Option<&str> {
match self {
DiagMessage::Str(s) => Some(s),
DiagMessage::FluentIdentifier(_, _) => None,
DiagMessage::FluentIdentifier(_, _) | DiagMessage::Inline(_) => None,
}
}
}
@ -353,6 +359,7 @@ impl From<DiagMessage> for SubdiagMessage {
// There isn't really a sensible behaviour for this because it loses information but
// this is the most sensible of the behaviours.
DiagMessage::FluentIdentifier(_, Some(attr)) => SubdiagMessage::FluentAttr(attr),
DiagMessage::Inline(s) => SubdiagMessage::Inline(s),
}
}
}

View file

@ -3,11 +3,13 @@ use std::env;
use std::error::Report;
use std::sync::Arc;
use rustc_error_messages::langid;
pub use rustc_error_messages::{FluentArgs, LazyFallbackBundle};
use tracing::{debug, trace};
use crate::error::{TranslateError, TranslateErrorKind};
use crate::{DiagArg, DiagMessage, FluentBundle, Style};
use crate::fluent_bundle::FluentResource;
use crate::{DiagArg, DiagMessage, FluentBundle, Style, fluent_bundle};
/// Convert diagnostic arguments (a rustc internal type that exists to implement
/// `Encodable`/`Decodable`) into `FluentArgs` which is necessary to perform translation.
@ -79,6 +81,28 @@ impl Translator {
return Ok(Cow::Borrowed(msg));
}
DiagMessage::FluentIdentifier(identifier, attr) => (identifier, attr),
// This translates an inline fluent diagnostic message
// It does this by creating a new `FluentBundle` with only one message,
// and then translating using this bundle.
DiagMessage::Inline(msg) => {
const GENERATED_MSG_ID: &str = "generated_msg";
let resource =
FluentResource::try_new(format!("{GENERATED_MSG_ID} = {msg}\n")).unwrap();
let mut bundle = fluent_bundle::FluentBundle::new(vec![langid!("en-US")]);
bundle.set_use_isolating(false);
bundle.add_resource(resource).unwrap();
let message = bundle.get_message(GENERATED_MSG_ID).unwrap();
let value = message.value().unwrap();
let mut errs = vec![];
let translated = bundle.format_pattern(value, Some(args), &mut errs).to_string();
debug!(?translated, ?errs);
return if errs.is_empty() {
Ok(Cow::Owned(translated))
} else {
Err(TranslateError::fluent(&Cow::Borrowed(GENERATED_MSG_ID), args, errs))
};
}
};
let translate_with_bundle =
|bundle: &'a FluentBundle| -> Result<Cow<'_, str>, TranslateError<'_>> {

View file

@ -27,15 +27,17 @@ impl<'a> DiagnosticDerive<'a> {
let preamble = builder.preamble(variant);
let body = builder.body(variant);
let Some(slug) = builder.primary_message() else {
let Some(message) = builder.primary_message() else {
return DiagnosticDeriveError::ErrorHandled.to_compile_error();
};
slugs.borrow_mut().push(slug.clone());
slugs.borrow_mut().extend(message.slug().cloned());
let message = message.diag_message();
let init = quote! {
let mut diag = rustc_errors::Diag::new(
dcx,
level,
crate::fluent_generated::#slug
#message
);
};
@ -91,12 +93,13 @@ impl<'a> LintDiagnosticDerive<'a> {
let preamble = builder.preamble(variant);
let body = builder.body(variant);
let Some(slug) = builder.primary_message() else {
let Some(message) = builder.primary_message() else {
return DiagnosticDeriveError::ErrorHandled.to_compile_error();
};
slugs.borrow_mut().push(slug.clone());
slugs.borrow_mut().extend(message.slug().cloned());
let message = message.diag_message();
let primary_message = quote! {
diag.primary_message(crate::fluent_generated::#slug);
diag.primary_message(#message);
};
let formatting_init = &builder.formatting_init;

View file

@ -4,13 +4,14 @@ use proc_macro2::{Ident, Span, TokenStream};
use quote::{format_ident, quote, quote_spanned};
use syn::parse::ParseStream;
use syn::spanned::Spanned;
use syn::{Attribute, Meta, Path, Token, Type, parse_quote};
use syn::{Attribute, LitStr, Meta, Path, Token, Type, parse_quote};
use synstructure::{BindingInfo, Structure, VariantInfo};
use super::utils::SubdiagnosticVariant;
use crate::diagnostics::error::{
DiagnosticDeriveError, span_err, throw_invalid_attr, throw_span_err,
};
use crate::diagnostics::message::Message;
use crate::diagnostics::utils::{
FieldInfo, FieldInnerTy, FieldMap, SetOnce, SpannedOption, SubdiagnosticKind,
build_field_mapping, is_doc_comment, report_error_if_not_applied_to_span, report_type_error,
@ -41,9 +42,9 @@ pub(crate) struct DiagnosticDeriveVariantBuilder {
/// derive builder.
pub field_map: FieldMap,
/// Slug is a mandatory part of the struct attribute as corresponds to the Fluent message that
/// Message is a mandatory part of the struct attribute as corresponds to the Fluent message that
/// has the actual diagnostic message.
pub slug: Option<Path>,
pub message: Option<Message>,
/// Error codes are a optional part of the struct attribute - this is only set to detect
/// multiple specifications.
@ -90,7 +91,7 @@ impl DiagnosticDeriveKind {
span,
field_map: build_field_mapping(variant),
formatting_init: TokenStream::new(),
slug: None,
message: None,
code: None,
};
f(builder, variant)
@ -105,8 +106,8 @@ impl DiagnosticDeriveKind {
}
impl DiagnosticDeriveVariantBuilder {
pub(crate) fn primary_message(&self) -> Option<&Path> {
match self.slug.as_ref() {
pub(crate) fn primary_message(&self) -> Option<&Message> {
match self.message.as_ref() {
None => {
span_err(self.span, "diagnostic slug not specified")
.help(
@ -116,7 +117,7 @@ impl DiagnosticDeriveVariantBuilder {
.emit();
None
}
Some(slug)
Some(Message::Slug(slug))
if let Some(Mismatch { slug_name, crate_name, slug_prefix }) =
Mismatch::check(slug) =>
{
@ -126,7 +127,7 @@ impl DiagnosticDeriveVariantBuilder {
.emit();
None
}
Some(slug) => Some(slug),
Some(msg) => Some(msg),
}
}
@ -163,7 +164,7 @@ impl DiagnosticDeriveVariantBuilder {
fn parse_subdiag_attribute(
&self,
attr: &Attribute,
) -> Result<Option<(SubdiagnosticKind, Path, bool)>, DiagnosticDeriveError> {
) -> Result<Option<(SubdiagnosticKind, Message, bool)>, DiagnosticDeriveError> {
let Some(subdiag) = SubdiagnosticVariant::from_attr(attr, &self.field_map)? else {
// Some attributes aren't errors - like documentation comments - but also aren't
// subdiagnostics.
@ -175,15 +176,18 @@ impl DiagnosticDeriveVariantBuilder {
.help("consider creating a `Subdiagnostic` instead"));
}
let slug = subdiag.slug.unwrap_or_else(|| 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!(),
// 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!(),
})
});
Ok(Some((subdiag.kind, slug, false)))
@ -210,13 +214,28 @@ impl DiagnosticDeriveVariantBuilder {
let mut input = &*input;
let slug_recovery_point = input.fork();
let slug = input.parse::<Path>()?;
if input.is_empty() || input.peek(Token![,]) {
self.slug = Some(slug);
if input.peek(LitStr) {
// Parse an inline message
let message = input.parse::<LitStr>()?;
if !message.suffix().is_empty() {
span_err(
message.span().unwrap(),
"Inline message is not allowed to have a suffix",
)
.emit();
}
self.message = Some(Message::Inline(message.value()));
} else {
input = &slug_recovery_point;
// 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;
}
}
// Parse arguments
while !input.is_empty() {
input.parse::<Token![,]>()?;
// Allow trailing comma
@ -429,6 +448,7 @@ impl DiagnosticDeriveVariantBuilder {
applicability.set_once(quote! { #static_applicability }, span);
}
let message = slug.diag_message();
let applicability = applicability
.value()
.unwrap_or_else(|| quote! { rustc_errors::Applicability::Unspecified });
@ -438,7 +458,7 @@ impl DiagnosticDeriveVariantBuilder {
Ok(quote! {
diag.span_suggestions_with_style(
#span_field,
crate::fluent_generated::#slug,
#message,
#code_field,
#applicability,
#style
@ -455,22 +475,24 @@ impl DiagnosticDeriveVariantBuilder {
&self,
field_binding: TokenStream,
kind: &Ident,
fluent_attr_identifier: Path,
message: Message,
) -> TokenStream {
let fn_name = format_ident!("span_{}", kind);
let message = message.diag_message();
quote! {
diag.#fn_name(
#field_binding,
crate::fluent_generated::#fluent_attr_identifier
#message
);
}
}
/// Adds a subdiagnostic by generating a `diag.span_$kind` call with the current slug
/// and `fluent_attr_identifier`.
fn add_subdiagnostic(&self, kind: &Ident, fluent_attr_identifier: Path) -> TokenStream {
fn add_subdiagnostic(&self, kind: &Ident, message: Message) -> TokenStream {
let message = message.diag_message();
quote! {
diag.#kind(crate::fluent_generated::#fluent_attr_identifier);
diag.#kind(#message);
}
}

View file

@ -0,0 +1,28 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::Path;
pub(crate) enum Message {
Slug(Path),
Inline(String),
}
impl Message {
pub(crate) fn slug(&self) -> Option<&Path> {
match self {
Message::Slug(slug) => Some(slug),
Message::Inline(_) => None,
}
}
pub(crate) fn diag_message(&self) -> TokenStream {
match self {
Message::Slug(slug) => {
quote! { crate::fluent_generated::#slug }
}
Message::Inline(message) => {
quote! { rustc_errors::DiagMessage::Inline(std::borrow::Cow::Borrowed(#message)) }
}
}
}
}

View file

@ -1,6 +1,7 @@
mod diagnostic;
mod diagnostic_builder;
mod error;
mod message;
mod subdiagnostic;
mod utils;

View file

@ -11,6 +11,7 @@ use super::utils::SubdiagnosticVariant;
use crate::diagnostics::error::{
DiagnosticDeriveError, invalid_attr, span_err, throw_invalid_attr, throw_span_err,
};
use crate::diagnostics::message::Message;
use crate::diagnostics::utils::{
AllowMultipleAlternatives, FieldInfo, FieldInnerTy, FieldMap, SetOnce, SpannedOption,
SubdiagnosticKind, build_field_mapping, build_suggestion_code, is_doc_comment, new_code_ident,
@ -182,7 +183,9 @@ impl<'a> FromIterator<&'a SubdiagnosticKind> for KindsStatistics {
}
impl<'parent, 'a> SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
fn identify_kind(&mut self) -> Result<Vec<(SubdiagnosticKind, Path)>, DiagnosticDeriveError> {
fn identify_kind(
&mut self,
) -> Result<Vec<(SubdiagnosticKind, Message)>, DiagnosticDeriveError> {
let mut kind_slugs = vec![];
for attr in self.variant.ast().attrs {
@ -532,9 +535,8 @@ impl<'parent, 'a> SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
let mut calls = TokenStream::new();
for (kind, slug) in kind_slugs {
let message = format_ident!("__message");
calls.extend(
quote! { let #message = #diag.eagerly_translate(crate::fluent_generated::#slug); },
);
let message_stream = slug.diag_message();
calls.extend(quote! { let #message = #diag.eagerly_translate(#message_stream); });
let name = format_ident!("{}{}", if span_field.is_some() { "span_" } else { "" }, kind);
let call = match kind {

View file

@ -16,6 +16,7 @@ use super::error::invalid_attr;
use crate::diagnostics::error::{
DiagnosticDeriveError, span_err, throw_invalid_attr, throw_span_err,
};
use crate::diagnostics::message::Message;
thread_local! {
pub(crate) static CODE_IDENT_COUNT: RefCell<u32> = RefCell::new(0);
@ -587,7 +588,7 @@ pub(super) enum SubdiagnosticKind {
pub(super) struct SubdiagnosticVariant {
pub(super) kind: SubdiagnosticKind,
pub(super) slug: Option<Path>,
pub(super) slug: Option<Message>,
}
impl SubdiagnosticVariant {
@ -696,11 +697,31 @@ impl SubdiagnosticVariant {
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() {
span_err(
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.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(arg_name);
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();
@ -709,6 +730,7 @@ impl SubdiagnosticVariant {
}
is_first = false;
// Try to parse an argument
match (arg_name.require_ident()?.to_string().as_str(), &mut kind) {
("code", SubdiagnosticKind::Suggestion { code_field, .. }) => {
let code_init = build_suggestion_code(