Skip to main content

rustc_macros/diagnostics/
message.rs

1use fluent_bundle::FluentResource;
2use fluent_syntax::ast::{Expression, InlineExpression, Pattern, PatternElement};
3use proc_macro2::{Span, TokenStream};
4use quote::quote;
5use syn::ext::IdentExt;
6use synstructure::VariantInfo;
7
8use crate::diagnostics::error::span_err;
9
10#[derive(Clone)]
11pub(crate) struct Message {
12    pub attr_span: Span,
13    pub message_span: Span,
14    pub value: String,
15}
16
17impl Message {
18    /// Get the diagnostic message for this diagnostic
19    /// The passed `variant` is used to check whether all variables in the message are used.
20    /// For subdiagnostics, we cannot check this.
21    pub(crate) fn diag_message(&self, variant: Option<&VariantInfo<'_>>) -> TokenStream {
22        let message = &self.value;
23        self.verify(variant);
24        quote! { rustc_errors::DiagMessage::Inline(std::borrow::Cow::Borrowed(#message)) }
25    }
26
27    fn verify(&self, variant: Option<&VariantInfo<'_>>) {
28        verify_variables_used(self.message_span, &self.value, variant);
29        verify_message_style(self.message_span, &self.value);
30        verify_message_formatting(self.attr_span, self.message_span, &self.value);
31    }
32}
33
34fn verify_variables_used(msg_span: Span, message_str: &str, variant: Option<&VariantInfo<'_>>) {
35    // Parse the fluent message
36    const GENERATED_MSG_ID: &str = "generated_msg";
37    let resource =
38        FluentResource::try_new(format!("{GENERATED_MSG_ID} = {message_str}\n")).unwrap();
39    assert_eq!(resource.entries().count(), 1);
40    let Some(fluent_syntax::ast::Entry::Message(message)) = resource.get_entry(0) else {
41        panic!("Did not parse into a message")
42    };
43
44    // Check if all variables are used
45    if let Some(variant) = variant {
46        let fields: Vec<String> = variant
47            .bindings()
48            .iter()
49            .flat_map(|b| b.ast().ident.as_ref())
50            .map(|id| id.unraw().to_string())
51            .collect();
52        for variable in variable_references(&message) {
53            if !fields.iter().any(|f| f == variable) {
54                span_err(
55                    msg_span.unwrap(),
56                    format!("Variable `{variable}` not found in diagnostic "),
57                )
58                .help(format!("Available fields: {:?}", fields.join(", ")))
59                .emit();
60            }
61        }
62    }
63}
64
65fn variable_references<'a>(msg: &fluent_syntax::ast::Message<&'a str>) -> Vec<&'a str> {
66    let mut refs = vec![];
67
68    if let Some(Pattern { elements }) = &msg.value {
69        for elt in elements {
70            traverse_pattern(elt, &mut refs);
71        }
72    }
73    for attr in &msg.attributes {
74        for elt in &attr.value.elements {
75            traverse_pattern(elt, &mut refs);
76        }
77    }
78
79    fn traverse_pattern<'a>(elem: &PatternElement<&'a str>, refs: &mut Vec<&'a str>) {
80        match elem {
81            PatternElement::TextElement { .. } => {}
82            PatternElement::Placeable { expression } => traverse_expression(expression, refs),
83        }
84    }
85    fn traverse_expression<'a>(expr: &Expression<&'a str>, refs: &mut Vec<&'a str>) {
86        match expr {
87            Expression::Select { selector, variants } => {
88                traverse_inline_expr(selector, refs);
89                for variant in variants {
90                    for pattern in &variant.value.elements {
91                        traverse_pattern(pattern, refs);
92                    }
93                }
94            }
95            Expression::Inline(expr) => {
96                traverse_inline_expr(expr, refs);
97            }
98        }
99    }
100    fn traverse_inline_expr<'a>(elem: &InlineExpression<&'a str>, refs: &mut Vec<&'a str>) {
101        match elem {
102            InlineExpression::VariableReference { id } => refs.push(id.name),
103            _ => {}
104        }
105    }
106
107    refs
108}
109
110const ALLOWED_CAPITALIZED_WORDS: &[&str] = &[
111    // tidy-alphabetical-start
112    "ABI",
113    "ABIs",
114    "ADT",
115    "C-variadic",
116    "CGU-reuse",
117    "Cargo",
118    "Ferris",
119    "GCC",
120    "MIR",
121    "NaNs",
122    "OK",
123    "Rust",
124    "ThinLTO",
125    "Unicode",
126    "VS",
127    // tidy-alphabetical-end
128];
129
130/// See: https://rustc-dev-guide.rust-lang.org/diagnostics.html#diagnostic-output-style-guide
131fn verify_message_style(msg_span: Span, message: &str) {
132    // Verify that message starts with lowercase char
133    let Some(first_word) = message.split_whitespace().next() else {
134        span_err(msg_span.unwrap(), "message must not be empty").emit();
135        return;
136    };
137    let first_char = first_word.chars().next().expect("Word is not empty");
138    if first_char.is_uppercase() && !ALLOWED_CAPITALIZED_WORDS.contains(&first_word) {
139        span_err(msg_span.unwrap(), "message `{value}` starts with an uppercase letter. Fix it or add it to `ALLOWED_CAPITALIZED_WORDS`").emit();
140        return;
141    }
142
143    // Verify that message does not end in `.`
144    if message.ends_with(".") && !message.ends_with("...") {
145        span_err(msg_span.unwrap(), "message `{value}` ends with a period").emit();
146        return;
147    }
148}
149
150/// Verifies that the message is properly indented into the code
151fn verify_message_formatting(attr_span: Span, msg_span: Span, message: &str) {
152    // Find the indent at the start of the message (`column()` is one-indexed)
153    let start = attr_span.unwrap().column() - 1;
154
155    for line in message.lines().skip(1) {
156        if line.is_empty() {
157            continue;
158        }
159        let indent = line.chars().take_while(|c| *c == ' ').count();
160        if indent < start {
161            span_err(
162                msg_span.unwrap(),
163                format!("message is not properly indented. {indent} < {start}"),
164            )
165            .emit();
166            return;
167        }
168        if indent % 4 != 0 {
169            span_err(msg_span.unwrap(), "message is not indented with a multiple of 4 spaces")
170                .emit();
171            return;
172        }
173    }
174}