Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ If you are already using other analyzers, you can check [which rules are duplica
|[MA0166](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0166.md)|Usage|Forward the TimeProvider to methods that take one|ℹ️|✔️|✔️|
|[MA0167](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0167.md)|Usage|Use an overload with a TimeProvider argument|ℹ️|❌|❌|
|[MA0168](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0168.md)|Performance|Use readonly struct for in or ref readonly parameter|ℹ️|❌|❌|
|[MA0169](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0169.md)|Design|Use Equals method instead of operator|⚠️|✔️|❌|

<!-- rules -->

Expand Down
7 changes: 7 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@
|[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>|✔️|✔️|
|[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>|❌|❌|
|[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>|❌|❌|
|[MA0169](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0169.md)|Design|Use Equals method instead of operator|<span title='Warning'>⚠️</span>|✔️|❌|

|Id|Suppressed rule|Justification|
|--|---------------|-------------|
Expand Down Expand Up @@ -680,6 +681,9 @@ dotnet_diagnostic.MA0167.severity = none

# MA0168: Use readonly struct for in or ref readonly parameter
dotnet_diagnostic.MA0168.severity = none

# MA0169: Use Equals method instead of operator
dotnet_diagnostic.MA0169.severity = warning
```

# .editorconfig - all rules disabled
Expand Down Expand Up @@ -1185,4 +1189,7 @@ dotnet_diagnostic.MA0167.severity = none

# MA0168: Use readonly struct for in or ref readonly parameter
dotnet_diagnostic.MA0168.severity = none

# MA0169: Use Equals method instead of operator
dotnet_diagnostic.MA0169.severity = none
```
44 changes: 44 additions & 0 deletions docs/Rules/MA0169.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# MA0169 - Use Equals method instead of operator

Using `==` or `!=` operator on a type that overrides `Equals` method, but not the operators, is not recommended.

````c#
Sample a = default;
Sample b = default;

_ = a == b; // ok as Equals is not overridden

class Sample { }
````

````c#
Sample a = default;
Sample b = default;

_ = a == b; // ok the equality operator are defined


class Sample
{
public static bool operator ==(Sample a, Sample b) => true;
public static bool operator !=(Sample a, Sample b) => false;
public override bool Equals(object obj) => true;
public override int GetHashCode() => 0;
}
````

````c#
Sample a = default;
Sample b = default;

_ = a.Equals(b); // ok
_ = object.Reference`Equals(a, b); // ok
_ = a == b; // non-compliant

class Sample
{
public override bool Equals(object obj) => true;
public override int GetHashCode() => 0;
}
````

Original file line number Diff line number Diff line change
Expand Up @@ -502,3 +502,6 @@ dotnet_diagnostic.MA0167.severity = none

# MA0168: Use readonly struct for in or ref readonly parameter
dotnet_diagnostic.MA0168.severity = none

# MA0169: Use Equals method instead of operator
dotnet_diagnostic.MA0169.severity = warning
3 changes: 3 additions & 0 deletions src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -502,3 +502,6 @@ dotnet_diagnostic.MA0167.severity = none

# MA0168: Use readonly struct for in or ref readonly parameter
dotnet_diagnostic.MA0168.severity = none

# MA0169: Use Equals method instead of operator
dotnet_diagnostic.MA0169.severity = none
37 changes: 36 additions & 1 deletion src/Meziantou.Analyzer/Internals/MethodSymbolExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Linq;
using System.Linq;
using Microsoft.CodeAnalysis;

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

public static bool IsOrOverrideMethod(this IMethodSymbol? symbol, IMethodSymbol? baseMethod)
{
if (symbol is null || baseMethod is null)
return false;

if (symbol.IsEqualTo(baseMethod))
return true;

while (symbol is not null)
{
if (symbol.IsEqualTo(baseMethod))
return true;

symbol = symbol.OverriddenMethod!;
}

return false;
}

public static bool OverrideMethod(this IMethodSymbol? symbol, IMethodSymbol? baseMethod)
{
if (symbol is null || baseMethod is null)
return false;

while (symbol.OverriddenMethod is not null)
{
if (symbol.OverriddenMethod.IsEqualTo(baseMethod))
return true;

symbol = symbol.OverriddenMethod!;
}

return false;
}

public static bool IsUnitTestMethod(this IMethodSymbol methodSymbol)
{
var attributes = methodSymbol.GetAttributes();
Expand Down
1 change: 1 addition & 0 deletions src/Meziantou.Analyzer/RuleIdentifiers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ internal static class RuleIdentifiers
public const string UseAnOverloadThatHasTimeProviderWhenAvailable = "MA0166";
public const string UseAnOverloadThatHasTimeProvider = "MA0167";
public const string UseReadOnlyStructForRefReadOnlyParameters = "MA0168";
public const string UseEqualsMethodInsteadOfOperator = "MA0169";

public static string GetHelpUri(string identifier)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System.Collections.Immutable;
using System.Linq;
using Meziantou.Analyzer.Internals;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Meziantou.Analyzer.Rules;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class UseEqualsMethodInsteadOfOperatorAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor Rule = new(
RuleIdentifiers.UseEqualsMethodInsteadOfOperator,
title: "Use Equals method instead of operator",
messageFormat: "Use Equals method instead of == or != operator",
RuleCategories.Design,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "",
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.UseEqualsMethodInsteadOfOperator));

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

context.RegisterCompilationStartAction(context =>
{
if (context.Compilation.GetSpecialType(SpecialType.System_Object).GetMembers("Equals").FirstOrDefault() is not IMethodSymbol objectEqualsSymbol)
return;

context.RegisterOperationAction(context => AnalyzerBinaryOperation(context, objectEqualsSymbol), OperationKind.Binary);
});
}

private static void AnalyzerBinaryOperation(OperationAnalysisContext context, IMethodSymbol objectEqualsSymbol)
{
var operation = (IBinaryOperation)context.Operation;
if (operation is { OperatorKind: BinaryOperatorKind.Equals or BinaryOperatorKind.NotEquals, OperatorMethod: null })
{
if (IsNull(operation.LeftOperand) || IsNull(operation.RightOperand))
return;

var leftType = operation.LeftOperand.UnwrapImplicitConversionOperations().Type;
if (operation.IsLifted)
{
leftType = leftType.GetUnderlyingNullableTypeOrSelf();
}

if (leftType is null)
return;

if (leftType.IsValueType)
return;

switch (leftType.SpecialType)
{
case SpecialType.System_Boolean:
case SpecialType.System_Char:
case SpecialType.System_DateTime:
case SpecialType.System_SByte:
case SpecialType.System_Int16:
case SpecialType.System_Int32:
case SpecialType.System_Int64:
case SpecialType.System_IntPtr:
case SpecialType.System_Byte:
case SpecialType.System_UInt16:
case SpecialType.System_UInt32:
case SpecialType.System_UInt64:
case SpecialType.System_UIntPtr:
case SpecialType.System_Single:
case SpecialType.System_Double:
case SpecialType.System_Decimal:
case SpecialType.System_Enum:
case SpecialType.System_Object:
case SpecialType.System_String:
return;
}

// Check if the type have an Equals method
var overrideEqualsSymbol = leftType.GetMembers("Equals").OfType<IMethodSymbol>().FirstOrDefault(m => m.IsOrOverrideMethod(objectEqualsSymbol));
if (overrideEqualsSymbol is not null)
{
context.ReportDiagnostic(Rule, operation);
}
}
}

public static bool IsNull(IOperation operation)
=> operation.UnwrapConversionOperations() is ILiteralOperation { ConstantValue: { HasValue: true, Value: null } };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System.Threading.Tasks;
using Meziantou.Analyzer.Rules;
using TestHelper;
using Xunit;

namespace Meziantou.Analyzer.Test.Rules;
public class UseEqualsMethodInsteadOfOperatorAnalyzerTests
{
private static ProjectBuilder CreateProjectBuilder()
{
return new ProjectBuilder()
.WithTargetFramework(Helpers.TargetFramework.Net9_0)
.WithOutputKind(Microsoft.CodeAnalysis.OutputKind.ConsoleApplication)
.WithAnalyzer<UseEqualsMethodInsteadOfOperatorAnalyzer>();
}

[Theory]
[InlineData("System.Net.IPAddress")]
public async Task Report_EqualsOperator(string type)
{
await CreateProjectBuilder()
.WithSourceCode($$"""
{{type}} a = null;
{{type}} b = null;
_ = [|a == b|];
""")
.ValidateAsync();
}

[Theory]
[InlineData("char")]
[InlineData("string")]
[InlineData("sbyte")]
[InlineData("byte")]
[InlineData("short")]
[InlineData("ushort")]
[InlineData("int")]
[InlineData("uint")]
[InlineData("long")]
[InlineData("ulong")]
[InlineData("System.Int128")]
[InlineData("System.UInt128")]
[InlineData("System.Half")]
[InlineData("float")]
[InlineData("double")]
[InlineData("decimal")]
[InlineData("System.DayOfWeek")]
public async Task NoReport_EqualsOperator(string type)
{
await CreateProjectBuilder()
.WithSourceCode($$"""
{{type}} a = default;
{{type}} b = default;
_ = a == b;
""")
.ValidateAsync();
}

[Fact]
public async Task ClassWithoutEqualsMethod()
{
await CreateProjectBuilder()
.WithSourceCode($$"""
Sample a = default;
Sample b = default;
_ = a == b;

class Sample {}
""")
.ValidateAsync();
}

[Fact]
public async Task RecordWithoutEqualsMethod()
{
await CreateProjectBuilder()
.WithSourceCode($$"""
Sample a = default;
Sample b = default;
_ = a == b; // Operator is implemented by the record

record Sample {}
""")
.ValidateAsync();
}
}