use crate::{body, helpers::add_test_attribute, theory};
use pm::TokenStream;
use quote::{quote, ToTokens};
use std::{iter::FromIterator, mem::replace};
use syn::{
parse::Error, parse_quote, punctuated::Punctuated, spanned::Spanned, visit_mut::VisitMut,
FnArg, ImplItem, ImplItemMethod, ItemFn, ItemImpl, Path, Type,
};
type FnArgs = Punctuated<syn::FnArg, syn::token::Comma>;
type CallArgs = Punctuated<syn::Pat, syn::token::Comma>;
pub fn generate(mut input: ItemImpl) -> Result<TokenStream, Error> {
assert_this_is_an_inherent_impl(&input)?;
let ty = input.self_ty.clone();
let mut output = TokenStream::new();
for func in input
.items
.iter_mut()
.map(|item| testable_function(item, &ty))
{
if let Some(func) = func? {
output.extend(func)
}
}
output.extend(input.into_token_stream());
Ok(output)
}
fn assert_this_is_an_inherent_impl(input: &ItemImpl) -> Result<(), Error> {
match &input.trait_ {
None => Ok(()),
Some((_, path, _)) => Err(Error::new(
path.span(),
"The `session` attribute must be used over an inherent implementation",
)),
}
}
fn testable_function(item: &mut ImplItem, ty: &Type) -> Result<Option<TokenStream>, Error> {
enum TypeTest {
Fact,
Theory,
}
impl TypeTest {
fn from_path(path: &Path) -> Option<TypeTest> {
if path.is_ident("fact") {
Some(TypeTest::Fact)
} else if path.is_ident("theory") {
Some(TypeTest::Theory)
} else {
None
}
}
}
fn is_test_method(
item: &mut syn::ImplItem,
) -> Result<Option<(TypeTest, &mut ImplItemMethod)>, Error> {
let out = if let ImplItem::Method(method) = item {
let test_attr = method
.attrs
.iter()
.position(|attr| TypeTest::from_path(&attr.path).is_some())
.map(|i| method.attrs.remove(i));
if test_attr.is_some() {
fn is_self_by_value(arg: &FnArg) -> bool {
if let FnArg::SelfValue(_) = arg {
true
} else {
false
}
}
match method.sig.decl.inputs.iter().next() {
Some(first_arg) if is_self_by_value(first_arg) => (),
_ => Err(Error::new(
method.sig.ident.span(),
"This method must take `Self` by value to be testable",
))?,
}
}
test_attr
.and_then(|attr| TypeTest::from_path(&attr.path))
.map(|tytest| (tytest, method))
} else {
None
};
Ok(out)
}
fn generate_test_func_from_method(
method: &mut ImplItemMethod,
ty: &Type,
) -> Result<ItemFn, Error> {
fn creates_the_call_args(params: &FnArgs) -> Result<CallArgs, Error> {
const ERRMSG: &str = "Expected a regular argument";
let args = params.iter().map(|fnarg| match fnarg {
FnArg::SelfRef(arg) => Err(Error::new(arg.span().into(), ERRMSG)),
FnArg::SelfValue(arg) => Err(Error::new(arg.span().into(), ERRMSG)),
FnArg::Inferred(arg) => Err(Error::new(arg.span().into(), ERRMSG)),
FnArg::Ignored(arg) => Err(Error::new(arg.span().into(), ERRMSG)),
FnArg::Captured(arg) => Ok(arg.pat.clone()),
});
args.collect()
}
let ident = &method.sig.ident;
let mut decl = method.sig.decl.clone();
decl.inputs = Punctuated::from_iter(decl.inputs.into_iter().skip(1));
add_test_attribute(&mut method.attrs);
let args = creates_the_call_args(&decl.inputs)?;
let func = ItemFn {
attrs: replace(&mut method.attrs, Default::default()),
vis: method.vis.clone(),
constness: None,
unsafety: None,
asyncness: None,
abi: None,
ident: ident.clone(),
decl: Box::new(decl),
block: Box::new(parse_quote! {{
#ty::default().#ident(#args)
}}),
};
body::Transform::new(None).visit_block_mut(&mut method.block);
Ok(func)
}
match is_test_method(item)? {
Some((TypeTest::Fact, method)) => {
generate_test_func_from_method(method, ty).map(|f| Some(f.into_token_stream()))
}
Some((TypeTest::Theory, method)) => theory::generate(
method.sig.ident.clone(),
&mut method.sig.decl,
&mut method.block,
&mut method.attrs,
Some(ty),
)
.map(|f| Some(f.into_token_stream())),
None => Ok(None),
}
}