Skip to main content

flux_macros/diagnostics/
message.rs

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