Skip to content

Commit 9189f3a

Browse files
Copilotarika0093
andauthored
fix: handle nested member access in anonymous capture aliases (#279)
* Initial plan * fix: handle nested member access in anonymous capture aliases Co-authored-by: arika0093 <4524647+arika0093@users.noreply.github.com> * refactor: improve code clarity per review feedback Co-authored-by: arika0093 <4524647+arika0093@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: arika0093 <4524647+arika0093@users.noreply.github.com>
1 parent 0969cbd commit 9189f3a

File tree

2 files changed

+117
-13
lines changed

2 files changed

+117
-13
lines changed

src/Linqraft.Core/SelectExprInfo.cs

Lines changed: 80 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -154,22 +154,22 @@ protected string GenerateAnonymousCaptureMemberAccessAliases()
154154

155155
if (initializer.Expression is MemberAccessExpressionSyntax memberAccess)
156156
{
157-
var rootName = GetRootIdentifierName(memberAccess.Expression);
157+
var rootName = GetRootIdentifierName(memberAccess);
158158
if (rootName == null)
159159
{
160160
continue;
161161
}
162162

163-
var memberName = memberAccess.Name.Identifier.Text;
164-
var captureName = capturePropertyName ?? memberName;
163+
var memberPath = GetMemberPathFromRoot(memberAccess, rootName);
164+
var captureName = capturePropertyName ?? memberAccess.Name.Identifier.Text;
165165

166166
if (!aliasMap.TryGetValue(rootName, out var members))
167167
{
168168
members = new List<(string MemberName, string CapturePropertyName)>();
169169
aliasMap[rootName] = members;
170170
}
171171

172-
members.Add((memberName, captureName));
172+
members.Add((memberPath, captureName));
173173
}
174174
}
175175

@@ -189,27 +189,70 @@ protected string GenerateAnonymousCaptureMemberAccessAliases()
189189
{
190190
var rootName = kvp.Key;
191191
var members = kvp.Value;
192-
sb.AppendLine($" var {rootName} = new");
193-
sb.AppendLine(" {");
194-
195-
var usedMemberNames = new HashSet<string>();
192+
193+
// Build a tree structure for nested member paths
194+
var tree = new Dictionary<string, object>();
196195
foreach (var memberPair in members)
197196
{
198-
var memberName = memberPair.MemberName;
197+
var memberPath = memberPair.MemberName;
199198
var captureName = memberPair.CapturePropertyName;
200-
if (!usedMemberNames.Add(memberName))
199+
200+
var pathParts = memberPath.Split('.');
201+
var current = tree;
202+
203+
for (int i = 0; i < pathParts.Length - 1; i++)
201204
{
202-
continue;
205+
var part = pathParts[i];
206+
if (!current.TryGetValue(part, out var child))
207+
{
208+
child = new Dictionary<string, object>();
209+
current[part] = child;
210+
}
211+
current = (Dictionary<string, object>)child;
212+
}
213+
214+
// Add the leaf node with the capture name
215+
var lastPart = pathParts[^1];
216+
if (!current.ContainsKey(lastPart))
217+
{
218+
current[lastPart] = captureName;
203219
}
204-
sb.AppendLine($" {memberName} = captureObj.{captureName},");
205220
}
206-
221+
222+
// Generate the nested anonymous object
223+
sb.AppendLine($" var {rootName} = new");
224+
sb.AppendLine(" {");
225+
GenerateNestedAnonymousObject(sb, tree, 2, "captureObj");
207226
sb.AppendLine(" };");
208227
}
209228

210229
return sb.ToString();
211230
}
212231

