1 use annotate_snippets::{
2 display_list::DisplayList,
3 snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation},
5 use fluent_bundle::{FluentBundle, FluentError, FluentResource};
8 Attribute, Entry, Expression, Identifier, InlineExpression, Message, Pattern,
13 use proc_macro::{Diagnostic, Level, Span};
14 use proc_macro2::TokenStream;
17 collections::{HashMap, HashSet},
20 path::{Path, PathBuf},
23 parse::{Parse, ParseStream},
25 punctuated::Punctuated,
26 token, Ident, LitStr, Result,
28 use unic_langid::langid;
33 fat_arrow_token: token::FatArrow,
34 resource_path: LitStr,
37 impl Parse for Resource {
38 fn parse(input: ParseStream<'_>) -> Result<Self> {
40 krate: input.parse()?,
41 fat_arrow_token: input.parse()?,
42 resource_path: input.parse()?,
47 struct Resources(Punctuated<Resource, token::Comma>);
49 impl Parse for Resources {
50 fn parse(input: ParseStream<'_>) -> Result<Self> {
51 let mut resources = Punctuated::new();
53 if input.is_empty() || input.peek(token::Brace) {
56 let value = input.parse()?;
57 resources.push_value(value);
58 if !input.peek(token::Comma) {
61 let punct = input.parse()?;
62 resources.push_punct(punct);
64 Ok(Resources(resources))
68 /// Helper function for returning an absolute path for macro-invocation relative file paths.
70 /// If the input is already absolute, then the input is returned. If the input is not absolute,
71 /// then it is appended to the directory containing the source file with this macro invocation.
72 fn invocation_relative_path_to_absolute(span: Span, path: &str) -> PathBuf {
73 let path = Path::new(path);
74 if path.is_absolute() {
77 // `/a/b/c/foo/bar.rs` contains the current macro invocation
78 let mut source_file_path = span.source_file().path();
80 source_file_path.pop();
81 // `/a/b/c/foo/../locales/en-US/example.ftl`
82 source_file_path.push(path);
87 /// See [rustc_macros::fluent_messages].
88 pub(crate) fn fluent_messages(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
89 let resources = parse_macro_input!(input as Resources);
91 // Cannot iterate over individual messages in a bundle, so do that using the
92 // `FluentResource` instead. Construct a bundle anyway to find out if there are conflicting
93 // messages in the resources.
94 let mut bundle = FluentBundle::new(vec![langid!("en-US")]);
96 // Map of Fluent identifiers to the `Span` of the resource that defined them, used for better
98 let mut previous_defns = HashMap::new();
100 // Set of Fluent attribute names already output, to avoid duplicate type errors - any given
101 // constant created for a given attribute is the same.
102 let mut previous_attrs = HashSet::new();
104 let mut includes = TokenStream::new();
105 let mut generated = TokenStream::new();
107 for res in resources.0 {
108 let krate_span = res.krate.span().unwrap();
109 let path_span = res.resource_path.span().unwrap();
111 let relative_ftl_path = res.resource_path.value();
112 let absolute_ftl_path =
113 invocation_relative_path_to_absolute(krate_span, &relative_ftl_path);
114 // As this macro also outputs an `include_str!` for this file, the macro will always be
115 // re-executed when the file changes.
116 let mut resource_file = match File::open(absolute_ftl_path) {
117 Ok(resource_file) => resource_file,
119 Diagnostic::spanned(path_span, Level::Error, "could not open Fluent resource")
125 let mut resource_contents = String::new();
126 if let Err(e) = resource_file.read_to_string(&mut resource_contents) {
127 Diagnostic::spanned(path_span, Level::Error, "could not read Fluent resource")
132 let resource = match FluentResource::try_new(resource_contents) {
133 Ok(resource) => resource,
134 Err((this, errs)) => {
135 Diagnostic::spanned(path_span, Level::Error, "could not parse Fluent resource")
136 .help("see additional errors emitted")
138 for ParserError { pos, slice: _, kind } in errs {
139 let mut err = kind.to_string();
140 // Entirely unnecessary string modification so that the error message starts
141 // with a lowercase as rustc errors do.
144 &err.chars().next().unwrap().to_lowercase().to_string(),
147 let line_starts: Vec<usize> = std::iter::once(0)
151 .filter_map(|(i, c)| Some(i + 1).filter(|_| c == '\n')),
154 let line_start = line_starts
157 .map(|(line, idx)| (line + 1, idx))
158 .filter(|(_, idx)| **idx <= pos.start)
163 let snippet = Snippet {
164 title: Some(Annotation {
167 annotation_type: AnnotationType::Error,
171 source: this.source(),
173 origin: Some(&relative_ftl_path),
175 annotations: vec![SourceAnnotation {
177 annotation_type: AnnotationType::Error,
178 range: (pos.start, pos.end - 1),
181 opt: Default::default(),
183 let dl = DisplayList::from(snippet);
190 let mut constants = TokenStream::new();
191 let mut messagerefs = Vec::new();
192 for entry in resource.entries() {
193 let span = res.krate.span();
194 if let Entry::Message(Message { id: Identifier { name }, attributes, value, .. }) =
197 let _ = previous_defns.entry(name.to_string()).or_insert(path_span);
199 if name.contains('-') {
203 format!("name `{name}` contains a '-' character"),
205 .help("replace any '-'s with '_'s")
209 if let Some(Pattern { elements }) = value {
210 for elt in elements {
211 if let PatternElement::Placeable {
213 Expression::Inline(InlineExpression::MessageReference { id, .. }),
216 messagerefs.push((id.name, *name));
221 // Require that the message name starts with the crate name
222 // `hir_typeck_foo_bar` (in `hir_typeck.ftl`)
223 // `const_eval_baz` (in `const_eval.ftl`)
224 // `const-eval-hyphen-having` => `hyphen_having` (in `const_eval.ftl`)
225 // The last case we error about above, but we want to fall back gracefully
226 // so that only the error is being emitted and not also one about the macro
228 let crate_prefix = format!("{}_", res.krate);
230 let snake_name = name.replace('-', "_");
231 if !snake_name.starts_with(&crate_prefix) {
235 format!("name `{name}` does not start with the crate name"),
238 "prepend `{crate_prefix}` to the slug name: `{crate_prefix}{snake_name}`"
243 let snake_name = Ident::new(&snake_name, span);
245 constants.extend(quote! {
246 pub const #snake_name: crate::DiagnosticMessage =
247 crate::DiagnosticMessage::FluentIdentifier(
248 std::borrow::Cow::Borrowed(#name),
253 for Attribute { id: Identifier { name: attr_name }, .. } in attributes {
254 let snake_name = Ident::new(&attr_name.replace('-', "_"), span);
255 if !previous_attrs.insert(snake_name.clone()) {
259 if attr_name.contains('-') {
263 format!("attribute `{attr_name}` contains a '-' character"),
265 .help("replace any '-'s with '_'s")
269 constants.extend(quote! {
270 pub const #snake_name: crate::SubdiagnosticMessage =
271 crate::SubdiagnosticMessage::FluentAttr(
272 std::borrow::Cow::Borrowed(#attr_name)
279 for (mref, name) in messagerefs.into_iter() {
280 if !previous_defns.contains_key(mref) {
284 format!("referenced message `{mref}` does not exist (in message `{name}`)"),
286 .help(&format!("you may have meant to use a variable reference (`{{${mref}}}`)"))
291 if let Err(errs) = bundle.add_resource(resource) {
294 FluentError::Overriding { kind, id } => {
298 format!("overrides existing {kind}: `{id}`"),
300 .span_help(previous_defns[&id], "previously defined in this resource")
303 FluentError::ResolverError(_) | FluentError::ParserError(_) => unreachable!(),
308 includes.extend(quote! { include_str!(#relative_ftl_path), });
310 generated.extend(constants);
314 #[allow(non_upper_case_globals)]
316 pub mod fluent_generated {
317 pub static DEFAULT_LOCALE_RESOURCES: &'static [&'static str] = &[
324 pub const help: crate::SubdiagnosticMessage =
325 crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("help"));
326 pub const note: crate::SubdiagnosticMessage =
327 crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("note"));
328 pub const warn: crate::SubdiagnosticMessage =
329 crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("warn"));
330 pub const label: crate::SubdiagnosticMessage =
331 crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("label"));
332 pub const suggestion: crate::SubdiagnosticMessage =
333 crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("suggestion"));