-
Notifications
You must be signed in to change notification settings - Fork 31
Add support for projectable constructors #160
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ef840f6
8468dc5
f68c572
7067bc2
75c638e
7a70fde
06627c2
9add2c9
3c91bf9
e2b1fad
f7f296b
3b56faa
ff4feb1
7ba8a84
b8af892
b697cd2
f2a805e
31f4267
adc95f5
5f83f40
8b144b5
2b70d43
b4989af
a741a27
3788729
f2d0ec8
9b7e61a
3f6cb13
9e222e0
c5b1ab6
403924e
41c653b
d84a121
2b7159f
ccfc63d
2f88136
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -188,19 +188,24 @@ x is IPropertySymbol xProperty && | |||||||||||||
| ? memberSymbol.ContainingType.ContainingType | ||||||||||||||
| : memberSymbol.ContainingType; | ||||||||||||||
|
|
||||||||||||||
| var methodSymbol = memberSymbol as IMethodSymbol; | ||||||||||||||
|
|
||||||||||||||
| // Sanitize constructor name (.ctor / .cctor are not valid C# identifiers, use _ctor) | ||||||||||||||
| var memberName = methodSymbol?.MethodKind is MethodKind.Constructor or MethodKind.StaticConstructor | ||||||||||||||
| ? "_ctor" | ||||||||||||||
| : memberSymbol.Name; | ||||||||||||||
|
|
||||||||||||||
| var descriptor = new ProjectableDescriptor | ||||||||||||||
| { | ||||||||||||||
| UsingDirectives = member.SyntaxTree.GetRoot().DescendantNodes().OfType<UsingDirectiveSyntax>(), | ||||||||||||||
| ClassName = classForNaming.Name, | ||||||||||||||
| ClassNamespace = classForNaming.ContainingNamespace.IsGlobalNamespace ? null : classForNaming.ContainingNamespace.ToDisplayString(), | ||||||||||||||
| MemberName = memberSymbol.Name, | ||||||||||||||
| MemberName = memberName, | ||||||||||||||
| NestedInClassNames = isExtensionMember | ||||||||||||||
| ? GetNestedInClassPathForExtensionMember(memberSymbol.ContainingType) | ||||||||||||||
| : GetNestedInClassPath(memberSymbol.ContainingType), | ||||||||||||||
| ParametersList = SyntaxFactory.ParameterList() | ||||||||||||||
| }; | ||||||||||||||
|
|
||||||||||||||
| var methodSymbol = memberSymbol as IMethodSymbol; | ||||||||||||||
|
|
||||||||||||||
| // Collect parameter type names for method overload disambiguation | ||||||||||||||
| if (methodSymbol is not null) | ||||||||||||||
|
|
@@ -288,7 +293,7 @@ x is IPropertySymbol xProperty && | |||||||||||||
| ) | ||||||||||||||
| ); | ||||||||||||||
| } | ||||||||||||||
| else if (!member.Modifiers.Any(SyntaxKind.StaticKeyword)) | ||||||||||||||
| else if (!member.Modifiers.Any(SyntaxKind.StaticKeyword) && member is not ConstructorDeclarationSyntax) | ||||||||||||||
| { | ||||||||||||||
| descriptor.ParametersList = descriptor.ParametersList.AddParameters( | ||||||||||||||
| SyntaxFactory.Parameter( | ||||||||||||||
|
|
@@ -452,6 +457,116 @@ x is IPropertySymbol xProperty && | |||||||||||||
| ? bodyExpression | ||||||||||||||
| : (ExpressionSyntax)expressionSyntaxRewriter.Visit(bodyExpression); | ||||||||||||||
| } | ||||||||||||||
| // Projectable constructors | ||||||||||||||
| else if (memberBody is ConstructorDeclarationSyntax constructorDeclarationSyntax) | ||||||||||||||
| { | ||||||||||||||
| var containingType = memberSymbol.ContainingType; | ||||||||||||||
| var fullTypeName = containingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); | ||||||||||||||
|
|
||||||||||||||
| descriptor.ReturnTypeName = fullTypeName; | ||||||||||||||
|
|
||||||||||||||
| // Add the constructor's own parameters to the lambda parameter list | ||||||||||||||
| foreach (var additionalParameter in ((ParameterListSyntax)declarationSyntaxRewriter.Visit(constructorDeclarationSyntax.ParameterList)).Parameters) | ||||||||||||||
| { | ||||||||||||||
| descriptor.ParametersList = descriptor.ParametersList.AddParameters(additionalParameter); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // Accumulated property-name → expression map (later converted to member-init) | ||||||||||||||
| var accumulatedAssignments = new Dictionary<string, ExpressionSyntax>(); | ||||||||||||||
|
|
||||||||||||||
| // 1. Process base/this initializer: propagate property assignments from the | ||||||||||||||
| // delegated constructor so callers don't have to duplicate them in the body. | ||||||||||||||
| if (constructorDeclarationSyntax.Initializer is { } initializer) | ||||||||||||||
| { | ||||||||||||||
| var initializerSymbol = semanticModel.GetSymbolInfo(initializer).Symbol as IMethodSymbol; | ||||||||||||||
| if (initializerSymbol is not null) | ||||||||||||||
| { | ||||||||||||||
| var delegatedAssignments = CollectDelegatedConstructorAssignments( | ||||||||||||||
| initializerSymbol, | ||||||||||||||
| initializer.ArgumentList.Arguments, | ||||||||||||||
| expressionSyntaxRewriter, | ||||||||||||||
| context, | ||||||||||||||
| memberSymbol.Name); | ||||||||||||||
|
|
||||||||||||||
| if (delegatedAssignments is null) | ||||||||||||||
| { | ||||||||||||||
| return null; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| foreach (var kvp in delegatedAssignments) | ||||||||||||||
| { | ||||||||||||||
| accumulatedAssignments[kvp.Key] = kvp.Value; | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // 2. Process this constructor's body (supports assignments, locals, if/else). | ||||||||||||||
| // Pass the already-accumulated base/this initializer assignments as the initial | ||||||||||||||
| // visible context so that references to those properties are correctly inlined. | ||||||||||||||
| if (constructorDeclarationSyntax.Body is { } body) | ||||||||||||||
| { | ||||||||||||||
| var bodyConverter = new ConstructorBodyConverter(context, expressionSyntaxRewriter); | ||||||||||||||
| IReadOnlyDictionary<string, ExpressionSyntax>? initialCtx = | ||||||||||||||
| accumulatedAssignments.Count > 0 ? accumulatedAssignments : null; | ||||||||||||||
| var bodyAssignments = bodyConverter.TryConvertBody(body.Statements, memberSymbol.Name, initialCtx); | ||||||||||||||
|
|
||||||||||||||
| if (bodyAssignments is null) | ||||||||||||||
| { | ||||||||||||||
| return null; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // Body assignments override anything set by the base/this initializer | ||||||||||||||
| foreach (var kvp in bodyAssignments) | ||||||||||||||
| { | ||||||||||||||
| accumulatedAssignments[kvp.Key] = kvp.Value; | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| if (accumulatedAssignments.Count == 0) | ||||||||||||||
| { | ||||||||||||||
| var diag = Diagnostic.Create(Diagnostics.RequiresBodyDefinition, | ||||||||||||||
| constructorDeclarationSyntax.GetLocation(), memberSymbol.Name); | ||||||||||||||
| context.ReportDiagnostic(diag); | ||||||||||||||
| return null; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // Verify the containing type has a parameterless (instance) constructor. | ||||||||||||||
| // The generated projection is: new T() { Prop = ... }, which requires one. | ||||||||||||||
| // INamedTypeSymbol.Constructors covers all partial declarations and also | ||||||||||||||
| // the implicit parameterless constructor that the compiler synthesizes when | ||||||||||||||
| // no constructors are explicitly defined. | ||||||||||||||
| var hasParameterlessConstructor = containingType.Constructors | ||||||||||||||
| .Any(c => !c.IsStatic && c.Parameters.IsEmpty); | ||||||||||||||
|
|
||||||||||||||
| if (!hasParameterlessConstructor) | ||||||||||||||
| { | ||||||||||||||
| context.ReportDiagnostic(Diagnostic.Create( | ||||||||||||||
| Diagnostics.MissingParameterlessConstructor, | ||||||||||||||
| constructorDeclarationSyntax.GetLocation(), | ||||||||||||||
| containingType.Name)); | ||||||||||||||
| return null; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| var initExpressions = accumulatedAssignments | ||||||||||||||
| .Select(kvp => (ExpressionSyntax)SyntaxFactory.AssignmentExpression( | ||||||||||||||
| SyntaxKind.SimpleAssignmentExpression, | ||||||||||||||
| SyntaxFactory.IdentifierName(kvp.Key), | ||||||||||||||
| kvp.Value)) | ||||||||||||||
| .ToList(); | ||||||||||||||
|
|
||||||||||||||
| var memberInit = SyntaxFactory.InitializerExpression( | ||||||||||||||
| SyntaxKind.ObjectInitializerExpression, | ||||||||||||||
| SyntaxFactory.SeparatedList(initExpressions)); | ||||||||||||||
|
|
||||||||||||||
| // Use a parameterless constructor + object initializer so EF Core only | ||||||||||||||
| // projects columns explicitly listed in the member-init bindings. | ||||||||||||||
| descriptor.ExpressionBody = SyntaxFactory.ObjectCreationExpression( | ||||||||||||||
| SyntaxFactory.Token(SyntaxKind.NewKeyword).WithTrailingTrivia(SyntaxFactory.Space), | ||||||||||||||
| SyntaxFactory.ParseTypeName(fullTypeName), | ||||||||||||||
| SyntaxFactory.ArgumentList(), | ||||||||||||||
| memberInit | ||||||||||||||
| ); | ||||||||||||||
| } | ||||||||||||||
| else | ||||||||||||||
| { | ||||||||||||||
| return null; | ||||||||||||||
|
|
@@ -460,6 +575,123 @@ x is IPropertySymbol xProperty && | |||||||||||||
| return descriptor; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /// <summary> | ||||||||||||||
| /// Collects the property-assignment expressions that the delegated constructor (base/this) | ||||||||||||||
| /// would perform, substituting its parameters with the actual call-site argument expressions. | ||||||||||||||
| /// Supports if/else logic inside the delegated constructor body, and follows the chain of | ||||||||||||||
| /// base/this initializers recursively. | ||||||||||||||
| /// Returns <c>null</c> when an unsupported statement is encountered (diagnostics reported). | ||||||||||||||
| /// </summary> | ||||||||||||||
| private static Dictionary<string, ExpressionSyntax>? CollectDelegatedConstructorAssignments( | ||||||||||||||
| IMethodSymbol delegatedCtor, | ||||||||||||||
| SeparatedSyntaxList<ArgumentSyntax> callerArgs, | ||||||||||||||
| ExpressionSyntaxRewriter expressionSyntaxRewriter, | ||||||||||||||
| SourceProductionContext context, | ||||||||||||||
| string memberName, | ||||||||||||||
| bool argsAlreadyRewritten = false) | ||||||||||||||
| { | ||||||||||||||
| // Only process constructors whose source is available in this compilation | ||||||||||||||
| var syntax = delegatedCtor.DeclaringSyntaxReferences | ||||||||||||||
| .Select(r => r.GetSyntax()) | ||||||||||||||
| .OfType<ConstructorDeclarationSyntax>() | ||||||||||||||
| .FirstOrDefault(); | ||||||||||||||
|
|
||||||||||||||
| if (syntax is null) | ||||||||||||||
| { | ||||||||||||||
| return new Dictionary<string, ExpressionSyntax>(); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // Build a mapping: delegated-param-name → caller argument expression. | ||||||||||||||
| // First-level args come from the original syntax tree and must be visited by the | ||||||||||||||
| // ExpressionSyntaxRewriter. Recursive-level args are already-substituted detached | ||||||||||||||
| // nodes and must NOT be visited (doing so throws "node not in syntax tree"). | ||||||||||||||
| var paramToArg = new Dictionary<string, ExpressionSyntax>(); | ||||||||||||||
| for (var i = 0; i < callerArgs.Count && i < delegatedCtor.Parameters.Length; i++) | ||||||||||||||
| { | ||||||||||||||
| var paramName = delegatedCtor.Parameters[i].Name; | ||||||||||||||
| var argExpr = argsAlreadyRewritten | ||||||||||||||
| ? callerArgs[i].Expression | ||||||||||||||
| : (ExpressionSyntax)expressionSyntaxRewriter.Visit(callerArgs[i].Expression); | ||||||||||||||
| paramToArg[paramName] = argExpr; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // The accumulated assignments start from the delegated ctor's own initializer (if any), | ||||||||||||||
| // so that base/this chains are followed recursively. | ||||||||||||||
| var accumulated = new Dictionary<string, ExpressionSyntax>(); | ||||||||||||||
|
|
||||||||||||||
| if (syntax.Initializer is { } delegatedInitializer) | ||||||||||||||
| { | ||||||||||||||
| // The delegated ctor's initializer is part of the original syntax tree, | ||||||||||||||
| // so we can safely use the semantic model to resolve its symbol. | ||||||||||||||
| var semanticModel = expressionSyntaxRewriter.GetSemanticModel(); | ||||||||||||||
|
Comment on lines
+624
to
+626
|
||||||||||||||
| // The delegated ctor's initializer is part of the original syntax tree, | |
| // so we can safely use the semantic model to resolve its symbol. | |
| var semanticModel = expressionSyntaxRewriter.GetSemanticModel(); | |
| // Obtain a SemanticModel for the delegated constructor's syntax tree so that | |
| // the initializer node belongs to the same tree as the semantic model. | |
| var semanticModel = context.Compilation.GetSemanticModel(syntax.SyntaxTree); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The parameterless-constructor check only verifies existence (any non-static ctor with zero parameters) but doesn’t verify accessibility from the generated projection class. If the only parameterless ctor is private/protected/private-protected, the generator will still emit
new T() { ... }, which will fail compilation. Consider checking DeclaredAccessibility (e.g., public/internal/protected internal) and aligning the EFP0007 diagnostic text accordingly.