| 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 { | |
| traverse_pattern(elt, &mut refs); | |
| } | |
| } | |
| for attr in &msg.attributes { | |
| for elt in &attr.value.elements { | |
| traverse_pattern(elt, &mut refs); | |
| } | |
| } | |
| fn traverse_pattern<'a>(elem: &PatternElement<&'a str>, refs: &mut Vec<&'a str>) { | |
| match elem { | |
| PatternElement::TextElement { .. } => {} | |
| PatternElement::Placeable { expression } => traverse_expression(expression, refs), | |
| } | |
| } | |
| fn traverse_expression<'a>(expr: &Expression<&'a str>, refs: &mut Vec<&'a str>) { | |
| match expr { | |
| Expression::Select { selector, variants } => { | |
| traverse_inline_expr(selector, refs); | |
| for variant in variants { | |
| for pattern in &variant.value.elements { | |
| traverse_pattern(pattern, refs); | |
| } | |
| } | |
| } | |
| Expression::Inline(expr) => { | |
| traverse_inline_expr(expr, refs); | |
| } | |
| } | |
| } | |
| fn traverse_inline_expr<'a>(elem: &InlineExpression<&'a str>, refs: &mut Vec<&'a str>) { | |
| match elem { | |
| InlineExpression::VariableReference { id } => 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", | |
| "ThinLTO", | |
| "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; | |
| } | |
| } | |
| } |