zerofrom_derive/
lib.rs

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/documents/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,
31    TraitBoundModifier, Type, TypeParamBound, TypePath, 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 = 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| {
59        if let Ok(i) = 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::new();
73    for attr in attrs {
74        if let Ok(list) = attr.parse_args::<MetaList>() {
75            if list.path.is_ident("may_borrow") {
76                if let Ok(list) =
77                    list.parse_args_with(Punctuated::<Ident, Token![,]>::parse_terminated)
78                {
79                    params.extend(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_mut()
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                // Remove `?Sized`` bound because we need a param to be Sized in order to take a ZeroFrom of it.
131                // This only applies to fields marked as `may_borrow`.
132                let mut bounds = core::mem::take(&mut param.bounds);
133                while let Some(bound_pair) = bounds.pop() {
134                    let bound = bound_pair.into_value();
135                    if let TypeParamBound::Trait(ref trait_bound) = bound {
136                        if trait_bound.path.get_ident().map(|ident| ident == "Sized") == Some(true)
137                            && matches!(trait_bound.modifier, TraitBoundModifier::Maybe(_))
138                        {
139                            continue;
140                        }
141                    }
142                    param.bounds.push(bound);
143                }
144                Some(Ident::new(
145                    &format!("{}ZFParamC", param.ident),
146                    param.ident.span(),
147                ))
148            } else {
149                None
150            };
151            (param.ident.clone(), maybe_new_param)
152        })
153        .collect();
154
155    // Do any of the generics potentially borrow?
156    let generics_may_borrow = generics_env.values().any(|x| x.is_some());
157
158    if lts == 0 && !generics_may_borrow {
159        let has_clone = structure
160            .variants()
161            .iter()
162            .flat_map(|variant| variant.bindings().iter())
163            .any(|binding| has_attr(&binding.ast().attrs, "clone"));
164        let (clone, clone_trait) = if has_clone {
165            (quote!(this.clone()), quote!(Clone))
166        } else {
167            (quote!(*this), quote!(Copy))
168        };
169        let bounds: Vec<WherePredicate> = typarams
170            .iter()
171            .map(|ty| parse_quote!(#ty: #clone_trait + 'static))
172            .collect();
173        quote! {
174            impl<'zf, #(#tybounds),*> zerofrom::ZeroFrom<'zf, #name<#(#typarams),*>> for #name<#(#typarams),*> where #(#bounds),* {
175                fn zero_from(this: &'zf Self) -> Self {
176                    #clone
177                }
178            }
179        }
180    } else {
181        if lts > 1 {
182            return syn::Error::new(
183                input.generics.span(),
184                "derive(ZeroFrom) cannot have multiple lifetime parameters",
185            )
186            .to_compile_error();
187        }
188
189        let mut zf_bounds: Vec<WherePredicate> = vec![];
190        let body = structure.each_variant(|vi| {
191            vi.construct(|f, i| {
192                let binding = format!("__binding_{i}");
193                let field = Ident::new(&binding, Span::call_site());
194
195                if has_attr(&f.attrs, "clone") {
196                    quote! {
197                        #field.clone()
198                    }
199                } else {
200                    // the field type
201                    let fty = replace_lifetime(&f.ty, custom_lt("'zf"));
202                    // the corresponding lifetimey type we are borrowing from (effectively, the C type)
203                    let lifetime_ty =
204                        replace_lifetime_and_type(&f.ty, custom_lt("'zf_inner"), &generics_env);
205
206                    let (has_ty, has_lt) = visitor::check_type_for_parameters(&f.ty, &generics_env);
207                    if has_ty {
208                        // For types without type parameters, the compiler can figure out that the field implements
209                        // ZeroFrom on its own. However, if there are type parameters, there may be complex preconditions
210                        // to `FieldTy: ZeroFrom` that need to be satisfied. We get them to be satisfied by requiring
211                        // `FieldTy<'zf>: ZeroFrom<'zf, FieldTy<'zf_inner>>`
212                        if has_lt {
213                            zf_bounds
214                                .push(parse_quote!(#fty: zerofrom::ZeroFrom<'zf, #lifetime_ty>));
215                        } else {
216                            zf_bounds.push(parse_quote!(#fty: zerofrom::ZeroFrom<'zf, #fty>));
217                        }
218                    }
219                    if has_ty || has_lt {
220                        // By doing this we essentially require ZF to be implemented
221                        // on all fields
222                        quote! {
223                            <#fty as zerofrom::ZeroFrom<'zf, #lifetime_ty>>::zero_from(#field)
224                        }
225                    } else {
226                        // No lifetimes, so we can just copy
227                        quote! { *#field }
228                    }
229                }
230            })
231        });
232        // Due to the possibility of generics_may_borrow, we might reach here with no lifetimes on self,
233        // don't accidentally feed them to self later
234        let (maybe_zf_lifetime, maybe_zf_inner_lifetime) = if lts == 0 {
235            (quote!(), quote!())
236        } else {
237            (quote!('zf,), quote!('zf_inner,))
238        };
239
240        // Array of C types. Only different if generics are allowed to borrow
241        let mut typarams_c = typarams.clone();
242
243        if generics_may_borrow {
244            for typaram_c in &mut typarams_c {
245                if let Some(Some(replacement)) = generics_env.get(typaram_c) {
246                    // we use mem::replace here so we can be really clear about the C vs the T type
247                    let typaram_t = mem::replace(typaram_c, replacement.clone());
248                    zf_bounds
249                        .push(parse_quote!(#typaram_c: zerofrom::ZeroFrom<'zf_inner, #typaram_t>));
250                    tybounds.push(parse_quote!(#typaram_c));
251                }
252            }
253        }
254
255        quote! {
256            impl<'zf, 'zf_inner, #(#tybounds),*> zerofrom::ZeroFrom<'zf, #name<#maybe_zf_inner_lifetime #(#typarams_c),*>> for #name<#maybe_zf_lifetime #(#typarams),*>
257                where
258                #(#zf_bounds,)* {
259                fn zero_from(this: &'zf #name<#maybe_zf_inner_lifetime #(#typarams_c),*>) -> Self {
260                    match *this { #body }
261                }
262            }
263        }
264    }
265}
266
267fn custom_lt(s: &str) -> Lifetime {
268    Lifetime::new(s, Span::call_site())
269}
270
271/// Replace all lifetimes in a type with a specified one
272fn replace_lifetime(x: &Type, lt: Lifetime) -> Type {
273    struct ReplaceLifetime(Lifetime);
274
275    impl Fold for ReplaceLifetime {
276        fn fold_lifetime(&mut self, _: Lifetime) -> Lifetime {
277            self.0.clone()
278        }
279    }
280    ReplaceLifetime(lt).fold_type(x.clone())
281}
282
283/// Replace all lifetimes in a type with a specified one, AND replace all types that have a corresponding C type
284/// with the C type
285fn replace_lifetime_and_type(
286    x: &Type,
287    lt: Lifetime,
288    generics_env: &HashMap<Ident, Option<Ident>>,
289) -> Type {
290    struct ReplaceLifetimeAndTy<'a>(Lifetime, &'a HashMap<Ident, Option<Ident>>);
291
292    impl Fold for ReplaceLifetimeAndTy<'_> {
293        fn fold_lifetime(&mut self, _: Lifetime) -> Lifetime {
294            self.0.clone()
295        }
296        fn fold_type_path(&mut self, i: TypePath) -> TypePath {
297            if i.qself.is_none() {
298                if let Some(ident) = i.path.get_ident() {
299                    if let Some(Some(replacement)) = self.1.get(ident) {
300                        return parse_quote!(#replacement);
301                    }
302                }
303            }
304            fold::fold_type_path(self, i)
305        }
306    }
307    ReplaceLifetimeAndTy(lt, generics_env).fold_type(x.clone())
308}