use std::fmt::Display;
use std::str::FromStr;
use proc_macro::TokenStream;
use quote::quote;
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::{parse_macro_input, Expr, Item, Meta, Token};
#[allow(clippy::test_attr_in_doctest)]
#[proc_macro_attribute]
pub fn fickle(attr: TokenStream, item: TokenStream) -> TokenStream {
let mut n_retries: Option<usize> = None;
let mut n_passes: Option<usize> = None;
let attrinp = parse_macro_input!(attr with Punctuated<Meta, Token![,]>::parse_terminated);
let attrspan = attrinp.span();
for a in attrinp {
match a {
Meta::NameValue(nv) => {
if nv.path.is_ident("retries") {
match get_int(nv.value) {
Ok(r) => n_retries = Some(r),
Err(err) => return err.into_compile_error().into(),
}
} else if nv.path.is_ident("passes") {
match get_int(nv.value) {
Ok(r) => n_passes = Some(r),
Err(err) => return err.into_compile_error().into(),
}
} else {
return syn::Error::new(nv.path.span(), "Unrecognized argument")
.into_compile_error()
.into();
}
}
_ => {
return syn::Error::new(a.span(), "Not a name value (`name = value`)")
.into_compile_error()
.into();
}
}
}
let fickle_obj = match (n_retries, n_passes) {
(Some(retries), Some(passes)) => {
if passes > retries + 1 {
return syn::Error::new(
attrspan,
format!(
"Requested a minimum of {} passes with a maximum of {} runs",
passes,
retries + 1
),
)
.into_compile_error()
.into();
}
quote! {
let fickle = fickle::Fickle::new(#retries, #passes).unwrap();
}
}
(Some(retries), None) => quote! {
let fickle = fickle::Fickle::new_retries(#retries).unwrap();
},
(None, Some(passes)) => {
if passes > 2 {
return syn::Error::new(
attrspan,
format!(
"Requested a minimum of {} passes with a maximum of {} runs",
passes, 2
),
)
.into_compile_error()
.into();
}
quote! {
let fickle = fickle::Fickle::new_passes(#passes).unwrap();
}
}
(None, None) => quote! {
let fickle = fickle::Fickle::default();
},
};
let iteminput = parse_macro_input!(item as Item);
let testfn = match iteminput {
Item::Fn(func) => func,
_ => {
return syn::Error::new(
iteminput.span(),
"Expected #[fickle] to annotate a function",
)
.into_compile_error()
.into();
}
};
let attrs = testfn.attrs;
let vis = testfn.vis;
let sig = testfn.sig;
let stmts = testfn.block.stmts;
let func_body = match &sig.output {
syn::ReturnType::Default => {
quote! {
{
#fickle_obj
let block: fn() -> () = || {
#(#stmts)*
};
fickle.run(block).unwrap();
}
}
}
syn::ReturnType::Type(_rarrow, typ) => match **typ {
syn::Type::Path(_) => {
quote! {
{
#fickle_obj
let block: fn() -> #typ = || {
#(#stmts)*
};
match fickle.run(block) {
Ok(res) => Ok(res),
Err(fails) => panic!("{}", fails)
}
}
}
}
_ => {
return syn::Error::new(typ.span(), "Not able to handle return type")
.into_compile_error()
.into();
}
},
};
let expanded = quote! {
#(#attrs)*
#vis #sig
#func_body
};
expanded.into()
}
fn get_int<T>(expr: Expr) -> syn::Result<T>
where
T: FromStr,
<T as FromStr>::Err: Display,
{
match expr {
Expr::Lit(lit) => match lit.lit {
syn::Lit::Int(litint) => match litint.base10_parse() {
Ok(r) => Ok(r),
Err(_) => Err(syn::Error::new(litint.span(), "Could not parse to a usize")),
},
_ => Err(syn::Error::new(lit.lit.span(), "Not an integer")),
},
_ => Err(syn::Error::new(expr.span(), "Not an literal")),
}
}