1 use annotate_snippets::{
2 display_list::DisplayList,
3 snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation},
5 use fluent_bundle::{FluentBundle, FluentError, FluentResource};
7 ast::{Attribute, Entry, Identifier, Message},
10 use proc_macro::{Diagnostic, Level, Span};
11 use proc_macro2::TokenStream;
17 path::{Path, PathBuf},
20 parse::{Parse, ParseStream},
22 punctuated::Punctuated,
23 token, Ident, LitStr, Result,
25 use unic_langid::langid;
30 fat_arrow_token: token::FatArrow,
34 impl Parse for Resource {
35 fn parse(input: ParseStream<'_>) -> Result<Self> {
37 ident: input.parse()?,
38 fat_arrow_token: input.parse()?,
39 resource: input.parse()?,
44 struct Resources(Punctuated<Resource, token::Comma>);
46 impl Parse for Resources {
47 fn parse(input: ParseStream<'_>) -> Result<Self> {
48 let mut resources = Punctuated::new();
50 if input.is_empty() || input.peek(token::Brace) {
53 let value = input.parse()?;
54 resources.push_value(value);
55 if !input.peek(token::Comma) {
58 let punct = input.parse()?;
59 resources.push_punct(punct);
61 Ok(Resources(resources))
65 /// Helper function for returning an absolute path for macro-invocation relative file paths.
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.
69 fn invocation_relative_path_to_absolute(span: Span, path: &str) -> PathBuf {
70 let path = Path::new(path);
71 if path.is_absolute() {
74 // `/a/b/c/foo/bar.rs` contains the current macro invocation
75 let mut source_file_path = span.source_file().path();
77 source_file_path.pop();
78 // `/a/b/c/foo/../locales/en-US/example.ftl`
79 source_file_path.push(path);
84 /// See [rustc_macros::fluent_messages].
85 pub(crate) fn fluent_messages(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
86 let resources = parse_macro_input!(input as Resources);
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")]);
93 // Map of Fluent identifiers to the `Span` of the resource that defined them, used for better
95 let mut previous_defns = HashMap::new();
97 let mut includes = TokenStream::new();
98 let mut generated = TokenStream::new();
99 for res in resources.0 {
100 let ident_span = res.ident.span().unwrap();
101 let path_span = res.resource.span().unwrap();
103 let relative_ftl_path = res.resource.value();
104 let absolute_ftl_path =
105 invocation_relative_path_to_absolute(ident_span, &relative_ftl_path);
106 // As this macro also outputs an `include_str!` for this file, the macro will always be
107 // re-executed when the file changes.
108 let mut resource_file = match File::open(absolute_ftl_path) {
109 Ok(resource_file) => resource_file,
111 Diagnostic::spanned(path_span, Level::Error, "could not open Fluent resource")
117 let mut resource_contents = String::new();
118 if let Err(e) = resource_file.read_to_string(&mut resource_contents) {
119 Diagnostic::spanned(path_span, Level::Error, "could not read Fluent resource")
124 let resource = match FluentResource::try_new(resource_contents) {
125 Ok(resource) => resource,
126 Err((this, errs)) => {
127 Diagnostic::spanned(path_span, Level::Error, "could not parse Fluent resource")
128 .help("see additional errors emitted")
130 for ParserError { pos, slice: _, kind } in errs {
131 let mut err = kind.to_string();
132 // Entirely unnecessary string modification so that the error message starts
133 // with a lowercase as rustc errors do.
136 &err.chars().next().unwrap().to_lowercase().to_string(),
139 let line_starts: Vec<usize> = std::iter::once(0)
143 .filter_map(|(i, c)| Some(i + 1).filter(|_| c == '\n')),
146 let line_start = line_starts
149 .map(|(line, idx)| (line + 1, idx))
150 .filter(|(_, idx)| **idx <= pos.start)
155 let snippet = Snippet {
156 title: Some(Annotation {
159 annotation_type: AnnotationType::Error,
163 source: this.source(),
165 origin: Some(&relative_ftl_path),
167 annotations: vec![SourceAnnotation {
169 annotation_type: AnnotationType::Error,
170 range: (pos.start, pos.end - 1),
173 opt: Default::default(),
175 let dl = DisplayList::from(snippet);
176 eprintln!("{}\n", dl);
182 let mut constants = TokenStream::new();
183 for entry in resource.entries() {
184 let span = res.ident.span();
185 if let Entry::Message(Message { id: Identifier { name }, attributes, .. }) = entry {
186 let _ = previous_defns.entry(name.to_string()).or_insert(ident_span);
188 // `typeck-foo-bar` => `foo_bar`
189 let snake_name = Ident::new(
190 &name.replace(&format!("{}-", res.ident), "").replace("-", "_"),
193 constants.extend(quote! {
194 pub const #snake_name: crate::DiagnosticMessage =
195 crate::DiagnosticMessage::FluentIdentifier(
196 std::borrow::Cow::Borrowed(#name),
201 for Attribute { id: Identifier { name: attr_name }, .. } in attributes {
202 let attr_snake_name = attr_name.replace("-", "_");
203 let snake_name = Ident::new(&format!("{snake_name}_{attr_snake_name}"), span);
204 constants.extend(quote! {
205 pub const #snake_name: crate::DiagnosticMessage =
206 crate::DiagnosticMessage::FluentIdentifier(
207 std::borrow::Cow::Borrowed(#name),
208 Some(std::borrow::Cow::Borrowed(#attr_name))
215 if let Err(errs) = bundle.add_resource(resource) {
218 FluentError::Overriding { kind, id } => {
222 format!("overrides existing {}: `{}`", kind, id),
224 .span_help(previous_defns[&id], "previously defined in this resource")
227 FluentError::ResolverError(_) | FluentError::ParserError(_) => unreachable!(),
232 includes.extend(quote! { include_str!(#relative_ftl_path), });
234 let ident = res.ident;
235 generated.extend(quote! {
243 #[allow(non_upper_case_globals)]
245 pub mod fluent_generated {
246 pub static DEFAULT_LOCALE_RESOURCES: &'static [&'static str] = &[