Skip to content

Commit 3011815

Browse files
committed
fix/issue-59
1 parent e9860ad commit 3011815

5 files changed

Lines changed: 168 additions & 1 deletion

File tree

src/ExpressiveSharp.Generator/Emitter/ExpressionTreeEmitter.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -753,13 +753,25 @@ private bool TryEmitEnumMethodExpansion(IInvocationOperation invocation, out str
753753
AppendLine($"var {defaultVar} = {Expr}.Default(typeof({returnTypeFqn}));");
754754
}
755755

756+
var receiverTypeFqn = receiverType.ToDisplayString(_fqnFormat);
757+
756758
// Build the ternary chain in reverse so the first member ends up as the outermost (and first-tested) branch.
757759
var currentVar = defaultVar;
758760
foreach (var member in enumMembers.AsEnumerable().Reverse())
759761
{
760762
var enumValueVar = NextVar();
761763
AppendLine($"var {enumValueVar} = {Expr}.Constant({enumTypeFqn}.{member.Name}, typeof({enumTypeFqn}));");
762764

765+
// The MethodInfo is bound on the original receiver type — for an instance method on
766+
// Nullable<TEnum> or an extension whose first param is Nullable<TEnum>, the per-arm
767+
// operand must also be Nullable<TEnum> or Expression.Call rejects the type mismatch.
768+
if (isNullable)
769+
{
770+
var lifted = NextVar();
771+
AppendLine($"var {lifted} = {Expr}.Convert({enumValueVar}, typeof({receiverTypeFqn}));");
772+
enumValueVar = lifted;
773+
}
774+
763775
// Static path passes the enum value as the first arg; instance path uses it as the receiver.
764776
string callVar;
765777
if (originalMethod.IsStatic)
@@ -800,7 +812,6 @@ private bool TryEmitEnumMethodExpansion(IInvocationOperation invocation, out str
800812
if (isNullable)
801813
{
802814
var nullConst = NextVar();
803-
var receiverTypeFqn = receiverType.ToDisplayString(_fqnFormat);
804815
AppendLine($"var {nullConst} = {Expr}.Constant(null, typeof({receiverTypeFqn}));");
805816

806817
var nullCheck = NextVar();
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// <auto-generated/>
2+
#nullable disable
3+
4+
using Foo;
5+
6+
namespace ExpressiveSharp.Generated
7+
{
8+
static partial class Foo_Entity
9+
{
10+
// [Expressive]
11+
// public string TrendLabel => Trend.Describe();
12+
static global::System.Linq.Expressions.Expression<global::System.Func<global::Foo.Entity, string>> TrendLabel_Expression()
13+
{
14+
var p__this = global::System.Linq.Expressions.Expression.Parameter(typeof(global::Foo.Entity), "@this");
15+
var expr_0 = global::System.Linq.Expressions.Expression.Property(p__this, typeof(global::Foo.Entity).GetProperty("Trend", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // Trend
16+
var expr_1 = global::System.Linq.Expressions.Expression.Constant(null, typeof(string));
17+
var expr_2 = global::System.Linq.Expressions.Expression.Constant(global::Foo.Trend.NoChange, typeof(global::Foo.Trend));
18+
var expr_3 = global::System.Linq.Expressions.Expression.Convert(expr_2, typeof(global::Foo.Trend?));
19+
var expr_4 = global::System.Linq.Expressions.Expression.Call(typeof(global::Foo.TrendExtensions).GetMethod("Describe", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static, null, new global::System.Type[] { typeof(global::Foo.Trend?) }, null), new global::System.Linq.Expressions.Expression[] { expr_3 });
20+
var expr_5 = global::System.Linq.Expressions.Expression.Equal(expr_0, expr_3);
21+
var expr_6 = global::System.Linq.Expressions.Expression.Condition(expr_5, expr_4, expr_1, typeof(string));
22+
var expr_7 = global::System.Linq.Expressions.Expression.Constant(global::Foo.Trend.Decrease, typeof(global::Foo.Trend));
23+
var expr_8 = global::System.Linq.Expressions.Expression.Convert(expr_7, typeof(global::Foo.Trend?));
24+
var expr_9 = global::System.Linq.Expressions.Expression.Call(typeof(global::Foo.TrendExtensions).GetMethod("Describe", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static, null, new global::System.Type[] { typeof(global::Foo.Trend?) }, null), new global::System.Linq.Expressions.Expression[] { expr_8 });
25+
var expr_10 = global::System.Linq.Expressions.Expression.Equal(expr_0, expr_8);
26+
var expr_11 = global::System.Linq.Expressions.Expression.Condition(expr_10, expr_9, expr_6, typeof(string));
27+
var expr_12 = global::System.Linq.Expressions.Expression.Constant(global::Foo.Trend.Increase, typeof(global::Foo.Trend));
28+
var expr_13 = global::System.Linq.Expressions.Expression.Convert(expr_12, typeof(global::Foo.Trend?));
29+
var expr_14 = global::System.Linq.Expressions.Expression.Call(typeof(global::Foo.TrendExtensions).GetMethod("Describe", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static, null, new global::System.Type[] { typeof(global::Foo.Trend?) }, null), new global::System.Linq.Expressions.Expression[] { expr_13 });
30+
var expr_15 = global::System.Linq.Expressions.Expression.Equal(expr_0, expr_13);
31+
var expr_16 = global::System.Linq.Expressions.Expression.Condition(expr_15, expr_14, expr_11, typeof(string));
32+
var expr_17 = global::System.Linq.Expressions.Expression.Constant(null, typeof(global::Foo.Trend?));
33+
var expr_18 = global::System.Linq.Expressions.Expression.Equal(expr_0, expr_17);
34+
var expr_19 = global::System.Linq.Expressions.Expression.Condition(expr_18, expr_1, expr_16, typeof(string));
35+
return global::System.Linq.Expressions.Expression.Lambda<global::System.Func<global::Foo.Entity, string>>(expr_19, p__this);
36+
}
37+
}
38+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// <auto-generated/>
2+
#nullable disable
3+
4+
using Foo;
5+
6+
namespace ExpressiveSharp.Generated
7+
{
8+
static partial class Foo_Entity
9+
{
10+
// [Expressive]
11+
// public string TrendLabel => Trend.ToString() ?? string.Empty;
12+
static global::System.Linq.Expressions.Expression<global::System.Func<global::Foo.Entity, string>> TrendLabel_Expression()
13+
{
14+
var p__this = global::System.Linq.Expressions.Expression.Parameter(typeof(global::Foo.Entity), "@this");
15+
var expr_1 = global::System.Linq.Expressions.Expression.Property(p__this, typeof(global::Foo.Entity).GetProperty("Trend", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // Trend
16+
var expr_2 = global::System.Linq.Expressions.Expression.Constant(null, typeof(string));
17+
var expr_3 = global::System.Linq.Expressions.Expression.Constant(global::Foo.Trend.NoChange, typeof(global::Foo.Trend));
18+
var expr_4 = global::System.Linq.Expressions.Expression.Convert(expr_3, typeof(global::Foo.Trend?));
19+
var expr_5 = global::System.Linq.Expressions.Expression.Call(expr_4, typeof(global::Foo.Trend?).GetMethod("ToString", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance, null, new global::System.Type[] { }, null), global::System.Array.Empty<global::System.Linq.Expressions.Expression>());
20+
var expr_6 = global::System.Linq.Expressions.Expression.Equal(expr_1, expr_4);
21+
var expr_7 = global::System.Linq.Expressions.Expression.Condition(expr_6, expr_5, expr_2, typeof(string));
22+
var expr_8 = global::System.Linq.Expressions.Expression.Constant(global::Foo.Trend.Decrease, typeof(global::Foo.Trend));
23+
var expr_9 = global::System.Linq.Expressions.Expression.Convert(expr_8, typeof(global::Foo.Trend?));
24+
var expr_10 = global::System.Linq.Expressions.Expression.Call(expr_9, typeof(global::Foo.Trend?).GetMethod("ToString", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance, null, new global::System.Type[] { }, null), global::System.Array.Empty<global::System.Linq.Expressions.Expression>());
25+
var expr_11 = global::System.Linq.Expressions.Expression.Equal(expr_1, expr_9);
26+
var expr_12 = global::System.Linq.Expressions.Expression.Condition(expr_11, expr_10, expr_7, typeof(string));
27+
var expr_13 = global::System.Linq.Expressions.Expression.Constant(global::Foo.Trend.Increase, typeof(global::Foo.Trend));
28+
var expr_14 = global::System.Linq.Expressions.Expression.Convert(expr_13, typeof(global::Foo.Trend?));
29+
var expr_15 = global::System.Linq.Expressions.Expression.Call(expr_14, typeof(global::Foo.Trend?).GetMethod("ToString", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance, null, new global::System.Type[] { }, null), global::System.Array.Empty<global::System.Linq.Expressions.Expression>());
30+
var expr_16 = global::System.Linq.Expressions.Expression.Equal(expr_1, expr_14);
31+
var expr_17 = global::System.Linq.Expressions.Expression.Condition(expr_16, expr_15, expr_12, typeof(string));
32+
var expr_18 = global::System.Linq.Expressions.Expression.Constant(null, typeof(global::Foo.Trend?));
33+
var expr_19 = global::System.Linq.Expressions.Expression.Equal(expr_1, expr_18);
34+
var expr_20 = global::System.Linq.Expressions.Expression.Condition(expr_19, expr_2, expr_17, typeof(string));
35+
var expr_21 = global::System.Linq.Expressions.Expression.Field(null, typeof(string).GetField("Empty", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)); // string.Empty
36+
var expr_0 = global::System.Linq.Expressions.Expression.Coalesce(expr_20, expr_21);
37+
return global::System.Linq.Expressions.Expression.Lambda<global::System.Func<global::Foo.Entity, string>>(expr_0, p__this);
38+
}
39+
}
40+
}

tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/EnumTests.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,4 +333,60 @@ public record Entity
333333

334334
return Verifier.Verify(result.GeneratedTrees[0].ToString());
335335
}
336+
337+
[TestMethod]
338+
public Task ExpandToStringOnNullableEnum()
339+
{
340+
var compilation = CreateCompilation(
341+
"""
342+
namespace Foo {
343+
public enum Trend { Increase, Decrease, NoChange }
344+
345+
public record Entity
346+
{
347+
public Trend? Trend { get; init; }
348+
349+
[Expressive]
350+
public string TrendLabel => Trend.ToString() ?? string.Empty;
351+
}
352+
}
353+
""");
354+
var result = RunExpressiveGenerator(compilation);
355+
356+
Assert.AreEqual(0, result.Diagnostics.Length);
357+
Assert.AreEqual(1, result.GeneratedTrees.Length);
358+
359+
return Verifier.Verify(result.GeneratedTrees[0].ToString());
360+
}
361+
362+
[TestMethod]
363+
public Task ExpandExtensionMethodOnNullableEnum()
364+
{
365+
var compilation = CreateCompilation(
366+
"""
367+
namespace Foo {
368+
public enum Trend { Increase, Decrease, NoChange }
369+
370+
public static class TrendExtensions
371+
{
372+
public static string Describe(this Trend? value) =>
373+
value.HasValue ? value.Value.ToString() : "n/a";
374+
}
375+
376+
public record Entity
377+
{
378+
public Trend? Trend { get; init; }
379+
380+
[Expressive]
381+
public string TrendLabel => Trend.Describe();
382+
}
383+
}
384+
""");
385+
var result = RunExpressiveGenerator(compilation);
386+
387+
Assert.AreEqual(0, result.Diagnostics.Length);
388+
Assert.AreEqual(1, result.GeneratedTrees.Length);
389+
390+
return Verifier.Verify(result.GeneratedTrees[0].ToString());
391+
}
336392
}

tests/ExpressiveSharp.IntegrationTests/Tests/EnumComparisonTests.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,25 @@ public void ExpressivePropertyOnEnumComparison_RegistryResolves()
7070

7171
Assert.IsNotNull(registered);
7272
}
73+
74+
[TestMethod]
75+
public void ToStringOnNullableEnum_ExpandExpressives_MaterializesAndEvaluates()
76+
{
77+
var source = new List<EnumComparisonEntity>
78+
{
79+
new() { NullableValue = Bucket.Low },
80+
new() { NullableValue = Bucket.Mid },
81+
new() { NullableValue = Bucket.High },
82+
new() { NullableValue = null },
83+
}.AsQueryable();
84+
85+
Expression<Func<EnumComparisonEntity, string>> expr = e => e.NullableLabel;
86+
var expanded = (Expression<Func<EnumComparisonEntity, string>>)expr.ExpandExpressives();
87+
88+
var labels = source.Select(expanded.Compile()).ToList();
89+
90+
CollectionAssert.AreEqual(new[] { "Low", "Mid", "High", "" }, labels);
91+
}
7392
}
7493

7594
public class EnumComparisonEntity
@@ -88,6 +107,9 @@ public class EnumComparisonEntity
88107

89108
[Expressive]
90109
public bool IsLowOrMid => NullableValue <= Bucket.Mid;
110+
111+
[Expressive]
112+
public string NullableLabel => NullableValue.ToString() ?? string.Empty;
91113
}
92114

93115
public enum Bucket { Low, Mid, High }

0 commit comments

Comments
 (0)