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.02k stars 4.03k forks source link

Incorrect nnullability warnings in deconstruction with user-defined conversions #50633

Open TessenR opened 3 years ago

TessenR commented 3 years ago

Version Used:

Branch master (19 Jan 2021)
Latest commit c36b5d3 by msftbot[bot]:
Merge pull request #50583 from CyrusNajmabadi/completoinCommon

Extract common code in completion providers.

Steps to Reproduce:

Compile the following code:

#nullable enable
class C
{
  void M()
  {
    (C c, _) = new C(); // false CS8624
  }

  public void Deconstruct(out D d1, out D d2) => d1 = d2 = new D();
}

class D
{
  public static implicit operator C(D d) => new C();
}

Expected Behavior: No warnings. There's no problem with the deconstruction that returned D! type that is converted to C! via an implicit conversion operator

Actual Behavior: CS8624: Argument of type 'C' cannot be used as an output of type 'D' for parameter 'd1' in 'void C.Deconstruct(out D d1, out D d2)' due to differences in the nullability of reference types. is reported. The warning is quite unclear as these types are just completely different and there's no nullability mismatch involved.

It looks like Roslyn ignores the conversion here and expects the types to have an exact match.

Sebazzz commented 3 years ago

Happens also with:


internal sealed class DecInherited : Ctx<Dec> { }

internal abstract class Ctx<TEntity> {
        public TEntity? ExistingEntity { get; set; }

        public Source? ExistingSource { get; set; }

        public CancellationToken CancellationToken { get; set; }

        public void ThrowIfCancellationRequested() => this.CancellationToken.ThrowIfCancellationRequested();

        public void Deconstruct(out TEntity? existingEntity, out Source? existingSource, out CancellationToken cancellationToken) {
            existingEntity = this.ExistingEntity;
            existingSource = this.ExistingSource;
            cancellationToken = this.CancellationToken;
        }
    }

And then:

// decInherited is DecInherited 
(Dec? variable, Source? existingSource, CancellationToken cancellationToken) = decInherited;

// This works as expected
(var variable, ...)