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