Skip to content

Commit a5b7251

Browse files
authored
New rules: MA0141 and MA0142 to convert null check to pattern matching (#647)
1 parent 14a7b30 commit a5b7251

9 files changed

Lines changed: 254 additions & 1 deletion

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ If you are already using other analyzers, you can check [which rules are duplica
156156
|[MA0138](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0138.md)|Design|Do not use 'Async' suffix when a method does not return an awaitable type|⚠️|||
157157
|[MA0139](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0139.md)|Design|Log Parameter type is not valid|⚠️|✔️||
158158
|[MA0140](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0140.md)|Design|Both if and else branch have identical code|⚠️|✔️||
159+
|[MA0141](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0141.md)|Usage|Use pattern matching instead of inequality operators|ℹ️||✔️|
160+
|[MA0142](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0142.md)|Usage|Use pattern matching instead of equality operators|ℹ️||✔️|
159161

160162
<!-- rules -->
161163

docs/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@
140140
|[MA0138](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0138.md)|Design|Do not use 'Async' suffix when a method does not return an awaitable type|<span title='Warning'>⚠️</span>|||
141141
|[MA0139](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0139.md)|Design|Log Parameter type is not valid|<span title='Warning'>⚠️</span>|✔️||
142142
|[MA0140](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0140.md)|Design|Both if and else branch have identical code|<span title='Warning'>⚠️</span>|✔️||
143+
|[MA0141](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0141.md)|Usage|Use pattern matching instead of inequality operators|<span title='Info'>ℹ️</span>||✔️|
144+
|[MA0142](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0142.md)|Usage|Use pattern matching instead of equality operators|<span title='Info'>ℹ️</span>||✔️|
143145

144146
|Id|Suppressed rule|Justification|
145147
|--|---------------|-------------|
@@ -566,6 +568,12 @@ dotnet_diagnostic.MA0139.severity = warning
566568
567569
# MA0140: Both if and else branch have identical code
568570
dotnet_diagnostic.MA0140.severity = warning
571+
572+
# MA0141: Use pattern matching instead of inequality operators
573+
dotnet_diagnostic.MA0141.severity = none
574+
575+
# MA0142: Use pattern matching instead of equality operators
576+
dotnet_diagnostic.MA0142.severity = none
569577
```
570578

571579
# .editorconfig - all rules disabled
@@ -987,4 +995,10 @@ dotnet_diagnostic.MA0139.severity = none
987995
988996
# MA0140: Both if and else branch have identical code
989997
dotnet_diagnostic.MA0140.severity = none
998+
999+
# MA0141: Use pattern matching instead of inequality operators
1000+
dotnet_diagnostic.MA0141.severity = none
1001+
1002+
# MA0142: Use pattern matching instead of equality operators
1003+
dotnet_diagnostic.MA0142.severity = none
9901004
```

docs/Rules/MA0141.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# MA0141 - Use pattern matching instead of inequality operators
2+
3+
````c#
4+
value != null; // not compliant
5+
6+
value is not null; // ok
7+
````

docs/Rules/MA0142.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# MA0142 - Use pattern matching instead of equality operators
2+
3+
````c#
4+
value == null; // not compliant
5+
6+
value is null; // ok
7+
````

global.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"sdk": {
33
"version": "7.0.400",
4-
"rollForward": "feature"
4+
"rollForward": "latestMajor"
55
}
66
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
using System.Collections.Immutable;
2+
using System.Composition;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CodeActions;
7+
using Microsoft.CodeAnalysis.CodeFixes;
8+
using Microsoft.CodeAnalysis.CSharp;
9+
using Microsoft.CodeAnalysis.CSharp.Syntax;
10+
using Microsoft.CodeAnalysis.Editing;
11+
using Microsoft.CodeAnalysis.Operations;
12+
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
13+
14+
namespace Meziantou.Analyzer.Rules;
15+
16+
[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
17+
public sealed class UsePatternMatchingForNullCheckFixer : CodeFixProvider
18+
{
19+
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.UsePatternMatchingForNullCheck, RuleIdentifiers.UsePatternMatchingForNullEquality);
20+
21+
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
22+
23+
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
24+
{
25+
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
26+
var nodeToFix = root?.FindNode(context.Span, getInnermostNodeForTie: true);
27+
if (nodeToFix is not BinaryExpressionSyntax invocation)
28+
return;
29+
30+
context.RegisterCodeFix(
31+
CodeAction.Create(
32+
"Use pattern matching",
33+
ct => Update(context.Document, invocation, ct),
34+
equivalenceKey: "Use pattern matching"),
35+
context.Diagnostics);
36+
}
37+
38+
private static async Task<Document> Update(Document document, BinaryExpressionSyntax node, CancellationToken cancellationToken)
39+
{
40+
var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);
41+
if (editor.SemanticModel.GetOperation(node, cancellationToken) is not IBinaryOperation operation)
42+
return document;
43+
44+
var valueSyntax = IsNull(operation.LeftOperand) ? operation.RightOperand.Syntax : operation.LeftOperand.Syntax;
45+
if (valueSyntax is not ExpressionSyntax expression)
46+
return document;
47+
48+
PatternSyntax constantExpression = ConstantPattern(LiteralExpression(SyntaxKind.NullLiteralExpression));
49+
if (operation.OperatorKind is BinaryOperatorKind.NotEquals)
50+
{
51+
constantExpression = UnaryPattern(constantExpression);
52+
}
53+
54+
var newSyntax = IsPatternExpression(expression, constantExpression);
55+
editor.ReplaceNode(node, newSyntax);
56+
return editor.GetChangedDocument();
57+
}
58+
59+
private static bool IsNull(IOperation operation)
60+
=> operation.UnwrapConversionOperations() is ILiteralOperation { ConstantValue: { HasValue: true, Value: null } };
61+
}

src/Meziantou.Analyzer/RuleIdentifiers.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ internal static class RuleIdentifiers
143143
public const string MethodsNotReturningAnAwaitableTypeMustNotHaveTheAsyncSuffix = "MA0138";
144144
public const string LoggerParameterType_Serilog = "MA0139";
145145
public const string IfElseBranchesAreIdentical = "MA0140";
146+
public const string UsePatternMatchingForNullCheck = "MA0141";
147+
public const string UsePatternMatchingForNullEquality = "MA0142";
146148

147149
public static string GetHelpUri(string identifier)
148150
{
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using System.Collections.Immutable;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.Diagnostics;
4+
using Microsoft.CodeAnalysis.Operations;
5+
6+
namespace Meziantou.Analyzer.Rules;
7+
8+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
9+
public sealed class UsePatternMatchingForNullCheckAnalyzer : DiagnosticAnalyzer
10+
{
11+
private static readonly DiagnosticDescriptor s_ruleEqual = new(
12+
RuleIdentifiers.UsePatternMatchingForNullEquality,
13+
title: "Use pattern matching instead of equality operators",
14+
messageFormat: "Use pattern matching instead of equality operators",
15+
RuleCategories.Usage,
16+
DiagnosticSeverity.Info,
17+
isEnabledByDefault: false,
18+
description: "",
19+
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.UsePatternMatchingForNullEquality));
20+
21+
private static readonly DiagnosticDescriptor s_ruleNotEqual = new(
22+
RuleIdentifiers.UsePatternMatchingForNullCheck,
23+
title: "Use pattern matching instead of inequality operators",
24+
messageFormat: "Use pattern matching instead of inequality operators",
25+
RuleCategories.Usage,
26+
DiagnosticSeverity.Info,
27+
isEnabledByDefault: false,
28+
description: "",
29+
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.UsePatternMatchingForNullCheck));
30+
31+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(s_ruleEqual, s_ruleNotEqual);
32+
33+
public override void Initialize(AnalysisContext context)
34+
{
35+
context.EnableConcurrentExecution();
36+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
37+
context.RegisterOperationAction(AnalyzeBinary, OperationKind.Binary);
38+
}
39+
40+
private void AnalyzeBinary(OperationAnalysisContext context)
41+
{
42+
var operation = (IBinaryOperation)context.Operation;
43+
if (operation is { OperatorKind: BinaryOperatorKind.Equals or BinaryOperatorKind.NotEquals, OperatorMethod: null })
44+
{
45+
var leftIfNull = IsNull(operation.LeftOperand);
46+
var rightIfNull = IsNull(operation.RightOperand);
47+
if (leftIfNull ^ rightIfNull)
48+
{
49+
context.ReportDiagnostic(operation.OperatorKind is BinaryOperatorKind.Equals ? s_ruleEqual : s_ruleNotEqual, operation);
50+
}
51+
}
52+
}
53+
54+
private static bool IsNull(IOperation operation)
55+
=> operation.UnwrapConversionOperations() is ILiteralOperation { ConstantValue: { HasValue: true, Value: null } };
56+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
using System.Threading.Tasks;
2+
using Meziantou.Analyzer.Rules;
3+
using TestHelper;
4+
using Xunit;
5+
6+
namespace Meziantou.Analyzer.Test.Rules;
7+
8+
public sealed class UsePatternMatchingForNullCheckAnalyzerTests
9+
{
10+
private static ProjectBuilder CreateProjectBuilder()
11+
{
12+
return new ProjectBuilder()
13+
.WithOutputKind(Microsoft.CodeAnalysis.OutputKind.ConsoleApplication)
14+
.WithAnalyzer<UsePatternMatchingForNullCheckAnalyzer>()
15+
.WithCodeFixProvider<UsePatternMatchingForNullCheckFixer>();
16+
}
17+
18+
[Fact]
19+
public async Task NullCheckForNullableOfT()
20+
{
21+
await CreateProjectBuilder()
22+
.WithSourceCode("_ = [|(int?)0 == null|];")
23+
.ShouldFixCodeWith("_ = (int?)0 is null;")
24+
.ValidateAsync();
25+
}
26+
27+
[Fact]
28+
public async Task NullCheckForNullableOfT_NotNull()
29+
{
30+
await CreateProjectBuilder()
31+
.WithSourceCode("_ = [|(int?)0 != null|];")
32+
.ShouldFixCodeWith("_ = (int?)0 is not null;")
33+
.ValidateAsync();
34+
}
35+
36+
[Fact]
37+
public async Task NullCheckForObject()
38+
{
39+
await CreateProjectBuilder()
40+
.WithSourceCode("_ = [|new object() == null|];")
41+
.ShouldFixCodeWith("_ = new object() is null;")
42+
.ValidateAsync();
43+
}
44+
45+
[Fact]
46+
public async Task NullCheckForObject_NullFirst()
47+
{
48+
await CreateProjectBuilder()
49+
.WithSourceCode("_ = [|null == new object()|];")
50+
.ShouldFixCodeWith("_ = new object() is null;")
51+
.ValidateAsync();
52+
}
53+
54+
[Fact]
55+
public async Task NullCheckForObject_NotNull_NullFirst()
56+
{
57+
await CreateProjectBuilder()
58+
.WithSourceCode("_ = [|null != new object()|];")
59+
.ShouldFixCodeWith("_ = new object() is not null;")
60+
.ValidateAsync();
61+
}
62+
63+
[Fact]
64+
public async Task NullEqualsNull()
65+
{
66+
// no report as "null is null" is not valid
67+
await CreateProjectBuilder()
68+
.WithSourceCode("_ = null == null;")
69+
.ValidateAsync();
70+
}
71+
72+
[Fact]
73+
public async Task NotNullCheck()
74+
{
75+
// no report as "null is null" is not valid
76+
await CreateProjectBuilder()
77+
.WithSourceCode("_ = new object() == new object();")
78+
.ValidateAsync();
79+
}
80+
81+
[Fact]
82+
public async Task NullCheckForObjectWithCustomOperator()
83+
{
84+
await CreateProjectBuilder()
85+
.WithSourceCode("""
86+
_ = new Sample() == null;
87+
88+
class Sample
89+
{
90+
public static bool operator ==(Sample left, Sample right) => false;
91+
public static bool operator !=(Sample left, Sample right) => false;
92+
}
93+
""")
94+
.ValidateAsync();
95+
}
96+
97+
[Fact]
98+
public async Task NullCheckForNullableOfT_IsNull()
99+
{
100+
await CreateProjectBuilder()
101+
.WithSourceCode(@"_ = (int?)0 is null;")
102+
.ValidateAsync();
103+
}
104+
}

0 commit comments

Comments
 (0)