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