Skip to content

Commit

Permalink
added support for returning Self wrapped in common types
Browse files Browse the repository at this point in the history
i.e., Rc, Arc, Box, Result, and Option
  • Loading branch information
nrxus committed Sep 19, 2022
1 parent f694453 commit a14e177
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 86 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# CHANGELOG

## NEXT
* Allow `#[faux::methods]` to wrap functions that return the mocked
struct wrapped in `Rc`, `Arc`, `Box`, `Result`, or `Option`.
* [test](/tests/return_self_method.rs)

## v0.1.8
* Fix issue where a type with `::` could not be used as a generic
argument in an `impl` signature.
Expand Down
122 changes: 79 additions & 43 deletions faux_macros/src/methods/morphed.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::{methods::receiver::Receiver, self_type::SelfType};
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::spanned::Spanned;
use syn::{spanned::Spanned, PathArguments};

struct SpanMarker(proc_macro2::Span);

Expand Down Expand Up @@ -252,47 +252,87 @@ impl<'a> Signature<'a> {
ty == morphed_ty || (ty.qself.is_none() && ty.path.is_ident("Self"))
};

let self_generic = |args: &syn::PathArguments| match args {
syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments {
args,
..
}) if args.len() == 1 => match args.first().unwrap() {
syn::GenericArgument::Type(syn::Type::Path(ty)) => is_self(ty),
_ => false,
},
_ => false,
let output = self.output.filter(|o| {
let output = o.to_token_stream().to_string();
if output.contains("Self") {
return true;
};
let morphed_ty = morphed_ty.to_token_stream().to_string();
output.contains(&morphed_ty)
});

let output = match output {
Some(o) => o,
None => return Ok(None),
};

