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 -        }; -    }  } | 
