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//! Custom derives for `ZeroFrom` from the `zerofrom` crate.
6
7// https://github.com/unicode-org/icu4x/blob/main/docs/process/boilerplate.md#library-annotations
8#![cfg_attr(
9 not(test),
10 deny(
11 clippy::indexing_slicing,
12 clippy::unwrap_used,
13 clippy::expect_used,
14 clippy::panic,
15 clippy::exhaustive_structs,
16 clippy::exhaustive_enums,
17 missing_debug_implementations,
18 )
19)]
20
21use core::mem;
22use proc_macro::TokenStream;
23use proc_macro2::{Span, TokenStream as TokenStream2};
24use quote::quote;
25use std::collections::{HashMap, HashSet};
26use syn::fold::{self, Fold};
27use syn::punctuated::Punctuated;
28use syn::spanned::Spanned;
29use syn::{
30 parse_macro_input, parse_quote, DeriveInput, Ident, Lifetime, MetaList, Token, Type, TypePath,
31 WherePredicate,
32};
33use synstructure::Structure;
34mod visitor;
35
36/// Custom derive for `zerofrom::ZeroFrom`,
37///
38/// This implements `ZeroFrom<Ty> for Ty` for types
39/// without a lifetime parameter, and `ZeroFrom<Ty<'data>> for Ty<'static>`
40/// for types with a lifetime parameter.
41///
42/// Apply the `#[zerofrom(clone)]` attribute to a field if it doesn't implement
43/// Copy or ZeroFrom; this data will be cloned when the struct is zero_from'ed.
44///
45/// Apply the `#[zerofrom(maybe_borrow(T, U, V))]` attribute to the struct to indicate
46/// that certain type parameters may themselves contain borrows (by default
47/// the derives assume that type parameters perform no borrows and can be copied or cloned).
48///
49/// In rust versions where [this issue](https://github.com/rust-lang/rust/issues/114393) is fixed,
50/// `#[zerofrom(may_borrow)]` can be applied directly to type parameters.
51#[proc_macro_derive(ZeroFrom, attributes(zerofrom))]
52pub fn zf_derive(input: TokenStream) -> TokenStream {
53 let input: DeriveInput = parse_macro_input!(input as DeriveInput);
54 TokenStream::from(zf_derive_impl(&input))
55}
56
57fn has_attr(attrs: &[syn::Attribute], name: &str) -> bool {
58 attrs.iter().any(|a: &Attribute| {
59 if let Ok(i: Ident) = a.parse_args::<Ident>() {
60 if i == name {
61 return true;
62 }
63 }
64 false
65 })
66}
67
68// Collects all idents from #[zerofrom(may_borrow(A, B, C, D))]
69// needed since #[zerofrom(may_borrow)] doesn't work yet
70// (https://github.com/rust-lang/rust/issues/114393)
71fn get_may_borrow_attr(attrs: &[syn::Attribute]) -> Result<HashSet<Ident>, Span> {
72 let mut params: HashSet = HashSet::new();
73 for attr: &Attribute in attrs {
74 if let Ok(list: MetaList) = attr.parse_args::<MetaList>() {
75 if list.path.is_ident("may_borrow") {
76 if let Ok(list: Punctuated) =
77 list.parse_args_with(parser:Punctuated::<Ident, Token![,]>::parse_terminated)
78 {
79 params.extend(iter:list)
80 } else {
81 return Err(attr.span());
82 }
83 }
84 }
85 }
86 Ok(params)
87}
88
89fn zf_derive_impl(input: &DeriveInput) -> TokenStream2 {
90 let mut tybounds = input
91 .generics
92 .type_params()
93 .map(|ty| {
94 // Strip out param defaults, we don't need them in the impl
95 let mut ty = ty.clone();
96 ty.eq_token = None;
97 ty.default = None;
98 ty
99 })
100 .collect::<Vec<_>>();
101 let typarams = tybounds
102 .iter()
103 .map(|ty| ty.ident.clone())
104 .collect::<Vec<_>>();
105 let lts = input.generics.lifetimes().count();
106 let name = &input.ident;
107 let structure = Structure::new(input);
108
109 let may_borrow_attrs = match get_may_borrow_attr(&input.attrs) {
110 Ok(mb) => mb,
111 Err(span) => {
112 return syn::Error::new(
113 span,
114 "#[zerofrom(may_borrow)] on the struct takes in a comma separated list of type parameters, like so: `#[zerofrom(may_borrow(A, B, C, D)]`",
115 ).to_compile_error();
116 }
117 };
118
119 // This contains every generic type introduced in this code.
120 // If the gneeric type is may_borrow, this additionally contains the identifier corresponding to
121 // a newly introduced mirror type parameter that we are borrowing from, similar to C in the original trait.
122 // For convenience, we are calling these "C types"
123 let generics_env: HashMap<Ident, Option<Ident>> = tybounds
124 .iter()
125 .map(|param| {
126 // First one doesn't work yet https://github.com/rust-lang/rust/issues/114393
127 let maybe_new_param = if has_attr(&param.attrs, "may_borrow")
128 || may_borrow_attrs.contains(&param.ident)
129 {
130 Some(Ident::new(
131 &format!("{}ZFParamC", param.ident),
132 param.ident.span(),
133 ))
134 } else {
135 None
136 };
137 (param.ident.clone(), maybe_new_param)
138 })
139 .collect();
140
141 // Do any of the generics potentially borrow?
142 let generics_may_borrow = generics_env.values().any(|x| x.is_some());
143
144 if lts == 0 && !generics_may_borrow {
145 let has_clone = structure
146 .variants()
147 .iter()
148 .flat_map(|variant| variant.bindings().iter())
149 .any(|binding| has_attr(&binding.ast().attrs, "clone"));
150 let (clone, clone_trait) = if has_clone {
151 (quote!(this.clone()), quote!(Clone))
152 } else {
153 (quote!(*this), quote!(Copy))
154 };
155 let bounds: Vec<WherePredicate> = typarams
156 .iter()
157 .map(|ty| parse_quote!(#ty: #clone_trait + 'static))
158 .collect();
159 quote! {
160 impl<'zf, #(#tybounds),*> zerofrom::ZeroFrom<'zf, #name<#(#typarams),*>> for #name<#(#typarams),*> where #(#bounds),* {
161 fn zero_from(this: &'zf Self) -> Self {
162 #clone
163 }
164 }
165 }
166 } else {
167 if lts > 1 {
168 return syn::Error::new(
169 input.generics.span(),
170 "derive(ZeroFrom) cannot have multiple lifetime parameters",
171 )
172 .to_compile_error();
173 }
174
175 let mut zf_bounds: Vec<WherePredicate> = vec![];
176 let body = structure.each_variant(|vi| {
177 vi.construct(|f, i| {
178 let binding = format!("__binding_{i}");
179 let field = Ident::new(&binding, Span::call_site());
180
181 if has_attr(&f.attrs, "clone") {
182 quote! {
183 #field.clone()
184 }
185 } else {
186 // the field type
187 let fty = replace_lifetime(&f.ty, custom_lt("'zf"));
188 // the corresponding lifetimey type we are borrowing from (effectively, the C type)
189 let lifetime_ty =
190 replace_lifetime_and_type(&f.ty, custom_lt("'zf_inner"), &generics_env);
191
192 let (has_ty, has_lt) = visitor::check_type_for_parameters(&f.ty, &generics_env);
193 if has_ty {
194 // For types without type parameters, the compiler can figure out that the field implements
195 // ZeroFrom on its own. However, if there are type parameters, there may be complex preconditions
196 // to `FieldTy: ZeroFrom` that need to be satisfied. We get them to be satisfied by requiring
197 // `FieldTy<'zf>: ZeroFrom<'zf, FieldTy<'zf_inner>>`
198 if has_lt {
199 zf_bounds
200 .push(parse_quote!(#fty: zerofrom::ZeroFrom<'zf, #lifetime_ty>));
201 } else {
202 zf_bounds.push(parse_quote!(#fty: zerofrom::ZeroFrom<'zf, #fty>));
203 }
204 }
205 if has_ty || has_lt {
206 // By doing this we essentially require ZF to be implemented
207 // on all fields
208 quote! {
209 <#fty as zerofrom::ZeroFrom<'zf, #lifetime_ty>>::zero_from(#field)
210 }
211 } else {
212 // No lifetimes, so we can just copy
213 quote! { *#field }
214 }
215 }
216 })
217 });
218 // Due to the possibility of generics_may_borrow, we might reach here with no lifetimes on self,
219 // don't accidentally feed them to self later
220 let (maybe_zf_lifetime, maybe_zf_inner_lifetime) = if lts == 0 {
221 (quote!(), quote!())
222 } else {
223 (quote!('zf,), quote!('zf_inner,))
224 };
225
226 // Array of C types. Only different if generics are allowed to borrow
227 let mut typarams_c = typarams.clone();
228
229 if generics_may_borrow {
230 for typaram_c in &mut typarams_c {
231 if let Some(Some(replacement)) = generics_env.get(typaram_c) {
232 // we use mem::replace here so we can be really clear about the C vs the T type
233 let typaram_t = mem::replace(typaram_c, replacement.clone());
234 zf_bounds
235 .push(parse_quote!(#typaram_c: zerofrom::ZeroFrom<'zf_inner, #typaram_t>));
236 tybounds.push(parse_quote!(#typaram_c));
237 }
238 }
239 }
240
241 quote! {
242 impl<'zf, 'zf_inner, #(#tybounds),*> zerofrom::ZeroFrom<'zf, #name<#maybe_zf_inner_lifetime #(#typarams_c),*>> for #name<#maybe_zf_lifetime #(#typarams),*>
243 where
244 #(#zf_bounds,)* {
245 fn zero_from(this: &'zf #name<#maybe_zf_inner_lifetime #(#typarams_c),*>) -> Self {
246 match *this { #body }
247 }
248 }
249 }
250 }
251}
252
253fn custom_lt(s: &str) -> Lifetime {
254 Lifetime::new(symbol:s, Span::call_site())
255}
256
257/// Replace all lifetimes in a type with a specified one
258fn replace_lifetime(x: &Type, lt: Lifetime) -> Type {
259 struct ReplaceLifetime(Lifetime);
260
261 impl Fold for ReplaceLifetime {
262 fn fold_lifetime(&mut self, _: Lifetime) -> Lifetime {
263 self.0.clone()
264 }
265 }
266 ReplaceLifetime(lt).fold_type(x.clone())
267}
268
269/// Replace all lifetimes in a type with a specified one, AND replace all types that have a corresponding C type
270/// with the C type
271fn replace_lifetime_and_type(
272 x: &Type,
273 lt: Lifetime,
274 generics_env: &HashMap<Ident, Option<Ident>>,
275) -> Type {
276 struct ReplaceLifetimeAndTy<'a>(Lifetime, &'a HashMap<Ident, Option<Ident>>);
277
278 impl Fold for ReplaceLifetimeAndTy<'_> {
279 fn fold_lifetime(&mut self, _: Lifetime) -> Lifetime {
280 self.0.clone()
281 }
282 fn fold_type_path(&mut self, i: TypePath) -> TypePath {
283 if i.qself.is_none() {
284 if let Some(ident: &Ident) = i.path.get_ident() {
285 if let Some(Some(replacement: &Ident)) = self.1.get(ident) {
286 return parse_quote!(#replacement);
287 }
288 }
289 }
290 fold::fold_type_path(self, node:i)
291 }
292 }
293 ReplaceLifetimeAndTy(lt, generics_env).fold_type(x.clone())
294}
295