• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 use annotate_snippets::{
2     display_list::DisplayList,
3     snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation},
4 };
5 use fluent_bundle::{FluentBundle, FluentError, FluentResource};
6 use fluent_syntax::{
7     ast::{
8         Attribute, Entry, Expression, Identifier, InlineExpression, Message, Pattern,
9         PatternElement,
10     },
11     parser::ParserError,
12 };
13 use proc_macro::{Diagnostic, Level, Span};
14 use proc_macro2::TokenStream;
15 use quote::quote;
16 use std::{
17     collections::{HashMap, HashSet},
18     fs::read_to_string,
19     path::{Path, PathBuf},
20 };
21 use syn::{parse_macro_input, Ident, LitStr};
22 use unic_langid::langid;
23 
24 /// Helper function for returning an absolute path for macro-invocation relative file paths.
25 ///
26 /// If the input is already absolute, then the input is returned. If the input is not absolute,
27 /// then it is appended to the directory containing the source file with this macro invocation.
invocation_relative_path_to_absolute(span: Span, path: &str) -> PathBuf28 fn invocation_relative_path_to_absolute(span: Span, path: &str) -> PathBuf {
29     let path = Path::new(path);
30     if path.is_absolute() {
31         path.to_path_buf()
32     } else {
33         // `/a/b/c/foo/bar.rs` contains the current macro invocation
34         let mut source_file_path = span.source_file().path();
35         // `/a/b/c/foo/`
36         source_file_path.pop();
37         // `/a/b/c/foo/../locales/en-US/example.ftl`
38         source_file_path.push(path);
39         source_file_path
40     }
41 }
42 
43 /// Tokens to be returned when the macro cannot proceed.
failed(crate_name: &Ident) -> proc_macro::TokenStream44 fn failed(crate_name: &Ident) -> proc_macro::TokenStream {
45     quote! {
46         pub static DEFAULT_LOCALE_RESOURCE: &'static str = "";
47 
48         #[allow(non_upper_case_globals)]
49         #[doc(hidden)]
50         pub(crate) mod fluent_generated {
51             pub mod #crate_name {
52             }
53 
54             pub mod _subdiag {
55                 pub const help: crate::SubdiagnosticMessage =
56                     crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("help"));
57                 pub const note: crate::SubdiagnosticMessage =
58                     crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("note"));
59                 pub const warn: crate::SubdiagnosticMessage =
60                     crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("warn"));
61                 pub const label: crate::SubdiagnosticMessage =
62                     crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("label"));
63                 pub const suggestion: crate::SubdiagnosticMessage =
64                     crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("suggestion"));
65             }
66         }
67     }
68     .into()
69 }
70 
71 /// See [rustc_fluent_macro::fluent_messages].
fluent_messages(input: proc_macro::TokenStream) -> proc_macro::TokenStream72 pub(crate) fn fluent_messages(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
73     let crate_name = std::env::var("CARGO_PKG_NAME")
74         // If `CARGO_PKG_NAME` is missing, then we're probably running in a test, so use
75         // `no_crate`.
76         .unwrap_or_else(|_| "no_crate".to_string())
77         .replace("rustc_", "");
78 
79     // Cannot iterate over individual messages in a bundle, so do that using the
80     // `FluentResource` instead. Construct a bundle anyway to find out if there are conflicting
81     // messages in the resources.
82     let mut bundle = FluentBundle::new(vec![langid!("en-US")]);
83 
84     // Set of Fluent attribute names already output, to avoid duplicate type errors - any given
85     // constant created for a given attribute is the same.
86     let mut previous_attrs = HashSet::new();
87 
88     let resource_str = parse_macro_input!(input as LitStr);
89     let resource_span = resource_str.span().unwrap();
90     let relative_ftl_path = resource_str.value();
91     let absolute_ftl_path = invocation_relative_path_to_absolute(resource_span, &relative_ftl_path);
92 
93     let crate_name = Ident::new(&crate_name, resource_str.span());
94 
95     // As this macro also outputs an `include_str!` for this file, the macro will always be
96     // re-executed when the file changes.
97     let resource_contents = match read_to_string(absolute_ftl_path) {
98         Ok(resource_contents) => resource_contents,
99         Err(e) => {
100             Diagnostic::spanned(
101                 resource_span,
102                 Level::Error,
103                 format!("could not open Fluent resource: {e}"),
104             )
105             .emit();
106             return failed(&crate_name);
107         }
108     };
109     let mut bad = false;
110     for esc in ["\\n", "\\\"", "\\'"] {
111         for _ in resource_contents.matches(esc) {
112             bad = true;
113             Diagnostic::spanned(resource_span, Level::Error, format!("invalid escape `{esc}` in Fluent resource"))
114                 .note("Fluent does not interpret these escape sequences (<https://projectfluent.org/fluent/guide/special.html>)")
115                 .emit();
116         }
117     }
118     if bad {
119         return failed(&crate_name);
120     }
121 
122     let resource = match FluentResource::try_new(resource_contents) {
123         Ok(resource) => resource,
124         Err((this, errs)) => {
125             Diagnostic::spanned(resource_span, Level::Error, "could not parse Fluent resource")
126                 .help("see additional errors emitted")
127                 .emit();
128             for ParserError { pos, slice: _, kind } in errs {
129                 let mut err = kind.to_string();
130                 // Entirely unnecessary string modification so that the error message starts
131                 // with a lowercase as rustc errors do.
132                 err.replace_range(0..1, &err.chars().next().unwrap().to_lowercase().to_string());
133 
134                 let line_starts: Vec<usize> = std::iter::once(0)
135                     .chain(
136                         this.source()
137                             .char_indices()
138                             .filter_map(|(i, c)| Some(i + 1).filter(|_| c == '\n')),
139                     )
140                     .collect();
141                 let line_start = line_starts
142                     .iter()
143                     .enumerate()
144                     .map(|(line, idx)| (line + 1, idx))
145                     .filter(|(_, idx)| **idx <= pos.start)
146                     .last()
147                     .unwrap()
148                     .0;
149 
150                 let snippet = Snippet {
151                     title: Some(Annotation {
152                         label: Some(&err),
153                         id: None,
154                         annotation_type: AnnotationType::Error,
155                     }),
156                     footer: vec![],
157                     slices: vec![Slice {
158                         source: this.source(),
159                         line_start,
160                         origin: Some(&relative_ftl_path),
161                         fold: true,
162                         annotations: vec![SourceAnnotation {
163                             label: "",
164                             annotation_type: AnnotationType::Error,
165                             range: (pos.start, pos.end - 1),
166                         }],
167                     }],
168                     opt: Default::default(),
169                 };
170                 let dl = DisplayList::from(snippet);
171                 eprintln!("{dl}\n");
172             }
173 
174             return failed(&crate_name);
175         }
176     };
177 
178     let mut constants = TokenStream::new();
179     let mut previous_defns = HashMap::new();
180     let mut message_refs = Vec::new();
181     for entry in resource.entries() {
182         if let Entry::Message(msg) = entry {
183             let Message { id: Identifier { name }, attributes, value, .. } = msg;
184             let _ = previous_defns.entry(name.to_string()).or_insert(resource_span);
185             if name.contains('-') {
186                 Diagnostic::spanned(
187                     resource_span,
188                     Level::Error,
189                     format!("name `{name}` contains a '-' character"),
190                 )
191                 .help("replace any '-'s with '_'s")
192                 .emit();
193             }
194 
195             if let Some(Pattern { elements }) = value {
196                 for elt in elements {
197                     if let PatternElement::Placeable {
198                         expression:
199                             Expression::Inline(InlineExpression::MessageReference { id, .. }),
200                     } = elt
201                     {
202                         message_refs.push((id.name, *name));
203                     }
204                 }
205             }
206 
207             // `typeck_foo_bar` => `foo_bar` (in `typeck.ftl`)
208             // `const_eval_baz` => `baz` (in `const_eval.ftl`)
209             // `const-eval-hyphen-having` => `hyphen_having` (in `const_eval.ftl`)
210             // The last case we error about above, but we want to fall back gracefully
211             // so that only the error is being emitted and not also one about the macro
212             // failing.
213             let crate_prefix = format!("{crate_name}_");
214 
215             let snake_name = name.replace('-', "_");
216             if !snake_name.starts_with(&crate_prefix) {
217                 Diagnostic::spanned(
218                     resource_span,
219                     Level::Error,
220                     format!("name `{name}` does not start with the crate name"),
221                 )
222                 .help(format!(
223                     "prepend `{crate_prefix}` to the slug name: `{crate_prefix}{snake_name}`"
224                 ))
225                 .emit();
226             };
227             let snake_name = Ident::new(&snake_name, resource_str.span());
228 
229             if !previous_attrs.insert(snake_name.clone()) {
230                 continue;
231             }
232 
233             let docstr =
234                 format!("Constant referring to Fluent message `{name}` from `{crate_name}`");
235             constants.extend(quote! {
236                 #[doc = #docstr]
237                 pub const #snake_name: crate::DiagnosticMessage =
238                     crate::DiagnosticMessage::FluentIdentifier(
239                         std::borrow::Cow::Borrowed(#name),
240                         None
241                     );
242             });
243 
244             for Attribute { id: Identifier { name: attr_name }, .. } in attributes {
245                 let snake_name = Ident::new(
246                     &format!("{}{}", &crate_prefix, &attr_name.replace('-', "_")),
247                     resource_str.span(),
248                 );
249                 if !previous_attrs.insert(snake_name.clone()) {
250                     continue;
251                 }
252 
253                 if attr_name.contains('-') {
254                     Diagnostic::spanned(
255                         resource_span,
256                         Level::Error,
257                         format!("attribute `{attr_name}` contains a '-' character"),
258                     )
259                     .help("replace any '-'s with '_'s")
260                     .emit();
261                 }
262 
263                 let msg = format!(
264                     "Constant referring to Fluent message `{name}.{attr_name}` from `{crate_name}`"
265                 );
266                 constants.extend(quote! {
267                     #[doc = #msg]
268                     pub const #snake_name: crate::SubdiagnosticMessage =
269                         crate::SubdiagnosticMessage::FluentAttr(
270                             std::borrow::Cow::Borrowed(#attr_name)
271                         );
272                 });
273             }
274 
275             // Record variables referenced by these messages so we can produce
276             // tests in the derive diagnostics to validate them.
277             let ident = quote::format_ident!("{snake_name}_refs");
278             let vrefs = variable_references(msg);
279             constants.extend(quote! {
280                 #[cfg(test)]
281                 pub const #ident: &[&str] = &[#(#vrefs),*];
282             })
283         }
284     }
285 
286     for (mref, name) in message_refs.into_iter() {
287         if !previous_defns.contains_key(mref) {
288             Diagnostic::spanned(
289                 resource_span,
290                 Level::Error,
291                 format!("referenced message `{mref}` does not exist (in message `{name}`)"),
292             )
293             .help(&format!("you may have meant to use a variable reference (`{{${mref}}}`)"))
294             .emit();
295         }
296     }
297 
298     if let Err(errs) = bundle.add_resource(resource) {
299         for e in errs {
300             match e {
301                 FluentError::Overriding { kind, id } => {
302                     Diagnostic::spanned(
303                         resource_span,
304                         Level::Error,
305                         format!("overrides existing {kind}: `{id}`"),
306                     )
307                     .emit();
308                 }
309                 FluentError::ResolverError(_) | FluentError::ParserError(_) => unreachable!(),
310             }
311         }
312     }
313 
314     quote! {
315         /// Raw content of Fluent resource for this crate, generated by `fluent_messages` macro,
316         /// imported by `rustc_driver` to include all crates' resources in one bundle.
317         pub static DEFAULT_LOCALE_RESOURCE: &'static str = include_str!(#relative_ftl_path);
318 
319         #[allow(non_upper_case_globals)]
320         #[doc(hidden)]
321         /// Auto-generated constants for type-checked references to Fluent messages.
322         pub(crate) mod fluent_generated {
323             #constants
324 
325             /// Constants expected to exist by the diagnostic derive macros to use as default Fluent
326             /// identifiers for different subdiagnostic kinds.
327             pub mod _subdiag {
328                 /// Default for `#[help]`
329                 pub const help: crate::SubdiagnosticMessage =
330                     crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("help"));
331                 /// Default for `#[note]`
332                 pub const note: crate::SubdiagnosticMessage =
333                     crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("note"));
334                 /// Default for `#[warn]`
335                 pub const warn: crate::SubdiagnosticMessage =
336                     crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("warn"));
337                 /// Default for `#[label]`
338                 pub const label: crate::SubdiagnosticMessage =
339                     crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("label"));
340                 /// Default for `#[suggestion]`
341                 pub const suggestion: crate::SubdiagnosticMessage =
342                     crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("suggestion"));
343             }
344         }
345     }
346     .into()
347 }
348 
variable_references<'a>(msg: &Message<&'a str>) -> Vec<&'a str>349 fn variable_references<'a>(msg: &Message<&'a str>) -> Vec<&'a str> {
350     let mut refs = vec![];
351     if let Some(Pattern { elements }) = &msg.value {
352         for elt in elements {
353             if let PatternElement::Placeable {
354                 expression: Expression::Inline(InlineExpression::VariableReference { id }),
355             } = elt
356             {
357                 refs.push(id.name);
358             }
359         }
360     }
361     for attr in &msg.attributes {
362         for elt in &attr.value.elements {
363             if let PatternElement::Placeable {
364                 expression: Expression::Inline(InlineExpression::VariableReference { id }),
365             } = elt
366             {
367                 refs.push(id.name);
368             }
369         }
370     }
371     refs
372 }
373