| // 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/documents/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; |
| |
| /// 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 `DynamicDataMarker` and/or `DataMarker` |
| /// by adding symbols with optional marker path 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 locale { |
| /// # pub mod fallback { |
| /// # pub use icu_provider::fallback::LocaleFallbackPriority; |
| /// # } |
| /// # } |
| /// # } |
| /// use icu::locale::fallback::*; |
| /// 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",) |
| /// )] |
| /// pub struct FooV1<'data> { |
| /// message: Cow<'data, str>, |
| /// }; |
| /// |
| /// // Note: FooV1Marker implements `DynamicDataMarker` but not `DataMarker`. |
| /// // The other two implement `DataMarker`. |
| /// |
| /// assert_eq!(BarV1Marker::INFO.path.as_str(), "demo/bar@1"); |
| /// assert_eq!( |
| /// BarV1Marker::INFO.fallback_config.priority, |
| /// LocaleFallbackPriority::Language |
| /// ); |
| /// |
| /// assert_eq!(BazV1Marker::INFO.path.as_str(), "demo/baz@1"); |
| /// assert_eq!( |
| /// BazV1Marker::INFO.fallback_config.priority, |
| /// LocaleFallbackPriority::Region |
| /// ); |
| /// ``` |
| #[proc_macro_attribute] |
| 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<DataStructArg, Token![,]>, |
| } |
| |
| impl Parse for DataStructArgs { |
| fn parse(input: ParseStream<'_>) -> parse::Result<Self> { |
| let args = input.parse_terminated(DataStructArg::parse, Token![,])?; |
| Ok(Self { args }) |
| } |
| } |
| struct DataStructArg { |
| marker_name: Path, |
| path_lit: Option<LitStr>, |
| fallback_by: Option<LitStr>, |
| attributes_domain: Option<LitStr>, |
| singleton: bool, |
| } |
| |
| impl DataStructArg { |
| fn new(marker_name: Path) -> Self { |
| Self { |
| marker_name, |
| path_lit: None, |
| fallback_by: None, |
| attributes_domain: None, |
| singleton: false, |
| } |
| } |
| } |
| |
| impl Parse for DataStructArg { |
| fn parse(input: ParseStream<'_>) -> parse::Result<Self> { |
| let path: Path = input.parse()?; |
| |
| fn at_most_one_option<T>( |
| o: &mut Option<T>, |
| 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<Path> = None; |
| let mut path_lit: Option<LitStr> = None; |
| let mut fallback_by: Option<LitStr> = None; |
| let mut attributes_domain: Option<LitStr> = 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 == "attributes_domain" { |
| at_most_one_option( |
| &mut attributes_domain, |
| value, |
| "attributes_domain", |
| 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 path_lit, lit, "literal path", 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, |
| path_lit, |
| fallback_by, |
| attributes_domain, |
| 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.path_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<Self> { |
| 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::<Vec<_>>(); |
| |
| let name = &input.ident; |
| |
| let name_with_lt = if !lifetimes.is_empty() { |
| 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 mut result = TokenStream2::new(); |
| |
| for single_attr in attr.args { |
| let DataStructArg { |
| marker_name, |
| path_lit, |
| fallback_by, |
| attributes_domain, |
| singleton, |
| } = single_attr; |
| |
| let docs = if let Some(ref path_lit) = path_lit { |
| let fallback_by_docs_str = match fallback_by { |
| Some(ref fallback_by) => fallback_by.value(), |
| None => "language (default)".to_string(), |
| }; |
| format!( |
| "Marker type for [`{name}`]: \"{}\"\n\n- Fallback priority: {fallback_by_docs_str}", |
| path_lit.value() |
| ) |
| } else { |
| format!("Marker type for [`{name}`]") |
| }; |
| |
| result.extend(quote!( |
| #[doc = #docs] |
| pub struct #marker_name; |
| impl icu_provider::DynamicDataMarker for #marker_name { |
| type DataStruct = #name_with_lt; |
| } |
| )); |
| |
| if let Some(path_lit) = path_lit { |
| let path_str = path_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::fallback::LocaleFallbackPriority::Region} |
| } |
| "script" => { |
| quote! {icu_provider::fallback::LocaleFallbackPriority::Script} |
| } |
| "language" => { |
| quote! {icu_provider::fallback::LocaleFallbackPriority::Language} |
| } |
| _ => panic!("Invalid value for fallback_by"), |
| } |
| } else { |
| quote! {icu_provider::fallback::LocaleFallbackPriority::default()} |
| }; |
| let attributes_domain_setter = if let Some(attributes_domain_lit) = attributes_domain { |
| quote! { info.attributes_domain = #attributes_domain_lit; } |
| } else { |
| quote!() |
| }; |
| result.extend(quote!( |
| impl icu_provider::DataMarker for #marker_name { |
| const INFO: icu_provider::DataMarkerInfo = { |
| let mut info = icu_provider::DataMarkerInfo::from_path(icu_provider::marker::data_marker_path!(#path_str)); |
| info.is_singleton = #singleton; |
| info.fallback_config.priority = #fallback_by_expr; |
| #attributes_domain_setter |
| info |
| }; |
| } |
| )); |
| } |
| } |
| |
| result.extend(quote!( |
| #[derive(icu_provider::prelude::yoke::Yokeable, icu_provider::prelude::zerofrom::ZeroFrom)] |
| #input |
| )); |
| |
| result |
| } |