Skip to content

Commit 22af5c5

Browse files
committed
Add MA0171: Replace HasValue with pattern matching
1 parent 8d79381 commit 22af5c5

10 files changed

Lines changed: 309 additions & 2 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ If you are already using other analyzers, you can check [which rules are duplica
186186
|[MA0168](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0168.md)|Performance|Use readonly struct for in or ref readonly parameter|ℹ️|||
187187
|[MA0169](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0169.md)|Design|Use Equals method instead of operator|⚠️|✔️||
188188
|[MA0170](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0170.md)|Design|Type cannot be used as an attribute argument|⚠️|||
189+
|[MA0171](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0171.md)|Usage|Use pattern matching instead of inequality operators for discrete value|ℹ️||✔️|
189190

190191
<!-- rules -->
191192

docs/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@
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>|||
171171
|[MA0169](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0169.md)|Design|Use Equals method instead of operator|<span title='Warning'>⚠️</span>|✔️||
172172
|[MA0170](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0170.md)|Design|Type cannot be used as an attribute argument|<span title='Warning'>⚠️</span>|||
173+
|[MA0171](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0171.md)|Usage|Use pattern matching instead of inequality operators for discrete value|<span title='Info'>ℹ️</span>||✔️|
173174

174175
|Id|Suppressed rule|Justification|
175176
|--|---------------|-------------|
@@ -688,6 +689,9 @@ dotnet_diagnostic.MA0169.severity = warning
688689
689690
# MA0170: Type cannot be used as an attribute argument
690691
dotnet_diagnostic.MA0170.severity = none
692+
693+
# MA0171: Use pattern matching instead of inequality operators for discrete value
694+
dotnet_diagnostic.MA0171.severity = none
691695
```
692696

693697
# .editorconfig - all rules disabled
@@ -1199,4 +1203,7 @@ dotnet_diagnostic.MA0169.severity = none
11991203
12001204
# MA0170: Type cannot be used as an attribute argument
12011205
dotnet_diagnostic.MA0170.severity = none
1206+
1207+
# MA0171: Use pattern matching instead of inequality operators for discrete value
1208+
dotnet_diagnostic.MA0171.severity = none
12021209
```

docs/Rules/MA0171.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# MA0171 - Use pattern matching instead of inequality operators for discrete value
2+
3+
Use pattern matching instead of the `HasValue` property to check for non-nullable value types or nullable value types.
4+
5+
````c#
6+
int? value = null;
7+
8+
_ = value.HasValue; // non-compliant
9+
_ = value is not null; // compliant
10+
````
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
using System.Collections.Immutable;
2+
using System.Composition;
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.CodeActions;
5+
using Microsoft.CodeAnalysis.CodeFixes;
6+
using Microsoft.CodeAnalysis.CSharp;
7+
using Microsoft.CodeAnalysis.CSharp.Syntax;
8+
using Microsoft.CodeAnalysis.Editing;
9+
using Microsoft.CodeAnalysis.Operations;
10+
using Microsoft.CodeAnalysis.Simplification;
11+
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
12+
13+
namespace Meziantou.Analyzer.Rules;
14+
15+
[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
16+
public sealed class UsePatternMatchingInsteadOfHasvalueFixer : CodeFixProvider
17+
{
18+
public override ImmutableArray<string> FixableDiagnosticIds =>
19+
ImmutableArray.Create(RuleIdentifiers.UsePatternMatchingInsteadOfHasvalue);
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 MemberAccessExpressionSyntax memberAccess)
28+
return;
29+
30+
context.RegisterCodeFix(
31+
CodeAction.Create(
32+
"Use pattern matching",
33+
ct => Update(context.Document, memberAccess, ct),
34+
equivalenceKey: "Use pattern matching"),
35+
context.Diagnostics);
36+
}
37+
38+
private static async Task<Document> Update(Document document, MemberAccessExpressionSyntax node, CancellationToken cancellationToken)
39+
{
40+
var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);
41+
if (editor.SemanticModel.GetOperation(node, cancellationToken) is not IPropertyReferenceOperation operation)
42+
return document;
43+
44+
var (nodeToReplace, negate) = GetNodeToReplace(operation);
45+
var target = operation.Instance?.Syntax as ExpressionSyntax;
46+
var newNode = MakeIsNotNull(target ?? node, negate);
47+
editor.ReplaceNode(nodeToReplace, newNode);
48+
return editor.GetChangedDocument();
49+
}
50+
51+
private static (SyntaxNode Node, bool Negate) GetNodeToReplace(IOperation operation)
52+
{
53+
if (operation.Parent is IUnaryOperation unaryOperation && unaryOperation.OperatorKind == UnaryOperatorKind.Not)
54+
return (operation.Parent.Syntax, true);
55+
56+
if (operation.Parent is IBinaryOperation binaryOperation &&
57+
(binaryOperation.OperatorKind is BinaryOperatorKind.Equals or BinaryOperatorKind.NotEquals))
58+
{
59+
if (binaryOperation.RightOperand.ConstantValue is { HasValue: true, Value: bool rightValue })
60+
{
61+
var negate = (!rightValue && binaryOperation.OperatorKind is BinaryOperatorKind.Equals) ||
62+
(rightValue && binaryOperation.OperatorKind is BinaryOperatorKind.NotEquals);
63+
return (operation.Parent.Syntax, negate);
64+
}
65+
66+
if (binaryOperation.LeftOperand.ConstantValue is { HasValue: true, Value: bool leftValue })
67+
{
68+
var negate = (!leftValue && binaryOperation.OperatorKind is BinaryOperatorKind.Equals) ||
69+
(leftValue && binaryOperation.OperatorKind is BinaryOperatorKind.NotEquals);
70+
return (operation.Parent.Syntax, negate);
71+
}
72+
}
73+
74+
if (operation.Parent is IIsPatternOperation { Pattern: IConstantPatternOperation { Value: ILiteralOperation { ConstantValue: { Value: bool value } } } })
75+
{
76+
if (value)
77+
{
78+
return (operation.Parent.Syntax, false);
79+
}
80+
else
81+
{
82+
return (operation.Parent.Syntax, true);
83+
}
84+
}
85+
86+
return (operation.Syntax, false);
87+
}
88+
89+
private static IsPatternExpressionSyntax MakeIsNotNull(ExpressionSyntax instance, bool negate)
90+
{
91+
PatternSyntax constantExpression = ConstantPattern(LiteralExpression(SyntaxKind.NullLiteralExpression));
92+
if (!negate)
93+
{
94+
constantExpression = UnaryPattern(constantExpression);
95+
}
96+
97+
return IsPatternExpression(ParenthesizedExpression(instance).WithAdditionalAnnotations(Simplifier.Annotation), constantExpression);
98+
}
99+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,3 +508,6 @@ dotnet_diagnostic.MA0169.severity = warning
508508

