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
7 changes: 1 addition & 6 deletions docs/advanced/ioperation-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ Unrecognized operations fall through to `EmitUnsupported()` which emits `Express
| `IConditionalAccessOperation` | `Expression.Condition(notNullCheck, whenNotNull, default)` | Implemented | `?.` operator with receiver stack for chained access. Handles `Nullable<T>` `.Value` injection. |
| `IConditionalAccessInstanceOperation` | *(resolved to receiver)* | Implemented | Pops from receiver stack during conditional access processing. |
| `ICoalesceOperation` | `Expression.Coalesce(left, right)` | Implemented | `??` operator. |
| `IThrowOperation` | `Expression.Throw(expr, typeof(T))` | Implemented | `throw` expressions. Uses typed overload in value positions (`?? throw`, ternary, switch arm), void overload for statements. `ReplaceThrowWithDefault` transformer replaces with `Expression.Default` for EF Core compatibility. |

## Pattern Matching

Expand Down Expand Up @@ -186,12 +187,6 @@ Unrecognized operations fall through to `EmitUnsupported()` which emits `Express

---

## Not Yet Implemented

| IOperation | Target Expression Factory | Notes |
|---|---|---|
| `IThrowOperation` | `Expression.Throw(expr, typeof(T))` | `throw` expressions. `Expression.Throw` exists but not all LINQ providers support it. Detected early by block body validation (EXP0006). |

---

## Not Applicable to Expression Trees
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ public sealed class ExpressiveOptionsBuilder
{
internal List<IExpressivePlugin> Plugins { get; } = [];

internal bool ShouldPreserveThrowExpressions { get; private set; }

/// <summary>
/// Registers an <see cref="IExpressivePlugin"/> that contributes services and/or
/// expression tree transformers to the EF Core pipeline.
Expand All @@ -17,4 +19,15 @@ public ExpressiveOptionsBuilder AddPlugin(IExpressivePlugin plugin)
Plugins.Add(plugin);
return this;
}

