diff --git a/docs/Rules/MA0062.md b/docs/Rules/MA0062.md index ddecb8b74..b343936e2 100644 --- a/docs/Rules/MA0062.md +++ b/docs/Rules/MA0062.md @@ -12,3 +12,23 @@ public enum Color Yellow = 4, } ```` + +# Configuration + +In the following case, `All` is not a power of 2 and not a combination of other values. However, this construct can be used to easily defined a value that contains all other flags. + +```` +[Flags] +public enum MyEnum +{ + None = 0, + Option1 = 1, + All = ~None, +} +```` + +You can allow this pattern by adding the following configuration to the `.editorconfig` file: + +```` +MA0062.allow_all_bits_set_value = true +```` diff --git a/src/Meziantou.Analyzer/Rules/NonFlagsEnumsShouldNotBeMarkedWithFlagsAttributeAnalyzer.cs b/src/Meziantou.Analyzer/Rules/NonFlagsEnumsShouldNotBeMarkedWithFlagsAttributeAnalyzer.cs index fe1e329e0..dd71c4847 100644 --- a/src/Meziantou.Analyzer/Rules/NonFlagsEnumsShouldNotBeMarkedWithFlagsAttributeAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/NonFlagsEnumsShouldNotBeMarkedWithFlagsAttributeAnalyzer.cs @@ -2,6 +2,7 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; +using Meziantou.Analyzer.Configurations; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; @@ -45,56 +46,62 @@ private static void AnalyzeSymbol(SymbolAnalysisContext context) var members = symbol.GetMembers() .OfType() .Where(member => member.ConstantValue != null) - .Select(member => (member, IsPowerOfTwo: IsPowerOfTwo(member.ConstantValue!))) - .ToList(); - foreach (var member in members.Where(member => !member.IsPowerOfTwo)) + .Select(member => (member, IsSingleBitSet: IsSingleBitSet(member.ConstantValue), IsZero: IsZero(member.ConstantValue))) + .ToArray(); + foreach (var member in members) { - var value = member.member.ConstantValue; - if (value != null) + if (member.IsSingleBitSet || member.IsZero) + continue; + + if (IsAllBitsSet(member.member.ConstantValue) && context.Options.GetConfigurationValue(member.member, RuleIdentifiers.NonFlagsEnumsShouldNotBeMarkedWithFlagsAttribute + ".allow_all_bits_set_value", defaultValue: false)) + continue; + + var value = member.member.ConstantValue!; + foreach (var otherMember in members) { - foreach (var powerOfTwo in members.Where(member => member.IsPowerOfTwo)) - { - if (powerOfTwo.member.ConstantValue != null) - { - value = RemoveValue(value, powerOfTwo.member.ConstantValue); - } - } + if (!otherMember.IsSingleBitSet) + continue; - if (!IsZero(value)) + if (otherMember.member.ConstantValue != null) { - context.ReportDiagnostic(s_rule, symbol, member.member.Name); - return; + value = RemoveValue(value, otherMember.member.ConstantValue); } } + + if (!IsZero(value)) + { + context.ReportDiagnostic(s_rule, symbol, member.member.Name); + return; + } } } - private static bool IsPowerOfTwo(object o) + private static bool IsSingleBitSet(object? o) { // https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/enum // The approved types for an enum are byte, sbyte, short, ushort, int, uint, long, or ulong. return o switch { - null => throw new ArgumentOutOfRangeException(nameof(o), "null is not a valid value"), - byte x => (x == 0) || ((x & (x - 1)) == 0), - sbyte x => (x == 0) || ((x & (x - 1)) == 0), - short x => (x == 0) || ((x & (x - 1)) == 0), - ushort x => (x == 0) || ((x & (x - 1)) == 0), - int x => (x == 0) || ((x & (x - 1)) == 0), - uint x => (x == 0) || ((x & (x - 1)) == 0), - long x => (x == 0) || ((x & (x - 1)) == 0), - ulong x => (x == 0) || ((x & (x - 1)) == 0), + null => false, + sbyte x => IsSingleBitSet((byte)x), + byte x => x > 0 && (x & (x - 1)) == 0, + short x => IsSingleBitSet((ushort)x), + ushort x => x > 0 && (x & (x - 1)) == 0, + int x => IsSingleBitSet((uint)x), + uint x => x > 0 && (x & (x - 1)) == 0, + long x => IsSingleBitSet((ulong)x), + ulong x => x > 0 && (x & (x - 1)) == 0, _ => throw new ArgumentOutOfRangeException(nameof(o), $"Type {o.GetType().FullName} is not supported"), }; } - private static bool IsZero(object o) + private static bool IsZero(object? o) { // https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/enum // The approved types for an enum are byte, sbyte, short, ushort, int, uint, long, or ulong. return o switch { - null => throw new ArgumentOutOfRangeException(nameof(o), "null is not a valid value"), + null => false, byte x => x == 0, sbyte x => x == 0, short x => x == 0, @@ -107,6 +114,25 @@ private static bool IsZero(object o) }; } + private static bool IsAllBitsSet(object? o) + { + // https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/enum + // The approved types for an enum are byte, sbyte, short, ushort, int, uint, long, or ulong. + return o switch + { + null => false, + sbyte x => x == -1, + byte x => x == 0xFF, + short x => x == -1, + ushort x => x == 0xFFFF, + int x => x == -1, + uint x => x == 0xFFFF_FFFF, + long x => x == -1, + ulong x => x == 0xFFFF_FFFF_FFFF_FFFF, + _ => throw new ArgumentOutOfRangeException(nameof(o), $"Type {o.GetType().FullName} is not supported"), + }; + } + [SuppressMessage("Style", "IDE0004:Remove Unnecessary Cast", Justification = "Clearer")] private static object RemoveValue(object o, object valueToRemove) { diff --git a/tests/Meziantou.Analyzer.Test/Rules/NonFlagsEnumsShouldNotBeMarkedWithFlagsAttributeAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/NonFlagsEnumsShouldNotBeMarkedWithFlagsAttributeAnalyzerTests.cs index cac88035e..e53791d3b 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/NonFlagsEnumsShouldNotBeMarkedWithFlagsAttributeAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/NonFlagsEnumsShouldNotBeMarkedWithFlagsAttributeAnalyzerTests.cs @@ -94,4 +94,63 @@ await CreateProjectBuilder() .WithSourceCode(sourceCode) .ValidateAsync(); } + + [Fact] + public async Task PowerOfTwo_NegativeValue_Sbyte() + { + var sourceCode = $$""" +[System.Flags] +enum Test : sbyte +{ + None = 0, + Option01 = 1, + Option02 = 2, + Option03 = 4, + Option04 = 8, + Option05 = 16, + Option06 = 32, + Option07 = 64, + Option08 = -128, + All = ~None, +} +"""; + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task AllBitSet_WithoutConfiguration() + { + var sourceCode = $$""" +[System.Flags] +enum [||]Test +{ + None = 0, + Option1 = 1, + All = ~None, +} +"""; + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task AllBitSet_WithConfiguration() + { + var sourceCode = """ +[System.Flags] +enum Test +{ + None = 0, + Option1 = 1, + All = ~None, +} +"""; + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .AddAnalyzerConfiguration("MA0062.allow_all_bits_set_value", "true") + .ValidateAsync(); + } }