diff --git a/CHANGELOG.md b/CHANGELOG.md index b4f8484..1f8196d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/faux_macros/src/methods/morphed.rs b/faux_macros/src/methods/morphed.rs index 9777805..c006d3e 100644 --- a/faux_macros/src/methods/morphed.rs +++ b/faux_macros/src/methods/morphed.rs @@ -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); @@ -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)) } } @@ -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) } diff --git a/faux_macros/src/self_type.rs b/faux_macros/src/self_type.rs index 8baabab..f0b7bd9 100644 --- a/faux_macros/src/self_type.rs +++ b/faux_macros/src/self_type.rs @@ -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 { diff --git a/src/lib.rs b/src/lib.rs index 2ab67ab..398a6a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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. /// @@ -647,49 +645,54 @@ pub use faux_macros::create; /// /// ## Returning mockable struct /// -/// Returning the mockable struct wrapped as a generic of another type -/// (e.g., `Option`) 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 { -/// 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`) /// -/// # 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 { +/// 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 { -/// Ok(MyStruct::new()) +/// pub fn make_either() -> Either { +/// 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 { .. }))); /// # } /// ``` /// @@ -697,8 +700,8 @@ pub use faux_macros::create; /// /// `#[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; diff --git a/tests/return_self_method.rs b/tests/return_self_method.rs index d6e6547..ca7c240 100644 --- a/tests/return_self_method.rs +++ b/tests/return_self_method.rs @@ -1,5 +1,9 @@ #![allow(clippy::redundant_clone)] +use std::{rc::Rc, sync::Arc}; + +type Result = std::result::Result; + #[faux::create] #[derive(Debug)] pub struct Foo { @@ -26,6 +30,31 @@ impl Foo { pub fn create_similar(&self) -> Self { Foo { a: self.a + 1 } } + + pub fn new_boxed() -> Box { + Box::new(Foo { a: 0 }) + } + + pub fn new_rc() -> Rc { + Rc::new(Foo { a: 1 }) + } + + pub fn new_arc() -> Arc { + Arc::new(Foo { a: 2 }) + } + + pub fn new_result() -> std::result::Result> { + Ok(Foo { a: 3 }) + } + + pub fn new_option() -> Option { + Some(Foo { a: 2 }) + } + + #[allow(clippy::result_unit_err)] + pub fn new_aliased_result() -> Result { + Ok(Foo { a: 0 }) + } } #[test]