]> git.proxmox.com Git - rustc.git/blame - compiler/rustc_macros/src/diagnostics/fluent.rs
New upstream version 1.66.0+dfsg1
[rustc.git] / compiler / rustc_macros / src / diagnostics / fluent.rs
CommitLineData
923072b8
FG
1use annotate_snippets::{
2 display_list::DisplayList,
3 snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation},
4};
5use fluent_bundle::{FluentBundle, FluentError, FluentResource};
6use fluent_syntax::{
7 ast::{Attribute, Entry, Identifier, Message},
8 parser::ParserError,
9};
10use proc_macro::{Diagnostic, Level, Span};
11use proc_macro2::TokenStream;
12use quote::quote;
13use std::{
14 collections::{HashMap, HashSet},
15 fs::File,
16 io::Read,
17 path::{Path, PathBuf},
18};
19use syn::{
20 parse::{Parse, ParseStream},
21 parse_macro_input,
22 punctuated::Punctuated,
23 token, Ident, LitStr, Result,
24};
25use unic_langid::langid;
26
27struct 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
34impl 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
44struct Resources(Punctuated<Resource, token::Comma>);
45
46impl 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.
69fn 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].
85pub(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}