mirror of
https://github.com/rust-lang/rust.git
synced 2024-11-22 06:44:35 +00:00
Auto merge of #111269 - clubby789:validate-fluent-variables, r=davidtwco
Validate fluent variable references in tests Closes #101109 Under `cfg(test)`, the `fluent_messages` macro will emit a list of variables referenced by each message and its attributes. The derive attribute will now emit a `#[test]` that checks that each referenced variable exists in the structure it's applied to.
This commit is contained in:
commit
08fd6f719e
@ -179,7 +179,8 @@ pub(crate) fn fluent_messages(input: proc_macro::TokenStream) -> proc_macro::Tok
|
|||||||
let mut previous_defns = HashMap::new();
|
let mut previous_defns = HashMap::new();
|
||||||
let mut message_refs = Vec::new();
|
let mut message_refs = Vec::new();
|
||||||
for entry in resource.entries() {
|
for entry in resource.entries() {
|
||||||
if let Entry::Message(Message { id: Identifier { name }, attributes, value, .. }) = entry {
|
if let Entry::Message(msg) = entry {
|
||||||
|
let Message { id: Identifier { name }, attributes, value, .. } = msg;
|
||||||
let _ = previous_defns.entry(name.to_string()).or_insert(resource_span);
|
let _ = previous_defns.entry(name.to_string()).or_insert(resource_span);
|
||||||
if name.contains('-') {
|
if name.contains('-') {
|
||||||
Diagnostic::spanned(
|
Diagnostic::spanned(
|
||||||
@ -229,9 +230,10 @@ pub(crate) fn fluent_messages(input: proc_macro::TokenStream) -> proc_macro::Tok
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let msg = format!("Constant referring to Fluent message `{name}` from `{crate_name}`");
|
let docstr =
|
||||||
|
format!("Constant referring to Fluent message `{name}` from `{crate_name}`");
|
||||||
constants.extend(quote! {
|
constants.extend(quote! {
|
||||||
#[doc = #msg]
|
#[doc = #docstr]
|
||||||
pub const #snake_name: crate::DiagnosticMessage =
|
pub const #snake_name: crate::DiagnosticMessage =
|
||||||
crate::DiagnosticMessage::FluentIdentifier(
|
crate::DiagnosticMessage::FluentIdentifier(
|
||||||
std::borrow::Cow::Borrowed(#name),
|
std::borrow::Cow::Borrowed(#name),
|
||||||
@ -269,6 +271,15 @@ pub(crate) fn fluent_messages(input: proc_macro::TokenStream) -> proc_macro::Tok
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Record variables referenced by these messages so we can produce
|
||||||
|
// tests in the derive diagnostics to validate them.
|
||||||
|
let ident = quote::format_ident!("{snake_name}_refs");
|
||||||
|
let vrefs = variable_references(msg);
|
||||||
|
constants.extend(quote! {
|
||||||
|
#[cfg(test)]
|
||||||
|
pub const #ident: &[&str] = &[#(#vrefs),*];
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -334,3 +345,28 @@ pub(crate) fn fluent_messages(input: proc_macro::TokenStream) -> proc_macro::Tok
|
|||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn variable_references<'a>(msg: &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
|
||||||
|
}
|
||||||
|
@ -137,7 +137,7 @@ hir_analysis_missing_trait_item_suggestion = implement the missing item: `{$snip
|
|||||||
|
|
||||||
hir_analysis_missing_trait_item_unstable = not all trait items implemented, missing: `{$missing_item_name}`
|
hir_analysis_missing_trait_item_unstable = not all trait items implemented, missing: `{$missing_item_name}`
|
||||||
.note = default implementation of `{$missing_item_name}` is unstable
|
.note = default implementation of `{$missing_item_name}` is unstable
|
||||||
.some_note = use of unstable library feature '{$feature}': {$r}
|
.some_note = use of unstable library feature '{$feature}': {$reason}
|
||||||
.none_note = use of unstable library feature '{$feature}'
|
.none_note = use of unstable library feature '{$feature}'
|
||||||
|
|
||||||
hir_analysis_missing_type_params =
|
hir_analysis_missing_type_params =
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
#![deny(unused_must_use)]
|
#![deny(unused_must_use)]
|
||||||
|
|
||||||
|
use std::cell::RefCell;
|
||||||
|
|
||||||
use crate::diagnostics::diagnostic_builder::{DiagnosticDeriveBuilder, DiagnosticDeriveKind};
|
use crate::diagnostics::diagnostic_builder::{DiagnosticDeriveBuilder, DiagnosticDeriveKind};
|
||||||
use crate::diagnostics::error::{span_err, DiagnosticDeriveError};
|
use crate::diagnostics::error::{span_err, DiagnosticDeriveError};
|
||||||
use crate::diagnostics::utils::SetOnce;
|
use crate::diagnostics::utils::SetOnce;
|
||||||
@ -28,6 +30,7 @@ impl<'a> DiagnosticDerive<'a> {
|
|||||||
pub(crate) fn into_tokens(self) -> TokenStream {
|
pub(crate) fn into_tokens(self) -> TokenStream {
|
||||||
let DiagnosticDerive { mut structure, mut builder } = self;
|
let DiagnosticDerive { mut structure, mut builder } = self;
|
||||||
|
|
||||||
|
let slugs = RefCell::new(Vec::new());
|
||||||
let implementation = builder.each_variant(&mut structure, |mut builder, variant| {
|
let implementation = builder.each_variant(&mut structure, |mut builder, variant| {
|
||||||
let preamble = builder.preamble(variant);
|
let preamble = builder.preamble(variant);
|
||||||
let body = builder.body(variant);
|
let body = builder.body(variant);
|
||||||
@ -56,6 +59,7 @@ impl<'a> DiagnosticDerive<'a> {
|
|||||||
return DiagnosticDeriveError::ErrorHandled.to_compile_error();
|
return DiagnosticDeriveError::ErrorHandled.to_compile_error();
|
||||||
}
|
}
|
||||||
Some(slug) => {
|
Some(slug) => {
|
||||||
|
slugs.borrow_mut().push(slug.clone());
|
||||||
quote! {
|
quote! {
|
||||||
let mut #diag = #handler.struct_diagnostic(crate::fluent_generated::#slug);
|
let mut #diag = #handler.struct_diagnostic(crate::fluent_generated::#slug);
|
||||||
}
|
}
|
||||||
@ -73,7 +77,8 @@ impl<'a> DiagnosticDerive<'a> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let DiagnosticDeriveKind::Diagnostic { handler } = &builder.kind else { unreachable!() };
|
let DiagnosticDeriveKind::Diagnostic { handler } = &builder.kind else { unreachable!() };
|
||||||
structure.gen_impl(quote! {
|
|
||||||
|
let mut imp = structure.gen_impl(quote! {
|
||||||
gen impl<'__diagnostic_handler_sess, G>
|
gen impl<'__diagnostic_handler_sess, G>
|
||||||
rustc_errors::IntoDiagnostic<'__diagnostic_handler_sess, G>
|
rustc_errors::IntoDiagnostic<'__diagnostic_handler_sess, G>
|
||||||
for @Self
|
for @Self
|
||||||
@ -89,7 +94,11 @@ impl<'a> DiagnosticDerive<'a> {
|
|||||||
#implementation
|
#implementation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
for test in slugs.borrow().iter().map(|s| generate_test(s, &structure)) {
|
||||||
|
imp.extend(test);
|
||||||
|
}
|
||||||
|
imp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,6 +133,7 @@ impl<'a> LintDiagnosticDerive<'a> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let slugs = RefCell::new(Vec::new());
|
||||||
let msg = builder.each_variant(&mut structure, |mut builder, variant| {
|
let msg = builder.each_variant(&mut structure, |mut builder, variant| {
|
||||||
// Collect the slug by generating the preamble.
|
// Collect the slug by generating the preamble.
|
||||||
let _ = builder.preamble(variant);
|
let _ = builder.preamble(variant);
|
||||||
@ -148,6 +158,7 @@ impl<'a> LintDiagnosticDerive<'a> {
|
|||||||
DiagnosticDeriveError::ErrorHandled.to_compile_error()
|
DiagnosticDeriveError::ErrorHandled.to_compile_error()
|
||||||
}
|
}
|
||||||
Some(slug) => {
|
Some(slug) => {
|
||||||
|
slugs.borrow_mut().push(slug.clone());
|
||||||
quote! {
|
quote! {
|
||||||
crate::fluent_generated::#slug.into()
|
crate::fluent_generated::#slug.into()
|
||||||
}
|
}
|
||||||
@ -156,7 +167,7 @@ impl<'a> LintDiagnosticDerive<'a> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let diag = &builder.diag;
|
let diag = &builder.diag;
|
||||||
structure.gen_impl(quote! {
|
let mut imp = structure.gen_impl(quote! {
|
||||||
gen impl<'__a> rustc_errors::DecorateLint<'__a, ()> for @Self {
|
gen impl<'__a> rustc_errors::DecorateLint<'__a, ()> for @Self {
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn decorate_lint<'__b>(
|
fn decorate_lint<'__b>(
|
||||||
@ -171,7 +182,12 @@ impl<'a> LintDiagnosticDerive<'a> {
|
|||||||
#msg
|
#msg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
for test in slugs.borrow().iter().map(|s| generate_test(s, &structure)) {
|
||||||
|
imp.extend(test);
|
||||||
|
}
|
||||||
|
|
||||||
|
imp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,3 +214,40 @@ impl Mismatch {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1150,14 +1150,6 @@ pub struct UnixSigpipeValues {
|
|||||||
pub span: Span,
|
pub span: Span,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Diagnostic)]
|
|
||||||
#[diag(passes_no_main_function, code = "E0601")]
|
|
||||||
pub struct NoMainFunction {
|
|
||||||
#[primary_span]
|
|
||||||
pub span: Span,
|
|
||||||
pub crate_name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct NoMainErr {
|
pub struct NoMainErr {
|
||||||
pub sp: Span,
|
pub sp: Span,
|
||||||
pub crate_name: Symbol,
|
pub crate_name: Symbol,
|
||||||
|
@ -3,3 +3,5 @@ no_crate_example = this is an example message used in testing
|
|||||||
.help = with a help
|
.help = with a help
|
||||||
.suggestion = with a suggestion
|
.suggestion = with a suggestion
|
||||||
.label = with a label
|
.label = with a label
|
||||||
|
|
||||||
|
no_crate_bad_reference = {$r} does not exist
|
||||||
|
21
tests/ui-fulldeps/session-diagnostic/invalid-variable.rs
Normal file
21
tests/ui-fulldeps/session-diagnostic/invalid-variable.rs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// run-fail
|
||||||
|
// compile-flags: --test
|
||||||
|
// test that messages referencing non-existent fields cause test failures
|
||||||
|
|
||||||
|
#![feature(rustc_private)]
|
||||||
|
#![crate_type = "lib"]
|
||||||
|
|
||||||
|
extern crate rustc_driver;
|
||||||
|
extern crate rustc_fluent_macro;
|
||||||
|
extern crate rustc_macros;
|
||||||
|
extern crate rustc_errors;
|
||||||
|
use rustc_fluent_macro::fluent_messages;
|
||||||
|
use rustc_macros::Diagnostic;
|
||||||
|
use rustc_errors::{SubdiagnosticMessage, DiagnosticMessage};
|
||||||
|
extern crate rustc_session;
|
||||||
|
|
||||||
|
fluent_messages! { "./example.ftl" }
|
||||||
|
|
||||||
|
#[derive(Diagnostic)]
|
||||||
|
#[diag(no_crate_bad_reference)]
|
||||||
|
struct BadRef;
|
@ -11,6 +11,10 @@ pub trait JustTrait {
|
|||||||
#[rustc_default_body_unstable(feature = "fun_default_body", issue = "none")]
|
#[rustc_default_body_unstable(feature = "fun_default_body", issue = "none")]
|
||||||
#[stable(feature = "stable_feature", since = "1.0.0")]
|
#[stable(feature = "stable_feature", since = "1.0.0")]
|
||||||
fn fun() {}
|
fn fun() {}
|
||||||
|
|
||||||
|
#[rustc_default_body_unstable(feature = "fun_default_body", issue = "none", reason = "reason")]
|
||||||
|
#[stable(feature = "stable_feature", since = "1.0.0")]
|
||||||
|
fn fun2() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rustc_must_implement_one_of(eq, neq)]
|
#[rustc_must_implement_one_of(eq, neq)]
|
||||||
|
@ -10,6 +10,7 @@ struct Type;
|
|||||||
impl JustTrait for Type {}
|
impl JustTrait for Type {}
|
||||||
//~^ ERROR not all trait items implemented, missing: `CONSTANT` [E0046]
|
//~^ ERROR not all trait items implemented, missing: `CONSTANT` [E0046]
|
||||||
//~| ERROR not all trait items implemented, missing: `fun` [E0046]
|
//~| ERROR not all trait items implemented, missing: `fun` [E0046]
|
||||||
|
//~| ERROR not all trait items implemented, missing: `fun2` [E0046]
|
||||||
|
|
||||||
impl Equal for Type {
|
impl Equal for Type {
|
||||||
//~^ ERROR not all trait items implemented, missing: `eq` [E0046]
|
//~^ ERROR not all trait items implemented, missing: `eq` [E0046]
|
||||||
|
@ -18,8 +18,18 @@ LL | impl JustTrait for Type {}
|
|||||||
= note: use of unstable library feature 'fun_default_body'
|
= note: use of unstable library feature 'fun_default_body'
|
||||||
= help: add `#![feature(fun_default_body)]` to the crate attributes to enable
|
= help: add `#![feature(fun_default_body)]` to the crate attributes to enable
|
||||||
|
|
||||||
|
error[E0046]: not all trait items implemented, missing: `fun2`
|
||||||
|
--> $DIR/default-body-stability-err.rs:10:1
|
||||||
|
|
|
||||||
|
LL | impl JustTrait for Type {}
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
|
||||||
|
= note: default implementation of `fun2` is unstable
|
||||||
|
= note: use of unstable library feature 'fun_default_body': reason
|
||||||
|
= help: add `#![feature(fun_default_body)]` to the crate attributes to enable
|
||||||
|
|
||||||
error[E0046]: not all trait items implemented, missing: `eq`
|
error[E0046]: not all trait items implemented, missing: `eq`
|
||||||
--> $DIR/default-body-stability-err.rs:14:1
|
--> $DIR/default-body-stability-err.rs:15:1
|
||||||
|
|
|
|
||||||
LL | / impl Equal for Type {
|
LL | / impl Equal for Type {
|
||||||
LL | |
|
LL | |
|
||||||
@ -33,6 +43,6 @@ LL | | }
|
|||||||
= note: use of unstable library feature 'eq_default_body'
|
= note: use of unstable library feature 'eq_default_body'
|
||||||
= help: add `#![feature(eq_default_body)]` to the crate attributes to enable
|
= help: add `#![feature(eq_default_body)]` to the crate attributes to enable
|
||||||
|
|
||||||
error: aborting due to 3 previous errors
|
error: aborting due to 4 previous errors
|
||||||
|
|
||||||
For more information about this error, try `rustc --explain E0046`.
|
For more information about this error, try `rustc --explain E0046`.
|
||||||
|
@ -12,6 +12,8 @@ impl JustTrait for Type {
|
|||||||
const CONSTANT: usize = 1;
|
const CONSTANT: usize = 1;
|
||||||
|
|
||||||
fn fun() {}
|
fn fun() {}
|
||||||
|
|
||||||
|
fn fun2() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Equal for Type {
|
impl Equal for Type {
|
||||||
|
Loading…
Reference in New Issue
Block a user