use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, format_ident};
use syn::{
parse_macro_input, FnArg, Ident, ItemFn, Lit, Meta, PatType, Type,
parse_quote, LitStr, Expr, ImplItemFn, Visibility,
};
#[proc_macro_attribute]
pub fn default(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}
struct ArgInfo {
name: Ident,
ty: Type,
default_expr: Option<String>,
}
#[proc_macro_attribute]
pub fn autoargs(_attr: TokenStream, item: TokenStream) -> TokenStream {
if let Ok(impl_method) = syn::parse::<ImplItemFn>(item.clone()) {
let _args_struct = generate_args_struct(&impl_method.sig.ident, &impl_method.sig.inputs, &impl_method.vis);
let self_param = impl_method.sig.inputs.iter().find_map(|arg| {
if let FnArg::Receiver(receiver) = arg {
Some(receiver)
} else {
None
}
});
if let Some(_self_param) = self_param {
let fn_args = parse_fn_args(&impl_method.sig.inputs);
let method_name = &impl_method.sig.ident;
let method_vis = &impl_method.vis;
let return_type = &impl_method.sig.output;
let method_body = &impl_method.block;
let method_name_str = method_name.to_string();
let camel_case_name = to_camel_case(&method_name_str);
let args_struct_name = format_ident!("{}Args", camel_case_name);
let arg_names: Vec<_> = fn_args.iter().map(|arg| &arg.name).collect();
let modified_method = quote! {
#[allow(unused_parens)]
#method_vis fn #method_name(#self_param, args: #args_struct_name) #return_type {
let (#(#arg_names),*) = (#(args.#arg_names),*);
#method_body
}
};
return TokenStream::from(modified_method);
}
}
let input_fn = parse_macro_input!(item as ItemFn);
let fn_name = &input_fn.sig.ident;
let fn_args = parse_fn_args(&input_fn.sig.inputs);
let return_type = &input_fn.sig.output;
let fn_visibility = &input_fn.vis;
let fn_name_str = fn_name.to_string();
let camel_case_name = to_camel_case(&fn_name_str);
let args_struct_name = format_ident!("{}Args", camel_case_name);
let struct_fields = fn_args.iter().map(|arg| {
let name = &arg.name;
let ty = &arg.ty;
quote! { pub #name: #ty }
});
let default_fields = fn_args.iter().map(|arg| {
let name = &arg.name;
let default_expr = match &arg.default_expr {
Some(expr) => tokenstream_from_str(expr).unwrap_or_else(|_| {
let expr_lit = LitStr::new(expr, proc_macro2::Span::call_site());
parse_quote! { #expr_lit.parse().unwrap() }
}),
None => parse_quote! { Default::default() },
};
quote! { #name: #default_expr }
});
let macro_arms = generate_function_macro_arms(&args_struct_name, fn_name);
let fn_body = &input_fn.block;
let arg_names: Vec<_> = fn_args.iter().map(|arg| &arg.name).collect();
let expanded = quote! {
#fn_visibility struct #args_struct_name {
#(#struct_fields),*
}
impl Default for #args_struct_name {
fn default() -> Self {
Self {
#(#default_fields),*
}
}
}
#[allow(unused_parens)]
#fn_visibility fn #fn_name(args: #args_struct_name) #return_type {
let (#(#arg_names),*) = (#(args.#arg_names),*);
#fn_body
}
#[macro_export]
macro_rules! #fn_name {
#(#macro_arms)*
}
};
TokenStream::from(expanded)
}
#[proc_macro_attribute]
pub fn impl_autoargs(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as syn::ItemImpl);
let _self_ty = &input.self_ty; let mut method_macros = vec![];
let mut method_args_structs = vec![];
for item in &input.items {
if let syn::ImplItem::Fn(method) = item {
let method_name = &method.sig.ident;
let has_autoargs = method.attrs.iter().any(|attr| {
attr.path().segments.iter().any(|seg| seg.ident == "autoargs")
});
if has_autoargs {
let method_vis = &method.vis;
let camel_case_name = to_camel_case(&method_name.to_string());
let args_struct_name = format_ident!("{}Args", camel_case_name);
let args_struct = generate_args_struct(&method_name, &method.sig.inputs, method_vis);
method_args_structs.push(args_struct);
let self_param = method.sig.inputs.iter().find_map(|arg| {
if let FnArg::Receiver(receiver) = arg {
Some(receiver)
} else {
None
}
});
if let Some(_self_param) = self_param {
let macro_rules = generate_method_macro_arms(&args_struct_name, method_name);
let method_macro = quote! {
#[macro_export]
macro_rules! #method_name {
#(#macro_rules)*
}
};
method_macros.push(method_macro);
}
}
}
}
let result = quote! {
#(#method_args_structs)*
#(#method_macros)*
#input
};
TokenStream::from(result)
}
fn generate_args_struct(method_name: &Ident, inputs: &syn::punctuated::Punctuated<FnArg, syn::token::Comma>, visibility: &Visibility) -> TokenStream2 {
let fn_args = parse_fn_args(inputs);
let method_name_str = method_name.to_string();
let camel_case_name = to_camel_case(&method_name_str);
let args_struct_name = format_ident!("{}Args", camel_case_name);
let struct_fields = fn_args.iter().map(|arg| {
let name = &arg.name;
let ty = &arg.ty;
quote! { pub #name: #ty }
});
let default_fields = fn_args.iter().map(|arg| {
let name = &arg.name;
let default_expr = match &arg.default_expr {
Some(expr) => tokenstream_from_str(expr).unwrap_or_else(|_| {
let expr_lit = LitStr::new(expr, proc_macro2::Span::call_site());
parse_quote! { #expr_lit.parse().unwrap() }
}),
None => parse_quote! { Default::default() },
};
quote! { #name: #default_expr }
});
quote! {
#visibility struct #args_struct_name {
#(#struct_fields),*
}
impl Default for #args_struct_name {
fn default() -> Self {
Self {
#(#default_fields),*
}
}
}
}
}
fn parse_fn_args(inputs: &syn::punctuated::Punctuated<FnArg, syn::token::Comma>) -> Vec<ArgInfo> {
inputs
.iter()
.filter_map(|arg| {
match arg {
FnArg::Receiver(_) => None,
FnArg::Typed(PatType { pat, ty, attrs, .. }) => {
if let syn::Pat::Ident(pat_ident) = &**pat {
let name = pat_ident.ident.clone();
if name.to_string() == "self" {
return None;
}
let default_expr = attrs.iter().find_map(|attr| {
if attr.path().segments.iter().any(|seg| seg.ident == "default") {
match &attr.meta {
Meta::NameValue(meta) => {
if let Expr::Lit(expr_lit) = &meta.value {
if let Lit::Str(lit) = &expr_lit.lit {
return Some(lit.value());
}
}
},
_ => {}
}
}
None
});
return Some(ArgInfo {
name,
ty: (**ty).clone(),
default_expr,
});
}
None
}
}
})
.collect()
}
fn generate_function_macro_arms(
args_struct_name: &Ident,
fn_name: &Ident,
) -> Vec<TokenStream2> {
let base_case = quote! {
() => {
#fn_name(#args_struct_name::default())
};
};
let named_args_case = quote! {
( $( $name:ident = $value:expr ),* $(,)? ) => {
{
let mut args = #args_struct_name::default();
$(
args.$name = $value;
)*
#fn_name(args)
}
};
};
let struct_case = quote! {
( $args:expr ) => {
#fn_name($args)
};
};
vec![base_case, named_args_case, struct_case]
}
fn generate_method_macro_arms(
args_struct_name: &Ident,
method_name: &Ident,
) -> Vec<TokenStream2> {
let base_case = quote! {
($self:expr) => {
$self.#method_name(#args_struct_name::default())
};
};
let named_args_case = quote! {
($self:expr, $( $name:ident = $value:expr ),* $(,)? ) => {
{
let mut args = #args_struct_name::default();
$(
args.$name = $value;
)*
$self.#method_name(args)
}
};
};
let struct_case = quote! {
($self:expr, $args:expr) => {
$self.#method_name($args)
};
};
vec![base_case, named_args_case, struct_case]
}
fn to_camel_case(input: &str) -> String {
input.split('_')
.map(|part| {
let mut chars = part.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().chain(chars).collect(),
}
})
.collect()
}
fn tokenstream_from_str(s: &str) -> Result<TokenStream2, proc_macro2::LexError> {
s.parse()
}