232+
private static void GenerateNestedAnonymousObject(StringBuilder sb, Dictionary<string, object> tree, int indentLevel, string captureObjName)
233+
{
234+
var indent = new string(' ', indentLevel * 4);
235+
foreach (var kvp in tree)
236+
{
237+
var memberName = kvp.Key;
238+
var value = kvp.Value;
239+
240+
if (value is string captureName)
241+
{
242+
// Leaf node: simple assignment
243+
sb.AppendLine($"{indent}{memberName} = {captureObjName}.{captureName},");
244+
}
245+
else if (value is Dictionary<string, object> nested)
246+
{
247+
// Nested node: create nested anonymous object
248+
sb.AppendLine($"{indent}{memberName} = new");
249+
sb.AppendLine($"{indent}{{");
250+
GenerateNestedAnonymousObject(sb, nested, indentLevel + 1, captureObjName);
251+
sb.AppendLine($"{indent}}},");
252+
}
253+
}
254+
}
255+
213256
private static string? GetCapturePropertyName(AnonymousObjectMemberDeclaratorSyntax initializer)
214257
{
215258
if (initializer.NameEquals != null)
@@ -235,6 +278,30 @@ protected string GenerateAnonymousCaptureMemberAccessAliases()
235278
};
236279
}
237280

281+
private static string GetMemberPathFromRoot(ExpressionSyntax expression, string rootName)
282+
{
283+
var parts = new List<string>();
284+
var current = expression;
285+
286+
while (current is MemberAccessExpressionSyntax memberAccess)
287+
{
288+
parts.Insert(0, memberAccess.Name.Identifier.Text);
289+
current = memberAccess.Expression;
290+
}
291+
292+
// If current is the root identifier, we've collected all member parts
293+
if (current is IdentifierNameSyntax identifier && identifier.Identifier.Text == rootName)
294+
{
295+
return string.Join(".", parts);
296+
}
297+
298+
// Fallback: This should not happen in normal usage since we only call this method
299+
// after verifying GetRootIdentifierName succeeds. Return the last member name as
300+
// a safe fallback to avoid breaking the generation if the expression structure
301+
// is unexpected (e.g., complex expressions that aren't simple member chains).
302+
return parts.Count > 0 ? parts[^1] : "";
303+
}
304+
238305
/// <summary>
239306
/// Gets the full name for a nested DTO class using the structure.
240307
/// This allows derived classes to compute namespace-based naming using the structure's hash.

tests/Linqraft.Tests/LocalVariableCaptureTest.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,38 @@ public void IEnumerable_WithCapturedVariable()
256256
first.NewValue.ShouldBe(110);
257257
}
258258

259+
[Fact]
260+
public void Case3_NestedMemberAccess_InCapturedFields()
261+
{
262+
var request = new NestedRequest
263+
{
264+
Range = new RequestRange
265+
{
266+
FromDate = new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero),
267+
ToDate = new DateTimeOffset(2024, 1, 3, 0, 0, 0, TimeSpan.Zero),
268+
}
269+
};
270+
var users = BuildUsers();
271+
272+
var converted = users
273+
.AsQueryable()
274+
.SelectExpr<UserWithCommits, UserCommitDto>(
275+
u => new
276+
{
277+
u.Id,
278+
CommitCount = u.Commits.Count(c =>
279+
request.Range.FromDate <= c.Created && c.Created <= request.Range.ToDate
280+
),
281+
},
282+
new { request.Range.FromDate, request.Range.ToDate }
283+
)
284+
.ToList();
285+
286+
converted.Count.ShouldBe(2);
287+
converted[0].CommitCount.ShouldBe(1);
288+
converted[1].CommitCount.ShouldBe(0);
289+
}
290+
259291
private static List<UserWithCommits> BuildUsers() =>
260292
[
261293
new()
@@ -319,6 +351,11 @@ internal class RequestRange
319351
public DateTimeOffset ToDate { get; set; }
320352
}
321353

354+
internal class NestedRequest
355+
{
356+
public RequestRange Range { get; set; } = new();
357+
}
358+
322359
internal class Commit
323360
{
324361
public DateTimeOffset Created { get; set; }

0 commit comments

Comments
 (0)