diff --git a/README.md b/README.md index 077c58fb..fbb2a806 100755 --- a/README.md +++ b/README.md @@ -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|ℹ️|❌|✔️| diff --git a/docs/README.md b/docs/README.md index 08d5ae0c..e0fd8a43 100755 --- a/docs/README.md +++ b/docs/README.md @@ -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|ℹ️|✔️|✔️| |[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|ℹ️|❌|✔️| |Id|Suppressed rule|Justification| |--|---------------|-------------| @@ -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 @@ -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 ``` diff --git a/docs/Rules/MA0192.md b/docs/Rules/MA0192.md new file mode 100644 index 00000000..31475d86 --- /dev/null +++ b/docs/Rules/MA0192.md @@ -0,0 +1,55 @@ +# MA0192 - Use HasFlag instead of bitwise checks + +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) + + +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 +```` diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/UseHasFlagMethodFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/UseHasFlagMethodFixer.cs new file mode 100644 index 00000000..74893384 --- /dev/null +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/UseHasFlagMethodFixer.cs @@ -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 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 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; + } +} diff --git a/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig index c1f0232b..22060bd0 100644 --- a/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig +++ b/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig @@ -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 diff --git a/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig index 3cc8e7a3..27bd6318 100644 --- a/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig +++ b/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig @@ -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 diff --git a/src/Meziantou.Analyzer/RuleIdentifiers.cs b/src/Meziantou.Analyzer/RuleIdentifiers.cs index 7d244575..f94bd899 100755 --- a/src/Meziantou.Analyzer/RuleIdentifiers.cs +++ b/src/Meziantou.Analyzer/RuleIdentifiers.cs @@ -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) { diff --git a/src/Meziantou.Analyzer/Rules/UseHasFlagMethodAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseHasFlagMethodAnalyzer.cs new file mode 100644 index 00000000..a59a983b --- /dev/null +++ b/src/Meziantou.Analyzer/Rules/UseHasFlagMethodAnalyzer.cs @@ -0,0 +1,173 @@ +using System.Collections.Immutable; +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 UseHasFlagMethodAnalyzer : DiagnosticAnalyzer +{ + private static readonly DiagnosticDescriptor Rule = new( + RuleIdentifiers.UseHasFlagMethod, + title: "Use HasFlag instead of bitwise checks", + messageFormat: "Use HasFlag instead of bitwise checks", + RuleCategories.Usage, + DiagnosticSeverity.Info, + isEnabledByDefault: false, + description: "", + helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.UseHasFlagMethod)); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + context.RegisterOperationAction(AnalyzeBinary, OperationKind.Binary); + context.RegisterOperationAction(AnalyzeIsPattern, OperationKind.IsPattern); + } + + private static void AnalyzeBinary(OperationAnalysisContext context) + { + var operation = (IBinaryOperation)context.Operation; + if (operation.OperatorKind is BinaryOperatorKind.Equals or BinaryOperatorKind.NotEquals && + TryGetHasFlagPattern(operation, out _)) + { + context.ReportDiagnostic(Rule, operation); + } + } + + private static void AnalyzeIsPattern(OperationAnalysisContext context) + { + var operation = (IIsPatternOperation)context.Operation; + if (TryGetHasFlagPattern(operation, out _)) + { + context.ReportDiagnostic(Rule, operation); + } + } + + 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, + }) + { + if (TryGetComparedOperand(patternOperation, out var comparedOperand)) + { + pattern = GetFromBitwiseAnd(andOperation, comparedOperand); + return pattern is not null; + } + } + + pattern = null; + return false; + } + + private static bool TryGetComparedOperand(IPatternOperation patternOperation, [NotNullWhen(true)] out IOperation? comparedOperand) + { + if (patternOperation is IConstantPatternOperation { Value: not null } constantPattern) + { + comparedOperand = constantPattern.Value; + return true; + } + + if (patternOperation is INegatedPatternOperation { Pattern: IConstantPatternOperation { Value: not null } negatedConstantPattern }) + { + comparedOperand = negatedConstantPattern.Value; + return true; + } + + comparedOperand = null; + return false; + } + + private static HasFlagPattern? GetFromBinaryComparison(IBinaryOperation operation) + { + var leftOperand = operation.LeftOperand.UnwrapImplicitConversionOperations(); + var rightOperand = operation.RightOperand.UnwrapImplicitConversionOperations(); + + if (leftOperand is IBinaryOperation { OperatorKind: BinaryOperatorKind.And } leftBitwiseAnd) + { + var pattern = GetFromBitwiseAnd(leftBitwiseAnd, rightOperand); + if (pattern is not null) + return pattern; + } + + if (rightOperand is IBinaryOperation { OperatorKind: BinaryOperatorKind.And } rightBitwiseAnd) + { + var pattern = GetFromBitwiseAnd(rightBitwiseAnd, leftOperand); + if (pattern is not null) + return pattern; + } + + return null; + } + + private static HasFlagPattern? GetFromBitwiseAnd(IBinaryOperation bitwiseAndOperation, IOperation comparedOperand) + { + var leftOperand = bitwiseAndOperation.LeftOperand.UnwrapImplicitConversionOperations(); + var rightOperand = bitwiseAndOperation.RightOperand.UnwrapImplicitConversionOperations(); + comparedOperand = comparedOperand.UnwrapImplicitConversionOperations(); + + if (TryGetEnumFlagReference(rightOperand, comparedOperand, out var flagOperation) && + IsValidPattern(leftOperand, flagOperation)) + { + return new(leftOperand, flagOperation); + } + + if (TryGetEnumFlagReference(leftOperand, comparedOperand, out flagOperation) && + IsValidPattern(rightOperand, flagOperation)) + { + return new(rightOperand, flagOperation); + } + + 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(IOperation EnumValueOperation, IOperation FlagOperation); +} diff --git a/tests/Meziantou.Analyzer.Test/Rules/UseHasFlagMethodAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UseHasFlagMethodAnalyzerTests.cs new file mode 100644 index 00000000..1c025384 --- /dev/null +++ b/tests/Meziantou.Analyzer.Test/Rules/UseHasFlagMethodAnalyzerTests.cs @@ -0,0 +1,271 @@ +using Meziantou.Analyzer.Rules; +using Microsoft.CodeAnalysis; +using TestHelper; + +namespace Meziantou.Analyzer.Test.Rules; + +public sealed class UseHasFlagMethodAnalyzerTests +{ + private static ProjectBuilder CreateProjectBuilder() + { + return new ProjectBuilder() + .WithAnalyzer() + .WithCodeFixProvider(); + } + + [Fact] + public async Task EqualityCheck_ReportDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + [System.Flags] + enum MyEnum + { + None = 0, + Flag1 = 1, + Flag2 = 2, + } + + class Sample + { + bool M(MyEnum value) => [|(value & MyEnum.Flag1) == MyEnum.Flag1|]; + } + """) + .ShouldFixCodeWith(""" + [System.Flags] + enum MyEnum + { + None = 0, + Flag1 = 1, + Flag2 = 2, + } + + class Sample + { + bool M(MyEnum value) => value.HasFlag(MyEnum.Flag1); + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task EqualityCheck_ReversedAndOperands_ReportDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + [System.Flags] + enum MyEnum + { + None = 0, + Flag1 = 1, + } + + class Sample + { + bool M(MyEnum value) => [|(MyEnum.Flag1 & value) == MyEnum.Flag1|]; + } + """) + .ShouldFixCodeWith(""" + [System.Flags] + enum MyEnum + { + None = 0, + Flag1 = 1, + } + + class Sample + { + bool M(MyEnum value) => value.HasFlag(MyEnum.Flag1); + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task IsPatternCheck_ReportDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + [System.Flags] + enum MyEnum + { + None = 0, + Flag1 = 1, + Flag2 = 2, + } + + class Sample + { + bool M(MyEnum value) => [|(value & MyEnum.Flag1) is MyEnum.Flag1|]; + } + """) + .ShouldFixCodeWith(""" + [System.Flags] + enum MyEnum + { + None = 0, + Flag1 = 1, + Flag2 = 2, + } + + class Sample + { + bool M(MyEnum value) => value.HasFlag(MyEnum.Flag1); + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task NotEqualsCheck_ReportDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + [System.Flags] + enum MyEnum + { + None = 0, + Flag1 = 1, + Flag2 = 2, + } + + class Sample + { + bool M(MyEnum value) => [|(value & MyEnum.Flag1) != MyEnum.Flag1|]; + } + """) + .ShouldFixCodeWith(""" + [System.Flags] + enum MyEnum + { + None = 0, + Flag1 = 1, + Flag2 = 2, + } + + class Sample + { + bool M(MyEnum value) => !value.HasFlag(MyEnum.Flag1); + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task IsNotPatternCheck_ReportDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + [System.Flags] + enum MyEnum + { + None = 0, + Flag1 = 1, + Flag2 = 2, + } + + class Sample + { + bool M(MyEnum value) => [|(value & MyEnum.Flag1) is not MyEnum.Flag1|]; + } + """) + .ShouldFixCodeWith(""" + [System.Flags] + enum MyEnum + { + None = 0, + Flag1 = 1, + Flag2 = 2, + } + + class Sample + { + bool M(MyEnum value) => !value.HasFlag(MyEnum.Flag1); + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task DifferentFlag_NoDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + [System.Flags] + enum MyEnum + { + None = 0, + Flag1 = 1, + Flag2 = 2, + } + + class Sample + { + bool M(MyEnum value) => (value & MyEnum.Flag1) == MyEnum.Flag2; + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task DifferentFlag_IsNotPattern_NoDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + [System.Flags] + enum MyEnum + { + None = 0, + Flag1 = 1, + Flag2 = 2, + } + + class Sample + { + bool M(MyEnum value) => (value & MyEnum.Flag1) is not MyEnum.Flag2; + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task IntegerBitwiseCheck_NoDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + class Sample + { + bool M(int value) => (value & 1) == 1; + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task NullableEnum_NoDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + [System.Flags] + enum MyEnum + { + None = 0, + Flag1 = 1, + } + + class Sample + { + bool M(MyEnum? value) => (value & MyEnum.Flag1) == MyEnum.Flag1; + } + """) + .ValidateAsync(); + } + + [Fact] + public void Rule_SeverityAndDefault() + { + var rule = new UseHasFlagMethodAnalyzer().SupportedDiagnostics[0]; + Assert.Equal(DiagnosticSeverity.Info, rule.DefaultSeverity); + Assert.False(rule.IsEnabledByDefault); + } +}