rustc_macros/diagnostics/
message.rs1use 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 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 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 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
68 if let Some(Pattern { elements }) = &msg.value {
69 for elt in elements {
70 traverse_pattern(elt, &mut refs);
71 }
72 }
73 for attr in &msg.attributes {
74 for elt in &attr.value.elements {
75 traverse_pattern(elt, &mut refs);
76 }
77 }
78
79 fn traverse_pattern<'a>(elem: &PatternElement<&'a str>, refs: &mut Vec<&'a str>) {
80 match elem {
81 PatternElement::TextElement { .. } => {}
82 PatternElement::Placeable { expression } => traverse_expression(expression, refs),
83 }
84 }
85 fn traverse_expression<'a>(expr: &Expression<&'a str>, refs: &mut Vec<&'a str>) {
86 match expr {
87 Expression::Select { selector, variants } => {
88 traverse_inline_expr(selector, refs);
89 for variant in variants {
90 for pattern in &variant.value.elements {
91 traverse_pattern(pattern, refs);
92 }
93 }
94 }
95 Expression::Inline(expr) => {
96 traverse_inline_expr(expr, refs);
97 }
98 }
99 }
100 fn traverse_inline_expr<'a>(elem: &InlineExpression<&'a str>, refs: &mut Vec<&'a str>) {
101 match elem {
102 InlineExpression::VariableReference { id } => refs.push(id.name),
103 _ => {}
104 }
105 }
106
107 refs
108}
109
110const ALLOWED_CAPITALIZED_WORDS: &[&str] = &[
111 "ABI",
113 "ABIs",
114 "ADT",
115 "C-variadic",
116 "CGU-reuse",
117 "Cargo",
118 "Ferris",
119 "GCC",
120 "MIR",
121 "NaNs",
122 "OK",
123 "Rust",
124 "ThinLTO",
125 "Unicode",
126 "VS",
127 ];
129
130fn verify_message_style(msg_span: Span, message: &str) {
132 let Some(first_word) = message.split_whitespace().next() else {
134 span_err(msg_span.unwrap(), "message must not be empty").emit();
135 return;
136 };
137 let first_char = first_word.chars().next().expect("Word is not empty");
138 if first_char.is_uppercase() && !ALLOWED_CAPITALIZED_WORDS.contains(&first_word) {
139 span_err(msg_span.unwrap(), "message `{value}` starts with an uppercase letter. Fix it or add it to `ALLOWED_CAPITALIZED_WORDS`").emit();
140 return;
141 }
142
143 if message.ends_with(".") && !message.ends_with("...") {
145 span_err(msg_span.unwrap(), "message `{value}` ends with a period").emit();
146 return;
147 }
148}
149
150fn verify_message_formatting(attr_span: Span, msg_span: Span, message: &str) {
152 let start = attr_span.unwrap().column() - 1;
154
155 for line in message.lines().skip(1) {
156 if line.is_empty() {
157 continue;
158 }
159 let indent = line.chars().take_while(|c| *c == ' ').count();
160 if indent < start {
161 span_err(
162 msg_span.unwrap(),
163 format!("message is not properly indented. {indent} < {start}"),
164 )
165 .emit();
166 return;
167 }
168 if indent % 4 != 0 {
169 span_err(msg_span.unwrap(), "message is not indented with a multiple of 4 spaces")
170 .emit();
171 return;
172 }
173 }
174}