@@ -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}
0 commit comments