use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, visit_mut::VisitMut, Expr, ItemFn, ReturnType, Stmt};
struct ReturnVisitor {
errors: Vec<syn::Error>,
}
impl ReturnVisitor {
fn new() -> Self {
Self { errors: Vec::new() }
}
}
impl VisitMut for ReturnVisitor {
fn visit_expr_mut(&mut self, expr: &mut Expr) {
match expr {
Expr::Return(return_expr) => {
if return_expr.expr.is_some() {
self.errors.push(syn::Error::new_spanned(
return_expr.expr.clone(),
"chainable functions should not have explicit return values",
));
}
return_expr.expr = Some(Box::new(syn::parse_quote!(self)));
}
_ => {
syn::visit_mut::visit_expr_mut(self, expr);
}
}
}
}
pub fn make_function_chainable(_attr: TokenStream, item: TokenStream) -> TokenStream {
let mut input_fn = parse_macro_input!(item as ItemFn);
if input_fn.sig.inputs.is_empty() {
let error = syn::Error::new_spanned(
&input_fn.sig.ident,
"chainable functions must have at least one argument",
)
.to_compile_error();
return quote! {
#error
#input_fn
}
.into();
}
let (is_ref, is_mut_self) = input_fn
.sig
.inputs
.first()
.map(|arg| match arg {
syn::FnArg::Receiver(recv) => (recv.reference.is_some(), recv.mutability.is_some()),
_ => (false, false),
})
.unwrap();
if !is_mut_self {
let error = syn::Error::new_spanned(
&input_fn.sig.inputs[0],
"expected '&mut self' or 'mut self'",
)
.to_compile_error();
return quote! {
#error
#input_fn
}
.into();
}
match &input_fn.sig.output {
ReturnType::Type(_, _) => {
let error = syn::Error::new_spanned(
&input_fn.sig.output,
"chainable functions should not have explicit return types",
)
.to_compile_error();
return quote! {
#error
#input_fn
}
.into();
}
ReturnType::Default => {} }
input_fn.sig.output = if is_ref {
syn::parse_quote!(-> &mut Self)
} else {
syn::parse_quote!(-> Self)
};
let mut visitor = ReturnVisitor::new();
visitor.visit_block_mut(&mut input_fn.block);
if !visitor.errors.is_empty() {
let error = visitor.errors[0].to_compile_error();
return quote! {
#error
#input_fn
}
.into();
}
if let Some(Stmt::Expr(Expr::Return(_), _)) = input_fn.block.stmts.last() {
} else {
let return_stmt: Stmt = syn::parse_quote!(return self;);
input_fn.block.stmts.push(return_stmt);
}
let result = quote! {
#input_fn
};
result.into()
}