509509
# MA0170: Type cannot be used as an attribute argument
510510
dotnet_diagnostic.MA0170.severity = none
511+
512+
# MA0171: Use pattern matching instead of inequality operators for discrete value
513+
dotnet_diagnostic.MA0171.severity = none

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,3 +508,6 @@ dotnet_diagnostic.MA0169.severity = none
508508

509509
# MA0170: Type cannot be used as an attribute argument
510510
dotnet_diagnostic.MA0170.severity = none
511+
512+
# MA0171: Use pattern matching instead of inequality operators for discrete value
513+
dotnet_diagnostic.MA0171.severity = none

src/Meziantou.Analyzer/RuleIdentifiers.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ internal static class RuleIdentifiers
173173
public const string UseReadOnlyStructForRefReadOnlyParameters = "MA0168";
174174
public const string UseEqualsMethodInsteadOfOperator = "MA0169";
175175
public const string TypeCannotBeUsedInAnAttributeParameter = "MA0170";
176+
public const string UsePatternMatchingInsteadOfHasvalue = "MA0171";
176177

177178
public static string GetHelpUri(string identifier)
178179
{
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.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 UsePatternMatchingInsteadOfHasValueAnalyzer : DiagnosticAnalyzer
12+
{
13+
private static readonly DiagnosticDescriptor Rule = new(
14+
RuleIdentifiers.UsePatternMatchingInsteadOfHasvalue,
15+
title: "Use pattern matching instead of inequality operators for discrete value",
16+
messageFormat: "Use pattern matching instead of inequality operators for discrete values",
17+
RuleCategories.Usage,
18+
DiagnosticSeverity.Info,
19+
isEnabledByDefault: false,
20+
description: null,
21+
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.UsePatternMatchingInsteadOfHasvalue));
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+
var tree = context.Compilation.SyntaxTrees.FirstOrDefault();
33+
if (tree is null)
34+
return;
35+
36+
if (tree.GetCSharpLanguageVersion() < Microsoft.CodeAnalysis.CSharp.LanguageVersion.CSharp8)
37+
return;
38+
39+
var analyzerContext = new AnalyzerContext(context.Compilation);
40+
context.RegisterOperationAction(analyzerContext.AnalyzeHasValue, OperationKind.PropertyReference);
41+
});
42+
}
43+
44+
private sealed class AnalyzerContext(Compilation compilation)
45+
{
46+
private readonly OperationUtilities _operationUtilities = new(compilation);
47+
private readonly ISymbol? _nullableSymbol = compilation.GetBestTypeByMetadataName("System.Nullable`1");
48+
49+
public void AnalyzeHasValue(OperationAnalysisContext context)
50+
{
51+
var propertyReference = (IPropertyReferenceOperation)context.Operation;
52+
if (propertyReference.Property.Name is "HasValue" && propertyReference.Property.ContainingType.ConstructedFrom.IsEqualTo(_nullableSymbol))
53+
{
54+
if (_operationUtilities.IsInExpressionContext(propertyReference))
55+
return;
56+
57+
context.ReportDiagnostic(Rule, propertyReference);
58+
}
59+
}
60+
}
61+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
using Meziantou.Analyzer.Rules;
2+
using TestHelper;
3+
using Xunit;
4+
5+
namespace Meziantou.Analyzer.Test.Rules;
6+
7+
public sealed class UsePatternMatchingForEqualityComparisonsAnalyzerHasValueTests
8+
{
9+
private static ProjectBuilder CreateProjectBuilder()
10+
{
11+
return new ProjectBuilder()
12+
.WithOutputKind(Microsoft.CodeAnalysis.OutputKind.ConsoleApplication)
13+
.WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.CSharp10)
14+
.WithAnalyzer<UsePatternMatchingInsteadOfHasValueAnalyzer>()
15+
.WithCodeFixProvider<UsePatternMatchingInsteadOfHasvalueFixer>();
16+
}
17+
18+
[Fact]
19+
public async Task HasValue()
20+
{
21+
await CreateProjectBuilder()
22+
.WithSourceCode("""
23+
var value = default(int?);
24+
_ = [|value.HasValue|];
25+
""")
26+
.ShouldFixCodeWith("""
27+
var value = default(int?);
28+
_ = value is not null;
29+
""")
30+
.ValidateAsync();
31+
}
32+
33+
[Fact]
34+
public async Task NotHasValue()
35+
{
36+
await CreateProjectBuilder()
37+
.WithSourceCode("""
38+
var value = default(int?);
39+
_ = ![|value.HasValue|];
40+
""")
41+
.ShouldFixCodeWith("""
42+
var value = default(int?);
43+
_ = value is null;
44+
""")
45+
.ValidateAsync();
46+
}
47+
48+
[Fact]
49+
public async Task HasValueEqualsTrue()
50+
{
51+
await CreateProjectBuilder()
52+
.WithSourceCode("""
53+
var value = default(int?);
54+
_ = [|value.HasValue|] == true;
55+
""")
56+
.ShouldFixCodeWith("""
57+
var value = default(int?);
58+
_ = value is not null;
59+
""")
60+
.ValidateAsync();
61+
}
62+
63+
[Fact]
64+
public async Task HasValueEqualsFalse()
65+
{
66+
await CreateProjectBuilder()
67+
.WithSourceCode("""
68+
var value = default(int?);
69+
_ = [|value.HasValue|] == false;
70+
""")
71+
.ShouldFixCodeWith("""
72+
var value = default(int?);
73+
_ = value is null;
74+
""")
75+
.ValidateAsync();
76+
}
77+
78+
[Fact]
79+
public async Task FalseEqualsHasValue()
80+
{
81+
await CreateProjectBuilder()
82+
.WithSourceCode("""
83+
var value = default(int?);
84+
_ = false == [|value.HasValue|];
85+
""")
86+
.ShouldFixCodeWith("""
87+
var value = default(int?);
88+
_ = value is null;
89+
""")
90+
.ValidateAsync();
91+
}
92+
93+
[Fact]
94+
public async Task HasValueIsTrue()
95+
{
96+
await CreateProjectBuilder()
97+
.WithSourceCode("""
98+
var value = default(int?);
99+
_ = [|value.HasValue|] is true;
100+
""")
101+
.ShouldFixCodeWith("""
102+
var value = default(int?);
103+
_ = value is not null;
104+
""")
105+
.ValidateAsync();
106+
}
107+
108+
[Fact]
109+
public async Task HasValueIsFalse()
110+
{
111+
await CreateProjectBuilder()
112+
.WithSourceCode("""
113+
var value = default(int?);
114+
_ = [|value.HasValue|] is false;
115+
""")
116+
.ShouldFixCodeWith("""
117+
var value = default(int?);
118+
_ = value is null;
119+
""")
120+
.ValidateAsync();
121+
}
122+
}

tests/Meziantou.Analyzer.Test/Rules/UsePatternMatchingForEqualityComparisonsAnalyzerTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ class Sample
117117
{
118118
public static bool operator ==(Sample left, Sample right) => false;
119119
public static bool operator !=(Sample left, Sample right) => false;
120-
}
120+
}
121121
""")
122122
.ValidateAsync();
123123
}
@@ -177,7 +177,7 @@ class Sample
177177
{
178178
public static bool operator ==(Sample left, int right) => false;
179179
public static bool operator !=(Sample left, int right) => false;
180-
}
180+
}
181181
""")
182182
.ValidateAsync();
183183
}

0 commit comments

Comments
 (0)