diff --git a/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/ConversionsBase.cs b/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/ConversionsBase.cs index 1ecf20866728a449d7c7f0dd50e9cd7b663eb29e..dfcc44dea762d6fc314e81fe0afd53aa7e7e682b 100644 --- a/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/ConversionsBase.cs +++ b/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/ConversionsBase.cs @@ -2712,6 +2712,7 @@ private bool HasVariantConversionNoCycleCheck(NamedTypeSymbol source, NamedTypeS source.GetAllTypeArguments(sourceTypeArguments, ref useSiteDiagnostics); destination.GetAllTypeArguments(destinationTypeArguments, ref useSiteDiagnostics); + Debug.Assert(TypeSymbol.Equals(source.OriginalDefinition, destination.OriginalDefinition, TypeCompareKind.AllIgnoreOptions)); Debug.Assert(typeParameters.Count == sourceTypeArguments.Count); Debug.Assert(typeParameters.Count == destinationTypeArguments.Count); @@ -2732,6 +2733,19 @@ private bool HasVariantConversionNoCycleCheck(NamedTypeSymbol source, NamedTypeS switch (typeParameterSymbol.Variance) { case VarianceKind.None: + // System.IEquatable is invariant for back compat reasons (dynamic type checks could start + // to succeed where they previously failed, creating different runtime behavior), but the uses + // require treatment specifically of nullability as contravariant, so we special case the + // behavior here. Normally we use GetWellKnownType for these kinds of checks, but in this + // case we don't want just the canonical IEquatable to be special-cased, we want all definitions + // to be treated as contravariant, in case there are other definitions in metadata that were + // compiled with that expectation. + if (isTypeIEquatable(destination.OriginalDefinition) && + TypeSymbol.Equals(destinationTypeArgument.Type, sourceTypeArgument.Type, TypeCompareKind.AllNullableIgnoreOptions) && + HasAnyNullabilityImplicitConversion(destinationTypeArgument, sourceTypeArgument)) + { + return true; + } return false; case VarianceKind.Out: @@ -2761,6 +2775,19 @@ private bool HasVariantConversionNoCycleCheck(NamedTypeSymbol source, NamedTypeS } return true; + + static bool isTypeIEquatable(NamedTypeSymbol type) + { + return type is + { + IsInterface: true, + Name: "IEquatable", + ContainingNamespace: { Name: "System", ContainingNamespace: { IsGlobalNamespace: true } }, + ContainingSymbol: { Kind: SymbolKind.Namespace }, + TypeParameters: { Length: 1 } + }; + } + } // Spec 6.1.10 diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs index a2648ea4f5d94727af98c992e23e5b54ae3e8bf9..dba4cf42453d74dfb7b32327db21481d819b8021 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs @@ -102101,14 +102101,7 @@ public class Broken : IEnumerable> where T : class } "; var comp = CreateCompilation(source, options: WithNonNullTypesTrue()); - comp.VerifyDiagnostics( - // (8,40): warning CS8613: Nullability of reference types in return type of 'IEnumerator> Working.GetEnumerator()' doesn't match implicitly implemented member 'IEnumerator> IEnumerable>.GetEnumerator()'. - // public IEnumerator> GetEnumerator() => throw null!; - Diagnostic(ErrorCode.WRN_NullabilityMismatchInReturnTypeOnImplicitImplementation, "GetEnumerator").WithArguments("IEnumerator> Working.GetEnumerator()", "IEnumerator> IEnumerable>.GetEnumerator()").WithLocation(8, 40), - // (15,60): warning CS8616: Nullability of reference types in return type doesn't match implemented member 'IEnumerator> IEnumerable>.GetEnumerator()'. - // IEnumerator> IEnumerable>.GetEnumerator() => throw null!; - Diagnostic(ErrorCode.WRN_NullabilityMismatchInReturnTypeOnExplicitImplementation, "GetEnumerator").WithArguments("IEnumerator> IEnumerable>.GetEnumerator()").WithLocation(15, 60) - ); + comp.VerifyDiagnostics(); } [WorkItem(30140, "https://github.com/dotnet/roslyn/issues/30140")] @@ -112197,5 +112190,209 @@ void M() // _ = location.ToString(); // 1 Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "location").WithLocation(19, 13)); } + + [Fact] + [WorkItem(37187, "https://github.com/dotnet/roslyn/issues/37187")] + public void IEquatableContravariantNullability() + { + var def = @" +using System; + +namespace System +{ + public interface IEquatable + { + bool Equals(T other); + } +} + +public class A : IEquatable +{ + public bool Equals(A? a) => false; + + public static void M(Span s) where T : IEquatable? + { + s[0]?.Equals(null); + s[0]?.Equals(s[1]); + } +} + +public class B : IEquatable<(B?, B?)> +{ + public bool Equals((B?, B?) l) => false; +} +"; + var spanRef = CreateCompilation(SpanSource, options: TestOptions.UnsafeReleaseDll) + .EmitToImageReference(); + var comp = CreateCompilation(def + @" +class C +{ + static void Main() + { + var x = new Span(); + var y = new Span(); + + A.M(x); + A.M(y); + + IEquatable i1 = new A(); + IEquatable i2 = i1; + IEquatable i3 = new A(); + IEquatable i4 = i3; // warn + _ = i4; + + IEquatable<(B?, B?)> i5 = new B(); + IEquatable<(B, B)> i6 = i5; + IEquatable<(B, B)> i7 = new B(); + IEquatable<(B?, B?)> i8 = i7; // warn + _ = i8; + } +}", + new[] { spanRef }, + options: WithNonNullTypesTrue().WithSpecificDiagnosticOptions("CS0436", ReportDiagnostic.Suppress)); + comp.VerifyDiagnostics( + // (34,29): warning CS8619: Nullability of reference types in value of type 'IEquatable' doesn't match target type 'IEquatable'. + // IEquatable i4 = i3; // warn + Diagnostic(ErrorCode.WRN_NullabilityMismatchInAssignment, "i3").WithArguments("System.IEquatable", "System.IEquatable").WithLocation(41, 29), + // (40,35): warning CS8619: Nullability of reference types in value of type 'IEquatable<(B, B)>' doesn't match target type 'IEquatable<(B?, B?)>'. + // IEquatable<(B?, B?)> i8 = i7; // warn + Diagnostic(ErrorCode.WRN_NullabilityMismatchInAssignment, "i7").WithArguments("System.IEquatable<(B, B)>", "System.IEquatable<(B?, B?)>").WithLocation(47, 35) + ); + + // Test with non-wellknown type + var defComp = CreateCompilation(def, + references: new[] { spanRef }, + options: WithNonNullTypesTrue().WithSpecificDiagnosticOptions("CS0436", ReportDiagnostic.Suppress)); + defComp.VerifyDiagnostics(); + var useComp = CreateCompilation(@" +using System; +public interface IEquatable +{ + bool Equals(T other); +} +class C +{ + static void Main() + { + var x = new Span(); + var y = new Span(); + + A.M(x); + A.M(y); + } +}", + references: new[] { spanRef, defComp.ToMetadataReference() }, + options: WithNonNullTypesTrue()); + useComp.VerifyDiagnostics(); + } + + [Fact] + public void IEquatableNotContravariantExceptNullability() + { + var src = @" +using System; + +class A : IEquatable +{ + public bool Equals(A? a) => false; +} + +class B : A {} + +class C +{ + static void Main() + { + var x = new Span(); + var y = new Span(); + + M(x); + M(y); + } + static void M(Span s) where T : IEquatable? { } +}"; + var comp = CreateCompilationWithSpan(src + @" +namespace System +{ + public interface IEquatable + { + bool Equals(T other); + } +} +", options: WithNonNullTypesTrue().WithSpecificDiagnosticOptions("CS0436", ReportDiagnostic.Suppress)); + comp.VerifyDiagnostics( + // (25,9): error CS0311: The type 'B' cannot be used as type parameter 'T' in the generic type or method 'C.M(Span)'. There is no implicit reference conversion from 'B' to 'System.IEquatable'. + // M(x); + Diagnostic(ErrorCode.ERR_GenericConstraintNotSatisfiedRefType, "M").WithArguments("C.M(System.Span)", "System.IEquatable", "T", "B").WithLocation(18, 9), + // (26,9): error CS0311: The type 'B' cannot be used as type parameter 'T' in the generic type or method 'C.M(Span)'. There is no implicit reference conversion from 'B' to 'System.IEquatable'. + // M(y); + Diagnostic(ErrorCode.ERR_GenericConstraintNotSatisfiedRefType, "M").WithArguments("C.M(System.Span)", "System.IEquatable", "T", "B").WithLocation(19, 9)); + + // If IEquatable is actually marked contravariant this is fine + comp = CreateCompilationWithSpan(src + @" +namespace System +{ + public interface IEquatable + { + bool Equals(T other); + } +} +", options: WithNonNullTypesTrue().WithSpecificDiagnosticOptions("CS0436", ReportDiagnostic.Suppress)); + comp.VerifyDiagnostics(); + } + + [Fact] + public void IEquatableInWrongNamespace() + { + var comp = CreateCompilation(@" +public interface IEquatable +{ + bool Equals(T other); +} + +public class A : IEquatable +{ + public bool Equals(A? a) => false; + + public static void Main() + { + IEquatable i1 = new A(); + IEquatable i2 = i1; + IEquatable i3 = i2; + _ = i3; + } +} +", options: WithNonNullTypesTrue()); + comp.VerifyDiagnostics( + // (14,28): warning CS8619: Nullability of reference types in value of type 'IEquatable' doesn't match target type 'IEquatable'. + // IEquatable i2 = i1; + Diagnostic(ErrorCode.WRN_NullabilityMismatchInAssignment, "i1").WithArguments("IEquatable", "IEquatable").WithLocation(14, 28), + // (15,29): warning CS8619: Nullability of reference types in value of type 'IEquatable' doesn't match target type 'IEquatable'. + // IEquatable i3 = i2; + Diagnostic(ErrorCode.WRN_NullabilityMismatchInAssignment, "i2").WithArguments("IEquatable", "IEquatable").WithLocation(15, 29)); + } + + [Fact] + public void IEquatableNullableVarianceOutParameters() + { + var comp = CreateCompilation(@" +using System; +class C +{ + void M(IEquatable input, out IEquatable output) where T: class + { + output = input; + } +} +namespace System +{ + public interface IEquatable + { + bool Equals(T other); + } +} +", options: WithNonNullTypesTrue().WithSpecificDiagnosticOptions("CS0436", ReportDiagnostic.Suppress)); + comp.VerifyDiagnostics(); + } } } diff --git a/src/Compilers/Test/Utilities/CSharp/CSharpTestBase.cs b/src/Compilers/Test/Utilities/CSharp/CSharpTestBase.cs index 429d2f6a39f65435b7fd475202061b74a51f9d80..77e19773dd63fb82379395c4243116148ff6e36f 100644 --- a/src/Compilers/Test/Utilities/CSharp/CSharpTestBase.cs +++ b/src/Compilers/Test/Utilities/CSharp/CSharpTestBase.cs @@ -1842,6 +1842,9 @@ protected static CSharpCompilation CreateCompilationWithSpan(SyntaxTree tree, CS return comp; } + protected static CSharpCompilation CreateCompilationWithSpan(string s, CSharpCompilationOptions options = null) + => CreateCompilationWithSpan(SyntaxFactory.ParseSyntaxTree(s), options); + protected static CSharpCompilation CreateCompilationWithMscorlibAndSpan(string text, CSharpCompilationOptions options = null, CSharpParseOptions parseOptions = null) { var reference = CreateEmptyCompilation(