//! Macros for Ridicule, a mocking library supporting non-static generics.
#![deny(clippy::all, clippy::pedantic, missing_docs)]
use proc_macro::TokenStream;
use proc_macro_error::{proc_macro_error, ResultExt};
use quote::{format_ident, quote};
use syn::{
parse,
FnArg,
GenericArgument,
ImplItem,
Path,
PathArguments,
PathSegment,
ReturnType,
Type,
TypeBareFn,
TypeParamBound,
};
use crate::expectation::Expectation;
use crate::mock::Mock;
use crate::mock_input::MockInput;
use crate::syn_ext::{PathExt, PathSegmentExt, WithLeadingColons};
use crate::util::create_path;
mod expectation;
mod mock;
mod mock_input;
mod syn_ext;
mod util;
/// Creates a mock.
///
/// # Examples
/// ```
/// use ridicule::mock;
///
/// trait Foo
/// {
/// fn bar(&self, a: A) -> B;
/// }
///
/// mock! {
/// MockFoo {}
///
/// impl Foo for MockFoo
/// {
/// fn bar(&self, a: A) -> B;
/// }
/// }
///
/// fn main()
/// {
/// let mut mock_foo = MockFoo::new();
///
/// mock_foo
/// .expect_bar()
/// .returning(|foo, a: u32| format!("Hello {a}"));
///
/// assert_eq!(mock_foo.bar::(123), "Hello 123");
/// }
/// ```
#[proc_macro]
#[proc_macro_error]
pub fn mock(input_stream: TokenStream) -> TokenStream
{
let input = parse::(input_stream.clone()).unwrap_or_abort();
let mock_ident = input.mock;
let mock_mod_ident = format_ident!("__{mock_ident}");
let method_items = input
.item_impl
.items
.into_iter()
.filter_map(|item| match item {
ImplItem::Method(mut item_method) => {
item_method.sig.inputs = item_method
.sig
.inputs
.into_iter()
.map(|fn_arg| match fn_arg {
FnArg::Typed(mut typed_arg) => {
typed_arg.ty = Box::new(replace_path_in_type(
*typed_arg.ty,
&create_path!(Self),
&Path::new(
WithLeadingColons::No,
[PathSegment::new(mock_ident.clone(), None)],
),
));
FnArg::Typed(typed_arg)
}
FnArg::Receiver(receiver) => FnArg::Receiver(receiver),
})
.collect();
item_method.sig.output = match item_method.sig.output {
ReturnType::Type(r_arrow, return_type) => ReturnType::Type(
r_arrow,
Box::new(replace_path_in_type(
*return_type,
&create_path!(Self),
&Path::new(
WithLeadingColons::No,
[PathSegment::new(mock_ident.clone(), None)],
),
)),
),
ReturnType::Default => ReturnType::Default,
};
Some(item_method)
}
_ => None,
})
.collect::>();
let mock = Mock::new(
mock_ident.clone(),
input.mocked_trait,
&method_items,
input.item_impl.generics.clone(),
);
let expectations = method_items.iter().map(|item_method| {
Expectation::new(
&mock_ident,
item_method,
input.item_impl.generics.params.clone(),
)
});
quote! {
mod #mock_mod_ident {
use super::*;
#mock
#(#expectations)*
}
use #mock_mod_ident::#mock_ident;
}
.into()
}
fn replace_path_in_type(ty: Type, target_path: &Path, replacement_path: &Path) -> Type
{
match ty {
Type::Ptr(mut type_ptr) => {
type_ptr.elem = Box::new(replace_path_in_type(
*type_ptr.elem,
target_path,
replacement_path,
));
Type::Ptr(type_ptr)
}
Type::Path(mut type_path) => {
if &type_path.path == target_path {
type_path.path = replacement_path.clone();
} else {
type_path.path =
replace_path_args(type_path.path, target_path, replacement_path);
}
Type::Path(type_path)
}
Type::Array(mut type_array) => {
type_array.elem = Box::new(replace_path_in_type(
*type_array.elem,
target_path,
replacement_path,
));
Type::Array(type_array)
}
Type::Group(mut type_group) => {
type_group.elem = Box::new(replace_path_in_type(
*type_group.elem,
target_path,
replacement_path,
));
Type::Group(type_group)
}
Type::BareFn(type_bare_fn) => Type::BareFn(replace_type_bare_fn_type_paths(
type_bare_fn,
target_path,
replacement_path,
)),
Type::Paren(mut type_paren) => {
type_paren.elem = Box::new(replace_path_in_type(
*type_paren.elem,
target_path,
replacement_path,
));
Type::Paren(type_paren)
}
Type::Slice(mut type_slice) => {
type_slice.elem = Box::new(replace_path_in_type(
*type_slice.elem,
target_path,
replacement_path,
));
Type::Slice(type_slice)
}
Type::Tuple(mut type_tuple) => {
type_tuple.elems = type_tuple
.elems
.into_iter()
.map(|elem_type| {
replace_path_in_type(elem_type, target_path, replacement_path)
})
.collect();
Type::Tuple(type_tuple)
}
Type::Reference(mut type_reference) => {
type_reference.elem = Box::new(replace_path_in_type(
*type_reference.elem,
target_path,
replacement_path,
));
Type::Reference(type_reference)
}
Type::TraitObject(mut type_trait_object) => {
type_trait_object.bounds = type_trait_object
.bounds
.into_iter()
.map(|bound| match bound {
TypeParamBound::Trait(mut trait_bound) => {
trait_bound.path = replace_path_args(
trait_bound.path,
target_path,
replacement_path,
);
TypeParamBound::Trait(trait_bound)
}
TypeParamBound::Lifetime(lifetime) => {
TypeParamBound::Lifetime(lifetime)
}
})
.collect();
Type::TraitObject(type_trait_object)
}
other_type => other_type,
}
}
fn replace_path_args(mut path: Path, target_path: &Path, replacement_path: &Path)
-> Path
{
path.segments = path
.segments
.into_iter()
.map(|mut segment| {
segment.arguments = match segment.arguments {
PathArguments::AngleBracketed(mut generic_args) => {
generic_args.args = generic_args
.args
.into_iter()
.map(|generic_arg| match generic_arg {
GenericArgument::Type(ty) => GenericArgument::Type(
replace_path_in_type(ty, target_path, replacement_path),
),
GenericArgument::Binding(mut binding) => {
binding.ty = replace_path_in_type(
binding.ty,
target_path,
replacement_path,
);
GenericArgument::Binding(binding)
}
generic_arg => generic_arg,
})
.collect();
PathArguments::AngleBracketed(generic_args)
}
PathArguments::Parenthesized(mut generic_args) => {
generic_args.inputs = generic_args
.inputs
.into_iter()
.map(|input_ty| {
replace_path_in_type(input_ty, target_path, replacement_path)
})
.collect();
generic_args.output = match generic_args.output {
ReturnType::Type(r_arrow, return_type) => ReturnType::Type(
r_arrow,
Box::new(replace_path_in_type(
*return_type,
target_path,
replacement_path,
)),
),
ReturnType::Default => ReturnType::Default,
};
PathArguments::Parenthesized(generic_args)
}
PathArguments::None => PathArguments::None,
};
segment
})
.collect();
path
}
fn replace_type_bare_fn_type_paths(
mut type_bare_fn: TypeBareFn,
target_path: &Path,
replacement_path: &Path,
) -> TypeBareFn
{
type_bare_fn.inputs = type_bare_fn
.inputs
.into_iter()
.map(|mut bare_fn_arg| {
bare_fn_arg.ty =
replace_path_in_type(bare_fn_arg.ty, target_path, replacement_path);
bare_fn_arg
})
.collect();
type_bare_fn.output = match type_bare_fn.output {
ReturnType::Type(r_arrow, return_type) => ReturnType::Type(
r_arrow,
Box::new(replace_path_in_type(
*return_type,
target_path,
replacement_path,
)),
),
ReturnType::Default => ReturnType::Default,
};
type_bare_fn
}