// This file is part of ICU4X. For terms of use, please see the file // called LICENSE at the top level of the ICU4X source tree // (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). // https://github.com/unicode-org/icu4x/blob/main/docs/process/boilerplate.md#library-annotations #![cfg_attr( not(test), deny( clippy::indexing_slicing, clippy::unwrap_used, clippy::expect_used, // Panics are OK in proc macros // clippy::panic, clippy::exhaustive_structs, clippy::exhaustive_enums, missing_debug_implementations, ) )] #![warn(missing_docs)] //! Proc macros for the ICU4X data provider. //! //! These macros are re-exported from `icu_provider`. extern crate proc_macro; use proc_macro::TokenStream; use proc_macro2::Span; use proc_macro2::TokenStream as TokenStream2; use quote::quote; use syn::parenthesized; use syn::parse::{self, Parse, ParseStream}; use syn::parse_macro_input; use syn::punctuated::Punctuated; use syn::spanned::Spanned; use syn::DeriveInput; use syn::{Ident, LitStr, Path, Token}; #[cfg(test)] mod tests; #[proc_macro_attribute] /// The `#[data_struct]` attribute should be applied to all types intended /// for use in a `DataStruct`. /// /// It does the following things: /// /// - `Apply #[derive(Yokeable, ZeroFrom)]`. The `ZeroFrom` derive can /// be customized with `#[zerofrom(clone)]` on non-ZeroFrom fields. /// /// In addition, the attribute can be used to implement `DataMarker` and/or `KeyedDataMarker` /// by adding symbols with optional key strings: /// /// ``` /// # // We DO NOT want to pull in the `icu` crate as a dev-dependency, /// # // because that will rebuild the whole tree in proc macro mode /// # // when using cargo test --all-features --all-targets. /// # pub mod icu { /// # pub mod locid_transform { /// # pub mod fallback { /// # pub use icu_provider::_internal::{LocaleFallbackPriority, LocaleFallbackSupplement}; /// # } /// # } /// # pub use icu_provider::_internal::locid; /// # } /// use icu::locid_transform::fallback::*; /// use icu::locid::extensions::unicode::key; /// use icu_provider::prelude::*; /// use std::borrow::Cow; /// /// #[icu_provider::data_struct( /// FooV1Marker, /// BarV1Marker = "demo/bar@1", /// marker( /// BazV1Marker, /// "demo/baz@1", /// fallback_by = "region", /// extension_key = "ca" /// ) /// )] /// pub struct FooV1<'data> { /// message: Cow<'data, str>, /// }; /// /// // Note: FooV1Marker implements `DataMarker` but not `KeyedDataMarker`. /// // The other two implement `KeyedDataMarker`. /// /// assert_eq!(&*BarV1Marker::KEY.path(), "demo/bar@1"); /// assert_eq!( /// BarV1Marker::KEY.metadata().fallback_priority, /// LocaleFallbackPriority::Language /// ); /// assert_eq!(BarV1Marker::KEY.metadata().extension_key, None); /// /// assert_eq!(&*BazV1Marker::KEY.path(), "demo/baz@1"); /// assert_eq!( /// BazV1Marker::KEY.metadata().fallback_priority, /// LocaleFallbackPriority::Region /// ); /// assert_eq!( /// BazV1Marker::KEY.metadata().extension_key, /// Some(key!("ca")) /// ); /// ``` /// /// If the `#[databake(path = ...)]` attribute is present on the data struct, this will also /// implement it on the markers. pub fn data_struct(attr: TokenStream, item: TokenStream) -> TokenStream { TokenStream::from(data_struct_impl( parse_macro_input!(attr as DataStructArgs), parse_macro_input!(item as DeriveInput), )) } pub(crate) struct DataStructArgs { args: Punctuated, } impl Parse for DataStructArgs { fn parse(input: ParseStream<'_>) -> parse::Result { let args = input.parse_terminated(DataStructArg::parse, Token![,])?; Ok(Self { args }) } } struct DataStructArg { marker_name: Path, key_lit: Option, fallback_by: Option, extension_key: Option, fallback_supplement: Option, singleton: bool, } impl DataStructArg { fn new(marker_name: Path) -> Self { Self { marker_name, key_lit: None, fallback_by: None, extension_key: None, fallback_supplement: None, singleton: false, } } } impl Parse for DataStructArg { fn parse(input: ParseStream<'_>) -> parse::Result { let path: Path = input.parse()?; fn at_most_one_option( o: &mut Option, new: T, name: &str, span: Span, ) -> parse::Result<()> { if o.replace(new).is_some() { Err(parse::Error::new( span, format!("marker() cannot contain multiple {name}s"), )) } else { Ok(()) } } if path.is_ident("marker") { let content; let paren = parenthesized!(content in input); let mut marker_name: Option = None; let mut key_lit: Option = None; let mut fallback_by: Option = None; let mut extension_key: Option = None; let mut fallback_supplement: Option = None; let mut singleton = false; let punct = content.parse_terminated(DataStructMarkerArg::parse, Token![,])?; for entry in punct { match entry { DataStructMarkerArg::Path(path) => { at_most_one_option(&mut marker_name, path, "marker", input.span())?; } DataStructMarkerArg::NameValue(name, value) => { if name == "fallback_by" { at_most_one_option( &mut fallback_by, value, "fallback_by", paren.span.join(), )?; } else if name == "extension_key" { at_most_one_option( &mut extension_key, value, "extension_key", paren.span.join(), )?; } else if name == "fallback_supplement" { at_most_one_option( &mut fallback_supplement, value, "fallback_supplement", paren.span.join(), )?; } else { return Err(parse::Error::new( name.span(), format!("unknown option {name} in marker()"), )); } } DataStructMarkerArg::Lit(lit) => { at_most_one_option(&mut key_lit, lit, "literal key", input.span())?; } DataStructMarkerArg::Singleton => { singleton = true; } } } let marker_name = if let Some(marker_name) = marker_name { marker_name } else { return Err(parse::Error::new( input.span(), "marker() must contain a marker!", )); }; Ok(Self { marker_name, key_lit, fallback_by, extension_key, fallback_supplement, singleton, }) } else { let mut this = DataStructArg::new(path); let lookahead = input.lookahead1(); if lookahead.peek(Token![=]) { let _t: Token![=] = input.parse()?; let lit: LitStr = input.parse()?; this.key_lit = Some(lit); Ok(this) } else { Ok(this) } } } } /// A single argument to `marker()` in `#[data_struct(..., marker(...), ...)] enum DataStructMarkerArg { Path(Path), NameValue(Ident, LitStr), Lit(LitStr), Singleton, } impl Parse for DataStructMarkerArg { fn parse(input: ParseStream<'_>) -> parse::Result { let lookahead = input.lookahead1(); if lookahead.peek(LitStr) { Ok(DataStructMarkerArg::Lit(input.parse()?)) } else { let path: Path = input.parse()?; let lookahead = input.lookahead1(); if lookahead.peek(Token![=]) { let _tok: Token![=] = input.parse()?; let ident = path.get_ident().ok_or_else(|| { parse::Error::new(path.span(), "Expected identifier before `=`, found path") })?; Ok(DataStructMarkerArg::NameValue( ident.clone(), input.parse()?, )) } else if path.is_ident("singleton") { Ok(DataStructMarkerArg::Singleton) } else { Ok(DataStructMarkerArg::Path(path)) } } } } fn data_struct_impl(attr: DataStructArgs, input: DeriveInput) -> TokenStream2 { if input.generics.type_params().count() > 0 { return syn::Error::new( input.generics.span(), "#[data_struct] does not support type parameters", ) .to_compile_error(); } let lifetimes = input.generics.lifetimes().collect::>(); let name = &input.ident; let name_with_lt = if lifetimes.get(0).is_some() { quote!(#name<'static>) } else { quote!(#name) }; if lifetimes.len() > 1 { return syn::Error::new( input.generics.span(), "#[data_struct] does not support more than one lifetime parameter", ) .to_compile_error(); } let bake_derive = input .attrs .iter() .find(|a| a.path().is_ident("databake")) .map(|a| { quote! { #[derive(databake::Bake)] #a } }) .unwrap_or_else(|| quote! {}); let mut result = TokenStream2::new(); for single_attr in attr.args { let DataStructArg { marker_name, key_lit, fallback_by, extension_key, fallback_supplement, singleton, } = single_attr; let docs = if let Some(ref key_lit) = key_lit { let fallback_by_docs_str = match fallback_by { Some(ref fallback_by) => fallback_by.value(), None => "language (default)".to_string(), }; let extension_key_docs_str = match extension_key { Some(ref extension_key) => extension_key.value(), None => "none (default)".to_string(), }; format!("Marker type for [`{}`]: \"{}\"\n\n- Fallback priority: {}\n- Extension keyword: {}", name, key_lit.value(), fallback_by_docs_str, extension_key_docs_str) } else { format!("Marker type for [`{name}`]") }; result.extend(quote!( #[doc = #docs] #bake_derive pub struct #marker_name; impl icu_provider::DataMarker for #marker_name { type Yokeable = #name_with_lt; } )); if let Some(key_lit) = key_lit { let key_str = key_lit.value(); let fallback_by_expr = if let Some(fallback_by_lit) = fallback_by { match fallback_by_lit.value().as_str() { "region" => { quote! {icu_provider::_internal::LocaleFallbackPriority::Region} } "collation" => { quote! {icu_provider::_internal::LocaleFallbackPriority::Collation} } "language" => { quote! {icu_provider::_internal::LocaleFallbackPriority::Language} } _ => panic!("Invalid value for fallback_by"), } } else { quote! {icu_provider::_internal::LocaleFallbackPriority::const_default()} }; let extension_key_expr = if let Some(extension_key_lit) = extension_key { quote! {Some(icu_provider::_internal::locid::extensions::unicode::key!(#extension_key_lit))} } else { quote! {None} }; let fallback_supplement_expr = if let Some(fallback_supplement_lit) = fallback_supplement { match fallback_supplement_lit.value().as_str() { "collation" => { quote! {Some(icu_provider::_internal::LocaleFallbackSupplement::Collation)} } _ => panic!("Invalid value for fallback_supplement"), } } else { quote! {None} }; result.extend(quote!( impl icu_provider::KeyedDataMarker for #marker_name { const KEY: icu_provider::DataKey = icu_provider::data_key!(#key_str, icu_provider::DataKeyMetadata::construct_internal( #fallback_by_expr, #extension_key_expr, #fallback_supplement_expr, #singleton, )); } )); } } result.extend(quote!( #[derive(icu_provider::prelude::yoke::Yokeable, icu_provider::prelude::zerofrom::ZeroFrom)] #input )); result }