Move macro-specific code into `macro_impl` module

finchie
Jul 31, 2024, 10:40 AM
CESJ4CTO26X4GBZBPXRXLOJT3JQJOGFN5EJSNAAZELNQRZF7QSYAC

Dependencies

  • [2] 7U2DXFMP Refactor `fluent_embed::Localize` to support overriding locales
  • [3] AAERM7PB Add selector tests for the `fr` locale
  • [4] OWXLFLRM Merge `cli_macros` shim into `fluent_embed`
  • [5] UOMQT7LT Add support for cardinal CLDR plural selectors
  • [6] 5TEX4MNU Split `fluent_embed` into `group` and `parse` modules
  • [7] 4XADHKM6 Convert fluent variable references to snake case
  • [8] XEEXWJLG Add simple end-to-end test for selectors
  • [9] BQ6N55O7 Refactor how `Group` stores messages
  • [10] V5S5K33A Add basic error handling for invalid paths in proc_macro attribute
  • [11] RLX6XPNZ Return an error when user provides an exact path
  • [12] KZLFC7OW Rename `fluent_embed_runtime` to `fluent_embed`
  • [13] NO3PDO7P Refactor `fluent_embed` to support structs
  • [14] 6ABVDTXZ Improve `fluent_embed_derive` test suite
  • [15] O77KA6C4 Create `fluent_embed` crate
  • [16] 2XQ6ZB4W Store multiple locales in a single `Group`
  • [17] 5FIVUZYF Unify `fluent_embed` macro API as `localize()`
  • [18] ROSR4HD5 Parse captured glob as locale
  • [19] F5LG7WEN Emit compilation errors from Fluent source code
  • [20] QSK7JRBA Add simple `attribute_path` function
  • [21] BANMRGRO Switch `wax` to temporary fork
  • [22] QFPQZR4K Refactor `fluent_embed`
  • [23] 2SITVDYW Handle common errors in Fluent code
  • [24] HHJDRLLN Create `fluent_embed_runtime` crate
  • [25] XGNME3WR Move `Group::derive_enum` to new `crate::parse_macro` module
  • [26] D652S2N3 Rename `parse` module to `parse_fluent`

