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