diff --git a/docs/features/nullable-reference-types.md b/docs/features/nullable-reference-types.md index bdf189e22b40e..62fc7da0a445b 100644 --- a/docs/features/nullable-reference-types.md +++ b/docs/features/nullable-reference-types.md @@ -39,6 +39,12 @@ If the analysis determines that a null check always (or never) passes, a hidden A number of null checks affect the flow state when tested for: - comparisons to `null`: `x == null` and `x != null` - `is` operator: `x is null`, `x is K` (where `K` is a constant), `x is string`, `x is string s` +- calls to well-known equality methods, including: + - `static bool object.Equals(object, object)` + - `static bool object.ReferenceEquals(object, object)` + - `bool object.Equals(object)` and overrides + - `bool IEquatable(T)` and implementations + - `bool IEqualityComparer(T, T)` and implementations Invocation of methods annotated with the following attributes will also affect flow analysis: - simple pre-conditions: `[AllowNull]` and `[DisallowNull]` diff --git a/src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.cs b/src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.cs index a25a9bf747971..34bc69c3ef9c0 100644 --- a/src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.cs +++ b/src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.cs @@ -2164,8 +2164,6 @@ protected override void AfterLeftChildHasBeenVisited(BoundBinaryOperator binary) if (operandComparedToNull != null) { - operandComparedToNull = SkipReferenceConversions(operandComparedToNull); - // Set all nested conditional slots. For example in a?.b?.c we'll set a, b, and c. bool nonNullCase = op != BinaryOperatorKind.Equal; // true represents WhenTrue splitAndLearnFromNonNullTest(operandComparedToNull, whenTrue: nonNullCase); @@ -2813,8 +2811,11 @@ private void ReinferMethodAndVisitArguments(BoundCall node, TypeWithState receiv method = (MethodSymbol)AsMemberOfType(receiverType.Type, method); } - method = VisitArguments(node, node.Arguments, refKindsOpt, method.Parameters, node.ArgsToParamsOpt, - node.Expanded, node.InvokedAsExtensionMethod, method).method; + ImmutableArray results; + (method, results) = VisitArguments(node, node.Arguments, refKindsOpt, method.Parameters, node.ArgsToParamsOpt, + node.Expanded, node.InvokedAsExtensionMethod, method); + + LearnFromEqualsMethod(method, node, receiverType, results); if (method.MethodKind == MethodKind.LocalFunction) { @@ -2825,6 +2826,104 @@ private void ReinferMethodAndVisitArguments(BoundCall node, TypeWithState receiv SetResult(node, GetReturnTypeWithState(method), method.ReturnTypeWithAnnotations); } + private void LearnFromEqualsMethod(MethodSymbol method, BoundCall node, TypeWithState receiverType, ImmutableArray results) + { + // easy out + var parameterCount = method.ParameterCount; + if ((parameterCount != 1 && parameterCount != 2) + || method.MethodKind != MethodKind.Ordinary + || method.ReturnType.SpecialType != SpecialType.System_Boolean + || (method.Name != SpecialMembers.GetDescriptor(SpecialMember.System_Object__Equals).Name + && method.Name != SpecialMembers.GetDescriptor(SpecialMember.System_Object__ReferenceEquals).Name)) + { + return; + } + + var arguments = node.Arguments; + + var isStaticEqualsMethod = method.Equals(compilation.GetSpecialTypeMember(SpecialMember.System_Object__EqualsObjectObject)) + || method.Equals(compilation.GetSpecialTypeMember(SpecialMember.System_Object__ReferenceEquals)); + if (isStaticEqualsMethod || + isWellKnownEqualityMethodOrImplementation(compilation, method, WellKnownMember.System_Collections_Generic_IEqualityComparer_T__Equals)) + { + Debug.Assert(arguments.Length == 2); + learnFromEqualsMethodArguments(arguments[0], results[0].RValueType, arguments[1], results[1].RValueType); + return; + } + + var isObjectEqualsMethodOrOverride = method.GetLeastOverriddenMethod(accessingTypeOpt: null) + .Equals(compilation.GetSpecialTypeMember(SpecialMember.System_Object__Equals)); + if (isObjectEqualsMethodOrOverride || + isWellKnownEqualityMethodOrImplementation(compilation, method, WellKnownMember.System_IEquatable_T__Equals)) + { + Debug.Assert(arguments.Length == 1); + learnFromEqualsMethodArguments(node.ReceiverOpt, receiverType, arguments[0], results[0].RValueType); + return; + } + + static bool isWellKnownEqualityMethodOrImplementation(CSharpCompilation compilation, MethodSymbol method, WellKnownMember wellKnownMember) + { + var wellKnownMethod = compilation.GetWellKnownTypeMember(wellKnownMember); + if (wellKnownMethod is null) + { + return false; + } + + var wellKnownType = wellKnownMethod.ContainingType; + var parameterType = method.Parameters[0].TypeWithAnnotations; + var constructedType = wellKnownType.Construct(ImmutableArray.Create(parameterType)); + + Symbol constructedMethod = null; + foreach (var member in constructedType.GetMembers(WellKnownMemberNames.ObjectEquals)) + { + if (member.OriginalDefinition.Equals(wellKnownMethod)) + { + constructedMethod = member; + break; + } + } + + Debug.Assert(constructedMethod != null, "the original definition is present but the constructed method isn't present"); + + // FindImplementationForInterfaceMember doesn't check if this method is itself the interface method we're looking for + if (constructedMethod.Equals(method)) + { + return true; + } + + var implementationMethod = method.ContainingType.FindImplementationForInterfaceMember(constructedMethod); + return method.Equals(implementationMethod); + } + + void learnFromEqualsMethodArguments(BoundExpression left, TypeWithState leftType, BoundExpression right, TypeWithState rightType) + { + // comparing anything to a null literal gives maybe-null when true and not-null when false + // comparing a maybe-null to a not-null gives us not-null when true, nothing learned when false + if (left.ConstantValue?.IsNull == true) + { + Split(); + LearnFromNullTest(right, ref StateWhenTrue); + LearnFromNonNullTest(right, ref StateWhenFalse); + } + else if (right.ConstantValue?.IsNull == true) + { + Split(); + LearnFromNullTest(left, ref StateWhenTrue); + LearnFromNonNullTest(left, ref StateWhenFalse); + } + else if (leftType.MayBeNull && rightType.IsNotNull) + { + Split(); + LearnFromNonNullTest(left, ref StateWhenTrue); + } + else if (rightType.MayBeNull && leftType.IsNotNull) + { + Split(); + LearnFromNonNullTest(right, ref StateWhenTrue); + } + } + } + private TypeWithState VisitCallReceiver(BoundCall node) { var receiverOpt = node.ReceiverOpt; diff --git a/src/Compilers/CSharp/Portable/Symbols/TypeSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/TypeSymbol.cs index 1efba690bbaa9..e95e076601021 100644 --- a/src/Compilers/CSharp/Portable/Symbols/TypeSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/TypeSymbol.cs @@ -1705,7 +1705,7 @@ private static Location GetInterfaceLocation(Symbol interfaceMember, TypeSymbol snt = implementingType as SourceMemberContainerTypeSymbol; } - return snt?.GetImplementsLocation(@interface) ?? implementingType.Locations[0]; + return snt?.GetImplementsLocation(@interface) ?? implementingType.Locations.FirstOrNone(); } private static bool ReportAnyMismatchedConstraints(MethodSymbol interfaceMethod, TypeSymbol implementingType, MethodSymbol implicitImpl, DiagnosticBag diagnostics) diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs index 86a77f966eefb..f1da3c6b94822 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs @@ -109735,6 +109735,975 @@ static void M( comp.VerifyDiagnostics(); } + [Theory] + [InlineData("object.Equals")] + [InlineData("object.ReferenceEquals")] + [WorkItem(35816, "https://github.com/dotnet/roslyn/issues/35816")] + public void StaticEqualsMethod_NonNullExpr_LiteralNull_WarnsWhenTrue(string equalsMethodName) + { + var source = $@" +#nullable enable +class C +{{ + static void M(string s) + {{ + if ({equalsMethodName}(s, null)) + {{ + _ = s.ToString(); // 1 + }} + else + {{ + _ = s.ToString(); + }} + }} + + static void M2(string s) + {{ + if ({equalsMethodName}(null, s)) + {{ + _ = s.ToString(); // 2 + }} + else + {{ + _ = s.ToString(); + }} + }} +}}"; + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (9,17): warning CS8602: Dereference of a possibly null reference. + // _ = s.ToString(); // 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(9, 17), + // (21,17): warning CS8602: Dereference of a possibly null reference. + // _ = s.ToString(); // 2 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(21, 17)); + } + + [Theory] + [InlineData("object.Equals")] + [InlineData("object.ReferenceEquals")] + [WorkItem(35816, "https://github.com/dotnet/roslyn/issues/35816")] + public void StaticEqualsMethod_NonNullExpr_NonNullExpr_NoWarning(string equalsMethodName) + { + var source = $@" +#nullable enable +class C +{{ + static void M(string s1, string s2) + {{ + if ({equalsMethodName}(s1, s2)) + {{ + _ = s1.ToString(); + _ = s2.ToString(); + }} + else + {{ + _ = s1.ToString(); + _ = s2.ToString(); + }} + }} + + static void M2(string s1, string s2) + {{ + if ({equalsMethodName}(s2, s1)) + {{ + _ = s1.ToString(); + _ = s2.ToString(); + }} + else + {{ + _ = s1.ToString(); + _ = s2.ToString(); + }} + }} +}}"; + var comp = CreateCompilation(source); + comp.VerifyDiagnostics(); + } + + [Theory] + [InlineData("object.Equals")] + [InlineData("object.ReferenceEquals")] + [WorkItem(35816, "https://github.com/dotnet/roslyn/issues/35816")] + public void StaticEqualsMethod_MaybeNullExpr_MaybeNullExpr_Warns(string equalsMethodName) + { + var source = $@" +#nullable enable +class C +{{ + static void M(string? s1, string? s2) + {{ + if ({equalsMethodName}(s1, s2)) + {{ + _ = s1.ToString(); // 1 + _ = s2.ToString(); // 2 + }} + else + {{ + _ = s1.ToString(); // 3 + _ = s2.ToString(); // 4 + }} + }} + + static void M2(string? s1, string? s2) + {{ + if ({equalsMethodName}(s2, s1)) + {{ + _ = s1.ToString(); // 5 + _ = s2.ToString(); // 6 + }} + else + {{ + _ = s1.ToString(); // 7 + _ = s2.ToString(); // 8 + }} + }} +}}"; + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (9,17): warning CS8602: Dereference of a possibly null reference. + // _ = s1.ToString(); // 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s1").WithLocation(9, 17), + // (10,17): warning CS8602: Dereference of a possibly null reference. + // _ = s2.ToString(); // 2 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s2").WithLocation(10, 17), + // (14,17): warning CS8602: Dereference of a possibly null reference. + // _ = s1.ToString(); // 3 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s1").WithLocation(14, 17), + // (15,17): warning CS8602: Dereference of a possibly null reference. + // _ = s2.ToString(); // 4 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s2").WithLocation(15, 17), + // (23,17): warning CS8602: Dereference of a possibly null reference. + // _ = s1.ToString(); // 5 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s1").WithLocation(23, 17), + // (24,17): warning CS8602: Dereference of a possibly null reference. + // _ = s2.ToString(); // 6 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s2").WithLocation(24, 17), + // (28,17): warning CS8602: Dereference of a possibly null reference. + // _ = s1.ToString(); // 7 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s1").WithLocation(28, 17), + // (29,17): warning CS8602: Dereference of a possibly null reference. + // _ = s2.ToString(); // 8 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s2").WithLocation(29, 17)); + } + + [Theory] + [InlineData("object.Equals")] + [InlineData("object.ReferenceEquals")] + [WorkItem(35816, "https://github.com/dotnet/roslyn/issues/35816")] + public void StaticEqualsMethod_NonNullExpr_ConstantNull_WarnsWhenTrue(string equalsMethodName) + { + var source = $@" +#nullable enable +class C +{{ + static void M1(string s) + {{ + const string? s2 = null; + if ({equalsMethodName}(s, s2)) + {{ + _ = s.ToString(); // 1 + }} + else + {{ + _ = s.ToString(); + }} + }} + + static void M2(string s) + {{ + const string? s2 = null; + if ({equalsMethodName}(s2, s)) + {{ + _ = s.ToString(); // 2 + }} + else + {{ + _ = s.ToString(); + }} + }} +}}"; + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (10,17): warning CS8602: Dereference of a possibly null reference. + // _ = s.ToString(); // 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(10, 17), + // (23,17): warning CS8602: Dereference of a possibly null reference. + // _ = s.ToString(); // 2 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(23, 17)); + } + + [Theory] + [InlineData("object.Equals")] + [InlineData("object.ReferenceEquals")] + [WorkItem(35816, "https://github.com/dotnet/roslyn/issues/35816")] + public void StaticEqualsMethod_MaybeNullExpr_ConstantNull_NoWarningWhenFalse(string equalsMethodName) + { + var source = $@" +#nullable enable +class C +{{ + static void M1(string? s) + {{ + const string? s2 = null; + if ({equalsMethodName}(s, s2)) + {{ + _ = s.ToString(); // 1 + }} + else + {{ + _ = s.ToString(); + }} + }} + + static void M2(string? s) + {{ + const string? s2 = null; + if ({equalsMethodName}(s2, s)) + {{ + _ = s.ToString(); // 2 + }} + else + {{ + _ = s.ToString(); + }} + }} +}}"; + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (10,17): warning CS8602: Dereference of a possibly null reference. + // _ = s.ToString(); // 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(10, 17), + // (23,17): warning CS8602: Dereference of a possibly null reference. + // _ = s.ToString(); // 2 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(23, 17)); + } + + [Theory] + [InlineData("object.Equals")] + [InlineData("object.ReferenceEquals")] + [WorkItem(35816, "https://github.com/dotnet/roslyn/issues/35816")] + public void StaticEqualsMethod_NonNullExpr_MaybeNullExpr_NoWarning(string equalsMethodName) + { + var source = $@" +#nullable enable +class C +{{ + static void M1(string s) + {{ + string? s2 = null; + if ({equalsMethodName}(s, s2)) + {{ + _ = s.ToString(); + }} + else + {{ + _ = s.ToString(); + }} + }} + + static void M2(string s) + {{ + string? s2 = null; + if ({equalsMethodName}(s2, s)) + {{ + _ = s.ToString(); + }} + else + {{ + _ = s.ToString(); + }} + }} +}}"; + var comp = CreateCompilation(source); + comp.VerifyDiagnostics(); + } + + [Theory] + [InlineData("object.Equals")] + [InlineData("object.ReferenceEquals")] + [WorkItem(35816, "https://github.com/dotnet/roslyn/issues/35816")] + public void StaticEqualsMethod_MaybeNullExpr_NonNullExpr_NoWarningWhenTrue(string equalsMethodName) + { + var source = $@" +#nullable enable +class C +{{ + static void M1(string? s) + {{ + if ({equalsMethodName}(s, """")) + {{ + _ = s.ToString(); + }} + else + {{ + _ = s.ToString(); // 1 + }} + }} + + static void M2(string? s) + {{ + if ({equalsMethodName}("""", s)) + {{ + _ = s.ToString(); + }} + else + {{ + _ = s.ToString(); // 2 + }} + }} +}}"; + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (13,17): warning CS8602: Dereference of a possibly null reference. + // _ = s.ToString(); // 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(13, 17), + // (25,17): warning CS8602: Dereference of a possibly null reference. + // _ = s.ToString(); // 2 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(25, 17)); + } + + [Theory] + [InlineData("object.Equals")] + [InlineData("object.ReferenceEquals")] + [WorkItem(35816, "https://github.com/dotnet/roslyn/issues/35816")] + public void StaticEqualsMethod__NullableValueTypeExpr_ValueTypeExpr_NoWarningWhenTrue(string equalsMethodName) + { + var source = $@" +#nullable enable +class C +{{ + int? i = 42; + static void M1(C? c) + {{ + if ({equalsMethodName}(c?.i, 42)) + {{ + _ = c.i.Value.ToString(); + }} + else + {{ + _ = c.i.Value.ToString(); // 1 + }} + }} + + static void M2(C? c) + {{ + if ({equalsMethodName}(42, c?.i)) + {{ + _ = c.i.Value.ToString(); + }} + else + {{ + _ = c.i.Value.ToString(); // 2 + }} + }} +}}"; + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (14,17): warning CS8602: Dereference of a possibly null reference. + // _ = c.i.Value.ToString(); // 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "c").WithLocation(14, 17), + // (14,17): warning CS8629: Nullable value type may be null. + // _ = c.i.Value.ToString(); // 1 + Diagnostic(ErrorCode.WRN_NullableValueTypeMayBeNull, "c.i").WithLocation(14, 17), + // (26,17): warning CS8602: Dereference of a possibly null reference. + // _ = c.i.Value.ToString(); // 2 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "c").WithLocation(26, 17), + // (26,17): warning CS8629: Nullable value type may be null. + // _ = c.i.Value.ToString(); // 2 + Diagnostic(ErrorCode.WRN_NullableValueTypeMayBeNull, "c.i").WithLocation(26, 17)); + } + + [Fact, WorkItem(35816, "https://github.com/dotnet/roslyn/issues/35816")] + public void InstanceEqualsMethod_MaybeNullExpr_NonNullExpr_NoWarningWhenTrue() + { + var source = @" +#nullable enable +class C +{ + static void M1(string? s) + { + if ("""".Equals(s)) + { + _ = s.ToString(); + } + else + { + _ = s.ToString(); // 1 + } + } +}"; + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (13,17): warning CS8602: Dereference of a possibly null reference. + // _ = s.ToString(); // 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(13, 17)); + } + + [Fact, WorkItem(35816, "https://github.com/dotnet/roslyn/issues/35816")] + public void InstanceEqualsMethod_NonNullExpr_LiteralNull_NoWarningWhenTrue() + { + var source = @" +#nullable enable +class C +{ + static void M1(string s) + { + if (s.Equals(null)) + { + _ = s.ToString(); // 1 + } + else + { + _ = s.ToString(); + } + } +}"; + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (9,17): warning CS8602: Dereference of a possibly null reference. + // _ = s.ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(9, 17)); + } + + [Fact, WorkItem(35816, "https://github.com/dotnet/roslyn/issues/35816")] + public void InstanceEqualsMethod_IEquatableMissing_MaybeNullExpr_NonNullExpr_Warns() + { + var source = @" +#nullable enable +class C +{ + static void M1(string? s) + { + if ("""".Equals(s)) + { + _ = s.ToString(); // 1 + } + else + { + _ = s.ToString(); // 2 + } + } +}"; + var expected = new[] + { + // (9,17): warning CS8602: Dereference of a possibly null reference. + // _ = s.ToString(); // 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(9, 17), + // (13,17): warning CS8602: Dereference of a possibly null reference. + // _ = s.ToString(); // 2 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(13, 17) + }; + var comp = CreateCompilation(source); + comp.MakeTypeMissing(WellKnownType.System_IEquatable_T); + comp.VerifyDiagnostics(expected); + + comp = CreateCompilation(source); + comp.MakeMemberMissing(WellKnownMember.System_IEquatable_T__Equals); + comp.VerifyDiagnostics(expected); + } + + [Fact, WorkItem(35816, "https://github.com/dotnet/roslyn/issues/35816")] + public void InstanceEqualsMethod_IEquatableEqualsOverloaded_MaybeNullExpr_NonNullExpr_NoWarningWhenTrue() + { + var source = @" +#pragma warning disable CS0436 +namespace System +{ + interface IEquatable + { + bool Equals(T t, int compareFlags); + bool Equals(T t); + } +} + +#nullable enable +class C : System.IEquatable +{ + public bool Equals(C? other) => throw null!; + public bool Equals(C? other, int compareFlags) => throw null!; + + static void M1(C? c) + { + C c1 = new C(); + if (c1.Equals(c)) + { + _ = c.ToString(); + } + else + { + _ = c.ToString(); // 1 + } + } +}"; + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (27,17): warning CS8602: Dereference of a possibly null reference. + // _ = c.ToString(); // 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "c").WithLocation(27, 17)); + } + + [Fact, WorkItem(35816, "https://github.com/dotnet/roslyn/issues/35816")] + public void InstanceEqualsMethod_IEquatableEqualsBadSignature_MaybeNullExpr_NonNullExpr_Warns() + { + var source = @" +#pragma warning disable CS0436 +namespace System +{ + interface IEquatable + { + bool Equals(T t, int compareFlags); + } +} + +#nullable enable +class C : System.IEquatable +{ + public bool Equals(C? c) => throw null!; + public bool Equals(C? c, int compareFlags) => throw null!; + + static void M1(C? c) + { + C c1 = new C(); + if (c1.Equals(c)) + { + _ = c.ToString(); // 1 + } + else + { + _ = c.ToString(); // 2 + } + } +}"; + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (22,17): warning CS8602: Dereference of a possibly null reference. + // _ = c.ToString(); // 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "c").WithLocation(22, 17), + // (26,17): warning CS8602: Dereference of a possibly null reference. + // _ = c.ToString(); // 2 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "c").WithLocation(26, 17)); + } + + [Fact, WorkItem(35816, "https://github.com/dotnet/roslyn/issues/35816")] + public void IEquatableEqualsMethod_MaybeNullExpr_NonNullExpr_NoWarningWhenTrue() + { + var source = @" +using System; + +#nullable enable +class C +{ + static void M1(IEquatable equatable, string? s) + { + if (equatable.Equals(s)) + { + _ = s.ToString(); + } + else + { + _ = s.ToString(); // 1 + } + } +}"; + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (15,17): warning CS8602: Dereference of a possibly null reference. + // _ = s.ToString(); // 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(15, 17)); + } + + [Fact, WorkItem(35816, "https://github.com/dotnet/roslyn/issues/35816")] + public void IEquatableEqualsMethod_OpenType_MaybeNullExpr_NonNullExpr_NoWarningWhenTrue() + { + var source = @" +using System; + +#nullable enable +class C +{ + static void M1(IEquatable equatable, T? t) where T : class + { + if (equatable.Equals(t)) + { + _ = t.ToString(); + } + else + { + _ = t.ToString(); // 1 + } + } +}"; + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (15,17): warning CS8602: Dereference of a possibly null reference. + // _ = t.ToString(); // 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "t").WithLocation(15, 17)); + } + + [Fact, WorkItem(35816, "https://github.com/dotnet/roslyn/issues/35816")] + public void IEquatableEqualsMethod_NullableVariance_MaybeNullExpr_NonNullExpr_NoWarningWhenTrue() + { + var source = @" +using System; + +#nullable enable +class C : IEquatable +{ + public bool Equals(string? s) => throw null!; + + static void M1(C c, string? s) + { + if (c.Equals(s)) + { + _ = s.ToString(); + } + else + { + _ = s.ToString(); // 1 + } + } +}"; + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (17,17): warning CS8602: Dereference of a possibly null reference. + // _ = s.ToString(); // 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(17, 17)); + } + + [Fact, WorkItem(35816, "https://github.com/dotnet/roslyn/issues/35816")] + public void InstanceEqualsMethod_NullableVariance_MaybeNullExpr_NonNullExpr_NoWarningWhenTrue() + { + var source = @" +using System; + +#nullable enable +class C : IEquatable +{ + public bool Equals(C? other) => throw null!; + + static void M1(C? c2) + { + C c1 = new C(); + if (c1.Equals(c2)) + { + _ = c2.ToString(); + } + else + { + _ = c2.ToString(); // 1 + } + } +}"; + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (18,17): warning CS8602: Dereference of a possibly null reference. + // _ = c2.ToString(); // 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "c2").WithLocation(18, 17)); + } + + [Fact, WorkItem(35816, "https://github.com/dotnet/roslyn/issues/35816")] + public void InstanceObjectEqualsMethod_MaybeNullExpr_NonNullExpr_NoWarningWhenTrue() + { + var source = @" +#nullable enable +class C +{ + static void M1(object? o) + { + if ("""".Equals(o)) + { + _ = o.ToString(); + } + else + { + _ = o.ToString(); // 1 + } + } +}"; + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (13,17): warning CS8602: Dereference of a possibly null reference. + // _ = o.ToString(); // 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "o").WithLocation(13, 17)); + } + + [Fact, WorkItem(35816, "https://github.com/dotnet/roslyn/issues/35816")] + public void InstanceObjectEqualsMethod_MaybeNullExprReferenceConversion_NonNullExpr_NoWarningWhenTrue() + { + var source = @" +#nullable enable +class C +{ + static void M1(string? s) + { + if ("""".Equals((object?)s)) + { + _ = s.ToString(); + } + else + { + _ = s.ToString(); // 1 + } + } +}"; + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (13,17): warning CS8602: Dereference of a possibly null reference. + // _ = s.ToString(); // 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(13, 17)); + } + + [Fact, WorkItem(35816, "https://github.com/dotnet/roslyn/issues/35816")] + public void InstanceObjectEqualsMethod_ConditionalAccessExpr_NonNullExpr_NoWarningWhenTrue() + { + var source = @" +#nullable enable +class C +{ + object? F = default; + + static void M1(C? c) + { + C c1 = new C(); + if (c1.Equals(c?.F)) + { + _ = c.F.ToString(); + } + else + { + _ = c.F.ToString(); // 1, 2 + } + } +}"; + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (16,17): warning CS8602: Dereference of a possibly null reference. + // _ = c.F.ToString(); // 1, 2 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "c").WithLocation(16, 17), + // (16,17): warning CS8602: Dereference of a possibly null reference. + // _ = c.F.ToString(); // 1, 2 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "c.F").WithLocation(16, 17)); + } + + [Fact, WorkItem(35816, "https://github.com/dotnet/roslyn/issues/35816")] + public void IEqualityComparerEqualsMethod_MaybeNullExpr_NonNullExpr_NoWarningWhenTrue() + { + var source = @" +using System.Collections.Generic; + +#nullable enable +class C +{ + static void M1(IEqualityComparer comparer, string? s) + { + if (comparer.Equals("""", s)) + { + _ = s.ToString(); + } + else + { + _ = s.ToString(); // 1 + } + } +}"; + + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (15,17): warning CS8602: Dereference of a possibly null reference. + // _ = s.ToString(); // 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(15, 17)); + } + + [Fact, WorkItem(35816, "https://github.com/dotnet/roslyn/issues/35816")] + public void EqualityComparerEqualsMethod_MaybeNullExpr_NonNullExpr_NoWarningWhenTrue() + { + var source = @" +using System.Collections.Generic; + +#nullable enable +class C +{ + static void M1(EqualityComparer comparer, string? s) + { + if (comparer.Equals("""", s)) + { + _ = s.ToString(); + } + else + { + _ = s.ToString(); // 1 + } + } +}"; + + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (15,17): warning CS8602: Dereference of a possibly null reference. + // _ = s.ToString(); // 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(15, 17)); + } + + [Fact, WorkItem(35816, "https://github.com/dotnet/roslyn/issues/35816")] + public void EqualityComparerEqualsMethod_IEqualityComparerMissing_MaybeNullExpr_NonNullExpr_Warns() + { + var source = @" +using System.Collections.Generic; + +#nullable enable +class C +{ + static void M1(EqualityComparer comparer, string? s) + { + if (comparer.Equals("""", s)) + { + _ = s.ToString(); // 1 + } + else + { + _ = s.ToString(); // 2 + } + } +}"; + + var comp = CreateCompilation(source); + comp.MakeTypeMissing(WellKnownType.System_Collections_Generic_IEqualityComparer_T); + comp.VerifyDiagnostics( + // (11,17): warning CS8602: Dereference of a possibly null reference. + // _ = s.ToString(); // 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(11, 17), + // (15,17): warning CS8602: Dereference of a possibly null reference. + // _ = s.ToString(); // 2 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(15, 17)); + } + + [Fact, WorkItem(35816, "https://github.com/dotnet/roslyn/issues/35816")] + public void IEqualityComparer_SubInterfaceEqualsMethod_MaybeNullExpr_NonNullExpr_NoWarningWhenTrue() + { + var source = @" +using System.Collections.Generic; + +#nullable enable + +interface MyEqualityComparer : IEqualityComparer { } + +class C +{ + static void M1(MyEqualityComparer comparer, string? s) + { + if (comparer.Equals("""", s)) + { + _ = s.ToString(); + } + else + { + _ = s.ToString(); // 1 + } + } +}"; + + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (18,17): warning CS8602: Dereference of a possibly null reference. + // _ = s.ToString(); // 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(18, 17)); + } + + [Fact, WorkItem(35816, "https://github.com/dotnet/roslyn/issues/35816")] + public void IEqualityComparer_SubSubInterfaceEqualsMethod_MaybeNullExpr_NonNullExpr_NoWarningWhenTrue() + { + var source = @" +using System.Collections.Generic; + +#nullable enable + +interface MyEqualityComparer : IEqualityComparer { } +interface MySubEqualityComparer : MyEqualityComparer { } + +class C +{ + static void M1(MySubEqualityComparer comparer, string? s) + { + if (comparer.Equals("""", s)) + { + _ = s.ToString(); + } + else + { + _ = s.ToString(); // 1 + } + } +}"; + + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (19,17): warning CS8602: Dereference of a possibly null reference. + // _ = s.ToString(); // 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(19, 17)); + } + + [Fact, WorkItem(35816, "https://github.com/dotnet/roslyn/issues/35816")] + public void OverrideEqualsMethod_MaybeNullExpr_NonNullExpr_NoWarningWhenTrue() + { + var source = @" +#nullable enable +class C +{ + public override bool Equals(object? other) => throw null!; + public override int GetHashCode() => throw null!; + + static void M1(C? c1) + { + C c2 = new C(); + if (c2.Equals(c1)) + { + _ = c1.ToString(); + } + else + { + _ = c1.ToString(); // 1 + } + } +}"; + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (17,17): warning CS8602: Dereference of a possibly null reference. + // _ = c1.ToString(); // 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "c1").WithLocation(17, 17)); + } + + [Fact, WorkItem(35816, "https://github.com/dotnet/roslyn/issues/35816")] + public void NotImplementingEqualsMethod_MaybeNullExpr_NonNullExpr_Warns() + { + var source = @" +#nullable enable +class C +{ + public bool Equals(C? other) => throw null!; + + static void M1(C? c1) + { + C c2 = new C(); + if (c2.Equals(c1)) + { + _ = c1.ToString(); // 1 + } + else + { + _ = c1.ToString(); // 2 + } + } +}"; + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (12,17): warning CS8602: Dereference of a possibly null reference. + // _ = c1.ToString(); // 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "c1").WithLocation(12, 17), + // (16,17): warning CS8602: Dereference of a possibly null reference. + // _ = c1.ToString(); // 2 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "c1").WithLocation(16, 17)); + } + [Fact] [WorkItem(36131, "https://github.com/dotnet/roslyn/issues/36131")] public void ShouldRunNullableWalker_GlobalDirective() @@ -109757,7 +110726,6 @@ void M(object? o1) Assert.True(comp.ShouldRunNullableWalker); } - [Theory] [InlineData("")] [InlineData("annotations")] @@ -109944,7 +110912,6 @@ void M(object? o1) // (8,21): warning CS8600: Converting null literal or possible null value to non-nullable type. // object o2 = o1; Diagnostic(ErrorCode.WRN_ConvertingNullableToNonNullable, "o1").WithLocation(8, 21)); - Assert.True(comp.ShouldRunNullableWalker); } } diff --git a/src/Compilers/Core/Portable/SpecialMember.cs b/src/Compilers/Core/Portable/SpecialMember.cs index 94d8f6119b93b..d182c41200c81 100644 --- a/src/Compilers/Core/Portable/SpecialMember.cs +++ b/src/Compilers/Core/Portable/SpecialMember.cs @@ -122,6 +122,7 @@ internal enum SpecialMember System_Object__GetHashCode, System_Object__Equals, + System_Object__EqualsObjectObject, System_Object__ToString, System_Object__ReferenceEquals, diff --git a/src/Compilers/Core/Portable/SpecialMembers.cs b/src/Compilers/Core/Portable/SpecialMembers.cs index bf31714bd22c8..7c69bce1125cb 100644 --- a/src/Compilers/Core/Portable/SpecialMembers.cs +++ b/src/Compilers/Core/Portable/SpecialMembers.cs @@ -834,6 +834,15 @@ static SpecialMembers() (byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Boolean, (byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Object, + // System_Object__EqualsObjectObject + (byte)(MemberFlags.Method | MemberFlags.Static), // Flags + (byte)SpecialType.System_Object, // DeclaringTypeId + 0, // Arity + 2, // Method Signature + (byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Boolean, + (byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Object, + (byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Object, + // System_Object__ToString (byte)(MemberFlags.Method | MemberFlags.Virtual), // Flags (byte)SpecialType.System_Object, // DeclaringTypeId @@ -1099,6 +1108,7 @@ static SpecialMembers() "GetUpperBound", // System_Array__GetUpperBound "GetHashCode", // System_Object__GetHashCode "Equals", // System_Object__Equals + "Equals", // System_Object__EqualsObjectObject "ToString", // System_Object__ToString "ReferenceEquals", // System_Object__ReferenceEquals "op_Explicit", // System_IntPtr__op_Explicit_ToPointer diff --git a/src/Compilers/Core/Portable/WellKnownMember.cs b/src/Compilers/Core/Portable/WellKnownMember.cs index 7e067350bd570..4019248afdbf0 100644 --- a/src/Compilers/Core/Portable/WellKnownMember.cs +++ b/src/Compilers/Core/Portable/WellKnownMember.cs @@ -68,6 +68,8 @@ internal enum WellKnownMember System_IEquatable_T__Equals, + System_Collections_Generic_IEqualityComparer_T__Equals, + System_Collections_Generic_EqualityComparer_T__Equals, System_Collections_Generic_EqualityComparer_T__GetHashCode, System_Collections_Generic_EqualityComparer_T__get_Default, diff --git a/src/Compilers/Core/Portable/WellKnownMembers.cs b/src/Compilers/Core/Portable/WellKnownMembers.cs index 10552a0313da7..9901beb80a1e3 100644 --- a/src/Compilers/Core/Portable/WellKnownMembers.cs +++ b/src/Compilers/Core/Portable/WellKnownMembers.cs @@ -448,6 +448,15 @@ static WellKnownMembers() (byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Boolean, // Return Type (byte)SignatureTypeCode.GenericTypeParameter, 0, + // System_Collections_Generic_IEqualityComparer_T__Equals + (byte)(MemberFlags.Method | MemberFlags.Virtual), // Flags + (byte)WellKnownType.ExtSentinel, (byte)(WellKnownType.System_Collections_Generic_IEqualityComparer_T - WellKnownType.ExtSentinel), // DeclaringTypeId + 0, // Arity + 2, // Method Signature + (byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Boolean, // Return Type + (byte)SignatureTypeCode.GenericTypeParameter, 0, + (byte)SignatureTypeCode.GenericTypeParameter, 0, + // System_Collections_Generic_EqualityComparer_T__Equals (byte)(MemberFlags.Method | MemberFlags.Virtual), // Flags (byte)WellKnownType.System_Collections_Generic_EqualityComparer_T, // DeclaringTypeId @@ -3494,6 +3503,7 @@ static WellKnownMembers() "GetFieldFromHandle", // System_Reflection_FieldInfo__GetFieldFromHandle2 "Value", // System_Reflection_Missing__Value "Equals", // System_IEquatable_T__Equals + "Equals", // System_Collections_Generic_IEqualityComparer_T__Equals "Equals", // System_Collections_Generic_EqualityComparer_T__Equals "GetHashCode", // System_Collections_Generic_EqualityComparer_T__GetHashCode "get_Default", // System_Collections_Generic_EqualityComparer_T__get_Default diff --git a/src/Compilers/Core/Portable/WellKnownTypes.cs b/src/Compilers/Core/Portable/WellKnownTypes.cs index b7067725c333e..30990f2dc688d 100644 --- a/src/Compilers/Core/Portable/WellKnownTypes.cs +++ b/src/Compilers/Core/Portable/WellKnownTypes.cs @@ -303,6 +303,7 @@ internal enum WellKnownType System_InvalidOperationException, System_Runtime_CompilerServices_SwitchExpressionException, + System_Collections_Generic_IEqualityComparer_T, NextAvailable, @@ -602,7 +603,8 @@ internal static class WellKnownTypes "System.Threading.CancellationTokenSource", "System.InvalidOperationException", - "System.Runtime.CompilerServices.SwitchExpressionException" + "System.Runtime.CompilerServices.SwitchExpressionException", + "System.Collections.Generic.IEqualityComparer`1", }; private readonly static Dictionary s_nameToTypeIdMap = new Dictionary((int)Count);