]> git.proxmox.com Git - rustc.git/blob - compiler/rustc_macros/src/diagnostics/fluent.rs
New upstream version 1.64.0+dfsg1
[rustc.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` (in `typeck.ftl`)
193 // `const-eval-baz` => `baz` (in `const_eval.ftl`)
194 let snake_name = Ident::new(
195 // FIXME: should probably trim prefix, not replace all occurrences
196 &name
197 .replace(&format!("{}-", res.ident).replace('_', "-"), "")
198 .replace('-', "_"),
199 span,
200 );
201 constants.extend(quote! {
202 pub const #snake_name: crate::DiagnosticMessage =
203 crate::DiagnosticMessage::FluentIdentifier(
204 std::borrow::Cow::Borrowed(#name),
205 None
206 );
207 });
208
209 for Attribute { id: Identifier { name: attr_name }, .. } in attributes {
210 let snake_name = Ident::new(&attr_name.replace('-', "_"), span);
211 if !previous_attrs.insert(snake_name.clone()) {
212 continue;
213 }
214
215 constants.extend(quote! {
216 pub const #snake_name: crate::SubdiagnosticMessage =
217 crate::SubdiagnosticMessage::FluentAttr(
218 std::borrow::Cow::Borrowed(#attr_name)
219 );
220 });
221 }
222 }
223 }
224
225 if let Err(errs) = bundle.add_resource(resource) {
226 for e in errs {
227 match e {
228 FluentError::Overriding { kind, id } => {
229 Diagnostic::spanned(
230 ident_span,
231 Level::Error,
232 format!("overrides existing {}: `{}`", kind, id),
233 )
234 .span_help(previous_defns[&id], "previously defined in this resource")
235 .emit();
236 }
237 FluentError::ResolverError(_) | FluentError::ParserError(_) => unreachable!(),
238 }
239 }
240 }
241
242 includes.extend(quote! { include_str!(#relative_ftl_path), });
243
244 let ident = res.ident;
245 generated.extend(quote! {
246 pub mod #ident {
247 #constants
248 }
249 });
250 }
251
252 quote! {
253 #[allow(non_upper_case_globals)]
254 #[doc(hidden)]
255 pub mod fluent_generated {
256 pub static DEFAULT_LOCALE_RESOURCES: &'static [&'static str] = &[
257 #includes
258 ];
259
260 #generated
261
262 pub mod _subdiag {
263 pub const help: crate::SubdiagnosticMessage =
264 crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("help"));
265 pub const note: crate::SubdiagnosticMessage =
266 crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("note"));
267 pub const warn: crate::SubdiagnosticMessage =
268 crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("warn"));
269 pub const label: crate::SubdiagnosticMessage =
270 crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("label"));
271 pub const suggestion: crate::SubdiagnosticMessage =
272 crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("suggestion"));
273 }
274 }
275 }
276 .into()
277 }