Ok(if let Some(syn::Type::Path(output)) = self.output {
let last_segment = &output.path.segments.last().unwrap();
match SelfType::from_path(output) {
SelfType::Owned if is_self(output) => Some(match real_self {
SelfType::Owned => quote! { Self(faux::MaybeFaux::Real(#block)) },
generic => {
let new_path = generic
.new_path()
.expect("Generic self should have new() function");
quote! { Self(faux::MaybeFaux::Real(#new_path(#block))) }
}
}),
generic if self_generic(&last_segment.arguments) => {
if generic == real_self {
let new_path = real_self
.new_path()
.expect("return type should not be Self");
Some(quote! { #new_path(Self(faux::MaybeFaux::Real(#block))) })
} else {
return Err(darling::Error::custom(wrong_self_type_error(
generic, real_self,
))
.with_span(&output));
}
let output = match output {
syn::Type::Path(output) => output,
output => return Err(unhandled_self_return(&output)),
};

let wrapped = if is_self(output) {
match real_self {
SelfType::Owned => quote! { Self(faux::MaybeFaux::Real(#block)) },
generic => {
let new_path = generic
.new_path()
.expect("Generic self should have new() function");
quote! { Self(faux::MaybeFaux::Real(#new_path(#block))) }
}
_ => None,
}
} else {
None
})
let unpathed_output = output.path.segments.last().unwrap();
let generics = match &unpathed_output.arguments {
syn::PathArguments::AngleBracketed(args) => args,
g => return Err(unhandled_self_return(&g)),
};
let first_arg = generics
.args
.first()
.expect("faux bug: no generic arguments but expected at least one");
let first_arg = match first_arg {
syn::GenericArgument::Type(syn::Type::Path(ty)) => ty,
_ => return Err(unhandled_self_return(&generics)),
};

if !is_self(first_arg) {
return Err(unhandled_self_return(&generics));
}

let output_ident = &unpathed_output.ident;
match real_self {
SelfType::Rc if output_ident == "Rc" => {
quote! { <#output>::new(Self(faux::MaybeFaux::Real(#block))) }
}
SelfType::Arc if output_ident == "Arc" => {
quote! { <#output>::new(Self(faux::MaybeFaux::Real(#block))) }
}
SelfType::Box if output_ident == "Box" => {
quote! { <#output>::new(Self(faux::MaybeFaux::Real(#block))) }
}
SelfType::Owned if output_ident == "Result" || output_ident == "Option" => {
quote! { { #block }.map(faux::MaybeFaux::Real).map(Self) }
}
SelfType::Owned if output_ident == "Box" => {
quote! { <#output>::new(Self(faux::MaybeFaux::Real(*#block))) }
}
SelfType::Owned if output_ident == "Rc" || output_ident == "Arc" => {
let ungenerified = {
// clone so we don't modify the original output
let mut output = output.clone();
output.path.segments.last_mut().unwrap().arguments = PathArguments::None;
output
};
quote! { <#output>::new(Self(faux::MaybeFaux::Real(
#ungenerified::try_unwrap(#block).ok().expect("faux: failed to grab value from reference counter because it was not unique.")
))) }
}
_ => return Err(unhandled_self_return(&output)),
}
};

Ok(Some(wrapped))
}
}

Expand Down Expand Up @@ -344,10 +384,6 @@ impl<'a> MethodData<'a> {
}
}

fn wrong_self_type_error(expected: SelfType, received: SelfType) -> impl std::fmt::Display {
format!(
"faux cannot create {expected} from a self type of {received}. Consider specifying a different self_type in the faux attributes, or moving this method to a non-faux impl block",
expected = expected,
received = received
)
fn unhandled_self_return(spanned: impl Spanned) -> darling::Error {
darling::Error::custom("faux: the return type refers to the mocked struct in a way that faux cannot handle. Split this function into an `impl` block not marked by #[faux::methods]. If you believe this is a mistake or it's a case that should be handled by faux please file an issue").with_span(&spanned)
}
16 changes: 0 additions & 16 deletions faux_macros/src/self_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,6 @@ impl SelfType {
SelfType::Arc => Some(quote!(std::sync::Arc)),
}
}

pub fn from_path(type_path: &syn::TypePath) -> Self {
let segment = type_path.path.segments.last().unwrap();
let ident = &segment.ident;

// can't match on Ident
if ident == "Rc" {
SelfType::Rc
} else if ident == "Arc" {
SelfType::Arc
} else if ident == "Box" {
SelfType::Box
} else {
SelfType::Owned
}
}
}

impl Default for SelfType {
Expand Down
57 changes: 30 additions & 27 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -634,8 +634,6 @@ pub use faux_macros::create;
///
/// # Known Limitations
///
/// * [#13]: Within a module, for a single struct, only a single inherent `impl`
/// and a single trait `impl` per trait may exist.
/// * [#14]: Methods cannot have arguments of the same type as their struct.
/// * [#18]: Generic methods and `impl` return types are not supported.
///
Expand All @@ -647,58 +645,63 @@ pub use faux_macros::create;
///
/// ## Returning mockable struct
///
/// Returning the mockable struct wrapped as a generic of another type
/// (e.g., `Option<Self>`) is not currently supported. The exception
/// to this is returning an instance wrapped by the
/// [self_type](#self_type).
/// When referring to the mockable struct in the signature (either by
/// name or by `Self`) only special cases are allowed. In particular,
/// it is only allowed in the return position of the signature for the
/// follow cases:
///
/// ```compile_fail
/// #[faux::create]
/// pub struct MyStruct {}
/// * Returning the struct itself (e.g., `fn new() -> Self`)
///
/// #[faux::methods]
/// impl MyStruct {
/// pub fn try_to_new() -> Result<Self, String> {
/// Ok(MyStruct {})
/// }
/// }
/// * Returning the struct wrapped directly in: `Rc`, `Arc`, `Box`,
/// `Result`, or `Option`. For `Result`, referring to the struct is
/// only allowed if it's the `Ok` variant of the result. (e.g., `fn
/// load() -> Result<Self, Error>`)
///
/// # fn main() {}
/// ```
/// Any other kind of return type that refers to the mocked struct is
/// not supported by `faux`. Please file an issue if you have a use
/// case that you believe should be common enough for `faux` to handle
/// automatically.
///
/// A workaround is to place these functions in an untagged `impl`
/// block and have them call methods inside the tagged `impl`.
/// A workaround is to place the functions in an untagged `impl` block
/// and have them call methods inside the tagged `impl`.
///
/// ```
/// pub enum Either<X, Y> {
/// Left(X),
/// Right(Y),
/// }
///
/// #[faux::create]
/// pub struct MyStruct {}
/// pub struct MyStruct {
/// x: i32
/// }
///
/// #[faux::methods]
/// impl MyStruct {
/// fn new() -> Self {
/// MyStruct {}
/// MyStruct { x: 4 }
/// }
/// }
///
/// // do not tag this one
/// impl MyStruct {
/// pub fn try_to_new() -> Result<Self, String> {
/// Ok(MyStruct::new())
/// pub fn make_either() -> Either<Self, String> {
/// Either::Left(MyStruct::new())
/// }
/// }
///
/// # fn main() {
/// let x = MyStruct::try_to_new();
/// assert!(x.is_ok());
/// let x = MyStruct::make_either();
/// assert!(matches!(x, Either::Left(MyStruct { .. })));
/// # }
/// ```
///
/// ## Paths in types
///
/// `#[methods]` can be added to blocks of the form `impl
/// path::to::Type` as long as the path does not contain the `super`
/// or `crate` keywords. If it does, use the [`path`](#path) argument to
/// explicitly specify the path.
/// or `crate` keywords. If it does, use the [`path`](#path) argument
/// to explicitly specify the path.
///
/// [receiver]: https://doc.rust-lang.org/reference/items/associated-items.html#methods
pub use faux_macros::methods;
Expand Down
29 changes: 29 additions & 0 deletions tests/return_self_method.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
#![allow(clippy::redundant_clone)]

use std::{rc::Rc, sync::Arc};

type Result<T> = std::result::Result<T, ()>;

#[faux::create]
#[derive(Debug)]
pub struct Foo {
Expand All @@ -26,6 +30,31 @@ impl Foo {
pub fn create_similar(&self) -> Self {
Foo { a: self.a + 1 }
}

pub fn new_boxed() -> Box<Self> {
Box::new(Foo { a: 0 })
}

pub fn new_rc() -> Rc<Self> {
Rc::new(Foo { a: 1 })
}

pub fn new_arc() -> Arc<Self> {
Arc::new(Foo { a: 2 })
}

pub fn new_result() -> std::result::Result<Self, Box<dyn std::error::Error>> {
Ok(Foo { a: 3 })
}

pub fn new_option() -> Option<Self> {
Some(Foo { a: 2 })
}

#[allow(clippy::result_unit_err)]
pub fn new_aliased_result() -> Result<Self> {
Ok(Foo { a: 0 })
}
}

#[test]
Expand Down

0 comments on commit a14e177

Please sign in to comment.