blob: 153abecf89372560421fada55ed9cca3a1532fae [file] [log] [blame]
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(Span, String),
}
impl Message {
pub(crate) fn diag_message(&self, variant: &VariantInfo<'_>) -> TokenStream {
match self {
Message::Slug(slug) => {
quote! { crate::fluent_generated::#slug }
}
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
}