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    if let Some(Pattern { elements }) = &msg.value {
68        for elt in elements {
69            if let PatternElement::Placeable {
70                expression: Expression::Inline(InlineExpression::VariableReference { id }),
71            } = elt
72            {
73                refs.push(id.name);
74            }
75        }
76    }
77    for attr in &msg.attributes {
78        for elt in &attr.value.elements {
79            if let PatternElement::Placeable {
80                expression: Expression::Inline(InlineExpression::VariableReference { id }),
81            } = elt
82            {
83                refs.push(id.name);
84            }
85        }
86    }
87    refs
88}
89
90const ALLOWED_CAPITALIZED_WORDS: &[&str] = &[
91    // tidy-alphabetical-start
92    "ABI",
93    "ABIs",
94    "ADT",
95    "C-variadic",
96    "CGU-reuse",
97    "Cargo",
98    "Ferris",
99    "GCC",
100    "MIR",
101    "NaNs",
102    "OK",
103    "Rust",
104    "ThinLTO",
105    "Unicode",
106    "VS",
107    // tidy-alphabetical-end
108];
109
110/// See: https://rustc-dev-guide.rust-lang.org/diagnostics.html#diagnostic-output-style-guide
111fn verify_message_style(msg_span: Span, message: &str) {
112    // Verify that message starts with lowercase char
113    let Some(first_word) = message.split_whitespace().next() else {
114        span_err(msg_span.unwrap(), "message must not be empty").emit();
115        return;
116    };
117    let first_char = first_word.chars().next().expect("Word is not empty");
118    if first_char.is_uppercase() && !ALLOWED_CAPITALIZED_WORDS.contains(&first_word) {
119        span_err(msg_span.unwrap(), "message `{value}` starts with an uppercase letter. Fix it or add it to `ALLOWED_CAPITALIZED_WORDS`").emit();
120        return;
121    }
122
123    // Verify that message does not end in `.`
124    if message.ends_with(".") && !message.ends_with("...") {
125        span_err(msg_span.unwrap(), "message `{value}` ends with a period").emit();
126        return;
127    }
128}
129
130/// Verifies that the message is properly indented into the code
131fn verify_message_formatting(attr_span: Span, msg_span: Span, message: &str) {
132    // Find the indent at the start of the message (`column()` is one-indexed)
133    let start = attr_span.unwrap().column() - 1;
134
135    for line in message.lines().skip(1) {
136        if line.is_empty() {
137            continue;
138        }
139        let indent = line.chars().take_while(|c| *c == ' ').count();
140        if indent < start {
141            span_err(
142                msg_span.unwrap(),
143                format!("message is not properly indented. {indent} < {start}"),
144            )
145            .emit();
146            return;
147        }
148        if indent % 4 != 0 {
149            span_err(msg_span.unwrap(), "message is not indented with a multiple of 4 spaces")
150                .emit();
151            return;
152        }
153    }
154}