1#![allow(clippy::pedantic)]
2use std::{
3 collections::{HashMap, HashSet},
4 fs::read_to_string,
5 path::{Path, PathBuf},
6};
7
8use annotate_snippets::{Annotation, AnnotationType, Renderer, Slice, Snippet, SourceAnnotation};
9use fluent_bundle::{FluentBundle, FluentError, FluentResource};
10use fluent_syntax::{
11 ast::{
12 Attribute, Entry, Expression, Identifier, InlineExpression, Message, Pattern,
13 PatternElement,
14 },
15 parser::ParserError,
16};
17use proc_macro::{Diagnostic, Level, Span};
18use proc_macro2::TokenStream;
19use quote::quote;
20use syn::{Ident, LitStr, parse_macro_input};
21use unic_langid::langid;
22
23fn invocation_relative_path_to_absolute(span: Span, path: &str) -> PathBuf {
28 let path = Path::new(path);
29 if path.is_absolute() {
30 path.to_path_buf()
31 } else {
32 let mut source_file_path = span.source_file().path();
34 source_file_path.pop();
36 source_file_path.push(path);
38 source_file_path
39 }
40}
41
42fn finish(body: TokenStream, resource: TokenStream) -> proc_macro::TokenStream {
44 quote! {
45 pub static DEFAULT_LOCALE_RESOURCE: &'static str = #resource;
48
49 #[allow(non_upper_case_globals)]
50 #[doc(hidden)]
51 pub(crate) mod fluent_generated {
53 #body
54
55 pub mod _subdiag {
58 pub const help: rustc_errors::SubdiagMessage =
60 rustc_errors::SubdiagMessage::FluentAttr(std::borrow::Cow::Borrowed("help"));
61 pub const note: rustc_errors::SubdiagMessage =
63 rustc_errors::SubdiagMessage::FluentAttr(std::borrow::Cow::Borrowed("note"));
64 pub const warn: rustc_errors::SubdiagMessage =
66 rustc_errors::SubdiagMessage::FluentAttr(std::borrow::Cow::Borrowed("warn"));
67 pub const label: rustc_errors::SubdiagMessage =
69 rustc_errors::SubdiagMessage::FluentAttr(std::borrow::Cow::Borrowed("label"));
70 pub const suggestion: rustc_errors::SubdiagMessage =
72 rustc_errors::SubdiagMessage::FluentAttr(std::borrow::Cow::Borrowed("suggestion"));
73 }
74 }
75 }
76 .into()
77}
78
79fn failed(crate_name: &Ident) -> proc_macro::TokenStream {
81 finish(quote! { pub mod #crate_name {} }, quote! { "" })
82}
83
84pub(crate) fn fluent_messages(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
86 let crate_name = std::env::var("CARGO_PKG_NAME")
87 .unwrap_or_else(|_| "no_crate".to_string())
90 .replace("-", "_")
91 .replace("flux_", "");
92
93 let mut bundle = FluentBundle::new(vec![langid!("en-US")]);
97
98 let mut previous_attrs = HashSet::new();
101
102 let resource_str = parse_macro_input!(input as LitStr);
103 let resource_span = resource_str.span().unwrap();
104 let relative_ftl_path = resource_str.value();
105 let absolute_ftl_path = invocation_relative_path_to_absolute(resource_span, &relative_ftl_path);
106
107 let crate_name = Ident::new(&crate_name, resource_str.span());
108
109 let resource_contents = match read_to_string(absolute_ftl_path) {
112 Ok(resource_contents) => resource_contents,
113 Err(e) => {
114 Diagnostic::spanned(
115 resource_span,
116 Level::Error,
117 format!("could not open Fluent resource: {e}"),
118 )
119 .emit();
120 return failed(&crate_name);
121 }
122 };
123 let mut bad = false;
124 for esc in ["\\n", "\\\"", "\\'"] {
125 for _ in resource_contents.matches(esc) {
126 bad = true;
127 Diagnostic::spanned(resource_span, Level::Error, format!("invalid escape `{esc}` in Fluent resource"))
128 .note("Fluent does not interpret these escape sequences (<https://projectfluent.org/fluent/guide/special.html>)")
129 .emit();
130 }
131 }
132 if bad {
133 return failed(&crate_name);
134 }
135
136 let resource = match FluentResource::try_new(resource_contents) {
137 Ok(resource) => resource,
138 Err((this, errs)) => {
139 Diagnostic::spanned(resource_span, Level::Error, "could not parse Fluent resource")
140 .help("see additional errors emitted")
141 .emit();
142 for ParserError { pos, slice: _, kind } in errs {
143 let mut err = kind.to_string();
144 err.replace_range(0..1, &err.chars().next().unwrap().to_lowercase().to_string());
147
148 let line_starts: Vec<usize> = std::iter::once(0)
149 .chain(
150 this.source()
151 .char_indices()
152 .filter_map(|(i, c)| Some(i + 1).filter(|_| c == '\n')),
153 )
154 .collect();
155 let line_start = line_starts
156 .iter()
157 .enumerate()
158 .map(|(line, idx)| (line + 1, idx))
159 .filter(|(_, idx)| **idx <= pos.start)
160 .last()
161 .unwrap()
162 .0;
163
164 let snippet = Snippet {
165 title: Some(Annotation {
166 label: Some(&err),
167 id: None,
168 annotation_type: AnnotationType::Error,
169 }),
170 footer: vec![],
171 slices: vec![Slice {
172 source: this.source(),
173 line_start,
174 origin: Some(&relative_ftl_path),
175 fold: true,
176 annotations: vec![SourceAnnotation {
177 label: "",
178 annotation_type: AnnotationType::Error,
179 range: (pos.start, pos.end - 1),
180 }],
181 }],
182 };
183 let renderer = Renderer::plain();
184 eprintln!("{}\n", renderer.render(snippet));
185 }
186
187 return failed(&crate_name);
188 }
189 };
190
191 let mut constants = TokenStream::new();
192 let mut previous_defns = HashMap::new();
193 let mut message_refs = Vec::new();
194 for entry in resource.entries() {
195 if let Entry::Message(msg) = entry {
196 let Message { id: Identifier { name }, attributes, value, .. } = msg;
197 let _ = previous_defns
198 .entry((*name).to_string())
199 .or_insert(resource_span);
200 if name.contains('-') {
201 Diagnostic::spanned(
202 resource_span,
203 Level::Error,
204 format!("name `{name}` contains a '-' character"),
205 )
206 .help("replace any '-'s with '_'s")
207 .emit();
208 }
209
210 if let Some(Pattern { elements }) = value {
211 for elt in elements {
212 if let PatternElement::Placeable {
213 expression:
214 Expression::Inline(InlineExpression::MessageReference { id, .. }),
215 } = elt
216 {
217 message_refs.push((id.name, *name));
218 }
219 }
220 }
221
222 let crate_prefix = format!("{crate_name}_");
229
230 let snake_name = name.replace('-', "_");
231 if !snake_name.starts_with(&crate_prefix) {
232 Diagnostic::spanned(
233 resource_span,
234 Level::Error,
235 format!("name `{name}` does not start with the crate name"),
236 )
237 .help(format!(
238 "prepend `{crate_prefix}` to the slug name: `{crate_prefix}{snake_name}`"
239 ))
240 .emit();
241 };
242 let snake_name = Ident::new(&snake_name, resource_str.span());
243
244 if !previous_attrs.insert(snake_name.clone()) {
245 continue;
246 }
247
248 let docstr =
249 format!("Constant referring to Fluent message `{name}` from `{crate_name}`");
250 constants.extend(quote! {
251 #[doc = #docstr]
252 pub const #snake_name: rustc_errors::DiagMessage =
253 rustc_errors::DiagMessage::FluentIdentifier(
254 std::borrow::Cow::Borrowed(#name),
255 None
256 );
257 });
258
259 for Attribute { id: Identifier { name: attr_name }, .. } in attributes {
260 let snake_name = Ident::new(
261 &format!("{}{}", &crate_prefix, &attr_name.replace('-', "_")),
262 resource_str.span(),
263 );
264 if !previous_attrs.insert(snake_name.clone()) {
265 continue;
266 }
267
268 if attr_name.contains('-') {
269 Diagnostic::spanned(
270 resource_span,
271 Level::Error,
272 format!("attribute `{attr_name}` contains a '-' character"),
273 )
274 .help("replace any '-'s with '_'s")
275 .emit();
276 }
277
278 let msg = format!(
279 "Constant referring to Fluent message `{name}.{attr_name}` from `{crate_name}`"
280 );
281 constants.extend(quote! {
282 #[doc = #msg]
283 pub const #snake_name: rustc_errors::SubdiagMessage =
284 rustc_errors::SubdiagMessage::FluentAttr(std::borrow::Cow::Borrowed(#attr_name));
285 });
286 }
287
288 let ident = quote::format_ident!("{snake_name}_refs");
291 let vrefs = variable_references(msg);
292 constants.extend(quote! {
293 #[cfg(test)]
294 pub const #ident: &[&str] = &[#(#vrefs),*];
295 })
296 }
297 }
298
299 for (mref, name) in message_refs.into_iter() {
300 if !previous_defns.contains_key(mref) {
301 Diagnostic::spanned(
302 resource_span,
303 Level::Error,
304 format!("referenced message `{mref}` does not exist (in message `{name}`)"),
305 )
306 .help(&format!("you may have meant to use a variable reference (`{{${mref}}}`)"))
307 .emit();
308 }
309 }
310
311 if let Err(errs) = bundle.add_resource(resource) {
312 for e in errs {
313 match e {
314 FluentError::Overriding { kind, id } => {
315 Diagnostic::spanned(
316 resource_span,
317 Level::Error,
318 format!("overrides existing {kind}: `{id}`"),
319 )
320 .emit();
321 }
322 FluentError::ResolverError(_) | FluentError::ParserError(_) => unreachable!(),
323 }
324 }
325 }
326
327 finish(constants, quote! { include_str!(#relative_ftl_path) })
328}
329
330fn variable_references<'a>(msg: &Message<&'a str>) -> Vec<&'a str> {
331 let mut refs = vec![];
332 if let Some(Pattern { elements }) = &msg.value {
333 for elt in elements {
334 if let PatternElement::Placeable {
335 expression: Expression::Inline(InlineExpression::VariableReference { id }),
336 } = elt
337 {
338 refs.push(id.name);
339 }
340 }
341 }
342 for attr in &msg.attributes {
343 for elt in &attr.value.elements {
344 if let PatternElement::Placeable {
345 expression: Expression::Inline(InlineExpression::VariableReference { id }),
346 } = elt
347 {
348 refs.push(id.name);
349 }
350 }
351 }
352 refs
353}