| use std::collections::{HashMap, HashSet}; |
| use std::fs::read_to_string; |
| use std::path::{Path, PathBuf}; |
| |
| use annotate_snippets::{Renderer, Snippet}; |
| use fluent_bundle::{FluentBundle, FluentError, FluentResource}; |
| use fluent_syntax::ast::{ |
| Attribute, Entry, Expression, Identifier, InlineExpression, Message, Pattern, PatternElement, |
| }; |
| use fluent_syntax::parser::ParserError; |
| use proc_macro::{Diagnostic, Level, Span}; |
| use proc_macro2::TokenStream; |
| use quote::quote; |
| use syn::{Ident, LitStr, parse_macro_input}; |
| use unic_langid::langid; |
| |
| /// Helper function for returning an absolute path for macro-invocation relative file paths. |
| /// |
| /// If the input is already absolute, then the input is returned. If the input is not absolute, |
| /// then it is appended to the directory containing the source file with this macro invocation. |
| fn invocation_relative_path_to_absolute(span: Span, path: &str) -> PathBuf { |
| let path = Path::new(path); |
| if path.is_absolute() { |
| path.to_path_buf() |
| } else { |
| // `/a/b/c/foo/bar.rs` contains the current macro invocation |
| let mut source_file_path = span.source_file().path(); |
| // `/a/b/c/foo/` |
| source_file_path.pop(); |
| // `/a/b/c/foo/../locales/en-US/example.ftl` |
| source_file_path.push(path); |
| source_file_path |
| } |
| } |
| |
| /// Final tokens. |
| fn finish(body: TokenStream, resource: TokenStream) -> proc_macro::TokenStream { |
| quote! { |
| /// Raw content of Fluent resource for this crate, generated by `fluent_messages` macro, |
| /// imported by `rustc_driver` to include all crates' resources in one bundle. |
| pub static DEFAULT_LOCALE_RESOURCE: &'static str = #resource; |
| |
| #[allow(non_upper_case_globals)] |
| #[doc(hidden)] |
| /// Auto-generated constants for type-checked references to Fluent messages. |
| pub(crate) mod fluent_generated { |
| #body |
| |
| /// Constants expected to exist by the diagnostic derive macros to use as default Fluent |
| /// identifiers for different subdiagnostic kinds. |
| pub mod _subdiag { |
| /// Default for `#[help]` |
| pub const help: rustc_errors::SubdiagMessage = |
| rustc_errors::SubdiagMessage::FluentAttr(std::borrow::Cow::Borrowed("help")); |
| /// Default for `#[note]` |
| pub const note: rustc_errors::SubdiagMessage = |
| rustc_errors::SubdiagMessage::FluentAttr(std::borrow::Cow::Borrowed("note")); |
| /// Default for `#[warn]` |
| pub const warn: rustc_errors::SubdiagMessage = |
| rustc_errors::SubdiagMessage::FluentAttr(std::borrow::Cow::Borrowed("warn")); |
| /// Default for `#[label]` |
| pub const label: rustc_errors::SubdiagMessage = |
| rustc_errors::SubdiagMessage::FluentAttr(std::borrow::Cow::Borrowed("label")); |
| /// Default for `#[suggestion]` |
| pub const suggestion: rustc_errors::SubdiagMessage = |
| rustc_errors::SubdiagMessage::FluentAttr(std::borrow::Cow::Borrowed("suggestion")); |
| } |
| } |
| } |
| .into() |
| } |
| |
| /// Tokens to be returned when the macro cannot proceed. |
| fn failed(crate_name: &Ident) -> proc_macro::TokenStream { |
| finish(quote! { pub mod #crate_name {} }, quote! { "" }) |
| } |
| |
| /// See [rustc_fluent_macro::fluent_messages]. |
| pub(crate) fn fluent_messages(input: proc_macro::TokenStream) -> proc_macro::TokenStream { |
| let crate_name = std::env::var("CARGO_PKG_NAME") |
| // If `CARGO_PKG_NAME` is missing, then we're probably running in a test, so use |
| // `no_crate`. |
| .unwrap_or_else(|_| "no_crate".to_string()) |
| .replace("rustc_", ""); |
| |
| // Cannot iterate over individual messages in a bundle, so do that using the |
| // `FluentResource` instead. Construct a bundle anyway to find out if there are conflicting |
| // messages in the resources. |
| let mut bundle = FluentBundle::new(vec![langid!("en-US")]); |
| |
| // Set of Fluent attribute names already output, to avoid duplicate type errors - any given |
| // constant created for a given attribute is the same. |
| let mut previous_attrs = HashSet::new(); |
| |
| let resource_str = parse_macro_input!(input as LitStr); |
| let resource_span = resource_str.span().unwrap(); |
| let relative_ftl_path = resource_str.value(); |
| let absolute_ftl_path = invocation_relative_path_to_absolute(resource_span, &relative_ftl_path); |
| |
| let crate_name = Ident::new(&crate_name, resource_str.span()); |
| |
| // As this macro also outputs an `include_str!` for this file, the macro will always be |
| // re-executed when the file changes. |
| let resource_contents = match read_to_string(absolute_ftl_path) { |
| Ok(resource_contents) => resource_contents, |
| Err(e) => { |
| Diagnostic::spanned( |
| resource_span, |
| Level::Error, |
| format!("could not open Fluent resource: {e}"), |
| ) |
| .emit(); |
| return failed(&crate_name); |
| } |
| }; |
| let mut bad = false; |
| for esc in ["\\n", "\\\"", "\\'"] { |
| for _ in resource_contents.matches(esc) { |
| bad = true; |
| Diagnostic::spanned(resource_span, Level::Error, format!("invalid escape `{esc}` in Fluent resource")) |
| .note("Fluent does not interpret these escape sequences (<https://projectfluent.org/fluent/guide/special.html>)") |
| .emit(); |
| } |
| } |
| if bad { |
| return failed(&crate_name); |
| } |
| |
| let resource = match FluentResource::try_new(resource_contents) { |
| Ok(resource) => resource, |
| Err((this, errs)) => { |
| Diagnostic::spanned(resource_span, Level::Error, "could not parse Fluent resource") |
| .help("see additional errors emitted") |
| .emit(); |
| for ParserError { pos, slice: _, kind } in errs { |
| let mut err = kind.to_string(); |
| // Entirely unnecessary string modification so that the error message starts |
| // with a lowercase as rustc errors do. |
| err.replace_range(0..1, &err.chars().next().unwrap().to_lowercase().to_string()); |
| |
| let message = annotate_snippets::Level::Error.title(&err).snippet( |
| Snippet::source(this.source()) |
| .origin(&relative_ftl_path) |
| .fold(true) |
| .annotation(annotate_snippets::Level::Error.span(pos.start..pos.end - 1)), |
| ); |
| let renderer = Renderer::plain(); |
| eprintln!("{}\n", renderer.render(message)); |
| } |
| |
| return failed(&crate_name); |
| } |
| }; |
| |
| let mut constants = TokenStream::new(); |
| let mut previous_defns = HashMap::new(); |
| let mut message_refs = Vec::new(); |
| for entry in resource.entries() { |
| if let Entry::Message(msg) = entry { |
| let Message { id: Identifier { name }, attributes, value, .. } = msg; |
| let _ = previous_defns.entry(name.to_string()).or_insert(resource_span); |
| if name.contains('-') { |
| Diagnostic::spanned( |
| resource_span, |
| Level::Error, |
| format!("name `{name}` contains a '-' character"), |
| ) |
| .help("replace any '-'s with '_'s") |
| .emit(); |
| } |
| |
| if let Some(Pattern { elements }) = value { |
| for elt in elements { |
| if let PatternElement::Placeable { |
| expression: |
| Expression::Inline(InlineExpression::MessageReference { id, .. }), |
| } = elt |
| { |
| message_refs.push((id.name, *name)); |
| } |
| } |
| } |
| |
| // `typeck_foo_bar` => `foo_bar` (in `typeck.ftl`) |
| // `const_eval_baz` => `baz` (in `const_eval.ftl`) |
| // `const-eval-hyphen-having` => `hyphen_having` (in `const_eval.ftl`) |
| // The last case we error about above, but we want to fall back gracefully |
| // so that only the error is being emitted and not also one about the macro |
| // failing. |
| let crate_prefix = format!("{crate_name}_"); |
| |
| let snake_name = name.replace('-', "_"); |
| if !snake_name.starts_with(&crate_prefix) { |
| Diagnostic::spanned( |
| resource_span, |
| Level::Error, |
| format!("name `{name}` does not start with the crate name"), |
| ) |
| .help(format!( |
| "prepend `{crate_prefix}` to the slug name: `{crate_prefix}{snake_name}`" |
| )) |
| .emit(); |
| }; |
| let snake_name = Ident::new(&snake_name, resource_str.span()); |
| |
| if !previous_attrs.insert(snake_name.clone()) { |
| continue; |
| } |
| |
| let docstr = |
| format!("Constant referring to Fluent message `{name}` from `{crate_name}`"); |
| constants.extend(quote! { |
| #[doc = #docstr] |
| pub const #snake_name: rustc_errors::DiagMessage = |
| rustc_errors::DiagMessage::FluentIdentifier( |
| std::borrow::Cow::Borrowed(#name), |
| None |
| ); |
| }); |
| |
| for Attribute { id: Identifier { name: attr_name }, .. } in attributes { |
| let snake_name = Ident::new( |
| &format!("{crate_prefix}{}", attr_name.replace('-', "_")), |
| resource_str.span(), |
| ); |
| if !previous_attrs.insert(snake_name.clone()) { |
| continue; |
| } |
| |
| if attr_name.contains('-') { |
| Diagnostic::spanned( |
| resource_span, |
| Level::Error, |
| format!("attribute `{attr_name}` contains a '-' character"), |
| ) |
| .help("replace any '-'s with '_'s") |
| .emit(); |
| } |
| |
| let msg = format!( |
| "Constant referring to Fluent message `{name}.{attr_name}` from `{crate_name}`" |
| ); |
| constants.extend(quote! { |
| #[doc = #msg] |
| pub const #snake_name: rustc_errors::SubdiagMessage = |
| rustc_errors::SubdiagMessage::FluentAttr(std::borrow::Cow::Borrowed(#attr_name)); |
| }); |
| } |
| |
| // Record variables referenced by these messages so we can produce |
| // tests in the derive diagnostics to validate them. |
| let ident = quote::format_ident!("{snake_name}_refs"); |
| let vrefs = variable_references(msg); |
| constants.extend(quote! { |
| #[cfg(test)] |
| pub const #ident: &[&str] = &[#(#vrefs),*]; |
| }) |
| } |
| } |
| |
| for (mref, name) in message_refs.into_iter() { |
| if !previous_defns.contains_key(mref) { |
| Diagnostic::spanned( |
| resource_span, |
| Level::Error, |
| format!("referenced message `{mref}` does not exist (in message `{name}`)"), |
| ) |
| .help(&format!("you may have meant to use a variable reference (`{{${mref}}}`)")) |
| .emit(); |
| } |
| } |
| |
| if let Err(errs) = bundle.add_resource(resource) { |
| for e in errs { |
| match e { |
| FluentError::Overriding { kind, id } => { |
| Diagnostic::spanned( |
| resource_span, |
| Level::Error, |
| format!("overrides existing {kind}: `{id}`"), |
| ) |
| .emit(); |
| } |
| FluentError::ResolverError(_) | FluentError::ParserError(_) => unreachable!(), |
| } |
| } |
| } |
| |
| finish(constants, quote! { include_str!(#relative_ftl_path) }) |
| } |
| |
| fn variable_references<'a>(msg: &Message<&'a str>) -> Vec<&'a str> { |
| let mut refs = vec![]; |
| if let Some(Pattern { elements }) = &msg.value { |
| for elt in elements { |
| if let PatternElement::Placeable { |
| expression: Expression::Inline(InlineExpression::VariableReference { id }), |
| } = elt |
| { |
| refs.push(id.name); |
| } |
| } |
| } |
| for attr in &msg.attributes { |
| for elt in &attr.value.elements { |
| if let PatternElement::Placeable { |
| expression: Expression::Inline(InlineExpression::VariableReference { id }), |
| } = elt |
| { |
| refs.push(id.name); |
| } |
| } |
| } |
| refs |
| } |