diff --git a/src/ExpressiveSharp.Generator/Emitter/ExpressionTreeEmitter.cs b/src/ExpressiveSharp.Generator/Emitter/ExpressionTreeEmitter.cs index 47f97943..33dc3f47 100644 --- a/src/ExpressiveSharp.Generator/Emitter/ExpressionTreeEmitter.cs +++ b/src/ExpressiveSharp.Generator/Emitter/ExpressionTreeEmitter.cs @@ -666,6 +666,11 @@ private string EmitFieldReference(IFieldReferenceOperation fieldRef) return resultVar; } + // Enum members and const fields are inlined as constants, matching the C# compiler. A reflected + // field access would be funcletized by query providers into a parameter instead. + if (fieldRef.Field.HasConstantValue) + return EmitConstantField(fieldRef.Field); + var fieldResultVar = NextVar(); var fieldName = _fieldCache.EnsureFieldInfo(fieldRef.Field); @@ -682,6 +687,30 @@ private string EmitFieldReference(IFieldReferenceOperation fieldRef) return fieldResultVar; } + private string EmitConstantField(IFieldSymbol field) + { + var resultVar = NextVar(); + var typeFqn = field.Type.ToDisplayString(_fqnFormat); + + string valueLiteral; + if (field.ContainingType.TypeKind == TypeKind.Enum) + { + valueLiteral = $"{typeFqn}.{EscapeIdentifier(field.Name)}"; + } + else if (field.Type.TypeKind == TypeKind.Enum) + { + // The const itself may be inaccessible from the generated class, so emit its value rather than its name. + valueLiteral = $"({typeFqn})({FormatConstantValue(field.ConstantValue, field.Type)})"; + } + else + { + valueLiteral = FormatConstantValue(field.ConstantValue, field.Type); + } + + AppendLine($"var {resultVar} = {Expr}.Constant({valueLiteral}, typeof({typeFqn}));"); + return resultVar; + } + private bool TryEmitEnumMethodExpansion(IInvocationOperation invocation, out string resultVar) { resultVar = ""; @@ -793,7 +822,7 @@ string BuildCall(string operandVar) foreach (var member in enumMembers.AsEnumerable().Reverse()) { var enumValueVar = NextVar(); - AppendLine($"var {enumValueVar} = {Expr}.Constant({enumTypeFqn}.{member.Name}, typeof({enumTypeFqn}));"); + AppendLine($"var {enumValueVar} = {Expr}.Constant({enumTypeFqn}.{EscapeIdentifier(member.Name)}, typeof({enumTypeFqn}));"); // The MethodInfo is bound on the original receiver type — for an instance method on // Nullable or an extension whose first param is Nullable, the per-arm @@ -3573,6 +3602,10 @@ private static string SanitizeIdentifier(string name) return name.Replace("@", "_").Replace(".", "_").Replace("<", "_").Replace(">", "_"); } + // Symbol display's EscapeKeywordIdentifiers does not cover enum members, so escape by hand. + private static string EscapeIdentifier(string name) => + SyntaxFacts.GetKeywordKind(name) != SyntaxKind.None ? "@" + name : name; + private static string FormatConstantValue(object? value, ITypeSymbol? type) { if (value is null) diff --git a/tests/ExpressiveSharp.EntityFrameworkCore.IntegrationTests/Tests/Sqlite/CommonScenarioTests.cs b/tests/ExpressiveSharp.EntityFrameworkCore.IntegrationTests/Tests/Sqlite/CommonScenarioTests.cs index 78ae8428..0c9b1fc8 100644 --- a/tests/ExpressiveSharp.EntityFrameworkCore.IntegrationTests/Tests/Sqlite/CommonScenarioTests.cs +++ b/tests/ExpressiveSharp.EntityFrameworkCore.IntegrationTests/Tests/Sqlite/CommonScenarioTests.cs @@ -1,4 +1,6 @@ +using System.Linq.Expressions; using ExpressiveSharp.EntityFrameworkCore.IntegrationTests.Infrastructure; +using ExpressiveSharp.IntegrationTests.Scenarios.Store.Models; using Microsoft.EntityFrameworkCore; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -13,4 +15,16 @@ protected override IAsyncDisposable CreateContextHandle(out DbContext context) context = handle.Context; return handle; } + + [TestMethod] + public void Select_EnumSwitch_InlinesEnumConstantsIntoSql() + { + Expression> expr = o => o.GetStatusLabelSwitchStatement(); + var expanded = (Expression>)expr.ExpandExpressives(); + + var sql = Context.Set().Select(expanded).ToQueryString(); + + StringAssert.Contains(sql, "CASE"); + Assert.IsFalse(sql.Contains('@')); + } } diff --git a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/ConstructorTests.ProjectableConstructor_ReferencingStaticConstMember.verified.txt b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/ConstructorTests.ProjectableConstructor_ReferencingStaticConstMember.verified.txt index 258f52ec..3474e8e1 100644 --- a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/ConstructorTests.ProjectableConstructor_ReferencingStaticConstMember.verified.txt +++ b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/ConstructorTests.ProjectableConstructor_ReferencingStaticConstMember.verified.txt @@ -17,7 +17,7 @@ namespace ExpressiveSharp.Generated var p_first = global::System.Linq.Expressions.Expression.Parameter(typeof(string), "first"); var p_last = global::System.Linq.Expressions.Expression.Parameter(typeof(string), "last"); var expr_0 = global::System.Linq.Expressions.Expression.New(typeof(global::Foo.PersonDto).GetConstructor(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance, null, new global::System.Type[] { }, null)); - var expr_3 = global::System.Linq.Expressions.Expression.Field(null, typeof(global::Foo.PersonDto).GetField("Separator", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)); // Separator + var expr_3 = global::System.Linq.Expressions.Expression.Constant(" - ", typeof(string)); // Separator var expr_2 = global::System.Linq.Expressions.Expression.Call(typeof(string).GetMethod("Concat", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static, null, new global::System.Type[] { typeof(string), typeof(string) }, null), p_first, expr_3); var expr_1 = global::System.Linq.Expressions.Expression.Call(typeof(string).GetMethod("Concat", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static, null, new global::System.Type[] { typeof(string), typeof(string) }, null), expr_2, p_last); var expr_4 = global::System.Linq.Expressions.Expression.Bind(typeof(global::Foo.PersonDto).GetProperty("FullName", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance), expr_1); diff --git a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/EnumTests.EnumRelationalComparison.verified.txt b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/EnumTests.EnumRelationalComparison.verified.txt index d7e40137..bb109141 100644 --- a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/EnumTests.EnumRelationalComparison.verified.txt +++ b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/EnumTests.EnumRelationalComparison.verified.txt @@ -13,13 +13,13 @@ namespace ExpressiveSharp.Generated { var p__this = global::System.Linq.Expressions.Expression.Parameter(typeof(global::Foo.Entity), "@this"); var expr_2 = global::System.Linq.Expressions.Expression.Property(p__this, typeof(global::Foo.Entity).GetProperty("Value", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // Value - var expr_3 = global::System.Linq.Expressions.Expression.Field(null, typeof(global::Foo.Bucket).GetField("Low", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)); // Bucket.Low + var expr_3 = global::System.Linq.Expressions.Expression.Constant(global::Foo.Bucket.Low, typeof(global::Foo.Bucket)); // Bucket.Low var expr_4 = global::System.Linq.Expressions.Expression.Convert(expr_2, typeof(int)); var expr_5 = global::System.Linq.Expressions.Expression.Convert(expr_3, typeof(int)); var expr_1 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.LessThanOrEqual, expr_4, expr_5); var expr_6 = global::System.Linq.Expressions.Expression.Constant("low", typeof(string)); // "low" var expr_9 = global::System.Linq.Expressions.Expression.Property(p__this, typeof(global::Foo.Entity).GetProperty("Value", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // Value - var expr_10 = global::System.Linq.Expressions.Expression.Field(null, typeof(global::Foo.Bucket).GetField("Mid", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)); // Bucket.Mid + var expr_10 = global::System.Linq.Expressions.Expression.Constant(global::Foo.Bucket.Mid, typeof(global::Foo.Bucket)); // Bucket.Mid var expr_11 = global::System.Linq.Expressions.Expression.Convert(expr_9, typeof(int)); var expr_12 = global::System.Linq.Expressions.Expression.Convert(expr_10, typeof(int)); var expr_8 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.LessThanOrEqual, expr_11, expr_12); diff --git a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/EnumTests.EnumRelationalComparisonWithNullableEnum.verified.txt b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/EnumTests.EnumRelationalComparisonWithNullableEnum.verified.txt index 7f110c78..8a0b3e62 100644 --- a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/EnumTests.EnumRelationalComparisonWithNullableEnum.verified.txt +++ b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/EnumTests.EnumRelationalComparisonWithNullableEnum.verified.txt @@ -13,7 +13,7 @@ namespace ExpressiveSharp.Generated { var p__this = global::System.Linq.Expressions.Expression.Parameter(typeof(global::Foo.Entity), "@this"); var expr_1 = global::System.Linq.Expressions.Expression.Property(p__this, typeof(global::Foo.Entity).GetProperty("Value", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // Value - var expr_3 = global::System.Linq.Expressions.Expression.Field(null, typeof(global::Foo.Bucket).GetField("Mid", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)); // Bucket.Mid + var expr_3 = global::System.Linq.Expressions.Expression.Constant(global::Foo.Bucket.Mid, typeof(global::Foo.Bucket)); // Bucket.Mid var expr_2 = global::System.Linq.Expressions.Expression.Convert(expr_3, typeof(global::Foo.Bucket?)); var expr_4 = global::System.Linq.Expressions.Expression.Convert(expr_1, typeof(global::System.Nullable)); var expr_5 = global::System.Linq.Expressions.Expression.Convert(expr_2, typeof(global::System.Nullable)); diff --git a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/EnumTests.EnumRelationalPattern.verified.txt b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/EnumTests.EnumRelationalPattern.verified.txt index c8fc2baf..f5924520 100644 --- a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/EnumTests.EnumRelationalPattern.verified.txt +++ b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/EnumTests.EnumRelationalPattern.verified.txt @@ -13,7 +13,7 @@ namespace ExpressiveSharp.Generated { var p__this = global::System.Linq.Expressions.Expression.Parameter(typeof(global::Foo.Entity), "@this"); var expr_0 = global::System.Linq.Expressions.Expression.Property(p__this, typeof(global::Foo.Entity).GetProperty("Value", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // Value - var expr_2 = global::System.Linq.Expressions.Expression.Field(null, typeof(global::Foo.Bucket).GetField("Mid", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)); // Bucket.Mid + var expr_2 = global::System.Linq.Expressions.Expression.Constant(global::Foo.Bucket.Mid, typeof(global::Foo.Bucket)); // Bucket.Mid var expr_3 = global::System.Linq.Expressions.Expression.Convert(expr_0, typeof(int)); var expr_4 = global::System.Linq.Expressions.Expression.Convert(expr_2, typeof(int)); var expr_1 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.LessThanOrEqual, expr_3, expr_4); diff --git a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/EnumTests.EnumToEnumSwitchMapping.verified.txt b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/EnumTests.EnumToEnumSwitchMapping.verified.txt new file mode 100644 index 00000000..65f238db --- /dev/null +++ b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/EnumTests.EnumToEnumSwitchMapping.verified.txt @@ -0,0 +1,38 @@ +// +#nullable disable + +using Foo; + +namespace ExpressiveSharp.Generated +{ + static partial class Foo_Entity + { + // [Expressive] + // public GeneralStatus General => Status switch + // { + // ActualStatus.New => GeneralStatus.Defined, + // ActualStatus.InProgress => GeneralStatus.PartiallyProcessed, + // ActualStatus.Archived => GeneralStatus.Annulled, + // _ => throw new System.NotImplementedException()}; + static global::System.Linq.Expressions.Expression> General_Expression() + { + var p__this = global::System.Linq.Expressions.Expression.Parameter(typeof(global::Foo.Entity), "@this"); + var expr_0 = global::System.Linq.Expressions.Expression.Property(p__this, typeof(global::Foo.Entity).GetProperty("Status", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // Status + var expr_2 = global::System.Linq.Expressions.Expression.New(typeof(global::System.NotImplementedException).GetConstructor(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance, null, new global::System.Type[] { }, null)); // new System.NotImplementedException() + var expr_1 = global::System.Linq.Expressions.Expression.Throw(expr_2, typeof(global::Foo.GeneralStatus)); + var expr_4 = global::System.Linq.Expressions.Expression.Constant(global::Foo.ActualStatus.Archived, typeof(global::Foo.ActualStatus)); // ActualStatus.Archived + var expr_3 = global::System.Linq.Expressions.Expression.Equal(expr_0, expr_4); + var expr_5 = global::System.Linq.Expressions.Expression.Constant(global::Foo.GeneralStatus.Annulled, typeof(global::Foo.GeneralStatus)); // GeneralStatus.Annulled + var expr_6 = global::System.Linq.Expressions.Expression.Condition(expr_3, expr_5, expr_1, typeof(global::Foo.GeneralStatus)); + var expr_8 = global::System.Linq.Expressions.Expression.Constant(global::Foo.ActualStatus.InProgress, typeof(global::Foo.ActualStatus)); // ActualStatus.InProgress + var expr_7 = global::System.Linq.Expressions.Expression.Equal(expr_0, expr_8); + var expr_9 = global::System.Linq.Expressions.Expression.Constant(global::Foo.GeneralStatus.PartiallyProcessed, typeof(global::Foo.GeneralStatus)); // GeneralStatus.PartiallyProcessed + var expr_10 = global::System.Linq.Expressions.Expression.Condition(expr_7, expr_9, expr_6, typeof(global::Foo.GeneralStatus)); + var expr_12 = global::System.Linq.Expressions.Expression.Constant(global::Foo.ActualStatus.New, typeof(global::Foo.ActualStatus)); // ActualStatus.New + var expr_11 = global::System.Linq.Expressions.Expression.Equal(expr_0, expr_12); + var expr_13 = global::System.Linq.Expressions.Expression.Constant(global::Foo.GeneralStatus.Defined, typeof(global::Foo.GeneralStatus)); // GeneralStatus.Defined + var expr_14 = global::System.Linq.Expressions.Expression.Condition(expr_11, expr_13, expr_10, typeof(global::Foo.GeneralStatus)); + return global::System.Linq.Expressions.Expression.Lambda>(expr_14, p__this); + } + } +} diff --git a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/EnumTests.KeywordNamedEnumMembers.verified.txt b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/EnumTests.KeywordNamedEnumMembers.verified.txt new file mode 100644 index 00000000..434c4f0c --- /dev/null +++ b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/EnumTests.KeywordNamedEnumMembers.verified.txt @@ -0,0 +1,50 @@ +// +#nullable disable + +using Foo; + +namespace ExpressiveSharp.Generated +{ + static partial class Foo_Entity + { + // [Expressive] + // public bool IsClass => Value == Kind.@class; + static global::System.Linq.Expressions.Expression> IsClass_Expression() + { + var p__this = global::System.Linq.Expressions.Expression.Parameter(typeof(global::Foo.Entity), "@this"); + var expr_1 = global::System.Linq.Expressions.Expression.Property(p__this, typeof(global::Foo.Entity).GetProperty("Value", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // Value + var expr_2 = global::System.Linq.Expressions.Expression.Constant(global::Foo.Kind.@class, typeof(global::Foo.Kind)); // Kind.@class + var expr_0 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.Equal, expr_1, expr_2); + return global::System.Linq.Expressions.Expression.Lambda>(expr_0, p__this); + } + } +} + +// +#nullable disable + +using Foo; + +namespace ExpressiveSharp.Generated +{ + static partial class Foo_Entity + { + // [Expressive] + // public string Description => Value.Describe(); + static global::System.Linq.Expressions.Expression> Description_Expression() + { + var p__this = global::System.Linq.Expressions.Expression.Parameter(typeof(global::Foo.Entity), "@this"); + var expr_0 = global::System.Linq.Expressions.Expression.Property(p__this, typeof(global::Foo.Entity).GetProperty("Value", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // Value + var expr_1 = global::System.Linq.Expressions.Expression.Constant(null, typeof(string)); + var expr_2 = global::System.Linq.Expressions.Expression.Constant(global::Foo.Kind.@default, typeof(global::Foo.Kind)); + var expr_3 = global::System.Linq.Expressions.Expression.Call(typeof(global::Foo.KindExtensions).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.Kind) }, null), new global::System.Linq.Expressions.Expression[] { expr_2 }); + var expr_4 = global::System.Linq.Expressions.Expression.Equal(expr_0, expr_2); + var expr_5 = global::System.Linq.Expressions.Expression.Condition(expr_4, expr_3, expr_1, typeof(string)); + var expr_6 = global::System.Linq.Expressions.Expression.Constant(global::Foo.Kind.@class, typeof(global::Foo.Kind)); + var expr_7 = global::System.Linq.Expressions.Expression.Call(typeof(global::Foo.KindExtensions).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.Kind) }, null), new global::System.Linq.Expressions.Expression[] { expr_6 }); + var expr_8 = global::System.Linq.Expressions.Expression.Equal(expr_0, expr_6); + var expr_9 = global::System.Linq.Expressions.Expression.Condition(expr_8, expr_7, expr_5, typeof(string)); + return global::System.Linq.Expressions.Expression.Lambda>(expr_9, p__this); + } + } +} diff --git a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/EnumTests.PrivateConstFields.verified.txt b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/EnumTests.PrivateConstFields.verified.txt new file mode 100644 index 00000000..b71ad305 --- /dev/null +++ b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/EnumTests.PrivateConstFields.verified.txt @@ -0,0 +1,29 @@ +// +#nullable disable + +using Foo; + +namespace ExpressiveSharp.Generated +{ + static partial class Foo_Entity + { + // [Expressive] + // public bool Matches => Value == Floor && Amount > Threshold && Name == Label; + static global::System.Linq.Expressions.Expression> Matches_Expression() + { + var p__this = global::System.Linq.Expressions.Expression.Parameter(typeof(global::Foo.Entity), "@this"); + var expr_3 = global::System.Linq.Expressions.Expression.Property(p__this, typeof(global::Foo.Entity).GetProperty("Value", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // Value + var expr_4 = global::System.Linq.Expressions.Expression.Constant((global::Foo.Level)(-1), typeof(global::Foo.Level)); // Floor + var expr_2 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.Equal, expr_3, expr_4); + var expr_6 = global::System.Linq.Expressions.Expression.Property(p__this, typeof(global::Foo.Entity).GetProperty("Amount", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // Amount + var expr_7 = global::System.Linq.Expressions.Expression.Constant(10L, typeof(long)); // Threshold + var expr_5 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.GreaterThan, expr_6, expr_7); + var expr_1 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.AndAlso, expr_2, expr_5); + var expr_9 = global::System.Linq.Expressions.Expression.Property(p__this, typeof(global::Foo.Entity).GetProperty("Name", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // Name + var expr_10 = global::System.Linq.Expressions.Expression.Constant("a\"b", typeof(string)); // Label + var expr_8 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.Equal, expr_9, expr_10); + var expr_0 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.AndAlso, expr_1, expr_8); + return global::System.Linq.Expressions.Expression.Lambda>(expr_0, p__this); + } + } +} diff --git a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/EnumTests.cs b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/EnumTests.cs index c65d64df..84e6d621 100644 --- a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/EnumTests.cs +++ b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/EnumTests.cs @@ -389,4 +389,100 @@ public record Entity return Verifier.Verify(result.GeneratedTrees[0].ToString()); } + + [TestMethod] + public Task EnumToEnumSwitchMapping() + { + var compilation = CreateCompilation( + """ + namespace Foo { + public enum ActualStatus { New, InProgress, Archived } + public enum GeneralStatus { Defined, PartiallyProcessed, Annulled } + + public record Entity + { + public ActualStatus Status { get; set; } + + [Expressive] + public GeneralStatus General => Status switch + { + ActualStatus.New => GeneralStatus.Defined, + ActualStatus.InProgress => GeneralStatus.PartiallyProcessed, + ActualStatus.Archived => GeneralStatus.Annulled, + _ => throw new System.NotImplementedException() + }; + } + } + """); + var result = RunExpressiveGenerator(compilation); + + Assert.AreEqual(0, result.Diagnostics.Length); + Assert.AreEqual(1, result.GeneratedTrees.Length); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [TestMethod] + public Task KeywordNamedEnumMembers() + { + var compilation = CreateCompilation( + """ + namespace Foo { + public enum Kind { @class, @default } + + public static class KindExtensions + { + public static string Describe(this Kind value) => value.ToString(); + } + + public record Entity + { + public Kind Value { get; set; } + + [Expressive] + public bool IsClass => Value == Kind.@class; + + [Expressive] + public string Description => Value.Describe(); + } + } + """); + var result = RunExpressiveGenerator(compilation); + + Assert.AreEqual(0, result.Diagnostics.Length); + Assert.AreEqual(2, result.GeneratedTrees.Length); + + return Verifier.Verify(string.Join("\n", result.GeneratedTrees.Select(t => t.ToString()))); + } + + [TestMethod] + public Task PrivateConstFields() + { + var compilation = CreateCompilation( + """ + namespace Foo { + public enum Level { Negative = -1, Zero, Positive } + + public record Entity + { + private const Level Floor = Level.Negative; + private const long Threshold = 10; + private const string Label = "a\"b"; + + public Level Value { get; set; } + public long Amount { get; set; } + public string Name { get; set; } + + [Expressive] + public bool Matches => Value == Floor && Amount > Threshold && Name == Label; + } + } + """); + var result = RunExpressiveGenerator(compilation); + + Assert.AreEqual(0, result.Diagnostics.Length); + Assert.AreEqual(1, result.GeneratedTrees.Length); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } } diff --git a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/LoopTests.ForEach_ConditionalMinWithMemberAccess.verified.txt b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/LoopTests.ForEach_ConditionalMinWithMemberAccess.verified.txt index 78db1b83..abbbf642 100644 --- a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/LoopTests.ForEach_ConditionalMinWithMemberAccess.verified.txt +++ b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/LoopTests.ForEach_ConditionalMinWithMemberAccess.verified.txt @@ -24,7 +24,7 @@ namespace ExpressiveSharp.Generated var p__this = global::System.Linq.Expressions.Expression.Parameter(typeof(global::Foo.C), "@this"); var p_items = global::System.Linq.Expressions.Expression.Parameter(typeof(global::System.Collections.Generic.List), "items"); var expr_0 = global::System.Linq.Expressions.Expression.Variable(typeof(int), "min"); // { var min = int.MaxValue; foreach... - var expr_1 = global::System.Linq.Expressions.Expression.Field(null, typeof(int).GetField("MaxValue", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)); // int.MaxValue + var expr_1 = global::System.Linq.Expressions.Expression.Constant(2147483647, typeof(int)); // int.MaxValue var expr_2 = global::System.Linq.Expressions.Expression.Assign(expr_0, expr_1); var expr_4 = global::System.Linq.Expressions.Expression.Variable(typeof(global::Foo.Item), "x"); // foreach (var x in items) { if (x.Value > 0) min = Math.Mi... var expr_5 = global::System.Linq.Expressions.Expression.Variable(typeof(global::System.Collections.Generic.IEnumerator), "enumerator"); diff --git a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/LoopTests.ForEach_Min.verified.txt b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/LoopTests.ForEach_Min.verified.txt index 1c64286c..86accbfe 100644 --- a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/LoopTests.ForEach_Min.verified.txt +++ b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/LoopTests.ForEach_Min.verified.txt @@ -23,7 +23,7 @@ namespace ExpressiveSharp.Generated var p__this = global::System.Linq.Expressions.Expression.Parameter(typeof(global::Foo.C), "@this"); var p_items = global::System.Linq.Expressions.Expression.Parameter(typeof(global::System.Collections.Generic.List), "items"); var expr_0 = global::System.Linq.Expressions.Expression.Variable(typeof(int), "min"); // { var min = int.MaxValue; foreach... - var expr_1 = global::System.Linq.Expressions.Expression.Field(null, typeof(int).GetField("MaxValue", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)); // int.MaxValue + var expr_1 = global::System.Linq.Expressions.Expression.Constant(2147483647, typeof(int)); // int.MaxValue var expr_2 = global::System.Linq.Expressions.Expression.Assign(expr_0, expr_1); var expr_4 = global::System.Linq.Expressions.Expression.Variable(typeof(int), "x"); // foreach (var x in items) { min = Math.Min(min, x); } var expr_5 = global::System.Linq.Expressions.Expression.Variable(typeof(global::System.Collections.Generic.IEnumerator), "enumerator"); diff --git a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/MethodTests.ConstMember.verified.txt b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/MethodTests.ConstMember.verified.txt index 61990053..28173ee2 100644 --- a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/MethodTests.ConstMember.verified.txt +++ b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/MethodTests.ConstMember.verified.txt @@ -13,7 +13,7 @@ namespace ExpressiveSharp.Generated { var p__this = global::System.Linq.Expressions.Expression.Parameter(typeof(global::Foo.Foo), "@this"); var expr_1 = global::System.Linq.Expressions.Expression.Property(p__this, typeof(global::Foo.Foo).GetProperty("Id", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // Id - var expr_2 = global::System.Linq.Expressions.Expression.Field(null, typeof(global::Foo.Foo).GetField("Bar", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)); // Bar + var expr_2 = global::System.Linq.Expressions.Expression.Constant(1, typeof(int)); // Bar var expr_0 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.Add, expr_1, expr_2); return global::System.Linq.Expressions.Expression.Lambda>(expr_0, p__this); } diff --git a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/SwitchPatternTests.SwitchStatementWithReturnArms.verified.txt b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/SwitchPatternTests.SwitchStatementWithReturnArms.verified.txt index f85d6aa8..28f49bb9 100644 --- a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/SwitchPatternTests.SwitchStatementWithReturnArms.verified.txt +++ b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/SwitchPatternTests.SwitchStatementWithReturnArms.verified.txt @@ -25,10 +25,10 @@ namespace ExpressiveSharp.Generated var p_o = global::System.Linq.Expressions.Expression.Parameter(typeof(global::Foo.Order), "o"); var expr_0 = global::System.Linq.Expressions.Expression.Property(p_o, typeof(global::Foo.Order).GetProperty("Status", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // o.Status var expr_1 = global::System.Linq.Expressions.Expression.Constant("New", typeof(string)); // "New" - var expr_2 = global::System.Linq.Expressions.Expression.Field(null, typeof(global::Foo.OrderStatus).GetField("Pending", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)); // OrderStatus.Pending + var expr_2 = global::System.Linq.Expressions.Expression.Constant(global::Foo.OrderStatus.Pending, typeof(global::Foo.OrderStatus)); // OrderStatus.Pending var expr_3 = global::System.Linq.Expressions.Expression.Equal(expr_0, expr_2); var expr_4 = global::System.Linq.Expressions.Expression.Constant("Active", typeof(string)); // "Active" - var expr_5 = global::System.Linq.Expressions.Expression.Field(null, typeof(global::Foo.OrderStatus).GetField("Paid", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)); // OrderStatus.Paid + var expr_5 = global::System.Linq.Expressions.Expression.Constant(global::Foo.OrderStatus.Paid, typeof(global::Foo.OrderStatus)); // OrderStatus.Paid var expr_6 = global::System.Linq.Expressions.Expression.Equal(expr_0, expr_5); var expr_7 = global::System.Linq.Expressions.Expression.Constant("Unknown", typeof(string)); // "Unknown" var expr_8 = global::System.Linq.Expressions.Expression.Condition(expr_6, expr_4, expr_7, typeof(string)); diff --git a/tests/ExpressiveSharp.IntegrationTests/Tests/EnumComparisonTests.cs b/tests/ExpressiveSharp.IntegrationTests/Tests/EnumComparisonTests.cs index 4a4f377a..ce78115e 100644 --- a/tests/ExpressiveSharp.IntegrationTests/Tests/EnumComparisonTests.cs +++ b/tests/ExpressiveSharp.IntegrationTests/Tests/EnumComparisonTests.cs @@ -108,6 +108,49 @@ public void ExtensionOnNullableEnum_NullBranch_PreservesUserDefinedFallback() // The null branch must invoke Describe(null) so the extension's own null handling wins. CollectionAssert.AreEqual(new[] { "Low", "n/a" }, labels); } + + [TestMethod] + public void EnumToEnumSwitch_ExpandExpressives_MaterializesAndEvaluates() + { + var source = new List + { + new() { Value = Bucket.Low }, + new() { Value = Bucket.Mid }, + new() { Value = Bucket.High }, + }.AsQueryable(); + + Expression> expr = e => e.Grade; + var expanded = (Expression>)expr.ExpandExpressives(); + + var grades = source.Select(expanded.Compile()).ToList(); + + CollectionAssert.AreEqual(new[] { Grade.Poor, Grade.Fair, Grade.Good }, grades); + } + + [TestMethod] + public void EnumToEnumSwitch_ExpandExpressives_EmitsConstantsNotFieldAccess() + { + Expression> expr = e => e.Grade; + var expanded = expr.ExpandExpressives(); + + var finder = new LiteralFieldAccessFinder(); + finder.Visit(expanded); + + Assert.AreEqual(0, finder.Count); + } + + private sealed class LiteralFieldAccessFinder : ExpressionVisitor + { + public int Count { get; private set; } + + protected override Expression VisitMember(MemberExpression node) + { + if (node.Member is System.Reflection.FieldInfo { IsLiteral: true }) + Count++; + + return base.VisitMember(node); + } + } } public static class BucketExtensions @@ -138,10 +181,21 @@ public class EnumComparisonEntity [Expressive] public string NullableDescription => NullableValue.Describe(); + + [Expressive] + public Grade Grade => Value switch + { + Bucket.Low => Grade.Poor, + Bucket.Mid => Grade.Fair, + Bucket.High => Grade.Good, + _ => throw new NotImplementedException() + }; } public enum Bucket { Low, Mid, High } +public enum Grade { Poor, Fair, Good } + public partial class EnumComparisonProperty { public Bucket Value { get; init; }