diff options
-rw-r--r-- | Cargo.toml | 5 | ||||
-rw-r--r-- | examples/simple.rs | 20 | ||||
-rw-r--r-- | macros/Cargo.toml | 13 | ||||
-rw-r--r-- | macros/src/expectation.rs | 376 | ||||
-rw-r--r-- | macros/src/lib.rs | 54 | ||||
-rw-r--r-- | macros/src/mock.rs | 313 | ||||
-rw-r--r-- | macros/src/mock_input.rs | 51 | ||||
-rw-r--r-- | macros/src/syn_ext.rs | 372 | ||||
-rw-r--r-- | macros/src/util.rs | 22 | ||||
-rw-r--r-- | src/lib.rs | 229 |
10 files changed, 1223 insertions, 232 deletions
@@ -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; @@ -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 - }; - } } |