// vim: tw=80 use proc_macro2::Span; use quote::{ToTokens, format_ident, quote}; use std::{ collections::hash_map::DefaultHasher, fmt::Write, hash::{Hash, Hasher} }; use syn::{ *, spanned::Spanned }; use crate::{ AttrFormatter, mock_function::{self, MockFunction}, compile_error }; pub(crate) struct MockTrait { pub attrs: Vec, pub consts: Vec, pub generics: Generics, pub methods: Vec, /// Internally-used name of the trait used. pub ss_name: Ident, /// Fully-qualified name of the trait pub trait_path: Path, /// Path on which the trait is implemented. Usually will be the same as /// structname, but might include concrete generic parameters. self_path: PathSegment, pub types: Vec, pub unsafety: Option } impl MockTrait { fn ss_name_priv(trait_path: &Path) -> Ident { let id = join_path_segments(trait_path, "__"); // Skip the hashing step for easy debugging of generated code if let Some(hash) = hash_path_arguments(trait_path) { // Hash the path args to permit mocking structs that implement // multiple traits distinguished only by their path args format_ident!("{id}_{hash}") } else { format_ident!("{id}") } } pub fn ss_name(&self) -> &Ident { &self.ss_name } /// Create a new MockTrait /// /// # Arguments /// * `structname` - name of the struct that implements this trait /// * `struct_generics` - Generics of the parent structure /// * `impl_` - Mockable ItemImpl for a trait /// * `vis` - Visibility of the struct pub fn new(structname: &Ident, struct_generics: &Generics, impl_: ItemImpl, vis: &Visibility) -> Self { let mut consts = Vec::new(); let mut methods = Vec::new(); let mut types = Vec::new(); let trait_path = if let Some((_, path, _)) = impl_.trait_ { path } else { compile_error(impl_.span(), "impl block must implement a trait"); Path::from(format_ident!("__mockall_invalid")) }; let ss_name = MockTrait::ss_name_priv(&trait_path); let self_path = match *impl_.self_ty { Type::Path(mut type_path) => type_path.path.segments.pop().unwrap().into_value(), x => { compile_error(x.span(), "mockall_derive only supports mocking traits and structs"); PathSegment::from(Ident::new("", Span::call_site())) } }; for ii in impl_.items.into_iter() { match ii { ImplItem::Const(iic) => { consts.push(iic); }, ImplItem::Fn(iif) => { let mf = mock_function::Builder::new(&iif.sig, vis) .attrs(&iif.attrs) .levels(2) .call_levels(0) .struct_(structname) .struct_generics(struct_generics) .trait_(&ss_name) .build(); methods.push(mf); }, ImplItem::Type(iit) => { types.push(iit); }, _ => { compile_error(ii.span(), "This impl item is not yet supported by MockAll"); } } } MockTrait { attrs: impl_.attrs, consts, generics: impl_.generics, methods, ss_name, trait_path, self_path, types, unsafety: impl_.unsafety } } /// Generate code for the trait implementation on the mock struct /// /// # Arguments /// /// * `modname`: Name of the parent struct's private module // Supplying modname is an unfortunately hack. Ideally MockTrait // wouldn't need to know that. pub fn trait_impl(&self, modname: &Ident) -> impl ToTokens { let trait_impl_attrs = AttrFormatter::new(&self.attrs) .must_use(false) .format(); let impl_attrs = AttrFormatter::new(&self.attrs) .async_trait(false) .doc(false) .format(); let (ig, _tg, wc) = self.generics.split_for_impl(); let consts = &self.consts; let path_args = &self.self_path.arguments; let calls = self.methods.iter() .map(|meth| meth.call(Some(modname))) .collect::>(); let contexts = self.methods.iter() .filter(|meth| meth.is_static()) .map(|meth| meth.context_fn(Some(modname))) .collect::>(); let expects = self.methods.iter() .filter(|meth| !meth.is_static()) .map(|meth| { if meth.is_method_generic() { // Specific impls with generic methods are TODO. meth.expect(modname, None) } else { meth.expect(modname, Some(path_args)) } }).collect::>(); let trait_path = &self.trait_path; let self_path = &self.self_path; let types = &self.types; let unsafety = &self.unsafety; quote!( #(#trait_impl_attrs)* #unsafety impl #ig #trait_path for #self_path #wc { #(#consts)* #(#types)* #(#calls)* } #(#impl_attrs)* impl #ig #self_path #wc { #(#expects)* #(#contexts)* } ) } } fn join_path_segments(path: &Path, sep: &str) -> String { let mut output = String::new(); for segment in &path.segments { if write!( output, "{}{}", if output.is_empty() { "" } else { sep }, segment.ident ) .is_err() { break; }; } output } fn hash_path_arguments(path: &Path) -> Option { let mut hasher = DefaultHasher::new(); let mut is_some = false; for arguments in path .segments .iter() .map(|segment| &segment.arguments) .filter(|arguments| !arguments.is_empty()) { arguments.hash(&mut hasher); is_some = true; } is_some.then(|| hasher.finish()) }