Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Add MA0171: Replace HasValue with pattern matching
  • Loading branch information
meziantou committed Jul 31, 2025
commit a4d485f6d0dcd7e954aa07dc86fc5a00fdc7fef3
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ If you are already using other analyzers, you can check [which rules are duplica
|[MA0168](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0168.md)|Performance|Use readonly struct for in or ref readonly parameter|ℹ️|❌|❌|
|[MA0169](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0169.md)|Design|Use Equals method instead of operator|⚠️|✔️|❌|
|[MA0170](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0170.md)|Design|Type cannot be used as an attribute argument|⚠️|❌|❌|
|[MA0171](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0171.md)|Usage|Use pattern matching instead of inequality operators for discrete value|ℹ️|❌|✔️|

<!-- rules -->

Expand Down
7 changes: 7 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@
|[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>|❌|❌|
|[MA0169](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0169.md)|Design|Use Equals method instead of operator|<span title='Warning'>⚠️</span>|✔️|❌|
|[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>|❌|❌|
|[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>|❌|✔️|

|Id|Suppressed rule|Justification|
|--|---------------|-------------|
Expand Down Expand Up @@ -688,6 +689,9 @@ dotnet_diagnostic.MA0169.severity = warning

# MA0170: Type cannot be used as an attribute argument
dotnet_diagnostic.MA0170.severity = none

# MA0171: Use pattern matching instead of inequality operators for discrete value
dotnet_diagnostic.MA0171.severity = none
```

# .editorconfig - all rules disabled
Expand Down Expand Up @@ -1199,4 +1203,7 @@ dotnet_diagnostic.MA0169.severity = none

# MA0170: Type cannot be used as an attribute argument
dotnet_diagnostic.MA0170.severity = none

# MA0171: Use pattern matching instead of inequality operators for discrete value
dotnet_diagnostic.MA0171.severity = none
```
10 changes: 10 additions & 0 deletions docs/Rules/MA0171.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# MA0171 - Use pattern matching instead of inequality operators for discrete value

Use pattern matching instead of the `HasValue` property to check for non-nullable value types or nullable value types.

````c#
int? value = null;

_ = value.HasValue; // non-compliant
_ = value is not null; // compliant
````
2 changes: 1 addition & 1 deletion src/DocumentationGenerator/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ static string GenerateRulesTable(List<DiagnosticAnalyzer> diagnosticAnalyzers, L
return sb.ToString();
}

[System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "The url must be lowercase")]
[SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "The url must be lowercase")]
static string GenerateSuppressorsTable(List<DiagnosticSuppressor> diagnosticSuppressors)
{
var sb = new StringBuilder();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using System.Collections.Immutable;
using System.Composition;
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 Microsoft.CodeAnalysis.Simplification;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

namespace Meziantou.Analyzer.Rules;

[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
public sealed class UsePatternMatchingInsteadOfHasvalueFixer : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds =>
ImmutableArray.Create(RuleIdentifiers.UsePatternMatchingInsteadOfHasvalue);

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 MemberAccessExpressionSyntax memberAccess)
return;

context.RegisterCodeFix(
CodeAction.Create(
"Use pattern matching",
ct => Update(context.Document, memberAccess, ct),
equivalenceKey: "Use pattern matching"),
context.Diagnostics);
}

private static async Task<Document> Update(Document document, MemberAccessExpressionSyntax node, CancellationToken cancellationToken)
{
var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);
if (editor.SemanticModel.GetOperation(node, cancellationToken) is not IPropertyReferenceOperation operation)
return document;

var (nodeToReplace, negate) = GetNodeToReplace(operation);
var target = operation.Instance?.Syntax as ExpressionSyntax;
var newNode = MakeIsNotNull(target ?? node, negate);
editor.ReplaceNode(nodeToReplace, newNode);
return editor.GetChangedDocument();
}

private static (SyntaxNode Node, bool Negate) GetNodeToReplace(IOperation operation)
{
if (operation.Parent is IUnaryOperation unaryOperation && unaryOperation.OperatorKind == UnaryOperatorKind.Not)
return (operation.Parent.Syntax, true);

if (operation.Parent is IBinaryOperation binaryOperation &&
(binaryOperation.OperatorKind is BinaryOperatorKind.Equals or BinaryOperatorKind.NotEquals))
{
if (binaryOperation.RightOperand.ConstantValue is { HasValue: true, Value: bool rightValue })
{
var negate = (!rightValue && binaryOperation.OperatorKind is BinaryOperatorKind.Equals) ||
(rightValue && binaryOperation.OperatorKind is BinaryOperatorKind.NotEquals);
return (operation.Parent.Syntax, negate);
}

if (binaryOperation.LeftOperand.ConstantValue is { HasValue: true, Value: bool leftValue })
{
var negate = (!leftValue && binaryOperation.OperatorKind is BinaryOperatorKind.Equals) ||
(leftValue && binaryOperation.OperatorKind is BinaryOperatorKind.NotEquals);
return (operation.Parent.Syntax, negate);
}
}

if (operation.Parent is IIsPatternOperation { Pattern: IConstantPatternOperation { Value: ILiteralOperation { ConstantValue: { Value: bool value } } } })
{
if (value)
{
return (operation.Parent.Syntax, false);
}
else
{
return (operation.Parent.Syntax, true);
}
}

return (operation.Syntax, false);
}

private static IsPatternExpressionSyntax MakeIsNotNull(ExpressionSyntax instance, bool negate)
{
PatternSyntax constantExpression = ConstantPattern(LiteralExpression(SyntaxKind.NullLiteralExpression));
if (!negate)
{
constantExpression = UnaryPattern(constantExpression);
}

return IsPatternExpression(ParenthesizedExpression(instance).WithAdditionalAnnotations(Simplifier.Annotation), constantExpression);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -508,3 +508,6 @@ dotnet_diagnostic.MA0169.severity = warning

# MA0170: Type cannot be used as an attribute argument
dotnet_diagnostic.MA0170.severity = none

# MA0171: Use pattern matching instead of inequality operators for discrete value
dotnet_diagnostic.MA0171.severity = none
3 changes: 3 additions & 0 deletions src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -508,3 +508,6 @@ dotnet_diagnostic.MA0169.severity = none

# MA0170: Type cannot be used as an attribute argument
dotnet_diagnostic.MA0170.severity = none

# MA0171: Use pattern matching instead of inequality operators for discrete value
dotnet_diagnostic.MA0171.severity = none
2 changes: 1 addition & 1 deletion src/Meziantou.Analyzer/Internals/ObjectPool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ internal abstract class ObjectPoolProvider
/// <typeparam name="T">The type to create a pool for.</typeparam>
public ObjectPool<T> Create<T>() where T : class, new()
{
return Create<T>(new DefaultPooledObjectPolicy<T>());
return Create(new DefaultPooledObjectPolicy<T>());
}

/// <summary>
Expand Down
1 change: 1 addition & 0 deletions src/Meziantou.Analyzer/RuleIdentifiers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ internal static class RuleIdentifiers
public const string UseReadOnlyStructForRefReadOnlyParameters = "MA0168";
public const string UseEqualsMethodInsteadOfOperator = "MA0169";
public const string TypeCannotBeUsedInAnAttributeParameter = "MA0170";
public const string UsePatternMatchingInsteadOfHasvalue = "MA0171";

public static string GetHelpUri(string identifier)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System.Collections.Immutable;
using System.Linq;
using Meziantou.Analyzer.Internals;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Meziantou.Analyzer.Rules;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class UsePatternMatchingInsteadOfHasValueAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor Rule = new(
RuleIdentifiers.UsePatternMatchingInsteadOfHasvalue,
title: "Use pattern matching instead of inequality operators for discrete value",
messageFormat: "Use pattern matching instead of inequality operators for discrete values",
RuleCategories.Usage,
DiagnosticSeverity.Info,
isEnabledByDefault: false,
description: null,
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.UsePatternMatchingInsteadOfHasvalue));

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

context.RegisterCompilationStartAction(context =>
{
var tree = context.Compilation.SyntaxTrees.FirstOrDefault();
if (tree is null)
return;

if (tree.GetCSharpLanguageVersion() < Microsoft.CodeAnalysis.CSharp.LanguageVersion.CSharp8)
return;

var analyzerContext = new AnalyzerContext(context.Compilation);
context.RegisterOperationAction(analyzerContext.AnalyzeHasValue, OperationKind.PropertyReference);
});
}

