]> git.lizzy.rs Git - rust.git/blob - compiler/rustc_macros/src/diagnostics/fluent.rs
Auto merge of #96964 - oli-obk:const_trait_mvp, r=compiler-errors
[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,
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     ident: Ident,
29     #[allow(dead_code)]
30     fat_arrow_token: token::FatArrow,
31     resource: LitStr,
32 }
33
34 impl Parse for Resource {
35     fn parse(input: ParseStream<'_>) -> Result<Self> {
36         Ok(Resource {
37             ident: input.parse()?,
38             fat_arrow_token: input.parse()?,
39             resource: 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     let mut includes = TokenStream::new();
98     let mut generated = TokenStream::new();
99     for res in resources.0 {
100         let ident_span = res.ident.span().unwrap();
101         let path_span = res.resource.span().unwrap();
102
103         let relative_ftl_path = res.resource.value();
104         let absolute_ftl_path =
105             invocation_relative_path_to_absolute(ident_span, &relative_ftl_path);
106         // As this macro also outputs an `include_str!` for this file, the macro will always be
107         // re-executed when the file changes.
108         let mut resource_file = match File::open(absolute_ftl_path) {
109             Ok(resource_file) => resource_file,
110             Err(e) => {
111                 Diagnostic::spanned(path_span, Level::Error, "could not open Fluent resource")
112                     .note(e.to_string())
113                     .emit();
114                 continue;
115             }
116         };
117         let mut resource_contents = String::new();
118         if let Err(e) = resource_file.read_to_string(&mut resource_contents) {
119             Diagnostic::spanned(path_span, Level::Error, "could not read Fluent resource")
120                 .note(e.to_string())
121                 .emit();
122             continue;
123         }
124         let resource = match FluentResource::try_new(resource_contents) {
125             Ok(resource) => resource,
126             Err((this, errs)) => {
127                 Diagnostic::spanned(path_span, Level::Error, "could not parse Fluent resource")
128                     .help("see additional errors emitted")
129                     .emit();
130                 for ParserError { pos, slice: _, kind } in errs {
131                     let mut err = kind.to_string();
132                     // Entirely unnecessary string modification so that the error message starts
133                     // with a lowercase as rustc errors do.
134                     err.replace_range(
135                         0..1,
136                         &err.chars().next().unwrap().to_lowercase().to_string(),
137                     );
138
139                     let line_starts: Vec<usize> = std::iter::once(0)
140                         .chain(
141                             this.source()
142                                 .char_indices()
143                                 .filter_map(|(i, c)| Some(i + 1).filter(|_| c == '\n')),
144                         )
145                         .collect();
146                     let line_start = line_starts
147                         .iter()
148                         .enumerate()
149                         .map(|(line, idx)| (line + 1, idx))
150                         .filter(|(_, idx)| **idx <= pos.start)
151                         .last()
152                         .unwrap()
153                         .0;
154
155                     let snippet = Snippet {
156                         title: Some(Annotation {
157                             label: Some(&err),
158                             id: None,
159                             annotation_type: AnnotationType::Error,
160                         }),
161                         footer: vec![],
162                         slices: vec![Slice {
163                             source: this.source(),
164                             line_start,
165                             origin: Some(&relative_ftl_path),
166                             fold: true,
167                             annotations: vec![SourceAnnotation {
168                                 label: "",
169                                 annotation_type: AnnotationType::Error,
170                                 range: (pos.start, pos.end - 1),
171                             }],
172                         }],
173                         opt: Default::default(),
174                     };
175                     let dl = DisplayList::from(snippet);
176                     eprintln!("{}\n", dl);
177                 }
178                 continue;
179             }
180         };
181
182         let mut constants = TokenStream::new();
183         for entry in resource.entries() {
184             let span = res.ident.span();
185             if let Entry::Message(Message { id: Identifier { name }, attributes, .. }) = entry {
186                 let _ = previous_defns.entry(name.to_string()).or_insert(ident_span);
187
188                 // `typeck-foo-bar` => `foo_bar`
189                 let snake_name = Ident::new(
190                     &name.replace(&format!("{}-", res.ident), "").replace("-", "_"),
191                     span,
192                 );
193                 constants.extend(quote! {
194                     pub const #snake_name: crate::DiagnosticMessage =
195                         crate::DiagnosticMessage::FluentIdentifier(
196                             std::borrow::Cow::Borrowed(#name),
197                             None
198                         );
199                 });
200
201                 for Attribute { id: Identifier { name: attr_name }, .. } in attributes {
202                     let attr_snake_name = attr_name.replace("-", "_");
203                     let snake_name = Ident::new(&format!("{snake_name}_{attr_snake_name}"), span);
204                     constants.extend(quote! {
205                         pub const #snake_name: crate::DiagnosticMessage =
206                             crate::DiagnosticMessage::FluentIdentifier(
207                                 std::borrow::Cow::Borrowed(#name),
208                                 Some(std::borrow::Cow::Borrowed(#attr_name))
209                             );
210                     });
211                 }
212             }
213         }
214
215         if let Err(errs) = bundle.add_resource(resource) {
216             for e in errs {
217                 match e {
218                     FluentError::Overriding { kind, id } => {
219                         Diagnostic::spanned(
220                             ident_span,
221                             Level::Error,
222                             format!("overrides existing {}: `{}`", kind, id),
223                         )
224                         .span_help(previous_defns[&id], "previously defined in this resource")
225                         .emit();
226                     }
227                     FluentError::ResolverError(_) | FluentError::ParserError(_) => unreachable!(),
228                 }
229             }
230         }
231
232         includes.extend(quote! { include_str!(#relative_ftl_path), });
233
234         let ident = res.ident;
235         generated.extend(quote! {
236             pub mod #ident {
237                 #constants
238             }
239         });
240     }
241
242     quote! {
243         #[allow(non_upper_case_globals)]
244         #[doc(hidden)]
245         pub mod fluent_generated {
246             pub static DEFAULT_LOCALE_RESOURCES: &'static [&'static str] = &[
247                 #includes
248             ];
249
250             #generated
251         }
252     }
253     .into()
254 }