Change contents

  • replacement in fluent_embed_derive/tests/selectors.rs at line 6
    [4.34][3.76:107]()
    use rstest::{fixture, rstest};
    [4.34]
    [4.340]
    use rstest::rstest;
  • edit in fluent_embed_derive/tests/selectors.rs at line 42
    [4.366][3.1191:1249]()
    let expected_message = data.expected_message(locale);
  • file addition: macro_impl (d--r------)
    [4.41]
  • file addition: mod.rs (----------)
    [0.43]
    use crate::fluent;
    use icu_locid::locale;
    use miette::Diagnostic;
    use quote::quote;
    use thiserror::Error;
    mod attribute;
    pub mod derive;
    pub mod error;
    #[derive(Diagnostic, Debug, Error)]
    #[diagnostic(transparent)]
    #[error(transparent)]
    pub enum MacroError {
    Attribute(#[from] attribute::Error),
    #[error("invalid Fluent source code")]
    Fluent(#[from] fluent::Error),
    }
    pub fn localize(
    attribute: &syn::LitStr,
    derive_input: &syn::DeriveInput,
    ) -> Result<proc_macro2::TokenStream, MacroError> {
    let locales = attribute::locales(attribute)?;
    // TODO: user-controlled canonical locale
    let group = fluent::Group::new(locale!("en-US"), locales)?;
    let canonical_locale = group.canonical_locale().id.clone().to_string();
    let locales = match &derive_input.data {
    syn::Data::Struct(_struct_data) => derive::locales_for_ident(&group, &derive_input.ident),
    syn::Data::Enum(enum_data) => derive::locales_for_enum(&group, &enum_data.variants),
    syn::Data::Union(_) => todo!(),
    };
    let message_body = match &derive_input.data {
    syn::Data::Struct(struct_data) => {
    derive::message_for_struct(group, &derive_input.ident, &struct_data.fields)
    }
    syn::Data::Enum(enum_data) => derive::messages_for_enum(group, &enum_data.variants),
    syn::Data::Union(_) => todo!(),
    }?;
    let ident = &derive_input.ident;
    Ok(quote! {
    impl ::fluent_embed::Localize for #ident {
    const CANONICAL_LOCALE: ::fluent_embed::icu_locid::LanguageIdentifier =
    ::fluent_embed::icu_locid::langid!(#canonical_locale);
    fn message_for_locale(&self, locale: &::fluent_embed::icu_locid::LanguageIdentifier) -> String {
    #message_body
    }
    fn localize(&self) -> String {
    let available_locales = #locales;
    let selected_locale = ::fluent_embed::locale_select::match_locales(&available_locales, &Self::CANONICAL_LOCALE);
    self.message_for_locale(&selected_locale)
    }
    }
    })
    }
  • file move: error.rs (----------)error.rs (----------)
    [0.43]
    [4.985]
  • replacement in fluent_embed_derive/src/macro_impl/error.rs at line 1
    [4.985][4.986:1027]()
    use super::{AttributeError, MacroError};
    [4.985]
    [4.1027]
    use super::{attribute, MacroError};
  • replacement in fluent_embed_derive/src/macro_impl/error.rs at line 3
    [4.1028][4.1028:1123]()
    use proc_macro_error::{abort, emit_call_site_error, emit_error};
    use quote::{quote, ToTokens};
    [4.1028]
    [4.1123]
    use proc_macro2::TokenStream;
    use proc_macro_error::{emit_call_site_error, emit_error};
    use syn::spanned::Spanned;
  • replacement in fluent_embed_derive/src/macro_impl/error.rs at line 7
    [4.1124][4.1124:1194]()
    fn attribute(error: AttributeError, derive_attribute: &syn::LitStr) {
    [4.1124]
    [4.1194]
    fn attribute(error: attribute::Error, attribute_stream: TokenStream) {
  • replacement in fluent_embed_derive/src/macro_impl/error.rs at line 9
    [4.1212][4.1212:2324]()
    AttributeError::Build(build_error) => {
    for location in build_error.locations() {
    // Create a token stream from the attribute's string literal
    let [proc_macro2::TokenTree::Literal(ref string_literal)] = derive_attribute
    .to_token_stream()
    .into_iter()
    .collect::<Vec<_>>()[..]
    else {
    abort!(derive_attribute, "unexpected macro attribute");
    };
    let (span_start, span_length) = location.span();
    let error_source = string_literal
    // Offset by 1 to skip the starting `"` double-quote character
    .subspan(span_start + 1..=span_start + span_length)
    // Fall back to the whole attribute if `subspan()` returns `None`
    // This will always happend on stable as subspan is nightly-only:
    // https://docs.rs/proc-macro2/latest/proc_macro2/struct.Literal.html#method.subspan
    .unwrap_or(derive_attribute.span());
    [4.1212]
    [4.2324]
    attribute::Error::Build(build_error) => {
    for located_error in build_error.locations() {
    let error_source = attribute_stream.span();
  • replacement in fluent_embed_derive/src/macro_impl/error.rs at line 14
    [4.2385][4.2385:2434]()
    note = location.to_string();
    [4.2385]
    [4.2434]
    note = located_error.to_string();
  • replacement in fluent_embed_derive/src/macro_impl/error.rs at line 18
    [4.2477][4.2477:2523]()
    AttributeError::Walk(walk_error) => {
    [4.2477]
    [4.2523]
    attribute::Error::Walk(walk_error) => {
  • replacement in fluent_embed_derive/src/macro_impl/error.rs at line 33
    [4.3134][4.3134:3238]()
    emit_error! { derive_attribute, "error at depth {} while walking path", walk_error.depth();
    [4.3134]
    [4.3238]
    emit_error! { attribute_stream, "error at depth {} while walking path", walk_error.depth();
  • replacement in fluent_embed_derive/src/macro_impl/error.rs at line 37
    [4.3292][4.3292:3328]()
    AttributeError::NoMatches {
    [4.3292]
    [4.3328]
    attribute::Error::NoMatches {
  • replacement in fluent_embed_derive/src/macro_impl/error.rs at line 45
    [4.3570][4.3570:3652]()
    emit_error! { derive_attribute, "cannot match against an exact path";
    [4.3570]
    [4.3652]
    emit_error! { attribute_stream, "cannot match against an exact path";
  • replacement in fluent_embed_derive/src/macro_impl/error.rs at line 55
    [4.3961][4.3961:4002]()
    fn fluent(error: super::fluent::Error) {
    [4.3961]
    [4.4002]
    fn fluent(error: crate::fluent::Error) {
  • replacement in fluent_embed_derive/src/macro_impl/error.rs at line 74
    [4.4667][4.4667:4797]()
    pub fn handle(
    error: MacroError,
    derive_attribute: &syn::LitStr,
    ident: &syn::Ident,
    ) -> proc_macro2::TokenStream {
    [4.4667]
    [4.4797]
    pub fn emit(error: MacroError, attribute_stream: TokenStream) {
  • replacement in fluent_embed_derive/src/macro_impl/error.rs at line 76
    [4.4815][4.4815:4891]()
    MacroError::Attribute(error) => attribute(error, derive_attribute),
    [4.4815]
    [4.4891]
    MacroError::Attribute(error) => attribute(error, attribute_stream),
  • edit in fluent_embed_derive/src/macro_impl/error.rs at line 78
    [4.4943][4.4943:5206]()
    };
    // Generate a minimal `localize()` implementation so the error is self-contained
    quote! {
    impl #ident {
    fn localize(&self) -> String {
    unimplemented!("Encountered error in derive macro")
    }
    }
  • file move: derive.rs (----------)derive.rs (----------)
    [0.43]
    [4.39]
  • replacement in fluent_embed_derive/src/macro_impl/derive.rs at line 16
    [4.9306][4.9306:9333]()
    pub struct DeriveContext {
    [4.9306]
    [4.9333]
    pub struct Context {
  • replacement in fluent_embed_derive/src/macro_impl/derive.rs at line 24
    [4.178][4.9451:9487]()
    reference_kind: &DeriveContext,
    [4.178]
    [4.9487]
    reference_kind: &Context,
  • replacement in fluent_embed_derive/src/macro_impl/derive.rs at line 87
    [4.1236][4.9763:9823]()
    syn::Fields::Named(named_fields) => DeriveContext {
    [4.1236]
    [4.1293]
    syn::Fields::Named(named_fields) => Context {
  • replacement in fluent_embed_derive/src/macro_impl/derive.rs at line 151
    [4.1786][4.10104:10168]()
    syn::Fields::Named(named_fields) => DeriveContext {
    [4.1786]
    [4.10168]
    syn::Fields::Named(named_fields) => Context {
  • file addition: attribute.rs (----------)
    [0.43]
    use std::collections::HashMap;
    use std::path::PathBuf;
    use icu_locid::Locale;
    use miette::Diagnostic;
    use thiserror::Error;
    use wax::walk::Entry;
    #[derive(Diagnostic, Debug, Error)]
    #[error(transparent)]
    pub enum Error {
    Build(#[from] wax::BuildError),
    Walk(#[from] wax::walk::WalkError),
    #[error("Directory could not be matched by glob in macro attribute")]
    NoMatches {
    path: PathBuf,
    complete_match: String,
    },
    }
    pub fn locales(attribute: &syn::LitStr) -> Result<HashMap<Locale, PathBuf>, Error> {
    // Read the fluent file at the given path
    let manifest_root = std::env::var("CARGO_MANIFEST_DIR").unwrap();
    let attribute_glob = attribute.value();
    let mut locales = HashMap::new();
    let glob = wax::Glob::new(&attribute_glob)?;
    for potential_entry in glob.walk(&manifest_root) {
    // TODO: this assumes that the locale is the first capture
    let entry = potential_entry?;
    let captured_locale = if let Some(captured) = entry.matched().get(1) {
    captured
    } else {
    return Err(Error::NoMatches {
    path: entry.path().to_path_buf(),
    complete_match: entry.matched().complete().to_string(),
    });
    };
    // Captured directories may have a suffix of `/`
    let stripped_locale = captured_locale.strip_suffix('/').unwrap_or(captured_locale);
    // TODO: return an error instead of panic
    let locale = Locale::try_from_bytes(stripped_locale.as_bytes()).unwrap();
    // Insert this locale (and make sure it's unique!)
    let previous_value = locales.insert(locale, entry.into_path());
    assert!(previous_value.is_none());
    }
    Ok(locales)
    }
  • edit in fluent_embed_derive/src/lib.rs at line 1
    [4.4253][4.4253:4284](),[4.4284][4.0:24](),[4.24][4.4284:4318](),[4.4284][4.4284:4318](),[4.4318][4.6612:6636]()
    use std::collections::HashMap;
    use std::path::PathBuf;
    use icu_locid::{locale, Locale};
    use miette::Diagnostic;
  • edit in fluent_embed_derive/src/lib.rs at line 5
    [4.144][4.6637:6659](),[4.4388][4.6637:6659](),[4.6659][4.0:22]()
    use thiserror::Error;
    use wax::walk::Entry;
  • edit in fluent_embed_derive/src/lib.rs at line 6
    [4.4389][4.0:12](),[4.12][4.145:156]()
    mod derive;
    mod error;
  • replacement in fluent_embed_derive/src/lib.rs at line 7
    [4.63][4.6718:6777](),[4.6718][4.6718:6777](),[4.6777][4.157:179](),[4.179][4.107:143](),[4.107][4.107:143](),[4.143][4.23:63](),[4.63][4.6778:6852](),[4.197][4.6778:6852](),[4.6852][4.216:294](),[4.216][4.216:294](),[4.294][4.6853:6941](),[4.6941][4.180:198](),[4.198][4.6963:7002](),[4.6963][4.6963:7002](),[4.7002][4.0:43](),[4.43][4.199:234](),[4.234][4.294:296](),[4.7079][4.294:296](),[4.294][4.294:296](),[4.296][4.1386:1387](),[4.912][4.1386:1387](),[4.1625][4.1386:1387](),[4.3486][4.1386:1387](),[4.1386][4.1386:1387](),[4.1387][4.235:262](),[4.262][4.95:180](),[4.95][4.95:180](),[4.87][4.4453:4569](),[4.180][4.4453:4569](),[4.384][4.4453:4569](),[4.4453][4.4453:4569](),[4.4569][4.181:225](),[4.225][4.4616:4617](),[4.4616][4.4616:4617](),[4.4617][4.226:264](),[4.264][4.4657:4658](),[4.7116][4.4657:4658](),[4.4657][4.4657:4658](),[4.4658][4.88:137](),[4.137][4.4715:4837](),[4.4715][4.4715:4837](),[4.4837][4.138:176](),[4.176][4.7117:7118](),[4.7118][4.385:702](),[4.176][4.385:702](),[4.702][4.4946:5096](),[4.4946][4.4946:5096](),[4.5096][4.44:94](),[4.94][4.5096:5179](),[4.5096][4.5096:5179](),[4.5179][4.265:439](),[4.439][4.5421:5428](),[4.7240][4.5421:5428](),[4.5421][4.5421:5428](),[4.5428][4.440:456](),[4.225][4.5472:5475](),[4.456][4.5472:5475](),[4.7296][4.5472:5475](),[4.5472][4.5472:5475](),[4.5475][4.263:281](),[4.281][4.474:540](),[4.474][4.474:540](),[4.540][4.282:388](),[4.388][4.664:774](),[4.664][4.664:774](),[4.774][4.1308:1384](),[4.1384][2.0:285]()
    #[derive(Diagnostic, Debug, Error)]
    #[error(transparent)]
    enum AttributeError {
    Build(#[from] wax::BuildError),
    Walk(#[from] wax::walk::WalkError),
    #[error("Directory could not be matched by glob in macro attribute")]
    NoMatches {
    path: PathBuf,
    complete_match: String,
    },
    }
    #[derive(Diagnostic, Debug, Error)]
    #[diagnostic(transparent)]
    #[error(transparent)]
    enum MacroError {
    Attribute(#[from] AttributeError),
    #[error("invalid Fluent source code")]
    Fluent(#[from] fluent::Error),
    }
    fn locales_from_attribute(
    attribute: &syn::LitStr,
    ) -> Result<HashMap<Locale, PathBuf>, AttributeError> {
    // Read the fluent file at the given path
    let manifest_root = std::env::var("CARGO_MANIFEST_DIR").unwrap();
    let attribute_glob = attribute.value();
    let mut locales = HashMap::new();
    let glob = wax::Glob::new(&attribute_glob)?;
    for potential_entry in glob.walk(&manifest_root) {
    // TODO: this assumes that the locale is the first capture
    let entry = potential_entry?;
    let captured_locale = if let Some(captured) = entry.matched().get(1) {
    captured
    } else {
    return Err(AttributeError::NoMatches {
    path: entry.path().to_path_buf(),
    complete_match: entry.matched().complete().to_string(),
    });
    };
    // Captured directories may have a suffix of `/`
    let stripped_locale = captured_locale.strip_suffix('/').unwrap_or(captured_locale);
    // TODO: return an error instead of panic
    let locale = Locale::try_from_bytes(stripped_locale.as_bytes()).unwrap();
    // Insert this locale (and make sure it's unique!)
    let previous_value = locales.insert(locale, entry.into_path());
    assert!(previous_value.is_none());
    }
    Ok(locales)
    }
    fn localize_impl(
    attribute: &syn::LitStr,
    derive_input: &syn::DeriveInput,
    ) -> Result<proc_macro2::TokenStream, MacroError> {
    let locales = locales_from_attribute(attribute)?;
    // TODO: user-controlled canonical locale
    let group = fluent::Group::new(locale!("en-US"), locales)?;
    let canonical_locale = group.canonical_locale().id.clone().to_string();
    let locales = match &derive_input.data {
    syn::Data::Struct(_struct_data) => derive::locales_for_ident(&group, &derive_input.ident),
    syn::Data::Enum(enum_data) => derive::locales_for_enum(&group, &enum_data.variants),
    syn::Data::Union(_) => todo!(),
    };
    [4.24]
    [4.5596]
    mod macro_impl;
  • edit in fluent_embed_derive/src/lib.rs at line 9
    [4.5597][2.286:336](),[2.336][4.7399:7443](),[4.5639][4.7399:7443](),[4.7443][2.337:425](),[2.425][4.7531:7541](),[4.855][4.7531:7541](),[4.7531][4.7531:7541](),[4.7541][2.426:519](),[2.519][4.5830:5870](),[4.940][4.5830:5870](),[4.5830][4.5830:5870](),[4.5870][4.7542:7550](),[4.7550][4.5877:5915](),[4.5877][4.5877:5915](),[4.5915][4.1385:1386](),[4.1386][4.384:400](),[4.5915][4.384:400](),[4.400][4.62:268](),[4.268][2.520:675](),[4.268][4.6021:6064](),[2.675][4.6021:6064](),[4.1617][4.6021:6064](),[4.6021][4.6021:6064](),[4.6064][2.676:914](),[2.914][4.6086:6110](),[4.6086][4.6086:6110](),[4.6110][4.401:408](),[4.408][4.6116:6118](),[4.6116][4.6116:6118](),[4.6118][4.389:390]()
    let message_body = match &derive_input.data {
    syn::Data::Struct(struct_data) => {
    derive::message_for_struct(group, &derive_input.ident, &struct_data.fields)
    }
    syn::Data::Enum(enum_data) => derive::messages_for_enum(group, &enum_data.variants),
    syn::Data::Union(_) => todo!(),
    }?;
    let ident = &derive_input.ident;
    Ok(quote! {
    impl ::fluent_embed::Localize for #ident {
    const CANONICAL_LOCALE: ::fluent_embed::icu_locid::LanguageIdentifier =
    ::fluent_embed::icu_locid::langid!(#canonical_locale);
    fn message_for_locale(&self, locale: &::fluent_embed::icu_locid::LanguageIdentifier) -> String {
    #message_body
    }
    fn localize(&self) -> String {
    let available_locales = #locales;
    let selected_locale = ::fluent_embed::locale_select::match_locales(&available_locales, &Self::CANONICAL_LOCALE);
    self.message_for_locale(&selected_locale)
    }
    }
    })
    }
  • edit in fluent_embed_derive/src/lib.rs at line 13
    [4.580]
    [4.580]
    let attribute_stream = proc_macro2::TokenStream::from(attribute.clone());
  • replacement in fluent_embed_derive/src/lib.rs at line 17
    [4.701][4.701:869]()
    let implementation = localize_impl(&derive_attribute, &derive_input)
    .unwrap_or_else(|error| error::handle(error, &derive_attribute, &derive_input.ident));
    [4.701]
    [4.869]
    match macro_impl::localize(&derive_attribute, &derive_input) {
    Ok(implementation) => {
    // No errors found, emit the generated `fluent_embed::Localize` implementation
    quote! {
    #original_item
  • replacement in fluent_embed_derive/src/lib.rs at line 23
    [4.870][4.870:906]()
    quote! {
    #original_item
    [4.870]
    [4.906]
    #implementation
    }
    }
    Err(error) => {
    // Emit the relevant error message
    macro_impl::error::emit(error, attribute_stream);
  • replacement in fluent_embed_derive/src/lib.rs at line 30
    [4.907][4.907:931]()
    #implementation
    [4.907]
    [4.931]
    // Generate a minimal `localize()` implementation so the error is self-contained
    let ident = derive_input.ident;
    quote! {
    impl #ident {
    fn localize(&self) -> String {
    unimplemented!("Encountered error in derive macro")
    }
    }
    }
    }
  • replacement in fluent_embed_derive/src/fluent/mod.rs at line 1
    [4.992][4.993:1027]()
    use crate::derive::DeriveContext;
    [4.992]
    [4.1027]
    use crate::macro_impl::derive;
  • replacement in fluent_embed_derive/src/fluent/mod.rs at line 66
    [4.1571][4.1571:1616]()
    derive_context: &'context DeriveContext,
    [4.1546]
    [4.1616]
    derive_context: &'context derive::Context,
  • replacement in fluent_embed_derive/src/fluent/mod.rs at line 179
    [4.3326][4.3326:3366]()
    derive_context: &DeriveContext,
    [4.3326]
    [4.3366]
    derive_context: &derive::Context,
  • replacement in fluent_embed_derive/src/fluent/group.rs at line 2
    [4.4042][4.4042:4076]()
    use crate::derive::DeriveContext;
    [4.4042]
    [4.3940]
    use crate::macro_impl::derive;
  • replacement in fluent_embed_derive/src/fluent/group.rs at line 80
    [4.7895][4.5281:5321]()
    derive_context: &DeriveContext,
    [4.7895]
    [4.5321]
    derive_context: &derive::Context,
  • replacement in fluent_embed_derive/src/fluent/group.rs at line 91
    [4.6789][4.5607:5647]()
    derive_context: &DeriveContext,
    [4.6789]
    [4.5647]
    derive_context: &derive::Context,
  • replacement in fluent_embed_derive/src/fluent/ast.rs at line 2
    [4.6447][4.6447:6481]()
    use crate::derive::ReferenceKind;
    [4.6447]
    [4.2216]
    use crate::macro_impl::derive::ReferenceKind;