Skip to content

Commit

Permalink
Fix HasFlag() implementation (#44)
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewlock authored Dec 18, 2022
1 parent 03d7b1f commit c894cce
Show file tree
Hide file tree
Showing 7 changed files with 467 additions and 26 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions src/NetEscapades.EnumGenerators/EnumGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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;
Expand Down
12 changes: 5 additions & 7 deletions src/NetEscapades.EnumGenerators/SourceGenerationHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,17 +110,15 @@ public static string ToStringFast(this ").Append(enumToGenerate.FullyQualifiedNa
/// <summary>
/// Determines whether one or more bit fields are set in the current instance.
/// Equivalent to calling <c>value.HasFlag(flag)</c>
/// Equivalent to calling <see cref=""Enum.HasFlag(Enum)"" /> on <paramref name=""value""/>.
/// </summary>
/// <param name=""value"">The value of the instance to investiage</param>
/// <param name=""flag"">The flag to check for</param>
/// <returns><c>true</c> if the fields set in the flag are also set in the current instance; otherwise <c>false</c>.</returns>
public static bool HasFlag(this ").Append(enumToGenerate.FullyQualifiedName).Append(@" value, ").Append(enumToGenerate.FullyQualifiedName).Append(@" flag)
=> value switch
{
0 => flag.Equals(0),
_ => (value & flag) != 0,
};");
/// <remarks>If the underlying value of <paramref name=""flag""/> is zero, the method returns true.
/// This is consistent with the behaviour of <see cref=""Enum.HasFlag(Enum)"" /></remarks>
public static bool HasFlagFast(this ").Append(enumToGenerate.FullyQualifiedName).Append(@" value, ").Append(enumToGenerate.FullyQualifiedName).Append(@" flag)
=> flag == 0 ? true : (value & flag) == flag;");
}

sb.Append(@"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using FluentAssertions;
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;

namespace NetEscapades.EnumGenerators.IntegrationTests;
Expand Down Expand Up @@ -44,6 +46,12 @@ protected override bool TryParse(in ReadOnlySpan<char> name, out FlagsEnum parse
=> FlagsEnumExtensions.TryParse(name, out parsed, ignoreCase);
#endif

/// <summary>
///
/// </summary>
/// <param name="value"></param>
/// <remarks>If the underlying value of <paramref name="flag"/> is zero, the method returns true.
/// This is consistent with the behaviour of <see cref=""Enum.HasFlag(Enum)""></remarks>
[Theory]
[MemberData(nameof(ValidEnumValues))]
public void GeneratesToStringFast(FlagsEnum value) => GeneratesToStringFastTest(value);
Expand All @@ -62,16 +70,29 @@ protected override bool TryParse(in ReadOnlySpan<char> name, out FlagsEnum parse
public void GeneratesIsDefinedUsingNameAsSpan(string name) => GeneratesIsDefinedTest(name.AsSpan(), allowMatchingMetadataAttribute: false);
#endif

public static IEnumerable<object[]> 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));
}
Expand Down
32 changes: 32 additions & 0 deletions tests/NetEscapades.EnumGenerators.Tests/EnumGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<EnumGenerator>(input);

Assert.Empty(diagnostics);
return Verifier.Verify(output)
.UseTextForParameters("Params")
.DisableRequireUniquePrefix()
.UseDirectory("Snapshots");
}
}
Loading

0 comments on commit c894cce

Please sign in to comment.