Skip to main content

typesift_macros/
lib.rs

1use proc_macro2::TokenStream;
2use quote::{format_ident, quote};
3use syn::{
4    Attribute, Data, DataEnum, DeriveInput, Error, Field, Fields, Index, Member, Path,
5    parse_macro_input, parse_quote,
6};
7
8/// Derives `typesift::TypeSift`: the value itself is offered first, then each field is visited in
9/// declaration order.
10///
11/// Every type parameter gets a `TypeSift` bound. Types with lifetime parameters are rejected
12/// because `TypeSift` requires `'static` types, and unions because the active field is unknown.
13///
14/// Fields accept one argument, and none of them removes the bound on type parameters:
15///
16/// - `#[typesift(skip)]` leaves the field out of the traversal, so its type needs no impl.
17/// - `#[typesift(leaf)]` offers the field itself but does not look inside it, so its type needs
18///   only to be `'static`. This is how a type from another crate can still be found.
19/// - `#[typesift(with = path)]` hands the field to a function generic over `typesift::Sifter`,
20///   which decides what of it the search sees.
21#[proc_macro_derive(TypeSift, attributes(typesift))]
22pub fn derive_type_sift(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
23    let input = parse_macro_input!(input as DeriveInput);
24    expand(input)
25        .unwrap_or_else(Error::into_compile_error)
26        .into()
27}
28
29fn expand(mut input: DeriveInput) -> syn::Result<TokenStream> {
30    reject_typesift_attributes(&input.attrs)?;
31
32    if let Some(lifetime) = input.generics.lifetimes().next() {
33        return Err(Error::new_spanned(
34            lifetime,
35            "`TypeSift` requires `'static` types, so it cannot be derived for types with lifetime parameters",
36        ));
37    }
38
39    let body = match &input.data {
40        Data::Struct(data) => visit_struct(&data.fields)?,
41        Data::Enum(data) => visit_enum(data)?,
42        Data::Union(data) => {
43            return Err(Error::new_spanned(
44                data.union_token,
45                "`TypeSift` cannot be derived for unions",
46            ));
47        }
48    };
49
50    for param in input.generics.type_params_mut() {
51        param.bounds.push(parse_quote!(::typesift::TypeSift));
52    }
53
54    let name = &input.ident;
55    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
56
57    // Generic names are prefixed with `__` so they cannot clash with the type's own parameters.
58    Ok(quote! {
59        impl #impl_generics ::typesift::TypeSift for #name #ty_generics #where_clause {
60            fn visit<'__a, __T: 'static, __B, __F>(
61                &'__a self,
62                __visitor: &mut __F,
63            ) -> ::core::ops::ControlFlow<__B>
64            where
65                __F: ::core::ops::FnMut(&'__a __T) -> ::core::ops::ControlFlow<__B>,
66            {
67                ::typesift::TypeSift::visit_self::<__T, __B, __F>(self, __visitor)?;
68                #body
69            }
70        }
71    })
72}
73
74fn visit_struct(fields: &Fields) -> syn::Result<TokenStream> {
75    let mut visits = Vec::new();
76    for (index, field) in fields.iter().enumerate() {
77        let mode = field_mode(field)?;
78        if matches!(mode, FieldMode::Skip) {
79            continue;
80        }
81        let member = match &field.ident {
82            Some(ident) => Member::Named(ident.clone()),
83            None => Member::Unnamed(Index::from(index)),
84        };
85        visits.push(mode.apply(&quote!(&self.#member)));
86    }
87
88    Ok(quote! {
89        #(#visits)*
90        ::core::ops::ControlFlow::Continue(())
91    })
92}
93
94fn visit_enum(data: &DataEnum) -> syn::Result<TokenStream> {
95    if data.variants.is_empty() {
96        return Ok(quote!(match *self {}));
97    }
98
99    let mut arms = Vec::new();
100    for variant in &data.variants {
101        reject_typesift_attributes(&variant.attrs)?;
102
103        // Skipped fields are matched with `_`, so they get neither a binding nor a visit.
104        let mut patterns = Vec::new();
105        let mut visits = Vec::new();
106        for (index, field) in variant.fields.iter().enumerate() {
107            let mode = field_mode(field)?;
108            if matches!(mode, FieldMode::Skip) {
109                patterns.push(quote!(_));
110            } else {
111                let binding = format_ident!("__field{}", index);
112                visits.push(mode.apply(&quote!(#binding)));
113                patterns.push(quote!(#binding));
114            }
115        }
116
117        let variant_name = &variant.ident;
118        let pattern = match &variant.fields {
119            Fields::Named(fields) => {
120                let names = fields.named.iter().map(|field| &field.ident);
121                quote!(Self::#variant_name { #(#names: #patterns),* })
122            }
123            Fields::Unnamed(_) => quote!(Self::#variant_name(#(#patterns),*)),
124            Fields::Unit => quote!(Self::#variant_name),
125        };
126
127        arms.push(quote! {
128            #pattern => {
129                #(#visits)*
130                ::core::ops::ControlFlow::Continue(())
131            }
132        });
133    }
134
135    Ok(quote! {
136        match self {
137            #(#arms)*
138        }
139    })
140}
141
142/// What the derive does with one field.
143enum FieldMode {
144    /// Hand the field to its own `TypeSift` impl.
145    Visit,
146    /// Leave the field out entirely: `#[typesift(skip)]`.
147    Skip,
148    /// Offer the field without looking inside it: `#[typesift(leaf)]`.
149    Leaf,
150    /// Hand the field to a function of the user's: `#[typesift(with = path)]`.
151    With(Path),
152}
153
154impl FieldMode {
155    /// The argument that selected this mode, for error messages.
156    fn name(&self) -> &'static str {
157        match self {
158            FieldMode::Visit => "",
159            FieldMode::Skip => "skip",
160            FieldMode::Leaf => "leaf",
161            FieldMode::With(_) => "with",
162        }
163    }
164
165    /// The code that visits one field. `field` must evaluate to a reference that lives as long as
166    /// `self`.
167    fn apply(&self, field: &TokenStream) -> TokenStream {
168        match self {
169            FieldMode::Visit => quote! {
170                ::typesift::TypeSift::visit::<__T, __B, __F>(#field, __visitor)?;
171            },
172            // The field's type needs no impl here, only `'static`, so the type check that
173            // `TypeSift::visit_self` performs is inlined instead of called.
174            FieldMode::Leaf => quote! {
175                if let ::core::option::Option::Some(__matched) =
176                    (#field as &dyn ::core::any::Any).downcast_ref::<__T>()
177                {
178                    __visitor(__matched)?;
179                }
180            },
181            // `__visitor` is reborrowed, so later fields can still use it.
182            FieldMode::With(path) => quote! {
183                #path(
184                    #field,
185                    &mut ::typesift::Sift::<__T, __B, __F>::new(&mut *__visitor),
186                )?;
187            },
188            FieldMode::Skip => TokenStream::new(),
189        }
190    }
191}
192
193/// Reads the `typesift` attributes of one field.
194fn field_mode(field: &Field) -> syn::Result<FieldMode> {
195    let mut mode = FieldMode::Visit;
196    for attr in typesift_attributes(&field.attrs) {
197        attr.parse_nested_meta(|meta| {
198            let found = if meta.path.is_ident("skip") {
199                FieldMode::Skip
200            } else if meta.path.is_ident("leaf") {
201                FieldMode::Leaf
202            } else if meta.path.is_ident("with") {
203                FieldMode::With(meta.value()?.parse()?)
204            } else {
205                return Err(
206                    meta.error("unknown `typesift` argument, expected `skip`, `leaf` or `with`")
207                );
208            };
209
210            let conflict = match (&mode, &found) {
211                (FieldMode::Visit, _) => None,
212                (existing, found) if existing.name() == found.name() => {
213                    Some(format!("duplicate `{}`", found.name()))
214                }
215                (existing, found) => Some(format!(
216                    "`{}` and `{}` cannot be combined",
217                    existing.name(),
218                    found.name()
219                )),
220            };
221
222            match conflict {
223                Some(message) => Err(meta.error(message)),
224                None => {
225                    mode = found;
226                    Ok(())
227                }
228            }
229        })?;
230    }
231    Ok(mode)
232}
233
234/// `typesift` attributes only have a meaning on fields, so anywhere else they are an error.
235fn reject_typesift_attributes(attrs: &[Attribute]) -> syn::Result<()> {
236    match typesift_attributes(attrs).next() {
237        Some(attr) => Err(Error::new_spanned(
238            attr,
239            "`#[typesift(...)]` is only allowed on fields",
240        )),
241        None => Ok(()),
242    }
243}
244
245fn typesift_attributes(attrs: &[Attribute]) -> impl Iterator<Item = &Attribute> {
246    attrs.iter().filter(|attr| attr.path().is_ident("typesift"))
247}
248
249#[cfg(test)]
250mod tests {
251    use syn::{DeriveInput, parse_quote};
252
253    use super::expand;
254
255    fn error_message(input: DeriveInput) -> String {
256        expand(input)
257            .expect_err("the derive should fail")
258            .to_string()
259    }
260
261    fn generated_code(input: DeriveInput) -> String {
262        expand(input)
263            .expect("the derive should succeed")
264            .to_string()
265    }
266
267    #[test]
268    fn typesift_attribute_is_rejected_outside_fields() {
269        let on_struct = parse_quote! {
270            #[typesift(skip)]
271            struct Session {
272                user: u64,
273            }
274        };
275        assert_eq!(
276            error_message(on_struct),
277            "`#[typesift(...)]` is only allowed on fields"
278        );
279
280        let on_variant = parse_quote! {
281            enum Event {
282                #[typesift(leaf)]
283                Login(u64),
284            }
285        };
286        assert_eq!(
287            error_message(on_variant),
288            "`#[typesift(...)]` is only allowed on fields"
289        );
290    }
291
292    #[test]
293    fn arguments_are_known_and_appear_once() {
294        let unknown = parse_quote! {
295            struct Session {
296                #[typesift(rename)]
297                user: u64,
298            }
299        };
300        assert_eq!(
301            error_message(unknown),
302            "unknown `typesift` argument, expected `skip`, `leaf` or `with`"
303        );
304
305        let duplicate_skip = parse_quote! {
306            struct Session {
307                #[typesift(skip, skip)]
308                user: u64,
309            }
310        };
311        assert_eq!(error_message(duplicate_skip), "duplicate `skip`");
312
313        let duplicate_leaf = parse_quote! {
314            struct Session {
315                #[typesift(leaf)]
316                #[typesift(leaf)]
317                user: u64,
318            }
319        };
320        assert_eq!(error_message(duplicate_leaf), "duplicate `leaf`");
321
322        let combined = parse_quote! {
323            enum Event {
324                Login(#[typesift(skip, leaf)] u64),
325            }
326        };
327        assert_eq!(
328            error_message(combined),
329            "`skip` and `leaf` cannot be combined"
330        );
331
332        let without_arguments = parse_quote! {
333            struct Session {
334                #[typesift]
335                user: u64,
336            }
337        };
338        assert!(error_message(without_arguments).contains("expected attribute arguments"));
339    }
340
341    #[test]
342    fn skipped_fields_are_left_out_of_the_generated_code() {
343        let generated = generated_code(parse_quote! {
344            struct Session {
345                user: u64,
346                #[typesift(skip)]
347                cache: Cache,
348            }
349        });
350        assert!(generated.contains("user"));
351        assert!(!generated.contains("cache"));
352
353        let generated = generated_code(parse_quote! {
354            enum Event {
355                Login { user: u64, #[typesift(skip)] token: Token },
356            }
357        });
358        assert!(generated.contains("token : _"));
359    }
360
361    #[test]
362    fn leaf_fields_are_checked_by_type_instead_of_visited() {
363        let generated = generated_code(parse_quote! {
364            struct Invoice {
365                #[typesift(leaf)]
366                id: Uuid,
367                amount: u64,
368            }
369        });
370        assert!(generated.contains("downcast_ref"));
371        assert!(generated.contains("self . id"));
372        // The field is offered, never walked into.
373        assert!(!generated.contains("visit :: < __T , __B , __F > (& self . id"));
374        assert!(generated.contains("visit :: < __T , __B , __F > (& self . amount"));
375    }
376
377    #[test]
378    fn with_takes_a_path_and_no_other_argument() {
379        let duplicate = parse_quote! {
380            struct Request {
381                #[typesift(with = visit_headers, with = visit_headers)]
382                headers: Headers,
383            }
384        };
385        assert_eq!(error_message(duplicate), "duplicate `with`");
386
387        let combined = parse_quote! {
388            struct Request {
389                #[typesift(skip, with = visit_headers)]
390                headers: Headers,
391            }
392        };
393        assert_eq!(
394            error_message(combined),
395            "`skip` and `with` cannot be combined"
396        );
397
398        let without_a_path = parse_quote! {
399            struct Request {
400                #[typesift(with)]
401                headers: Headers,
402            }
403        };
404        assert!(error_message(without_a_path).contains("expected"));
405
406        let generated = generated_code(parse_quote! {
407            struct Request {
408                path: String,
409                #[typesift(with = visit_headers)]
410                headers: Headers,
411            }
412        });
413        assert!(generated.contains("visit_headers"));
414        assert!(generated.contains("Sift"));
415        assert!(generated.contains("visit :: < __T , __B , __F > (& self . path"));
416    }
417}