rustc_macros/diagnostics/
message.rs1use 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 #[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 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 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 "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 ];
147
148fn verify_message_style(msg_span: Span, message: &str) {
150 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 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
168fn verify_message_formatting(attr_span: Span, msg_span: Span, message: &str) {
170 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}