Skip to content
Draft

Enums #250

160 changes: 159 additions & 1 deletion src/NetEscapades.EnumGenerators.Generators/SourceGenerationHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,35 @@ namespace
/// This is a non-distinct count of defined names.
/// </summary>
public const int Length =
""").Append(enumToGenerate.Names.Count).Append(";").Append(
""").Append(enumToGenerate.Names.Count).Append(";");

if (enumToGenerate.HasFlags)
{
sb.Append(
"""


/// <summary>
/// A bitwise OR combination of all defined values in the enum.
/// </summary>
public const
""").Append(' ').Append(fullyQualifiedName).Append(" All = ");

var first = true;
foreach (var member in enumToGenerate.Names)
{
if (!first)
{
sb.Append(" | ");
}
first = false;
sb.Append(fullyQualifiedName).Append('.').AppendIdentifier(member.Key);
}

sb.Append(';');
}

sb.Append(
"""


Expand Down Expand Up @@ -550,7 +578,123 @@ public static bool HasFlagFast(this
"""
flag)
=> flag == 0 ? true : (value & flag) == flag;

/// <summary>
/// Determines whether any of the bit fields are set in the current instance.
/// </summary>
/// <param name="value">The value of the instance to investigate</param>
/// <param name="otherFlags">The flags to check for</param>
/// <returns><see langword="true"/> if any of the fields set in <paramref name="otherFlags" /> are
/// also set in <paramref name="value" />; otherwise <see langword="false"/>.</returns>
/// <remarks>If the underlying value of <paramref name="otherFlags"/> is zero, the method
/// always returns <see langword="true"/>, consistent with the behaviour of <see cref="global::System.Enum.HasFlag" />.</remarks>
public static bool HasAnyFlags(this
""").Append(' ').Append(fullyQualifiedName).Append(" value, ").Append(fullyQualifiedName)
.Append(
"""
otherFlags)
=> otherFlags == 0 ? true : (value & otherFlags) != 0;
""");

// Collect single-bit flag members (powers of two) and build a mask of all defined bits.
// Composite members are allowed as long as every bit they set is also a defined single-bit flag.
// Members with bits outside the defined mask disqualify the enum from TryGetFlags emission.
List<(string Name, ulong Value)>? singleBitFlags = null;
var seenBitValues = new HashSet<ulong>();
ulong definedMask = 0;
foreach (var member in enumToGenerate.Names)
{
var bits = ToUInt64BitPattern(member.Value.ConstantValue);
if (bits == 0)
{
// Zero-valued members (e.g. None) are allowed but not emitted.
continue;
}
if ((bits & (bits - 1)) == 0 && seenBitValues.Add(bits))
{
singleBitFlags ??= new List<(string, ulong)>();
singleBitFlags.Add((member.Key, bits));
definedMask |= bits;
}
}

// Verify every non-zero member value is a subset of the defined-bit mask.
// Catches members with bits that don't correspond to any single-bit flag.
var hasInvalidValue = false;
if (singleBitFlags is { Count: > 0 })
{
foreach (var member in enumToGenerate.Names)
{
var bits = ToUInt64BitPattern(member.Value.ConstantValue);
if (bits != 0 && (bits & ~definedMask) != 0)
{
hasInvalidValue = true;
break;
}
}
}

if (!hasInvalidValue && singleBitFlags is { Count: > 0 })
{
singleBitFlags.Sort((a, b) => a.Value.CompareTo(b.Value));

sb.Append(
"""


/// <summary>
/// The number of distinct single-bit flag members defined in the enum.
/// This is the buffer size required by <see cref="TryGetFlags" />.
/// </summary>
public const int DistinctFlagCount =
""").Append(' ').Append(singleBitFlags.Count).Append(';');

sb.Append(
"""


