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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ If you are already using other analyzers, you can check [which rules are duplica
|[MA0189](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0189.md)|Design|Use InlineArray instead of fixed-size buffers|ℹ️|✔️|✔️|
|[MA0190](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0190.md)|Design|Use partial property instead of partial method for GeneratedRegex|ℹ️|✔️|✔️|
|[MA0191](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0191.md)|Design|Do not use the null-forgiving operator|⚠️|❌|❌|
|[MA0192](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0192.md)|Usage|Use HasFlag instead of bitwise checks|ℹ️|❌|✔️|

<!-- rules -->

Expand Down
7 changes: 7 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@
|[MA0189](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0189.md)|Design|Use InlineArray instead of fixed-size buffers|<span title='Info'>ℹ️</span>|✔️|✔️|
|[MA0190](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0190.md)|Design|Use partial property instead of partial method for GeneratedRegex|<span title='Info'>ℹ️</span>|✔️|✔️|
|[MA0191](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0191.md)|Design|Do not use the null-forgiving operator|<span title='Warning'>⚠️</span>|❌|❌|
|[MA0192](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0192.md)|Usage|Use HasFlag instead of bitwise checks|<span title='Info'>ℹ️</span>|❌|✔️|

|Id|Suppressed rule|Justification|
|--|---------------|-------------|
Expand Down Expand Up @@ -775,6 +776,9 @@ dotnet_diagnostic.MA0190.severity = suggestion

# MA0191: Do not use the null-forgiving operator
dotnet_diagnostic.MA0191.severity = none

# MA0192: Use HasFlag instead of bitwise checks
dotnet_diagnostic.MA0192.severity = none
```

# .editorconfig - all rules disabled
Expand Down Expand Up @@ -1346,4 +1350,7 @@ dotnet_diagnostic.MA0190.severity = none

# MA0191: Do not use the null-forgiving operator
dotnet_diagnostic.MA0191.severity = none

# MA0192: Use HasFlag instead of bitwise checks
dotnet_diagnostic.MA0192.severity = none
```
55 changes: 55 additions & 0 deletions docs/Rules/MA0192.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# MA0192 - Use HasFlag instead of bitwise checks
<!-- sources -->
Sources: [UseHasFlagMethodAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/UseHasFlagMethodAnalyzer.cs), [UseHasFlagMethodFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/UseHasFlagMethodFixer.cs)
<!-- sources -->

Use `Enum.HasFlag` instead of manually checking bits using equality or `is` patterns.

This rule reports patterns such as:

- `(value & MyEnum.Flag1) == MyEnum.Flag1`
- `(value & MyEnum.Flag1) != MyEnum.Flag1`
- `(value & MyEnum.Flag1) is MyEnum.Flag1`
- `(value & MyEnum.Flag1) is not MyEnum.Flag1`

## Non-compliant code

````csharp
[System.Flags]
enum MyEnum
{
None = 0,
Flag1 = 1,
Flag2 = 2,
}

bool M(MyEnum value)
{
return (value & MyEnum.Flag1) == MyEnum.Flag1;
}
````

## Compliant code

````csharp
[System.Flags]
enum MyEnum
{
None = 0,
Flag1 = 1,
Flag2 = 2,
}

bool M(MyEnum value)
{
return value.HasFlag(MyEnum.Flag1);
}
````

## Configuration

This rule is disabled by default. To enable it, add the following to your `.editorconfig` file:

````editorconfig
dotnet_diagnostic.MA0192.severity = suggestion
````
244 changes: 244 additions & 0 deletions src/Meziantou.Analyzer.CodeFixers/Rules/UseHasFlagMethodFixer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
using System.Collections.Immutable;
using System.Composition;
using Meziantou.Analyzer.Internals;
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.Formatting;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.Text;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

namespace Meziantou.Analyzer.Rules;

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

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 null)
return;

var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
if (semanticModel is null)
return;

if (!TryGetHasFlagPattern(semanticModel, nodeToFix, context.CancellationToken, out var pattern))
return;

const string Title = "Use HasFlag";
context.RegisterCodeFix(
CodeAction.Create(Title, ct => UseHasFlag(context.Document, pattern.OperationSpan, ct), equivalenceKey: Title),
context.Diagnostics);
}

