rustc_fluent_macro/
fluent.rs
1use std::collections::{HashMap, HashSet};
2use std::fs::read_to_string;
3use std::path::{Path, PathBuf};
4
5use annotate_snippets::{Renderer, Snippet};
6use fluent_bundle::{FluentBundle, FluentError, FluentResource};
7use fluent_syntax::ast::{
8 Attribute, Entry, Expression, Identifier, InlineExpression, Message, Pattern, PatternElement,
9};
10use fluent_syntax::parser::ParserError;
11use proc_macro::tracked_path::path;
12use proc_macro::{Diagnostic, Level, Span};
13use proc_macro2::TokenStream;
14use quote::quote;
15use syn::{Ident, LitStr, parse_macro_input};
16use unic_langid::langid;
17
18fn invocation_relative_path_to_absolute(span: Span, path: &str) -> PathBuf {
23 let path = Path::new(path);
24 if path.is_absolute() {
25 path.to_path_buf()
26 } else {
27 let mut source_file_path = span.source_file().path();
29 source_file_path.pop();
31 source_file_path.push(path);
33 source_file_path
34 }
35}
36
37fn finish(body: TokenStream, resource: TokenStream) -> proc_macro::TokenStream {
39 quote! {
40 pub static DEFAULT_LOCALE_RESOURCE: &'static str = #resource;
43
44 #[allow(non_upper_case_globals)]
45 #[doc(hidden)]
46 pub(crate) mod fluent_generated {
48 #body
49
50 pub mod _subdiag {
53 pub const help: rustc_errors::SubdiagMessage =
55 rustc_errors::SubdiagMessage::FluentAttr(std::borrow::Cow::Borrowed("help"));
56 pub const note: rustc_errors::SubdiagMessage =
58 rustc_errors::SubdiagMessage::FluentAttr(std::borrow::Cow::Borrowed("note"));
59 pub const warn: rustc_errors::SubdiagMessage =
61 rustc_errors::SubdiagMessage::FluentAttr(std::borrow::Cow::Borrowed("warn"));
62 pub const label: rustc_errors::SubdiagMessage =
64 rustc_errors::SubdiagMessage::FluentAttr(std::borrow::Cow::Borrowed("label"));
65 pub const suggestion: rustc_errors::SubdiagMessage =
67 rustc_errors::SubdiagMessage::FluentAttr(std::borrow::Cow::Borrowed("suggestion"));
68 }
69 }
70 }
71 .into()
72}
73
74fn failed(crate_name: &Ident) -> proc_macro::TokenStream {
76 finish(quote! { pub mod #crate_name {} }, quote! { "" })
77}
78
79pub(crate) fn fluent_messages(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
81 let crate_name = std::env::var("CARGO_PKG_NAME")
82 .unwrap_or_else(|_| "no_crate".to_string())
85 .replace("rustc_", "");
86
87 let mut bundle = FluentBundle::new(vec![langid!("en-US")]);
91
92 let mut previous_attrs = HashSet::new();
95
96 let resource_str = parse_macro_input!(input as LitStr);
97 let resource_span = resource_str.span().unwrap();
98 let relative_ftl_path = resource_str.value();
99 let absolute_ftl_path = invocation_relative_path_to_absolute(resource_span, &relative_ftl_path);
100
101 let crate_name = Ident::new(&crate_name, resource_str.span());
102
103 path(absolute_ftl_path.to_str().unwrap());
104 let resource_contents = match read_to_string(absolute_ftl_path) {
105 Ok(resource_contents) => resource_contents,
106 Err(e) => {
107 Diagnostic::spanned(
108 resource_span,
109 Level::Error,
110 format!("could not open Fluent resource: {e}"),
111 )
112 .emit();
113 return failed(&crate_name);
114 }
115 };
116 let mut bad = false;
117 for esc in ["\\n", "\\\"", "\\'"] {
118 for _ in resource_contents.matches(esc) {
119 bad = true;
120 Diagnostic::spanned(resource_span, Level::Error, format!("invalid escape `{esc}` in Fluent resource"))
121 .note("Fluent does not interpret these escape sequences (<https://projectfluent.org/fluent/guide/special.html>)")
122 .emit();
123 }
124 }
125 if bad {
126 return failed(&crate_name);
127 }
128
129 let resource = match FluentResource::try_new(resource_contents) {
130 Ok(resource) => resource,
131 Err((this, errs)) => {
132 Diagnostic::spanned(resource_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 err.replace_range(0..1, &err.chars().next().unwrap().to_lowercase().to_string());
140
141 let message = annotate_snippets::Level::Error.title(&err).snippet(
142 Snippet::source(this.source())
143 .origin(&relative_ftl_path)
144 .fold(true)
145 .annotation(annotate_snippets::Level::Error.span(pos.start..pos.end - 1)),
146 );
147 let renderer = Renderer::plain();
148 eprintln!("{}\n", renderer.render(message));
149 }
150
151 return failed(&crate_name);
152 }
153 };
154
155 let mut constants = TokenStream::new();
156 let mut previous_defns = HashMap::new();
157 let mut message_refs = Vec::new();
158 for entry in resource.entries() {
159 if let Entry::Message(msg) = entry {
160 let Message { id: Identifier { name }, attributes, value, .. } = msg;
161 let _ = previous_defns.entry(name.to_string()).or_insert(resource_span);
162 if name.contains('-') {
163 Diagnostic::spanned(
164 resource_span,
165 Level::Error,
166 format!("name `{name}` contains a '-' character"),
167 )
168 .help("replace any '-'s with '_'s")
169 .emit();
170 }
171
172 if let Some(Pattern { elements }) = value {
173 for elt in elements {
174 if let PatternElement::Placeable {
175 expression:
176 Expression::Inline(InlineExpression::MessageReference { id, .. }),
177 } = elt
178 {
179 message_refs.push((id.name, *name));
180 }
181 }
182 }
183
184 let crate_prefix = format!("{crate_name}_");
191
192 let snake_name = name.replace('-', "_");
193 if !snake_name.starts_with(&crate_prefix) {
194 Diagnostic::spanned(
195 resource_span,
196 Level::Error,
197 format!("name `{name}` does not start with the crate name"),
198 )
199 .help(format!(
200 "prepend `{crate_prefix}` to the slug name: `{crate_prefix}{snake_name}`"
201 ))
202 .emit();
203 };
204 let snake_name = Ident::new(&snake_name, resource_str.span());
205
206 if !previous_attrs.insert(snake_name.clone()) {
207 continue;
208 }
209
210 let docstr =
211 format!("Constant referring to Fluent message `{name}` from `{crate_name}`");
212 constants.extend(quote! {
213 #[doc = #docstr]
214 pub const #snake_name: rustc_errors::DiagMessage =
215 rustc_errors::DiagMessage::FluentIdentifier(
216 std::borrow::Cow::Borrowed(#name),
217 None
218 );
219 });
220
221 for Attribute { id: Identifier { name: attr_name }, .. } in attributes {
222 let snake_name = Ident::new(
223 &format!("{crate_prefix}{}", attr_name.replace('-', "_")),
224 resource_str.span(),
225 );
226 if !previous_attrs.insert(snake_name.clone()) {
227 continue;
228 }
229
230 if attr_name.contains('-') {
231 Diagnostic::spanned(
232 resource_span,
233 Level::Error,
234 format!("attribute `{attr_name}` contains a '-' character"),
235 )
236 .help("replace any '-'s with '_'s")
237 .emit();
238 }
239
240 let msg = format!(
241 "Constant referring to Fluent message `{name}.{attr_name}` from `{crate_name}`"
242 );
243 constants.extend(quote! {
244 #[doc = #msg]
245 pub const #snake_name: rustc_errors::SubdiagMessage =
246 rustc_errors::SubdiagMessage::FluentAttr(std::borrow::Cow::Borrowed(#attr_name));
247 });
248 }
249
250 let ident = quote::format_ident!("{snake_name}_refs");
253 let vrefs = variable_references(msg);
254 constants.extend(quote! {
255 #[cfg(test)]
256 pub const #ident: &[&str] = &[#(#vrefs),*];
257 })
258 }
259 }
260
261 for (mref, name) in message_refs.into_iter() {
262 if !previous_defns.contains_key(mref) {
263 Diagnostic::spanned(
264 resource_span,
265 Level::Error,
266 format!("referenced message `{mref}` does not exist (in message `{name}`)"),
267 )
268 .help(&format!("you may have meant to use a variable reference (`{{${mref}}}`)"))
269 .emit();
270 }
271 }
272
273 if let Err(errs) = bundle.add_resource(resource) {
274 for e in errs {
275 match e {
276 FluentError::Overriding { kind, id } => {
277 Diagnostic::spanned(
278 resource_span,
279 Level::Error,
280 format!("overrides existing {kind}: `{id}`"),
281 )
282 .emit();
283 }
284 FluentError::ResolverError(_) | FluentError::ParserError(_) => unreachable!(),
285 }
286 }
287 }
288
289 finish(constants, quote! { include_str!(#relative_ftl_path) })
290}
291
292fn variable_references<'a>(msg: &Message<&'a str>) -> Vec<&'a str> {
293 let mut refs = vec![];
294 if let Some(Pattern { elements }) = &msg.value {
295 for elt in elements {
296 if let PatternElement::Placeable {
297 expression: Expression::Inline(InlineExpression::VariableReference { id }),
298 } = elt
299 {
300 refs.push(id.name);
301 }
302 }
303 }
304 for attr in &msg.attributes {
305 for elt in &attr.value.elements {
306 if let PatternElement::Placeable {
307 expression: Expression::Inline(InlineExpression::VariableReference { id }),
308 } = elt
309 {
310 refs.push(id.name);
311 }
312 }
313 }
314 refs
315}