Refactor ExpressionSyntaxRewriter into focused partial class files#163
Refactor ExpressionSyntaxRewriter into focused partial class files#163
Conversation
…tiple files Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Refactors the Roslyn-based ExpressionSyntaxRewriter in the generator into multiple focused partial class files (with file-scoped namespaces) to reduce the size/complexity of the original monolithic implementation while preserving behavior.
Changes:
- Split switch-expression rewriting and pattern-to-expression conversion into a dedicated partial file.
- Split null-conditional (
?.) rewriting into a dedicated partial file. - Split enum method-call expansion into a dedicated partial file, leaving the core visitor logic in
ExpressionSyntaxRewriter.cs.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs | Converted to file-scoped namespace and reduced to core visitor logic + shared fields/ctor. |
| src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.SwitchExpressionRewrite.cs | Extracted switch expression rewriting + pattern conversion helpers. |
| src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.NullConditionalRewrite.cs | Extracted null-conditional rewriting (ConditionalAccessExpression + bindings). |
| src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.EnumMethodExpansion.cs | Extracted enum method expansion (TryExpandEnumMethodCall, helper call builder). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| { | ||
| public override SyntaxNode? VisitConditionalAccessExpression(ConditionalAccessExpressionSyntax node) | ||
| { | ||
| var targetExpression = (ExpressionSyntax)Visit(node.Expression); | ||
|
|
||
| _conditionalAccessExpressionsStack.Push(targetExpression); | ||
|
|
||
| if (_nullConditionalRewriteSupport == NullConditionalRewriteSupport.None) | ||
| { | ||
| var diagnostic = Diagnostic.Create(Diagnostics.NullConditionalRewriteUnsupported, node.GetLocation(), node); | ||
| _context.ReportDiagnostic(diagnostic); | ||
|
|
||
| // Return the original node, do not attempt further rewrites | ||
| return node; | ||
| } | ||
|
|
||
| else if (_nullConditionalRewriteSupport is NullConditionalRewriteSupport.Ignore) | ||
| { | ||
| // Ignore the conditional access and simply visit the WhenNotNull expression | ||
| return Visit(node.WhenNotNull); | ||
| } | ||
|
|
||
| else if (_nullConditionalRewriteSupport is NullConditionalRewriteSupport.Rewrite) | ||
| { | ||
| var typeInfo = _semanticModel.GetTypeInfo(node); | ||
|
|
||
| // Do not translate until we can resolve the target type | ||
| if (typeInfo.ConvertedType is not null) | ||
| { | ||
| // Translate null-conditional into a conditional expression, wrapped inside parenthesis | ||
| return SyntaxFactory.ParenthesizedExpression( | ||
| SyntaxFactory.ConditionalExpression( | ||
| SyntaxFactory.BinaryExpression( | ||
| SyntaxKind.NotEqualsExpression, | ||
| targetExpression.WithTrailingTrivia(SyntaxFactory.Whitespace(" ")), | ||
| SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression).WithLeadingTrivia(SyntaxFactory.Whitespace(" ")) | ||
| ).WithTrailingTrivia(SyntaxFactory.Whitespace(" ")), | ||
| SyntaxFactory.ParenthesizedExpression( | ||
| (ExpressionSyntax)Visit(node.WhenNotNull) | ||
| ).WithLeadingTrivia(SyntaxFactory.Whitespace(" ")).WithTrailingTrivia(SyntaxFactory.Whitespace(" ")), | ||
| SyntaxFactory.CastExpression( | ||
| SyntaxFactory.ParseName(typeInfo.ConvertedType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)), | ||
| SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression) | ||
| ).WithLeadingTrivia(SyntaxFactory.Whitespace(" ")) | ||
| ).WithLeadingTrivia(node.GetLeadingTrivia()).WithTrailingTrivia(node.GetTrailingTrivia())); | ||
| } | ||
| } | ||
|
|
||
| return base.VisitConditionalAccessExpression(node); |
There was a problem hiding this comment.
VisitConditionalAccessExpression pushes onto _conditionalAccessExpressionsStack before checking _nullConditionalRewriteSupport. In the None branch it returns the original node without popping, leaving the stack unbalanced for the rest of the traversal and potentially growing without bound when multiple conditional accesses are present. Consider popping before returning (or restructuring with a try/finally so every push is matched with a pop).
| { | |
| public override SyntaxNode? VisitConditionalAccessExpression(ConditionalAccessExpressionSyntax node) | |
| { | |
| var targetExpression = (ExpressionSyntax)Visit(node.Expression); | |
| _conditionalAccessExpressionsStack.Push(targetExpression); | |
| if (_nullConditionalRewriteSupport == NullConditionalRewriteSupport.None) | |
| { | |
| var diagnostic = Diagnostic.Create(Diagnostics.NullConditionalRewriteUnsupported, node.GetLocation(), node); | |
| _context.ReportDiagnostic(diagnostic); | |
| // Return the original node, do not attempt further rewrites | |
| return node; | |
| } | |
| else if (_nullConditionalRewriteSupport is NullConditionalRewriteSupport.Ignore) | |
| { | |
| // Ignore the conditional access and simply visit the WhenNotNull expression | |
| return Visit(node.WhenNotNull); | |
| } | |
| else if (_nullConditionalRewriteSupport is NullConditionalRewriteSupport.Rewrite) | |
| { | |
| var typeInfo = _semanticModel.GetTypeInfo(node); | |
| // Do not translate until we can resolve the target type | |
| if (typeInfo.ConvertedType is not null) | |
| { | |
| // Translate null-conditional into a conditional expression, wrapped inside parenthesis | |
| return SyntaxFactory.ParenthesizedExpression( | |
| SyntaxFactory.ConditionalExpression( | |
| SyntaxFactory.BinaryExpression( | |
| SyntaxKind.NotEqualsExpression, | |
| targetExpression.WithTrailingTrivia(SyntaxFactory.Whitespace(" ")), | |
| SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression).WithLeadingTrivia(SyntaxFactory.Whitespace(" ")) | |
| ).WithTrailingTrivia(SyntaxFactory.Whitespace(" ")), | |
| SyntaxFactory.ParenthesizedExpression( | |
| (ExpressionSyntax)Visit(node.WhenNotNull) | |
| ).WithLeadingTrivia(SyntaxFactory.Whitespace(" ")).WithTrailingTrivia(SyntaxFactory.Whitespace(" ")), | |
| SyntaxFactory.CastExpression( | |
| SyntaxFactory.ParseName(typeInfo.ConvertedType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)), | |
| SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression) | |
| ).WithLeadingTrivia(SyntaxFactory.Whitespace(" ")) | |
| ).WithLeadingTrivia(node.GetLeadingTrivia()).WithTrailingTrivia(node.GetTrailingTrivia())); | |
| } | |
| } | |
| return base.VisitConditionalAccessExpression(node); | |
| { | |
| public override SyntaxNode? VisitConditionalAccessExpression(ConditionalAccessExpressionSyntax node) | |
| { | |
| var targetExpression = (ExpressionSyntax)Visit(node.Expression); | |
| if (_nullConditionalRewriteSupport == NullConditionalRewriteSupport.None) | |
| { | |
| var diagnostic = Diagnostic.Create(Diagnostics.NullConditionalRewriteUnsupported, node.GetLocation(), node); | |
| _context.ReportDiagnostic(diagnostic); | |
| // Return the original node, do not attempt further rewrites | |
| return node; | |
| } | |
| _conditionalAccessExpressionsStack.Push(targetExpression); | |
| else if (_nullConditionalRewriteSupport is NullConditionalRewriteSupport.Ignore) | |
| { | |
| // Ignore the conditional access and simply visit the WhenNotNull expression | |
| return Visit(node.WhenNotNull); | |
| } | |
| else if (_nullConditionalRewriteSupport is NullConditionalRewriteSupport.Rewrite) | |
| { | |
| var typeInfo = _semanticModel.GetTypeInfo(node); | |
| // Do not translate until we can resolve the target type | |
| if (typeInfo.ConvertedType is not null) | |
| { | |
| // Translate null-conditional into a conditional expression, wrapped inside parenthesis | |
| return SyntaxFactory.ParenthesizedExpression( | |
| SyntaxFactory.ConditionalExpression( | |
| SyntaxFactory.BinaryExpression( | |
| SyntaxKind.NotEqualsExpression, | |
| targetExpression.WithTrailingTrivia(SyntaxFactory.Whitespace(" ")), | |
| SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression).WithLeadingTrivia(SyntaxFactory.Whitespace(" ")) | |
| ).WithTrailingTrivia(SyntaxFactory.Whitespace(" ")), | |
| SyntaxFactory.ParenthesizedExpression( | |
| (ExpressionSyntax)Visit(node.WhenNotNull) | |
| ).WithLeadingTrivia(SyntaxFactory.Whitespace(" ")).WithTrailingTrivia(SyntaxFactory.Whitespace(" ")), | |
| SyntaxFactory.CastExpression( | |
| SyntaxFactory.ParseName(typeInfo.ConvertedType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)), | |
| SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression) | |
| ).WithLeadingTrivia(SyntaxFactory.Whitespace(" ")) | |
| ).WithLeadingTrivia(node.GetLeadingTrivia()).WithTrailingTrivia(node.GetTrailingTrivia())); | |
| } | |
| } | |
| return base.VisitConditionalAccessExpression(node); |
ExpressionSyntaxRewriterhad grown into a single ~880-line file mixing enum expansion, switch expression rewriting, null-conditional handling, and pattern conversion logic.Changes
Split the class into four focused files using
partial classand file-scoped namespaces:ExpressionSyntaxRewriter.cs— Class declaration, constructor, fields, and core visit methods (VisitIdentifierName,VisitInvocationExpression,VisitIsPatternExpression, etc.)EnumMethodExpander.cs—TryExpandEnumMethodCall+CreateMethodCallOnEnumValueSwitchExpressionRewriter.cs—VisitSwitchExpression,ReplaceVariableWithCast,ConvertPatternToExpression,ConvertRecursivePattern,TypeRequiresNullCheckNullConditionalRewriter.cs—VisitConditionalAccessExpression,VisitMemberBindingExpression,VisitElementBindingExpressionAll files use file-scoped namespaces. No behavioral changes — all existing tests pass unchanged.
Original prompt
🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.