summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.toml5
-rw-r--r--examples/simple.rs20
-rw-r--r--macros/Cargo.toml13
-rw-r--r--macros/src/expectation.rs376
-rw-r--r--macros/src/lib.rs54
-rw-r--r--macros/src/mock.rs313
-rw-r--r--macros/src/mock_input.rs51
-rw-r--r--macros/src/syn_ext.rs372
-rw-r--r--macros/src/util.rs22
-rw-r--r--src/lib.rs229
10 files changed, 1223 insertions, 232 deletions
diff --git a/Cargo.toml b/Cargo.toml
index da8de9a..8edf1f1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,5 +6,8 @@ license = "MIT OR Apache-2.0"
description = "Rust mocking library supporting non-static function generics"
repository = "https://git.hampusmat.com/ridicule"
+[workspace]
+members = ["macros"]
+
[dependencies]
-paste = "1.0.12"
+ridicule-macros = { path = "./macros" }
diff --git a/examples/simple.rs b/examples/simple.rs
index a5b7203..cb6feb9 100644
--- a/examples/simple.rs
+++ b/examples/simple.rs
@@ -19,14 +19,14 @@ trait Foo
}
mock! {
- MockFoo;
+ MockFoo {}
impl Foo for MockFoo {
- fn bar<Baz>(self: (&Self), num: u128) -> Baz;
+ fn bar<Baz>(&self, num: u128) -> Baz;
- fn biz<Fiz: (Debug +), Bar>(self: (&Self), fiz: Fiz) -> &Bar;
+ fn biz<'a, Fiz: Debug, Bar>(&'a self, fiz: Fiz) -> &'a Bar;
- fn baz<Foobar>(self: (&Self), name: &str, foobar: Foobar)
+ fn baz<Foobar>(&self, name: &str, foobar: Foobar)
where
Foobar: SomeFoobar + Debug;
}
@@ -38,7 +38,17 @@ fn main()
mock_foo.expect_bar().returning(|_me, num| {
println!("bar was called with {num}");
+
+ "Hello".to_string()
+ });
+
+ mock_foo.expect_bar::<u128>().returning(|_me, num| {
+ println!("bar was called with {num}");
+
+ 136322
});
- mock_foo.bar::<()>(123);
+ assert_eq!(mock_foo.bar::<String>(123), "Hello".to_string());
+
+ assert_eq!(mock_foo.bar::<u128>(456), 136322);
}
diff --git a/macros/Cargo.toml b/macros/Cargo.toml
new file mode 100644
index 0000000..00efccf
--- /dev/null
+++ b/macros/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "ridicule-macros"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+proc-macro = true
+
+[dependencies]
+quote = "1.0.26"
+syn = { version = "1.0.109", features = ["full", "printing"] }
+proc-macro-error = "1.0.4"
+proc-macro2 = "1.0.52"
diff --git a/macros/src/expectation.rs b/macros/src/expectation.rs
new file mode 100644
index 0000000..ff3d192
--- /dev/null
+++ b/macros/src/expectation.rs
@@ -0,0 +1,376 @@
+use proc_macro2::{Ident, TokenStream};
+use quote::{format_ident, quote, ToTokens};
+use syn::punctuated::Punctuated;
+use syn::token::Brace;
+use syn::{
+ AngleBracketedGenericArguments,
+ Attribute,
+ BareFnArg,
+ Field,
+ Fields,
+ FieldsNamed,
+ FnArg,
+ GenericArgument,
+ GenericParam,
+ Generics,
+ ItemStruct,
+ Lifetime,
+ Path,
+ PathSegment,
+ Receiver,
+ ReturnType,
+ Token,
+ TraitItemMethod,
+ Type,
+ TypeBareFn,
+ TypePath,
+ TypeReference,
+ Visibility,
+};
+
+use crate::syn_ext::{
+ AngleBracketedGenericArgumentsExt,
+ AttributeExt,
+ AttributeStyle,
+ BareFnArgExt,
+ GenericsExt,
+ IsMut,
+ LifetimeExt,
+ PathExt,
+ PathSegmentExt,
+ TypeBareFnExt,
+ TypePathExt,
+ TypeReferenceExt,
+ VisibilityExt,
+ WithColons,
+ WithLeadingColons,
+};
+use crate::util::{create_path, create_unit_type_tuple};
+
+pub struct Expectation
+{
+ ident: Ident,
+ generics: Generics,
+ receiver: Option<Receiver>,
+ mock: Ident,
+ arg_types: Vec<Type>,
+ return_type: ReturnType,
+ phantom_fields: Vec<PhantomField>,
+}
+
+impl Expectation
+{
+ pub fn new(mock: &Ident, item_method: &TraitItemMethod) -> Self
+ {
+ let ident = create_expectation_ident(mock, &item_method.sig.ident);
+
+ let phantom_fields =
+ Self::create_phantom_fields(&item_method.sig.generics.params);
+
+ let receiver =
+ item_method
+ .sig
+ .inputs
+ .first()
+ .and_then(|first_arg| match first_arg {
+ FnArg::Receiver(receiver) => Some(receiver.clone()),
+ FnArg::Typed(_) => None,
+ });
+
+ let arg_types = item_method
+ .sig
+ .inputs
+ .iter()
+ .filter_map(|arg| match arg {
+ FnArg::Typed(typed_arg) => Some(*typed_arg.ty.clone()),
+ FnArg::Receiver(_) => None,
+ })
+ .collect::<Vec<_>>();
+
+ let return_type = item_method.sig.output.clone();
+
+ Self {
+ ident,
+ generics: item_method.sig.generics.clone(),
+ receiver,
+ mock: mock.clone(),
+ arg_types,
+ return_type,
+ phantom_fields,
+ }
+ }
+
+ fn create_phantom_fields(
+ generic_params: &Punctuated<GenericParam, Token![,]>,
+ ) -> Vec<PhantomField>
+ {
+ generic_params
+ .iter()
+ .filter_map(|generic_param| match generic_param {
+ GenericParam::Type(type_param) => {
+ let type_param_ident = &type_param.ident;
+
+ let field_ident = create_phantom_field_ident(
+ type_param_ident,
+ &PhantomFieldKind::Type,
+ );
+
+ let ty = create_phantom_data_type_path([GenericArgument::Type(
+ Type::Path(TypePath::new(Path::new(
+ WithLeadingColons::No,
+ [PathSegment::new(type_param_ident.clone(), None)],
+ ))),
+ )]);
+
+ Some(PhantomField {
+ field: field_ident,
+ type_path: ty,
+ })
+ }
+ GenericParam::Lifetime(lifetime_param) => {
+ let lifetime = &lifetime_param.lifetime;
+
+ let field_ident = create_phantom_field_ident(
+ &lifetime.ident,
+ &PhantomFieldKind::Lifetime,
+ );
+
+ let ty = create_phantom_data_type_path([GenericArgument::Type(
+ Type::Reference(TypeReference::new(
+ Some(lifetime.clone()),
+ IsMut::No,
+ Type::Tuple(create_unit_type_tuple()),
+ )),
+ )]);
+
+ Some(PhantomField {
+ field: field_ident,
+ type_path: ty,
+ })
+ }
+ GenericParam::Const(_) => None,
+ })
+ .collect()
+ }
+}
+
+impl ToTokens for Expectation
+{
+ fn to_tokens(&self, tokens: &mut TokenStream)
+ {
+ let generic_params = &self.generics.params;
+
+ let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl();
+
+ let bogus_generics = create_bogus_generics(generic_params);
+
+ let opt_self_type = receiver_to_mock_self_type(&self.receiver, self.mock.clone());
+
+ let ident = &self.ident;
+ let phantom_fields = &self.phantom_fields;
+
+ let returning_fn = Type::BareFn(TypeBareFn::new(
+ opt_self_type
+ .iter()
+ .chain(self.arg_types.iter())
+ .map(|ty| BareFnArg::new(ty.clone())),
+ self.return_type.clone(),
+ ));
+
+ let expectation_struct = ItemStruct {
+ attrs: vec![Attribute::new(
+ AttributeStyle::Outer,
+ create_path!(allow),
+ quote! { (non_camel_case_types, non_snake_case) },
+ )],
+ vis: Visibility::new_pub_crate(),
+ struct_token: <Token![struct]>::default(),
+ ident: self.ident.clone(),
+ generics: self.generics.clone().without_where_clause(),
+ fields: Fields::Named(FieldsNamed {
+ brace_token: Brace::default(),
+ named: [Field {
+ attrs: vec![],
+ vis: Visibility::Inherited,
+ ident: Some(format_ident!("returning")),
+ colon_token: Some(<Token![:]>::default()),
+ ty: Type::Path(TypePath::new(Path::new(
+ WithLeadingColons::No,
+ [PathSegment::new(
+ format_ident!("Option"),
+ Some(AngleBracketedGenericArguments::new(
+ WithColons::No,
+ [GenericArgument::Type(returning_fn.clone())],
+ )),
+ )],
+ ))),
+ }]
+ .into_iter()
+ .chain(phantom_fields.iter().map(|phantom_field| Field {
+ attrs: vec![],
+ vis: Visibility::Inherited,
+ ident: Some(phantom_field.field.clone()),
+ colon_token: Some(<Token![:]>::default()),
+ ty: Type::Path(phantom_field.type_path.clone()),
+ }))
+ .collect(),
+ }),
+ semi_token: None,
+ };
+
+ quote! {
+ #expectation_struct
+
+ impl #impl_generics #ident #ty_generics #where_clause
+ {
+ fn new() -> Self {
+ Self {
+ returning: None,
+ #(#phantom_fields),*
+ }
+ }
+
+ #[allow(unused)]
+ pub fn returning(
+ &mut self,
+ func: #returning_fn
+ ) -> &mut Self
+ {
+ self.returning = Some(func);
+
+ self
+ }
+
+ #[allow(unused)]
+ fn strip_generic_params(
+ self,
+ ) -> #ident<#(#bogus_generics),*>
+ {
+ unsafe { std::mem::transmute(self) }
+ }
+ }
+
+ impl #ident<#(#bogus_generics),*> {
+ #[allow(unused)]
+ fn with_generic_params<#generic_params>(
+ &self,
+ ) -> &#ident #ty_generics
+ {
+ // SAFETY: self is a pointer to a sane place, Rustc guarantees that
+ // by it being a reference. The generic parameters doesn't affect
+ // the size of self in any way, as they are only used in the function
+ // pointer field "returning"
+ unsafe { &*(self as *const Self).cast() }
+ }
+
+ #[allow(unused)]
+ fn with_generic_params_mut<#generic_params>(
+ &mut self,
+ ) -> &mut #ident #ty_generics
+ {
+ // SAFETY: self is a pointer to a sane place, Rustc guarantees that
+ // by it being a reference. The generic parameters doesn't affect
+ // the size of self in any way, as they are only used in the function
+ // pointer field "returning"
+ unsafe { &mut *(self as *mut Self).cast() }
+ }
+ }
+ }
+ .to_tokens(tokens);
+ }
+}
+
+pub fn create_expectation_ident(mock: &Ident, method: &Ident) -> Ident
+{
+ format_ident!("{mock}Expectation_{method}")
+}
+
+struct PhantomField
+{
+ field: Ident,
+ type_path: TypePath,
+}
+
+impl ToTokens for PhantomField
+{
+ fn to_tokens(&self, tokens: &mut TokenStream)
+ {
+ self.field.to_tokens(tokens);
+
+ <Token![:]>::default().to_tokens(tokens);
+
+ self.type_path.to_tokens(tokens);
+ }
+}
+
+fn create_phantom_field_ident(ident: &Ident, kind: &PhantomFieldKind) -> Ident
+{
+ match kind {
+ PhantomFieldKind::Type => format_ident!("{ident}_phantom"),
+ PhantomFieldKind::Lifetime => format_ident!("{ident}_lt_phantom"),
+ }
+}
+
+enum PhantomFieldKind
+{
+ Type,
+ Lifetime,
+}
+
+fn create_phantom_data_type_path(
+ generic_args: impl IntoIterator<Item = GenericArgument>,
+) -> TypePath
+{
+ TypePath::new(Path::new(
+ WithLeadingColons::Yes,
+ [
+ PathSegment::new(format_ident!("std"), None),
+ PathSegment::new(format_ident!("marker"), None),
+ PathSegment::new(
+ format_ident!("PhantomData"),
+ Some(AngleBracketedGenericArguments::new(
+ WithColons::Yes,
+ generic_args,
+ )),
+ ),
+ ],
+ ))
+}
+
+fn create_bogus_generics(
+ generic_params: &Punctuated<GenericParam, Token![,]>,
+) -> Vec<GenericArgument>
+{
+ generic_params
+ .iter()
+ .filter_map(|generic_param| match generic_param {
+ GenericParam::Type(_) => {
+ Some(GenericArgument::Type(Type::Tuple(create_unit_type_tuple())))
+ }
+ GenericParam::Lifetime(_) => Some(GenericArgument::Lifetime(
+ Lifetime::create(format_ident!("static")),
+ )),
+ GenericParam::Const(_) => None,
+ })
+ .collect()
+}
+
+fn receiver_to_mock_self_type(receiver: &Option<Receiver>, mock: Ident) -> Option<Type>
+{
+ receiver.as_ref().map(|receiver| {
+ let self_type = Type::Path(TypePath::new(Path::new(
+ WithLeadingColons::No,
+ [PathSegment::new(mock, None)],
+ )));
+
+ if let Some((_, lifetime)) = &receiver.reference {
+ return Type::Reference(TypeReference::new(
+ lifetime.clone(),
+ receiver.mutability.into(),
+ self_type,
+ ));
+ }
+
+ self_type
+ })
+}
diff --git a/macros/src/lib.rs b/macros/src/lib.rs
new file mode 100644
index 0000000..ce91f87
--- /dev/null
+++ b/macros/src/lib.rs
@@ -0,0 +1,54 @@
+#![deny(clippy::all, clippy::pedantic)]
+use proc_macro::TokenStream;
+use proc_macro_error::{proc_macro_error, ResultExt};
+use quote::{format_ident, quote};
+use syn::{parse, TraitItem};
+
+use crate::expectation::Expectation;
+use crate::mock::Mock;
+use crate::mock_input::MockInput;
+
+mod expectation;
+mod mock;
+mod mock_input;
+mod syn_ext;
+mod util;
+
+#[proc_macro]
+#[proc_macro_error]
+pub fn mock(input_stream: TokenStream) -> TokenStream
+{
+ let input = parse::<MockInput>(input_stream.clone()).unwrap_or_abort();
+
+ let mock_ident = input.mock;
+
+ let mock_mod_ident = format_ident!("__{mock_ident}");
+
+ let method_items = input
+ .items
+ .into_iter()
+ .filter_map(|item| match item {
+ TraitItem::Method(item_method) => Some(item_method),
+ _ => None,
+ })
+ .collect::<Vec<_>>();
+
+ let mock = Mock::new(mock_ident.clone(), input.mocked_trait, &method_items);
+
+ let expectations = method_items
+ .iter()
+ .map(|item_method| Expectation::new(&mock_ident, item_method));
+
+ quote! {
+ mod #mock_mod_ident {
+ use super::*;
+
+ #mock
+
+ #(#expectations)*
+ }
+
+ use #mock_mod_ident::#mock_ident;
+ }
+ .into()
+}
diff --git a/macros/src/mock.rs b/macros/src/mock.rs
new file mode 100644
index 0000000..88d3434
--- /dev/null
+++ b/macros/src/mock.rs
@@ -0,0 +1,313 @@
+use proc_macro2::{Ident, Span, TokenStream};
+use proc_macro_error::{abort_call_site, ResultExt};
+use quote::{format_ident, quote, ToTokens};
+use syn::{
+ parse2,
+ AngleBracketedGenericArguments,
+ FnArg,
+ GenericArgument,
+ GenericParam,
+ ImplItemMethod,
+ Lifetime,
+ Pat,
+ Path,
+ PathSegment,
+ Receiver,
+ ReturnType,
+ Signature,
+ Token,
+ TraitItemMethod,
+ Type,
+ TypePath,
+ TypeReference,
+ Visibility,
+};
+
+use crate::expectation::create_expectation_ident;
+use crate::syn_ext::{
+ AngleBracketedGenericArgumentsExt,
+ GenericArgumentExt,
+ IsMut,
+ PathExt,
+ PathSegmentExt,
+ ReturnTypeExt,
+ SignatureExt,
+ TypePathExt,
+ TypeReferenceExt,
+ VisibilityExt,
+ WithColons,
+ WithLeadingColons,
+};
+use crate::util::create_unit_type_tuple;
+
+pub struct Mock
+{
+ ident: Ident,
+ mocked_trait: TypePath,
+ expectations_fields: Vec<ExpectationsField>,
+ item_methods: Vec<TraitItemMethod>,
+}
+
+impl Mock
+{
+ pub fn new(
+ ident: Ident,
+ mocked_trait: TypePath,
+ item_methods: &[TraitItemMethod],
+ ) -> Self
+ {
+ let expectations_fields = item_methods
+ .iter()
+ .map(|method_item| {
+ let generic_args = method_item
+ .sig
+ .generics
+ .params
+ .iter()
+ .filter_map(|generic_param| match generic_param {
+ GenericParam::Type(_) => Some(GenericArgument::Type(
+ Type::Tuple(create_unit_type_tuple()),
+ )),
+ GenericParam::Lifetime(_) => {
+ Some(GenericArgument::Lifetime(Lifetime {
+ apostrophe: Span::call_site(),
+ ident: format_ident!("static"),
+ }))
+ }
+ GenericParam::Const(_) => None,
+ })
+ .collect::<Vec<_>>();
+
+ ExpectationsField {
+ field_ident: format_ident!("{}_expectations", method_item.sig.ident),
+ expectation_ident: create_expectation_ident(
+ &ident,
+ &method_item.sig.ident,
+ ),
+ generic_args,
+ }
+ })
+ .collect::<Vec<_>>();
+
+ Self {
+ ident,
+ mocked_trait,
+ expectations_fields,
+ item_methods: item_methods.to_vec(),
+ }
+ }
+}
+
+impl ToTokens for Mock
+{
+ fn to_tokens(&self, tokens: &mut TokenStream)
+ {
+ let Self {
+ ident,
+ mocked_trait,
+ expectations_fields,
+ item_methods,
+ } = self;
+
+ let expectations_field_idents = expectations_fields
+ .iter()
+ .map(|expectations_field| expectations_field.field_ident.clone());
+
+ let mock_functions = item_methods
+ .iter()
+ .map(|item_method| create_mock_function(item_method.clone()));
+
+ let expect_functions = item_methods
+ .iter()
+ .map(|item_method| create_expect_function(&self.ident, &item_method.clone()))
+ .collect::<Vec<_>>();
+
+ quote! {
+ pub struct #ident
+ {
+ #(#expectations_fields),*
+ }
+
+ impl #ident
+ {
+ pub fn new() -> Self
+ {
+ Self {
+ #(
+ #expectations_field_idents: ::std::collections::HashMap::new()
+ ),*
+ }
+ }
+
+ #(#expect_functions)*
+ }
+
+ impl #mocked_trait for #ident {
+ #(
+ #mock_functions
+ )*
+ }
+ }
+ .to_tokens(tokens);
+ }
+}
+
+struct ExpectationsField
+{
+ field_ident: Ident,
+ expectation_ident: Ident,
+ generic_args: Vec<GenericArgument>,
+}
+
+impl ToTokens for ExpectationsField
+{
+ fn to_tokens(&self, tokens: &mut TokenStream)
+ {
+ let Self {
+ field_ident,
+ expectation_ident,
+ generic_args,
+ } = self;
+
+ quote! {
+ #field_ident: ::std::collections::HashMap<
+ Vec<::ridicule::__private::type_id::TypeID>,
+ #expectation_ident<#(#generic_args),*>
+ >
+ }
+ .to_tokens(tokens);
+ }
+}
+
+fn create_mock_function(item_method: TraitItemMethod) -> ImplItemMethod
+{
+ let func_ident = &item_method.sig.ident;
+
+ let type_param_idents = item_method
+ .sig
+ .generics
+ .type_params()
+ .map(|type_param| type_param.ident.clone())
+ .collect::<Vec<_>>();
+
+ let args = item_method
+ .sig
+ .inputs
+ .iter()
+ .map(|fn_arg| match fn_arg {
+ FnArg::Receiver(_) => format_ident!("self"),
+ FnArg::Typed(pat_type) => {
+ let Pat::Ident(pat_ident) = pat_type.pat.as_ref() else {
+ abort_call_site!("Unsupport argument pattern");
+ };
+
+ pat_ident.ident.clone()
+ }
+ })
+ .collect::<Vec<_>>();
+
+ let expectations_field = format_ident!("{func_ident}_expectations");
+
+ let ids = quote! {
+ let ids = vec![
+ #(::ridicule::__private::type_id::TypeID::of::<#type_param_idents>()),*
+ ];
+ };
+
+ ImplItemMethod {
+ attrs: item_method.attrs,
+ vis: Visibility::Inherited,
+ defaultness: None,
+ sig: item_method.sig.clone(),
+ block: parse2(quote! {
+ {
+ #ids
+
+ let expectation = self
+ .#expectations_field
+ .get(&ids)
+ .expect(concat!(
+ "No expectation found for function ",
+ stringify!(#func_ident)
+ ))
+ .with_generic_params::<#(#type_param_idents),*>();
+
+ let Some(returning) = &expectation.returning else {
+ panic!(concat!(
+ "Expectation for function",
+ stringify!(#func_ident),
+ " is missing a function to call")
+ );
+ };
+
+ returning(#(#args),*)
+ }
+ })
+ .unwrap_or_abort(),
+ }
+}
+
+fn create_expect_function(mock: &Ident, item_method: &TraitItemMethod) -> ImplItemMethod
+{
+ let signature = Signature::new(
+ format_ident!("expect_{}", item_method.sig.ident),
+ item_method.sig.generics.clone(),
+ [FnArg::Receiver(Receiver {
+ attrs: vec![],
+ reference: Some((<Token![&]>::default(), None)),
+ mutability: Some(<Token![mut]>::default()),
+ self_token: <Token![self]>::default(),
+ })],
+ ReturnType::new(Type::Reference(TypeReference::new(
+ None,
+ IsMut::Yes,
+ Type::Path(TypePath::new(Path::new(
+ WithLeadingColons::No,
+ [PathSegment::new(
+ create_expectation_ident(mock, &item_method.sig.ident),
+ Some(AngleBracketedGenericArguments::new(
+ WithColons::No,
+ item_method.sig.generics.params.iter().map(|generic_param| {
+ GenericArgument::from_generic_param(generic_param.clone())
+ }),
+ )),
+ )],
+ ))),
+ ))),
+ );
+
+ let type_param_idents = item_method
+ .sig
+ .generics
+ .type_params()
+ .map(|type_param| type_param.ident.clone())
+ .collect::<Vec<_>>();
+
+ let expectation = create_expectation_ident(mock, &item_method.sig.ident);
+
+ let expectations_field = format_ident!("{}_expectations", item_method.sig.ident);
+
+ ImplItemMethod {
+ attrs: item_method.attrs.clone(),
+ vis: Visibility::new_pub_crate(),
+ defaultness: None,
+ sig: signature,
+ block: parse2(quote! {{
+ let ids = vec![
+ #(::ridicule::__private::type_id::TypeID::of::<#type_param_idents>()),*
+ ];
+
+ let expectation =
+ #expectation::<#(#type_param_idents),*>::new()
+ .strip_generic_params();
+
+ self.#expectations_field.insert(ids.clone(), expectation);
+
+ self.#expectations_field
+ .get_mut(&ids)
+ .unwrap()
+ .with_generic_params_mut()
+ }})
+ .unwrap_or_abort(),
+ }
+}
diff --git a/macros/src/mock_input.rs b/macros/src/mock_input.rs
new file mode 100644
index 0000000..379c342
--- /dev/null
+++ b/macros/src/mock_input.rs
@@ -0,0 +1,51 @@
+use syn::parse::{Parse, ParseStream};
+use syn::{braced, Ident, Token, TraitItem, TypePath, WhereClause};
+
+pub struct MockInput
+{
+ pub mock: Ident,
+ pub mocked_trait: TypePath,
+ pub items: Vec<TraitItem>,
+}
+
+impl Parse for MockInput
+{
+ fn parse(input: ParseStream) -> Result<Self, syn::Error>
+ {
+ let mock = input.parse()?;
+
+ let _generics = input.parse::<Option<WhereClause>>()?;
+
+ let _braced_content;
+
+ let _brace = braced!(_braced_content in input);
+
+ input.parse::<Token![impl]>()?;
+
+ let mocked_trait = input.parse()?;
+
+ input.parse::<Token![for]>()?;
+
+ let impl_target = input.parse::<Ident>()?;
+
+ if impl_target != mock {
+ return Err(input.error("Expected this to be the mock"));
+ }
+
+ let content;
+
+ braced!(content in input);
+
+ let mut items = Vec::new();
+
+ while !content.is_empty() {
+ items.push(content.parse()?);
+ }
+
+ Ok(Self {
+ mock,
+ mocked_trait,
+ items,
+ })
+ }
+}
diff --git a/macros/src/syn_ext.rs b/macros/src/syn_ext.rs
new file mode 100644
index 0000000..7a3e9ae
--- /dev/null
+++ b/macros/src/syn_ext.rs
@@ -0,0 +1,372 @@
+use proc_macro2::{Ident, Span, TokenStream};
+use quote::format_ident;
+use syn::token::{Bracket, Paren};
+use syn::{
+ AngleBracketedGenericArguments,
+ AttrStyle,
+ Attribute,
+ BareFnArg,
+ FnArg,
+ GenericArgument,
+ GenericParam,
+ Generics,
+ Lifetime,
+ Path,
+ PathArguments,
+ PathSegment,
+ ReturnType,
+ Signature,
+ Token,
+ Type,
+ TypeBareFn,
+ TypePath,
+ TypeReference,
+ VisRestricted,
+ Visibility,
+};
+
+pub trait GenericsExt: Sized
+{
+ fn without_where_clause(self) -> Self;
+}
+
+impl GenericsExt for Generics
+{
+ fn without_where_clause(mut self) -> Self
+ {
+ self.where_clause = None;
+
+ self
+ }
+}
+
+pub trait AttributeExt: Sized
+{
+ fn new(style: AttributeStyle, path: Path, token_stream: TokenStream) -> Self;
+}
+
+impl AttributeExt for Attribute
+{
+ fn new(style: AttributeStyle, path: Path, token_stream: TokenStream) -> Self
+ {
+ Self {
+ pound_token: <Token![#]>::default(),
+ style: style.into(),
+ bracket_token: Bracket::default(),
+ path,
+ tokens: token_stream,
+ }
+ }
+}
+
+pub enum AttributeStyle
+{
+ /// A outer attribute. For example like `#[repr(C)]`.
+ Outer,
+
+ /// A inner attribute. For example like `#![deny(clippy::all)]`.
+ Inner,
+}
+
+impl From<AttributeStyle> for AttrStyle
+{
+ fn from(style: AttributeStyle) -> Self
+ {
+ match style {
+ AttributeStyle::Outer => Self::Outer,
+ AttributeStyle::Inner => Self::Inner(<Token![!]>::default()),
+ }
+ }
+}
+
+pub trait VisibilityExt: Sized
+{
+ /// Returns a new `pub(crate)` visibility.
+ fn new_pub_crate() -> Self;
+}
+
+impl VisibilityExt for Visibility
+{
+ fn new_pub_crate() -> Self
+ {
+ Self::Restricted(VisRestricted {
+ pub_token: <Token![pub]>::default(),
+ paren_token: Paren::default(),
+ in_token: None,
+ path: Box::new(Path::new(
+ WithLeadingColons::No,
+ [PathSegment::new(format_ident!("crate"), None)],
+ )),
+ })
+ }
+}
+
+pub trait PathExt: Sized
+{
+ fn new(
+ with_leading_colons: WithLeadingColons,
+ segments: impl IntoIterator<Item = PathSegment>,
+ ) -> Self;
+}
+
+impl PathExt for Path
+{
+ fn new(
+ with_leading_colons: WithLeadingColons,
+ segments: impl IntoIterator<Item = PathSegment>,
+ ) -> Self
+ {
+ Self {
+ leading_colon: match with_leading_colons {
+ WithLeadingColons::Yes => Some(<Token![::]>::default()),
+ WithLeadingColons::No => None,
+ },
+ segments: segments.into_iter().collect(),
+ }
+ }
+}
+
+pub enum WithLeadingColons
+{
+ Yes,
+ No,
+}
+
+pub trait PathSegmentExt: Sized
+{
+ fn new(ident: Ident, args: Option<AngleBracketedGenericArguments>) -> Self;
+}
+
+impl PathSegmentExt for PathSegment
+{
+ fn new(ident: Ident, args: Option<AngleBracketedGenericArguments>) -> Self
+ {
+ Self {
+ ident,
+ arguments: args
+ .map_or_else(|| PathArguments::None, PathArguments::AngleBracketed),
+ }
+ }
+}
+
+pub trait AngleBracketedGenericArgumentsExt: Sized
+{
+ fn new(
+ with_colons: WithColons,
+ generic_args: impl IntoIterator<Item = GenericArgument>,
+ ) -> Self;
+}
+
+impl AngleBracketedGenericArgumentsExt for AngleBracketedGenericArguments
+{
+ fn new(
+ with_colons: WithColons,
+ generic_args: impl IntoIterator<Item = GenericArgument>,
+ ) -> Self
+ {
+ Self {
+ colon2_token: match with_colons {
+ WithColons::Yes => Some(<Token![::]>::default()),
+ WithColons::No => None,
+ },
+ lt_token: <Token![<]>::default(),
+ args: generic_args.into_iter().collect(),
+ gt_token: <::syn::Token![>]>::default(),
+ }
+ }
+}
+
+pub enum WithColons
+{
+ Yes,
+ No,
+}
+
+pub trait TypeReferenceExt: Sized
+{
+ fn new(lifetime: Option<Lifetime>, is_mut: IsMut, inner_type: Type) -> Self;
+}
+
+impl TypeReferenceExt for TypeReference
+{
+ fn new(lifetime: Option<Lifetime>, is_mut: IsMut, inner_type: Type) -> Self
+ {
+ Self {
+ and_token: <Token![&]>::default(),
+ lifetime,
+ mutability: is_mut.into(),
+ elem: Box::new(inner_type),
+ }
+ }
+}
+
+pub enum IsMut
+{
+ Yes,
+ No,
+}
+
+impl From<Option<Token![mut]>> for IsMut
+{
+ fn from(opt_mut: Option<Token![mut]>) -> Self
+ {
+ match opt_mut {
+ Some(_) => Self::Yes,
+ None => Self::No,
+ }
+ }
+}
+
+impl From<IsMut> for Option<Token![mut]>
+{
+ fn from(is_mut: IsMut) -> Self
+ {
+ match is_mut {
+ IsMut::Yes => Some(Self::None.unwrap_or_default()),
+ IsMut::No => None,
+ }
+ }
+}
+
+pub trait SignatureExt: Sized
+{
+ fn new(
+ ident: Ident,
+ generics: Generics,
+ inputs: impl IntoIterator<Item = FnArg>,
+ output: ReturnType,
+ ) -> Self;
+}
+
+impl SignatureExt for Signature
+{
+ fn new(
+ ident: Ident,
+ generics: Generics,
+ inputs: impl IntoIterator<Item = FnArg>,
+ output: ReturnType,
+ ) -> Self
+ {
+ Self {
+ constness: None,
+ asyncness: None,
+ unsafety: None,
+ abi: None,
+ fn_token: <Token![fn]>::default(),
+ ident,
+ generics,
+ paren_token: Paren::default(),
+ inputs: inputs.into_iter().collect(),
+ variadic: None,
+ output,
+ }
+ }
+}
+
+pub trait ReturnTypeExt: Sized
+{
+ /// Returns a new `ReturnType::Type`.
+ fn new(ty: Type) -> Self;
+}
+
+impl ReturnTypeExt for ReturnType
+{
+ fn new(ty: Type) -> Self
+ {
+ Self::Type(<Token![->]>::default(), Box::new(ty))
+ }
+}
+
+pub trait GenericArgumentExt: Sized
+{
+ fn from_generic_param(generic_param: GenericParam) -> Self;
+}
+
+impl GenericArgumentExt for GenericArgument
+{
+ fn from_generic_param(generic_param: GenericParam) -> Self
+ {
+ match generic_param {
+ GenericParam::Type(type_param) => {
+ GenericArgument::Type(Type::Path(TypePath::new(Path::new(
+ WithLeadingColons::No,
+ [PathSegment::new(type_param.ident, None)],
+ ))))
+ }
+ GenericParam::Lifetime(lifetime_param) => {
+ GenericArgument::Lifetime(lifetime_param.lifetime)
+ }
+ GenericParam::Const(_) => {
+ todo!();
+ }
+ }
+ }
+}
+
+pub trait TypePathExt: Sized
+{
+ fn new(path: Path) -> Self;
+}
+
+impl TypePathExt for TypePath
+{
+ fn new(path: Path) -> Self
+ {
+ Self { qself: None, path }
+ }
+}
+
+pub trait TypeBareFnExt: Sized
+{
+ fn new(inputs: impl IntoIterator<Item = BareFnArg>, output: ReturnType) -> Self;
+}
+
+impl TypeBareFnExt for TypeBareFn
+{
+ fn new(inputs: impl IntoIterator<Item = BareFnArg>, output: ReturnType) -> Self
+ {
+ Self {
+ lifetimes: None,
+ unsafety: None,
+ abi: None,
+ fn_token: <Token![fn]>::default(),
+ paren_token: Paren::default(),
+ inputs: inputs.into_iter().collect(),
+ variadic: None,
+ output,
+ }
+ }
+}
+
+pub trait LifetimeExt: Sized
+{
+ fn create(ident: Ident) -> Self;
+}
+
+impl LifetimeExt for Lifetime
+{
+ fn create(ident: Ident) -> Self
+ {
+ Self {
+ apostrophe: Span::call_site(),
+ ident,
+ }
+ }
+}
+
+pub trait BareFnArgExt: Sized
+{
+ fn new(ty: Type) -> Self;
+}
+
+impl BareFnArgExt for BareFnArg
+{
+ fn new(ty: Type) -> Self
+ {
+ Self {
+ attrs: vec![],
+ name: None,
+ ty,
+ }
+ }
+}
diff --git a/macros/src/util.rs b/macros/src/util.rs
new file mode 100644
index 0000000..363051f
--- /dev/null
+++ b/macros/src/util.rs
@@ -0,0 +1,22 @@
+use syn::punctuated::Punctuated;
+use syn::token::Paren;
+use syn::TypeTuple;
+
+pub fn create_unit_type_tuple() -> TypeTuple
+{
+ TypeTuple {
+ paren_token: Paren::default(),
+ elems: Punctuated::new(),
+ }
+}
+
+macro_rules! create_path {
+ ($($segment: ident)::+) => {
+ Path::new(
+ WithLeadingColons::No,
+ [$(PathSegment::new(format_ident!(stringify!($segment)), None))+],
+ )
+ };
+}
+
+pub(crate) use create_path;
diff --git a/src/lib.rs b/src/lib.rs
index c4868a2..45aea03 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,196 +1,10 @@
-#[macro_export]
-macro_rules! mock {
- (
- $mock: ident;
+#![deny(clippy::all, clippy::pedantic)]
- impl $mocked_trait: ident for $mocked_trait_target: ident {
- $(
- fn $func: ident$(<
- $($type_param: tt$(: ($($type_param_bound: tt +)+))?),*
- >)?(
- self: ($($self_type: tt)+),
- $($func_param: ident: $func_param_type: ty),*
- )$( -> $return_type: ty)?
- $(where $(
- $where_param: ident: $first_where_param_bound: tt $(+ $where_param_bound: tt)*
- ),*)?;
- )*
- }
- ) => {
- $crate::__private::paste! {
- mod [<__ $mock>] {
- use super::*;
-
- pub struct $mock {
- $(
- [<$func _expectations>]: std::collections::HashMap<
- Vec<$crate::__private::type_id::TypeID>,
- [<$mock Expectation _ $func>]$(<$($crate::__to_unit!($type_param)),*>)?
- >,
- )*
- }
-
- impl $mock
- {
- pub fn new() -> Self
- {
- Self {
- $(
- [<$func _expectations>]: std::collections::HashMap::new()
- ),*
- }
- }
-
- $(
- #[allow(unused)]
- pub(crate) fn [<expect_ $func>]$(<
- $($type_param$(: $($type_param_bound +)*)?),*
- >)?(&mut self)
- -> &mut [<$mock Expectation _ $func>]$(<$($type_param),*>)?
- $(where $(
- $where_param: $first_where_param_bound $(+ $where_param_bound)*
- ),*)?
- {
- let ids = vec![
- $($($crate::__private::type_id::TypeID::of::<$type_param>()),*)?
- ];
-
- let expectation =
- [<$mock Expectation _ $func>]$(::<$($type_param),*>)?::new()
- .strip_type_params();
-
- self.[<$func _expectations>].insert(ids.clone(), expectation);
-
- self.[<$func _expectations>]
- .get_mut(&ids)
- .unwrap()
- .with_type_params_mut()
- }
- )*
- }
-
- impl $mocked_trait for $mock {
- $(
- fn $func$(<$($type_param$(: $($type_param_bound +)+)?),*>)?(
- self: $($self_type)+,
- $($func_param: $func_param_type),*
- )$( -> $return_type)?
- $(where $(
- $where_param: $first_where_param_bound $(+ $where_param_bound)*
- ),*)?
- {
- let ids = vec![
- $($($crate::__private::type_id::TypeID::of::<$type_param>()),*)?
- ];
-
- let expectation = self
- .[<$func _expectations>]
- .get(&ids)
- .expect(concat!(
- "No expectation found for function ",
- stringify!($func)
- ))
- .with_type_params$(::<$($type_param),*>)?();
-
- let Some(returning) = &expectation.returning else {
- panic!(concat!(
- "Expectation for function",
- stringify!($func),
- " is missing a function to call")
- );
- };
-
- returning(self, $($func_param),*)
- }
- )*
- }
-
- $(
- #[allow(non_camel_case_types, non_snake_case)]
- pub struct [<$mock Expectation _ $func>]$(<$($type_param),*>)? {
- returning: Option<
- fn(
- $crate::__replace_ref_type!($($self_type)*, $mock),
- $($func_param_type),*
- )$( -> $return_type)?>,
-
- $(
- $([<$type_param _phantom>]: std::marker::PhantomData<$type_param>,)*
- )?
- }
-
- impl$(<$($type_param),*>)? [<$mock Expectation _ $func>]$(<
- $($type_param),*
- >)? {
- #[allow(unused)]
- fn new() -> Self {
- Self {
- returning: None,
- $(
- $([<$type_param _phantom>]: std::marker::PhantomData,)*
- )?
- }
- }
-
- #[allow(unused)]
- pub fn returning(
- &mut self,
- func: fn(
- $crate::__replace_ref_type!($($self_type)*, $mock),
- $($func_param_type),*
- )$( -> $return_type)?
- ) -> &mut Self
- {
- self.returning = Some(func);
-
- self
- }
-
- #[allow(unused)]
- fn strip_type_params(
- self,
- ) -> [<$mock Expectation _ $func>]$(<$($crate::__to_unit!($type_param)),*>)?
- {
- $crate::__if_empty_else!(($($($type_param)*)?); {
- // No type parameters are present
- self
- }, {
- // Type parameters are present
- unsafe { std::mem::transmute(self) }
- })
-
- }
- }
-
- impl [<$mock Expectation _ $func>]$(<$($crate::__to_unit!($type_param)),*>)? {
- fn with_type_params$(<$($type_param),*>)?(
- &self,
- ) -> &[<$mock Expectation _ $func>]$(<$($type_param),*>)?
- {
- unsafe { &*(self as *const Self).cast() }
- }
-
- #[allow(unused)]
- fn with_type_params_mut$(<$($type_param),*>)?(
- &mut self,
- ) -> &mut [<$mock Expectation _ $func>]$(<$($type_param),*>)?
- {
- unsafe { &mut *(self as *mut Self).cast() }
- }
- }
- )*
- }
-
- use [<__ $mock>]::$mock;
- }
- };
-}
+pub use ridicule_macros::mock;
#[doc(hidden)]
pub mod __private
{
- pub use paste::paste;
-
pub mod type_id
{
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -202,6 +16,7 @@ pub mod __private
impl TypeID
{
#[inline]
+ #[must_use]
pub fn of<T>() -> Self
{
Self {
@@ -210,42 +25,4 @@ pub mod __private
}
}
}
-
- #[macro_export]
- #[doc(hidden)]
- macro_rules! __to_unit {
- ($anything: tt) => {
- ()
- };
- }
-
- #[macro_export]
- #[doc(hidden)]
- macro_rules! __replace_ref_type {
- (&$old_type: tt, $new_type: tt) => {
- &$new_type
- };
-
- (&mut $old_type: tt, $new_type: tt) => {
- &mut $new_type
- };
- }
-
- #[macro_export]
- #[doc(hidden)]
- macro_rules! __if_empty_else {
- (($($input: tt)*); $if_empty: block, $else: block) => {
- $crate::__if_empty_else!(@($($input)*); $if_empty, $else)
- };
-
- // Empty
- (@(); $if_empty: block, $else: block) => {
- $if_empty
- };
-
- // Not empty
- (@($($input: tt)+); $if_empty: block, $else: block) => {
- $else
- };
- }
}