Skip to content

Commit 5959ffe

Browse files
committed
MA0169 - Detect equality operators that should be replaced with Equals method
1 parent eaef94d commit 5959ffe

File tree

9 files changed

+275
-1
lines changed

9 files changed

+275
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ If you are already using other analyzers, you can check [which rules are duplica
184184
|[MA0166](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0166.md)|Usage|Forward the TimeProvider to methods that take one|ℹ️|✔️|✔️|
185185
|[MA0167](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0167.md)|Usage|Use an overload with a TimeProvider argument|ℹ️|||
186186
|[MA0168](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0168.md)|Performance|Use readonly struct for in or ref readonly parameter|ℹ️|||
187+
|[MA0169](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0169.md)|Design|Use Equals method instead of operator|⚠️|✔️||
187188

188189
<!-- rules -->
189190

docs/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@
168168
|[MA0166](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0166.md)|Usage|Forward the TimeProvider to methods that take one|<span title='Info'>ℹ️</span>|✔️|✔️|
169169
|[MA0167](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0167.md)|Usage|Use an overload with a TimeProvider argument|<span title='Info'>ℹ️</span>|||
170170
|[MA0168](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0168.md)|Performance|Use readonly struct for in or ref readonly parameter|<span title='Info'>ℹ️</span>|||
171+
|[MA0169](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0169.md)|Design|Use Equals method instead of operator|<span title='Warning'>⚠️</span>|✔️||
171172

172173
|Id|Suppressed rule|Justification|
173174
|--|---------------|-------------|
@@ -680,6 +681,9 @@ dotnet_diagnostic.MA0167.severity = none
680681
681682
# MA0168: Use readonly struct for in or ref readonly parameter
682683
dotnet_diagnostic.MA0168.severity = none
684+
685+
# MA0169: Use Equals method instead of operator
686+
dotnet_diagnostic.MA0169.severity = warning
683687
```
684688

685689
# .editorconfig - all rules disabled
@@ -1185,4 +1189,7 @@ dotnet_diagnostic.MA0167.severity = none
11851189
11861190
# MA0168: Use readonly struct for in or ref readonly parameter
11871191
dotnet_diagnostic.MA0168.severity = none
1192+
1193+
# MA0169: Use Equals method instead of operator
1194+
dotnet_diagnostic.MA0169.severity = none
11881195
```

docs/Rules/MA0169.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# MA0169 - Use Equals method instead of operator
2+
3+
Using `==` or `!=` operator on a type that overrides `Equals` method, but not the operators, is not recommended.
4+
5+
````c#
6+
Sample a = default;
7+
Sample b = default;
8+
9+
_ = a == b; // ok as Equals is not overrided
10+
11+
class Sample { }
12+
````
13+
14+
````c#
15+
Sample a = default;
16+
Sample b = default;
17+
18+
_ = a == b; // ok the equality operator are defined
19+
20+
21+
class Sample
22+
{
23+
public static bool operator ==(Sample a, Sample b) => true;
24+
public static bool operator !=(Sample a, Sample b) => false;
25+
public override bool Equals(object obj) => true;
26+
public override int GetHashCode() => 0;
27+
}
28+
````
29+
30+
````c#
31+
Sample a = default;
32+
Sample b = default;
33+
34+
_ = a.Equals(b); // ok
35+
_ = object.Reference`Equals(a, b); // ok
36+
_ = a == b; // non-compliant
37+
38+
class Sample
39+
{
40+
public override bool Equals(object obj) => true;
41+
public override int GetHashCode() => 0;
42+
}
43+
````
44+

src/Meziantou.Analyzer.Pack/configuration/default.editorconfig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,3 +502,6 @@ dotnet_diagnostic.MA0167.severity = none
502502

503503
# MA0168: Use readonly struct for in or ref readonly parameter
504504
dotnet_diagnostic.MA0168.severity = none
505+
506+
# MA0169: Use Equals method instead of operator
507+
dotnet_diagnostic.MA0169.severity = warning

src/Meziantou.Analyzer.Pack/configuration/none.editorconfig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,3 +502,6 @@ dotnet_diagnostic.MA0167.severity = none
502502

503503
# MA0168: Use readonly struct for in or ref readonly parameter
504504
dotnet_diagnostic.MA0168.severity = none
505+
506+
# MA0169: Use Equals method instead of operator
507+
dotnet_diagnostic.MA0169.severity = none

src/Meziantou.Analyzer/Internals/MethodSymbolExtensions.cs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System.Linq;
1+
using System.Linq;
22
using Microsoft.CodeAnalysis;
33

44
namespace Meziantou.Analyzer.Internals;
@@ -56,6 +56,41 @@ private static bool IsInterfaceImplementation(this ISymbol symbol)
5656
.FirstOrDefault(interfaceMember => SymbolEqualityComparer.Default.Equals(symbol, symbol.ContainingType.FindImplementationForInterfaceMember(interfaceMember)));
5757
}
5858

59+
public static bool IsOrOverrideMethod(this IMethodSymbol? symbol, IMethodSymbol? baseMethod)
60+
{
61+
if (symbol is null || baseMethod is null)
62+
return false;
63+
64+
if (symbol.IsEqualTo(baseMethod))
65+
return true;
66+
67+
while (symbol is not null)
68+
{
69+
if (symbol.IsEqualTo(baseMethod))
70+
return true;
71+
72+
symbol = symbol.OverriddenMethod!;
73+
}
74+
75+
return false;
76+
}
77+
78+
public static bool OverrideMethod(this IMethodSymbol? symbol, IMethodSymbol? baseMethod)
79+
{
80+
if (symbol is null || baseMethod is null)
81+
return false;
82+
83+
while (symbol.OverriddenMethod is not null)
84+
{
85+
if (symbol.OverriddenMethod.IsEqualTo(baseMethod))
86+
return true;
87+
88+
symbol = symbol.OverriddenMethod!;
89+
}
90+
91+
return false;
92+
}
93+
5994
public static bool IsUnitTestMethod(this IMethodSymbol methodSymbol)
6095
{
6196
var attributes = methodSymbol.GetAttributes();

src/Meziantou.Analyzer/RuleIdentifiers.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ internal static class RuleIdentifiers
171171
public const string UseAnOverloadThatHasTimeProviderWhenAvailable = "MA0166";
172172
public const string UseAnOverloadThatHasTimeProvider = "MA0167";
173173
public const string UseReadOnlyStructForRefReadOnlyParameters = "MA0168";
174+
public const string UseEqualsMethodInsteadOfOperator = "MA0169";
174175

175176
public static string GetHelpUri(string identifier)
176177
{
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
using System.Collections.Immutable;
2+
using System.Linq;
3+
using Meziantou.Analyzer.Internals;
4+
using Microsoft.CodeAnalysis;
5+
using Microsoft.CodeAnalysis.Diagnostics;
6+
using Microsoft.CodeAnalysis.Operations;
7+
8+
namespace Meziantou.Analyzer.Rules;
9+
10+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
11+
public sealed class UseEqualsMethodInsteadOfOperatorAnalyzer : DiagnosticAnalyzer
12+
{
13+
private static readonly DiagnosticDescriptor Rule = new(
14+
RuleIdentifiers.UseEqualsMethodInsteadOfOperator,
15+
title: "Use Equals method instead of operator",
16+
messageFormat: "Use Equals method instead of == or != operator",
17+
RuleCategories.Design,
18+
DiagnosticSeverity.Warning,
19+
isEnabledByDefault: true,
20+
description: "",
21+
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.UseEqualsMethodInsteadOfOperator));
22+
23+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
24+
25+
public override void Initialize(AnalysisContext context)
26+
{
27+
context.EnableConcurrentExecution();
28+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
29+
30+
context.RegisterCompilationStartAction(context =>
31+
{
32+
if (context.Compilation.GetSpecialType(SpecialType.System_Object).GetMembers("Equals").FirstOrDefault() is not IMethodSymbol objectEqualsSymbol)
33+
return;
34+
35+
context.RegisterOperationAction(context => AnalyzerBinaryOperation(context, objectEqualsSymbol), OperationKind.Binary);
36+
});
37+
}
38+
39+
private static void AnalyzerBinaryOperation(OperationAnalysisContext context, IMethodSymbol objectEqualsSymbol)
40+
{
41+
var operation = (IBinaryOperation)context.Operation;
42+
if (operation is { OperatorKind: BinaryOperatorKind.Equals or BinaryOperatorKind.NotEquals, OperatorMethod: null })
43+
{
44+
if (IsNull(operation.LeftOperand) || IsNull(operation.RightOperand))
45+
return;
46+
47+
var leftType = operation.LeftOperand.UnwrapImplicitConversionOperations().Type;
48+
if (operation.IsLifted)
49+
{
50+
leftType = leftType.GetUnderlyingNullableTypeOrSelf();
51+
}
52+
53+
if (leftType is null)
54+
return;
55+
56+
if (leftType.IsValueType)
57+
return;
58+
59+
switch (leftType.SpecialType)
60+
{
61+
case SpecialType.System_Boolean:
62+
case SpecialType.System_Char:
63+
case SpecialType.System_DateTime:
64+
case SpecialType.System_SByte:
65+
case SpecialType.System_Int16:
66+
case SpecialType.System_Int32:
67+
case SpecialType.System_Int64:
68+
case SpecialType.System_IntPtr:
69+
case SpecialType.System_Byte:
70+
case SpecialType.System_UInt16:
71+
case SpecialType.System_UInt32:
72+
case SpecialType.System_UInt64:
73+
case SpecialType.System_UIntPtr:
74+
case SpecialType.System_Single:
75+
case SpecialType.System_Double:
76+
case SpecialType.System_Decimal:
77+
case SpecialType.System_Enum:
78+
case SpecialType.System_Object:
79+
case SpecialType.System_String:
80+
return;
81+
}
82+
83+
// Check if the type have an Equals method
84+
var overrideEqualsSymbol = leftType.GetMembers("Equals").OfType<IMethodSymbol>().FirstOrDefault(m => m.IsOrOverrideMethod(objectEqualsSymbol));
85+
if (overrideEqualsSymbol is not null)
86+
{
87+
context.ReportDiagnostic(Rule, operation);
88+
}
89+
}
90+
}
91+
92+
public static bool IsNull(IOperation operation)
93+
=> operation.UnwrapConversionOperations() is ILiteralOperation { ConstantValue: { HasValue: true, Value: null } };
94+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
using System.Threading.Tasks;
2+
using Meziantou.Analyzer.Rules;
3+
using TestHelper;
4+
using Xunit;
5+
6+
namespace Meziantou.Analyzer.Test.Rules;
7+
public class UseEqualsMethodInsteadOfOperatorAnalyzerTests
8+
{
9+
private static ProjectBuilder CreateProjectBuilder()
10+
{
11+
return new ProjectBuilder()
12+
.WithTargetFramework(Helpers.TargetFramework.Net9_0)
13+
.WithOutputKind(Microsoft.CodeAnalysis.OutputKind.ConsoleApplication)
14+
.WithAnalyzer<UseEqualsMethodInsteadOfOperatorAnalyzer>();
15+
}
16+
17+
[Theory]
18+
[InlineData("System.Net.IPAddress")]
19+
public async Task Report_EqualsOperator(string type)
20+
{
21+
await CreateProjectBuilder()
22+
.WithSourceCode($$"""
23+
{{type}} a = null;
24+
{{type}} b = null;
25+
_ = [|a == b|];
26+
""")
27+
.ValidateAsync();
28+
}
29+
30+
[Theory]
31+
[InlineData("char")]
32+
[InlineData("string")]
33+
[InlineData("sbyte")]
34+
[InlineData("byte")]
35+
[InlineData("short")]
36+
[InlineData("ushort")]
37+
[InlineData("int")]
38+
[InlineData("uint")]
39+
[InlineData("long")]
40+
[InlineData("ulong")]
41+
[InlineData("System.Int128")]
42+
[InlineData("System.UInt128")]
43+
[InlineData("System.Half")]
44+
[InlineData("float")]
45+
[InlineData("double")]
46+
[InlineData("decimal")]
47+
[InlineData("System.DayOfWeek")]
48+
public async Task NoReport_EqualsOperator(string type)
49+
{
50+
await CreateProjectBuilder()
51+
.WithSourceCode($$"""
52+
{{type}} a = default;
53+
{{type}} b = default;
54+
_ = a == b;
55+
""")
56+
.ValidateAsync();
57+
}
58+
59+
[Fact]
60+
public async Task ClassWithoutEqualsMethod()
61+
{
62+
await CreateProjectBuilder()
63+
.WithSourceCode($$"""
64+
Sample a = default;
65+
Sample b = default;
66+
_ = a == b;
67+
68+
class Sample {}
69+
""")
70+
.ValidateAsync();
71+
}
72+
73+
[Fact]
74+
public async Task RecordWithoutEqualsMethod()
75+
{
76+
await CreateProjectBuilder()
77+
.WithSourceCode($$"""
78+
Sample a = default;
79+
Sample b = default;
80+
_ = a == b; // Operator is implemented by the record
81+
82+
record Sample {}
83+
""")
84+
.ValidateAsync();
85+
}
86+
}

0 commit comments

Comments
 (0)