diff --git a/README.md b/README.md index 96dd5830f..6112f70d4 100755 --- a/README.md +++ b/README.md @@ -219,6 +219,7 @@ If you are already using other analyzers, you can check [which rules are duplica |[MA0198](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0198.md)|Design|Specify cref for ambiguous inheritdoc on types|⚠️|✔️|✔️| |[MA0199](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0199.md)|Design|Do not use inheritdoc on types without inheritance source|⚠️|✔️|❌| |[MA0200](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0200.md)|Usage|Do not use empty property patterns with non-nullable value types|ℹ️|✔️|✔️| +|[MA0201](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0201.md)|Usage|Do not use zero-valued enum flags in flag checks|⚠️|✔️|❌| diff --git a/docs/README.md b/docs/README.md index c62941ee2..e4320c0f1 100755 --- a/docs/README.md +++ b/docs/README.md @@ -199,6 +199,7 @@ |[MA0198](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0198.md)|Design|Specify cref for ambiguous inheritdoc on types|⚠️|✔️|✔️| |[MA0199](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0199.md)|Design|Do not use inheritdoc on types without inheritance source|⚠️|✔️|❌| |[MA0200](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0200.md)|Usage|Do not use empty property patterns with non-nullable value types|ℹ️|✔️|✔️| +|[MA0201](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0201.md)|Usage|Do not use zero-valued enum flags in flag checks|⚠️|✔️|❌| |Id|Suppressed rule|Justification| |--|---------------|-------------| @@ -811,6 +812,9 @@ dotnet_diagnostic.MA0199.severity = warning # MA0200: Do not use empty property patterns with non-nullable value types dotnet_diagnostic.MA0200.severity = suggestion + +# MA0201: Do not use zero-valued enum flags in flag checks +dotnet_diagnostic.MA0201.severity = warning ``` # .editorconfig - all rules disabled @@ -1409,4 +1413,7 @@ dotnet_diagnostic.MA0199.severity = none # MA0200: Do not use empty property patterns with non-nullable value types dotnet_diagnostic.MA0200.severity = none + +# MA0201: Do not use zero-valued enum flags in flag checks +dotnet_diagnostic.MA0201.severity = none ``` diff --git a/docs/Rules/MA0192.md b/docs/Rules/MA0192.md index a2202fa8e..cdffb96af 100644 --- a/docs/Rules/MA0192.md +++ b/docs/Rules/MA0192.md @@ -18,6 +18,8 @@ This rule reports patterns such as: For comparisons against `0`, the enum member used in the bitwise `&` must be a single-bit value (for example `1`, `2`, `4`, `8`, ...). Combined values are ignored. +Zero-valued enum members are not reported by this rule. They are covered by [MA0201](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0201.md). + ## Non-compliant code ````csharp diff --git a/docs/Rules/MA0201.md b/docs/Rules/MA0201.md new file mode 100644 index 000000000..201423b34 --- /dev/null +++ b/docs/Rules/MA0201.md @@ -0,0 +1,46 @@ +# MA0201 - Do not use zero-valued enum flags in flag checks + +Source: [UseHasFlagMethodAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/UseHasFlagMethodAnalyzer.cs) + + +Using a zero-valued enum member (`None = 0`) as a flag to test always produces a constant result. + +This rule reports patterns such as: + +- `(value & MyEnum.None) == MyEnum.None` (always `true`) +- `(value & MyEnum.None) != MyEnum.None` (always `false`) +- `(value & MyEnum.None) is MyEnum.None` (always `true`) +- `(value & MyEnum.None) is not MyEnum.None` (always `false`) +- `value.HasFlag(MyEnum.None)` (always `true`) + +## Non-compliant code + +````csharp +[System.Flags] +enum MyEnum +{ + None = 0, + Flag1 = 1, +} + +bool M(MyEnum value) +{ + return (value & MyEnum.None) == MyEnum.None; +} +```` + +## Compliant code + +````csharp +[System.Flags] +enum MyEnum +{ + None = 0, + Flag1 = 1, +} + +bool M(MyEnum value) +{ + return value.HasFlag(MyEnum.Flag1); +} +```` diff --git a/src/Meziantou.Analyzer.Pack/configuration/all-errors.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/all-errors.editorconfig index 0a0867114..65b502cbc 100644 --- a/src/Meziantou.Analyzer.Pack/configuration/all-errors.editorconfig +++ b/src/Meziantou.Analyzer.Pack/configuration/all-errors.editorconfig @@ -595,3 +595,6 @@ dotnet_diagnostic.MA0199.severity = error # MA0200: Do not use empty property patterns with non-nullable value types dotnet_diagnostic.MA0200.severity = error + +# MA0201: Do not use zero-valued enum flags in flag checks +dotnet_diagnostic.MA0201.severity = error diff --git a/src/Meziantou.Analyzer.Pack/configuration/all-suggestions.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/all-suggestions.editorconfig index 95de6e5a3..09aa7c3fe 100644 --- a/src/Meziantou.Analyzer.Pack/configuration/all-suggestions.editorconfig +++ b/src/Meziantou.Analyzer.Pack/configuration/all-suggestions.editorconfig @@ -595,3 +595,6 @@ dotnet_diagnostic.MA0199.severity = suggestion # MA0200: Do not use empty property patterns with non-nullable value types dotnet_diagnostic.MA0200.severity = suggestion + +# MA0201: Do not use zero-valued enum flags in flag checks +dotnet_diagnostic.MA0201.severity = suggestion diff --git a/src/Meziantou.Analyzer.Pack/configuration/all-warnings.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/all-warnings.editorconfig index 8661f041d..fea58ce76 100644 --- a/src/Meziantou.Analyzer.Pack/configuration/all-warnings.editorconfig +++ b/src/Meziantou.Analyzer.Pack/configuration/all-warnings.editorconfig @@ -595,3 +595,6 @@ dotnet_diagnostic.MA0199.severity = warning # MA0200: Do not use empty property patterns with non-nullable value types dotnet_diagnostic.MA0200.severity = warning + +# MA0201: Do not use zero-valued enum flags in flag checks +dotnet_diagnostic.MA0201.severity = warning diff --git a/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig index b6bd05b52..b17b312ab 100644 --- a/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig +++ b/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig @@ -595,3 +595,6 @@ dotnet_diagnostic.MA0199.severity = warning # MA0200: Do not use empty property patterns with non-nullable value types dotnet_diagnostic.MA0200.severity = suggestion + +# MA0201: Do not use zero-valued enum flags in flag checks +dotnet_diagnostic.MA0201.severity = warning diff --git a/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig index 9f50e81ff..32beb6a64 100644 --- a/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig +++ b/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig @@ -595,3 +595,6 @@ dotnet_diagnostic.MA0199.severity = none # MA0200: Do not use empty property patterns with non-nullable value types dotnet_diagnostic.MA0200.severity = none + +# MA0201: Do not use zero-valued enum flags in flag checks +dotnet_diagnostic.MA0201.severity = none diff --git a/src/Meziantou.Analyzer/RuleIdentifiers.cs b/src/Meziantou.Analyzer/RuleIdentifiers.cs index 6f7317178..a33eca897 100755 --- a/src/Meziantou.Analyzer/RuleIdentifiers.cs +++ b/src/Meziantou.Analyzer/RuleIdentifiers.cs @@ -200,6 +200,7 @@ internal static class RuleIdentifiers public const string InheritdocShouldNotBeAmbiguousOnTypes = "MA0198"; public const string InheritdocShouldHaveSourceOnTypes = "MA0199"; public const string DoNotUseEmptyPropertyPatternOnNonNullableValueType = "MA0200"; + public const string DoNotUseZeroValuedEnumFlagsInFlagChecks = "MA0201"; public static string GetHelpUri(string identifier) { diff --git a/src/Meziantou.Analyzer/Rules/UseHasFlagMethodAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseHasFlagMethodAnalyzer.cs index 850612796..a42e33dfd 100644 --- a/src/Meziantou.Analyzer/Rules/UseHasFlagMethodAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/UseHasFlagMethodAnalyzer.cs @@ -9,7 +9,9 @@ namespace Meziantou.Analyzer.Rules; [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class UseHasFlagMethodAnalyzer : DiagnosticAnalyzer { - private static readonly DiagnosticDescriptor Rule = new( + private const string EnumHasFlagMethodDocumentationId = "M:System.Enum.HasFlag(System.Enum)"; + + private static readonly DiagnosticDescriptor UseHasFlagRule = new( RuleIdentifiers.UseHasFlagMethod, title: "Use HasFlag instead of bitwise checks", messageFormat: "Use HasFlag instead of bitwise checks", @@ -19,76 +21,249 @@ public sealed class UseHasFlagMethodAnalyzer : DiagnosticAnalyzer description: "", helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.UseHasFlagMethod)); - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + private static readonly DiagnosticDescriptor DoNotUseZeroValuedEnumFlagsInFlagChecksRule = new( + RuleIdentifiers.DoNotUseZeroValuedEnumFlagsInFlagChecks, + title: "Do not use zero-valued enum flags in flag checks", + messageFormat: "This flag check is always '{0}' because the checked flag value is 0", + RuleCategories.Usage, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "", + helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.DoNotUseZeroValuedEnumFlagsInFlagChecks)); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(UseHasFlagRule, DoNotUseZeroValuedEnumFlagsInFlagChecksRule); public override void Initialize(AnalysisContext context) { context.EnableConcurrentExecution(); context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterOperationAction(AnalyzeBinary, OperationKind.Binary); - context.RegisterOperationAction(AnalyzeIsPattern, OperationKind.IsPattern); + context.RegisterCompilationStartAction(compilationContext => + { + var hasFlagMethod = DocumentationCommentId.GetFirstSymbolForDeclarationId(EnumHasFlagMethodDocumentationId, compilationContext.Compilation) as IMethodSymbol; + + compilationContext.RegisterOperationAction(AnalyzeBinary, OperationKind.Binary); + compilationContext.RegisterOperationAction(AnalyzeIsPattern, OperationKind.IsPattern); + + if (hasFlagMethod is not null) + { + compilationContext.RegisterOperationAction(context => AnalyzeInvocation(context, hasFlagMethod), OperationKind.Invocation); + } + }); } private static void AnalyzeBinary(OperationAnalysisContext context) { var operation = (IBinaryOperation)context.Operation; - if (operation.OperatorKind is BinaryOperatorKind.Equals or BinaryOperatorKind.NotEquals && - TryGetHasFlagPattern(operation, out _)) + if (operation.OperatorKind is not (BinaryOperatorKind.Equals or BinaryOperatorKind.NotEquals)) + return; + + if (TryGetZeroValuedFlagPattern(operation, out var isAlwaysTrue)) + { + context.ReportDiagnostic(DoNotUseZeroValuedEnumFlagsInFlagChecksRule, operation, isAlwaysTrue ? "true" : "false"); + return; + } + + if (TryGetHasFlagPattern(operation, out _)) { - context.ReportDiagnostic(Rule, operation); + context.ReportDiagnostic(UseHasFlagRule, operation); } } private static void AnalyzeIsPattern(OperationAnalysisContext context) { var operation = (IIsPatternOperation)context.Operation; + if (TryGetZeroValuedFlagPattern(operation, out var isAlwaysTrue)) + { + context.ReportDiagnostic(DoNotUseZeroValuedEnumFlagsInFlagChecksRule, operation, isAlwaysTrue ? "true" : "false"); + return; + } + if (TryGetHasFlagPattern(operation, out _)) { - context.ReportDiagnostic(Rule, operation); + context.ReportDiagnostic(UseHasFlagRule, operation); } } - private static bool TryGetHasFlagPattern(IOperation operation, [NotNullWhen(true)] out HasFlagPattern? pattern) + private static void AnalyzeInvocation(OperationAnalysisContext context, IMethodSymbol hasFlagMethod) + { + var operation = (IInvocationOperation)context.Operation; + if (!IsZeroValuedHasFlagInvocation(operation, hasFlagMethod)) + return; + + context.ReportDiagnostic(DoNotUseZeroValuedEnumFlagsInFlagChecksRule, operation, "true"); + } + + private static bool TryGetZeroValuedFlagPattern(IOperation operation, out bool isAlwaysTrue) { if (operation is IBinaryOperation { OperatorKind: BinaryOperatorKind.Equals or BinaryOperatorKind.NotEquals } binaryOperation) { - pattern = GetFromBinaryComparison(binaryOperation); - return pattern is not null; + if (TryGetZeroValuedFlagPattern(binaryOperation)) + { + isAlwaysTrue = binaryOperation.OperatorKind is BinaryOperatorKind.Equals; + return true; + } + } + else if (operation is IIsPatternOperation + { + Value: IBinaryOperation { OperatorKind: BinaryOperatorKind.And } andOperation, + Pattern: var patternOperation, + } && + TryGetComparedOperand(patternOperation, out var comparedOperand, out var negateResult) && + IsZeroValuedFlagCheck(andOperation, comparedOperand)) + { + isAlwaysTrue = !negateResult; + return true; } - if (operation is IIsPatternOperation - { - Value: IBinaryOperation { OperatorKind: BinaryOperatorKind.And } andOperation, - Pattern: IPatternOperation patternOperation, - }) + isAlwaysTrue = false; + return false; + } + + private static bool TryGetZeroValuedFlagPattern(IBinaryOperation operation) + { + var leftOperand = operation.LeftOperand.UnwrapImplicitConversionOperations(); + var rightOperand = operation.RightOperand.UnwrapImplicitConversionOperations(); + if (leftOperand is IBinaryOperation { OperatorKind: BinaryOperatorKind.And } leftBitwiseAnd && + IsZeroValuedFlagCheck(leftBitwiseAnd, rightOperand)) { - if (TryGetComparedOperand(patternOperation, out var comparedOperand)) - { - pattern = GetFromBitwiseAnd(andOperation, comparedOperand); - return pattern is not null; - } + return true; + } + + if (rightOperand is IBinaryOperation { OperatorKind: BinaryOperatorKind.And } rightBitwiseAnd && + IsZeroValuedFlagCheck(rightBitwiseAnd, leftOperand)) + { + return true; + } + + return false; + } + + private static bool IsZeroValuedFlagCheck(IBinaryOperation bitwiseAndOperation, IOperation comparedOperand) + { + var leftOperand = bitwiseAndOperation.LeftOperand.UnwrapImplicitConversionOperations(); + var rightOperand = bitwiseAndOperation.RightOperand.UnwrapImplicitConversionOperations(); + comparedOperand = comparedOperand.UnwrapImplicitConversionOperations(); + if (TryGetZeroValuedEnumFlagType(rightOperand, comparedOperand, out var enumType) && + IsValidPattern(leftOperand, enumType)) + { + return true; + } + + if (TryGetZeroValuedEnumFlagType(leftOperand, comparedOperand, out enumType) && + IsValidPattern(rightOperand, enumType)) + { + return true; } - pattern = null; return false; } - private static bool TryGetComparedOperand(IPatternOperation patternOperation, [NotNullWhen(true)] out IOperation? comparedOperand) + private static bool TryGetZeroValuedEnumFlagType(IOperation potentialFlag, IOperation comparedOperand, out ITypeSymbol enumType) + { + potentialFlag = potentialFlag.UnwrapImplicitConversionOperations(); + comparedOperand = comparedOperand.UnwrapImplicitConversionOperations(); + if (potentialFlag is not IFieldReferenceOperation firstFieldReference || + !firstFieldReference.Field.HasConstantValue || + !firstFieldReference.Field.ContainingType.IsEnumeration() || + !NumericHelpers.IsZero(firstFieldReference.Field.ConstantValue)) + { + enumType = null!; + return false; + } + + if (!IsComparedOperandZero(comparedOperand, firstFieldReference.Field.ContainingType)) + { + enumType = null!; + return false; + } + + enumType = firstFieldReference.Field.ContainingType; + return true; + } + + private static bool IsComparedOperandZero(IOperation comparedOperand, ITypeSymbol enumType) + { + comparedOperand = comparedOperand.UnwrapImplicitConversionOperations(); + if (comparedOperand is IFieldReferenceOperation comparedFieldReference && + comparedFieldReference.Field.HasConstantValue && + comparedFieldReference.Field.ContainingType.IsEqualTo(enumType) && + NumericHelpers.IsZero(comparedFieldReference.Field.ConstantValue)) + { + return true; + } + + if (comparedOperand.IsConstantZero()) + return true; + + if (comparedOperand.Type?.IsEqualTo(enumType) is true && + comparedOperand.ConstantValue.HasValue && + NumericHelpers.IsZero(comparedOperand.ConstantValue.Value)) + { + return true; + } + + return false; + } + + private static bool IsZeroValuedHasFlagInvocation(IInvocationOperation operation, IMethodSymbol hasFlagMethod) + { + if (!operation.TargetMethod.OriginalDefinition.IsEqualTo(hasFlagMethod)) + return false; + + if (operation.Arguments.Length is not 1 || operation.Instance is null) + return false; + + var enumValueOperation = operation.Instance.UnwrapImplicitConversionOperations(); + if (enumValueOperation.Type is null || !enumValueOperation.Type.IsEnumeration()) + return false; + + return IsComparedOperandZero(operation.Arguments[0].Value, enumValueOperation.Type); + } + + private static bool TryGetComparedOperand(IPatternOperation patternOperation, [NotNullWhen(true)] out IOperation? comparedOperand, out bool negateResult) { if (patternOperation is IConstantPatternOperation { Value: not null } constantPattern) { comparedOperand = constantPattern.Value; + negateResult = false; return true; } if (patternOperation is INegatedPatternOperation { Pattern: IConstantPatternOperation { Value: not null } negatedConstantPattern }) { comparedOperand = negatedConstantPattern.Value; + negateResult = true; return true; } comparedOperand = null; + negateResult = false; + 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, + }) + { + if (TryGetComparedOperand(patternOperation, out var comparedOperand, out _)) + { + pattern = GetFromBitwiseAnd(andOperation, comparedOperand); + return pattern is not null; + } + } + + pattern = null; return false; } @@ -146,7 +321,8 @@ private static bool TryGetEnumFlagReference(IOperation potentialFlag, IOperation { if (comparedOperand is IFieldReferenceOperation secondFieldReference && secondFieldReference.Field.HasConstantValue && - firstFieldReference.Field.IsEqualTo(secondFieldReference.Field)) + firstFieldReference.Field.IsEqualTo(secondFieldReference.Field) && + !NumericHelpers.IsZero(firstFieldReference.Field.ConstantValue)) { flagOperation = secondFieldReference; return true; @@ -165,16 +341,24 @@ private static bool TryGetEnumFlagReference(IOperation potentialFlag, IOperation private static bool IsValidPattern(IOperation enumValueOperation, IOperation flagOperation) { - if (enumValueOperation.Type is null || flagOperation.Type is null) + if (flagOperation.Type is null) + return false; + + return IsValidPattern(enumValueOperation, flagOperation.Type); + } + + private static bool IsValidPattern(IOperation enumValueOperation, ITypeSymbol flagType) + { + if (enumValueOperation.Type is null) return false; if (!enumValueOperation.Type.IsEnumeration()) return false; - if (!flagOperation.Type.IsEnumeration()) + if (!flagType.IsEnumeration()) return false; - return enumValueOperation.Type.IsEqualTo(flagOperation.Type); + return enumValueOperation.Type.IsEqualTo(flagType); } 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 index dbb48ff87..b1cbde114 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/UseHasFlagMethodAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/UseHasFlagMethodAnalyzerTests.cs @@ -1,5 +1,4 @@ using Meziantou.Analyzer.Rules; -using Microsoft.CodeAnalysis; using TestHelper; namespace Meziantou.Analyzer.Test.Rules; @@ -28,7 +27,7 @@ enum MyEnum class Sample { - bool M(MyEnum value) => [|(value & MyEnum.Flag1) == MyEnum.Flag1|]; + bool M(MyEnum value) => {|MA0192:(value & MyEnum.Flag1) == MyEnum.Flag1|}; } """) .ShouldFixCodeWith(""" @@ -62,7 +61,7 @@ enum MyEnum class Sample { - bool M(MyEnum value) => [|(MyEnum.Flag1 & value) == MyEnum.Flag1|]; + bool M(MyEnum value) => {|MA0192:(MyEnum.Flag1 & value) == MyEnum.Flag1|}; } """) .ShouldFixCodeWith(""" @@ -96,7 +95,7 @@ enum MyEnum class Sample { - bool M(MyEnum value) => [|(value & MyEnum.Flag1) is MyEnum.Flag1|]; + bool M(MyEnum value) => {|MA0192:(value & MyEnum.Flag1) is MyEnum.Flag1|}; } """) .ShouldFixCodeWith(""" @@ -131,7 +130,7 @@ enum MyEnum class Sample { - bool M(MyEnum value) => [|(value & MyEnum.Flag1) != MyEnum.Flag1|]; + bool M(MyEnum value) => {|MA0192:(value & MyEnum.Flag1) != MyEnum.Flag1|}; } """) .ShouldFixCodeWith(""" @@ -166,7 +165,7 @@ enum MyEnum class Sample { - bool M(MyEnum value) => [|(value & MyEnum.Flag1) is not MyEnum.Flag1|]; + bool M(MyEnum value) => {|MA0192:(value & MyEnum.Flag1) is not MyEnum.Flag1|}; } """) .ShouldFixCodeWith(""" @@ -201,7 +200,7 @@ enum MyEnum class Sample { - bool M(MyEnum value) => [|(value & MyEnum.Flag1) == 0|]; + bool M(MyEnum value) => {|MA0192:(value & MyEnum.Flag1) == 0|}; } """) .ShouldFixCodeWith(""" @@ -236,7 +235,7 @@ enum MyEnum class Sample { - bool M(MyEnum value) => [|(value & MyEnum.Flag1) != 0|]; + bool M(MyEnum value) => {|MA0192:(value & MyEnum.Flag1) != 0|}; } """) .ShouldFixCodeWith(""" @@ -271,7 +270,7 @@ enum MyEnum class Sample { - bool M(MyEnum value) => [|(value & MyEnum.Flag1) is 0|]; + bool M(MyEnum value) => {|MA0192:(value & MyEnum.Flag1) is 0|}; } """) .ShouldFixCodeWith(""" @@ -306,7 +305,7 @@ enum MyEnum class Sample { - bool M(MyEnum value) => [|(value & MyEnum.Flag1) is not 0|]; + bool M(MyEnum value) => {|MA0192:(value & MyEnum.Flag1) is not 0|}; } """) .ShouldFixCodeWith(""" @@ -326,6 +325,146 @@ class Sample .ValidateAsync(); } + [Fact] + public async Task ZeroFlagEqualityCheck_ReportDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + [System.Flags] + enum MyEnum + { + None = 0, + Flag1 = 1, + } + + class Sample + { + bool M(MyEnum value) => {|MA0201:(value & MyEnum.None) == MyEnum.None|}; + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task ZeroLiteralEqualityCheck_ReportDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + [System.Flags] + enum MyEnum + { + None = 0, + Flag1 = 1, + } + + class Sample + { + bool M(MyEnum value) => {|MA0201:(value & MyEnum.None) == 0|}; + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task ZeroFlagNotEqualsCheck_ReportDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + [System.Flags] + enum MyEnum + { + None = 0, + Flag1 = 1, + } + + class Sample + { + bool M(MyEnum value) => {|MA0201:(value & MyEnum.None) != MyEnum.None|}; + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task ZeroFlagIsPatternCheck_ReportDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + [System.Flags] + enum MyEnum + { + None = 0, + Flag1 = 1, + } + + class Sample + { + bool M(MyEnum value) => {|MA0201:(value & MyEnum.None) is MyEnum.None|}; + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task ZeroFlagIsNotPatternCheck_ReportDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + [System.Flags] + enum MyEnum + { + None = 0, + Flag1 = 1, + } + + class Sample + { + bool M(MyEnum value) => {|MA0201:(value & MyEnum.None) is not MyEnum.None|}; + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task HasFlagZeroFlag_ReportDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + [System.Flags] + enum MyEnum + { + None = 0, + Flag1 = 1, + } + + class Sample + { + bool M(MyEnum value) => {|MA0201:value.HasFlag(MyEnum.None)|}; + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task HasFlagExplicitZeroCast_ReportDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + [System.Flags] + enum MyEnum + { + None = 0, + Flag1 = 1, + } + + class Sample + { + bool M(MyEnum value) => {|MA0201:value.HasFlag((MyEnum)0)|}; + } + """) + .ValidateAsync(); + } + [Fact] public async Task DifferentFlag_NoDiagnostic() { @@ -347,6 +486,51 @@ class Sample .ValidateAsync(); } + [Fact] + public async Task HasFlagsExtensionZeroFlag_NoDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + [System.Flags] + enum MyEnum + { + None = 0, + Flag1 = 1, + } + + static class MyEnumExtensions + { + public static bool HasFlags(this MyEnum value, MyEnum flags) => (value & flags) == flags; + } + + class Sample + { + bool M(MyEnum value) => value.HasFlags(MyEnum.None); + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task NonZeroHasFlag_NoDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + [System.Flags] + enum MyEnum + { + None = 0, + Flag1 = 1, + } + + class Sample + { + bool M(MyEnum value) => value.HasFlag(MyEnum.Flag1); + } + """) + .ValidateAsync(); + } + [Fact] public async Task CombinedFlag_NotEqualsZero_NoDiagnostic() { @@ -423,11 +607,4 @@ class Sample .ValidateAsync(); } - [Fact] - public void Rule_SeverityAndDefault() - { - var rule = new UseHasFlagMethodAnalyzer().SupportedDiagnostics[0]; - Assert.Equal(DiagnosticSeverity.Info, rule.DefaultSeverity); - Assert.False(rule.IsEnabledByDefault); - } }