Skip to main content

magic_args_derive/
lib.rs

1#![expect(missing_docs, clippy::panic)]
2
3extern crate proc_macro;
4
5use std::collections::HashSet;
6
7use proc_macro2::TokenStream;
8use quote::{ToTokens, quote};
9use syn::parse::{Parse, ParseStream};
10use syn::{Data, DeriveInput, Index};
11
12mod keyword {
13    syn::custom_keyword!(skip);
14}
15
16enum MagicArgsAttribute {
17    Skip,
18}
19
20impl Parse for MagicArgsAttribute {
21    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
22        let lookahead = input.lookahead1();
23
24        if lookahead.peek(keyword::skip) {
25            let _skip: keyword::skip = input.parse()?;
26
27            Ok(Self::Skip)
28        } else {
29            panic!("unknown attribute")
30        }
31    }
32}
33
34#[expect(clippy::missing_panics_doc)]
35#[proc_macro_derive(MagicArgs, attributes(magic_args))]
36pub fn magic_args_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
37    let item = syn::parse_macro_input!(input as DeriveInput);
38
39    let Data::Struct(data) = item.data else {
40        panic!("MagicArgs can only be derived on structs")
41    };
42
43    let item_name = item.ident.clone();
44    let (impl_generics, type_generics, where_clause) = item.generics.split_for_impl();
45
46    let mut output = TokenStream::new();
47
48    let mut types_seen = HashSet::new();
49
50    for (idx, field) in data
51        .fields
52        .into_iter()
53        .enumerate()
54        .map(|(idx, field)| (Index::from(idx), field))
55    {
56        let mut skip = false;
57
58        field
59            .attrs
60            .iter()
61            .map(|attr| attr.parse_args().expect("cannot parse args"))
62            .for_each(|attr: MagicArgsAttribute| match attr {
63                MagicArgsAttribute::Skip => skip = true,
64            });
65
66        if skip {
67            continue;
68        }
69
70        let field_type = field.ty;
71
72        let field_accessor = match field.ident {
73            Some(ident) => ident.to_token_stream(),
74            None => idx.to_token_stream(),
75        };
76
77        output.extend(quote! {
78            impl #impl_generics ::magic_args::Args<::magic_args::__private::Tagged<#field_type, #idx>> for #item_name #type_generics
79                #where_clause
80            {
81                #[inline]
82                fn get(&self) -> ::magic_args::__private::Tagged<#field_type, #idx> {
83                    ::magic_args::__private::Tagged(::core::clone::Clone::clone(&self.#field_accessor))
84                }
85            }
86        });
87
88        if !types_seen.insert(field_type) {
89            panic!("MagicArgs cannot contain two items of the same type");
90        }
91    }
92
93    quote! {
94        const _: () = {
95            #output
96        };
97    }
98    .into()
99}