]> git.proxmox.com Git - rustc.git/blob - compiler/rustc_fluent_macro/src/fluent.rs
New upstream version 1.72.1+dfsg1
[rustc.git] / compiler / rustc_fluent_macro / src / 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::{
8 Attribute, Entry, Expression, Identifier, InlineExpression, Message, Pattern,
9 PatternElement,
10 },
11 parser::ParserError,
12 };
13 use proc_macro::{Diagnostic, Level, Span};
14 use proc_macro2::TokenStream;
15 use quote::quote;
16 use std::{
17 collections::{HashMap, HashSet},
18 fs::read_to_string,
19 path::{Path, PathBuf},
20 };
21 use syn::{parse_macro_input, Ident, LitStr};
22 use unic_langid::langid;
23
24 /// Helper function for returning an absolute path for macro-invocation relative file paths.
25 ///
26 /// If the input is already absolute, then the input is returned. If the input is not absolute,
27 /// then it is appended to the directory containing the source file with this macro invocation.
28 fn invocation_relative_path_to_absolute(span: Span, path: &str) -> PathBuf {
29 let path = Path::new(path);
30 if path.is_absolute() {
31 path.to_path_buf()
32 } else {
33 // `/a/b/c/foo/bar.rs` contains the current macro invocation
34 let mut source_file_path = span.source_file().path();
35 // `/a/b/c/foo/`
36 source_file_path.pop();
37 // `/a/b/c/foo/../locales/en-US/example.ftl`
38 source_file_path.push(path);
39 source_file_path
40 }
41 }
42
43 /// Tokens to be returned when the macro cannot proceed.
44 fn failed(crate_name: &Ident) -> proc_macro::TokenStream {
45 quote! {
46 pub static DEFAULT_LOCALE_RESOURCE: &'static str = "";
47
48 #[allow(non_upper_case_globals)]
49 #[doc(hidden)]
50 pub(crate) mod fluent_generated {
51 pub mod #crate_name {
52 }
53
54 pub mod _subdiag {
55 pub const help: crate::SubdiagnosticMessage =
56 crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("help"));
57 pub const note: crate::SubdiagnosticMessage =
58 crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("note"));
59 pub const warn: crate::SubdiagnosticMessage =
60 crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("warn"));
61 pub const label: crate::SubdiagnosticMessage =
62 crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("label"));
63 pub const suggestion: crate::SubdiagnosticMessage =
64 crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("suggestion"));
65 }
66 }
67 }
68 .into()
69 }
70
71 /// See [rustc_fluent_macro::fluent_messages].
72 pub(crate) fn fluent_messages(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
73 let crate_name = std::env::var("CARGO_PKG_NAME")
74 // If `CARGO_PKG_NAME` is missing, then we're probably running in a test, so use
75 // `no_crate`.
76 .unwrap_or_else(|_| "no_crate".to_string())
77 .replace("rustc_", "");
78
79 // Cannot iterate over individual messages in a bundle, so do that using the
80 // `FluentResource` instead. Construct a bundle anyway to find out if there are conflicting
81 // messages in the resources.
82 let mut bundle = FluentBundle::new(vec![langid!("en-US")]);
83
84 // Set of Fluent attribute names already output, to avoid duplicate type errors - any given
85 // constant created for a given attribute is the same.
86 let mut previous_attrs = HashSet::new();
87
88 let resource_str = parse_macro_input!(input as LitStr);
89 let resource_span = resource_str.span().unwrap();
90 let relative_ftl_path = resource_str.value();
91 let absolute_ftl_path = invocation_relative_path_to_absolute(resource_span, &relative_ftl_path);
92
93 let crate_name = Ident::new(&crate_name, resource_str.span());
94
95 // As this macro also outputs an `include_str!` for this file, the macro will always be
96 // re-executed when the file changes.
97 let resource_contents = match read_to_string(absolute_ftl_path) {
98 Ok(resource_contents) => resource_contents,
99 Err(e) => {
100 Diagnostic::spanned(
101 resource_span,
102 Level::Error,
103 format!("could not open Fluent resource: {e}"),
104 )
105 .emit();
106 return failed(&crate_name);
107 }
108 };
109 let mut bad = false;
110 for esc in ["\\n", "\\\"", "\\'"] {
111 for _ in resource_contents.matches(esc) {
112 bad = true;
113 Diagnostic::spanned(resource_span, Level::Error, format!("invalid escape `{esc}` in Fluent resource"))
114 .note("Fluent does not interpret these escape sequences (<https://projectfluent.org/fluent/guide/special.html>)")
115 .emit();
116 }
117 }
118 if bad {
119 return failed(&crate_name);
120 }
121
122 let resource = match FluentResource::try_new(resource_contents) {
123 Ok(resource) => resource,
124 Err((this, errs)) => {
125 Diagnostic::spanned(resource_span, Level::Error, "could not parse Fluent resource")
126 .help("see additional errors emitted")
127 .emit();
128 for ParserError { pos, slice: _, kind } in errs {
129 let mut err = kind.to_string();
130 // Entirely unnecessary string modification so that the error message starts
131 // with a lowercase as rustc errors do.
132 err.replace_range(0..1, &err.chars().next().unwrap().to_lowercase().to_string());
133
134 let line_starts: Vec<usize> = std::iter::once(0)
135 .chain(
136 this.source()
137 .char_indices()
138 .filter_map(|(i, c)| Some(i + 1).filter(|_| c == '\n')),
139 )
140 .collect();
141 let line_start = line_starts
142 .iter()
143 .enumerate()
144 .map(|(line, idx)| (line + 1, idx))
145 .filter(|(_, idx)| **idx <= pos.start)
146 .last()
147 .unwrap()
148 .0;
149
150 let snippet = Snippet {
151 title: Some(Annotation {
152 label: Some(&err),
153 id: None,
154 annotation_type: AnnotationType::Error,
155 }),
156 footer: vec![],
157 slices: vec![Slice {
158 source: this.source(),
159 line_start,
160 origin: Some(&relative_ftl_path),
161 fold: true,
162 annotations: vec![SourceAnnotation {
163 label: "",
164 annotation_type: AnnotationType::Error,
165 range: (pos.start, pos.end - 1),
166 }],
167 }],
168 opt: Default::default(),
169 };
170 let dl = DisplayList::from(snippet);
171 eprintln!("{dl}\n");
172 }
173
174 return failed(&crate_name);
175 }
176 };
177
178 let mut constants = TokenStream::new();
179 let mut previous_defns = HashMap::new();
180 let mut message_refs = Vec::new();
181 for entry in resource.entries() {
182 if let Entry::Message(msg) = entry {
183 let Message { id: Identifier { name }, attributes, value, .. } = msg;
184 let _ = previous_defns.entry(name.to_string()).or_insert(resource_span);
185 if name.contains('-') {
186 Diagnostic::spanned(
187 resource_span,
188 Level::Error,
189 format!("name `{name}` contains a '-' character"),
190 )
191 .help("replace any '-'s with '_'s")
192 .emit();
193 }
194
195 if let Some(Pattern { elements }) = value {
196 for elt in elements {
197 if let PatternElement::Placeable {
198 expression:
199 Expression::Inline(InlineExpression::MessageReference { id, .. }),
200 } = elt
201 {
202 message_refs.push((id.name, *name));
203 }
204 }
205 }
206
207 // `typeck_foo_bar` => `foo_bar` (in `typeck.ftl`)
208 // `const_eval_baz` => `baz` (in `const_eval.ftl`)
209 // `const-eval-hyphen-having` => `hyphen_having` (in `const_eval.ftl`)
210 // The last case we error about above, but we want to fall back gracefully
211 // so that only the error is being emitted and not also one about the macro
212 // failing.
213 let crate_prefix = format!("{crate_name}_");
214
215 let snake_name = name.replace('-', "_");
216 if !snake_name.starts_with(&crate_prefix) {
217 Diagnostic::spanned(
218 resource_span,
219 Level::Error,
220 format!("name `{name}` does not start with the crate name"),
221 )
222 .help(format!(
223 "prepend `{crate_prefix}` to the slug name: `{crate_prefix}{snake_name}`"
224 ))
225 .emit();
226 };
227 let snake_name = Ident::new(&snake_name, resource_str.span());
228
229 if !previous_attrs.insert(snake_name.clone()) {
230 continue;
231 }
232
233 let docstr =
234 format!("Constant referring to Fluent message `{name}` from `{crate_name}`");
235 constants.extend(quote! {
236 #[doc = #docstr]
237 pub const #snake_name: crate::DiagnosticMessage =
238 crate::DiagnosticMessage::FluentIdentifier(
239 std::borrow::Cow::Borrowed(#name),
240 None
241 );
242 });
243
244 for Attribute { id: Identifier { name: attr_name }, .. } in attributes {
245 let snake_name = Ident::new(
246 &format!("{}{}", &crate_prefix, &attr_name.replace('-', "_")),
247 resource_str.span(),
248 );
249 if !previous_attrs.insert(snake_name.clone()) {
250 continue;
251 }
252
253 if attr_name.contains('-') {
254 Diagnostic::spanned(
255 resource_span,
256 Level::Error,
257 format!("attribute `{attr_name}` contains a '-' character"),
258 )
259 .help("replace any '-'s with '_'s")
260 .emit();
261 }
262
263 let msg = format!(
264 "Constant referring to Fluent message `{name}.{attr_name}` from `{crate_name}`"
265 );
266 constants.extend(quote! {
267 #[doc = #msg]
268 pub const #snake_name: crate::SubdiagnosticMessage =
269 crate::SubdiagnosticMessage::FluentAttr(
270 std::borrow::Cow::Borrowed(#attr_name)
271 );
272 });
273 }
274
275 // Record variables referenced by these messages so we can produce
276 // tests in the derive diagnostics to validate them.
277 let ident = quote::format_ident!("{snake_name}_refs");
278 let vrefs = variable_references(msg);
279 constants.extend(quote! {
280 #[cfg(test)]
281 pub const #ident: &[&str] = &[#(#vrefs),*];
282 })
283 }
284 }
285
286 for (mref, name) in message_refs.into_iter() {
287 if !previous_defns.contains_key(mref) {
288 Diagnostic::spanned(
289 resource_span,
290 Level::Error,
291 format!("referenced message `{mref}` does not exist (in message `{name}`)"),
292 )
293 .help(&format!("you may have meant to use a variable reference (`{{${mref}}}`)"))
294 .emit();
295 }
296 }
297
298 if let Err(errs) = bundle.add_resource(resource) {
299 for e in errs {
300 match e {
301 FluentError::Overriding { kind, id } => {
302 Diagnostic::spanned(
303 resource_span,
304 Level::Error,
305 format!("overrides existing {kind}: `{id}`"),
306 )
307 .emit();
308 }
309 FluentError::ResolverError(_) | FluentError::ParserError(_) => unreachable!(),
310 }
311 }
312 }
313
314 quote! {
315 /// Raw content of Fluent resource for this crate, generated by `fluent_messages` macro,
316 /// imported by `rustc_driver` to include all crates' resources in one bundle.
317 pub static DEFAULT_LOCALE_RESOURCE: &'static str = include_str!(#relative_ftl_path);
318
319 #[allow(non_upper_case_globals)]
320 #[doc(hidden)]
321 /// Auto-generated constants for type-checked references to Fluent messages.
322 pub(crate) mod fluent_generated {
323 #constants
324
325 /// Constants expected to exist by the diagnostic derive macros to use as default Fluent
326 /// identifiers for different subdiagnostic kinds.
327 pub mod _subdiag {
328 /// Default for `#[help]`
329 pub const help: crate::SubdiagnosticMessage =
330 crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("help"));
331 /// Default for `#[note]`
332 pub const note: crate::SubdiagnosticMessage =
333 crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("note"));
334 /// Default for `#[warn]`
335 pub const warn: crate::SubdiagnosticMessage =
336 crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("warn"));
337 /// Default for `#[label]`
338 pub const label: crate::SubdiagnosticMessage =
339 crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("label"));
340 /// Default for `#[suggestion]`
341 pub const suggestion: crate::SubdiagnosticMessage =
342 crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("suggestion"));
343 }
344 }
345 }
346 .into()
347 }
348
349 fn variable_references<'a>(msg: &Message<&'a str>) -> Vec<&'a str> {
350 let mut refs = vec![];
351 if let Some(Pattern { elements }) = &msg.value {
352 for elt in elements {
353 if let PatternElement::Placeable {
354 expression: Expression::Inline(InlineExpression::VariableReference { id }),
355 } = elt
356 {
357 refs.push(id.name);
358 }
359 }
360 }
361 for attr in &msg.attributes {
362 for elt in &attr.value.elements {
363 if let PatternElement::Placeable {
364 expression: Expression::Inline(InlineExpression::VariableReference { id }),
365 } = elt
366 {
367 refs.push(id.name);
368 }
369 }
370 }
371 refs
372 }