Skip to content

Commit 9b5574f

Browse files
committed
Validate fluent variable references with debug_assertions
1 parent be72f25 commit 9b5574f

File tree

2 files changed

+106
-7
lines changed

2 files changed

+106
-7
lines changed

compiler/rustc_fluent_macro/src/fluent.rs

+42-3
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,8 @@ pub(crate) fn fluent_messages(input: proc_macro::TokenStream) -> proc_macro::Tok
179179
let mut previous_defns = HashMap::new();
180180
let mut message_refs = Vec::new();
181181
for entry in resource.entries() {
182-
if let Entry::Message(Message { id: Identifier { name }, attributes, value, .. }) = entry {
182+
if let Entry::Message(msg) = entry {
183+
let Message { id: Identifier { name }, attributes, value, .. } = msg;
183184
let _ = previous_defns.entry(name.to_string()).or_insert(resource_span);
184185
if name.contains('-') {
185186
Diagnostic::spanned(
@@ -229,9 +230,10 @@ pub(crate) fn fluent_messages(input: proc_macro::TokenStream) -> proc_macro::Tok
229230
continue;
230231
}
231232

232-
let msg = format!("Constant referring to Fluent message `{name}` from `{crate_name}`");
233+
let docstr =
234+
format!("Constant referring to Fluent message `{name}` from `{crate_name}`");
233235
constants.extend(quote! {
234-
#[doc = #msg]
236+
#[doc = #docstr]
235237
pub const #snake_name: crate::DiagnosticMessage =
236238
crate::DiagnosticMessage::FluentIdentifier(
237239
std::borrow::Cow::Borrowed(#name),
@@ -269,6 +271,17 @@ pub(crate) fn fluent_messages(input: proc_macro::TokenStream) -> proc_macro::Tok
269271
);
270272
});
271273
}
274+
#[cfg(debug_assertions)]
275+
{
276+
// Record variables referenced by these messages so we can produce
277+
// tests in the derive diagnostics to validate them.
278+
let ident = quote::format_ident!("{snake_name}_refs");
279+
let vrefs = variable_references(msg);
280+
constants.extend(quote! {
281+
#[cfg(test)]
282+
pub const #ident: &[&str] = &[#(#vrefs),*];
283+
})
284+
}
272285
}
273286
}
274287

@@ -334,3 +347,29 @@ pub(crate) fn fluent_messages(input: proc_macro::TokenStream) -> proc_macro::Tok
334347
}
335348
.into()
336349
}
350+
351+
#[cfg(debug_assertions)]
352+
fn variable_references<'a>(msg: &Message<&'a str>) -> Vec<&'a str> {
353+
let mut refs = vec![];
354+
if let Some(Pattern { elements }) = &msg.value {
355+
for elt in elements {
356+
if let PatternElement::Placeable {
357+
expression: Expression::Inline(InlineExpression::VariableReference { id }),
358+
} = elt
359+
{
360+
refs.push(id.name);
361+
}
362+
}
363+
}
364+
for attr in &msg.attributes {
365+
for elt in &attr.value.elements {
366+
if let PatternElement::Placeable {
367+
expression: Expression::Inline(InlineExpression::VariableReference { id }),
368+
} = elt
369+
{
370+
refs.push(id.name);
371+
}
372+
}
373+
}
374+
refs
375+
}

compiler/rustc_macros/src/diagnostics/diagnostic.rs

+64-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#![deny(unused_must_use)]
22

3+
use std::cell::RefCell;
4+
35
use crate::diagnostics::diagnostic_builder::{DiagnosticDeriveBuilder, DiagnosticDeriveKind};
46
use crate::diagnostics::error::{span_err, DiagnosticDeriveError};
57
use crate::diagnostics::utils::SetOnce;
@@ -28,6 +30,7 @@ impl<'a> DiagnosticDerive<'a> {
2830
pub(crate) fn into_tokens(self) -> TokenStream {
2931
let DiagnosticDerive { mut structure, mut builder } = self;
3032

33+
let slugs = RefCell::new(Vec::new());
3134
let implementation = builder.each_variant(&mut structure, |mut builder, variant| {
3235
let preamble = builder.preamble(variant);
3336
let body = builder.body(variant);
@@ -56,6 +59,7 @@ impl<'a> DiagnosticDerive<'a> {
5659
return DiagnosticDeriveError::ErrorHandled.to_compile_error();
5760
}
5861
Some(slug) => {
62+
slugs.borrow_mut().push(slug.clone());
5963
quote! {
6064
let mut #diag = #handler.struct_diagnostic(crate::fluent_generated::#slug);
6165
}
@@ -73,7 +77,8 @@ impl<'a> DiagnosticDerive<'a> {
7377
});
7478

7579
let DiagnosticDeriveKind::Diagnostic { handler } = &builder.kind else { unreachable!() };
76-
structure.gen_impl(quote! {
80+
#[allow(unused_mut)]
81+
let mut imp = structure.gen_impl(quote! {
7782
gen impl<'__diagnostic_handler_sess, G>
7883
rustc_errors::IntoDiagnostic<'__diagnostic_handler_sess, G>
7984
for @Self
@@ -89,7 +94,14 @@ impl<'a> DiagnosticDerive<'a> {
8994
#implementation
9095
}
9196
}
92-
})
97+
});
98+
#[cfg(debug_assertions)]
99+
{
100+
for test in slugs.borrow().iter().map(|s| generate_test(s, &structure)) {
101+
imp.extend(test);
102+
}
103+
}
104+
imp
93105
}
94106
}
95107

@@ -124,6 +136,7 @@ impl<'a> LintDiagnosticDerive<'a> {
124136
}
125137
});
126138

