Skip to content

Commit

Permalink
wip mocking traits
Browse files Browse the repository at this point in the history
we probably want to create a single struct that implements all traits instead
  • Loading branch information
nrxus committed Sep 17, 2023
1 parent b7736f5 commit 257f415
Show file tree
Hide file tree
Showing 6 changed files with 293 additions and 143 deletions.
83 changes: 83 additions & 0 deletions faux_macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,90 @@ mod methods;
mod self_type;

use darling::{export::NestedMeta, FromMeta};
use methods::morphed::Signature;
use proc_macro::TokenStream;
use quote::quote;

#[proc_macro_attribute]
pub fn faux(_args: TokenStream, original: TokenStream) -> TokenStream {
let item = syn::parse_macro_input!(original as syn::Item);
match item {
syn::Item::Impl(_) => todo!(),
syn::Item::Struct(_) => todo!(),
syn::Item::Trait(trait_) => {
let trait_ident = &trait_.ident;
let struct_ident = syn::Ident::new(&format!("{trait_ident}_Faux"), trait_.span());
let (impl_generics, ty_generics, where_clause) = trait_.generics.split_for_impl();
let methods = trait_.items.iter().filter_map(|item| match item {
syn::TraitItem::Fn(m) => Some(m),
_ => None,
});

let mut new_methods = vec![];
let mut when_methods = vec![];

for func in methods {
let signature = match Signature::morph(
&func.sig,
None,
&syn::Visibility::Public(Default::default()),
) {
Ok(s) => s,
Err(e) => return e.write_errors().into(),
};

let block = match signature.create_body(SelfType::Owned, None) {
Ok(block) => block,
Err(e) => return e.write_errors().into(),
};
if let Some(methods) = signature.create_when(true) {
when_methods.extend(methods.into_iter().map(syn::ImplItem::Fn))
}

new_methods.push(syn::ImplItemFn {
attrs: vec![],
vis: syn::Visibility::Inherited,
defaultness: None,
sig: func.sig.clone(),
block,
});
}

let extra = quote! {
#[allow(non_camel_case_types)]
pub struct #struct_ident(::faux::Faux);

impl #impl_generics dyn #trait_ident #ty_generics #where_clause {
pub fn faux() -> #struct_ident {
#struct_ident(::faux::Faux::new(stringify!(trait_ident)))
}
}

#[allow(unused_variables)]
impl #impl_generics #trait_ident #ty_generics for #struct_ident #where_clause {
#(#new_methods) *
}

impl #struct_ident {
#(#when_methods) *
}
};

quote! {
#trait_

#extra
}
.into()
}
x => {
return syn::Error::new_spanned(x, "Unsupported item wrapped by #[faux]")
.into_compile_error()
.into()
}
}
}

#[proc_macro_attribute]
pub fn create(args: TokenStream, original: TokenStream) -> TokenStream {
let original = syn::parse_macro_input!(original as syn::ItemStruct);
Expand Down Expand Up @@ -80,6 +161,8 @@ pub fn when(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
}

use quote::ToTokens;
use self_type::SelfType;
use syn::spanned::Spanned;

fn ref_matcher_maybe(
expr: &syn::Expr,
Expand Down
10 changes: 5 additions & 5 deletions faux_macros/src/methods.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
mod morphed;
mod receiver;
pub mod morphed;
pub mod receiver;

use crate::{create, self_type::SelfType};
use darling::FromMeta;
Expand Down Expand Up @@ -67,9 +67,9 @@ impl Mockable {
&func.sig,
real.trait_.as_ref().map(|(_, path, _)| path),
&func.vis,
);
func.block = signature.create_body(args.self_type, &real_ty, &morphed_ty)?;
if let Some(methods) = signature.create_when() {
)?;
func.block = signature.create_body(args.self_type, Some((&real_ty, &morphed_ty)))?;
if let Some(methods) = signature.create_when(false) {
when_methods.extend(methods.into_iter().map(syn::ImplItem::Fn));
}
}
Expand Down
103 changes: 63 additions & 40 deletions faux_macros/src/methods/morphed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::{spanned::Spanned, PathArguments, Type, TypePath};

#[derive(Debug)]
pub struct Signature<'a> {
name: &'a syn::Ident,
args: Vec<&'a syn::Pat>,
Expand All @@ -12,6 +13,7 @@ pub struct Signature<'a> {
trait_path: Option<&'a syn::Path>,
}

#[derive(Debug)]
pub struct MethodData<'a> {
receiver: Receiver,
arg_types: Vec<WhenArg<'a>>,
Expand Down Expand Up @@ -87,8 +89,8 @@ impl<'a> Signature<'a> {
signature: &'a syn::Signature,
trait_path: Option<&'a syn::Path>,
vis: &syn::Visibility,
) -> Signature<'a> {
let receiver = Receiver::from_signature(signature);
) -> darling::Result<Signature<'a>> {
let receiver = Receiver::from_signature(signature)?;

let output = match &signature.output {
syn::ReturnType::Default => None,
Expand All @@ -115,7 +117,7 @@ impl<'a> Signature<'a> {
}
});

Signature {
Ok(Signature {
name: &signature.ident,
args: signature
.inputs
Expand All @@ -132,53 +134,64 @@ impl<'a> Signature<'a> {
output,
method_data,
trait_path,
}
})
}