private sealed class AnalyzerContext(Compilation compilation)
{
private readonly OperationUtilities _operationUtilities = new(compilation);
private readonly ISymbol? _nullableSymbol = compilation.GetBestTypeByMetadataName("System.Nullable`1");

public void AnalyzeHasValue(OperationAnalysisContext context)
{
var propertyReference = (IPropertyReferenceOperation)context.Operation;
if (propertyReference.Property.Name is "HasValue" && propertyReference.Property.ContainingType.ConstructedFrom.IsEqualTo(_nullableSymbol))
{
if (_operationUtilities.IsInExpressionContext(propertyReference))
return;

context.ReportDiagnostic(Rule, propertyReference);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ private async Task<Document[]> GetDocuments()
return documents;
}

[System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")]
[SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")]
private Task<Project> CreateProject()
{
var fileNamePrefix = "Test";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public static async Task<bool> IsUrlAccessible(this HttpClient httpClient, Uri u
}
}

[System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")]
[SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")]
private static HttpClient CreateHttpClient()
{
#if NETCOREAPP2_1_OR_GREATER
Expand Down
2 changes: 1 addition & 1 deletion tests/Meziantou.Analyzer.Test/Helpers/TargetFramework.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace Meziantou.Analyzer.Test.Helpers;

[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1027:Mark enums with FlagsAttribute")]
[SuppressMessage("Design", "CA1027:Mark enums with FlagsAttribute")]
public enum TargetFramework
{
NetStandard2_0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ private static ProjectBuilder CreateProjectBuilder()
}

[Fact]
public async System.Threading.Tasks.Task TestAsync()
public async Task TestAsync()
{
const string SourceCode = @"using System.Collections.Generic;
class Test
Expand Down
Loading