Skip to content

Commit 30b1868

Browse files
committed
Enhance constructor handling in ExpressiveSharp generator and fixed various issues
1 parent 8026759 commit 30b1868

16 files changed

Lines changed: 661 additions & 150 deletions

src/ExpressiveSharp.Generator/Emitter/ExpressionTreeEmitter.cs

Lines changed: 287 additions & 50 deletions
Large diffs are not rendered by default.

src/ExpressiveSharp.Generator/Interpretation/ExpressiveInterpreter.BodyProcessors.cs

Lines changed: 97 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -339,21 +339,61 @@ private static bool TryApplyConstructorBody(
339339
descriptor.ReturnTypeName = fullTypeName;
340340
ApplyParameterList(constructorDeclarationSyntax.ParameterList, declarationSyntaxRewriter, descriptor);
341341

342-
// Verify parameterless constructor exists
343-
var hasAccessibleParameterlessConstructor = containingType.Constructors
344-
.Any(c => !c.IsStatic
345-
&& c.Parameters.IsEmpty
346-
&& c.DeclaredAccessibility is Accessibility.Public
347-
or Accessibility.Internal
348-
or Accessibility.ProtectedOrInternal);
342+
// Detect `: this(...)` chaining to a parameterized ctor (records' primary ctor or any
343+
// `this(args)` overload). In that case we emit Expression.New(targetCtor, args) so the
344+
// target ctor is invoked with the caller's values — the parameterless requirement is
345+
// then irrelevant because we never synthesize `new T()`.
346+
IMethodSymbol? chainedTargetCtor = null;
347+
List<SyntaxNode>? chainedArgExpressions = null;
348+
if (constructorDeclarationSyntax.Initializer is { } initializer
349+
&& initializer.ThisOrBaseKeyword.IsKind(SyntaxKind.ThisKeyword))
350+
{
351+
if (semanticModel.GetSymbolInfo(initializer).Symbol is IMethodSymbol target
352+
&& target.Parameters.Length > 0)
353+
{
354+
chainedTargetCtor = target;
355+
chainedArgExpressions = initializer.ArgumentList.Arguments
356+
.Select(a => (SyntaxNode)a.Expression)
357+
.ToList();
358+
}
359+
}
349360

350-
if (!hasAccessibleParameterlessConstructor)
361+
// Verify parameterless constructor exists — skip when chaining to a parameterized ctor.
362+
if (chainedTargetCtor is null)
363+
{
364+
var hasAccessibleParameterlessConstructor = containingType.Constructors
365+
.Any(c => !c.IsStatic
366+
&& c.Parameters.IsEmpty
367+
&& c.DeclaredAccessibility is Accessibility.Public
368+
or Accessibility.Internal
369+
or Accessibility.ProtectedOrInternal);
370+
371+
if (!hasAccessibleParameterlessConstructor)
372+
{
373+
context.ReportDiagnostic(Diagnostic.Create(
374+
Diagnostics.MissingParameterlessConstructor,
375+
constructorDeclarationSyntax.GetLocation(),
376+
containingType.Name));
377+
return false;
378+
}
379+
}
380+
381+
// Preserve EXP0003: report when a `: base(...)` initializer targets a ctor with no
382+
// available source (e.g. BCL exception base classes). The expression tree cannot
383+
// represent a call to such a ctor — we still emit the outer body's bindings via the
384+
// parameterless ctor, but users should be warned that the base-ctor side effects are
385+
// not captured.
386+
if (constructorDeclarationSyntax.Initializer is { } baseInit
387+
&& baseInit.ThisOrBaseKeyword.IsKind(SyntaxKind.BaseKeyword)
388+
&& semanticModel.GetSymbolInfo(baseInit).Symbol is IMethodSymbol baseTarget
389+
&& !baseTarget.DeclaringSyntaxReferences.Any(r => r.GetSyntax() is ConstructorDeclarationSyntax))
351390
{
352391
context.ReportDiagnostic(Diagnostic.Create(
353-
Diagnostics.MissingParameterlessConstructor,
354-
constructorDeclarationSyntax.GetLocation(),
355-
containingType.Name));
356-
return false;
392+
Diagnostics.NoSourceAvailableForDelegatedConstructor,
393+
baseTarget.Locations.FirstOrDefault() ?? Location.None,
394+
baseTarget.ToDisplayString(),
395+
baseTarget.ContainingType?.ToDisplayString() ?? "<unknown>",
396+
memberSymbol.Name));
357397
}
358398

359399
// Pass the constructor body to the emitter — it will emit the block as-is.
@@ -374,17 +414,6 @@ or Accessibility.Internal
374414
return ReportRequiresBodyAndFail(context, constructorDeclarationSyntax, memberSymbol.Name);
375415
}
376416

