-
-
Notifications
You must be signed in to change notification settings - Fork 53
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
New rules: MA0141 and MA0142 to convert null check to pattern matching (
#647)
- Loading branch information
Showing
9 changed files
with
254 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# MA0141 - Use pattern matching instead of inequality operators | ||
|
||
````c# | ||
value != null; // not compliant | ||
value is not null; // ok | ||
```` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# MA0142 - Use pattern matching instead of equality operators | ||
|
||
````c# | ||
value == null; // not compliant | ||
value is null; // ok | ||
```` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
{ | ||
"sdk": { | ||
"version": "7.0.400", | ||
"rollForward": "feature" | ||
"rollForward": "latestMajor" | ||
} | ||
} |
61 changes: 61 additions & 0 deletions
61
src/Meziantou.Analyzer.CodeFixers/Rules/UsePatternMatchingForNullCheckFixer.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
using System.Collections.Immutable; | ||
using System.Composition; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CodeActions; | ||
using Microsoft.CodeAnalysis.CodeFixes; | ||
using Microsoft.CodeAnalysis.CSharp; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
using Microsoft.CodeAnalysis.Editing; | ||
using Microsoft.CodeAnalysis.Operations; | ||
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; | ||
|
||
namespace Meziantou.Analyzer.Rules; | ||
|
||
[ExportCodeFixProvider(LanguageNames.CSharp), Shared] | ||
public sealed class UsePatternMatchingForNullCheckFixer : CodeFixProvider | ||
{ | ||
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.UsePatternMatchingForNullCheck, RuleIdentifiers.UsePatternMatchingForNullEquality); | ||
|
||
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; | ||
|
||
public override async Task RegisterCodeFixesAsync(CodeFixContext context) | ||
{ | ||
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); | ||
var nodeToFix = root?.FindNode(context.Span, getInnermostNodeForTie: true); | ||
if (nodeToFix is not BinaryExpressionSyntax invocation) | ||
return; | ||
|
||
context.RegisterCodeFix( | ||
CodeAction.Create( | ||
"Use pattern matching", | ||
ct => Update(context.Document, invocation, ct), | ||
equivalenceKey: "Use pattern matching"), | ||
context.Diagnostics); | ||
} | ||
|
||
private static async Task<Document> Update(Document document, BinaryExpressionSyntax node, CancellationToken cancellationToken) | ||
{ | ||
var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); | ||
if (editor.SemanticModel.GetOperation(node, cancellationToken) is not IBinaryOperation operation) | ||
return document; | ||
|
||
var valueSyntax = IsNull(operation.LeftOperand) ? operation.RightOperand.Syntax : operation.LeftOperand.Syntax; | ||
if (valueSyntax is not ExpressionSyntax expression) | ||
return document; | ||
|
||
PatternSyntax constantExpression = ConstantPattern(LiteralExpression(SyntaxKind.NullLiteralExpression)); | ||
if (operation.OperatorKind is BinaryOperatorKind.NotEquals) | ||
{ | ||
constantExpression = UnaryPattern(constantExpression); | ||
} | ||
|
||
var newSyntax = IsPatternExpression(expression, constantExpression); | ||
editor.ReplaceNode(node, newSyntax); | ||
return editor.GetChangedDocument(); | ||
} | ||
|
||
private static bool IsNull(IOperation operation) | ||
=> operation.UnwrapConversionOperations() is ILiteralOperation { ConstantValue: { HasValue: true, Value: null } }; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
56 changes: 56 additions & 0 deletions
56
src/Meziantou.Analyzer/Rules/UsePatternMatchingForNullCheckAnalyzer.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
using System.Collections.Immutable; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.Diagnostics; | ||
using Microsoft.CodeAnalysis.Operations; | ||
|
||
namespace Meziantou.Analyzer.Rules; | ||
|
||
[DiagnosticAnalyzer(LanguageNames.CSharp)] | ||
public sealed class UsePatternMatchingForNullCheckAnalyzer : DiagnosticAnalyzer | ||
{ | ||
private static readonly DiagnosticDescriptor s_ruleEqual = new( | ||
RuleIdentifiers.UsePatternMatchingForNullEquality, | ||
title: "Use pattern matching instead of equality operators", | ||
messageFormat: "Use pattern matching instead of equality operators", | ||
RuleCategories.Usage, | ||
DiagnosticSeverity.Info, | ||
isEnabledByDefault: false, | ||
description: "", | ||
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.UsePatternMatchingForNullEquality)); | ||
|
||
private static readonly DiagnosticDescriptor s_ruleNotEqual = new( | ||
RuleIdentifiers.UsePatternMatchingForNullCheck, | ||
title: "Use pattern matching instead of inequality operators", | ||
messageFormat: "Use pattern matching instead of inequality operators", | ||
RuleCategories.Usage, | ||
DiagnosticSeverity.Info, | ||
isEnabledByDefault: false, | ||
description: "", | ||
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.UsePatternMatchingForNullCheck)); | ||
|
||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(s_ruleEqual, s_ruleNotEqual); | ||
|
||
public override void Initialize(AnalysisContext context) | ||
{ | ||
context.EnableConcurrentExecution(); | ||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); | ||
context.RegisterOperationAction(AnalyzeBinary, OperationKind.Binary); | ||
} | ||
|
||
private void AnalyzeBinary(OperationAnalysisContext context) | ||
{ | ||
var operation = (IBinaryOperation)context.Operation; | ||
if (operation is { OperatorKind: BinaryOperatorKind.Equals or BinaryOperatorKind.NotEquals, OperatorMethod: null }) | ||
{ | ||
var leftIfNull = IsNull(operation.LeftOperand); | ||
var rightIfNull = IsNull(operation.RightOperand); | ||
if (leftIfNull ^ rightIfNull) | ||
{ | ||
context.ReportDiagnostic(operation.OperatorKind is BinaryOperatorKind.Equals ? s_ruleEqual : s_ruleNotEqual, operation); | ||
} | ||
} | ||
} | ||
|
||
private static bool IsNull(IOperation operation) | ||
=> operation.UnwrapConversionOperations() is ILiteralOperation { ConstantValue: { HasValue: true, Value: null } }; | ||
} |
104 changes: 104 additions & 0 deletions
104
tests/Meziantou.Analyzer.Test/Rules/UsePatternMatchingForNullCheckAnalyzerTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
using System.Threading.Tasks; | ||
using Meziantou.Analyzer.Rules; | ||
using TestHelper; | ||
using Xunit; | ||
|
||
namespace Meziantou.Analyzer.Test.Rules; | ||
|
||
public sealed class UsePatternMatchingForNullCheckAnalyzerTests | ||
{ | ||
private static ProjectBuilder CreateProjectBuilder() | ||
{ | ||
return new ProjectBuilder() | ||
.WithOutputKind(Microsoft.CodeAnalysis.OutputKind.ConsoleApplication) | ||
.WithAnalyzer<UsePatternMatchingForNullCheckAnalyzer>() | ||
.WithCodeFixProvider<UsePatternMatchingForNullCheckFixer>(); | ||
} | ||
|
||
[Fact] | ||
public async Task NullCheckForNullableOfT() | ||
{ | ||
await CreateProjectBuilder() | ||
.WithSourceCode("_ = [|(int?)0 == null|];") | ||
.ShouldFixCodeWith("_ = (int?)0 is null;") | ||
.ValidateAsync(); | ||
} | ||
|
||
[Fact] | ||
public async Task NullCheckForNullableOfT_NotNull() | ||
{ | ||
await CreateProjectBuilder() | ||
.WithSourceCode("_ = [|(int?)0 != null|];") | ||
.ShouldFixCodeWith("_ = (int?)0 is not null;") | ||
.ValidateAsync(); | ||
} | ||
|
||
[Fact] | ||
public async Task NullCheckForObject() | ||
{ | ||
await CreateProjectBuilder() | ||
.WithSourceCode("_ = [|new object() == null|];") | ||
.ShouldFixCodeWith("_ = new object() is null;") | ||
.ValidateAsync(); | ||
} | ||
|
||
[Fact] | ||
public async Task NullCheckForObject_NullFirst() | ||
{ | ||
await CreateProjectBuilder() | ||
.WithSourceCode("_ = [|null == new object()|];") | ||
.ShouldFixCodeWith("_ = new object() is null;") | ||
.ValidateAsync(); | ||
} | ||
|
||
[Fact] | ||
public async Task NullCheckForObject_NotNull_NullFirst() | ||
{ | ||
await CreateProjectBuilder() | ||
.WithSourceCode("_ = [|null != new object()|];") | ||
.ShouldFixCodeWith("_ = new object() is not null;") | ||
.ValidateAsync(); | ||
} | ||
|
||
[Fact] | ||
public async Task NullEqualsNull() | ||
{ | ||
// no report as "null is null" is not valid | ||
await CreateProjectBuilder() | ||
.WithSourceCode("_ = null == null;") | ||
.ValidateAsync(); | ||
} | ||
|
||
[Fact] | ||
public async Task NotNullCheck() | ||
{ | ||
// no report as "null is null" is not valid | ||
await CreateProjectBuilder() | ||
.WithSourceCode("_ = new object() == new object();") | ||
.ValidateAsync(); | ||
} | ||
|
||
[Fact] | ||
public async Task NullCheckForObjectWithCustomOperator() | ||
{ | ||
await CreateProjectBuilder() | ||
.WithSourceCode(""" | ||
_ = new Sample() == null; | ||
class Sample | ||
{ | ||
public static bool operator ==(Sample left, Sample right) => false; | ||
public static bool operator !=(Sample left, Sample right) => false; | ||
} | ||
""") | ||
.ValidateAsync(); | ||
} | ||
|
||
[Fact] | ||
public async Task NullCheckForNullableOfT_IsNull() | ||
{ | ||
await CreateProjectBuilder() | ||
.WithSourceCode(@"_ = (int?)0 is null;") | ||
.ValidateAsync(); | ||
} | ||
} |