roslynator icon indicating copy to clipboard operation
roslynator copied to clipboard

Detect implicit conversion of async delegates

Open jnyrup opened this issue 6 years ago • 4 comments
trafficstars

To complement CS4014 that detects async calls not await'ed, it could be nice to have an analyzer to detect implicit conversions from async delegates to sync ones.

async Task FooAsync<T>(Func<T> func)
{
    await Task.Delay(0);
    func(); // the async delegate is not await'ed!
}

Task BarAsync(Func<Task> func)
{
    return FooAsync(func); // implicit conversion to Func<T>
}

It's not always easily detectable using Intellisense image

jnyrup avatar Oct 28 '19 15:10 jnyrup

How the code fix would look like?

josefpihrt avatar Nov 01 '19 00:11 josefpihrt

My thought was mostly as an analyzer that could detect the problematic usage. So an analyzer without an automatic code fix.

A manual code fix could of course be

async Task FooAsync<T>(Func<Task> func) // Func<Task> overload
{
    await Task.Delay(0);
    await func(); // await the async delegate
}

jnyrup avatar Nov 01 '19 07:11 jnyrup

To summarize it, you propose to change:

func();

to

await func();

where func is of type Func<TAwaitable>

is this correct?

josefpihrt avatar Nov 05 '19 18:11 josefpihrt

Not exactly, let me try with a better example.

I have a utility function Retry.UpToThreeTimes that retries a Func<T> up to three times if it throws exceptions.

static class Retry
{
    public static void UpToThreeTimes<T>(Func<T> func)
    {
        Console.WriteLine("start retry");
        for (int i = 0; i < 3; i++)
        {
            try
            {
                func();
            }
            catch
            {
                Console.WriteLine("try again");
                continue;
            }

            Console.WriteLine("non-failure");
            return;
        }

        Console.WriteLine("tried three times without luck");
    }
}

It's used in two of my programs.

class Program
{
    public static void Main()
    {
        Func<int> func = () => throw new Exception();
        Retry.UpToThreeTimes(func);
    }
}

class Program2
{
    public static void Main()
    {
        Func<int> func = () => throw new Exception();
        Retry.UpToThreeTimes(func);
    }
}

The output of both programs is

start retry
try again
try again
try again
tried three times without luck

Now I want to refactor Program2 to use some async method, but I'm missing out that Retry.UpToThreeTimes is not Task aware.

class Program2
{
    public static async Task Main()
    {
        Func<Task<int>> func = async () =>
        {
            await Task.Delay(0);
            throw new Exception();
        };

        Retry.UpToThreeTimes(func); // Func<Task<int>> still matches the `Func<T>` signature
    }
}

As Retry.UpToThreeTimes is not Task aware it cannot await func() and is not testing whether the running task throws an exception, but whether creating the task throws an exception. So instead of retrying the Func<Task<int>> three times, the output is now simply:

start retry
non-failure

To summarize:

  • An analyzer that catches Func<Task<TResult>> being cast to Func<TNonAwaitable>
  • I'm not sure about an automatic codefix as Program uses Retry.UpToThreeTimes correctly.

jnyrup avatar Nov 06 '19 08:11 jnyrup