Skip to content

Commit ad79231

Browse files
Copilotarika0093
andcommitted
feat: Improve formatting of generated source code for nested structures (#145)
* Initial plan * feat: Improve formatting of generated source code for nested structures Co-authored-by: arika0093 <4524647+arika0093@users.noreply.github.com> * feat: Improve formatting of generated source code for nested structures Co-authored-by: arika0093 <4524647+arika0093@users.noreply.github.com> * fix: Improve nested Select formatting with proper indentation levels 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> (cherry picked from commit 58b8170)
1 parent 4cec9f3 commit ad79231

File tree

2 files changed

+154
-67
lines changed

2 files changed

+154
-67
lines changed

src/Linqraft.Core/RoslynHelpers/RoslynTypeHelper.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ public static bool ContainsSelectInvocation(ExpressionSyntax expression)
193193
return false;
194194

195195
return expression
196-
.DescendantNodes()
196+
.DescendantNodesAndSelf()
197197
.OfType<InvocationExpressionSyntax>()
198198
.Any(inv =>
199199
inv.Expression is MemberAccessExpressionSyntax ma
@@ -212,7 +212,7 @@ public static bool ContainsSelectManyInvocation(ExpressionSyntax expression)
212212
return false;
213213

214214
return expression
215-
.DescendantNodes()
215+
.DescendantNodesAndSelf()
216216
.OfType<InvocationExpressionSyntax>()
217217
.Any(inv =>
218218
inv.Expression is MemberAccessExpressionSyntax ma

src/Linqraft.Core/SelectExprInfo.cs

Lines changed: 152 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -236,25 +236,7 @@ protected string GeneratePropertyAssignment(DtoProperty property, int indents)
236236
// For nested structure cases
237237
if (property.NestedStructure is not null)
238238
{
239-
// General approach: Replace any anonymous type creation in the expression with the DTO
240-
// This works for direct anonymous types, ternary operators, and any other expression structure
241-
var anonymousCreation = syntax
242-
.DescendantNodesAndSelf()
243-
.OfType<AnonymousObjectCreationExpressionSyntax>()
244-
.FirstOrDefault();
245-
246-
if (anonymousCreation != null)
247-
{
248-
// Convert the expression by replacing the anonymous type with the DTO
249-
return ConvertExpressionWithAnonymousTypeToDto(
250-
syntax,
251-
anonymousCreation,
252-
property.NestedStructure,
253-
indents
254-
);
255-
}
256-
257-
// Check if this contains SelectMany
239+
// Check if this contains SelectMany first (it's more specific)
258240
if (RoslynTypeHelper.ContainsSelectManyInvocation(syntax))
259241
{
260242
// For nested SelectMany (collection flattening) case
@@ -275,19 +257,44 @@ protected string GeneratePropertyAssignment(DtoProperty property, int indents)
275257
return convertedSelectMany;
276258
}
277259

278-
// For nested Select (collection) case
279-
var convertedSelect = ConvertNestedSelectWithRoslyn(
280-
syntax,
281-
property.NestedStructure,
282-
indents
283-
);
284-
// Debug: Check if conversion was performed correctly
285-
if (convertedSelect == expression && RoslynTypeHelper.ContainsSelectInvocation(syntax))
260+
// Check if this contains Select - use dedicated formatting for better output
261+
if (RoslynTypeHelper.ContainsSelectInvocation(syntax))
286262
{
287-
// If conversion was not performed, leave the original expression as a comment
288-
return $"{convertedSelect} /* CONVERSION FAILED: {property.Name} */";
263+
// For nested Select (collection) case - handles both anonymous and named types
264+
var convertedSelect = ConvertNestedSelectWithRoslyn(
265+
syntax,
266+
property.NestedStructure,
267+
indents
268+
);
269+
// Debug: Check if conversion was performed correctly
270+
if (convertedSelect == expression)
271+
{
272+
// If conversion was not performed, leave the original expression as a comment
273+
return $"{convertedSelect} /* CONVERSION FAILED: {property.Name} */";
274+
}
275+
return convertedSelect;
276+
}
277+
278+
// For other cases with anonymous types (e.g., ternary operators, direct anonymous types)
279+
// Replace any anonymous type creation in the expression with the DTO
280+
var anonymousCreation = syntax
281+
.DescendantNodesAndSelf()
282+
.OfType<AnonymousObjectCreationExpressionSyntax>()
283+
.FirstOrDefault();
284+
285+
if (anonymousCreation != null)
286+
{
287+
// Convert the expression by replacing the anonymous type with the DTO
288+
return ConvertExpressionWithAnonymousTypeToDto(
289+
syntax,
290+
anonymousCreation,
291+
property.NestedStructure,
292+
indents
293+
);
289294
}
290-
return convertedSelect;
295+
296+
// Fallback: return the original expression
297+
return expression;
291298
}
292299
// If nullable operator is used, convert to explicit null check
293300
if (
@@ -426,24 +433,29 @@ protected string ConvertExpressionWithAnonymousTypeToDto(
426433
int indents
427434
)
428435
{
436+
var spaces = CodeFormatter.IndentSpaces(indents);
437+
var innerSpaces = CodeFormatter.IndentSpaces(indents + CodeFormatter.IndentSize);
429438
var nestedClassName = GetClassName(nestedStructure);
430439
var nestedDtoName = string.IsNullOrEmpty(nestedClassName)
431440
? ""
432441
: GetNestedDtoFullName(nestedClassName);
433442

434-
// Generate the DTO object creation to replace the anonymous type
435-
// We need to preserve indentation based on where the anonymous type appears
443+
// Generate the DTO object creation to replace the anonymous type with proper formatting
436444
var propertyAssignments = new List<string>();
437445
foreach (var prop in nestedStructure.Properties)
438446
{
439-
var assignment = GeneratePropertyAssignment(prop, 0);
440-
propertyAssignments.Add($" {prop.Name} = {assignment}");
447+
var assignment = GeneratePropertyAssignment(prop, indents + CodeFormatter.IndentSize);
448+
propertyAssignments.Add($"{innerSpaces}{prop.Name} = {assignment}");
441449
}
442450
var propertiesCode = string.Join($",{CodeFormatter.DefaultNewLine}", propertyAssignments);
443451

444-
// Build the DTO creation as a compact single-line or multi-line depending on complexity
445-
var dtoCreation =
446-
$"new {nestedDtoName}{CodeFormatter.DefaultNewLine}{{{CodeFormatter.DefaultNewLine}{propertiesCode}{CodeFormatter.DefaultNewLine}}}";
452+
// Build the DTO creation with proper formatting
453+
var dtoCreation = $$"""
454+
new {{nestedDtoName}}
455+
{{spaces}}{
456+
{{propertiesCode}}
457+
{{spaces}}}
458+
""";
447459

448460
// Remove comments from syntax before processing
449461
var cleanSyntax = RemoveComments(syntax);
@@ -469,26 +481,29 @@ int indents
469481
)
470482
{
471483
var spaces = CodeFormatter.IndentSpaces(indents);
484+
var innerSpaces = CodeFormatter.IndentSpaces(indents + CodeFormatter.IndentSize);
472485
var nestedClassName = GetClassName(nestedStructure);
473486
// For anonymous types (empty class name), don't use namespace qualification
474487
var nestedDtoName = string.IsNullOrEmpty(nestedClassName)
475488
? ""
476489
: GetNestedDtoFullName(nestedClassName);
477490

478-
// Generate property assignments for nested DTO
491+
// Generate property assignments for nested DTO with proper formatting
479492
var propertyAssignments = new List<string>();
480493
foreach (var prop in nestedStructure.Properties)
481494
{
482-
var assignment = GeneratePropertyAssignment(prop, indents + CodeFormatter.IndentSize);
483-
propertyAssignments.Add(
484-
$"{spaces}{CodeFormatter.Indent(1)}{prop.Name} = {assignment},"
495+
var assignment = GeneratePropertyAssignment(
496+
prop,
497+
indents + CodeFormatter.IndentSize * 2
485498
);
499+
propertyAssignments.Add($"{innerSpaces}{prop.Name} = {assignment}");
486500
}
487-
var propertiesCode = string.Join(CodeFormatter.DefaultNewLine, propertyAssignments);
501+
var propertiesCode = string.Join($",{CodeFormatter.DefaultNewLine}", propertyAssignments);
488502

489-
// Build the new DTO object creation expression
503+
// Build the new DTO object creation expression with proper formatting
490504
var code = $$"""
491-
new {{nestedDtoName}} {
505+
new {{nestedDtoName}}
506+
{{spaces}}{
492507
{{propertiesCode}}
493508
{{spaces}}}
494509
""";
@@ -505,6 +520,7 @@ int indents
505520
)
506521
{
507522
var spaces = CodeFormatter.IndentSpaces(indents);
523+
var innerSpaces = CodeFormatter.IndentSpaces(indents + CodeFormatter.IndentSize);
508524
var nestedClassName = GetClassName(nestedStructure);
509525
// For anonymous types (empty class name), don't use namespace qualification
510526
var nestedDtoName = string.IsNullOrEmpty(nestedClassName)
@@ -535,18 +551,25 @@ int indents
535551
"."
536552
);
537553

538-
// Generate property assignments for nested DTO
554+
// Generate property assignments for nested DTO with proper formatting
555+
// Properties should be indented two levels from the base (one for Select block, one for properties)
556+
var propertyIndentSpaces = CodeFormatter.IndentSpaces(indents + CodeFormatter.IndentSize * 2);
539557
var propertyAssignments = new List<string>();
540558
foreach (var prop in nestedStructure.Properties)
541559
{
542-
var assignment = GeneratePropertyAssignment(prop, indents + CodeFormatter.IndentSize);
543-
propertyAssignments.Add(
544-
$"{spaces}{CodeFormatter.Indent(1)}{prop.Name} = {assignment},"
560+
var assignment = GeneratePropertyAssignment(
561+
prop,
562+
indents + CodeFormatter.IndentSize * 2
545563
);
564+
propertyAssignments.Add($"{propertyIndentSpaces}{prop.Name} = {assignment}");
546565
}
547-
var propertiesCode = string.Join(CodeFormatter.DefaultNewLine, propertyAssignments);
566+
var propertiesCode = string.Join($",{CodeFormatter.DefaultNewLine}", propertyAssignments);
548567

549-
// Build the Select expression
568+
// Format chained methods with proper indentation (one level from base)
569+
var formattedChainedMethods = FormatChainedMethods(chainedMethods, innerSpaces);
570+
571+
// Build the Select expression with proper formatting
572+
// The .Select, {, }, and chained methods should all be indented one level from the property assignment
550573
if (hasNullableAccess)
551574
{
552575
// Determine default value
@@ -559,23 +582,78 @@ int indents
559582
);
560583

561584
var code = $$"""
562-
{{baseExpression}} != null ? {{baseExpression}}.Select({{paramName}} => new {{nestedDtoName}} {
585+
{{baseExpression}} != null ? {{baseExpression}}
586+
{{innerSpaces}}.Select({{paramName}} => new {{nestedDtoName}}
587+
{{innerSpaces}}{
563588
{{propertiesCode}}
564-
{{spaces}}}){{chainedMethods}} : {{defaultValue}}
589+
{{innerSpaces}}}){{formattedChainedMethods}} : {{defaultValue}}
565590
""";
566591
return code;
567592
}
568593
else
569594
{
570595
var code = $$"""
571-
{{baseExpression}}.Select({{paramName}} => new {{nestedDtoName}} {
596+
{{baseExpression}}
597+
{{innerSpaces}}.Select({{paramName}} => new {{nestedDtoName}}
598+
{{innerSpaces}}{
572599
{{propertiesCode}}
573-
{{spaces}}}){{chainedMethods}}
600+
{{innerSpaces}}}){{formattedChainedMethods}}
574601
""";
575602
return code;
576603
}
577604
}
578605

606+
/// <summary>
607+
/// Formats chained method calls (like .ToList()) with proper indentation
608+
/// </summary>
609+
private static string FormatChainedMethods(string chainedMethods, string spaces)
610+
{
611+
if (string.IsNullOrEmpty(chainedMethods))
612+
return "";
613+
614+
// Normalize whitespace from chained method calls
615+
var normalized = System.Text.RegularExpressions.Regex.Replace(
616+
chainedMethods.Trim(),
617+
@"\s+",
618+
""
619+
);
620+
621+
// Each method call should be on a new line with proper indentation
622+
var result = new StringBuilder();
623+
var currentMethod = new StringBuilder();
624+
var parenDepth = 0;
625+
626+
foreach (var c in normalized)
627+
{
628+
currentMethod.Append(c);
629+
630+
if (c == '(')
631+
parenDepth++;
632+
else if (c == ')')
633+
{
634+
parenDepth--;
635+
if (parenDepth == 0)
636+
{
637+
// Complete method call with parentheses
638+
result.Append(CodeFormatter.DefaultNewLine);
639+
result.Append(spaces);
640+
result.Append(currentMethod);
641+
currentMethod.Clear();
642+
}
643+
}
644+
}
645+
646+
// Handle any remaining content (e.g., property access without parentheses like .Count)
647+
if (currentMethod.Length > 0)
648+
{
649+
result.Append(CodeFormatter.DefaultNewLine);
650+
result.Append(spaces);
651+
result.Append(currentMethod);
652+
}
653+
654+
return result.ToString();
655+
}
656+
579657
/// <summary>
580658
/// Converts nested SelectMany expressions using Roslyn syntax analysis
581659
/// </summary>
@@ -586,6 +664,7 @@ int indents
586664
)
587665
{
588666
var spaces = CodeFormatter.IndentSpaces(indents);
667+
var innerSpaces = CodeFormatter.IndentSpaces(indents + CodeFormatter.IndentSize);
589668

590669
// Use Roslyn to extract SelectMany information
591670
var selectManyInfo = ExtractSelectManyInfoFromSyntax(syntax);
@@ -619,21 +698,25 @@ int indents
619698
? ""
620699
: GetNestedDtoFullName(nestedClassName);
621700

622-
// Generate property assignments for nested DTO
701+
// Generate property assignments for nested DTO with proper formatting
702+
// Properties should be indented two levels from the base (one for SelectMany block, one for properties)
703+
var propertyIndentSpaces = CodeFormatter.IndentSpaces(indents + CodeFormatter.IndentSize * 2);
623704
var propertyAssignments = new List<string>();
624705
foreach (var prop in nestedStructure.Properties)
625706
{
626707
var assignment = GeneratePropertyAssignment(
627708
prop,
628-
indents + CodeFormatter.IndentSize
629-
);
630-
propertyAssignments.Add(
631-
$"{spaces}{CodeFormatter.Indent(1)}{prop.Name} = {assignment},"
709+
indents + CodeFormatter.IndentSize * 2
632710
);
711+
propertyAssignments.Add($"{propertyIndentSpaces}{prop.Name} = {assignment}");
633712
}
634-
var propertiesCode = string.Join(CodeFormatter.DefaultNewLine, propertyAssignments);
713+
var propertiesCode = string.Join($",{CodeFormatter.DefaultNewLine}", propertyAssignments);
714+
715+
// Format chained methods with proper indentation (one level from base)
716+
var formattedChainedMethods = FormatChainedMethods(chainedMethods, innerSpaces);
635717

636-
// Build the SelectMany expression with projection
718+
// Build the SelectMany expression with projection and proper formatting
719+
// The .SelectMany, {, }, and chained methods should all be indented one level from the property assignment
637720
if (hasNullableAccess)
638721
{
639722
var defaultValue =
@@ -645,18 +728,22 @@ int indents
645728
);
646729

647730
var code = $$"""
648-
{{baseExpression}} != null ? {{baseExpression}}.SelectMany({{paramName}} => new {{nestedDtoName}} {
731+
{{baseExpression}} != null ? {{baseExpression}}
732+
{{innerSpaces}}.SelectMany({{paramName}} => new {{nestedDtoName}}
733+
{{innerSpaces}}{
649734
{{propertiesCode}}
650-
{{spaces}}}){{chainedMethods}} : {{defaultValue}}
735+
{{innerSpaces}}}){{formattedChainedMethods}} : {{defaultValue}}
651736
""";
652737
return code;
653738
}
654739
else
655740
{
656741
var code = $$"""
657-
{{baseExpression}}.SelectMany({{paramName}} => new {{nestedDtoName}} {
742+
{{baseExpression}}
743+
{{innerSpaces}}.SelectMany({{paramName}} => new {{nestedDtoName}}
744+
{{innerSpaces}}{
658745
{{propertiesCode}}
659-
{{spaces}}}){{chainedMethods}}
746+
{{innerSpaces}}}){{formattedChainedMethods}}
660747
""";
661748
return code;
662749
}

0 commit comments

Comments
 (0)