pub fn create_body(
&self,
real_self: SelfType,
real_ty: &syn::TypePath,
morphed_ty: &syn::TypePath,
// Option<real_ty, morphed_ty>
struct_tys: Option<(&syn::TypePath, &syn::TypePath)>,
) -> darling::Result<syn::Block> {
let name = &self.name;
let args = &self.args;

let proxy = match self.trait_path {
None => quote! { <#real_ty>::#name },
Some(path) => quote! { <#real_ty as #path>::#name },
};
let proxy_real = struct_tys
.map(|(real_ty, morphed_ty)| -> darling::Result<TokenStream> {
let proxy = match self.trait_path {
None => quote! { <#real_ty>::#name },
Some(path) => quote! { <#real_ty as #path>::#name },
};

let real_self_arg = self.method_data.as_ref().map(|_| {
// need to pass the real Self arg to the real method
syn::Pat::Ident(syn::PatIdent {
attrs: vec![],
by_ref: None,
mutability: None,
ident: syn::Ident::new("_maybe_faux_real", proc_macro2::Span::call_site()),
subpat: None,
})
});
let real_self_arg = real_self_arg.as_ref();
let real_self_arg = self.method_data.as_ref().map(|_| {
// need to pass the real Self arg to the real method
syn::Pat::Ident(syn::PatIdent {
attrs: vec![],
by_ref: None,
mutability: None,
ident: syn::Ident::new("_maybe_faux_real", proc_macro2::Span::call_site()),
subpat: None,
})
});
let real_self_arg = real_self_arg.as_ref();
let proxy_args = real_self_arg.iter().chain(args);
let mut proxy_real = quote! { #proxy(#(#proxy_args),*) };
if self.is_async {
proxy_real.extend(quote! { .await })
}
if let Some(wrapped_self) = self.wrap_self(morphed_ty, real_self, &proxy_real)? {
proxy_real = wrapped_self;
}

let proxy_args = real_self_arg.iter().chain(args);
let mut proxy_real = quote! { #proxy(#(#proxy_args),*) };
if self.is_async {
proxy_real.extend(quote! { .await })
}
if let Some(wrapped_self) = self.wrap_self(morphed_ty, real_self, &proxy_real)? {
proxy_real = wrapped_self;
}
Ok(proxy_real)
})
.transpose()?;

let ret = match &self.method_data {
// not stubbable
// proxy to real associated function
None => syn::parse2(proxy_real).unwrap(),
None => match proxy_real {
Some(proxy_real) => syn::parse2(proxy_real),
None => syn::parse2(quote! {
panic!("faux: mocking static trait methods is not supported")
}),
}
.expect("failed to create proxy real"),
// else we can either proxy for real instances
// or call the mock store for faux instances
Some(method_data) => {
let call_stub = if method_data.is_private {
quote! { panic!("faux error: private methods are not stubbable; and therefore not directly callable in a mock") }
syn::parse_quote! { panic!("faux error: private methods are not stubbable; and therefore not directly callable in a mock") }
} else {
let faux_ident =
syn::Ident::new(&format!("_faux_{}", name), proc_macro2::Span::call_site());
Expand Down Expand Up @@ -227,11 +240,11 @@ impl<'a> Signature<'a> {
})
}

pub fn create_when(&self) -> Option<Vec<syn::ImplItemFn>> {
pub fn create_when(&self, is_trait: bool) -> Option<Vec<syn::ImplItemFn>> {
self.method_data
.as_ref()
.filter(|m| !m.is_private)
.map(|m| m.create_when(self.output, self.name))
.map(|m| m.create_when(self.output, self.name, is_trait))
}

fn wrap_self(
Expand Down Expand Up @@ -328,6 +341,7 @@ impl<'a> MethodData<'a> {
&self,
output: Option<&syn::Type>,
name: &syn::Ident,
is_trait: bool,
) -> Vec<syn::ImplItemFn> {
let MethodData {
arg_types,
Expand All @@ -345,19 +359,28 @@ impl<'a> MethodData<'a> {
let output = output.unwrap_or(&empty);
let name_str = name.to_string();

let when_method = syn::parse_quote! {
pub fn #when_ident<'m>(&'m mut self) -> faux::When<'m, #receiver_ty, (#(#arg_types),*), #output, faux::matcher::AnyInvocation> {
let call_when =
quote! { faux::When::new(<Self>::#faux_ident, #name_str, _maybe_faux_faux) };
let body = if is_trait {
quote! {
let _maybe_faux_faux = &mut self.0;
#call_when
}
} else {
quote! {
match &mut self.0 {
faux::MaybeFaux::Faux(_maybe_faux_faux) => faux::When::new(
<Self>::#faux_ident,
#name_str,
_maybe_faux_faux
),
faux::MaybeFaux::Faux(_maybe_faux_faux) => #call_when,
faux::MaybeFaux::Real(_) => panic!("not allowed to stub a real instance!"),
}
}
};

let when_method = syn::parse_quote! {
pub fn #when_ident<'m>(&'m mut self) -> faux::When<'m, #receiver_ty, (#(#arg_types),*), #output, faux::matcher::AnyInvocation> {
#body
}
};

let panic_message = format!("do not call this ({})", name);
let faux_method = syn::parse_quote! {
#[allow(clippy::needless_arbitrary_self_type)]
Expand Down
Loading

0 comments on commit 257f415

Please sign in to comment.