Skip to content

Commit a4d485f

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

File tree

16 files changed

+314
-8
lines changed

16 files changed

+314
-8
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+
````

src/DocumentationGenerator/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ static string GenerateRulesTable(List<DiagnosticAnalyzer> diagnosticAnalyzers, L
173173
return sb.ToString();
174174
}
175175

176-
[System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "The url must be lowercase")]
176+
[SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "The url must be lowercase")]
177177
static string GenerateSuppressorsTable(List<DiagnosticSuppressor> diagnosticSuppressors)
178178
{
179179
var sb = new StringBuilder();
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/Internals/ObjectPool.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ internal abstract class ObjectPoolProvider
7777
/// <typeparam name="T">The type to create a pool for.</typeparam>
7878
public ObjectPool<T> Create<T>() where T : class, new()
7979
{
80-
return Create<T>(new DefaultPooledObjectPolicy<T>());
80+
return Create(new DefaultPooledObjectPolicy<T>());
8181
}
8282

8383
/// <summary>

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+
}

0 commit comments

Comments
 (0)