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