blob: 2d3824f6e5d000b36ebcb269d033f99109526781 [file] [log] [blame]
// vim: tw=80
use super::*;
use syn::parse::{Parse, ParseStream};
/// Make any implicit lifetime parameters explicit
fn add_lifetime_parameters(sig: &mut Signature) {
fn add_to_trait_object(generics: &mut Generics, var: &Pat, to: &mut TypeTraitObject) {
let mut has_lifetime = false;
for bound in to.bounds.iter() {
if let TypeParamBound::Lifetime(_) = bound {
has_lifetime = true;
}
}
if ! has_lifetime {
let arg_ident = match *var {
Pat::Wild(_) => {
compile_error(var.span(),
"Mocked methods must have named arguments");
format_ident!("dont_care")
},
Pat::Ident(ref pat_ident) => {
if let Some(r) = &pat_ident.by_ref {
compile_error(r.span(),
"Mockall does not support by-reference argument bindings");
}
if let Some((_at, subpat)) = &pat_ident.subpat {
compile_error(subpat.span(),
"Mockall does not support subpattern bindings");
}
pat_ident.ident.clone()
},
_ => {
compile_error(var.span(),
"Unsupported argument type");
format_ident!("dont_care")
}
};
let s = format!("'__mockall_{}", arg_ident);
let span = Span::call_site();
let lt = Lifetime::new(&s, span);
to.bounds.push(TypeParamBound::Lifetime(lt.clone()));
generics.lt_token.get_or_insert(Token![<](span));
generics.gt_token.get_or_insert(Token![>](span));
let gpl = GenericParam::Lifetime(LifetimeDef::new(lt));
generics.params.push(gpl);
}
}
fn add_to_type(generics: &mut Generics, var: &Pat, ty: &mut Type) {
match ty {
Type::Array(ta) => add_to_type(generics, var, ta.elem.as_mut()),
Type::BareFn(_) => (),
Type::ImplTrait(_) => (),
Type::Path(_) => (),
Type::Ptr(_) => (),
Type::Reference(tr) => {
match tr.elem.as_mut() {
Type::Paren(tp) => {
if let Type::TraitObject(to) = tp.elem.as_mut() {
add_to_trait_object(generics, var, to);
} else {
add_to_type(generics, var, tr.elem.as_mut());
}
},
Type::TraitObject(to) => {
add_to_trait_object(generics, var, to);
// We need to wrap it in a Paren. Otherwise it won't be
// syntactically valid after we add a lifetime bound,
// due to a "ambiguous `+` in a type" error
*tr.elem = Type::Paren(TypeParen {
paren_token: token::Paren::default(),
elem: Box::new(Type::TraitObject(to.clone()))
});
},
_ => add_to_type(generics, var, tr.elem.as_mut()),
}
},
Type::Slice(ts) => add_to_type(generics, var, ts.elem.as_mut()),
Type::Tuple(tt) => {
for mut ty in tt.elems.iter_mut() {
add_to_type(generics, var, &mut ty)
}
},
_ => compile_error(ty.span(), "unsupported type in this position")
}
}
for arg in sig.inputs.iter_mut() {
if let FnArg::Typed(pt) = arg {
add_to_type(&mut sig.generics, &pt.pat, &mut pt.ty)
}
}
}
/// Filter a generics list, keeping only the elements specified by path_args
/// e.g. filter_generics(<A: Copy, B: Clone>, <A>) -> <A: Copy>
fn filter_generics(g: &Generics, path_args: &PathArguments)
-> Generics
{
let mut params = Punctuated::new();
match path_args {
PathArguments::None => ()/* No generics selected */,
PathArguments::Parenthesized(p) => {
compile_error(p.span(),
"Mockall does not support mocking Fn objects. See https://github.com/asomers/mockall/issues/139");
},
PathArguments::AngleBracketed(abga) => {
let args = &abga.args;
if g.where_clause.is_some() {
compile_error(g.where_clause.span(),
"Mockall does not yet support where clauses here");
return g.clone();
}
for param in g.params.iter() {
match param {
GenericParam::Type(tp) => {
let matches = |ga: &GenericArgument| {
if let GenericArgument::Type(
Type::Path(type_path)) = ga
{
type_path.path.is_ident(&tp.ident)
} else {
false
}
};
if args.iter().any(matches) {
params.push(param.clone())
}
},
GenericParam::Lifetime(ld) => {
let matches = |ga: &GenericArgument| {
if let GenericArgument::Lifetime(lt) = ga {
*lt == ld.lifetime
} else {
false
}
};
if args.iter().any(matches) {
params.push(param.clone())
}
},
GenericParam::Const(_) => ()/* Ignore */,
}
}
}
};
if params.is_empty() {
Generics::default()
} else {
Generics {
lt_token: Some(Token![<](g.span())),
params,
gt_token: Some(Token![>](g.span())),
where_clause: None
}
}
}
fn find_ident_from_path(path: &Path) -> (Ident, PathArguments) {
if path.segments.len() != 1 {
compile_error(path.span(),
"mockall_derive only supports structs defined in the current module");
return (Ident::new("", path.span()), PathArguments::None);
}
let last_seg = path.segments.last().unwrap();
(last_seg.ident.clone(), last_seg.arguments.clone())
}
/// Performs transformations on the ItemImpl to make it mockable
fn mockable_item_impl(mut impl_: ItemImpl, name: &Ident, generics: &Generics)
-> ItemImpl
{
for item in impl_.items.iter_mut() {
if let ImplItem::Method(ref mut iim) = item {
mockable_method(iim, name, generics);
}
}
impl_
}
/// Performs transformations on the method to make it mockable
fn mockable_method(meth: &mut ImplItemMethod, name: &Ident, generics: &Generics)
{
demutify(&mut meth.sig.inputs);
add_lifetime_parameters(&mut meth.sig);
deimplify(&mut meth.sig.output);
if let ReturnType::Type(_, ty) = &mut meth.sig.output {
deselfify(ty, name, generics);
deanonymize(ty);
}
sanity_check_sig(&meth.sig);
}
/// Performs transformations on the method to make it mockable
fn mockable_trait_method(
meth: &mut TraitItemMethod,
name: &Ident,
generics: &Generics)
{
demutify(&mut meth.sig.inputs);
add_lifetime_parameters(&mut meth.sig);
deimplify(&mut meth.sig.output);
if let ReturnType::Type(_, ty) = &mut meth.sig.output {
deselfify(ty, name, generics);
deanonymize(ty);
}
sanity_check_sig(&meth.sig);
}
/// Generates a mockable item impl from a trait method definition
fn mockable_trait(trait_: ItemTrait, name: &Ident, generics: &Generics)
-> ItemImpl
{
let items = trait_.items.into_iter()
.map(|ti| {
match ti {
TraitItem::Method(mut tim) => {
mockable_trait_method(&mut tim, name, generics);
ImplItem::Method(tim2iim(tim, &Visibility::Inherited))
},
TraitItem::Const(tic) => {
ImplItem::Const(tic2iic(tic, &Visibility::Inherited))
},
TraitItem::Type(tit) => {
ImplItem::Type(tit2iit(tit, &Visibility::Inherited))
},
_ => {
compile_error(ti.span(), "Unsupported in this context");
ImplItem::Verbatim(TokenStream::new())
}
}
}).collect::<Vec<_>>();
let mut path = Path::from(trait_.ident);
let (_, tg, _) = trait_.generics.split_for_impl();
if let Ok(abga) = parse2::<AngleBracketedGenericArguments>(quote!(#tg)) {
path.segments.last_mut().unwrap().arguments =
PathArguments::AngleBracketed(abga);
}
ItemImpl {
attrs: trait_.attrs,
defaultness: None,
unsafety: trait_.unsafety,
impl_token: <Token![impl]>::default(),
generics: trait_.generics,
trait_: Some((None, path, <Token![for]>::default())),
// For now, self_ty is unused
self_ty: Box::new(Type::Verbatim(TokenStream::new())),
brace_token: trait_.brace_token,
items
}
}
fn sanity_check_sig(sig: &Signature) {
for arg in sig.inputs.iter() {
if let FnArg::Typed(pt) = arg {
if let Type::ImplTrait(it) = pt.ty.as_ref() {
let bounds = &it.bounds;
let s = format!(
"Mockall does not support \"impl trait\" in argument position. Use \"T: {}\" instead",
quote!(#bounds)
);
compile_error(it.span(), &s);
}
}
}
}
/// Converts a TraitItemConst into an ImplItemConst
fn tic2iic(tic: TraitItemConst, vis: &syn::Visibility) -> ImplItemConst {
let span = tic.span();
let (eq_token, expr) = tic.default.unwrap_or_else(|| {
compile_error(span,
"Mocked associated consts must have a default implementation");
(<Token![=]>::default(), Expr::Verbatim(TokenStream::new()))
});
ImplItemConst {
attrs: tic.attrs,
vis: vis.clone(),
defaultness: None,
const_token: tic.const_token,
ident: tic.ident,
colon_token: tic.colon_token,
ty: tic.ty,
eq_token,
expr,
semi_token: tic.semi_token
}
}
/// Converts a TraitItemMethod into an ImplItemMethod
fn tim2iim(m: syn::TraitItemMethod, vis: &syn::Visibility)
-> syn::ImplItemMethod
{
let empty_block = Block {
brace_token: token::Brace::default(),
stmts: Vec::new()
};
syn::ImplItemMethod{
attrs: m.attrs,
vis: vis.clone(),
defaultness: None,
sig: m.sig,
block: empty_block
}
}
/// Converts a TraitItemType into an ImplItemType
fn tit2iit(tit: TraitItemType, vis: &Visibility) -> ImplItemType {
let span = tit.span();
let (eq_token, ty) = tit.default.unwrap_or_else(|| {
compile_error(span,
"associated types in mock! must be fully specified");
(token::Eq::default(), Type::Verbatim(TokenStream::new()))
});
ImplItemType {
attrs: tit.attrs,
vis: vis.clone(),
defaultness: None,
type_token: tit.type_token,
ident: tit.ident,
generics: tit.generics,
eq_token,
ty,
semi_token: tit.semi_token,
}
}
pub(crate) struct MockableStruct {
pub attrs: Vec<Attribute>,
pub consts: Vec<ImplItemConst>,
pub generics: Generics,
/// Inherent methods of the mockable struct
pub methods: Vec<ImplItemMethod>,
pub name: Ident,
pub vis: Visibility,
pub impls: Vec<ItemImpl>
}
impl From<(Attrs, ItemTrait)> for MockableStruct {
fn from((attrs, item_trait): (Attrs, ItemTrait)) -> MockableStruct {
let trait_ = attrs.substitute_trait(&item_trait);
let attrs = trait_.attrs.clone();
let vis = trait_.vis.clone();
let name = gen_mock_ident(&trait_.ident);
let generics = trait_.generics.clone();
let impls = vec![mockable_trait(trait_, &name, &generics)];
MockableStruct {
attrs,
consts: Vec::new(),
vis,
name,
generics,
methods: Vec::new(),
impls
}
}
}
impl From<ItemImpl> for MockableStruct {
fn from(item_impl: ItemImpl) -> MockableStruct {
let name = match *item_impl.self_ty {
Type::Path(type_path) => {
let n = find_ident_from_path(&type_path.path).0;
gen_mock_ident(&n)
},
x => {
compile_error(x.span(),
"mockall_derive only supports mocking traits and structs");
Ident::new("", Span::call_site())
}
};
let mut consts = Vec::new();
let mut methods = Vec::new();
let pub_token = Token![pub](Span::call_site());
let vis = Visibility::Public(VisPublic{pub_token});
let mut impls = Vec::new();
if let Some((bang, path, _)) = item_impl.trait_ {
if bang.is_some() {
compile_error(bang.span(), "Unsupported by automock");
}
// Regenerate the trait that this block is implementing
let generics = &item_impl.generics;
let traitname = &path.segments.last().unwrap().ident;
let trait_path_args = &path.segments.last().unwrap().arguments;
let mut items = Vec::new();
let mut attrs = Attrs::default();
for item in item_impl.items.into_iter() {
match item {
ImplItem::Const(iic) =>
items.push(
TraitItem::Const(TraitItemConst {
attrs: iic.attrs,
const_token: iic.const_token,
ident: iic.ident,
colon_token: iic.colon_token,
ty: iic.ty,
default: Some((
iic.eq_token,
iic.expr
)),
semi_token: iic.semi_token
})
),
ImplItem::Method(meth) =>
items.push(
TraitItem::Method(TraitItemMethod {
attrs: meth.attrs,
sig: meth.sig,
default: None,
semi_token: Some(Token![;](Span::call_site()))
})
),
ImplItem::Type(ty) => {
attrs.attrs.insert(ty.ident.clone(), ty.ty.clone());
items.push(
TraitItem::Type(TraitItemType {
attrs: ty.attrs,
type_token: ty.type_token,
ident: ty.ident,
generics: ty.generics,
colon_token: None,
bounds: Punctuated::new(),
default: Some((ty.eq_token, ty.ty.clone())),
semi_token: ty.semi_token
})
);
},
x => compile_error(x.span(), "Unsupported by automock")
}
}
let trait_ = ItemTrait {
attrs: item_impl.attrs.clone(),
vis: vis.clone(),
unsafety: item_impl.unsafety,
auto_token: None,
trait_token: Token![trait](Span::call_site()),
ident: traitname.clone(),
generics: filter_generics(&item_impl.generics, trait_path_args),
colon_token: None,
supertraits: Punctuated::new(),
brace_token: token::Brace::default(),
items
};
let trait_ = attrs.substitute_trait(&trait_);
impls.push(mockable_trait(trait_, traitname, &generics));
} else {
for item in item_impl.items.into_iter() {
match item {
ImplItem::Method(mut meth) => {
mockable_method(&mut meth, &name, &item_impl.generics);
methods.push(meth)
},
ImplItem::Const(iic) => consts.push(iic),
// Rust doesn't allow types in an inherent impl
x => compile_error(x.span(),
"Unsupported by Mockall in this context"),
}
}
};
MockableStruct {
attrs: item_impl.attrs,
consts,
name,
generics: item_impl.generics,
methods,
impls,
vis
}
}
}
impl Parse for MockableStruct {
fn parse(input: ParseStream) -> syn::parse::Result<Self> {
let attrs = input.call(syn::Attribute::parse_outer)?;
let vis: syn::Visibility = input.parse()?;
let original_name: syn::Ident = input.parse()?;
let mut generics: syn::Generics = input.parse()?;
let wc: Option<syn::WhereClause> = input.parse()?;
generics.where_clause = wc;
let name = gen_mock_ident(&original_name);
let impl_content;
let _brace_token = braced!(impl_content in input);
let mut consts = Vec::new();
let mut methods = Vec::new();
while !impl_content.is_empty() {
let item: ImplItem = impl_content.parse()?;
match item {
ImplItem::Method(mut iim) => {
mockable_method(&mut iim, &name, &generics);
methods.push(iim);
},
ImplItem::Const(iic) => consts.push(iic),
_ => {
return Err(input.error("Unsupported in this context"));
}
}
}
let mut impls = Vec::new();
while !input.is_empty() {
let item: Item = input.parse()?;
match item {
Item::Trait(it) => {
let note = "Deprecated mock! syntax. Instead of \"trait X\", write \"impl X for Y\". See PR #205";
let mut impl_ = mockable_trait(it, &name, &generics);
impl_.attrs.push(Attribute {
pound_token: <token::Pound>::default(),
style: AttrStyle::Outer,
bracket_token: token::Bracket::default(),
path: Path::from(format_ident!("deprecated")),
tokens: quote!((since = "0.9.0", note = #note)),
});
impls.push(impl_)
},
Item::Impl(ii) =>
impls.push(mockable_item_impl(ii, &name, &generics)),
_ => return Err(input.error("Unsupported in this context")),
}
}
Ok(
MockableStruct {
attrs,
consts,
vis,
name,
generics,
methods,
impls
}
)
}
}
#[cfg(test)]
mod t {
use super::*;
mod add_lifetime_parameters {
use super::*;
#[test]
fn array() {
let mut meth: TraitItemMethod = parse2(quote!(
fn foo(&self, x: [&dyn T; 1]);
)).unwrap();
add_lifetime_parameters(&mut meth.sig);
assert_eq!(
quote!(fn foo<'__mockall_x>(&self, x: [&(dyn T + '__mockall_x); 1]);)
.to_string(),
quote!(#meth).to_string()
);
}
#[test]
fn bare_fn_with_named_args() {
let mut meth: TraitItemMethod = parse2(quote!(
fn foo(&self, x: fn(&dyn T));
)).unwrap();
add_lifetime_parameters(&mut meth.sig);
assert_eq!(
quote!(fn foo(&self, x: fn(&dyn T));).to_string(),
quote!(#meth).to_string()
);
}
#[test]
fn plain() {
let mut meth: TraitItemMethod = parse2(quote!(
fn foo(&self, x: &dyn T);
)).unwrap();
add_lifetime_parameters(&mut meth.sig);
assert_eq!(
quote!(fn foo<'__mockall_x>(&self, x: &(dyn T + '__mockall_x));)
.to_string(),
quote!(#meth).to_string()
);
}
#[test]
fn slice() {
let mut meth: TraitItemMethod = parse2(quote!(
fn foo(&self, x: &[&dyn T]);
)).unwrap();
add_lifetime_parameters(&mut meth.sig);
assert_eq!(
quote!(fn foo<'__mockall_x>(&self, x: &[&(dyn T + '__mockall_x)]);)
.to_string(),
quote!(#meth).to_string()
);
}
#[test]
fn tuple() {
let mut meth: TraitItemMethod = parse2(quote!(
fn foo(&self, x: (&dyn T, u32));
)).unwrap();
add_lifetime_parameters(&mut meth.sig);
assert_eq!(
quote!(fn foo<'__mockall_x>(&self, x: (&(dyn T + '__mockall_x), u32));)
.to_string(),
quote!(#meth).to_string()
);
}
#[test]
fn with_anonymous_lifetime() {
let mut meth: TraitItemMethod = parse2(quote!(
fn foo(&self, x: &(dyn T + '_));
)).unwrap();
add_lifetime_parameters(&mut meth.sig);
assert_eq!(
quote!(fn foo(&self, x: &(dyn T + '_));).to_string(),
quote!(#meth).to_string()
);
}
#[test]
fn with_parens() {
let mut meth: TraitItemMethod = parse2(quote!(
fn foo(&self, x: &(dyn T));
)).unwrap();
add_lifetime_parameters(&mut meth.sig);
assert_eq!(
quote!(fn foo<'__mockall_x>(&self, x: &(dyn T + '__mockall_x));)
.to_string(),
quote!(#meth).to_string()
);
}
#[test]
fn with_lifetime_parameter() {
let mut meth: TraitItemMethod = parse2(quote!(
fn foo<'a>(&self, x: &(dyn T + 'a));
)).unwrap();
add_lifetime_parameters(&mut meth.sig);
assert_eq!(
quote!(fn foo<'a>(&self, x: &(dyn T + 'a));).to_string(),
quote!(#meth).to_string()
);
}
#[test]
fn with_static_lifetime() {
let mut meth: TraitItemMethod = parse2(quote!(
fn foo(&self, x: &(dyn T + 'static));
)).unwrap();
add_lifetime_parameters(&mut meth.sig);
assert_eq!(
quote!(fn foo(&self, x: &(dyn T + 'static));).to_string(),
quote!(#meth).to_string()
);
}
}
mod sanity_check_sig {
use super::*;
#[test]
#[should_panic(expected = "Mockall does not support \"impl trait\" in argument position. Use \"T: SomeTrait\" instead.")]
fn impl_trait() {
let meth: ImplItemMethod = parse2(quote!(
fn foo(&self, x: impl SomeTrait);
)).unwrap();
sanity_check_sig(&meth.sig);
}
}
}