dotnet / roslynator

Roslynator is a set of code analysis tools for C#, powered by Roslyn.
https://josefpihrt.github.io/docs/roslynator
Other
3.05k stars 255 forks source link

Task/Async-related analyzers are not aware of custom task types and duck-typed awaitables (affects RCS1046, RCS1047, RCS1090, RCS1229, and RCS1261) #1529

Open Govorunb opened 1 week ago

Govorunb commented 1 week ago

Product and Version Used

VS extension version 4.12.5

Background

C# supports awaiting any expression whose type has a specific shape (C# spec). Additionally, async methods can have their return type be any "task-like" type, which is not too complex to implement in user code (C# spec).

Currently, as far as I've been able to tell, analyzers like RCS1047 don't check for those shapes and thus end up misfiring (or not firing) on custom task types.

Example/reproduction code

using System;
using System.IO;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

#pragma warning disable CA1822 // Mark members as static
#pragma warning disable CA1050 // Declare types in namespaces
#pragma warning disable CS9113 // Parameter is unread.
#nullable disable

class CustomTask
{
    // class must have a (visible) instance or extension GetAwaiter method (returning an awaiter type) to be awaitable
    public Awaiter GetAwaiter() => default;
    public CustomTask DoSomething() => this; // expected RCS1046 (if enabled)
    public CustomTask DoSomethingAsync() => this; // expected no RCS1047
    public async Task ClassicallyAsync() => await DoSomethingAsync(); // expected RCS1090 (if enabled)
    public Task ExpectedlyAsync() => ClassicallyAsync(); // no RCS1047 as expected (since Task is known)

    // must return an awaitable type
    public ConfiguredAwaitable ConfigureAwait(bool continueOnCapturedContext) => new ConfiguredAwaitable(this);
}

interface ICustomTask
{
    CustomTask DoAsync(); // expected no RCS1047
}

#region Awaiter shape
public struct Awaiter : INotifyCompletion
{
    public bool IsCompleted => false;
    public void OnCompleted(Action continuation) { }
    public void GetResult() { }
}

public struct Awaiter<T> : INotifyCompletion
{
    public bool IsCompleted => false;
    public void OnCompleted(Action continuation) { }
    public T GetResult() => default;
}
#endregion

// This attribute is required for the type to be a valid return type for an async method
[AsyncMethodBuilder(typeof(CustomTask<>))]
class CustomTask<T>
{
    //public async CustomTask Nope() { } // CS1983
    public CustomTask(T t) { }
    public Awaiter<T> GetAwaiter() => default;

    public CustomTask<int> Foo()
    {
        using FileStream fs = default!; // expected RCS1261
        return new CustomTask<int>(1);
    }

    public async CustomTask<int> FooAsync()
    {
        using FileStream fs = default!; // get RCS1261 as expected (since it checks if method is async)
        if (fs.CanRead)
        {
            return await new CustomTask<int>(1).ConfigureAwait(false);
        }
        else
        {
            return 2;
        }
    }

    public async Task Bar()
    {
        await Foo(); // expected RCS1090 (if enabled)
    }

    public CustomTask<int> Baz() // expected RCS1229
    {
        using (default(IDisposable))
        {
            return new CustomTask<int>(1);
        }
    }

    #region Async method builder shape
    public static CustomTask<T> Create() => default!;
    public CustomTask<T> Task { get; set; } = null!;
    public void SetStateMachine(IAsyncStateMachine stateMachine) { }
    public void SetException(Exception ex) { }
    public void SetResult(T result) { }
    public void Start<TStateMachine>(ref TStateMachine stateMachine)
        where TStateMachine : IAsyncStateMachine
    { }
    public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : INotifyCompletion
        where TStateMachine : IAsyncStateMachine
    { }
    public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : ICriticalNotifyCompletion
        where TStateMachine : IAsyncStateMachine
    { }
    #endregion
}

struct ConfiguredAwaitable
{
    public ConfiguredAwaitable(CustomTask task) { }

    public Awaiter GetAwaiter() => default;
}

static class Extensions
{
    public static CustomTask<T> ConfigureAwait<T>(this CustomTask<T> task, bool continueOnCapturedContext)
    {
        return task; // just needs to be awaitable
    }
}

I have a branch ready with fixes, let me know if you're cool with a PR 👍

josefpihrt commented 1 week ago

Hi @Govorunb,

Please go ahead with the PR!