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