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}