139+
let slugs = RefCell::new(Vec::new());
127140
let msg = builder.each_variant(&mut structure, |mut builder, variant| {
128141
// Collect the slug by generating the preamble.
129142
let _ = builder.preamble(variant);
@@ -148,6 +161,7 @@ impl<'a> LintDiagnosticDerive<'a> {
148161
DiagnosticDeriveError::ErrorHandled.to_compile_error()
149162
}
150163
Some(slug) => {
164+
slugs.borrow_mut().push(slug.clone());
151165
quote! {
152166
crate::fluent_generated::#slug.into()
153167
}
@@ -156,7 +170,8 @@ impl<'a> LintDiagnosticDerive<'a> {
156170
});
157171

158172
let diag = &builder.diag;
159-
structure.gen_impl(quote! {
173+
#[allow(unused_mut)]
174+
let mut imp = structure.gen_impl(quote! {
160175
gen impl<'__a> rustc_errors::DecorateLint<'__a, ()> for @Self {
161176
#[track_caller]
162177
fn decorate_lint<'__b>(
@@ -171,7 +186,14 @@ impl<'a> LintDiagnosticDerive<'a> {
171186
#msg
172187
}
173188
}
174-
})
189+
});
190+
#[cfg(debug_assertions)]
191+
{
192+
for test in slugs.borrow().iter().map(|s| generate_test(s, &structure)) {
193+
imp.extend(test);
194+
}
195+
}
196+
imp
175197
}
176198
}
177199

@@ -198,3 +220,41 @@ impl Mismatch {
198220
}
199221
}
200222
}
223+
224+
/// Generates a `#[test]` that verifies that all referenced variables
225+
/// exist on this structure.
226+
#[cfg(debug_assertions)]
227+
fn generate_test(slug: &syn::Path, structure: &Structure<'_>) -> TokenStream {
228+
// FIXME: We can't identify variables in a subdiagnostic
229+
for field in structure.variants().iter().flat_map(|v| v.ast().fields.iter()) {
230+
for attr_name in field.attrs.iter().filter_map(|at| at.path().get_ident()) {
231+
if attr_name == "subdiagnostic" {
232+
return quote!();
233+
}
234+
}
235+
}
236+
use std::sync::atomic::{AtomicUsize, Ordering};
237+
// We need to make sure that the same diagnostic slug can be used multiple times without causing an
238+
// error, so just have a global counter here.
239+
static COUNTER: AtomicUsize = AtomicUsize::new(0);
240+
let slug = slug.get_ident().unwrap();
241+
let ident = quote::format_ident!("verify_{slug}_{}", COUNTER.fetch_add(1, Ordering::Relaxed));
242+
let ref_slug = quote::format_ident!("{slug}_refs");
243+
let struct_name = &structure.ast().ident;
244+
let variables: Vec<_> = structure
245+
.variants()
246+
.iter()
247+
.flat_map(|v| v.ast().fields.iter().filter_map(|f| f.ident.as_ref().map(|i| i.to_string())))
248+
.collect();
249+
// tidy errors on `#[test]` outside of test files, so we use `#[test ]` to work around this
250+
quote! {
251+
#[cfg(test)]
252+
#[test ]
253+
fn #ident() {
254+
let variables = [#(#variables),*];
255+
for vref in crate::fluent_generated::#ref_slug {
256+
assert!(variables.contains(vref), "{}: variable `{vref}` not found ({})", stringify!(#struct_name), stringify!(#slug));
257+
}
258+
}
259+
}
260+
}

0 commit comments

Comments
 (0)