377-
// Collect assignments from base/this delegated constructors
378-
var delegatedBindings = new List<(IPropertySymbol Property, SyntaxNode ValueSyntax)>();
379-
if (constructorDeclarationSyntax.Initializer is { } initializer && compilation is not null)
380-
{
381-
var initializerSymbol = semanticModel.GetSymbolInfo(initializer).Symbol as IMethodSymbol;
382-
if (initializerSymbol is not null)
383-
{
384-
CollectDelegatedBindings(initializerSymbol, compilation, delegatedBindings, context, memberSymbol.Name);
385-
}
386-
}
387-
388417
var emitter = new ExpressionTreeEmitter(semanticModel, context);
389418

390419
// Build emitter parameters (constructor params, no @this)
@@ -403,76 +432,11 @@ or Accessibility.Internal
403432

404433
descriptor.ExpressionTreeEmission = emitter.EmitConstructor(
405434
bodySyntax, emitterParams, descriptor.ReturnTypeName!, delegateTypeFqn,
406-
containingType, delegatedBindings);
435+
containingType, chainedTargetCtor, chainedArgExpressions);
407436

408437
return true;
409438
}
410439

411-
/// <summary>
412-
/// Recursively collects property assignments from a delegated constructor chain.
413-
/// </summary>
414-
private static void CollectDelegatedBindings(
415-
IMethodSymbol delegatedCtor,
416-
Compilation compilation,
417-
List<(IPropertySymbol Property, SyntaxNode ValueSyntax)> bindings,
418-
SourceProductionContext context,
419-
string memberName)
420-
{
421-
var syntax = delegatedCtor.DeclaringSyntaxReferences
422-
.Select(r => r.GetSyntax())
423-
.OfType<ConstructorDeclarationSyntax>()
424-
.FirstOrDefault();
425-
426-
if (syntax is null)
427-
{
428-
context.ReportDiagnostic(Diagnostic.Create(
429-
Diagnostics.NoSourceAvailableForDelegatedConstructor,
430-
delegatedCtor.Locations.FirstOrDefault() ?? Location.None,
431-
delegatedCtor.ToDisplayString(),
432-
delegatedCtor.ContainingType?.ToDisplayString() ?? "<unknown>",
433-
memberName));
434-
return;
435-
}
436-
437-
// Follow the chain recursively
438-
if (syntax.Initializer is { } initializer)
439-
{
440-
var delegatedSemanticModel = compilation.GetSemanticModel(syntax.SyntaxTree);
441-
var initializerSymbol = delegatedSemanticModel.GetSymbolInfo(initializer).Symbol as IMethodSymbol;
442-
if (initializerSymbol is not null)
443-
{
444-
CollectDelegatedBindings(initializerSymbol, compilation, bindings, context, memberName);
445-
}
446-
}
447-
448-
// Collect property assignments from this constructor's body
449-
if (syntax.Body is not null)
450-
{
451-
var ctorSemanticModel = compilation.GetSemanticModel(syntax.SyntaxTree);
452-
foreach (var statement in syntax.Body.Statements)
453-
{
454-
if (statement is ExpressionStatementSyntax { Expression: AssignmentExpressionSyntax assignment }
455-
&& assignment.Left is MemberAccessExpressionSyntax memberAccess)
456-
{
457-
var symbol = ctorSemanticModel.GetSymbolInfo(memberAccess).Symbol;
458-
if (symbol is IPropertySymbol prop)
459-
{
460-
bindings.Add((prop, assignment.Right));
461-
}
462-
}
463-
else if (statement is ExpressionStatementSyntax { Expression: AssignmentExpressionSyntax simpleAssignment }
464-
&& simpleAssignment.Left is IdentifierNameSyntax identifierName)
465-
{
466-
var symbol = ctorSemanticModel.GetSymbolInfo(identifierName).Symbol;
467-
if (symbol is IPropertySymbol prop)
468-
{
469-
bindings.Add((prop, simpleAssignment.Right));
470-
}
471-
}
472-
}
473-
}
474-
}
475-
476440
/// <summary>
477441
/// Shared helper: emits expression tree building code for a method body.
478442
/// </summary>
@@ -628,11 +592,17 @@ private static void WalkOperations(
628592
memberName, "??= operator"));
629593
return;
630594

