| use std::collections::HashSet; | |
| use fluent_bundle::FluentResource; | |
| use fluent_syntax::ast::{Expression, InlineExpression, Pattern, PatternElement}; | |
| use indexmap::IndexMap; | |
| use proc_macro2::{Span, TokenStream}; | |
| use quote::quote; | |
| use syn::ext::IdentExt; | |
| use crate::diagnostics::error::span_err; | |
| use crate::diagnostics::utils::FieldMap; | |
| #[derive(Clone)] | |
| pub(crate) struct Message { | |
| pub attr_span: Span, | |
| pub message_span: Span, | |
| pub value: String, | |
| } | |
| impl Message { | |
| pub(crate) fn new( | |
| attr_span: Span, | |
| message_span: Span, | |
| message_str: String, | |
| field_map: &FieldMap, | |
| used_fields: &mut HashSet<proc_macro2::Ident>, | |
| ) -> Self { | |
| // 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(flt_message)) = resource.get_entry(0) else { | |
| panic!("Did not parse into a message") | |
| }; | |
| let mut fields: IndexMap<String, (&syn::Ident, bool)> = | |
| IndexMap::with_capacity(field_map.len()); | |
| for (_, (ident, _)) in field_map { | |
| fields.insert(ident.unraw().to_string(), (ident, false)); | |
| } | |
| for variable in variable_references(&flt_message) { | |
| match fields.get_mut(variable) { | |
| Some((_, seen)) => *seen = true, | |
| None => { | |
| span_err( | |
| message_span.unwrap(), | |
| format!("Variable `{variable}` not found in diagnostic "), | |
| ) | |
| .help(format!( | |
| "Available fields: {:?}", | |
| fields.keys().map(|s| s.as_str()).collect::<Vec<&str>>().join(", ") | |
| )) | |
| .emit(); | |
| } | |
| } | |
| } | |
| for (name, seen) in fields.values() { | |
| if *seen { | |
| used_fields.insert((*name).clone()); | |
| } | |
| } | |
| Self { attr_span, message_span, value: message_str } | |
| } | |
| /// 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) -> TokenStream { | |
| let message = &self.value; | |
| self.verify(); | |
| quote! { rustc_errors::DiagMessage::Inline(std::borrow::Cow::Borrowed(#message)) } | |
| } | |
| fn verify(&self) { | |
| verify_message_style(self.message_span, &self.value); | |
| verify_message_formatting(self.attr_span, self.message_span, &self.value); | |
| } | |
| } | |
| 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", | |
| "LLVM", | |
| "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; | |
| } | |
| } | |
| } |