Task/Async-related analyzers are not aware of custom task types and duck-typed awaitables (affects RCS1046, RCS1047, RCS1090, RCS1229, and RCS1261) #1529
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 👍
Product and Version Used
VS extension version 4.12.5
Background
C# supports
await
ing 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
I have a branch ready with fixes, let me know if you're cool with a PR 👍