#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER || NETESCAPADES_ENUMGENERATORS_SYSTEM_MEMORY
/// <summary>
/// Writes each defined single-bit flag set in <paramref name="value"/> into <paramref name="buffer"/>.
/// Composite members (e.g. a member defined as <c>A | B</c>) are decomposed into their constituent single-bit flags.
/// The zero/None value never appears in the output.
/// </summary>
/// <param name="value">The value to decompose into individual flags.</param>
/// <param name="buffer">The buffer to write the flags into. Must be at least <see cref="DistinctFlagCount" /> elements.</param>
/// <param name="count">The number of flags written to <paramref name="buffer"/>. Set to 0 if the method returns <see langword="false"/>.</param>
/// <returns><see langword="true"/> if the buffer was large enough; <see langword="false"/> if it was smaller than <see cref="DistinctFlagCount" />.</returns>
public static bool TryGetFlags(this
""").Append(' ').Append(fullyQualifiedName).Append(" value, global::System.Span<").Append(fullyQualifiedName).Append(
"""
> buffer, out int count)
{
if (buffer.Length < DistinctFlagCount)
{
count = 0;
return false;
}
count = 0;
""");

foreach (var flag in singleBitFlags)
{
sb.Append("\n if ((value & ")
.Append(fullyQualifiedName).Append('.').AppendIdentifier(flag.Name)
.Append(") == ")
.Append(fullyQualifiedName).Append('.').AppendIdentifier(flag.Name)
.Append(")\n buffer[count++] = ")
.Append(fullyQualifiedName).Append('.').AppendIdentifier(flag.Name)
.Append(';');
}

sb.Append(
"""

return true;
}
#endif
""");
}
}

sb.Append(
Expand Down Expand Up @@ -2380,6 +2524,20 @@ private static StringBuilder AppendIdentifier(this StringBuilder sb, string iden
return sb.Append(identifier);
}

private static ulong ToUInt64BitPattern(object value)
=> value switch
{
sbyte v => (byte)v,
byte v => v,
short v => (ushort)v,
ushort v => v,
int v => (uint)v,
uint v => v,
long v => unchecked((ulong)v),
ulong v => v,
_ => 0UL,
};

private enum AlternativeMethodChoice
{
None,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using FluentAssertions;
using System;
using Xunit;

#if INTEGRATION_TESTS
namespace NetEscapades.EnumGenerators.IntegrationTests;
#elif PRIVATEASSETS_INTEGRATION_TESTS
namespace NetEscapades.EnumGenerators.PrivateAssets.IntegrationTests;
#elif NETSTANDARD_INTEGRATION_TESTS
namespace NetEscapades.EnumGenerators.NetStandard.IntegrationTests;
#elif NETSTANDARD_SYSTEMMEMORY_INTEGRATION_TESTS
namespace NetEscapades.EnumGenerators.NetStandard.SystemMemory.IntegrationTests;
#elif INTERCEPTOR_TESTS
namespace NetEscapades.EnumGenerators.Interceptors.IntegrationTests;
#elif NUGET_INTEGRATION_TESTS
namespace NetEscapades.EnumGenerators.Nuget.IntegrationTests;
#elif NUGET_INTERCEPTOR_TESTS
namespace NetEscapades.EnumGenerators.Nuget.Interceptors.IntegrationTests;
#elif NUGET_SYSTEMMEMORY_INTEGRATION_TESTS
namespace NetEscapades.EnumGenerators.Nuget.SystemMemory.IntegrationTests;
#elif NUGET_SYSTEMMEMORY_PRIVATEASSETS_INTEGRATION_TESTS
namespace NetEscapades.EnumGenerators.Nuget.SystemMemory.PrivateAssets.IntegrationTests;
#else
#error Unknown integration tests
#endif

public class BitFlagsEnumExtensionsTests
{
[Fact]
public void DistinctFlagCountExcludesNone()
{
BitFlagsEnumExtensions.DistinctFlagCount.Should().Be(4);
}

#if READONLYSPAN
public static TheoryData<BitFlagsEnum, int, BitFlagsEnum[]> TryGetFlagsData => new()
{
// Single flag → itself
{ BitFlagsEnum.First, 1, new[] { BitFlagsEnum.First } },
// Multiple flags — bit-value order
{ BitFlagsEnum.First | BitFlagsEnum.Third, 2, new[] { BitFlagsEnum.First, BitFlagsEnum.Third } },
// None → empty output
{ BitFlagsEnum.None, 0, Array.Empty<BitFlagsEnum>() },
// Undefined bits silently dropped (16 is not defined)
{ (BitFlagsEnum)17, 1, new[] { BitFlagsEnum.First } },
// Only undefined bits → empty output
{ (BitFlagsEnum)16, 0, Array.Empty<BitFlagsEnum>() },
// All defined flags set → 4 single-bit members
{ BitFlagsEnumExtensions.All, 4,
new[] { BitFlagsEnum.First, BitFlagsEnum.Second, BitFlagsEnum.Third, BitFlagsEnum.Fourth } },
// All bits set across underlying type → still just the 4 defined flags
{ (BitFlagsEnum)0xFF, 4,
new[] { BitFlagsEnum.First, BitFlagsEnum.Second, BitFlagsEnum.Third, BitFlagsEnum.Fourth } },
};

[Theory]
[MemberData(nameof(TryGetFlagsData))]
public void TryGetFlags(BitFlagsEnum value, int expectedCount, BitFlagsEnum[] expectedFlags)
{
Span<BitFlagsEnum> buffer = stackalloc BitFlagsEnum[BitFlagsEnumExtensions.DistinctFlagCount];
var result = value.TryGetFlags(buffer, out var count);
result.Should().BeTrue();
count.Should().Be(expectedCount);
buffer.Slice(0, count).ToArray().Should().Equal(expectedFlags);
}

[Fact]
public void TryGetFlags_BufferSmallerThanDistinctFlagCount_ReturnsFalse()
{
Span<BitFlagsEnum> buffer = stackalloc BitFlagsEnum[BitFlagsEnumExtensions.DistinctFlagCount - 1];
var result = BitFlagsEnum.First.TryGetFlags(buffer, out var count);
result.Should().BeFalse();
count.Should().Be(0);
}
#endif
}
11 changes: 11 additions & 0 deletions tests/NetEscapades.EnumGenerators.IntegrationTests/Enums.cs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,17 @@ public enum FlagsEnum
ThirdAndFourth = Third | Fourth,
}

[EnumExtensions]
[Flags]
public enum BitFlagsEnum
{
None = 0,
First = 1 << 0,
Second = 1 << 1,
Third = 1 << 2,
Fourth = 1 << 3,
}

[EnumExtensions(MetadataSource = MetadataSource.DescriptionAttribute)]
public enum StringTesting
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,104 @@ public void HasFlags(FlagsEnum value, FlagsEnum flag)

isDefined.Should().Be(value.HasFlag(flag));
}


[Fact]
public void AllContainsFlag()
{
foreach (var flag in FlagsEnumExtensions.GetValues())
{
FlagsEnumExtensions.All.HasFlag(flag).Should().BeTrue();
}
}

[Fact]
public void AllHasExpectedValue()
{
((int)FlagsEnumExtensions.All).Should().Be(0b1111); // 15
}

public static TheoryData<FlagsEnum, FlagsEnum, bool> HasAnyFlagsData => new()
{
// Single flag present
{ FlagsEnum.First | FlagsEnum.Second, FlagsEnum.First, true },
// No queried flags present
{ FlagsEnum.First | FlagsEnum.Second, FlagsEnum.Third, false },
// Partial overlap — only some flags match
{ FlagsEnum.First, FlagsEnum.First | FlagsEnum.Third, true },
// None has no flags
{ FlagsEnum.None, FlagsEnum.First, false },
// Querying with zero (None) — consistent with HasFlag(0) behavior
{ FlagsEnum.First, FlagsEnum.None, true },
// Both zero
{ FlagsEnum.None, FlagsEnum.None, true },
// Composite member overlaps
{ FlagsEnum.Third, FlagsEnum.ThirdAndFourth, true },
// Composite member no overlap
{ FlagsEnum.First, FlagsEnum.ThirdAndFourth, false },
// Undefined value with overlap (65 = 64|1, First = 1)
{ (FlagsEnum)65, FlagsEnum.First, true },
// Undefined value no overlap
{ (FlagsEnum)64, FlagsEnum.First, false },
};

[Theory]
[MemberData(nameof(HasAnyFlagsData))]
public void HasAnyFlags(FlagsEnum value, FlagsEnum otherFlags, bool expected)
{
value.HasAnyFlags(otherFlags).Should().Be(expected);
}

[Fact]
public void DistinctFlagCountCountsSingleBitFlags()
{
// First, Second, Third, Fourth — ThirdAndFourth is a composite, None is zero
FlagsEnumExtensions.DistinctFlagCount.Should().Be(4);
}

#if READONLYSPAN
public static TheoryData<FlagsEnum, int, FlagsEnum[]> TryGetFlagsData => new()
{
// Single flag
{ FlagsEnum.First, 1, new[] { FlagsEnum.First } },
// Multiple flags — bit-value order
{ FlagsEnum.First | FlagsEnum.Third, 2, new[] { FlagsEnum.First, FlagsEnum.Third } },
// Composite member decomposes into its constituent single-bit flags
{ FlagsEnum.ThirdAndFourth, 2, new[] { FlagsEnum.Third, FlagsEnum.Fourth } },
// Combination of single-bit and composite — same as combining the bits
{ FlagsEnum.First | FlagsEnum.ThirdAndFourth, 3,
new[] { FlagsEnum.First, FlagsEnum.Third, FlagsEnum.Fourth } },
// None → empty
{ FlagsEnum.None, 0, Array.Empty<FlagsEnum>() },
// Undefined bit (16) is silently dropped, defined bit (1) is kept
{ (FlagsEnum)17, 1, new[] { FlagsEnum.First } },
// Only undefined bits → empty
{ (FlagsEnum)16, 0, Array.Empty<FlagsEnum>() },
// All defined flags set
{ FlagsEnumExtensions.All, 4,
new[] { FlagsEnum.First, FlagsEnum.Second, FlagsEnum.Third, FlagsEnum.Fourth } },
};

[Theory]
[MemberData(nameof(TryGetFlagsData))]
public void TryGetFlags(FlagsEnum value, int expectedCount, FlagsEnum[] expectedFlags)
{
Span<FlagsEnum> buffer = stackalloc FlagsEnum[FlagsEnumExtensions.DistinctFlagCount];
var result = value.TryGetFlags(buffer, out var count);
result.Should().BeTrue();
count.Should().Be(expectedCount);
buffer.Slice(0, count).ToArray().Should().Equal(expectedFlags);
}

[Fact]
public void TryGetFlags_BufferSmallerThanDistinctFlagCount_ReturnsFalse()
{
Span<FlagsEnum> buffer = stackalloc FlagsEnum[FlagsEnumExtensions.DistinctFlagCount - 1];
var result = FlagsEnum.First.TryGetFlags(buffer, out var count);
result.Should().BeFalse();
count.Should().Be(0);
}
#endif

private PackageEnumParseOptions Map(EnumParseOptions options)
=> new(comparisonType: options.ComparisonType,
allowMatchingMetadataAttribute: options.AllowMatchingMetadataAttribute,
Expand Down
Loading
Loading