Skip to content

Commit ca2be71

Browse files
Add verification for inline fluent messages
1 parent 523d9d9 commit ca2be71

File tree

9 files changed

+181
-71
lines changed

9 files changed

+181
-71
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4229,6 +4229,8 @@ dependencies = [
42294229
name = "rustc_macros"
42304230
version = "0.0.0"
42314231
dependencies = [
4232+
"fluent-bundle",
4233+
"fluent-syntax",
42324234
"proc-macro2",
42334235
"quote",
42344236
"syn 2.0.110",

compiler/rustc_macros/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ proc-macro = true
88

99
[dependencies]
1010
# tidy-alphabetical-start
11+
fluent-bundle = "0.16"
12+
fluent-syntax = "0.12"
1113
proc-macro2 = "1"
1214
quote = "1"
1315
syn = { version = "2.0.9", features = ["full"] }

compiler/rustc_macros/src/diagnostics/diagnostic.rs

Lines changed: 8 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,16 @@ impl<'a> DiagnosticDerive<'a> {
2222
pub(crate) fn into_tokens(self) -> TokenStream {
2323
let DiagnosticDerive { mut structure } = self;
2424
let kind = DiagnosticDeriveKind::Diagnostic;
25-
let slugs = RefCell::new(Vec::new());
25+
let messages = RefCell::new(Vec::new());
2626
let implementation = kind.each_variant(&mut structure, |mut builder, variant| {
2727
let preamble = builder.preamble(variant);
2828
let body = builder.body(variant);
2929

3030
let Some(message) = builder.primary_message() else {
3131
return DiagnosticDeriveError::ErrorHandled.to_compile_error();
3232
};
33-
slugs.borrow_mut().extend(message.slug().cloned());
34-
let message = message.diag_message();
33+
messages.borrow_mut().push(message.clone());
34+
let message = message.diag_message(variant);
3535

3636
let init = quote! {
3737
let mut diag = rustc_errors::Diag::new(
@@ -68,7 +68,7 @@ impl<'a> DiagnosticDerive<'a> {
6868
}
6969
}
7070
});
71-
for test in slugs.borrow().iter().map(|s| generate_test(s, &structure)) {
71+
for test in messages.borrow().iter().map(|s| s.generate_test(&structure)) {
7272
imp.extend(test);
7373
}
7474
imp
@@ -88,16 +88,16 @@ impl<'a> LintDiagnosticDerive<'a> {
8888
pub(crate) fn into_tokens(self) -> TokenStream {
8989
let LintDiagnosticDerive { mut structure } = self;
9090
let kind = DiagnosticDeriveKind::LintDiagnostic;
91-
let slugs = RefCell::new(Vec::new());
91+
let messages = RefCell::new(Vec::new());
9292
let implementation = kind.each_variant(&mut structure, |mut builder, variant| {
9393
let preamble = builder.preamble(variant);
9494
let body = builder.body(variant);
9595

9696
let Some(message) = builder.primary_message() else {
9797
return DiagnosticDeriveError::ErrorHandled.to_compile_error();
9898
};
99-
slugs.borrow_mut().extend(message.slug().cloned());
100-
let message = message.diag_message();
99+
messages.borrow_mut().push(message.clone());
100+
let message = message.diag_message(variant);
101101
let primary_message = quote! {
102102
diag.primary_message(#message);
103103
};
@@ -125,47 +125,10 @@ impl<'a> LintDiagnosticDerive<'a> {
125125
}
126126
}
127127
});
128-
for test in slugs.borrow().iter().map(|s| generate_test(s, &structure)) {
128+
for test in messages.borrow().iter().map(|s| s.generate_test(&structure)) {
129129
imp.extend(test);
130130
}
131131

132132
imp
133133
}
134134
}
135-
136-
/// Generates a `#[test]` that verifies that all referenced variables
137-
/// exist on this structure.
138-
fn generate_test(slug: &syn::Path, structure: &Structure<'_>) -> TokenStream {
139-
// FIXME: We can't identify variables in a subdiagnostic
140-
for field in structure.variants().iter().flat_map(|v| v.ast().fields.iter()) {
141-
for attr_name in field.attrs.iter().filter_map(|at| at.path().get_ident()) {
142-
if attr_name == "subdiagnostic" {
143-
return quote!();
144-
}
145-
}
146-
}
147-
use std::sync::atomic::{AtomicUsize, Ordering};
148-
// We need to make sure that the same diagnostic slug can be used multiple times without
149-
// causing an error, so just have a global counter here.
150-
static COUNTER: AtomicUsize = AtomicUsize::new(0);
151-
let slug = slug.get_ident().unwrap();
152-
let ident = quote::format_ident!("verify_{slug}_{}", COUNTER.fetch_add(1, Ordering::Relaxed));
153-
let ref_slug = quote::format_ident!("{slug}_refs");
154-
let struct_name = &structure.ast().ident;
155-
let variables: Vec<_> = structure
156-
.variants()
157-
.iter()
158-
.flat_map(|v| v.ast().fields.iter().filter_map(|f| f.ident.as_ref().map(|i| i.to_string())))
159-
.collect();
160-
// tidy errors on `#[test]` outside of test files, so we use `#[test ]` to work around this
161-
quote! {
162-
#[cfg(test)]
163-
#[test ]
164-
fn #ident() {
165-
let variables = [#(#variables),*];
166-
for vref in crate::fluent_generated::#ref_slug {
167-
assert!(variables.contains(vref), "{}: variable `{vref}` not found ({})", stringify!(#struct_name), stringify!(#slug));
168-
}
169-
}
170-
}
171-
}

compiler/rustc_macros/src/diagnostics/diagnostic_builder.rs

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,8 @@ impl DiagnosticDeriveVariantBuilder {
137137
let ast = variant.ast();
138138
let attrs = &ast.attrs;
139139
let preamble = attrs.iter().map(|attr| {
140-
self.generate_structure_code_for_attr(attr).unwrap_or_else(|v| v.to_compile_error())
140+
self.generate_structure_code_for_attr(attr, variant)
141+
.unwrap_or_else(|v| v.to_compile_error())
141142
});
142143

143144
quote! {
@@ -155,7 +156,7 @@ impl DiagnosticDeriveVariantBuilder {
155156
}
156157
// ..and then subdiagnostic additions.
157158
for binding in variant.bindings().iter().filter(|bi| !should_generate_arg(bi.ast())) {
158-
body.extend(self.generate_field_attrs_code(binding));
159+
body.extend(self.generate_field_attrs_code(binding, variant));
159160
}
160161
body
161162
}
@@ -199,6 +200,7 @@ impl DiagnosticDeriveVariantBuilder {
199200
fn generate_structure_code_for_attr(
200201
&mut self,
201202
attr: &Attribute,
203+
variant: &VariantInfo<'_>,
202204
) -> Result<TokenStream, DiagnosticDeriveError> {
203205
// Always allow documentation comments.
204206
if is_doc_comment(attr) {
@@ -224,7 +226,7 @@ impl DiagnosticDeriveVariantBuilder {
224226
)
225227
.emit();
226228
}
227-
self.message = Some(Message::Inline(message.value()));
229+
self.message = Some(Message::Inline(message.span(), message.value()));
228230
} else {
229231
// Parse a slug
230232
let slug = input.parse::<Path>()?;
@@ -285,7 +287,7 @@ impl DiagnosticDeriveVariantBuilder {
285287
| SubdiagnosticKind::NoteOnce
286288
| SubdiagnosticKind::Help
287289
| SubdiagnosticKind::HelpOnce
288-
| SubdiagnosticKind::Warn => Ok(self.add_subdiagnostic(&fn_ident, slug)),
290+
| SubdiagnosticKind::Warn => Ok(self.add_subdiagnostic(&fn_ident, slug, variant)),
289291
SubdiagnosticKind::Label | SubdiagnosticKind::Suggestion { .. } => {
290292
throw_invalid_attr!(attr, |diag| diag
291293
.help("`#[label]` and `#[suggestion]` can only be applied to fields"));
@@ -313,7 +315,11 @@ impl DiagnosticDeriveVariantBuilder {
313315
}
314316
}
315317

316-
fn generate_field_attrs_code(&mut self, binding_info: &BindingInfo<'_>) -> TokenStream {
318+
fn generate_field_attrs_code(
319+
&mut self,
320+
binding_info: &BindingInfo<'_>,
321+
variant: &VariantInfo<'_>,
322+
) -> TokenStream {
317323
let field = binding_info.ast();
318324
let field_binding = &binding_info.binding;
319325

@@ -352,6 +358,7 @@ impl DiagnosticDeriveVariantBuilder {
352358
attr,
353359
FieldInfo { binding: binding_info, ty: inner_ty, span: &field.span() },
354360
binding,
361+
variant
355362
)
356363
.unwrap_or_else(|v| v.to_compile_error());
357364

@@ -369,6 +376,7 @@ impl DiagnosticDeriveVariantBuilder {
369376
attr: &Attribute,
370377
info: FieldInfo<'_>,
371378
binding: TokenStream,
379+
variant: &VariantInfo<'_>,
372380
) -> Result<TokenStream, DiagnosticDeriveError> {
373381
let ident = &attr.path().segments.last().unwrap().ident;
374382
let name = ident.to_string();
@@ -407,7 +415,7 @@ impl DiagnosticDeriveVariantBuilder {
407415
match subdiag {
408416
SubdiagnosticKind::Label => {
409417
report_error_if_not_applied_to_span(attr, &info)?;
410-
Ok(self.add_spanned_subdiagnostic(binding, &fn_ident, slug))
418+
Ok(self.add_spanned_subdiagnostic(binding, &fn_ident, slug, variant))
411419
}
412420
SubdiagnosticKind::Note
413421
| SubdiagnosticKind::NoteOnce
@@ -418,11 +426,11 @@ impl DiagnosticDeriveVariantBuilder {
418426
if type_matches_path(inner, &["rustc_span", "Span"])
419427
|| type_matches_path(inner, &["rustc_span", "MultiSpan"])
420428
{
421-
Ok(self.add_spanned_subdiagnostic(binding, &fn_ident, slug))
429+
Ok(self.add_spanned_subdiagnostic(binding, &fn_ident, slug, variant))
422430
} else if type_is_unit(inner)
423431
|| (matches!(info.ty, FieldInnerTy::Plain(_)) && type_is_bool(inner))
424432
{
425-
Ok(self.add_subdiagnostic(&fn_ident, slug))
433+
Ok(self.add_subdiagnostic(&fn_ident, slug, variant))
426434
} else {
427435
report_type_error(attr, "`Span`, `MultiSpan`, `bool` or `()`")?
428436
}
@@ -448,7 +456,7 @@ impl DiagnosticDeriveVariantBuilder {
448456
applicability.set_once(quote! { #static_applicability }, span);
449457
}
450458

451-
let message = slug.diag_message();
459+
let message = slug.diag_message(variant);
452460
let applicability = applicability
453461
.value()
454462
.unwrap_or_else(|| quote! { rustc_errors::Applicability::Unspecified });
@@ -476,9 +484,10 @@ impl DiagnosticDeriveVariantBuilder {
476484
field_binding: TokenStream,
477485
kind: &Ident,
478486
message: Message,
487+
variant: &VariantInfo<'_>,
479488
) -> TokenStream {
480489
let fn_name = format_ident!("span_{}", kind);
481-
let message = message.diag_message();
490+
let message = message.diag_message(variant);
482491
quote! {
483492
diag.#fn_name(
484493
#field_binding,
@@ -489,8 +498,13 @@ impl DiagnosticDeriveVariantBuilder {
489498

490499
/// Adds a subdiagnostic by generating a `diag.span_$kind` call with the current slug
491500
/// and `fluent_attr_identifier`.
492-
fn add_subdiagnostic(&self, kind: &Ident, message: Message) -> TokenStream {
493-
let message = message.diag_message();
501+
fn add_subdiagnostic(
502+
&self,
503+
kind: &Ident,
504+
message: Message,
505+
variant: &VariantInfo<'_>,
506+
) -> TokenStream {
507+
let message = message.diag_message(variant);
494508
quote! {
495509
diag.#kind(#message);
496510
}
Lines changed: 114 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,133 @@
1-
use proc_macro2::TokenStream;
1+
use fluent_bundle::FluentResource;
2+
use fluent_syntax::ast::{Expression, InlineExpression, Pattern, PatternElement};
3+
use proc_macro2::{Span, TokenStream};
24
use quote::quote;
35
use syn::Path;
6+
use synstructure::{Structure, VariantInfo};
47

8+
use crate::diagnostics::error::span_err;
9+
10+
#[derive(Clone)]
511
pub(crate) enum Message {
612
Slug(Path),
7-
Inline(String),
13+
Inline(Span, String),
814
}
915

1016
impl Message {
11-
pub(crate) fn slug(&self) -> Option<&Path> {
17+
pub(crate) fn diag_message(&self, variant: &VariantInfo<'_>) -> TokenStream {
1218
match self {
13-
Message::Slug(slug) => Some(slug),
14-
Message::Inline(_) => None,
19+
Message::Slug(slug) => {
20+
quote! { crate::fluent_generated::#slug }
21+
}
22+
Message::Inline(message_span, message) => {
23+
verify_fluent_message(*message_span, &message, variant);
24+
quote! { rustc_errors::DiagMessage::Inline(std::borrow::Cow::Borrowed(#message)) }
25+
}
1526
}
1627
}
1728

18-
pub(crate) fn diag_message(&self) -> TokenStream {
29+
/// Generates a `#[test]` that verifies that all referenced variables
30+
/// exist on this structure.
31+
pub(crate) fn generate_test(&self, structure: &Structure<'_>) -> TokenStream {
1932
match self {
2033
Message::Slug(slug) => {
21-
quote! { crate::fluent_generated::#slug }
34+
// FIXME: We can't identify variables in a subdiagnostic
35+
for field in structure.variants().iter().flat_map(|v| v.ast().fields.iter()) {
36+
for attr_name in field.attrs.iter().filter_map(|at| at.path().get_ident()) {
37+
if attr_name == "subdiagnostic" {
38+
return quote!();
39+
}
40+
}
41+
}
42+
use std::sync::atomic::{AtomicUsize, Ordering};
43+
// We need to make sure that the same diagnostic slug can be used multiple times without
44+
// causing an error, so just have a global counter here.
45+
static COUNTER: AtomicUsize = AtomicUsize::new(0);
46+
let slug = slug.get_ident().unwrap();
47+
let ident = quote::format_ident!(
48+
"verify_{slug}_{}",
49+
COUNTER.fetch_add(1, Ordering::Relaxed)
50+
);
51+
let ref_slug = quote::format_ident!("{slug}_refs");
52+
let struct_name = &structure.ast().ident;
53+
let variables: Vec<_> = structure
54+
.variants()
55+
.iter()
56+
.flat_map(|v| {
57+
v.ast()
58+
.fields
59+
.iter()
60+
.filter_map(|f| f.ident.as_ref().map(|i| i.to_string()))
61+
})
62+
.collect();
63+
// tidy errors on `#[test]` outside of test files, so we use `#[test ]` to work around this
64+
quote! {
65+
#[cfg(test)]
66+
#[test ]
67+
fn #ident() {
68+
let variables = [#(#variables),*];
69+
for vref in crate::fluent_generated::#ref_slug {
70+
assert!(variables.contains(vref), "{}: variable `{vref}` not found ({})", stringify!(#struct_name), stringify!(#slug));
71+
}
72+
}
73+
}
2274
}
23-
Message::Inline(message) => {
24-
quote! { rustc_errors::DiagMessage::Inline(std::borrow::Cow::Borrowed(#message)) }
75+
Message::Inline(..) => {
76+
// We don't generate a test for inline diagnostics, we can verify these at compile-time!
77+
// This verification is done in the `diag_message` function above
78+
quote! {}
79+
}
80+
}
81+
}
82+
}
83+
84+
fn verify_fluent_message(msg_span: Span, message: &str, variant: &VariantInfo<'_>) {
85+
// Parse the fluent message
86+
const GENERATED_MSG_ID: &str = "generated_msg";
87+
let resource = FluentResource::try_new(format!("{GENERATED_MSG_ID} = {message}\n")).unwrap();
88+
assert_eq!(resource.entries().count(), 1);
89+
let Some(fluent_syntax::ast::Entry::Message(message)) = resource.get_entry(0) else {
90+
panic!("Did not parse into a message")
91+
};
92+
93+
// Check if all variables are used
94+
let fields: Vec<String> = variant
95+
.bindings()
96+
.iter()
97+
.flat_map(|b| b.ast().ident.as_ref())
98+
.map(|id| id.to_string())
99+
.collect();
100+
for variable in variable_references(&message) {
101+
if !fields.iter().any(|f| f == variable) {
102+
span_err(msg_span.unwrap(), format!("Variable `{variable}` not found in diagnostic "))
103+
.help(format!("Available fields: {:?}", fields.join(", ")))
104+
.emit();
105+
}
106+
// assert!(, );
107+
}
108+
}
109+
110+
fn variable_references<'a>(msg: &fluent_syntax::ast::Message<&'a str>) -> Vec<&'a str> {
111+
let mut refs = vec![];
112+
if let Some(Pattern { elements }) = &msg.value {
113+
for elt in elements {
114+
if let PatternElement::Placeable {
115+
expression: Expression::Inline(InlineExpression::VariableReference { id }),
116+
} = elt
117+
{
118+
refs.push(id.name);
119+
}
120+
}
121+
}
122+
for attr in &msg.attributes {
123+
for elt in &attr.value.elements {
124+
if let PatternElement::Placeable {
125+
expression: Expression::Inline(InlineExpression::VariableReference { id }),
126+
} = elt
127+
{
128+
refs.push(id.name);
25129
}
26130
}
27131
}
132+
refs
28133
}

0 commit comments

Comments
 (0)