Skip to main content

rustc_macros/diagnostics/
message.rs

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