]>
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 { | |
2b03887a | 28 | krate: Ident, |
923072b8 FG |
29 | #[allow(dead_code)] |
30 | fat_arrow_token: token::FatArrow, | |
2b03887a | 31 | resource_path: LitStr, |
923072b8 FG |
32 | } |
33 | ||
34 | impl Parse for Resource { | |
35 | fn parse(input: ParseStream<'_>) -> Result<Self> { | |
36 | Ok(Resource { | |
2b03887a | 37 | krate: input.parse()?, |
923072b8 | 38 | fat_arrow_token: input.parse()?, |
2b03887a | 39 | resource_path: input.parse()?, |
923072b8 FG |
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 | ||
2b03887a FG |
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 | ||
923072b8 FG |
101 | let mut includes = TokenStream::new(); |
102 | let mut generated = TokenStream::new(); | |
923072b8 | 103 | |
2b03887a FG |
104 | for res in resources.0 { |
105 | let krate_span = res.krate.span().unwrap(); | |
106 | let path_span = res.resource_path.span().unwrap(); | |
923072b8 | 107 | |
2b03887a | 108 | let relative_ftl_path = res.resource_path.value(); |
923072b8 | 109 | let absolute_ftl_path = |
2b03887a | 110 | invocation_relative_path_to_absolute(krate_span, &relative_ftl_path); |
923072b8 FG |
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!("{}\n", dl); | |
182 | } | |
183 | continue; | |
184 | } | |
185 | }; | |
186 | ||
187 | let mut constants = TokenStream::new(); | |
188 | for entry in resource.entries() { | |
2b03887a | 189 | let span = res.krate.span(); |
923072b8 | 190 | if let Entry::Message(Message { id: Identifier { name }, attributes, .. }) = entry { |
f2b60f7d FG |
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 | ||
2b03887a FG |
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`) | |
f2b60f7d FG |
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. | |
2b03887a | 210 | let crate_prefix = format!("{}_", res.krate); |
f2b60f7d FG |
211 | |
212 | let snake_name = name.replace('-', "_"); | |
2b03887a FG |
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(); | |
f2b60f7d | 223 | }; |
923072b8 | 224 | |
2b03887a FG |
225 | let snake_name = Ident::new(&snake_name, span); |
226 | ||
923072b8 FG |
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 { | |
064997fb | 236 | let snake_name = Ident::new(&attr_name.replace('-', "_"), span); |
923072b8 FG |
237 | if !previous_attrs.insert(snake_name.clone()) { |
238 | continue; | |
239 | } | |
240 | ||
f2b60f7d FG |
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 | ||
923072b8 FG |
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( | |
f2b60f7d | 266 | path_span, |
923072b8 FG |
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 | ||
2b03887a | 280 | generated.extend(constants); |
923072b8 FG |
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 | |
064997fb FG |
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 | } | |
923072b8 FG |
305 | } |
306 | } | |
307 | .into() | |
308 | } |