1- using Microsoft . CodeAnalysis . CSharp ;
2-
31namespace GraphQL . EntityFramework . CodeFixes ;
42
53[ ExportCodeFixProvider ( LanguageNames . CSharp , Name = nameof ( AbstractNavigationProjectionCodeFixProvider ) ) ]
@@ -23,9 +21,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
2321 }
2422
2523 var diagnostic = context . Diagnostics [ 0 ] ;
26- var diagnosticSpan = diagnostic . Location . SourceSpan ;
27-
28- var node = root . FindNode ( diagnosticSpan ) ;
24+ var node = root . FindNode ( diagnostic . Location . SourceSpan ) ;
2925 var invocation = node as InvocationExpressionSyntax ?? node . FirstAncestorOrSelf < InvocationExpressionSyntax > ( ) ;
3026
3127 if ( invocation == null )
@@ -73,50 +69,31 @@ static async Task<Document> ConvertToExplicitProjectionAsync(
7369 }
7470 }
7571
76- // If no explicit projection argument, this is 4-parameter syntax
77- var is4ParamSyntax = projectionArgumentIndex == - 1 ;
78-
7972 if ( filterLambda == null )
8073 {
8174 return document ;
8275 }
8376
84- // Extract accessed properties from filter
85- var accessedProperties = ExtractAccessedProperties ( filterLambda ) ;
86- if ( accessedProperties . Count == 0 )
77+ var entityParamName = GetLastParameterName ( filterLambda ) ;
78+ if ( entityParamName == null )
8779 {
8880 return document ;
8981 }
9082
91- // Get the entity parameter name from the filter (last parameter)
92- string entityParamName ;
93- if ( filterLambda is SimpleLambdaExpressionSyntax simpleLambda )
94- {
95- entityParamName = simpleLambda . Parameter . Identifier . Text ;
96- }
97- else if ( filterLambda is ParenthesizedLambdaExpressionSyntax parenthesizedLambda )
98- {
99- var paramCount = parenthesizedLambda . ParameterList . Parameters . Count ;
100- entityParamName = paramCount > 0
101- ? parenthesizedLambda . ParameterList . Parameters [ paramCount - 1 ] . Identifier . Text
102- : "entity" ;
103- }
104- else
83+ var accessedProperties = ExtractAccessedProperties ( filterLambda . Body , entityParamName ) ;
84+ if ( accessedProperties . Count == 0 )
10585 {
10686 return document ;
10787 }
10888
109- // Build new projection lambda: e => new { e.Id, Prop1 = e.Nav.Prop1, ... }
11089 var newProjectionLambda = BuildProjectionLambda ( entityParamName , accessedProperties ) ;
111-
112- // Build new filter lambda with renamed parameter: (_, _, _, proj) => proj.Prop1 == value
11390 var newFilterLambda = BuildFilterLambda ( filterLambda , accessedProperties ) ;
11491
11592 // Replace arguments
11693 SyntaxNode newInvocation ;
117- if ( is4ParamSyntax )
94+ if ( projectionArgumentIndex == - 1 )
11895 {
119- // Add projection argument
96+ // 4-parameter syntax: add projection argument
12097 var projectionArg = SyntaxFactory . Argument ( newProjectionLambda )
12198 . WithNameColon ( SyntaxFactory . NameColon ( "projection" ) )
12299 . WithLeadingTrivia ( SyntaxFactory . Whitespace ( "\n " ) ) ;
@@ -131,22 +108,16 @@ static async Task<Document> ConvertToExplicitProjectionAsync(
131108 }
132109 else
133110 {
134- // Replace projection and filter arguments
111+ // Replace existing projection and filter arguments
135112 var newArguments = arguments ;
136113
137- if ( projectionArgumentIndex >= 0 )
138- {
139- newArguments = newArguments . Replace (
140- newArguments [ projectionArgumentIndex ] ,
141- newArguments [ projectionArgumentIndex ] . WithExpression ( newProjectionLambda ) ) ;
142- }
114+ newArguments = newArguments . Replace (
115+ newArguments [ projectionArgumentIndex ] ,
116+ newArguments [ projectionArgumentIndex ] . WithExpression ( newProjectionLambda ) ) ;
143117
144- if ( filterArgumentIndex >= 0 )
145- {
146- newArguments = newArguments . Replace (
147- newArguments [ filterArgumentIndex ] ,
148- newArguments [ filterArgumentIndex ] . WithExpression ( newFilterLambda ) ) ;
149- }
118+ newArguments = newArguments . Replace (
119+ newArguments [ filterArgumentIndex ] ,
120+ newArguments [ filterArgumentIndex ] . WithExpression ( newFilterLambda ) ) ;
150121
151122 newInvocation = invocation . WithArgumentList (
152123 invocation . ArgumentList . WithArguments ( newArguments ) ) ;
@@ -156,69 +127,47 @@ static async Task<Document> ConvertToExplicitProjectionAsync(
156127 return document . WithSyntaxRoot ( newRoot ) ;
157128 }
158129
159- static List < PropertyAccess > ExtractAccessedProperties ( LambdaExpressionSyntax filterLambda )
130+ static string ? GetLastParameterName ( LambdaExpressionSyntax lambda ) =>
131+ lambda switch
132+ {
133+ SimpleLambdaExpressionSyntax simple => simple . Parameter . Identifier . Text ,
134+ ParenthesizedLambdaExpressionSyntax { ParameterList . Parameters . Count : > 0 } parenthesized =>
135+ parenthesized . ParameterList . Parameters [ parenthesized . ParameterList . Parameters . Count - 1 ] . Identifier . Text ,
136+ _ => null
137+ } ;
138+
139+ static ExpressionSyntax UnwrapNullForgiving ( ExpressionSyntax expression ) =>
140+ expression is PostfixUnaryExpressionSyntax { RawKind : ( int ) SyntaxKind . SuppressNullableWarningExpression } postfix
141+ ? postfix . Operand
142+ : expression ;
143+
144+ static List < PropertyAccess > ExtractAccessedProperties ( CSharpSyntaxNode body , string paramName )
160145 {
161146 var properties = new List < PropertyAccess > ( ) ;
162- var body = filterLambda . Body ;
163147
164- // Get filter parameter name
165- string ? paramName = null ;
166- if ( filterLambda is SimpleLambdaExpressionSyntax simpleLambda )
148+ foreach ( var memberAccess in body . DescendantNodesAndSelf ( ) . OfType < MemberAccessExpressionSyntax > ( ) )
167149 {
168- paramName = simpleLambda . Parameter . Identifier . Text ;
169- }
170- else if ( filterLambda is ParenthesizedLambdaExpressionSyntax parenthesizedLambda )
171- {
172- var paramCount = parenthesizedLambda . ParameterList . Parameters . Count ;
173- if ( paramCount > 0 )
150+ // Look for: e.Nav.Prop or e.Nav!.Prop
151+ var inner = UnwrapNullForgiving ( memberAccess . Expression ) ;
152+
153+ if ( inner is not MemberAccessExpressionSyntax nestedAccess )
174154 {
175- paramName = parenthesizedLambda . ParameterList . Parameters [ paramCount - 1 ] . Identifier . Text ;
155+ continue ;
176156 }
177- }
178157
179- if ( paramName == null )
180- {
181- return properties ;
182- }
183-
184- // Find all member accesses
185- var memberAccesses = body . DescendantNodesAndSelf ( )
186- . OfType < MemberAccessExpressionSyntax > ( ) ;
187-
188- foreach ( var memberAccess in memberAccesses )
189- {
190- // Look for patterns like: e.Parent.Property or e.Parent!.Property (with null-forgiving operator)
191- // The null-forgiving operator creates a PostfixUnaryExpressionSyntax wrapper
192- var innerExpression = memberAccess . Expression ;
193-
194- // Unwrap null-forgiving operator if present: e.Parent! -> e.Parent
195- if ( innerExpression is PostfixUnaryExpressionSyntax { RawKind : ( int ) SyntaxKind . SuppressNullableWarningExpression } postfix )
158+ var root = UnwrapNullForgiving ( nestedAccess . Expression ) ;
159+ if ( root is not IdentifierNameSyntax identifier || identifier . Identifier . Text != paramName )
196160 {
197- innerExpression = postfix . Operand ;
161+ continue ;
198162 }
199163
200- if ( innerExpression is MemberAccessExpressionSyntax nestedAccess )
164+ var navName = nestedAccess . Name . Identifier . Text ;
165+ var propName = memberAccess . Name . Identifier . Text ;
166+ var fullPath = $ "{ navName } .{ propName } ";
167+
168+ if ( ! properties . Any ( _ => _ . FullPath == fullPath ) )
201169 {
202- // Check if the root is our parameter (possibly with null-forgiving)
203- var rootExpr = nestedAccess . Expression ;
204- if ( rootExpr is PostfixUnaryExpressionSyntax { RawKind : ( int ) SyntaxKind . SuppressNullableWarningExpression } rootPostfix )
205- {
206- rootExpr = rootPostfix . Operand ;
207- }
208-
209- if ( rootExpr is IdentifierNameSyntax identifier && identifier . Identifier . Text == paramName )
210- {
211- // e.Parent.Property - extract as "Parent.Property"
212- var navName = nestedAccess . Name . Identifier . Text ;
213- var propName = memberAccess . Name . Identifier . Text ;
214- var fullPath = $ "{ navName } .{ propName } ";
215- var flatName = $ "{ navName } { propName } ";
216-
217- if ( ! properties . Any ( p => p . FullPath == fullPath ) )
218- {
219- properties . Add ( new ( fullPath , flatName , memberAccess ) ) ;
220- }
221- }
170+ properties . Add ( new ( fullPath , $ "{ navName } { propName } ", memberAccess ) ) ;
222171 }
223172 }
224173
@@ -229,26 +178,20 @@ static LambdaExpressionSyntax BuildProjectionLambda(
229178 string paramName ,
230179 List < PropertyAccess > properties )
231180 {
232- // Build: _ => new { _.Id, Prop1 = _.Nav.Prop1, ... }
233- var parameter = SyntaxFactory . Parameter ( SyntaxFactory . Identifier ( paramName ) ) ;
234-
235- List < AnonymousObjectMemberDeclaratorSyntax > initializers = [ ] ;
236-
237- // Add Id property
238- initializers . Add (
181+ var initializers = new List < AnonymousObjectMemberDeclaratorSyntax >
182+ {
183+ // Always include Id
239184 SyntaxFactory . AnonymousObjectMemberDeclarator (
240185 SyntaxFactory . MemberAccessExpression (
241186 SyntaxKind . SimpleMemberAccessExpression ,
242187 SyntaxFactory . IdentifierName ( paramName ) ,
243- SyntaxFactory . IdentifierName ( "Id" ) ) ) ) ;
188+ SyntaxFactory . IdentifierName ( "Id" ) ) )
189+ } ;
244190
245- // Add accessed properties with flattened names
246191 foreach ( var prop in properties )
247192 {
248- var parts = prop . FullPath . Split ( '.' ) ;
249193 ExpressionSyntax expression = SyntaxFactory . IdentifierName ( paramName ) ;
250-
251- foreach ( var part in parts )
194+ foreach ( var part in prop . FullPath . Split ( '.' ) )
252195 {
253196 expression = SyntaxFactory . MemberAccessExpression (
254197 SyntaxKind . SimpleMemberAccessExpression ,
@@ -262,38 +205,26 @@ static LambdaExpressionSyntax BuildProjectionLambda(
262205 expression ) ) ;
263206 }
264207
265- var anonymousObject = SyntaxFactory . AnonymousObjectCreationExpression (
266- SyntaxFactory . SeparatedList ( initializers ) ) ;
267-
268- return SyntaxFactory . SimpleLambdaExpression ( parameter , anonymousObject ) ;
208+ return SyntaxFactory . SimpleLambdaExpression (
209+ SyntaxFactory . Parameter ( SyntaxFactory . Identifier ( paramName ) ) ,
210+ SyntaxFactory . AnonymousObjectCreationExpression ( SyntaxFactory . SeparatedList ( initializers ) ) ) ;
269211 }
270212
271213 static LambdaExpressionSyntax BuildFilterLambda (
272214 LambdaExpressionSyntax originalFilter ,
273215 List < PropertyAccess > properties )
274216 {
275- // Replace entity parameter references with proj and update property accesses
276- var newBody = originalFilter . Body ;
277-
278- // Replace each property access with flattened name
279- foreach ( var prop in properties )
280- {
281- // Replace e.Parent.Property with proj.ParentProperty
282- newBody = newBody . ReplaceNodes (
283- newBody . DescendantNodesAndSelf ( ) . Where ( _ => _ == prop . OriginalAccess ) ,
284- ( _ , _ ) => SyntaxFactory . MemberAccessExpression (
217+ // Replace all property accesses in a single pass
218+ var replacements = properties . ToDictionary (
219+ _ => _ . OriginalAccess , SyntaxNode ( _ ) =>
220+ SyntaxFactory . MemberAccessExpression (
285221 SyntaxKind . SimpleMemberAccessExpression ,
286222 SyntaxFactory . IdentifierName ( "proj" ) ,
287- SyntaxFactory . IdentifierName ( prop . FlatName ) ) ) ;
288- }
223+ SyntaxFactory . IdentifierName ( _ . FlatName ) ) ) ;
289224
290- // Build new parameter list with "proj" as last parameter
291- if ( originalFilter is SimpleLambdaExpressionSyntax )
292- {
293- return SyntaxFactory . SimpleLambdaExpression (
294- SyntaxFactory . Parameter ( SyntaxFactory . Identifier ( "proj" ) ) ,
295- newBody ) ;
296- }
225+ var newBody = originalFilter . Body . ReplaceNodes (
226+ replacements . Keys ,
227+ ( original , _ ) => replacements [ original ] ) ;
297228
298229 if ( originalFilter is ParenthesizedLambdaExpressionSyntax parenthesizedLambda )
299230 {
@@ -306,20 +237,15 @@ static LambdaExpressionSyntax BuildFilterLambda(
306237 newBody ) ;
307238 }
308239
309- return originalFilter ;
240+ return SyntaxFactory . SimpleLambdaExpression (
241+ SyntaxFactory . Parameter ( SyntaxFactory . Identifier ( "proj" ) ) ,
242+ newBody ) ;
310243 }
311244
312- class PropertyAccess
245+ class PropertyAccess ( string fullPath , string flatName , SyntaxNode originalAccess )
313246 {
314- public PropertyAccess ( string fullPath , string flatName , SyntaxNode originalAccess )
315- {
316- FullPath = fullPath ;
317- FlatName = flatName ;
318- OriginalAccess = originalAccess ;
319- }
320-
321- public string FullPath { get ; }
322- public string FlatName { get ; }
323- public SyntaxNode OriginalAccess { get ; }
247+ public string FullPath { get ; } = fullPath ;
248+ public string FlatName { get ; } = flatName ;
249+ public SyntaxNode OriginalAccess { get ; } = originalAccess ;
324250 }
325251}
0 commit comments