spectacle_derive/
lib.rs

1use proc_macro2::TokenStream;
2use proc_macro_error::{emit_error, proc_macro_error};
3use quote::{format_ident, quote};
4use std::borrow::Borrow;
5use syn::{
6    parse_macro_input, parse_quote, punctuated::Punctuated, spanned::Spanned, token::Comma,
7    DeriveInput, Fields, GenericParam, Generics, Ident, Index, Type, Variant,
8};
9
10#[proc_macro_derive(Spectacle)]
11#[proc_macro_error]
12pub fn derive_spectacle(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
13    let input = parse_macro_input!(input as DeriveInput);
14
15    let name = input.ident;
16    let generics = add_trait_bounds(input.generics);
17
18    let out = match input.data {
19        syn::Data::Struct(data) => impl_introspect_struct(&name, &generics, &data.fields),
20        syn::Data::Enum(data) => impl_introspect_enum(&name, &generics, &data.variants),
21        syn::Data::Union(_) => {
22            emit_error!(
23                name.span(),
24                "Spectacle can only be derived for structs and enums"
25            );
26
27            TokenStream::new()
28        }
29    };
30    // eprintln!("{}", out);
31    out.into()
32}
33
34// Add a bound `T: 'static + Introspect` to every type parameter T.
35fn add_trait_bounds(mut generics: Generics) -> Generics {
36    for param in &mut generics.params {
37        if let GenericParam::Type(ref mut type_param) = *param {
38            type_param.bounds.push(parse_quote!(spectacle::Introspect));
39            type_param.bounds.push(parse_quote!('static));
40        }
41    }
42    generics
43}
44
45// Create an unused generic identifier
46fn create_generic_ident(generics: &Generics) -> Ident {
47    let mut ident = Ident::new("F", generics.span());
48    let mut n: u8 = 0;
49    while generics.params.iter().any(|param| match param {
50        GenericParam::Type(param) => param.ident == ident,
51        GenericParam::Const(param) => param.ident == ident,
52        GenericParam::Lifetime(param) => param.lifetime.ident == ident,
53    }) {
54        ident = Ident::new(&format!("F{}", n), generics.span());
55        n += 1;
56        if n == std::u8::MAX {
57            emit_error!(
58                generics,
59                "could not generate an appropriate unused type parameter";
60                note = "#[derive(Spectacle)] must be able to generate an unused type parameter F or F{n} where n is in the u16 range";
61                help = "consider removing the type parameter F from your list of generic parameters";
62            );
63        }
64    }
65    ident
66}
67
68fn impl_introspect_struct(name: &Ident, generics: &Generics, fields: &Fields) -> TokenStream {
69    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
70    let f = create_generic_ident(&generics);
71    let field_names: Vec<_> = fields
72        .iter()
73        .enumerate()
74        .map(|(idx, field)| match field.ident {
75            Some(ref name) => quote!(self.#name),
76            None => {
77                let idx = Index::from(idx);
78                quote!(self.#idx)
79            }
80        })
81        .collect();
82    let recurse = recurse_fields(fields, |field_idx| field_names[field_idx].clone(), None)
83        .unwrap_or_default();
84
85    quote! {
86        impl #impl_generics spectacle::Introspect for #name #ty_generics #where_clause
87        {
88            fn introspect_from<#f>(&self, breadcrumbs: spectacle::Breadcrumbs, mut visit: #f)
89            where
90                #f: FnMut(&spectacle::Breadcrumbs, &dyn std::any::Any),
91            {
92                visit(&breadcrumbs, self);
93
94                #recurse
95            }
96        }
97    }
98}
99
100// TODO: more fine-grained control of field visibility somehow
101// for now, we'll visit all fields, even private ones
102fn recurse_fields<Accessor>(
103    fields: &Fields,
104    access: Accessor,
105    inject_breadcrumb: Option<TokenStream>,
106) -> Option<TokenStream>
107where
108    Accessor: Fn(usize) -> TokenStream,
109{
110    let inject_breadcrumb = inject_breadcrumb.unwrap_or_default();
111
112    match fields {
113        Fields::Unit => None,
114        Fields::Named(fields) => {
115            let recurse = fields.named.iter().enumerate().map(|(idx, field)| {
116                let name = field.ident.clone().expect("named fields have names");
117                let name_lit = syn::LitStr::new(&name.to_string(), field.span());
118                let field = access(idx);
119
120                quote! {{
121                    let mut breadcrumbs = breadcrumbs.clone();
122                    #inject_breadcrumb
123                    breadcrumbs.push_back(spectacle::Breadcrumb::Field(#name_lit));
124                    spectacle::Introspect::introspect_from(&#field, breadcrumbs, &mut visit);
125                }}
126            });
127
128            Some(quote! { #( #recurse )* })
129        }
130        Fields::Unnamed(fields) => {
131            let recurse = fields.unnamed.iter().enumerate().map(|(idx, _)| {
132                let field = access(idx);
133
134                quote! {{
135                    let mut breadcrumbs = breadcrumbs.clone();
136                    #inject_breadcrumb
137                    breadcrumbs.push_back(spectacle::Breadcrumb::TupleIndex(#idx));
138                    spectacle::Introspect::introspect_from(&#field, breadcrumbs, &mut visit);
139                }}
140            });
141
142            Some(quote! { #( #recurse )* })
143        }
144    }
145}
146
147fn impl_introspect_enum(
148    name: &Ident,
149    generics: &Generics,
150    variants: &Punctuated<Variant, Comma>,
151) -> TokenStream {
152    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
153    let f = create_generic_ident(&generics);
154    let recurse = recurse_variants(variants);
155
156    quote! {
157        impl #impl_generics spectacle::Introspect for #name #ty_generics #where_clause
158        {
159            fn introspect_from<#f>(&self, breadcrumbs: spectacle::Breadcrumbs, mut visit: #f)
160            where
161                #f: FnMut(&spectacle::Breadcrumbs, &dyn std::any::Any),
162            {
163                visit(&breadcrumbs, self);
164
165                match self {
166                    #( #recurse ),*
167                    _ => {}
168                }
169            }
170        }
171    }
172}
173
174// form an ident to refer to an unnamed type:
175// lowercase + append index
176fn type_var<T>(t: T, n: Option<usize>) -> Ident
177where
178    T: Borrow<Type> + Spanned,
179{
180    let ident = match t.borrow() {
181        Type::Array(t) => return format_ident!("{}s", type_var(t.elem.borrow(), n)),
182        Type::Slice(t) => return format_ident!("{}s", type_var(t.elem.borrow(), n)),
183        Type::Group(t) => return type_var(t.elem.borrow(), n),
184        Type::Paren(t) => return type_var(t.elem.borrow(), n),
185        Type::Ptr(t) => return type_var(t.elem.borrow(), n),
186        Type::Reference(t) => return type_var(t.elem.borrow(), n),
187        Type::Path(t) => t
188            .path
189            .segments
190            .last()
191            .expect("type paths should not be empty")
192            .ident
193            .clone(),
194        Type::Tuple(t) => {
195            let names = t
196                .elems
197                .iter()
198                .map(|tt| type_var(tt, None).to_string())
199                .collect::<Vec<_>>();
200            format_ident!("{}", names.join("_"))
201        }
202        _ => {
203            emit_error!(
204                t.span(),
205                "cannot create appropriate type variable for this type"
206            );
207            format_ident!("_{}", n.unwrap_or_default())
208        }
209    };
210    match n {
211        None => ident,
212        Some(n) => format_ident!("{}{}", ident.to_string().to_lowercase(), n),
213    }
214}
215
216fn recurse_variants(variants: &Punctuated<Variant, Comma>) -> Vec<TokenStream> {
217    variants
218        .iter()
219        .filter_map(|variant| {
220            if variant.fields.is_empty() {
221                return None;
222            }
223
224            let name = &variant.ident;
225            let field_name: Vec<_> = variant
226                .fields
227                .iter()
228                .enumerate()
229                .map(|(idx, field)| {
230                    let field_name = match field.ident {
231                        Some(ref id) => id.clone(),
232                        None => type_var(&field.ty, Some(idx)),
233                    };
234                    quote!(#field_name)
235                })
236                .collect();
237
238            let field_names = match variant.fields {
239                Fields::Named(_) => quote!({#( #field_name ),*}),
240                Fields::Unnamed(_) => quote!((#( #field_name ),*)),
241                _ => unreachable!(),
242            };
243            let variant_lit = syn::LitStr::new(&name.to_string(), name.span());
244            let recurse = recurse_fields(
245                &variant.fields,
246                |field_idx| field_name[field_idx].clone(),
247                Some(quote! {
248                    breadcrumbs.push_back(spectacle::Breadcrumb::Variant(#variant_lit));
249                }),
250            );
251
252            Some(quote! {
253                Self::#name #field_names => {#recurse}
254            })
255        })
256        .collect()
257}