/// <summary>
/// Prevents the <see cref="ExpressiveSharp.Transformers.ReplaceThrowWithDefault"/> transformer from
/// being applied. When set, <c>Expression.Throw</c> nodes are preserved in the
/// expression tree, and the LINQ provider is responsible for translating them.
/// </summary>
public ExpressiveOptionsBuilder PreserveThrowExpressions()
{
ShouldPreserveThrowExpressions = true;
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public static DbContextOptionsBuilder UseExpressives(
var builder = new ExpressiveOptionsBuilder();
configure(builder);

var extension = new ExpressiveOptionsExtension(builder.Plugins);
var extension = new ExpressiveOptionsExtension(builder.Plugins, builder.ShouldPreserveThrowExpressions);

((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,21 @@ namespace ExpressiveSharp.EntityFrameworkCore.Infrastructure.Internal;
public class ExpressiveOptionsExtension : IDbContextOptionsExtension
{
private readonly IReadOnlyList<IExpressivePlugin> _plugins;
private readonly bool _preserveThrowExpressions;
private readonly int _pluginHash;

public ExpressiveOptionsExtension(IReadOnlyList<IExpressivePlugin> plugins)
public ExpressiveOptionsExtension(IReadOnlyList<IExpressivePlugin> plugins, bool preserveThrowExpressions = false)
{
_plugins = plugins;
_preserveThrowExpressions = preserveThrowExpressions;

var hash = new HashCode();
foreach (var plugin in plugins)
{
hash.Add(plugin.GetType().FullName);
hash.Add(plugin.GetHashCode());
}
hash.Add(preserveThrowExpressions);
_pluginHash = hash.ToHashCode();

Info = new ExtensionInfo(this);
Expand Down Expand Up @@ -79,9 +82,12 @@ public void ApplyServices(IServiceCollection services)

// Register a dedicated ExpressiveOptions instance with EF Core transformers
// plus any transformers contributed by plugins
var preserveThrow = _preserveThrowExpressions;
services.AddSingleton(sp =>
{
var options = new ExpressiveOptions();
if (!preserveThrow)
options.AddTransformers(new ReplaceThrowWithDefault());
options.AddTransformers(
new ConvertLoopsToLinq(),
new RemoveNullConditionalPatterns(),
Expand Down
34 changes: 34 additions & 0 deletions src/ExpressiveSharp.Generator/Emitter/ExpressionTreeEmitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,7 @@ private string EmitOperation(IOperation operation)
IForEachLoopOperation forEach => EmitForEachLoop(forEach),
IForLoopOperation forLoop => EmitForLoop(forLoop),
IWhileLoopOperation whileLoop => EmitWhileLoop(whileLoop),
IThrowOperation throwOp => EmitThrow(throwOp),
_ => EmitUnsupported(operation),
};

Expand Down Expand Up @@ -949,6 +950,11 @@ private string EmitConversion(IConversionOperation conversion)
if (conversion.Conversion.IsIdentity)
return EmitOperation(conversion.Operand);

// Throw expressions are void-typed; Expression.Convert(void, T) is invalid.
// Emit Expression.Throw(exception, targetType) directly instead.
if (conversion.Operand is IThrowOperation throwOp && throwOp.Exception is not null)
return EmitThrowWithType(throwOp, conversion.Type);

var resultVar = NextVar();
var operandVar = EmitOperation(conversion.Operand);
var targetTypeFqn = conversion.Type?.ToDisplayString(_fqnFormat) ?? "object";
Expand Down Expand Up @@ -1237,6 +1243,34 @@ private string EmitCoalesce(ICoalesceOperation coalesce)
return resultVar;
}

// ── Throw expressions ─────────────────────────────────────────────────────

private string EmitThrow(IThrowOperation throwOp)
{
if (throwOp.Exception is null)
return EmitUnsupported(throwOp);

return EmitThrowWithType(throwOp, throwOp.Type);
}

private string EmitThrowWithType(IThrowOperation throwOp, ITypeSymbol? targetType)
{
var resultVar = NextVar();
var exceptionVar = EmitOperation(throwOp.Exception!);

if (targetType is not null && targetType.SpecialType != SpecialType.System_Void)
{
var typeFqn = ResolveTypeFqn(targetType);
AppendLine($"var {resultVar} = {Expr}.Throw({exceptionVar}, typeof({typeFqn}));");
}
else
{
AppendLine($"var {resultVar} = {Expr}.Throw({exceptionVar});");
}

return resultVar;
}

// ── Arrays ───────────────────────────────────────────────────────────────

private string EmitArrayCreation(IArrayCreationOperation arrayCreate)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -414,13 +414,6 @@ private static void WalkOperations(
memberName, "lock statement"));
return;

case IThrowOperation:
context.ReportDiagnostic(Diagnostic.Create(
Diagnostics.UnsupportedStatementInBlockBody,
operation.Syntax?.GetLocation() ?? Location.None,
memberName, "throw expression"));
return;

case IForLoopOperation:
context.ReportDiagnostic(Diagnostic.Create(
Diagnostics.UnsupportedStatementInBlockBody,
Expand Down
25 changes: 25 additions & 0 deletions src/ExpressiveSharp/Transformers/ReplaceThrowWithDefault.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Linq.Expressions;

namespace ExpressiveSharp.Transformers;

/// <summary>
/// Replaces <see cref="ExpressionType.Throw"/> nodes with <see cref="Expression.Default(Type)"/>.
/// </summary>
/// <remarks>
/// This is useful for LINQ providers like EF Core that cannot translate
/// <c>Expression.Throw</c> to SQL. The throw node is replaced with a
/// type-compatible default, preserving the surrounding tree structure
/// (e.g., <c>Coalesce</c>, <c>Condition</c>) and its type contracts.
/// </remarks>
public sealed class ReplaceThrowWithDefault : ExpressionVisitor, IExpressionTreeTransformer
{
public Expression Transform(Expression expression)
=> Visit(expression);

protected override Expression VisitUnary(UnaryExpression node)
{
if (node.NodeType == ExpressionType.Throw)
return Expression.Default(node.Type);
return base.VisitUnary(node);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// <auto-generated/>
#nullable disable

using Foo;

namespace ExpressiveSharp.Generated
{
static partial class Foo_C
{
// [Expressive(AllowBlockBody = true)]
// public int Foo()
// {
// if (true)
// throw new System.Exception();
// return 1;
// }
static global::System.Linq.Expressions.Expression<global::System.Func<global::Foo.C, int>> Foo_Expression()
{
var p__this = global::System.Linq.Expressions.Expression.Parameter(typeof(global::Foo.C), "@this");
var expr_1 = global::System.Linq.Expressions.Expression.Constant(true, typeof(bool)); // true
var expr_3 = global::System.Linq.Expressions.Expression.New(typeof(global::System.Exception).GetConstructor(new global::System.Type[] { })); // new System.Exception()
var expr_2 = global::System.Linq.Expressions.Expression.Throw(expr_3);
var expr_0 = global::System.Linq.Expressions.Expression.IfThen(expr_1, expr_2);
var expr_4 = global::System.Linq.Expressions.Expression.Constant(1, typeof(int)); // 1
var expr_5 = global::System.Linq.Expressions.Expression.Block(global::System.Array.Empty<global::System.Linq.Expressions.ParameterExpression>(), new global::System.Linq.Expressions.Expression[] { expr_0, expr_4 });
return global::System.Linq.Expressions.Expression.Lambda<global::System.Func<global::Foo.C, int>>(expr_5, p__this);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ public async Task<int> Foo()
}

[TestMethod]
public void BlockBody_WithThrow_ReportsWarning()
public Task BlockBody_WithThrow()
{
var compilation = CreateCompilation(
"""
Expand All @@ -303,7 +303,9 @@ public int Foo()
""");
var result = RunExpressiveGenerator(compilation);

Assert.IsTrue(result.Diagnostics.Any(d => d.Id == "EXP0006"),
"Expected EXP0006 warning for throw in block body");
Assert.AreEqual(0, result.Diagnostics.Length);
Assert.AreEqual(1, result.GeneratedTrees.Length);

return Verifier.Verify(result.GeneratedTrees[0].ToString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,28 +102,6 @@ class C {
"Expected EXP0007 for nested member initializer");
}

// ── EXP0008: UnsupportedOperation ───────────────────────────────────────

[TestMethod]
public void ThrowExpression_ReportsEXP0008()
{
var compilation = CreateCompilation(
"""
namespace Foo {
class C {
public string? Name { get; set; }

[Expressive]
public string SafeName => Name ?? throw new System.InvalidOperationException();
}
}
""");
var result = RunExpressiveGenerator(compilation);

Assert.IsTrue(result.Diagnostics.Any(d => d.Id == "EXP0008"),
"Expected EXP0008 for throw expression (IThrowOperation not handled)");
}

// ── EXP0009: UnsupportedOperator ────────────────────────────────────────

[TestMethod]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// <auto-generated/>
#nullable disable

using Foo;

namespace ExpressiveSharp.Generated
{
static partial class Foo_C
{
// [Expressive]
// public static int SafeValue(int? x) => x.HasValue ? x.Value : throw new System.ArgumentNullException();
static global::System.Linq.Expressions.Expression<global::System.Func<int?, int>> SafeValue_P0_int__Expression()
{
var p_x = global::System.Linq.Expressions.Expression.Parameter(typeof(int?), "x");
var expr_1 = global::System.Linq.Expressions.Expression.Property(p_x, typeof(int?).GetProperty("HasValue")); // x.HasValue
var expr_2 = global::System.Linq.Expressions.Expression.Property(p_x, typeof(int?).GetProperty("Value")); // x.Value
var expr_5 = global::System.Linq.Expressions.Expression.New(typeof(global::System.ArgumentNullException).GetConstructor(new global::System.Type[] { })); // new System.ArgumentNullException()
var expr_4 = global::System.Linq.Expressions.Expression.Convert(expr_5, typeof(global::System.Exception));
var expr_3 = global::System.Linq.Expressions.Expression.Throw(expr_4, typeof(int));
var expr_0 = global::System.Linq.Expressions.Expression.Condition(expr_1, expr_2, expr_3, typeof(int));
return global::System.Linq.Expressions.Expression.Lambda<global::System.Func<int?, int>>(expr_0, p_x);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// <auto-generated/>
#nullable disable

using Foo;

namespace ExpressiveSharp.Generated
{
static partial class Foo_C
{
// [Expressive]
// public string SafeName => Name ?? throw new System.InvalidOperationException();
static global::System.Linq.Expressions.Expression<global::System.Func<global::Foo.C, string>> SafeName_Expression()
{
var p__this = global::System.Linq.Expressions.Expression.Parameter(typeof(global::Foo.C), "@this");
var expr_1 = global::System.Linq.Expressions.Expression.Property(p__this, typeof(global::Foo.C).GetProperty("Name")); // Name
var expr_4 = global::System.Linq.Expressions.Expression.New(typeof(global::System.InvalidOperationException).GetConstructor(new global::System.Type[] { })); // new System.InvalidOperationException()
var expr_3 = global::System.Linq.Expressions.Expression.Convert(expr_4, typeof(global::System.Exception));
var expr_2 = global::System.Linq.Expressions.Expression.Throw(expr_3, typeof(string));
var expr_0 = global::System.Linq.Expressions.Expression.Coalesce(expr_1, expr_2);
return global::System.Linq.Expressions.Expression.Lambda<global::System.Func<global::Foo.C, string>>(expr_0, p__this);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// <auto-generated/>
#nullable disable

using Foo;

namespace ExpressiveSharp.Generated
{
static partial class Foo_C
{
// [Expressive]
// public static string SafeValue(string? x) => x switch
// {
// null => throw new System.ArgumentNullException(),
// _ => x,
// };
static global::System.Linq.Expressions.Expression<global::System.Func<string, string>> SafeValue_P0_string_Expression()
{
var p_x = global::System.Linq.Expressions.Expression.Parameter(typeof(string), "x");
var expr_2 = global::System.Linq.Expressions.Expression.Constant(null, typeof(object)); // null
var expr_1 = global::System.Linq.Expressions.Expression.Convert(expr_2, typeof(string));
var expr_0 = global::System.Linq.Expressions.Expression.Equal(p_x, expr_1);
var expr_5 = global::System.Linq.Expressions.Expression.New(typeof(global::System.ArgumentNullException).GetConstructor(new global::System.Type[] { })); // new System.ArgumentNullException()
var expr_4 = global::System.Linq.Expressions.Expression.Convert(expr_5, typeof(global::System.Exception));
var expr_3 = global::System.Linq.Expressions.Expression.Throw(expr_4, typeof(string));
var expr_6 = global::System.Linq.Expressions.Expression.Condition(expr_0, expr_3, p_x, typeof(string));
return global::System.Linq.Expressions.Expression.Lambda<global::System.Func<string, string>>(expr_6, p_x);
}
}
}
Loading
Loading