nrxus / faux

Struct mocking library for Rust
https://nrxus.github.io/faux/
MIT License
411 stars 14 forks source link

Does not support associated types #57

Open zhixinwen opened 8 months ago

zhixinwen commented 8 months ago
    #[cfg_attr(test, faux::create)]
    struct Object {}

    #[cfg_attr(test, faux::methods)]
    impl Object {
        pub fn method<A>(&self, data: impl IntoIterator<Item = A>) {}
    }

    #[test]
    fn test_mock() {
        let o = Object::faux();
    }

Error is

error[E0191]: the value of the associated type `IntoIter` (from trait `IntoIterator`) must be specified
    |
784 |         pub fn method<A>(&self, data: impl IntoIterator<Item = A>) {}
    |                                            ^^^^^^^^^^^^^^^^^^^^^^ help: specify the associated type: `IntoIterator<Item = A, IntoIter = Type>`
nrxus commented 8 months ago

Hmm this is a little tricky. To be specific though faux supports associated types but not elided associated types. IntoIterator has two different associated types (Item + IntoIter), but only one is explicitly stated (Item), meaning that IntoIter is elided and faux doesn't know about it.

As a workaround you could get away with using impl Iterator<Item =A>. It does mean your callers will have to call .into_iter() manually but I don't think that makes the API any less flexible.

The reason why this is tricky is a bit complex but I am going to try to explain it as a reference to myself in the future (or if you're interested in submitting a PR for it ofc you'd be welcome to).

When you have a function like:

fn method<A>(&self, data: impl IntoIterator<Item = A>) {}

Rust will under the hood desugar it to something like:

fn method<A, I>(&self, data: impl IntoIterator<Item = A, IntoIter=I>) {}

Which means that there is a hidden generic there (I), but Rust allows it to elided, which is fine because Rust will know during monomorphization what that hidden generic is. But if we AREN'T doing monomorphization (i.e., using dyn) then Rust wouldn't be able to do that, so dynamic objects (i.e., Box<dyn Trait>) cannot elide associated types.

faux currently handles arguments of the syntax impl Trait by converting them to Box<dyn Trait> when mocking (so the signature of the function remains the same but the signature of how it's mocked uses the dynamic form). Because we've converted it to Box<dyn Trait> we now need to specify the associated type explicitly.

Specifying the associated type is actually not that hard, it just means adding a generic in some internal function that faux generates but the issue is that faux has no way of knowing that it needs to add that extra generic because there is no hint that impl IntoIterator<Item =A> has an elided associated type missing (proc macros work purely at the syntax level and there is nothing in the syntax about the missing associated type). So something else needs to tell faux about this missing associated type, which means I gotta come up with a good syntax for it.

Maybe something like:

#[cfg_attr(test, faux::methods)]
// specifies all associated types of `IntoIterator` so `faux` knows about them. 
#[cfg_attr(test, faux(hint = "IntoIterator<type Item, type IntoIter>"))]
impl Object {
    pub fn method<A>(&self, data: impl IntoIterator<Item = A>) {}
}

But that's just my first idea off the top of my head so I may need to chew on it more to see if i can think of a better UX for this, but something does need to be explicitly given to faux so it knows about the elided associated type.