flux_macros/diagnostics/
message.rs1use 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 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 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 "ABI",
129 "ABIs",
130 "ADT",
131 "C-variadic",
132 "CGU-reuse",
133 "Cargo",
134 "Ferris",
135 "Flux", "GCC",
137 "MIR",
138 "NaNs",
139 "OK",
140 "Rust",
141 "ThinLTO",
142 "Unicode",
143 "VS",
144 ];
146
147fn verify_message_style(msg_span: Span, message: &str) {
149 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 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
167fn verify_message_formatting(attr_span: Span, msg_span: Span, message: &str) {
169 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}