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 | |