diff --git a/README.md b/README.md index 3b45b9a..7ea1cba 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,13 @@ public static partial class MyEnumExtensions } ``` +If you create a "Flags" `enum` by decorating it with the `[Flags]` attribute, an additional method is created, which provides a bitwise alternative to : + +```csharp +public static bool HasFlagFast(this MyEnum value, MyEnum flag) + => flag == 0 ? true : (value & flag) == flag; +``` + You can override the name of the extension class by setting `ExtensionClassName` in the attribute and/or the namespace of the class by setting `ExtensionClassNamespace`. By default, the class will be public if the enum is public, otherwise it will be internal. ## Embedding the attributes in your project diff --git a/src/NetEscapades.EnumGenerators/EnumGenerator.cs b/src/NetEscapades.EnumGenerators/EnumGenerator.cs index 0efd6cf..1569890 100644 --- a/src/NetEscapades.EnumGenerators/EnumGenerator.cs +++ b/src/NetEscapades.EnumGenerators/EnumGenerator.cs @@ -10,7 +10,7 @@ public class EnumGenerator : IIncrementalGenerator { private const string DisplayAttribute = "System.ComponentModel.DataAnnotations.DisplayAttribute"; private const string EnumExtensionsAttribute = "NetEscapades.EnumGenerators.EnumExtensionsAttribute"; - private const string HasFlagsAttribute = "System.HasFlagsAttribute"; + private const string FlagsAttribute = "System.FlagsAttribute"; public void Initialize(IncrementalGeneratorInitializationContext context) { @@ -55,8 +55,9 @@ static void Execute(in EnumToGenerate? enumToGenerate, SourceProductionContext c foreach (AttributeData attributeData in enumSymbol.GetAttributes()) { - if (attributeData.AttributeClass?.Name == "HasFlagsAttribute" && - attributeData.AttributeClass.ToDisplayString() == HasFlagsAttribute) + if ((attributeData.AttributeClass?.Name == "FlagsAttribute" || + attributeData.AttributeClass?.Name == "Flags") && + attributeData.AttributeClass.ToDisplayString() == FlagsAttribute) { hasFlags = true; continue; diff --git a/src/NetEscapades.EnumGenerators/SourceGenerationHelper.cs b/src/NetEscapades.EnumGenerators/SourceGenerationHelper.cs index e547835..0a94077 100644 --- a/src/NetEscapades.EnumGenerators/SourceGenerationHelper.cs +++ b/src/NetEscapades.EnumGenerators/SourceGenerationHelper.cs @@ -110,17 +110,15 @@ public static string ToStringFast(this ").Append(enumToGenerate.FullyQualifiedNa /// /// Determines whether one or more bit fields are set in the current instance. - /// Equivalent to calling value.HasFlag(flag) + /// Equivalent to calling on . /// /// The value of the instance to investiage /// The flag to check for /// true if the fields set in the flag are also set in the current instance; otherwise false. - public static bool HasFlag(this ").Append(enumToGenerate.FullyQualifiedName).Append(@" value, ").Append(enumToGenerate.FullyQualifiedName).Append(@" flag) - => value switch - { - 0 => flag.Equals(0), - _ => (value & flag) != 0, - };"); + /// If the underlying value of is zero, the method returns true. + /// This is consistent with the behaviour of + public static bool HasFlagFast(this ").Append(enumToGenerate.FullyQualifiedName).Append(@" value, ").Append(enumToGenerate.FullyQualifiedName).Append(@" flag) + => flag == 0 ? true : (value & flag) == flag;"); } sb.Append(@" diff --git a/tests/NetEscapades.EnumGenerators.IntegrationTests/FlagsEnumExtensionsTests.cs b/tests/NetEscapades.EnumGenerators.IntegrationTests/FlagsEnumExtensionsTests.cs index e2cec8e..a1b870d 100644 --- a/tests/NetEscapades.EnumGenerators.IntegrationTests/FlagsEnumExtensionsTests.cs +++ b/tests/NetEscapades.EnumGenerators.IntegrationTests/FlagsEnumExtensionsTests.cs @@ -1,5 +1,7 @@ using FluentAssertions; using System; +using System.Collections.Generic; +using System.Linq; using Xunit; namespace NetEscapades.EnumGenerators.IntegrationTests; @@ -44,6 +46,12 @@ protected override bool TryParse(in ReadOnlySpan name, out FlagsEnum parse => FlagsEnumExtensions.TryParse(name, out parsed, ignoreCase); #endif + /// + /// + /// + /// + /// If the underlying value of is zero, the method returns true. + /// This is consistent with the behaviour of [Theory] [MemberData(nameof(ValidEnumValues))] public void GeneratesToStringFast(FlagsEnum value) => GeneratesToStringFastTest(value); @@ -62,16 +70,29 @@ protected override bool TryParse(in ReadOnlySpan name, out FlagsEnum parse public void GeneratesIsDefinedUsingNameAsSpan(string name) => GeneratesIsDefinedTest(name.AsSpan(), allowMatchingMetadataAttribute: false); #endif + public static IEnumerable AllFlags() + { + var values = new[] + { + FlagsEnum.First, + FlagsEnum.Second, + FlagsEnum.Third, + FlagsEnum.ThirdAndFourth, + FlagsEnum.First | FlagsEnum.Second, + (FlagsEnum)65, + (FlagsEnum)0, + }; + + return from v1 in values + from v2 in values + select new object[] { v1, v2 }; + } + [Theory] - [InlineData(FlagsEnum.First)] - [InlineData(FlagsEnum.Second)] - [InlineData(FlagsEnum.First | FlagsEnum.Second)] - [InlineData(FlagsEnum.Third)] - [InlineData((FlagsEnum)65)] - public void HasFlags(FlagsEnum value) + [MemberData(nameof(AllFlags))] + public void HasFlags(FlagsEnum value, FlagsEnum flag) { - var flag = FlagsEnum.Second; - var isDefined = value.HasFlag(flag); + var isDefined = value.HasFlagFast(flag); isDefined.Should().Be(value.HasFlag(flag)); } diff --git a/tests/NetEscapades.EnumGenerators.Tests/EnumGeneratorTests.cs b/tests/NetEscapades.EnumGenerators.Tests/EnumGeneratorTests.cs index 149cc86..32cadaa 100644 --- a/tests/NetEscapades.EnumGenerators.Tests/EnumGeneratorTests.cs +++ b/tests/NetEscapades.EnumGenerators.Tests/EnumGeneratorTests.cs @@ -188,4 +188,36 @@ public enum MyEnum Assert.Empty(diagnostics); return Verifier.Verify(output).UseDirectory("Snapshots"); } + + [Theory] + [InlineData("", "System.Flags")] + [InlineData("", "System.FlagsAttribute")] + [InlineData("using System;", "FlagsAttribute")] + [InlineData("using System;", "Flags")] + public Task CanGenerateEnumExtensionsForFlagsEnum(string usings, string attribute) + { + string input = $$""" + using NetEscapades.EnumGenerators; + {{usings}} + + namespace MyTestNameSpace + { + [EnumExtensions, {{attribute}}] + public enum MyEnum + { + First = 1, + Second = 2, + Third = 4, + } + } + """; + + var (diagnostics, output) = TestHelpers.GetGeneratedOutput(input); + + Assert.Empty(diagnostics); + return Verifier.Verify(output) + .UseTextForParameters("Params") + .DisableRequireUniquePrefix() + .UseDirectory("Snapshots"); + } } \ No newline at end of file diff --git a/tests/NetEscapades.EnumGenerators.Tests/Snapshots/EnumGeneratorTests.CanGenerateEnumExtensionsForFlagsEnum_Params.verified.txt b/tests/NetEscapades.EnumGenerators.Tests/Snapshots/EnumGeneratorTests.CanGenerateEnumExtensionsForFlagsEnum_Params.verified.txt new file mode 100644 index 0000000..20f14a5 --- /dev/null +++ b/tests/NetEscapades.EnumGenerators.Tests/Snapshots/EnumGeneratorTests.CanGenerateEnumExtensionsForFlagsEnum_Params.verified.txt @@ -0,0 +1,384 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by the NetEscapades.EnumGenerators source generator +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable +#if NETCOREAPP && !NETCOREAPP2_0 && !NETCOREAPP1_1 && !NETCOREAPP1_0 +using System; +#endif + +namespace MyTestNameSpace +{ + /// + /// Extension methods for + /// + public static partial class MyEnumExtensions + { + /// + /// The number of members in the enum. + /// This is a non-distinct count of defined names. + /// + public const int Length = 3; + + /// + /// Returns the string representation of the value. + /// If the attribute is decorated with a , then + /// uses the provided value. Otherwise uses the name of the member, equivalent to + /// calling ToString() on . + /// + /// The value to retrieve the string value for + /// The string representation of the value + public static string ToStringFast(this MyTestNameSpace.MyEnum value) + => value switch + { + MyTestNameSpace.MyEnum.First => nameof(MyTestNameSpace.MyEnum.First), + MyTestNameSpace.MyEnum.Second => nameof(MyTestNameSpace.MyEnum.Second), + MyTestNameSpace.MyEnum.Third => nameof(MyTestNameSpace.MyEnum.Third), + _ => value.ToString(), + }; + + /// + /// Determines whether one or more bit fields are set in the current instance. + /// Equivalent to calling on . + /// + /// The value of the instance to investiage + /// The flag to check for + /// true if the fields set in the flag are also set in the current instance; otherwise false. + /// If the underlying value of is zero, the method returns true. + /// This is consistent with the behaviour of + public static bool HasFlagFast(this MyTestNameSpace.MyEnum value, MyTestNameSpace.MyEnum flag) + => flag == 0 ? true : (value & flag) == flag; + + /// + /// Returns a boolean telling whether the given enum value exists in the enumeration. + /// + /// The value to check if it's defined + /// true if the value exists in the enumeration, false otherwise + public static bool IsDefined(MyTestNameSpace.MyEnum value) + => value switch + { + MyTestNameSpace.MyEnum.First => true, + MyTestNameSpace.MyEnum.Second => true, + MyTestNameSpace.MyEnum.Third => true, + _ => false, + }; + + /// + /// Returns a boolean telling whether an enum with the given name exists in the enumeration. + /// + /// The name to check if it's defined + /// true if a member with the name exists in the enumeration, false otherwise + public static bool IsDefined(string name) => IsDefined(name, allowMatchingMetadataAttribute: false); + + /// + /// Returns a boolean telling whether an enum with the given name exists in the enumeration, + /// or if a member decorated with a + /// with the required name exists. + /// + /// The name to check if it's defined + /// If true, considers the value of metadata attributes,otherwise ignores them + /// true if a member with the name exists in the enumeration, or a member is decorated + /// with a with the name, false otherwise + public static bool IsDefined(string name, bool allowMatchingMetadataAttribute) + { + return name switch + { + nameof(MyTestNameSpace.MyEnum.First) => true, + nameof(MyTestNameSpace.MyEnum.Second) => true, + nameof(MyTestNameSpace.MyEnum.Third) => true, + _ => false, + }; + } + +#if NETCOREAPP && !NETCOREAPP2_0 && !NETCOREAPP1_1 && !NETCOREAPP1_0 + /// + /// Returns a boolean telling whether an enum with the given name exists in the enumeration + /// + /// The name to check if it's defined + /// true if a member with the name exists in the enumeration, false otherwise + public static bool IsDefined(in ReadOnlySpan name) => IsDefined(name, allowMatchingMetadataAttribute: false); + + /// + /// Returns a boolean telling whether an enum with the given name exists in the enumeration, + /// or optionally if a member decorated with a + /// with the required name exists. + /// Slower then the overload, but doesn't allocate memory./> + /// + /// The name to check if it's defined + /// If true, considers the value of metadata attributes,otherwise ignores them + /// true if a member with the name exists in the enumeration, or a member is decorated + /// with a with the name, false otherwise + public static bool IsDefined(in ReadOnlySpan name, bool allowMatchingMetadataAttribute) + { + return name switch + { + ReadOnlySpan current when current.Equals(nameof(MyTestNameSpace.MyEnum.First).AsSpan(), System.StringComparison.Ordinal) => true, + ReadOnlySpan current when current.Equals(nameof(MyTestNameSpace.MyEnum.Second).AsSpan(), System.StringComparison.Ordinal) => true, + ReadOnlySpan current when current.Equals(nameof(MyTestNameSpace.MyEnum.Third).AsSpan(), System.StringComparison.Ordinal) => true, + _ => false, + }; + } +#endif + + /// + /// Converts the string representation of the name or numeric value of + /// an to the equivalent instance. + /// The return value indicates whether the conversion succeeded. + /// + /// The case-sensitive string representation of the enumeration name or underlying value to convert + /// When this method returns, contains an object of type + /// whose + /// value is represented by if the parse operation succeeds. + /// If the parse operation fails, contains the default value of the underlying type + /// of . This parameter is passed uninitialized. + /// true if the value parameter was converted successfully; otherwise, false. + public static bool TryParse( +#if NETCOREAPP3_0_OR_GREATER + [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] +#endif + string? name, + out MyTestNameSpace.MyEnum value) + => TryParse(name, out value, false, false); + + /// + /// Converts the string representation of the name or numeric value of + /// an to the equivalent instance. + /// The return value indicates whether the conversion succeeded. + /// + /// The string representation of the enumeration name or underlying value to convert + /// When this method returns, contains an object of type + /// whose + /// value is represented by if the parse operation succeeds. + /// If the parse operation fails, contains the default value of the underlying type + /// of . This parameter is passed uninitialized. + /// true to read value in case insensitive mode; false to read value in case sensitive mode. + /// true if the value parameter was converted successfully; otherwise, false. + public static bool TryParse( +#if NETCOREAPP3_0_OR_GREATER + [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] +#endif + string? name, + out MyTestNameSpace.MyEnum value, + bool ignoreCase) + => TryParse(name, out value, ignoreCase, false); + + /// + /// Converts the string representation of the name or numeric value of + /// an to the equivalent instance. + /// The return value indicates whether the conversion succeeded. + /// + /// The string representation of the enumeration name or underlying value to convert + /// When this method returns, contains an object of type + /// whose + /// value is represented by if the parse operation succeeds. + /// If the parse operation fails, contains the default value of the underlying type + /// of . This parameter is passed uninitialized. + /// true to read value in case insensitive mode; false to read value in case sensitive mode. + /// If true, considers the value included in metadata attributes such as + /// when parsing, otherwise only considers the member names. + /// true if the value parameter was converted successfully; otherwise, false. + public static bool TryParse( +#if NETCOREAPP3_0_OR_GREATER + [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] +#endif + string? name, + out MyTestNameSpace.MyEnum value, + bool ignoreCase, + bool allowMatchingMetadataAttribute) + { + if (ignoreCase) + { + switch (name) + { + case string s when s.Equals(nameof(MyTestNameSpace.MyEnum.First), System.StringComparison.OrdinalIgnoreCase): + value = MyTestNameSpace.MyEnum.First; + return true; + case string s when s.Equals(nameof(MyTestNameSpace.MyEnum.Second), System.StringComparison.OrdinalIgnoreCase): + value = MyTestNameSpace.MyEnum.Second; + return true; + case string s when s.Equals(nameof(MyTestNameSpace.MyEnum.Third), System.StringComparison.OrdinalIgnoreCase): + value = MyTestNameSpace.MyEnum.Third; + return true; + case string s when int.TryParse(name, out var val): + value = (MyTestNameSpace.MyEnum)val; + return true; + default: + value = default; + return false; + } + } + else + { + switch (name) + { + case nameof(MyTestNameSpace.MyEnum.First): + value = MyTestNameSpace.MyEnum.First; + return true; + case nameof(MyTestNameSpace.MyEnum.Second): + value = MyTestNameSpace.MyEnum.Second; + return true; + case nameof(MyTestNameSpace.MyEnum.Third): + value = MyTestNameSpace.MyEnum.Third; + return true; + case string s when int.TryParse(name, out var val): + value = (MyTestNameSpace.MyEnum)val; + return true; + default: + value = default; + return false; + } + } + } + +#if NETCOREAPP && !NETCOREAPP2_0 && !NETCOREAPP1_1 && !NETCOREAPP1_0 + /// + /// Converts the span representation of the name or numeric value of + /// an to the equivalent instance. + /// The return value indicates whether the conversion succeeded. + /// + /// The span representation of the enumeration name or underlying value to convert + /// When this method returns, contains an object of type + /// whose + /// value is represented by if the parse operation succeeds. + /// If the parse operation fails, contains the default value of the underlying type + /// of . This parameter is passed uninitialized. + /// true if the value parameter was converted successfully; otherwise, false. + public static bool TryParse( +#if NETCOREAPP3_0_OR_GREATER + [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] +#endif + in ReadOnlySpan name, + out MyTestNameSpace.MyEnum value) + => TryParse(name, out value, false, false); + + /// + /// Converts the span representation of the name or numeric value of + /// an to the equivalent instance. + /// The return value indicates whether the conversion succeeded. + /// + /// The span representation of the enumeration name or underlying value to convert + /// When this method returns, contains an object of type + /// whose + /// value is represented by if the parse operation succeeds. + /// If the parse operation fails, contains the default value of the underlying type + /// of . This parameter is passed uninitialized. + /// true to read value in case insensitive mode; false to read value in case sensitive mode. + /// true if the value parameter was converted successfully; otherwise, false. + public static bool TryParse( +#if NETCOREAPP3_0_OR_GREATER + [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] +#endif + in ReadOnlySpan name, + out MyTestNameSpace.MyEnum value, + bool ignoreCase) + => TryParse(name, out value, ignoreCase, false); + + /// + /// Converts the span representation of the name or numeric value of + /// an to the equivalent instance. + /// The return value indicates whether the conversion succeeded. + /// + /// The span representation of the enumeration name or underlying value to convert + /// When this method returns, contains an object of type + /// whose + /// value is represented by if the parse operation succeeds. + /// If the parse operation fails, contains the default value of the underlying type + /// of . This parameter is passed uninitialized. + /// true to read value in case insensitive mode; false to read value in case sensitive mode. + /// If true, considers the value included in metadata attributes such as + /// when parsing, otherwise only considers the member names. + /// true if the value parameter was converted successfully; otherwise, false. + public static bool TryParse( +#if NETCOREAPP3_0_OR_GREATER + [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] +#endif + in ReadOnlySpan name, + out MyTestNameSpace.MyEnum result, + bool ignoreCase, + bool allowMatchingMetadataAttribute) + { + if (ignoreCase) + { + switch (name) + { + case ReadOnlySpan current when current.Equals(nameof(MyTestNameSpace.MyEnum.First).AsSpan(), System.StringComparison.OrdinalIgnoreCase): + result = MyTestNameSpace.MyEnum.First; + return true; + case ReadOnlySpan current when current.Equals(nameof(MyTestNameSpace.MyEnum.Second).AsSpan(), System.StringComparison.OrdinalIgnoreCase): + result = MyTestNameSpace.MyEnum.Second; + return true; + case ReadOnlySpan current when current.Equals(nameof(MyTestNameSpace.MyEnum.Third).AsSpan(), System.StringComparison.OrdinalIgnoreCase): + result = MyTestNameSpace.MyEnum.Third; + return true; + case ReadOnlySpan current when int.TryParse(name, out var numericResult): + result = (MyTestNameSpace.MyEnum)numericResult; + return true; + default: + result = default; + return false; + } + } + else + { + switch (name) + { + case ReadOnlySpan current when current.Equals(nameof(MyTestNameSpace.MyEnum.First).AsSpan(), System.StringComparison.Ordinal): + result = MyTestNameSpace.MyEnum.First; + return true; + case ReadOnlySpan current when current.Equals(nameof(MyTestNameSpace.MyEnum.Second).AsSpan(), System.StringComparison.Ordinal): + result = MyTestNameSpace.MyEnum.Second; + return true; + case ReadOnlySpan current when current.Equals(nameof(MyTestNameSpace.MyEnum.Third).AsSpan(), System.StringComparison.Ordinal): + result = MyTestNameSpace.MyEnum.Third; + return true; + case ReadOnlySpan current when int.TryParse(name, out var numericResult): + result = (MyTestNameSpace.MyEnum)numericResult; + return true; + default: + result = default; + return false; + } + } + } +#endif + + /// + /// Retrieves an array of the values of the members defined in + /// . + /// Note that this returns a new array with every invocation, so + /// should be cached if appropriate. + /// + /// An array of the values defined in + public static MyTestNameSpace.MyEnum[] GetValues() + { + return new[] + { + MyTestNameSpace.MyEnum.First, + MyTestNameSpace.MyEnum.Second, + MyTestNameSpace.MyEnum.Third, + }; + } + + /// + /// Retrieves an array of the names of the members defined in + /// . + /// Note that this returns a new array with every invocation, so + /// should be cached if appropriate. + /// + /// An array of the names of the members defined in + public static string[] GetNames() + { + return new[] + { + nameof(MyTestNameSpace.MyEnum.First), + nameof(MyTestNameSpace.MyEnum.Second), + nameof(MyTestNameSpace.MyEnum.Third), + }; + } + } +} \ No newline at end of file diff --git a/tests/NetEscapades.EnumGenerators.Tests/Snapshots/SourceGenerationHelperSnapshotTests.GeneratesFlagsEnumCorrectly.verified.txt b/tests/NetEscapades.EnumGenerators.Tests/Snapshots/SourceGenerationHelperSnapshotTests.GeneratesFlagsEnumCorrectly.verified.txt index a2111aa..5975495 100644 --- a/tests/NetEscapades.EnumGenerators.Tests/Snapshots/SourceGenerationHelperSnapshotTests.GeneratesFlagsEnumCorrectly.verified.txt +++ b/tests/NetEscapades.EnumGenerators.Tests/Snapshots/SourceGenerationHelperSnapshotTests.GeneratesFlagsEnumCorrectly.verified.txt @@ -1,4 +1,4 @@ -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // This code was generated by the NetEscapades.EnumGenerators source generator // @@ -43,17 +43,15 @@ namespace Something.Blah /// /// Determines whether one or more bit fields are set in the current instance. - /// Equivalent to calling value.HasFlag(flag) + /// Equivalent to calling on . /// /// The value of the instance to investiage /// The flag to check for /// true if the fields set in the flag are also set in the current instance; otherwise false. - public static bool HasFlag(this Something.Blah.ShortName value, Something.Blah.ShortName flag) - => value switch - { - 0 => flag.Equals(0), - _ => (value & flag) != 0, - }; + /// If the underlying value of is zero, the method returns true. + /// This is consistent with the behaviour of + public static bool HasFlagFast(this Something.Blah.ShortName value, Something.Blah.ShortName flag) + => flag == 0 ? true : (value & flag) == flag; /// /// Returns a boolean telling whether the given enum value exists in the enumeration.