flux_macros/diagnostics/
fluent.rs1use std::{
2 collections::{HashMap, HashSet},
3 fs::read_to_string,
4 path::{Path, PathBuf},
5};
6
7use annotate_snippets::{Renderer, Snippet};
8use fluent_bundle::{FluentBundle, FluentError, FluentResource};
9use fluent_syntax::{
10 ast::{
11 Attribute, Entry, Expression, Identifier, InlineExpression, Message, Pattern,
12 PatternElement,
13 },
14 parser::ParserError,
15};
16use proc_macro::{Diagnostic, Level, Span, tracked::path};
17use proc_macro2::TokenStream;
18use quote::quote;
19use syn::{Ident, LitStr, parse_macro_input};
20use unic_langid::langid;
21
22fn invocation_relative_path_to_absolute(span: Span, path: &str) -> PathBuf {
27 let path = Path::new(path);
28 if path.is_absolute() {
29 path.to_path_buf()
30 } else {
31 let mut source_file_path = span.local_file().unwrap();
33 source_file_path.pop();
35 source_file_path.push(path);
37 source_file_path
38 }
39}
40
41fn finish(body: TokenStream, resource: TokenStream) -> proc_macro::TokenStream {
43 quote! {
44 pub static DEFAULT_LOCALE_RESOURCE: &'static str = #resource;
47
48 #[allow(non_upper_case_globals)]
49 #[doc(hidden)]
50 pub(crate) mod fluent_generated {
52 #body
53
54 pub mod _subdiag {
57 pub const help: rustc_errors::SubdiagMessage =
59 rustc_errors::SubdiagMessage::FluentAttr(std::borrow::Cow::Borrowed("help"));
60 pub const note: rustc_errors::SubdiagMessage =
62 rustc_errors::SubdiagMessage::FluentAttr(std::borrow::Cow::Borrowed("note"));
63 pub const warn: rustc_errors::SubdiagMessage =
65 rustc_errors::SubdiagMessage::FluentAttr(std::borrow::Cow::Borrowed("warn"));
66 pub const label: rustc_errors::SubdiagMessage =
68 rustc_errors::SubdiagMessage::FluentAttr(std::borrow::Cow::Borrowed("label"));
69 pub const suggestion: rustc_errors::SubdiagMessage =
71 rustc_errors::SubdiagMessage::FluentAttr(std::borrow::Cow::Borrowed("suggestion"));
72 }
73 }
74 }
75 .into()
76}
77
78fn failed(crate_name: &Ident) -> proc_macro::TokenStream {
80 finish(quote! { pub mod #crate_name {} }, quote! { "" })
81}
82
83pub(crate) fn fluent_messages(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
84 let crate_name = std::env::var("CARGO_CRATE_NAME")
85 .unwrap_or_else(|_| "no_crate".to_string())
88 .replace("-", "_")
89 .replace("flux_", "");
90
91 let mut bundle = FluentBundle::new(vec![langid!("en-US")]);
95
96 let mut previous_attrs = HashSet::new();
99
100 let resource_str = parse_macro_input!(input as LitStr);
101 let resource_span = resource_str.span().unwrap();
102 let relative_ftl_path = resource_str.value();
103 let absolute_ftl_path = invocation_relative_path_to_absolute(resource_span, &relative_ftl_path);
104
105 let crate_name = Ident::new(&crate_name, resource_str.span());
106
107 path(absolute_ftl_path.to_str().unwrap());
108 let resource_contents = match read_to_string(absolute_ftl_path) {
109 Ok(resource_contents) => resource_contents,
110 Err(e) => {
111 Diagnostic::spanned(
112 resource_span,
113 Level::Error,
114 format!("could not open Fluent resource: {e}"),
115 )
116 .emit();
117 return failed(&crate_name);
118 }
119 };
120 let mut bad = false;
121 for esc in ["\\n", "\\\"", "\\'"] {
122 for _ in resource_contents.matches(esc) {
123 bad = true;
124 Diagnostic::spanned(resource_span, Level::Error, format!("invalid escape `{esc}` in Fluent resource"))
125 .note("Fluent does not interpret these escape sequences (<https://projectfluent.org/fluent/guide/special.html>)")
126 .emit();
127 }
128 }
129 if bad {
130 return failed(&crate_name);
131 }
132
133 let resource = match FluentResource::try_new(resource_contents) {
134 Ok(resource) => resource,
135 Err((this, errs)) => {
136 Diagnostic::spanned(resource_span, Level::Error, "could not parse Fluent resource")
137 .help("see additional errors emitted")
138 .emit();
139 for ParserError { pos, slice: _, kind } in errs {
140 let mut err = kind.to_string();
141 err.replace_range(0..1, &err.chars().next().unwrap().to_lowercase().to_string());
144
145 let message = annotate_snippets::Level::Error.title(&err).snippet(
146 Snippet::source(this.source())
147 .origin(&relative_ftl_path)
148 .fold(true)
149 .annotation(annotate_snippets::Level::Error.span(pos.start..pos.end - 1)),
150 );
151 let renderer = Renderer::plain();
152 eprintln!("{}\n", renderer.render(message));
153 }
154
155 return failed(&crate_name);
156 }
157 };
158
159 let mut constants = TokenStream::new();
160 let mut previous_defns = HashMap::new();
161 let mut message_refs = Vec::new();
162 for entry in resource.entries() {
163 if let Entry::Message(msg) = entry {
164 let Message { id: Identifier { name }, attributes, value, .. } = msg;
165 let _ = previous_defns
166 .entry((*name).to_string())
167 .or_insert(resource_span);
168 if name.contains('-') {
169 Diagnostic::spanned(
170 resource_span,
171 Level::Error,
172 format!("name `{name}` contains a '-' character"),
173 )
174 .help("replace any '-'s with '_'s")
175 .emit();
176 }
177
178 if let Some(Pattern { elements }) = value {
179 for elt in elements {
180 if let PatternElement::Placeable {
181 expression:
182 Expression::Inline(InlineExpression::MessageReference { id, .. }),
183 } = elt
184 {
185 message_refs.push((id.name, *name));
186 }
187 }
188 }
189
190 let crate_prefix = format!("{crate_name}_");
197
198 let snake_name = name.replace('-', "_");
199 if !snake_name.starts_with(&crate_prefix) {
200 Diagnostic::spanned(
201 resource_span,
202 Level::Error,
203 format!("name `{name}` does not start with the crate name"),
204 )
205 .help(format!(
206 "prepend `{crate_prefix}` to the slug name: `{crate_prefix}{snake_name}`"
207 ))
208 .emit();
209 };
210 let snake_name = Ident::new(&snake_name, resource_str.span());
211
212 if !previous_attrs.insert(snake_name.clone()) {
213 continue;
214 }
215
216 let docstr =
217 format!("Constant referring to Fluent message `{name}` from `{crate_name}`");
218 constants.extend(quote! {
219 #[doc = #docstr]
220 pub const #snake_name: rustc_errors::DiagMessage =
221 rustc_errors::DiagMessage::FluentIdentifier(
222 std::borrow::Cow::Borrowed(#name),
223 None
224 );
225 });
226
227 for Attribute { id: Identifier { name: attr_name }, .. } in attributes {
228 let snake_name = Ident::new(
229 &format!("{crate_prefix}{}", attr_name.replace('-', "_")),
230 resource_str.span(),
231 );
232 if !previous_attrs.insert(snake_name.clone()) {
233 continue;
234 }
235
236 if attr_name.contains('-') {
237 Diagnostic::spanned(
238 resource_span,
239 Level::Error,
240 format!("attribute `{attr_name}` contains a '-' character"),
241 )
242 .help("replace any '-'s with '_'s")
243 .emit();
244 }
245
246 let msg = format!(
247 "Constant referring to Fluent message `{name}.{attr_name}` from `{crate_name}`"
248 );
249 constants.extend(quote! {
250 #[doc = #msg]
251 pub const #snake_name: rustc_errors::SubdiagMessage =
252 rustc_errors::SubdiagMessage::FluentAttr(std::borrow::Cow::Borrowed(#attr_name));
253 });
254 }
255
256 let ident = quote::format_ident!("{snake_name}_refs");
259 let vrefs = variable_references(msg);
260 constants.extend(quote! {
261 #[cfg(test)]
262 pub const #ident: &[&str] = &[#(#vrefs),*];
263 });
264 }
265 }
266
267 for (mref, name) in message_refs {
268 if !previous_defns.contains_key(mref) {
269 Diagnostic::spanned(
270 resource_span,
271 Level::Error,
272 format!("referenced message `{mref}` does not exist (in message `{name}`)"),
273 )
274 .help(&format!("you may have meant to use a variable reference (`{{${mref}}}`)"))
275 .emit();
276 }
277 }
278
279 if let Err(errs) = bundle.add_resource(resource) {
280 for e in errs {
281 match e {
282 FluentError::Overriding { kind, id } => {
283 Diagnostic::spanned(
284 resource_span,
285 Level::Error,
286 format!("overrides existing {kind}: `{id}`"),
287 )
288 .emit();
289 }
290 FluentError::ResolverError(_) | FluentError::ParserError(_) => unreachable!(),
291 }
292 }
293 }
294
295 finish(constants, quote! { include_str!(#relative_ftl_path) })
296}
297
298fn variable_references<'a>(msg: &Message<&'a str>) -> Vec<&'a str> {
299 let mut refs = vec![];
300 if let Some(Pattern { elements }) = &msg.value {
301 for elt in elements {
302 if let PatternElement::Placeable {
303 expression: Expression::Inline(InlineExpression::VariableReference { id }),
304 } = elt
305 {
306 refs.push(id.name);
307 }
308 }
309 }
310 for attr in &msg.attributes {
311 for elt in &attr.value.elements {
312 if let PatternElement::Placeable {
313 expression: Expression::Inline(InlineExpression::VariableReference { id }),
314 } = elt
315 {
316 refs.push(id.name);
317 }
318 }
319 }
320 refs
321}