blob: 3276abfce4132ebf67268bd435785f3bf599fbce [file] [log] [blame] [edit]
use fluent_bundle::FluentResource;
use fluent_syntax::ast::{Expression, InlineExpression, Pattern, PatternElement};
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::ext::IdentExt;
use synstructure::VariantInfo;
use crate::diagnostics::error::span_err;
#[derive(Clone)]
pub(crate) struct Message {
pub attr_span: Span,
pub message_span: Span,
pub value: String,
}
impl Message {
/// Get the diagnostic message for this diagnostic
/// The passed `variant` is used to check whether all variables in the message are used.
/// For subdiagnostics, we cannot check this.
pub(crate) fn diag_message(&self, variant: Option<&VariantInfo<'_>>) -> TokenStream {
let message = &self.value;
self.verify(variant);
quote! { rustc_errors::DiagMessage::Inline(std::borrow::Cow::Borrowed(#message)) }
}
fn verify(&self, variant: Option<&VariantInfo<'_>>) {
verify_variables_used(self.message_span, &self.value, variant);
verify_message_style(self.message_span, &self.value);
verify_message_formatting(self.attr_span, self.message_span, &self.value);
}
}
fn verify_variables_used(msg_span: Span, message_str: &str, variant: Option<&VariantInfo<'_>>) {
// Parse the fluent message
const GENERATED_MSG_ID: &str = "generated_msg";
let resource =
FluentResource::try_new(format!("{GENERATED_MSG_ID} = {message_str}\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
if let Some(variant) = variant {
let fields: Vec<String> = variant
.bindings()
.iter()
.flat_map(|b| b.ast().ident.as_ref())
.map(|id| id.unraw().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();
}
}
}
}
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
}
const ALLOWED_CAPITALIZED_WORDS: &[&str] = &[
// tidy-alphabetical-start
"ABI",
"ABIs",
"ADT",
"C-variadic",
"CGU-reuse",
"Cargo",
"Ferris",
"GCC",
"MIR",
"NaNs",
"OK",
"Rust",
"Unicode",
"VS",
// tidy-alphabetical-end
];
/// See: https://rustc-dev-guide.rust-lang.org/diagnostics.html#diagnostic-output-style-guide
fn verify_message_style(msg_span: Span, message: &str) {
// Verify that message starts with lowercase char
let Some(first_word) = message.split_whitespace().next() else {
span_err(msg_span.unwrap(), "message must not be empty").emit();
return;
};
let first_char = first_word.chars().next().expect("Word is not empty");
if first_char.is_uppercase() && !ALLOWED_CAPITALIZED_WORDS.contains(&first_word) {
span_err(msg_span.unwrap(), "message `{value}` starts with an uppercase letter. Fix it or add it to `ALLOWED_CAPITALIZED_WORDS`").emit();
return;
}
// Verify that message does not end in `.`
if message.ends_with(".") && !message.ends_with("...") {
span_err(msg_span.unwrap(), "message `{value}` ends with a period").emit();
return;
}
}
/// Verifies that the message is properly indented into the code
fn verify_message_formatting(attr_span: Span, msg_span: Span, message: &str) {
// Find the indent at the start of the message (`column()` is one-indexed)
let start = attr_span.unwrap().column() - 1;
for line in message.lines().skip(1) {
if line.is_empty() {
continue;
}
let indent = line.chars().take_while(|c| *c == ' ').count();
if indent < start {
span_err(
msg_span.unwrap(),
format!("message is not properly indented. {indent} < {start}"),
)
.emit();
return;
}
if indent % 4 != 0 {
span_err(msg_span.unwrap(), "message is not indented with a multiple of 4 spaces")
.emit();
return;
}
}
}