dotnet / roslyn

The Roslyn .NET compiler provides C# and Visual Basic languages with rich code analysis APIs.
https://docs.microsoft.com/dotnet/csharp/roslyn-sdk/
MIT License
19.04k stars 4.03k forks source link

Likely unexpected breaking change introduced recently for `await using` #72819

Closed AlekseyTs closed 4 months ago

AlekseyTs commented 7 months ago

The tests below reflects the current behavior:

        [Fact]
        public void Test()
        {
            var src = @"
using System;
using System.Threading.Tasks;

interface IMyAsyncDisposable1
{
    ValueTask DisposeAsync();
}

interface IMyAsyncDisposable2
{
    ValueTask DisposeAsync();
}

struct S2 : IMyAsyncDisposable1, IMyAsyncDisposable2, IAsyncDisposable
{
    ValueTask IMyAsyncDisposable1.DisposeAsync() => throw null;
    ValueTask IMyAsyncDisposable2.DisposeAsync() => throw null;

    public ValueTask DisposeAsync()
    {
        System.Console.Write('D');
        return ValueTask.CompletedTask;
    }
}

class C
{
    static async Task Main()
    {
        await Test<S2>();
    }

    static async Task Test<T>() where T : IMyAsyncDisposable1, IMyAsyncDisposable2, IAsyncDisposable, new()
    {
        await using (new T())
        {
            System.Console.Write(123);
        }
    }
}
";
            var comp = CreateCompilation(src, targetFramework: TargetFramework.Net80, options: TestOptions.ReleaseExe);

            comp.VerifyDiagnostics(
                // (36,22): error CS0121: The call is ambiguous between the following methods or properties: 'IMyAsyncDisposable1.DisposeAsync()' and 'IMyAsyncDisposable2.DisposeAsync()'
                //         await using (new T())
                Diagnostic(ErrorCode.ERR_AmbigCall, "new T()").WithArguments("IMyAsyncDisposable1.DisposeAsync()", "IMyAsyncDisposable2.DisposeAsync()").WithLocation(36, 22)
                );
        }

The code used to compile successfully and was printing "123D". This is likely a regression from https://github.com/dotnet/roslyn/pull/72598.

Note, similar scenario in await foreach works the same way the await using used to work (i.e. it falls back to using IAsyncDisposable implementation).

using System;
using System.Threading;
using System.Threading.Tasks;

interface ICustomEnumerator
{
    public int Current {get;}

    public ValueTask<bool> MoveNextAsync();
}

interface IGetEnumerator<TEnumerator> where TEnumerator : ICustomEnumerator
{
    TEnumerator GetAsyncEnumerator(CancellationToken token = default);
}

struct S1 : IGetEnumerator<S2>
{
    public S2 GetAsyncEnumerator(CancellationToken token = default)
    {
        return new S2();
    }
}

interface IMyAsyncDisposable1
{
    ValueTask DisposeAsync();
}

interface IMyAsyncDisposable2
{
    ValueTask DisposeAsync();
}

struct S2 : ICustomEnumerator, IMyAsyncDisposable1, IMyAsyncDisposable2, IAsyncDisposable
{
    ValueTask IMyAsyncDisposable1.DisposeAsync() => throw null;
    ValueTask IMyAsyncDisposable2.DisposeAsync() => throw null;
    public ValueTask DisposeAsync()
    { 
        System.Console.Write("D");
        return ValueTask.CompletedTask;
    }

    public int Current => 123;
    public ValueTask<bool> MoveNextAsync()
    {
        return ValueTask.FromResult(false);
    }
}

class C
{
    static async Task Main()
    {
        await Test<S1, S2>();
    }

    static async Task Test<TEnumerable, TEnumerator>()
        where TEnumerable : IGetEnumerator<TEnumerator>
        where TEnumerator : ICustomEnumerator, IMyAsyncDisposable1, IMyAsyncDisposable2, IAsyncDisposable
    {
        await foreach (var i in default(TEnumerable))
        {
            System.Console.Write(i);
        }
    }
}
AlekseyTs commented 7 months ago

CC @jcouv

jaredpar commented 7 months ago

Keeping this in 17.11 for now as we don't have any user reports of this breaking them. If there are reports though we may need to go back and service 17.10 .

MaceWindu commented 5 months ago

Probably same issue with different error

public class Class1 : IAsyncDisposable
{
    ValueTask IAsyncDisposable.DisposeAsync() => default;

    protected virtual ValueTask DisposeAsync(bool disposing) => default;
}

public class Class2
{
    public static async void Test()
    {
                // error CS0122: 'Class1.DisposeAsync(bool)' is inaccessible due to its protection level
        await using var x = new Class1();
    }
}

Quite puzzling, as I wouldn't expect reference to DisposeAsync(bool) method here