Add support for unnamed fields in `l10n_embed_derive`
Dependencies
- [2]
ARB66QTXAdd support for unit structs/variants - [3]
PGBXJWIHMove `l10n_embed` re-exports into `macro_prelude` module - [4]
CESJ4CTOMove macro-specific code into `macro_impl` module - [5]
ROSR4HD5Parse captured glob as locale - [6]
VQBJBFEXImprove error handling for missing Fluent messages - [7]
RUCC2HKZRename from `fluent_embed` to `l10n_embed` - [8]
5FIVUZYFUnify `fluent_embed` macro API as `localize()` - [9]
VNSHGQYNSupport using glob paths in `localize` macro - [10]
XGNME3WRMove `Group::derive_enum` to new `crate::parse_macro` module - [11]
OCR4YRQ2Parse group from fluent file specified by macro attribute - [12]
NEBSVXIAApply Clippy fixes - [13]
QSK7JRBAAdd simple `attribute_path` function - [14]
NO3PDO7PRefactor `fluent_embed` to support structs - [15]
2XQ6ZB4WStore multiple locales in a single `Group` - [16]
OWXLFLRMMerge `cli_macros` shim into `fluent_embed` - [17]
QJC4IQITRefactor `Localize` functions to infallibly return `String` - [18]
QFPQZR4KRefactor `fluent_embed` - [19]
LU6IFZFGRemove `std::io::Write` trait bound from `Localize` - [20]
6XEMHUGSUse full `Locale` instead of `LanguageIdentifier` subset - [21]
XEEXWJLGAdd simple end-to-end test for selectors - [22]
JWZT34UCAdd `Localize`` trait bound for each field in the derived item - [23]
7JPOCQEIAdd explicit error handling for macro parsing - [24]
7U2DXFMPRefactor `fluent_embed::Localize` to support overriding locales - [25]
YZ6PVVQCAdd error handling for common unsupported Rust code - [26]
F5LG7WENEmit compilation errors from Fluent source code - [27]
3NMKD6I5Refactor `Localize` trait to use `std::io::Write` - [28]
RUFPE6OOInclude canonical locale in list of available locales - [29]
5I5NR4DQMake `Localize::CANONICAL_LOCALE` a function instead of associated constant
Change contents
- file addition: unnamed_fields.rs[4.101]
//! End-to-end test for unnamed field support in the `l10n_embed_derive` macromod common;use icu_locale::{Locale, locale};use l10n_embed::Localize;use l10n_embed_derive::localize;const DEFAULT_LOCALE: Locale = locale!("en-US");const EXPECTED_LOCALES: [Locale; 4] = [locale!("zh-CN"),locale!("en-US"),locale!("ja-JP"),locale!("fr"),];pub struct LocalizableType;impl Localize for LocalizableType {fn available_locales(&self) -> Vec<Locale> {EXPECTED_LOCALES.to_vec()}fn localize_for(&self, locale: &Locale) -> String {format!("Localized with locale: {locale}")}}#[localize("tests/locale/**/basic.ftl")]pub struct MessageStruct(LocalizableType);#[localize("tests/locale/**/basic.ftl")]pub enum MessageEnum {Greeting { name: String },Forward(LocalizableType),}mod forward {use super::common::compare_message;use super::{DEFAULT_LOCALE, EXPECTED_LOCALES, LocalizableType, MessageEnum, MessageStruct};use l10n_embed::Localize;use pretty_assertions::assert_eq;#[test]fn message() {let expected = "Localized with locale: en-US";compare_message(MessageStruct(LocalizableType), expected, DEFAULT_LOCALE);compare_message(MessageEnum::Forward(LocalizableType),expected,DEFAULT_LOCALE,);}#[test]fn available_locales() {let expected_locales = EXPECTED_LOCALES.to_vec();assert_eq!(expected_locales,MessageStruct(LocalizableType).available_locales());assert_eq!(expected_locales,MessageEnum::Forward(LocalizableType).available_locales());}}/// Ensure other enum variants are not affectedmod preserves_enum_variants {use super::common::compare_message;use super::{DEFAULT_LOCALE, LocalizableType, MessageEnum};use l10n_embed::Localize;use pretty_assertions::{assert_eq, assert_ne};#[test]fn message() {compare_message(MessageEnum::Greeting {name: String::from("Ferris"),},"Hello, Ferris!",DEFAULT_LOCALE,);}#[test]fn available_locales() {let greeting = MessageEnum::Greeting {name: String::from("Ferris"),};assert_ne!(greeting.available_locales(),MessageEnum::Forward(LocalizableType).available_locales());assert_eq!(vec![DEFAULT_LOCALE], greeting.available_locales());}} - edit in l10n_embed_derive/src/macro_impl/mod.rs at line 2
use crate::macro_impl::derive::ReferenceKind; - replacement in l10n_embed_derive/src/macro_impl/mod.rs at line 14
pub enum UnsupportedReason {pub enum UnsupportedError { - replacement in l10n_embed_derive/src/macro_impl/mod.rs at line 16
Union,#[error("Unnamed fields are not supported")]UnnamedFields,}#[derive(Debug, Error)]#[error("Unsupported Rust code")]pub struct UnsupportedError {span: syn::Ident,reason: UnsupportedReason,Union { span: syn::Ident },#[error("Only 1 unnamed field is supported")]UnnamedFields {span: syn::Ident,field_count: usize,}, - replacement in l10n_embed_derive/src/macro_impl/mod.rs at line 76
syn::Data::Struct(_struct_data) => derive::locales_for_ident(&group, &derive_input.ident),syn::Data::Struct(struct_data) => derive::locales_for_ident(&group,&struct_data.fields,ReferenceKind::StructField,&derive_input.ident,), - replacement in l10n_embed_derive/src/macro_impl/mod.rs at line 84
return Err(MacroError::Unsupported(UnsupportedError {return Err(MacroError::Unsupported(UnsupportedError::Union { - edit in l10n_embed_derive/src/macro_impl/mod.rs at line 86
reason: UnsupportedReason::Union, - replacement in l10n_embed_derive/src/macro_impl/mod.rs at line 96
return Err(MacroError::Unsupported(UnsupportedError {return Err(MacroError::Unsupported(UnsupportedError::Union { - edit in l10n_embed_derive/src/macro_impl/mod.rs at line 98
reason: UnsupportedReason::Union, - replacement in l10n_embed_derive/src/macro_impl/mod.rs at line 109[4.1700]→[4.1700:1976](∅→∅),[4.1976]→[2.0:100](∅→∅),[2.100]→[4.836:1022](∅→∅),[4.836]→[4.836:1022](∅→∅),[4.1022]→[4.1119:1140](∅→∅),[4.1140]→[4.1042:1056](∅→∅),[4.1042]→[4.1042:1056](∅→∅),[4.1056]→[4.2002:2013](∅→∅),[4.2002]→[4.2002:2013](∅→∅)
let named_fields: Vec<&syn::Type> = match &derive_input.data {syn::Data::Struct(struct_data) => match &struct_data.fields {syn::Fields::Named(named_fields) => {named_fields.named.iter().map(|field| &field.ty).collect()}syn::Fields::Unit => Vec::new(),syn::Fields::Unnamed(_unnamed_fields) => {return Err(MacroError::Unsupported(UnsupportedError {span: derive_input.ident.clone(),reason: UnsupportedReason::UnnamedFields,}));}},let field_types: Vec<&syn::Type> = match &derive_input.data {syn::Data::Struct(struct_data) => {types_for_fields(&struct_data.fields, derive_input.ident.clone())?} - replacement in l10n_embed_derive/src/macro_impl/mod.rs at line 116[4.2103]→[4.1057:1108](∅→∅),[4.1108]→[4.2159:2213](∅→∅),[4.2159]→[4.2159:2213](∅→∅),[4.2213]→[4.1109:1192](∅→∅),[4.1192]→[4.2282:2300](∅→∅),[4.2282]→[4.2282:2300](∅→∅),[4.2300]→[2.101:441](∅→∅),[2.441]→[4.2330:2345](∅→∅),[4.1393]→[4.2330:2345](∅→∅),[4.2330]→[4.2330:2345](∅→∅),[4.2345]→[4.1394:1453](∅→∅)
.map(|variant| match &variant.fields {syn::Fields::Named(named_fields) => {Ok(named_fields.named.iter().map(|field| &field.ty).collect())}syn::Fields::Unit => Ok(Vec::new()),syn::Fields::Unnamed(_unnamed_fields) => {Err(MacroError::Unsupported(UnsupportedError {span: variant.ident.clone(),reason: UnsupportedReason::UnnamedFields,}))}}).collect::<Result<Vec<Vec<&syn::Type>>, _>>()?.map(|variant| types_for_fields(&variant.fields, variant.ident.clone())).collect::<Result<Vec<Vec<&syn::Type>>, UnsupportedError>>()? - replacement in l10n_embed_derive/src/macro_impl/mod.rs at line 121
syn::Data::Union(_union_data) => todo!(),syn::Data::Union(_union_data) => {return Err(MacroError::Unsupported(UnsupportedError::Union {span: derive_input.ident.clone(),}));} - replacement in l10n_embed_derive/src/macro_impl/mod.rs at line 130[4.2536]→[4.2536:2745](∅→∅),[4.2745]→[4.991:1063](∅→∅),[4.178]→[4.2822:2834](∅→∅),[4.1063]→[4.2822:2834](∅→∅),[4.2822]→[4.2822:2834](∅→∅)
let additional_bounds = named_fields.into_iter().map(|field| -> syn::WherePredicate {// Attribute this bound to the original source codelet span = field.span();parse_quote_spanned!(span=> #field: ::l10n_embed::Localize)});let additional_bounds = field_types.into_iter().map(|field| -> syn::WherePredicate {// Attribute this bound to the original source codelet span = field.span();parse_quote_spanned!(span=> #field: ::l10n_embed::Localize)}); - edit in l10n_embed_derive/src/macro_impl/mod.rs at line 168[4.2182]
fn types_for_fields(fields: &syn::Fields,span: syn::Ident,) -> Result<Vec<&syn::Type>, UnsupportedError> {match fields {syn::Fields::Named(named_fields) => {Ok(named_fields.named.iter().map(|field| &field.ty).collect())}syn::Fields::Unit => Ok(Vec::new()),syn::Fields::Unnamed(unnamed_fields) => {let unnamed_field_types: Vec<&syn::Type> = unnamed_fields.unnamed.iter().map(|field| &field.ty).collect();match unnamed_field_types.len() {1 => Ok(unnamed_field_types),_ => Err(UnsupportedError::UnnamedFields {span,field_count: unnamed_field_types.len(),}),}}}} - replacement in l10n_embed_derive/src/macro_impl/error.rs at line 2
MacroError, ParseError, UnsupportedError, UnsupportedReason, attribute,MacroError, ParseError, UnsupportedError, attribute, - replacement in l10n_embed_derive/src/macro_impl/error.rs at line 107
match error.reason {UnsupportedReason::Union => emit_error! { error.span, "unions are not supported";match error {UnsupportedError::Union { span } => emit_error! { span, "unions are not supported"; - replacement in l10n_embed_derive/src/macro_impl/error.rs at line 111
UnsupportedReason::UnnamedFields => {emit_error! { error.span, "only named fields are supported";help = "Each field needs a name so it can be referenced by Fluent code";note = "There must be at least one named field (unit structs are unsupported!)";UnsupportedError::UnnamedFields { span, field_count } => {emit_error! { span, "only 1 unnamed field is supported, got {} fields", field_count;help = "When there are multiple fields, each field needs a name so it can be referenced by Fluent code";note = "Using a single unnamed field forwards to that field's implementation of `Localize`"; - replacement in l10n_embed_derive/src/macro_impl/derive.rs at line 9
use super::{MacroError, UnsupportedError, UnsupportedReason};use super::{MacroError, UnsupportedError}; - replacement in l10n_embed_derive/src/macro_impl/derive.rs at line 26
reference_kind: &Context,context: &Context, - replacement in l10n_embed_derive/src/macro_impl/derive.rs at line 28
let canonical_message = group.remove_canonical_message(ident, reference_kind)?;let canonical_message = group.remove_canonical_message(ident, context)?; - replacement in l10n_embed_derive/src/macro_impl/derive.rs at line 31
.remove_additional_messages(ident, reference_kind)?.remove_additional_messages(ident, context)? - edit in l10n_embed_derive/src/macro_impl/derive.rs at line 68
fn expr_for_unnamed_fields(unnamed_fields: &syn::FieldsUnnamed,ident: &syn::Ident,context: &Context,) -> Result<TokenStream, UnsupportedError> {let field_count = unnamed_fields.unnamed.iter().count();if field_count != 1 {return Err(UnsupportedError::UnnamedFields {span: ident.clone(),field_count,});}let field_reference = match context.reference_kind {ReferenceKind::EnumField => quote!(unnamed_field),ReferenceKind::StructField => quote!(self.0),};Ok(quote!(#field_reference.localize_for(locale)))} - replacement in l10n_embed_derive/src/macro_impl/derive.rs at line 105
pub fn locales_for_ident(group: &fluent::Group, ident: &syn::Ident) -> TokenStream {let id = ident.to_string().to_kebab_case();pub fn locales_for_ident(group: &fluent::Group,fields: &syn::Fields,reference_kind: ReferenceKind,ident: &syn::Ident,) -> TokenStream {match fields {// Provide available locales based on what Fluent source files are availablesyn::Fields::Named(_) | syn::Fields::Unit => {let id = ident.to_string().to_kebab_case(); - replacement in l10n_embed_derive/src/macro_impl/derive.rs at line 116
let locale_literals = group.locales_for_message(&id).map(|locale| locale.id.to_string()).map(|locale_string| syn::LitStr::new(&locale_string, proc_macro2::Span::call_site()));let locale_literals = group.locales_for_message(&id).map(|locale| locale.id.to_string()).map(|locale_string| {syn::LitStr::new(&locale_string, proc_macro2::Span::call_site())});quote!(vec![self.canonical_locale(), #(::l10n_embed::macro_prelude::icu_locale::locale!(#locale_literals)),*])}// Forward to the unnamed field's implementationsyn::Fields::Unnamed(_unnamed_fields) => {let unnamed_field_ident = match reference_kind {ReferenceKind::EnumField => quote!(unnamed_field),ReferenceKind::StructField => quote!(self.0),}; - replacement in l10n_embed_derive/src/macro_impl/derive.rs at line 134
// There is only one message for this struct, so just list every supported localequote!(vec![self.canonical_locale(), #(::l10n_embed::macro_prelude::icu_locale::locale!(#locale_literals)),*])quote!(#unnamed_field_ident.available_locales())}} - replacement in l10n_embed_derive/src/macro_impl/derive.rs at line 145[4.1200]→[4.1200:1236](∅→∅),[4.1236]→[4.3187:3241](∅→∅),[4.3241]→[4.1293:1348](∅→∅),[4.9823]→[4.1293:1348](∅→∅),[4.1293]→[4.1293:1348](∅→∅),[4.1348]→[4.9824:9880](∅→∅),[4.9880]→[4.1394:1468](∅→∅),[4.1394]→[4.1394:1468](∅→∅),[4.1468]→[4.9881:9946](∅→∅),[4.9946]→[4.1527:1538](∅→∅),[4.1527]→[4.1527:1538](∅→∅),[4.1538]→[2.442:481](∅→∅)
let references = match fields {syn::Fields::Named(named_fields) => Context {// Reference using `self.{reference_name}`reference_kind: ReferenceKind::StructField,// Create a list of unique field names that can be referencedvalid_references: unique_named_fields(named_fields),},syn::Fields::Unit => Context {let context = match fields {syn::Fields::Named(named_fields) => {Context {// Reference using `self.{reference_name}`reference_kind: ReferenceKind::StructField,// Create a list of unique field names that can be referencedvalid_references: unique_named_fields(named_fields),}}syn::Fields::Unit | syn::Fields::Unnamed(_) => Context { - edit in l10n_embed_derive/src/macro_impl/derive.rs at line 158[2.594]→[2.594:645](∅→∅),[2.645]→[4.2414:2575](∅→∅),[4.2414]→[4.2414:2575](∅→∅),[4.2575]→[4.1290:1307](∅→∅),[4.1307]→[4.2591:2601](∅→∅),[4.2591]→[4.2591:2601](∅→∅)
syn::Fields::Unnamed(_unnamed_fields) => {return Err(MacroError::Unsupported(UnsupportedError {span: ident.clone(),reason: UnsupportedReason::UnnamedFields,}));} - replacement in l10n_embed_derive/src/macro_impl/derive.rs at line 160
Ok(expr_for_message(&mut group, ident, &references)?)Ok(match fields {syn::Fields::Named(_) | syn::Fields::Unit => expr_for_message(&mut group, ident, &context)?,syn::Fields::Unnamed(unnamed_fields) => {expr_for_unnamed_fields(unnamed_fields, ident, &context)?}}) - replacement in l10n_embed_derive/src/macro_impl/derive.rs at line 177
// Simplify match code by always ignoring enum fields (even if they don't exist)// We are matching the variant name, not any data, so each arm will have something like:// Self::VariantName { .. }// Even if `Self::VariantName` doesn't contain any datalet locales_for_variant = locales_for_ident(group, variant_ident);match_arms.push(quote!(Self::#variant_ident { .. } => #locales_for_variant));let destructuring_pattern = match &enum_variant.fields {// Simplify match code by always ignoring enum fields (even if they don't exist)// We are matching the variant name, not any data, so each arm will have something like:// Self::VariantName { .. }// Even if `Self::VariantName` doesn't contain any datasyn::Fields::Named(_) | syn::Fields::Unit => quote!(Self::#variant_ident { .. }),// Bind the single unnamed field so we can use its implementation of `Localize::available_locales()`syn::Fields::Unnamed(_unnamed_fields) => quote!(Self::#variant_ident(unnamed_field)),};let locales_for_variant = locales_for_ident(group,&enum_variant.fields,ReferenceKind::EnumField,variant_ident,);match_arms.push(quote!(#destructuring_pattern => #locales_for_variant)); - replacement in l10n_embed_derive/src/macro_impl/derive.rs at line 223[2.709]→[2.709:764](∅→∅),[2.764]→[4.2720:2906](∅→∅),[4.2720]→[4.2720:2906](∅→∅),[4.2906]→[4.1308:1329](∅→∅)
syn::Fields::Unnamed(_unnamed_fields) => {return Err(MacroError::Unsupported(UnsupportedError {span: enum_variant.ident.clone(),reason: UnsupportedReason::UnnamedFields,}));syn::Fields::Unnamed(unnamed_fields) => {let field_count = unnamed_fields.unnamed.iter().count();if field_count != 1 {return Err(MacroError::Unsupported(UnsupportedError::UnnamedFields {span: enum_variant.ident.clone(),field_count,}));}quote!(#variant_pascal_case(unnamed_field)) - replacement in l10n_embed_derive/src/macro_impl/derive.rs at line 236
let references = match &enum_variant.fields {let context = match &enum_variant.fields { - replacement in l10n_embed_derive/src/macro_impl/derive.rs at line 241
syn::Fields::Unit => Context {syn::Fields::Unit | syn::Fields::Unnamed(_) => Context { - replacement in l10n_embed_derive/src/macro_impl/derive.rs at line 245[2.931]→[2.931:986](∅→∅),[2.986]→[4.2960:3146](∅→∅),[4.2960]→[4.2960:3146](∅→∅),[4.3146]→[4.1330:1351](∅→∅)
syn::Fields::Unnamed(_unnamed_fields) => {return Err(MacroError::Unsupported(UnsupportedError {span: enum_variant.ident.clone(),reason: UnsupportedReason::UnnamedFields,}));};let arm_body = match &enum_variant.fields {syn::Fields::Named(_) | syn::Fields::Unit => {expr_for_message(&mut group, &enum_variant.ident, &context)?}syn::Fields::Unnamed(unnamed_fields) => {expr_for_unnamed_fields(unnamed_fields, &enum_variant.ident, &context)? - edit in l10n_embed_derive/src/macro_impl/derive.rs at line 255
let arm_body = expr_for_message(&mut group, &enum_variant.ident, &references)?;