blob: 1b02662421e7dd88c405d164522ac2b961ce0af7 [file] [log] [blame]
// 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<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,
key_lit: Option<LitStr>,
fallback_by: Option<LitStr>,
extension_key: Option<LitStr>,
fallback_supplement: Option<LitStr>,
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<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 key_lit: Option<LitStr> = None;
let mut fallback_by: Option<LitStr> = None;
let mut extension_key: Option<LitStr> = None;
let mut fallback_supplement: 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 == "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<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.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
}