diff --git a/src/fnvalue.rs b/src/fnvalue.rs new file mode 100644 index 00000000..8e9f4923 --- /dev/null +++ b/src/fnvalue.rs @@ -0,0 +1,591 @@ +// Copyright 2021-2023 Martin Pool + +//! Mutations of replacing a function body with a value of a (hopefully) appropriate type. + +use itertools::Itertools; +use proc_macro2::TokenStream; +use quote::quote; +use syn::{ + AngleBracketedGenericArguments, Expr, GenericArgument, Ident, Path, PathArguments, ReturnType, + Type, TypeArray, TypeSlice, TypeTuple, +}; +use tracing::trace; + +/// Generate replacement text for a function based on its return type. +pub(crate) fn return_type_replacements( + return_type: &ReturnType, + error_exprs: &[Expr], +) -> impl Iterator { + match return_type { + ReturnType::Default => vec![quote! { () }], + ReturnType::Type(_rarrow, type_) => type_replacements(type_, error_exprs).collect_vec(), + } + .into_iter() +} + +/// Generate some values that we hope are reasonable replacements for a type. +/// +/// This is really the heart of cargo-mutants. +fn type_replacements(type_: &Type, error_exprs: &[Expr]) -> impl Iterator { + // This could probably change to run from some configuration rather than + // hardcoding various types, which would make it easier to support tree-specific + // mutation values, and perhaps reduce duplication. However, it seems better + // to support all the core cases with direct code first to learn what generalizations + // are needed. + let mut reps = Vec::new(); + match type_ { + Type::Path(syn::TypePath { path, .. }) => { + // dbg!(&path); + if path.is_ident("bool") { + reps.push(quote! { true }); + reps.push(quote! { false }); + } else if path.is_ident("String") { + reps.push(quote! { String::new() }); + reps.push(quote! { "xyzzy".into() }); + } else if path.is_ident("str") { + reps.push(quote! { "" }); + reps.push(quote! { "xyzzy" }); + } else if path_is_unsigned(path) { + reps.push(quote! { 0 }); + reps.push(quote! { 1 }); + } else if path_is_signed(path) { + reps.push(quote! { 0 }); + reps.push(quote! { 1 }); + reps.push(quote! { -1 }); + } else if path_is_nonzero_signed(path) { + reps.extend([quote! { 1 }, quote! { -1 }]); + } else if path_is_nonzero_unsigned(path) { + reps.push(quote! { 1 }); + } else if path_is_float(path) { + reps.push(quote! { 0.0 }); + reps.push(quote! { 1.0 }); + reps.push(quote! { -1.0 }); + } else if path_ends_with(path, "Result") { + if let Some(ok_type) = result_ok_type(path) { + reps.extend(type_replacements(ok_type, error_exprs).map(|rep| { + quote! { Ok(#rep) } + })); + } else { + // A result but with no type arguments, like `fmt::Result`; hopefully + // the Ok value can be constructed with Default. + reps.push(quote! { Ok(Default::default()) }); + } + reps.extend(error_exprs.iter().map(|error_expr| { + quote! { Err(#error_expr) } + })); + } else if path_ends_with(path, "HttpResponse") { + reps.push(quote! { HttpResponse::Ok().finish() }); + } else if let Some(some_type) = match_first_type_arg(path, "Option") { + reps.push(quote! { None }); + reps.extend(type_replacements(some_type, error_exprs).map(|rep| { + quote! { Some(#rep) } + })); + } else if let Some(boxed_type) = match_first_type_arg(path, "Vec") { + // Generate an empty Vec, and then a one-element vec for every recursive + // value. + reps.push(quote! { vec![] }); + reps.extend(type_replacements(boxed_type, error_exprs).map(|rep| { + quote! { vec![#rep] } + })) + } else if let Some(boxed_type) = match_first_type_arg(path, "Cow") { + reps.extend(type_replacements(boxed_type, error_exprs).flat_map(|rep| { + [ + quote! { Cow::Borrowed(#rep) }, + quote! { Cow::Owned(#rep.to_owned()) }, + ] + })) + } else if let Some((container_type, inner_type)) = known_container(path) { + // Something like Arc, Mutex, etc. + + // TODO: Ideally we should use the path without relying on it being + // imported, but we must strip or rewrite the arguments, so that + // `std::sync::Arc` becomes either `std::sync::Arc::::new` + // or at least `std::sync::Arc::new`. Similarly for other types. + reps.extend(type_replacements(inner_type, error_exprs).map(|rep| { + quote! { #container_type::new(#rep) } + })) + } else if let Some((collection_type, inner_type)) = known_collection(path) { + reps.push(quote! { #collection_type::new() }); + reps.extend(type_replacements(inner_type, error_exprs).map(|rep| { + quote! { #collection_type::from_iter([#rep]) } + })); + } else if let Some((collection_type, inner_type)) = maybe_collection_or_container(path) + { + // Something like `T` or `T<'a, A>`, when we don't know exactly how + // to call it, but we strongly suspect that you could construct it from + // an `A`. + reps.push(quote! { #collection_type::new() }); + reps.extend(type_replacements(inner_type, error_exprs).flat_map(|rep| { + [ + quote! { #collection_type::from_iter([#rep]) }, + quote! { #collection_type::new(#rep) }, + quote! { #collection_type::from(#rep) }, + ] + })); + } else { + trace!(?type_, "Return type is not recognized, trying Default"); + reps.push(quote! { Default::default() }); + } + } + Type::Array(TypeArray { elem, len, .. }) => reps.extend( + // Generate arrays that repeat each replacement value however many times. + // In principle we could generate combinations, but that might get very + // large, and values like "all zeros" and "all ones" seem likely to catch + // lots of things. + type_replacements(elem, error_exprs).map(|r| quote! { [ #r; #len ] }), + ), + Type::Reference(syn::TypeReference { + mutability: None, + elem, + .. + }) => match &**elem { + // You can't currently match box patterns in Rust + Type::Path(path) if path.path.is_ident("str") => { + reps.push(quote! { "" }); + reps.push(quote! { "xyzzy" }); + } + Type::Slice(TypeSlice { elem, .. }) => { + reps.push(quote! { Vec::leak(Vec::new()) }); + reps.extend( + type_replacements(elem, error_exprs).map(|r| quote! { Vec::leak(vec![ #r ]) }), + ); + } + _ => { + reps.extend(type_replacements(elem, error_exprs).map(|rep| { + quote! { &#rep } + })); + } + }, + Type::Reference(syn::TypeReference { + mutability: Some(_), + elem, + .. + }) => match &**elem { + Type::Slice(TypeSlice { elem, .. }) => { + reps.push(quote! { Vec::leak(Vec::new()) }); + reps.extend( + type_replacements(elem, error_exprs).map(|r| quote! { Vec::leak(vec![ #r ]) }), + ); + } + _ => { + // Make &mut with static lifetime by leaking them on the heap. + reps.extend(type_replacements(elem, error_exprs).map(|rep| { + quote! { Box::leak(Box::new(#rep)) } + })) + } + }, + Type::Tuple(TypeTuple { elems, .. }) if elems.is_empty() => { + reps.push(quote! { () }); + // TODO: Also recurse into non-empty tuples. + } + Type::Never(_) => { + // In theory we could mutate this to a function that just + // loops or sleeps, but it seems unlikely to be useful, + // so generate nothing. + } + _ => { + trace!(?type_, "Return type is not recognized, trying Default"); + reps.push(quote! { Default::default() }); + } + } + reps.into_iter() +} + +fn path_ends_with(path: &Path, ident: &str) -> bool { + path.segments.last().map_or(false, |s| s.ident == ident) +} + +/// If the type has a single type argument then, perhaps it's a simple container +/// like Box, Cell, Mutex, etc, that can be constructed with `T::new(inner_val)`. +/// +/// If so, return the short name (like "Box") and the inner type. +fn known_container(path: &Path) -> Option<(&Ident, &Type)> { + let last = path.segments.last()?; + if ["Box", "Cell", "RefCell", "Arc", "Rc", "Mutex"] + .iter() + .any(|v| last.ident == v) + { + if let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) = + &last.arguments + { + // TODO: Skip lifetime args. + // TODO: Return the path with args stripped out. + if args.len() == 1 { + if let Some(GenericArgument::Type(inner_type)) = args.first() { + return Some((&last.ident, inner_type)); + } + } + } + } + None +} + +/// Match known simple collections that can be empty or constructed from an +/// iterator. +fn known_collection(path: &Path) -> Option<(&Ident, &Type)> { + let last = path.segments.last()?; + if ![ + "BinaryHeap", + "BTreeSet", + "HashSet", + "LinkedList", + "VecDeque", + ] + .iter() + .any(|v| last.ident == v) + { + return None; + } + if let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) = + &last.arguments + { + // TODO: Skip lifetime args. + // TODO: Return the path with args stripped out. + if args.len() == 1 { + if let Some(GenericArgument::Type(inner_type)) = args.first() { + return Some((&last.ident, inner_type)); + } + } + } + None +} + +/// Match a type with one type argument, which might be a container or collection. +fn maybe_collection_or_container(path: &Path) -> Option<(&Ident, &Type)> { + let last = path.segments.last()?; + if let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) = + &last.arguments + { + let type_args: Vec<_> = args + .iter() + .filter_map(|a| match a { + GenericArgument::Type(t) => Some(t), + _ => None, + }) + .collect(); + // TODO: Return the path with args stripped out. + if type_args.len() == 1 { + return Some((&last.ident, type_args.first().unwrap())); + } + } + None +} + +fn path_is_float(path: &Path) -> bool { + ["f32", "f64"].iter().any(|s| path.is_ident(s)) +} + +fn path_is_unsigned(path: &Path) -> bool { + ["u8", "u16", "u32", "u64", "u128", "usize"] + .iter() + .any(|s| path.is_ident(s)) +} + +fn path_is_signed(path: &Path) -> bool { + ["i8", "i16", "i32", "i64", "i128", "isize"] + .iter() + .any(|s| path.is_ident(s)) +} + +fn path_is_nonzero_signed(path: &Path) -> bool { + if let Some(l) = path.segments.last().map(|p| p.ident.to_string()) { + matches!( + l.as_str(), + "NonZeroIsize" + | "NonZeroI8" + | "NonZeroI16" + | "NonZeroI32" + | "NonZeroI64" + | "NonZeroI128", + ) + } else { + false + } +} + +fn path_is_nonzero_unsigned(path: &Path) -> bool { + if let Some(l) = path.segments.last().map(|p| p.ident.to_string()) { + matches!( + l.as_str(), + "NonZeroUsize" + | "NonZeroU8" + | "NonZeroU16" + | "NonZeroU32" + | "NonZeroU64" + | "NonZeroU128", + ) + } else { + false + } +} + +/// If this looks like `Result` (optionally with `Result` in some module), return `T`. +fn result_ok_type(path: &Path) -> Option<&Type> { + match_first_type_arg(path, "Result") +} + +/// If this is a path ending in `expected_ident`, return the first type argument, ignoring +/// lifetimes. +fn match_first_type_arg<'p>(path: &'p Path, expected_ident: &str) -> Option<&'p Type> { + // TODO: Maybe match only things witn one arg? + let last = path.segments.last()?; + if last.ident == expected_ident { + if let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) = + &last.arguments + { + for arg in args { + match arg { + GenericArgument::Type(arg_type) => return Some(arg_type), + GenericArgument::Lifetime(_) => (), + _ => return None, + } + } + } + } + None +} + +#[cfg(test)] +mod test { + use pretty_assertions::assert_eq; + use syn::{parse_quote, Expr, ReturnType}; + + use crate::pretty::tokens_to_pretty_string; + + use super::return_type_replacements; + + #[test] + fn path_is_result() { + let path: syn::Path = parse_quote! { Result<(), ()> }; + assert!(super::result_ok_type(&path).is_some()); + } + + #[test] + fn recurse_into_result_bool() { + let return_type: syn::ReturnType = parse_quote! {-> std::result::Result }; + let reps = return_type_replacements(&return_type, &[]); + assert_eq!( + reps.map(tokens_to_pretty_string).collect::>(), + &["Ok(true)", "Ok(false)",] + ); + } + + #[test] + fn recurse_into_result_result_bool() { + let return_type: syn::ReturnType = parse_quote! {-> std::result::Result> }; + let error_expr: syn::Expr = parse_quote! { anyhow!("mutated") }; + let reps = return_type_replacements(&return_type, &[error_expr]); + assert_eq!( + reps.map(tokens_to_pretty_string).collect::>(), + &[ + "Ok(Ok(true))", + "Ok(Ok(false))", + "Ok(Err(anyhow!(\"mutated\")))", + "Err(anyhow!(\"mutated\"))" + ] + ); + } + + #[test] + fn u16_replacements() { + let reps = return_type_replacements(&parse_quote! { -> u16 }, &[]); + assert_eq!( + reps.map(tokens_to_pretty_string).collect::>(), + &["0", "1",] + ); + } + + #[test] + fn isize_replacements() { + let reps = return_type_replacements(&parse_quote! { -> isize }, &[]); + assert_eq!( + reps.map(tokens_to_pretty_string).collect::>(), + &["0", "1", "-1"] + ); + } + + #[test] + fn nonzero_integer_replacements() { + let reps = return_type_replacements(&parse_quote! { -> std::num::NonZeroIsize }, &[]); + assert_eq!( + reps.map(tokens_to_pretty_string).collect::>(), + &["1", "-1"] + ); + + let reps = return_type_replacements(&parse_quote! { -> std::num::NonZeroUsize }, &[]); + assert_eq!( + reps.map(tokens_to_pretty_string).collect::>(), + &["1"] + ); + + let reps = return_type_replacements(&parse_quote! { -> std::num::NonZeroU32 }, &[]); + assert_eq!( + reps.map(tokens_to_pretty_string).collect::>(), + &["1"] + ); + } + + #[test] + fn unit_replacement() { + let reps = return_type_replacements(&parse_quote! { -> () }, &[]); + assert_eq!( + reps.map(tokens_to_pretty_string).collect::>(), + &["()"] + ); + } + + #[test] + fn result_unit_replacement() { + let reps = return_type_replacements(&parse_quote! { -> Result<(), Error> }, &[]); + assert_eq!( + reps.map(tokens_to_pretty_string).collect::>(), + &["Ok(())"] + ); + + let reps = return_type_replacements(&parse_quote! { -> Result<()> }, &[]); + assert_eq!( + reps.map(tokens_to_pretty_string).collect::>(), + &["Ok(())"] + ); + } + + #[test] + fn http_response_replacement() { + assert_eq!( + replace(&parse_quote! { -> HttpResponse }, &[]), + &["HttpResponse::Ok().finish()"] + ); + } + + #[test] + fn option_usize_replacement() { + let reps = return_type_replacements(&parse_quote! { -> Option }, &[]); + assert_eq!( + reps.map(tokens_to_pretty_string).collect::>(), + &["None", "Some(0)", "Some(1)"] + ); + } + + #[test] + fn box_usize_replacement() { + let reps = return_type_replacements(&parse_quote! { -> Box }, &[]); + assert_eq!( + reps.map(tokens_to_pretty_string).collect::>(), + &["Box::new(0)", "Box::new(1)"] + ); + } + + #[test] + fn box_unrecognized_type_replacement() { + let reps = return_type_replacements(&parse_quote! { -> Box }, &[]); + assert_eq!( + reps.map(tokens_to_pretty_string).collect::>(), + &["Box::new(Default::default())"] + ); + } + + #[test] + fn vec_string_replacement() { + let reps = return_type_replacements(&parse_quote! { -> std::vec::Vec }, &[]); + assert_eq!( + reps.map(tokens_to_pretty_string).collect::>(), + &["vec![]", "vec![String::new()]", "vec![\"xyzzy\".into()]"] + ); + } + + #[test] + fn float_replacement() { + let reps = return_type_replacements(&parse_quote! { -> f32 }, &[]); + assert_eq!( + reps.map(tokens_to_pretty_string).collect::>(), + &["0.0", "1.0", "-1.0"] + ); + } + + #[test] + fn ref_replacement_recurses() { + let reps = return_type_replacements(&parse_quote! { -> &bool }, &[]); + assert_eq!( + reps.map(tokens_to_pretty_string).collect::>(), + &["&true", "&false"] + ); + } + + #[test] + fn array_replacement() { + assert_eq!( + replace(&parse_quote! { -> [u8; 256] }, &[]), + &["[0; 256]", "[1; 256]"] + ); + } + + #[test] + fn arc_replacement() { + // Also checks that it matches the path, even using an atypical path. + // TODO: Ideally this would be fully qualified like `alloc::sync::Arc::new(String::new())`. + assert_eq!( + replace(&parse_quote! { -> alloc::sync::Arc }, &[]), + &["Arc::new(String::new())", "Arc::new(\"xyzzy\".into())"] + ); + } + + #[test] + fn rc_replacement() { + // Also checks that it matches the path, even using an atypical path. + // TODO: Ideally this would be fully qualified like `alloc::sync::Rc::new(String::new())`. + assert_eq!( + replace(&parse_quote! { -> alloc::sync::Rc }, &[]), + &["Rc::new(String::new())", "Rc::new(\"xyzzy\".into())"] + ); + } + + #[test] + fn btreeset_replacement() { + assert_eq!( + replace(&parse_quote! { -> std::collections::BTreeSet }, &[]), + &[ + "BTreeSet::new()", + "BTreeSet::from_iter([String::new()])", + "BTreeSet::from_iter([\"xyzzy\".into()])" + ] + ); + } + + #[test] + fn cow_generates_borrowed_and_owned() { + assert_eq!( + replace(&parse_quote! { -> Cow<'static, str> }, &[]), + &[ + "Cow::Borrowed(\"\")", + "Cow::Owned(\"\".to_owned())", + "Cow::Borrowed(\"xyzzy\")", + "Cow::Owned(\"xyzzy\".to_owned())", + ] + ); + } + + #[test] + fn unknown_container_replacement() { + // This looks like something that holds a String, and maybe can be constructed + // from a String, but we don't know anythig else about it. + assert_eq!( + replace(&parse_quote! { -> UnknownContainer<'static, str> }, &[]), + &[ + "UnknownContainer::new()", + "UnknownContainer::from_iter([\"\"])", + "UnknownContainer::new(\"\")", + "UnknownContainer::from(\"\")", + "UnknownContainer::from_iter([\"xyzzy\"])", + "UnknownContainer::new(\"xyzzy\")", + "UnknownContainer::from(\"xyzzy\")", + ] + ); + } + + fn replace(return_type: &ReturnType, error_exprs: &[Expr]) -> Vec { + return_type_replacements(return_type, error_exprs) + .into_iter() + .map(tokens_to_pretty_string) + .collect::>() + } +} diff --git a/src/main.rs b/src/main.rs index b9d8c92d..ba8d29e5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ mod cargo; mod config; mod console; mod exit_code; +mod fnvalue; mod interrupt; mod lab; mod log_file; diff --git a/src/pretty.rs b/src/pretty.rs index 0a35ed42..65587b3e 100644 --- a/src/pretty.rs +++ b/src/pretty.rs @@ -4,6 +4,7 @@ use proc_macro2::{Delimiter, TokenTree}; use quote::ToTokens; +use syn::ReturnType; /// Convert a TokenStream representing some code to a reasonably formatted /// string of Rust code. @@ -67,3 +68,39 @@ pub(crate) fn tokens_to_pretty_string(t: T) -> String { ); b } + +pub(crate) fn return_type_to_pretty_string(return_type: &ReturnType) -> String { + match return_type { + ReturnType::Default => String::new(), + ReturnType::Type(arrow, typ) => { + format!( + "{} {}", + arrow.to_token_stream(), + tokens_to_pretty_string(typ) + ) + } + } +} + +#[cfg(test)] +mod test { + use pretty_assertions::assert_eq; + use quote::quote; + + use super::tokens_to_pretty_string; + + #[test] + fn pretty_format() { + assert_eq!( + tokens_to_pretty_string(quote! { + > :: next + -> Option < Self :: Item > + }), + ">::next -> Option" + ); + assert_eq!( + tokens_to_pretty_string(quote! { Lex < 'buf >::take }), + "Lex<'buf>::take" + ); + } +} diff --git a/src/visit.rs b/src/visit.rs index 9e6f5127..a3fa7398 100644 --- a/src/visit.rs +++ b/src/visit.rs @@ -2,8 +2,6 @@ //! Visit the abstract syntax tree and discover things to mutate. //! -//! Knowledge of the `syn` API is localized here. -//! //! Walking the tree starts with some root files known to the build tool: //! e.g. for cargo they are identified from the targets. The tree walker then //! follows `mod` statements to recursively visit other referenced files. @@ -13,17 +11,13 @@ use std::sync::Arc; use anyhow::Context; use itertools::Itertools; -use proc_macro2::TokenStream; -use quote::{quote, ToTokens}; use syn::ext::IdentExt; use syn::visit::Visit; -use syn::{ - AngleBracketedGenericArguments, Attribute, Expr, GenericArgument, Ident, ItemFn, Path, - PathArguments, ReturnType, Type, TypeArray, TypeSlice, TypeTuple, -}; +use syn::{Attribute, Expr, ItemFn, ReturnType}; use tracing::{debug, debug_span, trace, trace_span, warn}; -use crate::pretty::tokens_to_pretty_string; +use crate::fnvalue::return_type_replacements; +use crate::pretty::{return_type_to_pretty_string, tokens_to_pretty_string}; use crate::source::SourceFile; use crate::*; @@ -141,7 +135,7 @@ struct DiscoveryVisitor<'o> { impl<'o> DiscoveryVisitor<'o> { fn collect_fn_mutants(&mut self, return_type: &ReturnType, span: &proc_macro2::Span) { let full_function_name = Arc::new(self.namespace_stack.join("::")); - let return_type_str = Arc::new(return_type_to_string(return_type)); + let return_type_str = Arc::new(return_type_to_pretty_string(return_type)); let mut new_mutants = return_type_replacements(return_type, self.error_exprs) .map(|rep| Mutant { source_file: Arc::clone(&self.source_file), @@ -305,353 +299,6 @@ fn find_mod_source( Ok(None) } -/// Generate replacement text for a function based on its return type. -fn return_type_replacements( - return_type: &ReturnType, - error_exprs: &[Expr], -) -> impl Iterator { - match return_type { - ReturnType::Default => vec![quote! { () }], - ReturnType::Type(_rarrow, type_) => type_replacements(type_, error_exprs).collect_vec(), - } - .into_iter() -} - -/// Generate some values that we hope are reasonable replacements for a type. -/// -/// This is really the heart of cargo-mutants. -fn type_replacements(type_: &Type, error_exprs: &[Expr]) -> impl Iterator { - // This could probably change to run from some configuration rather than - // hardcoding various types, which would make it easier to support tree-specific - // mutation values, and perhaps reduce duplication. However, it seems better - // to support all the core cases with direct code first to learn what generalizations - // are needed. - let mut reps = Vec::new(); - match type_ { - Type::Path(syn::TypePath { path, .. }) => { - // dbg!(&path); - if path.is_ident("bool") { - reps.push(quote! { true }); - reps.push(quote! { false }); - } else if path.is_ident("String") { - reps.push(quote! { String::new() }); - reps.push(quote! { "xyzzy".into() }); - } else if path.is_ident("str") { - reps.push(quote! { "" }); - reps.push(quote! { "xyzzy" }); - } else if path_is_unsigned(path) { - reps.push(quote! { 0 }); - reps.push(quote! { 1 }); - } else if path_is_signed(path) { - reps.push(quote! { 0 }); - reps.push(quote! { 1 }); - reps.push(quote! { -1 }); - } else if path_is_nonzero_signed(path) { - reps.extend([quote! { 1 }, quote! { -1 }]); - } else if path_is_nonzero_unsigned(path) { - reps.push(quote! { 1 }); - } else if path_is_float(path) { - reps.push(quote! { 0.0 }); - reps.push(quote! { 1.0 }); - reps.push(quote! { -1.0 }); - } else if path_ends_with(path, "Result") { - if let Some(ok_type) = result_ok_type(path) { - reps.extend(type_replacements(ok_type, error_exprs).map(|rep| { - quote! { Ok(#rep) } - })); - } else { - // A result but with no type arguments, like `fmt::Result`; hopefully - // the Ok value can be constructed with Default. - reps.push(quote! { Ok(Default::default()) }); - } - reps.extend(error_exprs.iter().map(|error_expr| { - quote! { Err(#error_expr) } - })); - } else if path_ends_with(path, "HttpResponse") { - reps.push(quote! { HttpResponse::Ok().finish() }); - } else if let Some(some_type) = match_first_type_arg(path, "Option") { - reps.push(quote! { None }); - reps.extend(type_replacements(some_type, error_exprs).map(|rep| { - quote! { Some(#rep) } - })); - } else if let Some(boxed_type) = match_first_type_arg(path, "Vec") { - // Generate an empty Vec, and then a one-element vec for every recursive - // value. - reps.push(quote! { vec![] }); - reps.extend(type_replacements(boxed_type, error_exprs).map(|rep| { - quote! { vec![#rep] } - })) - } else if let Some(boxed_type) = match_first_type_arg(path, "Cow") { - reps.extend(type_replacements(boxed_type, error_exprs).flat_map(|rep| { - [ - quote! { Cow::Borrowed(#rep) }, - quote! { Cow::Owned(#rep.to_owned()) }, - ] - })) - } else if let Some((container_type, inner_type)) = known_container(path) { - // Something like Arc, Mutex, etc. - - // TODO: Ideally we should use the path without relying on it being - // imported, but we must strip or rewrite the arguments, so that - // `std::sync::Arc` becomes either `std::sync::Arc::::new` - // or at least `std::sync::Arc::new`. Similarly for other types. - reps.extend(type_replacements(inner_type, error_exprs).map(|rep| { - quote! { #container_type::new(#rep) } - })) - } else if let Some((collection_type, inner_type)) = known_collection(path) { - reps.push(quote! { #collection_type::new() }); - reps.extend(type_replacements(inner_type, error_exprs).map(|rep| { - quote! { #collection_type::from_iter([#rep]) } - })); - } else if let Some((collection_type, inner_type)) = maybe_collection_or_container(path) - { - // Something like `T` or `T<'a, A>`, when we don't know exactly how - // to call it, but we strongly suspect that you could construct it from - // an `A`. - reps.push(quote! { #collection_type::new() }); - reps.extend(type_replacements(inner_type, error_exprs).flat_map(|rep| { - [ - quote! { #collection_type::from_iter([#rep]) }, - quote! { #collection_type::new(#rep) }, - quote! { #collection_type::from(#rep) }, - ] - })); - } else { - trace!(?type_, "Return type is not recognized, trying Default"); - reps.push(quote! { Default::default() }); - } - } - Type::Array(TypeArray { elem, len, .. }) => reps.extend( - // Generate arrays that repeat each replacement value however many times. - // In principle we could generate combinations, but that might get very - // large, and values like "all zeros" and "all ones" seem likely to catch - // lots of things. - type_replacements(elem, error_exprs).map(|r| quote! { [ #r; #len ] }), - ), - Type::Reference(syn::TypeReference { - mutability: None, - elem, - .. - }) => match &**elem { - // You can't currently match box patterns in Rust - Type::Path(path) if path.path.is_ident("str") => { - reps.push(quote! { "" }); - reps.push(quote! { "xyzzy" }); - } - Type::Slice(TypeSlice { elem, .. }) => { - reps.push(quote! { Vec::leak(Vec::new()) }); - reps.extend( - type_replacements(elem, error_exprs).map(|r| quote! { Vec::leak(vec![ #r ]) }), - ); - } - _ => { - reps.extend(type_replacements(elem, error_exprs).map(|rep| { - quote! { &#rep } - })); - } - }, - Type::Reference(syn::TypeReference { - mutability: Some(_), - elem, - .. - }) => match &**elem { - Type::Slice(TypeSlice { elem, .. }) => { - reps.push(quote! { Vec::leak(Vec::new()) }); - reps.extend( - type_replacements(elem, error_exprs).map(|r| quote! { Vec::leak(vec![ #r ]) }), - ); - } - _ => { - // Make &mut with static lifetime by leaking them on the heap. - reps.extend(type_replacements(elem, error_exprs).map(|rep| { - quote! { Box::leak(Box::new(#rep)) } - })) - } - }, - Type::Tuple(TypeTuple { elems, .. }) if elems.is_empty() => { - reps.push(quote! { () }); - // TODO: Also recurse into non-empty tuples. - } - Type::Never(_) => { - // In theory we could mutate this to a function that just - // loops or sleeps, but it seems unlikely to be useful, - // so generate nothing. - } - _ => { - trace!(?type_, "Return type is not recognized, trying Default"); - reps.push(quote! { Default::default() }); - } - } - reps.into_iter() -} - -fn return_type_to_string(return_type: &ReturnType) -> String { - match return_type { - ReturnType::Default => String::new(), - ReturnType::Type(arrow, typ) => { - format!( - "{} {}", - arrow.to_token_stream(), - tokens_to_pretty_string(typ) - ) - } - } -} - -fn path_ends_with(path: &Path, ident: &str) -> bool { - path.segments.last().map_or(false, |s| s.ident == ident) -} - -/// If the type has a single type argument then, perhaps it's a simple container -/// like Box, Cell, Mutex, etc, that can be constructed with `T::new(inner_val)`. -/// -/// If so, return the short name (like "Box") and the inner type. -fn known_container(path: &Path) -> Option<(&Ident, &Type)> { - let last = path.segments.last()?; - if ["Box", "Cell", "RefCell", "Arc", "Rc", "Mutex"] - .iter() - .any(|v| last.ident == v) - { - if let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) = - &last.arguments - { - // TODO: Skip lifetime args. - // TODO: Return the path with args stripped out. - if args.len() == 1 { - if let Some(GenericArgument::Type(inner_type)) = args.first() { - return Some((&last.ident, inner_type)); - } - } - } - } - None -} - -/// Match known simple collections that can be empty or constructed from an -/// iterator. -fn known_collection(path: &Path) -> Option<(&Ident, &Type)> { - let last = path.segments.last()?; - if ![ - "BinaryHeap", - "BTreeSet", - "HashSet", - "LinkedList", - "VecDeque", - ] - .iter() - .any(|v| last.ident == v) - { - return None; - } - if let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) = - &last.arguments - { - // TODO: Skip lifetime args. - // TODO: Return the path with args stripped out. - if args.len() == 1 { - if let Some(GenericArgument::Type(inner_type)) = args.first() { - return Some((&last.ident, inner_type)); - } - } - } - None -} - -/// Match a type with one type argument, which might be a container or collection. -fn maybe_collection_or_container(path: &Path) -> Option<(&Ident, &Type)> { - let last = path.segments.last()?; - if let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) = - &last.arguments - { - let type_args: Vec<_> = args - .iter() - .filter_map(|a| match a { - GenericArgument::Type(t) => Some(t), - _ => None, - }) - .collect(); - // TODO: Return the path with args stripped out. - if type_args.len() == 1 { - return Some((&last.ident, type_args.first().unwrap())); - } - } - None -} - -fn path_is_float(path: &Path) -> bool { - ["f32", "f64"].iter().any(|s| path.is_ident(s)) -} - -fn path_is_unsigned(path: &Path) -> bool { - ["u8", "u16", "u32", "u64", "u128", "usize"] - .iter() - .any(|s| path.is_ident(s)) -} - -fn path_is_signed(path: &Path) -> bool { - ["i8", "i16", "i32", "i64", "i128", "isize"] - .iter() - .any(|s| path.is_ident(s)) -} - -fn path_is_nonzero_signed(path: &Path) -> bool { - if let Some(l) = path.segments.last().map(|p| p.ident.to_string()) { - matches!( - l.as_str(), - "NonZeroIsize" - | "NonZeroI8" - | "NonZeroI16" - | "NonZeroI32" - | "NonZeroI64" - | "NonZeroI128", - ) - } else { - false - } -} - -fn path_is_nonzero_unsigned(path: &Path) -> bool { - if let Some(l) = path.segments.last().map(|p| p.ident.to_string()) { - matches!( - l.as_str(), - "NonZeroUsize" - | "NonZeroU8" - | "NonZeroU16" - | "NonZeroU32" - | "NonZeroU64" - | "NonZeroU128", - ) - } else { - false - } -} - -/// If this looks like `Result` (optionally with `Result` in some module), return `T`. -fn result_ok_type(path: &Path) -> Option<&Type> { - match_first_type_arg(path, "Result") -} - -/// If this is a path ending in `expected_ident`, return the first type argument, ignoring -/// lifetimes. -fn match_first_type_arg<'p>(path: &'p Path, expected_ident: &str) -> Option<&'p Type> { - // TODO: Maybe match only things witn one arg? - let last = path.segments.last()?; - if last.ident == expected_ident { - if let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) = - &last.arguments - { - for arg in args { - match arg { - GenericArgument::Type(arg_type) => return Some(arg_type), - GenericArgument::Lifetime(_) => (), - _ => return None, - } - } - } - } - None -} - /// True if the signature of a function is such that it should be excluded. fn fn_sig_excluded(sig: &syn::Signature) -> bool { if sig.unsafety.is_some() { @@ -732,261 +379,3 @@ fn attr_is_mutants_skip(attr: &Attribute) -> bool { } skip } - -#[cfg(test)] -mod test { - use pretty_assertions::assert_eq; - use quote::quote; - use syn::{parse_quote, Expr, ReturnType}; - - use super::{return_type_replacements, tokens_to_pretty_string}; - - #[test] - fn path_is_result() { - let path: syn::Path = syn::parse_quote! { Result<(), ()> }; - assert!(super::result_ok_type(&path).is_some()); - } - - #[test] - fn pretty_format() { - assert_eq!( - tokens_to_pretty_string(quote! { - > :: next - -> Option < Self :: Item > - }), - ">::next -> Option" - ); - assert_eq!( - tokens_to_pretty_string(quote! { Lex < 'buf >::take }), - "Lex<'buf>::take" - ); - } - - #[test] - fn recurse_into_result_bool() { - let return_type: syn::ReturnType = parse_quote! {-> std::result::Result }; - let reps = return_type_replacements(&return_type, &[]); - assert_eq!( - reps.map(tokens_to_pretty_string).collect::>(), - &["Ok(true)", "Ok(false)",] - ); - } - - #[test] - fn recurse_into_result_result_bool() { - let return_type: syn::ReturnType = parse_quote! {-> std::result::Result> }; - let error_expr: syn::Expr = parse_quote! { anyhow!("mutated") }; - let reps = return_type_replacements(&return_type, &[error_expr]); - assert_eq!( - reps.map(tokens_to_pretty_string).collect::>(), - &[ - "Ok(Ok(true))", - "Ok(Ok(false))", - "Ok(Err(anyhow!(\"mutated\")))", - "Err(anyhow!(\"mutated\"))" - ] - ); - } - - #[test] - fn u16_replacements() { - let reps = return_type_replacements(&parse_quote! { -> u16 }, &[]); - assert_eq!( - reps.map(tokens_to_pretty_string).collect::>(), - &["0", "1",] - ); - } - - #[test] - fn isize_replacements() { - let reps = return_type_replacements(&parse_quote! { -> isize }, &[]); - assert_eq!( - reps.map(tokens_to_pretty_string).collect::>(), - &["0", "1", "-1"] - ); - } - - #[test] - fn nonzero_integer_replacements() { - let reps = return_type_replacements(&parse_quote! { -> std::num::NonZeroIsize }, &[]); - assert_eq!( - reps.map(tokens_to_pretty_string).collect::>(), - &["1", "-1"] - ); - - let reps = return_type_replacements(&parse_quote! { -> std::num::NonZeroUsize }, &[]); - assert_eq!( - reps.map(tokens_to_pretty_string).collect::>(), - &["1"] - ); - - let reps = return_type_replacements(&parse_quote! { -> std::num::NonZeroU32 }, &[]); - assert_eq!( - reps.map(tokens_to_pretty_string).collect::>(), - &["1"] - ); - } - - #[test] - fn unit_replacement() { - let reps = return_type_replacements(&parse_quote! { -> () }, &[]); - assert_eq!( - reps.map(tokens_to_pretty_string).collect::>(), - &["()"] - ); - } - - #[test] - fn result_unit_replacement() { - let reps = return_type_replacements(&parse_quote! { -> Result<(), Error> }, &[]); - assert_eq!( - reps.map(tokens_to_pretty_string).collect::>(), - &["Ok(())"] - ); - - let reps = return_type_replacements(&parse_quote! { -> Result<()> }, &[]); - assert_eq!( - reps.map(tokens_to_pretty_string).collect::>(), - &["Ok(())"] - ); - } - - #[test] - fn http_response_replacement() { - assert_eq!( - replace(&parse_quote! { -> HttpResponse }, &[]), - &["HttpResponse::Ok().finish()"] - ); - } - - #[test] - fn option_usize_replacement() { - let reps = return_type_replacements(&parse_quote! { -> Option }, &[]); - assert_eq!( - reps.map(tokens_to_pretty_string).collect::>(), - &["None", "Some(0)", "Some(1)"] - ); - } - - #[test] - fn box_usize_replacement() { - let reps = return_type_replacements(&parse_quote! { -> Box }, &[]); - assert_eq!( - reps.map(tokens_to_pretty_string).collect::>(), - &["Box::new(0)", "Box::new(1)"] - ); - } - - #[test] - fn box_unrecognized_type_replacement() { - let reps = return_type_replacements(&parse_quote! { -> Box }, &[]); - assert_eq!( - reps.map(tokens_to_pretty_string).collect::>(), - &["Box::new(Default::default())"] - ); - } - - #[test] - fn vec_string_replacement() { - let reps = return_type_replacements(&parse_quote! { -> std::vec::Vec }, &[]); - assert_eq!( - reps.map(tokens_to_pretty_string).collect::>(), - &["vec![]", "vec![String::new()]", "vec![\"xyzzy\".into()]"] - ); - } - - #[test] - fn float_replacement() { - let reps = return_type_replacements(&parse_quote! { -> f32 }, &[]); - assert_eq!( - reps.map(tokens_to_pretty_string).collect::>(), - &["0.0", "1.0", "-1.0"] - ); - } - - #[test] - fn ref_replacement_recurses() { - let reps = return_type_replacements(&parse_quote! { -> &bool }, &[]); - assert_eq!( - reps.map(tokens_to_pretty_string).collect::>(), - &["&true", "&false"] - ); - } - - #[test] - fn array_replacement() { - assert_eq!( - replace(&parse_quote! { -> [u8; 256] }, &[]), - &["[0; 256]", "[1; 256]"] - ); - } - - #[test] - fn arc_replacement() { - // Also checks that it matches the path, even using an atypical path. - // TODO: Ideally this would be fully qualified like `alloc::sync::Arc::new(String::new())`. - assert_eq!( - replace(&parse_quote! { -> alloc::sync::Arc }, &[]), - &["Arc::new(String::new())", "Arc::new(\"xyzzy\".into())"] - ); - } - - #[test] - fn rc_replacement() { - // Also checks that it matches the path, even using an atypical path. - // TODO: Ideally this would be fully qualified like `alloc::sync::Rc::new(String::new())`. - assert_eq!( - replace(&parse_quote! { -> alloc::sync::Rc }, &[]), - &["Rc::new(String::new())", "Rc::new(\"xyzzy\".into())"] - ); - } - - #[test] - fn btreeset_replacement() { - assert_eq!( - replace(&parse_quote! { -> std::collections::BTreeSet }, &[]), - &[ - "BTreeSet::new()", - "BTreeSet::from_iter([String::new()])", - "BTreeSet::from_iter([\"xyzzy\".into()])" - ] - ); - } - - #[test] - fn cow_generates_borrowed_and_owned() { - assert_eq!( - replace(&parse_quote! { -> Cow<'static, str> }, &[]), - &[ - "Cow::Borrowed(\"\")", - "Cow::Owned(\"\".to_owned())", - "Cow::Borrowed(\"xyzzy\")", - "Cow::Owned(\"xyzzy\".to_owned())", - ] - ); - } - - #[test] - fn unknown_container_replacement() { - // This looks like something that holds a String, and maybe can be constructed - // from a String, but we don't know anythig else about it. - assert_eq!( - replace(&parse_quote! { -> UnknownContainer<'static, str> }, &[]), - &[ - "UnknownContainer::new()", - "UnknownContainer::from_iter([\"\"])", - "UnknownContainer::new(\"\")", - "UnknownContainer::from(\"\")", - "UnknownContainer::from_iter([\"xyzzy\"])", - "UnknownContainer::new(\"xyzzy\")", - "UnknownContainer::from(\"xyzzy\")", - ] - ); - } - fn replace(return_type: &ReturnType, error_exprs: &[Expr]) -> Vec { - return_type_replacements(return_type, error_exprs) - .into_iter() - .map(tokens_to_pretty_string) - .collect::>() - } -}