| 1 | // This file is part of ICU4X. For terms of use, please see the file |
| 2 | // called LICENSE at the top level of the ICU4X source tree |
| 3 | // (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). |
| 4 | |
| 5 | // https://github.com/unicode-org/icu4x/blob/main/documents/process/boilerplate.md#library-annotations |
| 6 | #![cfg_attr ( |
| 7 | not(test), |
| 8 | deny( |
| 9 | clippy::indexing_slicing, |
| 10 | clippy::unwrap_used, |
| 11 | clippy::expect_used, |
| 12 | // Panics are OK in proc macros |
| 13 | // clippy::panic, |
| 14 | clippy::exhaustive_structs, |
| 15 | clippy::exhaustive_enums, |
| 16 | missing_debug_implementations, |
| 17 | ) |
| 18 | )] |
| 19 | #![warn (missing_docs)] |
| 20 | |
| 21 | //! Proc macros for the ICU4X data provider. |
| 22 | //! |
| 23 | //! These macros are re-exported from `icu_provider`. |
| 24 | |
| 25 | extern crate proc_macro; |
| 26 | use proc_macro::TokenStream; |
| 27 | use proc_macro2::Span; |
| 28 | use proc_macro2::TokenStream as TokenStream2; |
| 29 | use quote::quote; |
| 30 | use syn::parenthesized; |
| 31 | use syn::parse::{self, Parse, ParseStream}; |
| 32 | use syn::parse_macro_input; |
| 33 | use syn::punctuated::Punctuated; |
| 34 | use syn::spanned::Spanned; |
| 35 | use syn::DeriveInput; |
| 36 | use syn::{Ident, LitStr, Path, Token}; |
| 37 | #[cfg (test)] |
| 38 | mod tests; |
| 39 | |
| 40 | #[proc_macro_attribute ] |
| 41 | |
| 42 | /// The `#[data_struct]` attribute should be applied to all types intended |
| 43 | /// for use in a `DataStruct`. |
| 44 | /// |
| 45 | /// It does the following things: |
| 46 | /// |
| 47 | /// - `Apply #[derive(Yokeable, ZeroFrom)]`. The `ZeroFrom` derive can |
| 48 | /// be customized with `#[zerofrom(clone)]` on non-ZeroFrom fields. |
| 49 | /// |
| 50 | /// In addition, the attribute can be used to implement `DataMarker` and/or `KeyedDataMarker` |
| 51 | /// by adding symbols with optional key strings: |
| 52 | /// |
| 53 | /// ``` |
| 54 | /// # // We DO NOT want to pull in the `icu` crate as a dev-dependency, |
| 55 | /// # // because that will rebuild the whole tree in proc macro mode |
| 56 | /// # // when using cargo test --all-features --all-targets. |
| 57 | /// # pub mod icu { |
| 58 | /// # pub mod locid_transform { |
| 59 | /// # pub mod fallback { |
| 60 | /// # pub use icu_provider::_internal::LocaleFallbackPriority; |
| 61 | /// # } |
| 62 | /// # } |
| 63 | /// # pub use icu_provider::_internal::locid; |
| 64 | /// # } |
| 65 | /// use icu::locid::extensions::unicode::key; |
| 66 | /// use icu::locid_transform::fallback::*; |
| 67 | /// use icu_provider::yoke; |
| 68 | /// use icu_provider::zerofrom; |
| 69 | /// use icu_provider::KeyedDataMarker; |
| 70 | /// use std::borrow::Cow; |
| 71 | /// |
| 72 | /// #[icu_provider::data_struct( |
| 73 | /// FooV1Marker, |
| 74 | /// BarV1Marker = "demo/bar@1" , |
| 75 | /// marker( |
| 76 | /// BazV1Marker, |
| 77 | /// "demo/baz@1" , |
| 78 | /// fallback_by = "region" , |
| 79 | /// extension_key = "ca" |
| 80 | /// ) |
| 81 | /// )] |
| 82 | /// pub struct FooV1<'data> { |
| 83 | /// message: Cow<'data, str>, |
| 84 | /// }; |
| 85 | /// |
| 86 | /// // Note: FooV1Marker implements `DataMarker` but not `KeyedDataMarker`. |
| 87 | /// // The other two implement `KeyedDataMarker`. |
| 88 | /// |
| 89 | /// assert_eq!(&*BarV1Marker::KEY.path(), "demo/bar@1" ); |
| 90 | /// assert_eq!( |
| 91 | /// BarV1Marker::KEY.metadata().fallback_priority, |
| 92 | /// LocaleFallbackPriority::Language |
| 93 | /// ); |
| 94 | /// assert_eq!(BarV1Marker::KEY.metadata().extension_key, None); |
| 95 | /// |
| 96 | /// assert_eq!(&*BazV1Marker::KEY.path(), "demo/baz@1" ); |
| 97 | /// assert_eq!( |
| 98 | /// BazV1Marker::KEY.metadata().fallback_priority, |
| 99 | /// LocaleFallbackPriority::Region |
| 100 | /// ); |
| 101 | /// assert_eq!(BazV1Marker::KEY.metadata().extension_key, Some(key!("ca" ))); |
| 102 | /// ``` |
| 103 | /// |
| 104 | /// If the `#[databake(path = ...)]` attribute is present on the data struct, this will also |
| 105 | /// implement it on the markers. |
| 106 | pub fn data_struct (attr: TokenStream, item: TokenStream) -> TokenStream { |
| 107 | TokenStream::from(data_struct_impl( |
| 108 | attr:parse_macro_input!(attr as DataStructArgs), |
| 109 | input:parse_macro_input!(item as DeriveInput), |
| 110 | )) |
| 111 | } |
| 112 | |
| 113 | pub(crate) struct DataStructArgs { |
| 114 | args: Punctuated<DataStructArg, Token![,]>, |
| 115 | } |
| 116 | |
| 117 | impl Parse for DataStructArgs { |
| 118 | fn parse(input: ParseStream<'_>) -> parse::Result<Self> { |
| 119 | let args: Punctuated = input.parse_terminated(parser:DataStructArg::parse, separator:Token![,])?; |
| 120 | Ok(Self { args }) |
| 121 | } |
| 122 | } |
| 123 | struct DataStructArg { |
| 124 | marker_name: Path, |
| 125 | key_lit: Option<LitStr>, |
| 126 | fallback_by: Option<LitStr>, |
| 127 | extension_key: Option<LitStr>, |
| 128 | fallback_supplement: Option<LitStr>, |
| 129 | singleton: bool, |
| 130 | } |
| 131 | |
| 132 | impl DataStructArg { |
| 133 | fn new(marker_name: Path) -> Self { |
| 134 | Self { |
| 135 | marker_name, |
| 136 | key_lit: None, |
| 137 | fallback_by: None, |
| 138 | extension_key: None, |
| 139 | fallback_supplement: None, |
| 140 | singleton: false, |
| 141 | } |
| 142 | } |
| 143 | } |
| 144 | |
| 145 | impl Parse for DataStructArg { |
| 146 | fn parse(input: ParseStream<'_>) -> parse::Result<Self> { |
| 147 | let path: Path = input.parse()?; |
| 148 | |
| 149 | fn at_most_one_option<T>( |
| 150 | o: &mut Option<T>, |
| 151 | new: T, |
| 152 | name: &str, |
| 153 | span: Span, |
| 154 | ) -> parse::Result<()> { |
| 155 | if o.replace(new).is_some() { |
| 156 | Err(parse::Error::new( |
| 157 | span, |
| 158 | format!("marker() cannot contain multiple {name}s" ), |
| 159 | )) |
| 160 | } else { |
| 161 | Ok(()) |
| 162 | } |
| 163 | } |
| 164 | |
| 165 | if path.is_ident("marker" ) { |
| 166 | let content; |
| 167 | let paren = parenthesized!(content in input); |
| 168 | let mut marker_name: Option<Path> = None; |
| 169 | let mut key_lit: Option<LitStr> = None; |
| 170 | let mut fallback_by: Option<LitStr> = None; |
| 171 | let mut extension_key: Option<LitStr> = None; |
| 172 | let mut fallback_supplement: Option<LitStr> = None; |
| 173 | let mut singleton = false; |
| 174 | let punct = content.parse_terminated(DataStructMarkerArg::parse, Token![,])?; |
| 175 | |
| 176 | for entry in punct { |
| 177 | match entry { |
| 178 | DataStructMarkerArg::Path(path) => { |
| 179 | at_most_one_option(&mut marker_name, path, "marker" , input.span())?; |
| 180 | } |
| 181 | DataStructMarkerArg::NameValue(name, value) => { |
| 182 | if name == "fallback_by" { |
| 183 | at_most_one_option( |
| 184 | &mut fallback_by, |
| 185 | value, |
| 186 | "fallback_by" , |
| 187 | paren.span.join(), |
| 188 | )?; |
| 189 | } else if name == "extension_key" { |
| 190 | at_most_one_option( |
| 191 | &mut extension_key, |
| 192 | value, |
| 193 | "extension_key" , |
| 194 | paren.span.join(), |
| 195 | )?; |
| 196 | } else if name == "fallback_supplement" { |
| 197 | at_most_one_option( |
| 198 | &mut fallback_supplement, |
| 199 | value, |
| 200 | "fallback_supplement" , |
| 201 | paren.span.join(), |
| 202 | )?; |
| 203 | } else { |
| 204 | return Err(parse::Error::new( |
| 205 | name.span(), |
| 206 | format!("unknown option {name} in marker()" ), |
| 207 | )); |
| 208 | } |
| 209 | } |
| 210 | DataStructMarkerArg::Lit(lit) => { |
| 211 | at_most_one_option(&mut key_lit, lit, "literal key" , input.span())?; |
| 212 | } |
| 213 | DataStructMarkerArg::Singleton => { |
| 214 | singleton = true; |
| 215 | } |
| 216 | } |
| 217 | } |
| 218 | let marker_name = if let Some(marker_name) = marker_name { |
| 219 | marker_name |
| 220 | } else { |
| 221 | return Err(parse::Error::new( |
| 222 | input.span(), |
| 223 | "marker() must contain a marker!" , |
| 224 | )); |
| 225 | }; |
| 226 | |
| 227 | Ok(Self { |
| 228 | marker_name, |
| 229 | key_lit, |
| 230 | fallback_by, |
| 231 | extension_key, |
| 232 | fallback_supplement, |
| 233 | singleton, |
| 234 | }) |
| 235 | } else { |
| 236 | let mut this = DataStructArg::new(path); |
| 237 | let lookahead = input.lookahead1(); |
| 238 | if lookahead.peek(Token![=]) { |
| 239 | let _t: Token![=] = input.parse()?; |
| 240 | let lit: LitStr = input.parse()?; |
| 241 | this.key_lit = Some(lit); |
| 242 | Ok(this) |
| 243 | } else { |
| 244 | Ok(this) |
| 245 | } |
| 246 | } |
| 247 | } |
| 248 | } |
| 249 | |
| 250 | /// A single argument to `marker()` in `#[data_struct(..., marker(...), ...)] |
| 251 | enum DataStructMarkerArg { |
| 252 | Path(Path), |
| 253 | NameValue(Ident, LitStr), |
| 254 | Lit(LitStr), |
| 255 | Singleton, |
| 256 | } |
| 257 | impl Parse for DataStructMarkerArg { |
| 258 | fn parse(input: ParseStream<'_>) -> parse::Result<Self> { |
| 259 | let lookahead = input.lookahead1(); |
| 260 | if lookahead.peek(LitStr) { |
| 261 | Ok(DataStructMarkerArg::Lit(input.parse()?)) |
| 262 | } else { |
| 263 | let path: Path = input.parse()?; |
| 264 | let lookahead = input.lookahead1(); |
| 265 | if lookahead.peek(Token![=]) { |
| 266 | let _tok: Token![=] = input.parse()?; |
| 267 | let ident = path.get_ident().ok_or_else(|| { |
| 268 | parse::Error::new(path.span(), "Expected identifier before `=`, found path" ) |
| 269 | })?; |
| 270 | Ok(DataStructMarkerArg::NameValue( |
| 271 | ident.clone(), |
| 272 | input.parse()?, |
| 273 | )) |
| 274 | } else if path.is_ident("singleton" ) { |
| 275 | Ok(DataStructMarkerArg::Singleton) |
| 276 | } else { |
| 277 | Ok(DataStructMarkerArg::Path(path)) |
| 278 | } |
| 279 | } |
| 280 | } |
| 281 | } |
| 282 | |
| 283 | fn data_struct_impl(attr: DataStructArgs, input: DeriveInput) -> TokenStream2 { |
| 284 | if input.generics.type_params().count() > 0 { |
| 285 | return syn::Error::new( |
| 286 | input.generics.span(), |
| 287 | "#[data_struct] does not support type parameters" , |
| 288 | ) |
| 289 | .to_compile_error(); |
| 290 | } |
| 291 | let lifetimes = input.generics.lifetimes().collect::<Vec<_>>(); |
| 292 | |
| 293 | let name = &input.ident; |
| 294 | |
| 295 | let name_with_lt = if !lifetimes.is_empty() { |
| 296 | quote!(#name<'static>) |
| 297 | } else { |
| 298 | quote!(#name) |
| 299 | }; |
| 300 | |
| 301 | if lifetimes.len() > 1 { |
| 302 | return syn::Error::new( |
| 303 | input.generics.span(), |
| 304 | "#[data_struct] does not support more than one lifetime parameter" , |
| 305 | ) |
| 306 | .to_compile_error(); |
| 307 | } |
| 308 | |
| 309 | let bake_derive = input |
| 310 | .attrs |
| 311 | .iter() |
| 312 | .find(|a| a.path().is_ident("databake" )) |
| 313 | .map(|a| { |
| 314 | quote! { |
| 315 | #[derive(databake::Bake)] |
| 316 | #a |
| 317 | } |
| 318 | }) |
| 319 | .unwrap_or_else(|| quote! {}); |
| 320 | |
| 321 | let mut result = TokenStream2::new(); |
| 322 | |
| 323 | for single_attr in attr.args { |
| 324 | let DataStructArg { |
| 325 | marker_name, |
| 326 | key_lit, |
| 327 | fallback_by, |
| 328 | extension_key, |
| 329 | fallback_supplement, |
| 330 | singleton, |
| 331 | } = single_attr; |
| 332 | |
| 333 | let docs = if let Some(ref key_lit) = key_lit { |
| 334 | let fallback_by_docs_str = match fallback_by { |
| 335 | Some(ref fallback_by) => fallback_by.value(), |
| 336 | None => "language (default)" .to_string(), |
| 337 | }; |
| 338 | let extension_key_docs_str = match extension_key { |
| 339 | Some(ref extension_key) => extension_key.value(), |
| 340 | None => "none (default)" .to_string(), |
| 341 | }; |
| 342 | format!("Marker type for [` {}`]: \"{}\"\n\n- Fallback priority: {}\n- Extension keyword: {}" , name, key_lit.value(), fallback_by_docs_str, extension_key_docs_str) |
| 343 | } else { |
| 344 | format!("Marker type for [` {name}`]" ) |
| 345 | }; |
| 346 | |
| 347 | result.extend(quote!( |
| 348 | #[doc = #docs] |
| 349 | #bake_derive |
| 350 | pub struct #marker_name; |
| 351 | impl icu_provider::DataMarker for #marker_name { |
| 352 | type Yokeable = #name_with_lt; |
| 353 | } |
| 354 | )); |
| 355 | |
| 356 | if let Some(key_lit) = key_lit { |
| 357 | let key_str = key_lit.value(); |
| 358 | let fallback_by_expr = if let Some(fallback_by_lit) = fallback_by { |
| 359 | match fallback_by_lit.value().as_str() { |
| 360 | "region" => { |
| 361 | quote! {icu_provider::_internal::LocaleFallbackPriority::Region} |
| 362 | } |
| 363 | "collation" => { |
| 364 | quote! {icu_provider::_internal::LocaleFallbackPriority::Collation} |
| 365 | } |
| 366 | "language" => { |
| 367 | quote! {icu_provider::_internal::LocaleFallbackPriority::Language} |
| 368 | } |
| 369 | _ => panic!("Invalid value for fallback_by" ), |
| 370 | } |
| 371 | } else { |
| 372 | quote! {icu_provider::_internal::LocaleFallbackPriority::const_default()} |
| 373 | }; |
| 374 | let extension_key_expr = if let Some(extension_key_lit) = extension_key { |
| 375 | quote! {Some(icu_provider::_internal::locid::extensions::unicode::key!(#extension_key_lit))} |
| 376 | } else { |
| 377 | quote! {None} |
| 378 | }; |
| 379 | let fallback_supplement_expr = if let Some(fallback_supplement_lit) = |
| 380 | fallback_supplement |
| 381 | { |
| 382 | match fallback_supplement_lit.value().as_str() { |
| 383 | "collation" => { |
| 384 | quote! {Some(icu_provider::_internal::LocaleFallbackSupplement::Collation)} |
| 385 | } |
| 386 | _ => panic!("Invalid value for fallback_supplement" ), |
| 387 | } |
| 388 | } else { |
| 389 | quote! {None} |
| 390 | }; |
| 391 | result.extend(quote!( |
| 392 | impl icu_provider::KeyedDataMarker for #marker_name { |
| 393 | const KEY: icu_provider::DataKey = icu_provider::data_key!(#key_str, icu_provider::DataKeyMetadata::construct_internal( |
| 394 | #fallback_by_expr, |
| 395 | #extension_key_expr, |
| 396 | #fallback_supplement_expr, |
| 397 | #singleton, |
| 398 | )); |
| 399 | } |
| 400 | )); |
| 401 | } |
| 402 | } |
| 403 | |
| 404 | result.extend(quote!( |
| 405 | #[derive(icu_provider::prelude::yoke::Yokeable, icu_provider::prelude::zerofrom::ZeroFrom)] |
| 406 | #input |
| 407 | )); |
| 408 | |
| 409 | result |
| 410 | } |
| 411 | |