]> git.lizzy.rs Git - rust.git/blob - compiler/rustc_macros/src/diagnostics/fluent.rs
Auto merge of #107843 - bjorn3:sync_cg_clif-2023-02-09, r=bjorn3
[rust.git] / compiler / rustc_macros / src / diagnostics / fluent.rs
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::File,
19     io::Read,
20     path::{Path, PathBuf},
21 };
22 use syn::{
23     parse::{Parse, ParseStream},
24     parse_macro_input,
25     punctuated::Punctuated,
26     token, Ident, LitStr, Result,
27 };
28 use unic_langid::langid;
29
30 struct Resource {
31     krate: Ident,
32     #[allow(dead_code)]
33     fat_arrow_token: token::FatArrow,
34     resource_path: LitStr,
35 }
36
37 impl Parse for Resource {
38     fn parse(input: ParseStream<'_>) -> Result<Self> {
39         Ok(Resource {
40             krate: input.parse()?,
41             fat_arrow_token: input.parse()?,
42             resource_path: input.parse()?,
43         })
44     }
45 }
46
47 struct Resources(Punctuated<Resource, token::Comma>);
48
49 impl Parse for Resources {
50     fn parse(input: ParseStream<'_>) -> Result<Self> {
51         let mut resources = Punctuated::new();
52         loop {
53             if input.is_empty() || input.peek(token::Brace) {
54                 break;
55             }
56             let value = input.parse()?;
57             resources.push_value(value);
58             if !input.peek(token::Comma) {
59                 break;
60             }
61             let punct = input.parse()?;
62             resources.push_punct(punct);
63         }
64         Ok(Resources(resources))
65     }
66 }
67
68 /// Helper function for returning an absolute path for macro-invocation relative file paths.
69 ///
70 /// If the input is already absolute, then the input is returned. If the input is not absolute,
71 /// then it is appended to the directory containing the source file with this macro invocation.
72 fn invocation_relative_path_to_absolute(span: Span, path: &str) -> PathBuf {
73     let path = Path::new(path);
74     if path.is_absolute() {
75         path.to_path_buf()
76     } else {
77         // `/a/b/c/foo/bar.rs` contains the current macro invocation
78         let mut source_file_path = span.source_file().path();
79         // `/a/b/c/foo/`
80         source_file_path.pop();
81         // `/a/b/c/foo/../locales/en-US/example.ftl`
82         source_file_path.push(path);
83         source_file_path
84     }
85 }
86
87 /// See [rustc_macros::fluent_messages].
88 pub(crate) fn fluent_messages(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
89     let resources = parse_macro_input!(input as Resources);
90
91     // Cannot iterate over individual messages in a bundle, so do that using the
92     // `FluentResource` instead. Construct a bundle anyway to find out if there are conflicting
93     // messages in the resources.
94     let mut bundle = FluentBundle::new(vec![langid!("en-US")]);
95
96     // Map of Fluent identifiers to the `Span` of the resource that defined them, used for better
97     // diagnostics.
98     let mut previous_defns = HashMap::new();
99
100     // Set of Fluent attribute names already output, to avoid duplicate type errors - any given
101     // constant created for a given attribute is the same.
102     let mut previous_attrs = HashSet::new();
103
104     let mut includes = TokenStream::new();
105     let mut generated = TokenStream::new();
106
107     for res in resources.0 {
108         let krate_span = res.krate.span().unwrap();
109         let path_span = res.resource_path.span().unwrap();
110
111         let relative_ftl_path = res.resource_path.value();
112         let absolute_ftl_path =
113             invocation_relative_path_to_absolute(krate_span, &relative_ftl_path);
114         // As this macro also outputs an `include_str!` for this file, the macro will always be
115         // re-executed when the file changes.
116         let mut resource_file = match File::open(absolute_ftl_path) {
117             Ok(resource_file) => resource_file,
118             Err(e) => {
119                 Diagnostic::spanned(path_span, Level::Error, "could not open Fluent resource")
120                     .note(e.to_string())
121                     .emit();
122                 continue;
123             }
124         };
125         let mut resource_contents = String::new();
126         if let Err(e) = resource_file.read_to_string(&mut resource_contents) {
127             Diagnostic::spanned(path_span, Level::Error, "could not read Fluent resource")
128                 .note(e.to_string())
129                 .emit();
130             continue;
131         }
132         let resource = match FluentResource::try_new(resource_contents) {
133             Ok(resource) => resource,
134             Err((this, errs)) => {
135                 Diagnostic::spanned(path_span, Level::Error, "could not parse Fluent resource")
136                     .help("see additional errors emitted")
137                     .emit();
138                 for ParserError { pos, slice: _, kind } in errs {
139                     let mut err = kind.to_string();
140                     // Entirely unnecessary string modification so that the error message starts
141                     // with a lowercase as rustc errors do.
142                     err.replace_range(
143                         0..1,
144                         &err.chars().next().unwrap().to_lowercase().to_string(),
145                     );
146
147                     let line_starts: Vec<usize> = std::iter::once(0)
148                         .chain(
149                             this.source()
150                                 .char_indices()
151                                 .filter_map(|(i, c)| Some(i + 1).filter(|_| c == '\n')),
152                         )
153                         .collect();
154                     let line_start = line_starts
155                         .iter()
156                         .enumerate()
157                         .map(|(line, idx)| (line + 1, idx))
158                         .filter(|(_, idx)| **idx <= pos.start)
159                         .last()
160                         .unwrap()
161                         .0;
162
163                     let snippet = Snippet {
164                         title: Some(Annotation {
165                             label: Some(&err),
166                             id: None,
167                             annotation_type: AnnotationType::Error,
168                         }),
169                         footer: vec![],
170                         slices: vec![Slice {
171                             source: this.source(),
172                             line_start,
173                             origin: Some(&relative_ftl_path),
174                             fold: true,
175                             annotations: vec![SourceAnnotation {
176                                 label: "",
177                                 annotation_type: AnnotationType::Error,
178                                 range: (pos.start, pos.end - 1),
179                             }],
180                         }],
181                         opt: Default::default(),
182                     };
183                     let dl = DisplayList::from(snippet);
184                     eprintln!("{dl}\n");
185                 }
186                 continue;
187             }
188         };
189
190         let mut constants = TokenStream::new();
191         let mut messagerefs = Vec::new();
192         for entry in resource.entries() {
193             let span = res.krate.span();
194             if let Entry::Message(Message { id: Identifier { name }, attributes, value, .. }) =
195                 entry
196             {
197                 let _ = previous_defns.entry(name.to_string()).or_insert(path_span);
198
199                 if name.contains('-') {
200                     Diagnostic::spanned(
201                         path_span,
202                         Level::Error,
203                         format!("name `{name}` contains a '-' character"),
204                     )
205                     .help("replace any '-'s with '_'s")
206                     .emit();
207                 }
208
209                 if let Some(Pattern { elements }) = value {
210                     for elt in elements {
211                         if let PatternElement::Placeable {
212                             expression:
213                                 Expression::Inline(InlineExpression::MessageReference { id, .. }),
214                         } = elt
215                         {
216                             messagerefs.push((id.name, *name));
217                         }
218                     }
219                 }
220
221                 // Require that the message name starts with the crate name
222                 // `hir_typeck_foo_bar` (in `hir_typeck.ftl`)
223                 // `const_eval_baz` (in `const_eval.ftl`)
224                 // `const-eval-hyphen-having` => `hyphen_having` (in `const_eval.ftl`)
225                 // The last case we error about above, but we want to fall back gracefully
226                 // so that only the error is being emitted and not also one about the macro
227                 // failing.
228                 let crate_prefix = format!("{}_", res.krate);
229
230                 let snake_name = name.replace('-', "_");
231                 if !snake_name.starts_with(&crate_prefix) {
232                     Diagnostic::spanned(
233                         path_span,
234                         Level::Error,
235                         format!("name `{name}` does not start with the crate name"),
236                     )
237                     .help(format!(
238                         "prepend `{crate_prefix}` to the slug name: `{crate_prefix}{snake_name}`"
239                     ))
240                     .emit();
241                 };
242
243                 let snake_name = Ident::new(&snake_name, span);
244
245                 constants.extend(quote! {
246                     pub const #snake_name: crate::DiagnosticMessage =
247                         crate::DiagnosticMessage::FluentIdentifier(
248                             std::borrow::Cow::Borrowed(#name),
249                             None
250                         );
251                 });
252
253                 for Attribute { id: Identifier { name: attr_name }, .. } in attributes {
254                     let snake_name = Ident::new(&attr_name.replace('-', "_"), span);
255                     if !previous_attrs.insert(snake_name.clone()) {
256                         continue;
257                     }
258
259                     if attr_name.contains('-') {
260                         Diagnostic::spanned(
261                             path_span,
262                             Level::Error,
263                             format!("attribute `{attr_name}` contains a '-' character"),
264                         )
265                         .help("replace any '-'s with '_'s")
266                         .emit();
267                     }
268
269                     constants.extend(quote! {
270                         pub const #snake_name: crate::SubdiagnosticMessage =
271                             crate::SubdiagnosticMessage::FluentAttr(
272                                 std::borrow::Cow::Borrowed(#attr_name)
273                             );
274                     });
275                 }
276             }
277         }
278
279         for (mref, name) in messagerefs.into_iter() {
280             if !previous_defns.contains_key(mref) {
281                 Diagnostic::spanned(
282                     path_span,
283                     Level::Error,
284                     format!("referenced message `{mref}` does not exist (in message `{name}`)"),
285                 )
286                 .help(&format!("you may have meant to use a variable reference (`{{${mref}}}`)"))
287                 .emit();
288             }
289         }
290
291         if let Err(errs) = bundle.add_resource(resource) {
292             for e in errs {
293                 match e {
294                     FluentError::Overriding { kind, id } => {
295                         Diagnostic::spanned(
296                             path_span,
297                             Level::Error,
298                             format!("overrides existing {kind}: `{id}`"),
299                         )
300                         .span_help(previous_defns[&id], "previously defined in this resource")
301                         .emit();
302                     }
303                     FluentError::ResolverError(_) | FluentError::ParserError(_) => unreachable!(),
304                 }
305             }
306         }
307
308         includes.extend(quote! { include_str!(#relative_ftl_path), });
309
310         generated.extend(constants);
311     }
312
313     quote! {
314         #[allow(non_upper_case_globals)]
315         #[doc(hidden)]
316         pub mod fluent_generated {
317             pub static DEFAULT_LOCALE_RESOURCES: &'static [&'static str] = &[
318                 #includes
319             ];
320
321             #generated
322
323             pub mod _subdiag {
324                 pub const help: crate::SubdiagnosticMessage =
325                     crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("help"));
326                 pub const note: crate::SubdiagnosticMessage =
327                     crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("note"));
328                 pub const warn: crate::SubdiagnosticMessage =
329                     crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("warn"));
330                 pub const label: crate::SubdiagnosticMessage =
331                     crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("label"));
332                 pub const suggestion: crate::SubdiagnosticMessage =
333                     crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("suggestion"));
334             }
335         }
336     }
337     .into()
338 }