Add verification for inline fluent messages
This commit is contained in:
parent
523d9d9200
commit
ca2be71a18
9 changed files with 183 additions and 73 deletions
|
|
@ -4229,6 +4229,8 @@ dependencies = [
|
|||
name = "rustc_macros"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"fluent-bundle",
|
||||
"fluent-syntax",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ proc-macro = true
|
|||
|
||||
[dependencies]
|
||||
# tidy-alphabetical-start
|
||||
fluent-bundle = "0.16"
|
||||
fluent-syntax = "0.12"
|
||||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
syn = { version = "2.0.9", features = ["full"] }
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ impl<'a> DiagnosticDerive<'a> {
|
|||
pub(crate) fn into_tokens(self) -> TokenStream {
|
||||
let DiagnosticDerive { mut structure } = self;
|
||||
let kind = DiagnosticDeriveKind::Diagnostic;
|
||||
let slugs = RefCell::new(Vec::new());
|
||||
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,8 +30,8 @@ impl<'a> DiagnosticDerive<'a> {
|
|||
let Some(message) = builder.primary_message() else {
|
||||
return DiagnosticDeriveError::ErrorHandled.to_compile_error();
|
||||
};
|
||||
slugs.borrow_mut().extend(message.slug().cloned());
|
||||
let message = message.diag_message();
|
||||
messages.borrow_mut().push(message.clone());
|
||||
let message = message.diag_message(variant);
|
||||
|
||||
let init = quote! {
|
||||
let mut diag = rustc_errors::Diag::new(
|
||||
|
|
@ -68,7 +68,7 @@ impl<'a> DiagnosticDerive<'a> {
|
|||
}
|
||||
}
|
||||
});
|
||||
for test in slugs.borrow().iter().map(|s| generate_test(s, &structure)) {
|
||||
for test in messages.borrow().iter().map(|s| s.generate_test(&structure)) {
|
||||
imp.extend(test);
|
||||
}
|
||||
imp
|
||||
|
|
@ -88,7 +88,7 @@ impl<'a> LintDiagnosticDerive<'a> {
|
|||
pub(crate) fn into_tokens(self) -> TokenStream {
|
||||
let LintDiagnosticDerive { mut structure } = self;
|
||||
let kind = DiagnosticDeriveKind::LintDiagnostic;
|
||||
let slugs = RefCell::new(Vec::new());
|
||||
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,8 +96,8 @@ impl<'a> LintDiagnosticDerive<'a> {
|
|||
let Some(message) = builder.primary_message() else {
|
||||
return DiagnosticDeriveError::ErrorHandled.to_compile_error();
|
||||
};
|
||||
slugs.borrow_mut().extend(message.slug().cloned());
|
||||
let message = message.diag_message();
|
||||
messages.borrow_mut().push(message.clone());
|
||||
let message = message.diag_message(variant);
|
||||
let primary_message = quote! {
|
||||
diag.primary_message(#message);
|
||||
};
|
||||
|
|
@ -125,47 +125,10 @@ impl<'a> LintDiagnosticDerive<'a> {
|
|||
}
|
||||
}
|
||||
});
|
||||
for test in slugs.borrow().iter().map(|s| generate_test(s, &structure)) {
|
||||
for test in messages.borrow().iter().map(|s| s.generate_test(&structure)) {
|
||||
imp.extend(test);
|
||||
}
|
||||
|
||||
imp
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a `#[test]` that verifies that all referenced variables
|
||||
/// exist on this structure.
|
||||
fn generate_test(slug: &syn::Path, structure: &Structure<'_>) -> TokenStream {
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -137,7 +137,8 @@ impl DiagnosticDeriveVariantBuilder {
|
|||
let ast = variant.ast();
|
||||
let attrs = &ast.attrs;
|
||||
let preamble = attrs.iter().map(|attr| {
|
||||
self.generate_structure_code_for_attr(attr).unwrap_or_else(|v| v.to_compile_error())
|
||||
self.generate_structure_code_for_attr(attr, variant)
|
||||
.unwrap_or_else(|v| v.to_compile_error())
|
||||
});
|
||||
|
||||
quote! {
|
||||
|
|
@ -155,7 +156,7 @@ impl DiagnosticDeriveVariantBuilder {
|
|||
}
|
||||
// ..and then subdiagnostic additions.
|
||||
for binding in variant.bindings().iter().filter(|bi| !should_generate_arg(bi.ast())) {
|
||||
body.extend(self.generate_field_attrs_code(binding));
|
||||
body.extend(self.generate_field_attrs_code(binding, variant));
|
||||
}
|
||||
body
|
||||
}
|
||||
|
|
@ -199,6 +200,7 @@ impl DiagnosticDeriveVariantBuilder {
|
|||
fn generate_structure_code_for_attr(
|
||||
&mut self,
|
||||
attr: &Attribute,
|
||||
variant: &VariantInfo<'_>,
|
||||
) -> Result<TokenStream, DiagnosticDeriveError> {
|
||||
// Always allow documentation comments.
|
||||
if is_doc_comment(attr) {
|
||||
|
|
@ -224,7 +226,7 @@ impl DiagnosticDeriveVariantBuilder {
|
|||
)
|
||||
.emit();
|
||||
}
|
||||
self.message = Some(Message::Inline(message.value()));
|
||||
self.message = Some(Message::Inline(message.span(), message.value()));
|
||||
} else {
|
||||
// Parse a slug
|
||||
let slug = input.parse::<Path>()?;
|
||||
|
|
@ -285,7 +287,7 @@ impl DiagnosticDeriveVariantBuilder {
|
|||
| SubdiagnosticKind::NoteOnce
|
||||
| SubdiagnosticKind::Help
|
||||
| SubdiagnosticKind::HelpOnce
|
||||
| SubdiagnosticKind::Warn => Ok(self.add_subdiagnostic(&fn_ident, slug)),
|
||||
| SubdiagnosticKind::Warn => Ok(self.add_subdiagnostic(&fn_ident, slug, variant)),
|
||||
SubdiagnosticKind::Label | SubdiagnosticKind::Suggestion { .. } => {
|
||||
throw_invalid_attr!(attr, |diag| diag
|
||||
.help("`#[label]` and `#[suggestion]` can only be applied to fields"));
|
||||
|
|
@ -313,7 +315,11 @@ impl DiagnosticDeriveVariantBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
fn generate_field_attrs_code(&mut self, binding_info: &BindingInfo<'_>) -> TokenStream {
|
||||
fn generate_field_attrs_code(
|
||||
&mut self,
|
||||
binding_info: &BindingInfo<'_>,
|
||||
variant: &VariantInfo<'_>,
|
||||
) -> TokenStream {
|
||||
let field = binding_info.ast();
|
||||
let field_binding = &binding_info.binding;
|
||||
|
||||
|
|
@ -352,6 +358,7 @@ impl DiagnosticDeriveVariantBuilder {
|
|||
attr,
|
||||
FieldInfo { binding: binding_info, ty: inner_ty, span: &field.span() },
|
||||
binding,
|
||||
variant
|
||||
)
|
||||
.unwrap_or_else(|v| v.to_compile_error());
|
||||
|
||||
|
|
@ -369,6 +376,7 @@ impl DiagnosticDeriveVariantBuilder {
|
|||
attr: &Attribute,
|
||||
info: FieldInfo<'_>,
|
||||
binding: TokenStream,
|
||||
variant: &VariantInfo<'_>,
|
||||
) -> Result<TokenStream, DiagnosticDeriveError> {
|
||||
let ident = &attr.path().segments.last().unwrap().ident;
|
||||
let name = ident.to_string();
|
||||
|
|
@ -407,7 +415,7 @@ impl DiagnosticDeriveVariantBuilder {
|
|||
match subdiag {
|
||||
SubdiagnosticKind::Label => {
|
||||
report_error_if_not_applied_to_span(attr, &info)?;
|
||||
Ok(self.add_spanned_subdiagnostic(binding, &fn_ident, slug))
|
||||
Ok(self.add_spanned_subdiagnostic(binding, &fn_ident, slug, variant))
|
||||
}
|
||||
SubdiagnosticKind::Note
|
||||
| SubdiagnosticKind::NoteOnce
|
||||
|
|
@ -418,11 +426,11 @@ impl DiagnosticDeriveVariantBuilder {
|
|||
if type_matches_path(inner, &["rustc_span", "Span"])
|
||||
|| type_matches_path(inner, &["rustc_span", "MultiSpan"])
|
||||
{
|
||||
Ok(self.add_spanned_subdiagnostic(binding, &fn_ident, slug))
|
||||
Ok(self.add_spanned_subdiagnostic(binding, &fn_ident, slug, variant))
|
||||
} else if type_is_unit(inner)
|
||||
|| (matches!(info.ty, FieldInnerTy::Plain(_)) && type_is_bool(inner))
|
||||
{
|
||||
Ok(self.add_subdiagnostic(&fn_ident, slug))
|
||||
Ok(self.add_subdiagnostic(&fn_ident, slug, variant))
|
||||
} else {
|
||||
report_type_error(attr, "`Span`, `MultiSpan`, `bool` or `()`")?
|
||||
}
|
||||
|
|
@ -448,7 +456,7 @@ impl DiagnosticDeriveVariantBuilder {
|
|||
applicability.set_once(quote! { #static_applicability }, span);
|
||||
}
|
||||
|
||||
let message = slug.diag_message();
|
||||
let message = slug.diag_message(variant);
|
||||
let applicability = applicability
|
||||
.value()
|
||||
.unwrap_or_else(|| quote! { rustc_errors::Applicability::Unspecified });
|
||||
|
|
@ -476,9 +484,10 @@ impl DiagnosticDeriveVariantBuilder {
|
|||
field_binding: TokenStream,
|
||||
kind: &Ident,
|
||||
message: Message,
|
||||
variant: &VariantInfo<'_>,
|
||||
) -> TokenStream {
|
||||
let fn_name = format_ident!("span_{}", kind);
|
||||
let message = message.diag_message();
|
||||
let message = message.diag_message(variant);
|
||||
quote! {
|
||||
diag.#fn_name(
|
||||
#field_binding,
|
||||
|
|
@ -489,8 +498,13 @@ impl DiagnosticDeriveVariantBuilder {
|
|||
|
||||
/// Adds a subdiagnostic by generating a `diag.span_$kind` call with the current slug
|
||||
/// and `fluent_attr_identifier`.
|
||||
fn add_subdiagnostic(&self, kind: &Ident, message: Message) -> TokenStream {
|
||||
let message = message.diag_message();
|
||||
fn add_subdiagnostic(
|
||||
&self,
|
||||
kind: &Ident,
|
||||
message: Message,
|
||||
variant: &VariantInfo<'_>,
|
||||
) -> TokenStream {
|
||||
let message = message.diag_message(variant);
|
||||
quote! {
|
||||
diag.#kind(#message);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,133 @@
|
|||
use proc_macro2::TokenStream;
|
||||
use fluent_bundle::FluentResource;
|
||||
use fluent_syntax::ast::{Expression, InlineExpression, Pattern, PatternElement};
|
||||
use proc_macro2::{Span, TokenStream};
|
||||
use quote::quote;
|
||||
use syn::Path;
|
||||
use synstructure::{Structure, VariantInfo};
|
||||
|
||||
use crate::diagnostics::error::span_err;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum Message {
|
||||
Slug(Path),
|
||||
Inline(String),
|
||||
Inline(Span, 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 {
|
||||
pub(crate) fn diag_message(&self, variant: &VariantInfo<'_>) -> TokenStream {
|
||||
match self {
|
||||
Message::Slug(slug) => {
|
||||
quote! { crate::fluent_generated::#slug }
|
||||
}
|
||||
Message::Inline(message) => {
|
||||
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! {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_fluent_message(msg_span: Span, message: &str, variant: &VariantInfo<'_>) {
|
||||
// Parse the fluent message
|
||||
const GENERATED_MSG_ID: &str = "generated_msg";
|
||||
let resource = FluentResource::try_new(format!("{GENERATED_MSG_ID} = {message}\n")).unwrap();
|
||||
assert_eq!(resource.entries().count(), 1);
|
||||
let Some(fluent_syntax::ast::Entry::Message(message)) = resource.get_entry(0) else {
|
||||
panic!("Did not parse into a message")
|
||||
};
|
||||
|
||||
// Check if all variables are used
|
||||
let fields: Vec<String> = variant
|
||||
.bindings()
|
||||
.iter()
|
||||
.flat_map(|b| b.ast().ident.as_ref())
|
||||
.map(|id| id.to_string())
|
||||
.collect();
|
||||
for variable in variable_references(&message) {
|
||||
if !fields.iter().any(|f| f == variable) {
|
||||
span_err(msg_span.unwrap(), format!("Variable `{variable}` not found in diagnostic "))
|
||||
.help(format!("Available fields: {:?}", fields.join(", ")))
|
||||
.emit();
|
||||
}
|
||||
// assert!(, );
|
||||
}
|
||||
}
|
||||
|
||||
fn variable_references<'a>(msg: &fluent_syntax::ast::Message<&'a str>) -> Vec<&'a str> {
|
||||
let mut refs = vec![];
|
||||
if let Some(Pattern { elements }) = &msg.value {
|
||||
for elt in elements {
|
||||
if let PatternElement::Placeable {
|
||||
expression: Expression::Inline(InlineExpression::VariableReference { id }),
|
||||
} = elt
|
||||
{
|
||||
refs.push(id.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
for attr in &msg.attributes {
|
||||
for elt in &attr.value.elements {
|
||||
if let PatternElement::Placeable {
|
||||
expression: Expression::Inline(InlineExpression::VariableReference { id }),
|
||||
} = elt
|
||||
{
|
||||
refs.push(id.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
refs
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ impl SubdiagnosticDerive {
|
|||
has_subdiagnostic: false,
|
||||
is_enum,
|
||||
};
|
||||
builder.into_tokens().unwrap_or_else(|v| v.to_compile_error())
|
||||
builder.into_tokens(variant).unwrap_or_else(|v| v.to_compile_error())
|
||||
});
|
||||
|
||||
quote! {
|
||||
|
|
@ -497,7 +497,10 @@ impl<'parent, 'a> SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn into_tokens(&mut self) -> Result<TokenStream, DiagnosticDeriveError> {
|
||||
pub(crate) fn into_tokens(
|
||||
&mut self,
|
||||
variant: &VariantInfo<'_>,
|
||||
) -> Result<TokenStream, DiagnosticDeriveError> {
|
||||
let kind_slugs = self.identify_kind()?;
|
||||
|
||||
let kind_stats: KindsStatistics = kind_slugs.iter().map(|(kind, _slug)| kind).collect();
|
||||
|
|
@ -535,7 +538,7 @@ impl<'parent, 'a> SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
|
|||
let mut calls = TokenStream::new();
|
||||
for (kind, slug) in kind_slugs {
|
||||
let message = format_ident!("__message");
|
||||
let message_stream = slug.diag_message();
|
||||
let message_stream = slug.diag_message(variant);
|
||||
calls.extend(quote! { let #message = #diag.eagerly_translate(#message_stream); });
|
||||
|
||||
let name = format_ident!("{}{}", if span_field.is_some() { "span_" } else { "" }, kind);
|
||||
|
|
|
|||
|
|
@ -708,7 +708,7 @@ impl SubdiagnosticVariant {
|
|||
}
|
||||
if !input.is_empty() { input.parse::<Token![,]>()?; }
|
||||
if is_first {
|
||||
slug = Some(Message::Inline(message.value()));
|
||||
slug = Some(Message::Inline(message.span(), message.value()));
|
||||
is_first = false;
|
||||
} else {
|
||||
span_err(message.span().unwrap(), "a diagnostic message must be the first argument to the attribute").emit();
|
||||
|
|
|
|||
|
|
@ -762,3 +762,16 @@ struct SuggestionOnVec {
|
|||
//~^ ERROR `#[suggestion(...)]` is not a valid attribute
|
||||
sub: Vec<Span>,
|
||||
}
|
||||
|
||||
#[derive(Diagnostic)]
|
||||
#[diag("exists: {$sub}")]
|
||||
struct VariableExists {
|
||||
sub: String,
|
||||
}
|
||||
|
||||
#[derive(Diagnostic)]
|
||||
#[diag("does not exist: {$nosub}")]
|
||||
//~^ ERROR Variable `nosub` not found in diagnostic
|
||||
struct VariableDoesNotExist {
|
||||
sub: String,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -508,6 +508,14 @@ LL | #[suggestion("with a suggestion", code = "")]
|
|||
= help: to show a suggestion consisting of multiple parts, use a `Subdiagnostic` annotated with `#[multipart_suggestion(...)]`
|
||||
= help: to show a variable set of suggestions, use a `Vec` of `Subdiagnostic`s annotated with `#[suggestion(...)]`
|
||||
|
||||
error: derive(Diagnostic): Variable `nosub` not found in diagnostic
|
||||
--> $DIR/diagnostic-derive-inline.rs:773:8
|
||||
|
|
||||
LL | #[diag("does not exist: {$nosub}")]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
= help: Available fields: "sub"
|
||||
|
||||
error: cannot find attribute `nonsense` in this scope
|
||||
--> $DIR/diagnostic-derive-inline.rs:61:3
|
||||
|
|
||||
|
|
@ -622,6 +630,6 @@ note: required by a bound in `Diag::<'a, G>::arg`
|
|||
= note: in this macro invocation
|
||||
= note: this error originates in the macro `with_fn` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
|
||||
error: aborting due to 79 previous errors
|
||||
error: aborting due to 80 previous errors
|
||||
|
||||
For more information about this error, try `rustc --explain E0277`.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue