use quote::ToTokens;
use syn::visit::Visit;
use syn::Attribute;
use syn::ItemFn;
use crate::mutate::{Mutation, MutationOp};
use crate::source::SourceFile;
pub struct DiscoveryVisitor<'sf> {
pub mutations: Vec<Mutation>,
source_file: &'sf SourceFile,
namespace_stack: Vec<String>,
}
impl<'sf> DiscoveryVisitor<'sf> {
pub fn new(source_file: &'sf SourceFile) -> DiscoveryVisitor<'sf> {
DiscoveryVisitor {
source_file,
mutations: Vec::new(),
namespace_stack: Vec::new(),
}
}
fn collect_fn_mutations(
&mut self,
ident: &syn::Ident,
return_type: &syn::ReturnType,
span: &proc_macro2::Span,
) {
self.in_namespace(&ident.to_string(), |v| {
let function_name = v.namespace_stack.join("::");
let return_type_str = format!("{}", return_type.to_token_stream());
for op in ops_for_return_type(return_type) {
v.mutations.push(Mutation::new(
v.source_file.clone(),
op,
function_name.clone(),
return_type_str.clone(),
span.into(),
))
}
});
}
fn in_namespace<F, T>(&mut self, name: &str, f: F) -> T
where
F: FnOnce(&mut Self) -> T,
{
self.namespace_stack.push(name.to_owned());
let r = f(self);
assert_eq!(self.namespace_stack.pop().unwrap(), name);
r
}
}
impl<'ast, 'sf> Visit<'ast> for DiscoveryVisitor<'sf> {
fn visit_item_fn(&mut self, i: &'ast ItemFn) {
if attrs_excluded(&i.attrs) {
return; }
self.collect_fn_mutations(&i.sig.ident, &i.sig.output, &i.block.brace_token.span);
self.in_namespace(&i.sig.ident.to_string(), |v| {
syn::visit::visit_item_fn(v, i);
});
}
fn visit_item_impl(&mut self, i: &'ast syn::ItemImpl) {
if attrs_excluded(&i.attrs) {
return;
}
let name = type_name_string(&i.self_ty);
self.in_namespace(&name, |v| syn::visit::visit_item_impl(v, i));
}
fn visit_impl_item_method(&mut self, i: &'ast syn::ImplItemMethod) {
if attrs_excluded(&i.attrs) {
return;
}
self.collect_fn_mutations(&i.sig.ident, &i.sig.output, &i.block.brace_token.span);
self.in_namespace(&i.sig.ident.to_string(), |v| {
syn::visit::visit_impl_item_method(v, i)
});
}
fn visit_item_mod(&mut self, node: &'ast syn::ItemMod) {
if !attrs_excluded(&node.attrs) {
self.in_namespace(&node.ident.to_string(), |v| {
syn::visit::visit_item_mod(v, node)
});
}
}
}
fn ops_for_return_type(return_type: &syn::ReturnType) -> Vec<MutationOp> {
let mut ops: Vec<MutationOp> = Vec::new();
match return_type {
syn::ReturnType::Default => ops.push(MutationOp::Unit),
syn::ReturnType::Type(_rarrow, box_typ) => match &**box_typ {
syn::Type::Path(syn::TypePath { path, .. }) => {
if path.is_ident("bool") {
ops.push(MutationOp::True);
ops.push(MutationOp::False);
} else if path.is_ident("String") {
ops.push(MutationOp::EmptyString);
ops.push(MutationOp::Xyzzy);
} else if path_is_result(path) {
ops.push(MutationOp::OkDefault);
} else {
ops.push(MutationOp::Default)
}
}
_ => ops.push(MutationOp::Default),
},
}
ops
}
fn type_name_string(ty: &syn::Type) -> String {
match ty {
syn::Type::Path(p) => {
if let Some(ident) = p.path.get_ident() {
format!("{}", ident)
} else {
"<??>".into()
}
}
_ => "<??>".into(),
}
}
fn path_is_result(path: &syn::Path) -> bool {
path.segments
.last()
.map(|segment| segment.ident == "Result")
.unwrap_or_default()
}
fn attrs_excluded(attrs: &[Attribute]) -> bool {
attrs
.iter()
.any(|attr| attr_is_cfg_test(attr) || attr_is_test(attr) || attr_is_mutants_skip(attr))
}
fn attr_is_cfg_test(attr: &Attribute) -> bool {
if !attr.path.is_ident("cfg") {
return false;
}
if let syn::Meta::List(meta_list) = attr.parse_meta().unwrap() {
assert!(meta_list.path.is_ident("cfg"));
for nested_meta in meta_list.nested {
if let syn::NestedMeta::Meta(syn::Meta::Path(cfg_path)) = nested_meta {
if cfg_path.is_ident("test") {
return true;
}
}
}
}
false
}
fn attr_is_test(attr: &Attribute) -> bool {
attr.path.is_ident("test")
}
fn attr_is_mutants_skip(attr: &Attribute) -> bool {
attr.path
.segments
.iter()
.map(|ps| &ps.ident)
.eq(["mutants", "skip"].iter())
}
#[cfg(test)]
mod test {
#[test]
fn path_is_result() {
let path: syn::Path = syn::parse_quote! { Result<(), ()> };
assert!(super::path_is_result(&path));
}
}