private static async Task<Document> UseHasFlag(Document document, TextSpan operationSpan, CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
if (root is null)
return document;

var nodeToFix = root.FindNode(operationSpan, getInnermostNodeForTie: true);

var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
if (semanticModel is null)
return document;

if (!TryGetHasFlagPattern(semanticModel, nodeToFix, cancellationToken, out var pattern))
return document;

var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);
var target = AddParenthesesIfNeeded(pattern.EnumValueExpression.WithoutTrivia());

var replacementNode = InvocationExpression(
MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, target, IdentifierName(nameof(Enum.HasFlag))),
ArgumentList(SingletonSeparatedList(Argument(pattern.FlagExpression.WithoutTrivia()))));

ExpressionSyntax updatedNode = replacementNode;
if (pattern.Negate)
{
updatedNode = PrefixUnaryExpression(SyntaxKind.LogicalNotExpression, updatedNode);
}

editor.ReplaceNode(pattern.OperationExpression, updatedNode.WithTriviaFrom(pattern.OperationExpression).WithAdditionalAnnotations(Formatter.Annotation));
return editor.GetChangedDocument();
}

private static ExpressionSyntax AddParenthesesIfNeeded(ExpressionSyntax expression)
{
return expression switch
{
IdentifierNameSyntax => expression,
GenericNameSyntax => expression,
ThisExpressionSyntax => expression,
BaseExpressionSyntax => expression,
MemberAccessExpressionSyntax => expression,
InvocationExpressionSyntax => expression,
ElementAccessExpressionSyntax => expression,
ParenthesizedExpressionSyntax => expression,
_ => expression.Parentheses(),
};
}

private static bool TryGetHasFlagPattern(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken, [NotNullWhen(true)] out HasFlagPattern? pattern)
{
foreach (var candidateNode in node.AncestorsAndSelf())
{
var operation = semanticModel.GetOperation(candidateNode, cancellationToken);
if (operation is null)
continue;

if (TryGetHasFlagPattern(operation, out pattern))
return true;
}

pattern = null;
return false;
}

private static bool TryGetHasFlagPattern(IOperation operation, [NotNullWhen(true)] out HasFlagPattern? pattern)
{
if (operation is IBinaryOperation { OperatorKind: BinaryOperatorKind.Equals or BinaryOperatorKind.NotEquals } binaryOperation)
{
pattern = GetFromBinaryComparison(binaryOperation);
return pattern is not null;
}

if (operation is IIsPatternOperation
{
Value: IBinaryOperation { OperatorKind: BinaryOperatorKind.And } andOperation,
Pattern: IPatternOperation patternOperation,
Syntax: ExpressionSyntax operationExpression,
})
{
if (TryGetComparedOperand(patternOperation, out var comparedOperand, out var negate))
{
pattern = GetFromBitwiseAnd(andOperation, comparedOperand, operationExpression, negate);
return pattern is not null;
}
}

pattern = null;
return false;
}

private static bool TryGetComparedOperand(IPatternOperation patternOperation, [NotNullWhen(true)] out IOperation? comparedOperand, out bool negate)
{
if (patternOperation is IConstantPatternOperation { Value: not null } constantPattern)
{
comparedOperand = constantPattern.Value;
negate = false;
return true;
}

if (patternOperation is INegatedPatternOperation { Pattern: IConstantPatternOperation { Value: not null } constantPattern2 })
{
comparedOperand = constantPattern2.Value;
negate = true;
return true;
}

comparedOperand = null;
negate = false;
return false;
}

private static HasFlagPattern? GetFromBinaryComparison(IBinaryOperation operation)
{
var leftOperand = operation.LeftOperand.UnwrapImplicitConversionOperations();
var rightOperand = operation.RightOperand.UnwrapImplicitConversionOperations();
var negate = operation.OperatorKind is BinaryOperatorKind.NotEquals;

if (operation.Syntax is not ExpressionSyntax operationExpression)
return null;

if (leftOperand is IBinaryOperation { OperatorKind: BinaryOperatorKind.And } leftBitwiseAnd)
{
var pattern = GetFromBitwiseAnd(leftBitwiseAnd, rightOperand, operationExpression, negate);
if (pattern is not null)
return pattern;
}

if (rightOperand is IBinaryOperation { OperatorKind: BinaryOperatorKind.And } rightBitwiseAnd)
{
var pattern = GetFromBitwiseAnd(rightBitwiseAnd, leftOperand, operationExpression, negate);
if (pattern is not null)
return pattern;
}

return null;
}

