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 | |
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, 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. |
107 | pub 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 | |
114 | pub(crate) struct DataStructArgs { |
115 | args: Punctuated<DataStructArg, Token![,]>, |
116 | } |
117 | |
118 | impl 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 | } |
124 | struct 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 | |
133 | impl 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 | |
146 | impl 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(...), ...)] |
252 | enum DataStructMarkerArg { |
253 | Path(Path), |
254 | NameValue(Ident, LitStr), |
255 | Lit(LitStr), |
256 | Singleton, |
257 | } |
258 | impl 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 | |
284 | fn 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 | |