#![warn(clippy::pedantic)]
use std::iter;
use itertools::Itertools;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{
AngleBracketedGenericArguments, AssocType, Expr, GenericArgument, Ident, Path, PathArguments,
ReturnType, TraitBound, Type, TypeArray, TypeImplTrait, TypeParamBound, TypeSlice, TypeTuple,
};
use tracing::trace;
use crate::pretty::ToPrettyString;
pub(crate) fn return_type_replacements(
return_type: &ReturnType,
error_exprs: &[Expr],
) -> Vec<TokenStream> {
match return_type {
ReturnType::Default => vec![quote! { () }],
ReturnType::Type(_rarrow, type_) => type_replacements(type_, error_exprs).collect_vec(),
}
}
#[allow(clippy::too_many_lines)]
fn type_replacements(type_: &Type, error_exprs: &[Expr]) -> impl Iterator<Item = TokenStream> {
match type_ {
Type::Path(syn::TypePath { path, .. }) => {
if path.is_ident("bool") {
vec![quote! { true }, quote! { false }]
} else if path.is_ident("String") {
vec![quote! { String::new() }, quote! { "xyzzy".into() }]
} else if path.is_ident("str") {
vec![quote! { "" }, quote! { "xyzzy" }]
} else if path_is_unsigned(path) {
vec![quote! { 0 }, quote! { 1 }]
} else if path_is_signed(path) {
vec![quote! { 0 }, quote! { 1 }, quote! { -1 }]
} else if path_is_nonzero_signed(path) {
vec![
quote! { 1.try_into().unwrap() },
quote! { (-1).try_into().unwrap() },
]
} else if path_is_nonzero_unsigned(path) {
vec![quote! { 1.try_into().unwrap() }]
} else if path_is_float(path) {
vec![quote! { 0.0 }, quote! { 1.0 }, quote! { -1.0 }]
} else if path_ends_with(path, "Result") {
if let Some(ok_type) = match_first_type_arg(path, "Result") {
type_replacements(ok_type, error_exprs)
.map(|rep| {
quote! { Ok(#rep) }
})
.collect_vec()
} else {
vec![quote! { Ok(Default::default()) }]
}
.into_iter()
.chain(error_exprs.iter().map(|error_expr| {
quote! { Err(#error_expr) }
}))
.collect_vec()
} else if path_ends_with(path, "HttpResponse") {
vec![quote! { HttpResponse::Ok().finish() }]
} else if let Some(some_type) = match_first_type_arg(path, "Option") {
iter::once(quote! { None })
.chain(type_replacements(some_type, error_exprs).map(|rep| {
quote! { Some(#rep) }
}))
.collect_vec()
} else if let Some(element_type) = match_first_type_arg(path, "Vec") {
iter::once(quote! { vec![] })
.chain(type_replacements(element_type, error_exprs).map(|rep| {
quote! { vec![#rep] }
}))
.collect_vec()
} else if let Some(borrowed_type) = match_first_type_arg(path, "Cow") {
type_replacements(borrowed_type, error_exprs)
.flat_map(|rep| {
[
quote! { Cow::Borrowed(#rep) },
quote! { Cow::Owned(#rep.to_owned()) },
]
})
.collect_vec()
} else if let Some((container_type, inner_type)) = known_container(path) {
type_replacements(inner_type, error_exprs)
.map(|rep| {
quote! { #container_type::new(#rep) }
})
.collect_vec()
} else if let Some((collection_type, inner_type)) = known_collection(path) {
iter::once(quote! { #collection_type::new() })
.chain(type_replacements(inner_type, error_exprs).map(|rep| {
quote! { #collection_type::from_iter([#rep]) }
}))
.collect_vec()
} else if let Some((collection_type, key_type, value_type)) = known_map(path) {
let key_reps = type_replacements(key_type, error_exprs).collect_vec();
let val_reps = type_replacements(value_type, error_exprs).collect_vec();
iter::once(quote! { #collection_type::new() })
.chain(
key_reps
.iter()
.cartesian_product(val_reps)
.map(|(k, v)| quote! { #collection_type::from_iter([(#k, #v)]) }),
)
.collect_vec()
} else if let Some((collection_type, inner_type)) = maybe_collection_or_container(path)
{
iter::once(quote! { #collection_type::new() })
.chain(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) },
]
}))
.collect_vec()
} else {
trace!(
type_ = type_.to_pretty_string(),
"Return type is not recognized, trying Default"
);
vec![quote! { Default::default() }]
}
}
Type::Array(TypeArray { elem, len, .. }) =>
{
type_replacements(elem, error_exprs)
.map(|r| quote! { [ #r; #len ] })
.collect_vec()
}
Type::Slice(TypeSlice { elem, .. }) => iter::once(quote! { Vec::leak(Vec::new()) })
.chain(type_replacements(elem, error_exprs).map(|r| quote! { Vec::leak(vec![ #r ]) }))
.collect_vec(),
Type::Reference(syn::TypeReference {
mutability: None,
elem,
..
}) => match &**elem {
Type::Path(path) if path.path.is_ident("str") => {
vec![quote! { "" }, quote! { "xyzzy" }]
}
Type::Slice(TypeSlice { elem, .. }) => iter::once(quote! { Vec::leak(Vec::new()) })
.chain(
type_replacements(elem, error_exprs).map(|r| quote! { Vec::leak(vec![ #r ]) }),
)
.collect_vec(),
_ => type_replacements(elem, error_exprs)
.map(|rep| {
quote! { Box::leak(Box::new(#rep)) }
})
.collect_vec(),
},
Type::Reference(syn::TypeReference {
mutability: Some(_),
elem,
..
}) => match &**elem {
Type::Slice(TypeSlice { elem, .. }) => iter::once(quote! { Vec::leak(Vec::new()) })
.chain(
type_replacements(elem, error_exprs).map(|r| quote! { Vec::leak(vec![ #r ]) }),
)
.collect_vec(),
_ => {
type_replacements(elem, error_exprs)
.map(|rep| {
quote! { Box::leak(Box::new(#rep)) }
})
.collect_vec()
}
},
Type::Tuple(TypeTuple { elems, .. }) => {
elems
.iter()
.map(|elem| type_replacements(elem, error_exprs).collect_vec())
.multi_cartesian_product()
.map(|reps| {
quote! { ( #( #reps ),* ) }
})
.collect_vec()
}
Type::ImplTrait(impl_trait) => {
if let Some(item_type) = match_impl_iterator(impl_trait) {
iter::once(quote! { ::std::iter::empty() })
.chain(
type_replacements(item_type, error_exprs)
.map(|r| quote! { ::std::iter::once(#r) }),
)
.collect_vec()
} else {
vec![]
}
}
Type::Never(_) => {
vec![]
}
_ => {
trace!(?type_, "Return type is not recognized, trying Default");
vec![quote! { Default::default() }]
}
}
.into_iter()
}
fn path_ends_with(path: &Path, ident: &str) -> bool {
path.segments.last().is_some_and(|s| s.ident == ident)
}
fn match_impl_iterator(TypeImplTrait { bounds, .. }: &TypeImplTrait) -> Option<&Type> {
for bound in bounds {
if let TypeParamBound::Trait(TraitBound { path, .. }) = bound
&& let Some(last_segment) = path.segments.last()
&& last_segment.ident == "Iterator"
&& let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) =
&last_segment.arguments
&& let Some(GenericArgument::AssocType(AssocType { ident, ty, .. })) = args.first()
&& ident == "Item"
{
return Some(ty);
}
}
None
}
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)
&& let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) =
&last.arguments
{
if args.len() == 1
&& let Some(GenericArgument::Type(inner_type)) = args.first()
{
return Some((&last.ident, inner_type));
}
}
None
}
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
{
if args.len() == 1
&& let Some(GenericArgument::Type(inner_type)) = args.first()
{
return Some((&last.ident, inner_type));
}
}
None
}
fn known_map(path: &Path) -> Option<(&Ident, &Type, &Type)> {
let last = path.segments.last()?;
if !["BTreeMap", "HashMap"].iter().any(|v| last.ident == v) {
return None;
}
if let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) =
&last.arguments
{
if let Some((GenericArgument::Type(key_type), GenericArgument::Type(value_type))) =
args.iter().collect_tuple()
{
return Some((&last.ident, key_type, value_type));
}
}
None
}
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();
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
}
}
fn match_first_type_arg<'p>(path: &'p Path, expected_ident: &str) -> Option<&'p Type> {
let last = path.segments.last()?;
if last.ident == expected_ident
&& 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 itertools::Itertools;
use pretty_assertions::assert_eq;
use syn::{Expr, ReturnType, parse_quote};
use crate::fnvalue::match_impl_iterator;
use crate::pretty::ToPrettyString;
use super::{known_map, return_type_replacements};
#[test]
fn recurse_into_result_bool() {
check_replacements(
&parse_quote! {-> std::result::Result<bool> },
&[],
&["Ok(true)", "Ok(false)"],
);
}
#[test]
fn recurse_into_result_result_bool_with_error_values() {
check_replacements(
&parse_quote! {-> std::result::Result<Result<bool>> },
&[parse_quote! { anyhow!("mutated") }],
&[
"Ok(Ok(true))",
"Ok(Ok(false))",
r#"Ok(Err(anyhow!("mutated")))"#,
r#"Err(anyhow!("mutated"))"#,
],
);
}
#[test]
fn u16_replacements() {
check_replacements(&parse_quote! { -> u16 }, &[], &["0", "1"]);
}
#[test]
fn isize_replacements() {
check_replacements(&parse_quote! { -> isize }, &[], &["0", "1", "-1"]);
}
#[test]
fn nonzero_integer_replacements() {
check_replacements(
&parse_quote! { -> std::num::NonZeroIsize },
&[],
&["1.try_into().unwrap()", "(-1).try_into().unwrap()"],
);
check_replacements(
&parse_quote! { -> std::num::NonZeroUsize },
&[],
&["1.try_into().unwrap()"],
);
check_replacements(
&parse_quote! { -> std::num::NonZeroU32 },
&[],
&["1.try_into().unwrap()"],
);
}
#[test]
fn unit_replacement() {
check_replacements(&parse_quote! { -> () }, &[], &["()"]);
}
#[test]
fn result_unit_replacement() {
check_replacements(&parse_quote! { -> Result<(), Error> }, &[], &["Ok(())"]);
check_replacements(&parse_quote! { -> Result<()> }, &[], &["Ok(())"]);
}
#[test]
fn http_response_replacement() {
check_replacements(
&parse_quote! { -> HttpResponse },
&[],
&["HttpResponse::Ok().finish()"],
);
}
#[test]
fn option_usize_replacement() {
check_replacements(
&parse_quote! { -> Option<usize> },
&[],
&["None", "Some(0)", "Some(1)"],
);
}
#[test]
fn box_usize_replacement() {
check_replacements(
&parse_quote! { -> Box<usize> },
&[],
&["Box::new(0)", "Box::new(1)"],
);
}
#[test]
fn box_unrecognized_type_replacement() {
check_replacements(
&parse_quote! { -> Box<MyObject> },
&[],
&["Box::new(Default::default())"],
);
}
#[test]
fn vec_string_replacement() {
check_replacements(
&parse_quote! { -> std::vec::Vec<String> },
&[],
&["vec![]", "vec![String::new()]", r#"vec!["xyzzy".into()]"#],
);
}
#[test]
fn float_replacement() {
check_replacements(&parse_quote! { -> f32 }, &[], &["0.0", "1.0", "-1.0"]);
}
#[test]
fn ref_replacement_leaks_values() {
check_replacements(
&parse_quote! { -> &'static String },
&[],
&[
"Box::leak(Box::new(String::new()))",
"Box::leak(Box::new(\"xyzzy\".into()))",
],
);
}
#[test]
fn ref_replacement_recurses() {
check_replacements(
&parse_quote! { -> &bool },
&[],
&["Box::leak(Box::new(true))", "Box::leak(Box::new(false))"],
);
}
#[test]
fn ref_mut() {
check_replacements(
&parse_quote! { -> &mut bool },
&[],
&["Box::leak(Box::new(true))", "Box::leak(Box::new(false))"],
);
}
#[test]
fn array_replacement() {
check_replacements(
&parse_quote! { -> [u8; 256] },
&[],
&["[0; 256]", "[1; 256]"],
);
}
#[test]
fn arc_replacement() {
check_replacements(
&parse_quote! { -> alloc::sync::Arc<String> },
&[],
&["Arc::new(String::new())", r#"Arc::new("xyzzy".into())"#],
);
}
#[test]
fn rc_replacement() {
check_replacements(
&parse_quote! { -> alloc::sync::Rc<String> },
&[],
&["Rc::new(String::new())", r#"Rc::new("xyzzy".into())"#],
);
}
#[test]
fn match_known_collection() {
assert_eq!(
super::known_collection(&parse_quote! { std::collections::VecDeque<String> }),
Some((&parse_quote! { VecDeque }, &parse_quote! { String }))
);
assert_eq!(
super::known_collection(&parse_quote! { std::collections::BinaryHeap<(u32, u32)> }),
Some((&parse_quote! { BinaryHeap }, &parse_quote! { (u32, u32) }))
);
assert_eq!(
super::known_collection(&parse_quote! { LinkedList<[u8; 256]> }),
Some((&parse_quote! { LinkedList }, &parse_quote! { [u8; 256] }))
);
assert_eq!(super::known_collection(&parse_quote! { Arc<String> }), None);
assert_eq!(
super::known_collection(&parse_quote! { Wibble<&str> }),
None
);
}
#[test]
fn match_known_map() {
assert_eq!(
super::known_map(&parse_quote! { std::collections::BTreeMap<String, usize> }),
Some((
&parse_quote! { BTreeMap },
&parse_quote! { String },
&parse_quote! { usize }
))
);
assert_eq!(
super::known_map(&parse_quote! { std::collections::HashMap<(usize, usize), bool> }),
Some((
&parse_quote! { HashMap },
&parse_quote! { (usize, usize) },
&parse_quote! { bool }
))
);
assert_eq!(
super::known_map(&parse_quote! { Option<(usize, usize)> }),
None
);
assert_eq!(
super::known_map(&parse_quote! { MyMap<String, usize> }),
None,
);
assert_eq!(
super::known_map(&parse_quote! { Pair<String, usize> }),
None,
);
}
#[test]
fn btreeset_replacement() {
check_replacements(
&parse_quote! { -> std::collections::BTreeSet<String> },
&[],
&[
"BTreeSet::new()",
"BTreeSet::from_iter([String::new()])",
r#"BTreeSet::from_iter(["xyzzy".into()])"#,
],
);
}
#[test]
fn cow_generates_borrowed_and_owned() {
check_replacements(
&parse_quote! { -> Cow<'static, str> },
&[],
&[
r#"Cow::Borrowed("")"#,
r#"Cow::Owned("".to_owned())"#,
r#"Cow::Borrowed("xyzzy")"#,
r#"Cow::Owned("xyzzy".to_owned())"#,
],
);
}
#[test]
fn unknown_container_replacement() {
check_replacements(
&parse_quote! { -> UnknownContainer<'static, str> },
&[],
&[
"UnknownContainer::new()",
r#"UnknownContainer::from_iter([""])"#,
r#"UnknownContainer::new("")"#,
r#"UnknownContainer::from("")"#,
r#"UnknownContainer::from_iter(["xyzzy"])"#,
r#"UnknownContainer::new("xyzzy")"#,
r#"UnknownContainer::from("xyzzy")"#,
],
);
}
#[test]
fn tuple_combinations() {
check_replacements(
&parse_quote! { -> (bool, usize) },
&[],
&["(true, 0)", "(true, 1)", "(false, 0)", "(false, 1)"],
);
}
#[test]
fn tuple_combination_longer() {
check_replacements(
&parse_quote! { -> (bool, Option<String>) },
&[],
&[
"(true, None)",
"(true, Some(String::new()))",
r#"(true, Some("xyzzy".into()))"#,
"(false, None)",
"(false, Some(String::new()))",
r#"(false, Some("xyzzy".into()))"#,
],
);
}
#[test]
fn iter_replacement() {
check_replacements(
&parse_quote! { -> impl Iterator<Item = String> },
&[],
&[
"::std::iter::empty()",
"::std::iter::once(String::new())",
r#"::std::iter::once("xyzzy".into())"#,
],
);
}
#[test]
fn impl_matches_iterator() {
assert_eq!(
match_impl_iterator(&parse_quote! { impl std::iter::Iterator<Item = String> }),
Some(&parse_quote! { String })
);
assert_eq!(
match_impl_iterator(&parse_quote! { impl Iterator<Item = String> }),
Some(&parse_quote! { String })
);
assert_eq!(match_impl_iterator(&parse_quote! { impl Iterator }), None);
assert_eq!(
match_impl_iterator(&parse_quote! { impl Borrow<String> }),
None
);
}
#[test]
fn slice_replacement() {
check_replacements(
&parse_quote! { -> [u8] },
&[],
&[
"Vec::leak(Vec::new())",
"Vec::leak(vec![0])",
"Vec::leak(vec![1])",
],
);
}
#[test]
fn btreemap_replacement() {
check_replacements(
&parse_quote! { -> BTreeMap<String, bool> },
&[],
&[
"BTreeMap::new()",
"BTreeMap::from_iter([(String::new(), true)])",
"BTreeMap::from_iter([(String::new(), false)])",
"BTreeMap::from_iter([(\"xyzzy\".into(), true)])",
"BTreeMap::from_iter([(\"xyzzy\".into(), false)])",
],
);
}
fn check_replacements(return_type: &ReturnType, error_exprs: &[Expr], expected: &[&str]) {
assert_eq!(
return_type_replacements(return_type, error_exprs)
.into_iter()
.map(|t| t.to_pretty_string())
.collect_vec(),
expected
);
}
#[test]
fn match_map() {
assert!(known_map(&parse_quote! { BTreeMap<String, usize> }).is_some());
assert!(known_map(&parse_quote! { HashMap<(usize, usize), bool> }).is_some());
assert!(known_map(&parse_quote! { Option<(usize, usize)> }).is_none());
}
}