Skip to content

Commit ee0a595

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

File tree

10 files changed

+308
-2
lines changed

10 files changed

+308
-2
lines changed

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: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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+
.WithAnalyzer<UsePatternMatchingInsteadOfHasValueAnalyzer>()
14+
.WithCodeFixProvider<UsePatternMatchingInsteadOfHasvalueFixer>();
15+
}
16+
17+
[Fact]
18+
public async Task HasValue()
19+
{
20+
await CreateProjectBuilder()
21+
.WithSourceCode("""
22+
var value = default(int?);
23+
_ = [|value.HasValue|];
24+
""")
25+
.ShouldFixCodeWith("""
26+
var value = default(int?);
27+
_ = value is not null;
28+
""")
29+
.ValidateAsync();
30+
}
31+
32+
[Fact]
33+
public async Task NotHasValue()
34+
{
35+
await CreateProjectBuilder()
36+
.WithSourceCode("""
37+
var value = default(int?);
38+
_ = ![|value.HasValue|];
39+
""")
40+
.ShouldFixCodeWith("""
41+
var value = default(int?);
42+
_ = value is null;
43+
""")
44+
.ValidateAsync();
45+
}
46+
47+
[Fact]
48+
public async Task HasValueEqualsTrue()
49+
{
50+
await CreateProjectBuilder()
51+
.WithSourceCode("""
52+
var value = default(int?);
53+
_ = [|value.HasValue|] == true;
54+
""")
55+
.ShouldFixCodeWith("""
56+
var value = default(int?);
57+
_ = value is not null;
58+
""")
59+
.ValidateAsync();
60+
}
61+
62+
[Fact]
63+
public async Task HasValueEqualsFalse()
64+
{
65+
await CreateProjectBuilder()
66+
.WithSourceCode("""
67+
var value = default(int?);
68+
_ = [|value.HasValue|] == false;
69+
""")
70+
.ShouldFixCodeWith("""
71+
var value = default(int?);
72+
_ = value is null;
73+
""")
74+
.ValidateAsync();
75+
}
76+
77+
[Fact]
78+
public async Task FalseEqualsHasValue()
79+
{
80+
await CreateProjectBuilder()
81+
.WithSourceCode("""
82+
var value = default(int?);
83+
_ = false == [|value.HasValue|];
84+
""")
85+
.ShouldFixCodeWith("""
86+
var value = default(int?);
87+
_ = value is null;
88+
""")
89+
.ValidateAsync();
90+
}
91+
92+
[Fact]
93+
public async Task HasValueIsTrue()
94+
{
95+
await CreateProjectBuilder()
96+
.WithSourceCode("""
97+
var value = default(int?);
98+
_ = [|value.HasValue|] is true;
99+
""")
100+
.ShouldFixCodeWith("""
101+
var value = default(int?);
102+
_ = value is not null;
103+
""")
104+
.ValidateAsync();
105+
}
106+
107+
[Fact]
108+
public async Task HasValueIsFalse()
109+
{
110+
await CreateProjectBuilder()
111+
.WithSourceCode("""
112+
var value = default(int?);
113+
_ = [|value.HasValue|] is false;
114+
""")
115+
.ShouldFixCodeWith("""
116+
var value = default(int?);
117+
_ = value is null;
118+
""")
119+
.ValidateAsync();
120+
}
121+
}

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)