631-
case IDeconstructionAssignmentOperation:
632-
context.ReportDiagnostic(Diagnostic.Create(
633-
Diagnostics.UnsupportedStatementInBlockBody,
634-
operation.Syntax?.GetLocation() ?? Location.None,
635-
memberName, "deconstruction assignment"));
595+
case IDeconstructionAssignmentOperation decon:
596+
// Accept the tuple-literal-to-tuple-literal shape (e.g. `(A, B) = (x, y);`) —
597+
// the ctor-body emitter decomposes it into individual property bindings.
598+
// Reject everything else (Deconstruct method calls, nested tuples, discards, etc.).
599+
if (!IsSimpleTupleLiteralDeconstruction(decon))
600+
{
601+
context.ReportDiagnostic(Diagnostic.Create(
602+
Diagnostics.UnsupportedStatementInBlockBody,
603+
operation.Syntax?.GetLocation() ?? Location.None,
604+
memberName, "deconstruction assignment"));
605+
}
636606
return;
637607

638608
case IDynamicInvocationOperation or IDynamicMemberReferenceOperation
@@ -650,4 +620,37 @@ private static void WalkOperations(
650620
WalkOperations(child, memberName, context);
651621
}
652622
}
623+
624+
/// <summary>
625+
/// Returns true when the deconstruction has the shape
626+
/// <c>(member, member, ...) = (value, value, ...)</c> — two tuple literals of equal arity
627+
/// with the left side referencing only properties or fields of <c>this</c>. Only this shape
628+
/// is supported by the constructor-body emitter.
629+
/// </summary>
630+
private static bool IsSimpleTupleLiteralDeconstruction(IDeconstructionAssignmentOperation decon)
631+
{
632+
var target = UnwrapConversions(decon.Target);
633+
var value = UnwrapConversions(decon.Value);
634+
635+
if (target is not ITupleOperation targetTuple || value is not ITupleOperation valueTuple
636+
|| targetTuple.Elements.Length != valueTuple.Elements.Length)
637+
{
638+
return false;
639+
}
640+
641+
foreach (var element in targetTuple.Elements)
642+
{
643+
var unwrapped = UnwrapConversions(element);
644+
if (unwrapped is not IPropertyReferenceOperation && unwrapped is not IFieldReferenceOperation)
645+
return false;
646+
}
647+
return true;
648+
}
649+
650+
private static IOperation UnwrapConversions(IOperation operation)
651+
{
652+
while (operation is IConversionOperation conv)
653+
operation = conv.Operand;
654+
return operation;
655+
}
653656
}

tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/ConstructorTests.ProjectableConstructor_ThisInitializer_ChainedThisAndBase.verified.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ namespace ExpressiveSharp.Generated
1717
var p_id = global::System.Linq.Expressions.Expression.Parameter(typeof(int), "id");
1818
var p_name = global::System.Linq.Expressions.Expression.Parameter(typeof(string), "name");
1919
var p_suffix = global::System.Linq.Expressions.Expression.Parameter(typeof(string), "suffix");
20-
var expr_0 = global::System.Linq.Expressions.Expression.New(typeof(global::Foo.Child).GetConstructor(new global::System.Type[] { }));
20+
var expr_0 = global::System.Linq.Expressions.Expression.New(typeof(global::Foo.Child).GetConstructor(new global::System.Type[] { typeof(int), typeof(string) }), p_id, p_name);
2121
var p___this = global::System.Linq.Expressions.Expression.Parameter(typeof(object), "@this"); // Name
2222
var expr_2 = global::System.Linq.Expressions.Expression.Property(p___this, typeof(global::Foo.Child).GetProperty("Name"));
2323
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_suffix);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// <auto-generated/>
2+
#nullable disable
3+
4+
using Foo;
5+
6+
namespace ExpressiveSharp.Generated
7+
{
8+
static partial class Foo_OrderSummary
9+
{
10+
// [Expressive]
11+
// public OrderSummary(int id, int unit, int qty) : this(id, unit * qty)
12+
// {
13+
// }
14+
static global::System.Linq.Expressions.Expression<global::System.Func<int, int, int, global::Foo.OrderSummary>> _ctor_P0_int_P1_int_P2_int_Expression()
15+
{
16+
var p_id = global::System.Linq.Expressions.Expression.Parameter(typeof(int), "id");
17+
var p_unit = global::System.Linq.Expressions.Expression.Parameter(typeof(int), "unit");
18+
var p_qty = global::System.Linq.Expressions.Expression.Parameter(typeof(int), "qty");
19+
var expr_1 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.Multiply, p_unit, p_qty); // unit * qty
20+
var expr_0 = global::System.Linq.Expressions.Expression.New(typeof(global::Foo.OrderSummary).GetConstructor(new global::System.Type[] { typeof(int), typeof(decimal) }), p_id, expr_1);
21+
return global::System.Linq.Expressions.Expression.Lambda<global::System.Func<int, int, int, global::Foo.OrderSummary>>(expr_0, p_id, p_unit, p_qty);
22+
}
23+
}
24+
}

tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/ConstructorTests.ProjectableConstructor_ThisInitializer_RefPreviouslyAssignedProperty.verified.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ namespace ExpressiveSharp.Generated
1414
static global::System.Linq.Expressions.Expression<global::System.Func<string, global::Foo.PersonDto>> _ctor_P0_string_Expression()
1515
{
1616
var p_firstName = global::System.Linq.Expressions.Expression.Parameter(typeof(string), "firstName");
17-
var expr_0 = global::System.Linq.Expressions.Expression.New(typeof(global::Foo.PersonDto).GetConstructor(new global::System.Type[] { }));
17+
var expr_1 = global::System.Linq.Expressions.Expression.Constant("Doe", typeof(string)); // "Doe"
18+
var expr_0 = global::System.Linq.Expressions.Expression.New(typeof(global::Foo.PersonDto).GetConstructor(new global::System.Type[] { typeof(string), typeof(string) }), p_firstName, expr_1);
1819
return global::System.Linq.Expressions.Expression.Lambda<global::System.Func<string, global::Foo.PersonDto>>(expr_0, p_firstName);
1920
}
2021
}

tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/ConstructorTests.ProjectableConstructor_ThisInitializer_SimpleOverload.verified.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ namespace ExpressiveSharp.Generated
1414
static global::System.Linq.Expressions.Expression<global::System.Func<string, global::Foo.PersonDto>> _ctor_P0_string_Expression()
1515
{
1616
var p_fullName = global::System.Linq.Expressions.Expression.Parameter(typeof(string), "fullName");
17-
var expr_0 = global::System.Linq.Expressions.Expression.New(typeof(global::Foo.PersonDto).GetConstructor(new global::System.Type[] { }));
17+
var expr_1 = global::System.Linq.Expressions.Expression.Call(p_fullName, typeof(string).GetMethod("ToUpper", 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>()); // fullName.ToUpper()
18+
var expr_2 = global::System.Linq.Expressions.Expression.Call(p_fullName, typeof(string).GetMethod("ToLower", 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>()); // fullName.ToLower()
19+
var expr_0 = global::System.Linq.Expressions.Expression.New(typeof(global::Foo.PersonDto).GetConstructor(new global::System.Type[] { typeof(string), typeof(string) }), expr_1, expr_2);
1820
return global::System.Linq.Expressions.Expression.Lambda<global::System.Func<string, global::Foo.PersonDto>>(expr_0, p_fullName);
1921
}
2022
}

tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/ConstructorTests.ProjectableConstructor_ThisInitializer_WithBodyAfter.verified.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ namespace ExpressiveSharp.Generated
1717
var p_fn = global::System.Linq.Expressions.Expression.Parameter(typeof(string), "fn");
1818
var p_ln = global::System.Linq.Expressions.Expression.Parameter(typeof(string), "ln");
1919
var p_upper = global::System.Linq.Expressions.Expression.Parameter(typeof(bool), "upper");
20-
var expr_0 = global::System.Linq.Expressions.Expression.New(typeof(global::Foo.PersonDto).GetConstructor(new global::System.Type[] { }));
20+
var expr_0 = global::System.Linq.Expressions.Expression.New(typeof(global::Foo.PersonDto).GetConstructor(new global::System.Type[] { typeof(string), typeof(string) }), p_fn, p_ln);
2121
var p___this = global::System.Linq.Expressions.Expression.Parameter(typeof(object), "@this"); // FirstName
2222
var expr_5 = global::System.Linq.Expressions.Expression.Property(p___this, typeof(global::Foo.PersonDto).GetProperty("FirstName"));
2323
var expr_6 = global::System.Linq.Expressions.Expression.Constant(" ", typeof(string)); // " "

tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/ConstructorTests.ProjectableConstructor_ThisInitializer_WithIfElseInDelegated.verified.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ namespace ExpressiveSharp.Generated
1616
{
1717
var p_score = global::System.Linq.Expressions.Expression.Parameter(typeof(int), "score");
1818
var p_prefix = global::System.Linq.Expressions.Expression.Parameter(typeof(string), "prefix");
19-
var expr_0 = global::System.Linq.Expressions.Expression.New(typeof(global::Foo.PersonDto).GetConstructor(new global::System.Type[] { }));
19+
var expr_0 = global::System.Linq.Expressions.Expression.New(typeof(global::Foo.PersonDto).GetConstructor(new global::System.Type[] { typeof(int) }), p_score);
2020
var p___this = global::System.Linq.Expressions.Expression.Parameter(typeof(object), "@this"); // Label
2121
var expr_2 = global::System.Linq.Expressions.Expression.Property(p___this, typeof(global::Foo.PersonDto).GetProperty("Label"));
2222
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), p_prefix, expr_2);

0 commit comments

Comments
 (0)