Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions src/CodegenTests/CodeFormatterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,24 @@ public enum Numbers
two
}

[Flags]
public enum Toppings
{
None = 0,
Cheese = 1,
Pepperoni = 2,
Mushrooms = 4
}

// Mimics the Npgsql.NpgsqlDbType shape: a non-[Flags] enum whose
// callers nevertheless OR member values together (NpgsqlDbType.Array
// is int.MinValue and gets bit-or'd with type tags).
public enum DirtyFlagless
{
A = unchecked((int)0x80000000),
B = 19
}

public class CodeFormatterTests
{
[Fact]
Expand Down Expand Up @@ -50,6 +68,29 @@ public void write_enum()
.ShouldBe("CodegenTests.Numbers.one");
}

[Fact]
public void write_flags_enum_combination()
{
// [Flags] enums whose Or'd value has a multi-name string representation
// should produce a piped C# expression — never the comma-separated string
// that Enum.ToString returns directly.
CodeFormatter.Write(Toppings.Cheese | Toppings.Pepperoni)
.ShouldBe("CodegenTests.Toppings.Cheese | CodegenTests.Toppings.Pepperoni");
}

[Fact]
public void write_undefined_enum_value_uses_cast()
{
// Reproduces the Npgsql.NpgsqlDbType.Array | NpgsqlDbType.Text scenario.
// Without [Flags], Enum.ToString returns the integer literal string,
// which used to be emitted directly and produced uncompilable code such as
// `CodegenTests.DirtyFlagless.-2147483629`. The fix emits a cast instead.
var combined = (DirtyFlagless)((int)DirtyFlagless.A | (int)DirtyFlagless.B);
var raw = (int)combined;
CodeFormatter.Write(combined)
.ShouldBe($"((CodegenTests.DirtyFlagless)({raw}))");
}

[Fact]
public void write_type()
{
Expand Down
55 changes: 54 additions & 1 deletion src/JasperFx/CodeGeneration/CodeFormatter.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Globalization;
using JasperFx.CodeGeneration.Model;
using JasperFx.Core;
using JasperFx.Core.Reflection;
Expand Down Expand Up @@ -25,7 +26,7 @@ public static string Write(object? value)

if (value.GetType().IsEnum)
{
return value.GetType().FullNameInCode() + "." + value;
return WriteEnum((Enum)value);
}

if (value.GetType() == typeof(string[]))
Expand Down Expand Up @@ -71,4 +72,56 @@ public static string Write(object? value)

return value.ToString()!;
}

/// <summary>
/// Render an enum value as valid C# source. Handles three cases safely:
/// (1) a single defined named member ⇒ <c>Namespace.Type.Name</c>;
/// (2) a <see cref="FlagsAttribute"/> combination whose bits all map to defined members ⇒ <c>Type.A | Type.B</c>;
/// (3) any other value (including bit-or'd values on non-Flags enums like <c>NpgsqlDbType</c>) ⇒ a checked cast
/// of the underlying integer literal: <c>((Type)(rawValue))</c>. The cast form is necessary because
/// <see cref="Enum.ToString()"/> returns the integer literal as a string for undefined values, which is not a
/// valid C# identifier and used to produce uncompilable code such as <c>NpgsqlDbType.-2147483629</c>.
/// </summary>
private static string WriteEnum(Enum value)
{
var enumType = value.GetType();
var typeName = enumType.FullNameInCode();

// Case 1: defined single member.
if (Enum.IsDefined(enumType, value))
{
return typeName + "." + value;
}

// Case 2: [Flags] enum where ToString() yields comma-separated names.
if (enumType.IsDefined(typeof(FlagsAttribute), inherit: false))
{
var asString = value.ToString();
if (asString.Length > 0 && !IsNumericLiteral(asString))
{
var parts = asString.Split(", ");
return string.Join(" | ", parts.Select(p => typeName + "." + p));
}
}

// Case 3: undefined value — emit a cast of the underlying integer literal.
var underlying = Enum.GetUnderlyingType(enumType);
var raw = Convert.ChangeType(value, underlying, CultureInfo.InvariantCulture);
return $"(({typeName})({raw}))";
}

private static bool IsNumericLiteral(string text)
{
var i = 0;
if (text[0] == '-' || text[0] == '+')
{
if (text.Length == 1) return false;
i = 1;
}
for (; i < text.Length; i++)
{
if (!char.IsDigit(text[i])) return false;
}
return true;
}
}
Loading