]>
Commit | Line | Data |
---|---|---|
923072b8 FG |
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 { | |
f2b60f7d FG |
190 | let _ = previous_defns.entry(name.to_string()).or_insert(path_span); |
191 | ||
192 | if name.contains('-') { | |
193 | Diagnostic::spanned( | |
194 | path_span, | |
195 | Level::Error, | |
196 | format!("name `{name}` contains a '-' character"), | |
197 | ) | |
198 | .help("replace any '-'s with '_'s") | |
199 | .emit(); | |
200 | } | |
201 | ||
202 | // `typeck_foo_bar` => `foo_bar` (in `typeck.ftl`) | |
203 | // `const_eval_baz` => `baz` (in `const_eval.ftl`) | |
204 | // `const-eval-hyphen-having` => `hyphen_having` (in `const_eval.ftl`) | |
205 | // The last case we error about above, but we want to fall back gracefully | |
206 | // so that only the error is being emitted and not also one about the macro | |
207 | // failing. | |
208 | let crate_prefix = format!("{}_", res.ident); | |
209 | ||
210 | let snake_name = name.replace('-', "_"); | |
211 | let snake_name = match snake_name.strip_prefix(&crate_prefix) { | |
212 | Some(rest) => Ident::new(rest, span), | |
213 | None => { | |
214 | Diagnostic::spanned( | |
215 | path_span, | |
216 | Level::Error, | |
217 | format!("name `{name}` does not start with the crate name"), | |
218 | ) | |
219 | .help(format!("prepend `{crate_prefix}` to the slug name: `{crate_prefix}{snake_name}`")) | |
220 | .emit(); | |
221 | Ident::new(&snake_name, span) | |
222 | } | |
223 | }; | |
923072b8 | 224 | |
923072b8 FG |
225 | constants.extend(quote! { |
226 | pub const #snake_name: crate::DiagnosticMessage = | |
227 | crate::DiagnosticMessage::FluentIdentifier( | |
228 | std::borrow::Cow::Borrowed(#name), | |
229 | None | |
230 | ); | |
231 | }); | |
232 | ||
233 | for Attribute { id: Identifier { name: attr_name }, .. } in attributes { | |
064997fb | 234 | let snake_name = Ident::new(&attr_name.replace('-', "_"), span); |
923072b8 FG |
235 | if !previous_attrs.insert(snake_name.clone()) { |
236 | continue; | |
237 | } | |
238 | ||
f2b60f7d FG |
239 | if attr_name.contains('-') { |
240 | Diagnostic::spanned( | |
241 | path_span, | |
242 | Level::Error, | |
243 | format!("attribute `{attr_name}` contains a '-' character"), | |
244 | ) | |
245 | .help("replace any '-'s with '_'s") | |
246 | .emit(); | |
247 | } | |
248 | ||
923072b8 FG |
249 | constants.extend(quote! { |
250 | pub const #snake_name: crate::SubdiagnosticMessage = | |
251 | crate::SubdiagnosticMessage::FluentAttr( | |
252 | std::borrow::Cow::Borrowed(#attr_name) | |
253 | ); | |
254 | }); | |
255 | } | |
256 | } | |
257 | } | |
258 | ||
259 | if let Err(errs) = bundle.add_resource(resource) { | |
260 | for e in errs { | |
261 | match e { | |
262 | FluentError::Overriding { kind, id } => { | |
263 | Diagnostic::spanned( | |
f2b60f7d | 264 | path_span, |
923072b8 FG |
265 | Level::Error, |
266 | format!("overrides existing {}: `{}`", kind, id), | |
267 | ) | |
268 | .span_help(previous_defns[&id], "previously defined in this resource") | |
269 | .emit(); | |
270 | } | |
271 | FluentError::ResolverError(_) | FluentError::ParserError(_) => unreachable!(), | |
272 | } | |
273 | } | |
274 | } | |
275 | ||
276 | includes.extend(quote! { include_str!(#relative_ftl_path), }); | |
277 | ||
278 | let ident = res.ident; | |
279 | generated.extend(quote! { | |
280 | pub mod #ident { | |
281 | #constants | |
282 | } | |
283 | }); | |
284 | } | |
285 | ||
286 | quote! { | |
287 | #[allow(non_upper_case_globals)] | |
288 | #[doc(hidden)] | |
289 | pub mod fluent_generated { | |
290 | pub static DEFAULT_LOCALE_RESOURCES: &'static [&'static str] = &[ | |
291 | #includes | |
292 | ]; | |
293 | ||
294 | #generated | |
064997fb FG |
295 | |
296 | pub mod _subdiag { | |
297 | pub const help: crate::SubdiagnosticMessage = | |
298 | crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("help")); | |
299 | pub const note: crate::SubdiagnosticMessage = | |
300 | crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("note")); | |
301 | pub const warn: crate::SubdiagnosticMessage = | |
302 | crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("warn")); | |
303 | pub const label: crate::SubdiagnosticMessage = | |
304 | crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("label")); | |
305 | pub const suggestion: crate::SubdiagnosticMessage = | |
306 | crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("suggestion")); | |
307 | } | |
923072b8 FG |
308 | } |
309 | } | |
310 | .into() | |
311 | } |