private static HasFlagPattern? GetFromBitwiseAnd(IBinaryOperation bitwiseAndOperation, IOperation comparedOperand, ExpressionSyntax operationExpression, bool negate)
{
var leftOperand = bitwiseAndOperation.LeftOperand.UnwrapImplicitConversionOperations();
var rightOperand = bitwiseAndOperation.RightOperand.UnwrapImplicitConversionOperations();
comparedOperand = comparedOperand.UnwrapImplicitConversionOperations();

if (TryGetEnumFlagReference(rightOperand, comparedOperand, out var flagOperation) &&
IsValidPattern(leftOperand, flagOperation) &&
leftOperand.Syntax is ExpressionSyntax enumValueExpression &&
flagOperation.Syntax is ExpressionSyntax flagExpression)
{
return new(operationExpression, enumValueExpression, flagExpression, negate);
}

if (TryGetEnumFlagReference(leftOperand, comparedOperand, out flagOperation) &&
IsValidPattern(rightOperand, flagOperation) &&
rightOperand.Syntax is ExpressionSyntax enumValueExpression2 &&
flagOperation.Syntax is ExpressionSyntax flagExpression2)
{
return new(operationExpression, enumValueExpression2, flagExpression2, negate);
}

return null;
}

private static bool TryGetEnumFlagReference(IOperation potentialFlag, IOperation comparedOperand, [NotNullWhen(true)] out IFieldReferenceOperation? flagOperation)
{
potentialFlag = potentialFlag.UnwrapImplicitConversionOperations();
comparedOperand = comparedOperand.UnwrapImplicitConversionOperations();

if (potentialFlag is IFieldReferenceOperation firstFieldReference &&
comparedOperand is IFieldReferenceOperation secondFieldReference &&
firstFieldReference.Field.HasConstantValue &&
secondFieldReference.Field.HasConstantValue &&
firstFieldReference.Field.IsEqualTo(secondFieldReference.Field) &&
firstFieldReference.Field.ContainingType.IsEnumeration())
{
flagOperation = secondFieldReference;
return true;
}

flagOperation = null;
return false;
}

private static bool IsValidPattern(IOperation enumValueOperation, IOperation flagOperation)
{
if (enumValueOperation.Type is null || flagOperation.Type is null)
return false;

if (!enumValueOperation.Type.IsEnumeration())
return false;

if (!flagOperation.Type.IsEnumeration())
return false;

return enumValueOperation.Type.IsEqualTo(flagOperation.Type);
}

private sealed record HasFlagPattern(ExpressionSyntax OperationExpression, ExpressionSyntax EnumValueExpression, ExpressionSyntax FlagExpression, bool Negate)
{
public TextSpan OperationSpan => OperationExpression.Span;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -568,3 +568,6 @@ dotnet_diagnostic.MA0190.severity = suggestion

# MA0191: Do not use the null-forgiving operator
dotnet_diagnostic.MA0191.severity = none

# MA0192: Use HasFlag instead of bitwise checks
dotnet_diagnostic.MA0192.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 @@ -568,3 +568,6 @@ dotnet_diagnostic.MA0190.severity = none

# MA0191: Do not use the null-forgiving operator
dotnet_diagnostic.MA0191.severity = none

# MA0192: Use HasFlag instead of bitwise checks
dotnet_diagnostic.MA0192.severity = none
1 change: 1 addition & 0 deletions src/Meziantou.Analyzer/RuleIdentifiers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ internal static class RuleIdentifiers
public const string UseInlineArrayInsteadOfFixedBuffer = "MA0189";
public const string UsePartialPropertyInsteadOfPartialMethodForGeneratedRegex = "MA0190";
public const string DoNotUseNullForgiveness = "MA0191";
public const string UseHasFlagMethod = "MA0192";

public static string GetHelpUri(string identifier)
{
Expand Down
Loading
Loading