daveoftheparty / speedy-moq

Generate boilerplate code for Moq in C#
MIT License
1 stars 0 forks source link

Better handling of Generics #47

Closed daveoftheparty closed 1 year ago

daveoftheparty commented 2 years ago

Problem Statement

Currently, the only way to generate setups for the generic:

public interface IStats<T>
{
    T GetMean(IEnumerable<T> data);
}

is to put this on a single line:

IStats

IStats<int> won't give the code tip/generation option.

Then, when the Moq setups are generated, they all end up as

var stats = new Mock<IStats<T>>...

Potential Solutions

Multi-cursor

generate as-is, and switch editor to multi-cursor mode over T which may not be available on all editors

Full implementation

actually implement the mock setups with T, so that IStats<int> becomes

public interface IStats<int>
{
    T GetMean(IEnumerable<int> data);
}

With this, will need to deal with multiple generic arguments such as

public interface IService<T, K, V, X>
{
    ...
}
daveoftheparty commented 2 years ago

After reviewing the code I have a few initial conclusions:

When I initially added generic support, I just modified the model of an interface: InterfaceDefinition and added another property InterfaceNameWithGenerics. I then used that as a direct string substitute in MockText... see the merge of PR #46

It might be easy enough to fix the mock text generation to output the following:

var stats = new Mock<IStats<int>>...

given

IStats<int>

by modifying the part of InterfaceStore that reads the generic args from the semantic model. That method is called GetInterfaceTypeArguments and is simply returning a string that matches the declaration, with a string.Join over all the generic type arguments.

By changing that InterfaceStore method to instead return a list of type arguments, and passing them to MockText, through the InterfaceDefinition model we can still easily do the text replacement.

What might make all this ridiculously hard is the interaction with Diagnoser.cs-- we'd have to be able to identify the generic type arguments passed by the user when they are trying to get the Generate Moq Setups prompt, and we'd have to be able to pass the user-supplied generic type arguments into MockText

tackling this issue should be attempted from the Diagnoser.cs side first... capturing the user generic type arguments, and successfully bouncing that off of interfaceStore.Exists()-- because all the other trappings around converting the string of generic args in InterfaceStore to a proper list model won't matter if the Diagnoser stuff can't be done/is too hard and interest is lost in solving this problem through code.

Also, there is a workaround, though not exactly pretty:

given the interface:

public interface IGenericService<TSource, TResult>
{
    IEnumerable<TResult> TransformSource(IEnumerable<TSource> items);
}

if a user inputs this in a test file (note lack of angle brackets or generic type arguments):

[Test]
public void Go()
{
    IGenericService
}

they will get this, which produces a compilation error due to TSource, TResult being undefined:

[Test]
public void Go()
{
    var genericService = new Mock<IGenericService<TSource, TResult>>();

    Expression<Func<IGenericService<TSource, TResult>, IEnumerable<TResult>>> transformSource = x =>
        x.TransformSource(It.IsAny<IEnumerable<TSource>>());

    genericService
        .Setup(transformSource)
        .Returns((IEnumerable<TSource> items) =>
        {
            return default;
        });

    genericService.Verify(transformSource, Times.Once);
}

and yet, IT CAN BE FIXED WITH using statements:

using TSource=System.Int32;
using TResult=System.Int32;
daveoftheparty commented 2 years ago

Note all of these are valid interfaces:


private interface IService
{
    void DoIt();
}

private interface IService<T>
{
    T DoIt();
}

private interface IService<T, K, V>
{
    T DoIt(K key, V value);
}