Skip to content

Commit 458dadf

Browse files
Copilotarika0093
andcommitted
Colocate explicit DTO generation with SelectExpr files (#248)
* Initial plan * feat: colocate explicit dtos with expressions Co-authored-by: arika0093 <4524647+arika0093@users.noreply.github.com> * chore: factor hash namespace prefix constant Co-authored-by: arika0093 <4524647+arika0093@users.noreply.github.com> * chore: refine hash namespace detection Co-authored-by: arika0093 <4524647+arika0093@users.noreply.github.com> * docs: reflect dto emission in generator comment 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 67d7693)
1 parent d725e2e commit 458dadf

File tree

4 files changed

+109
-11
lines changed

4 files changed

+109
-11
lines changed

src/Linqraft.Core/GenerateSourceCodeSnippets.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,18 @@ IncrementalGeneratorPostInitializationContext context
3030
// Generate expression and headers
3131
public static string BuildExprCodeSnippetsWithHeaders(
3232
List<string> expressions,
33-
List<string> staticFields
33+
List<string> staticFields,
34+
string? dtoCode = null
3435
)
3536
{
37+
var dtoPart = string.IsNullOrWhiteSpace(dtoCode)
38+
? string.Empty
39+
: $"{CodeFormatter.DefaultNewLine}{dtoCode}";
40+
3641
return $$"""
3742
{{GenerateCommentHeaderPart()}}
3843
{{GenerateHeaderFlagsPart}}
39-
{{BuildExprCodeSnippets(expressions, staticFields)}}
44+
{{BuildExprCodeSnippets(expressions, staticFields)}}{{dtoPart}}
4045
""";
4146
}
4247

src/Linqraft.SourceGenerator/SelectExprGenerator.cs

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Collections.Generic;
23
using System.Linq;
34
using Linqraft.Core;
@@ -117,32 +118,66 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
117118
})
118119
.ToList();
119120

120-
// Collect all DTOs from all groups and deduplicate globally
121-
var allDtoClassInfos = new List<GenerateDtoClassInfo>();
121+
var hashNamespaceDtoClassInfos = new List<GenerateDtoClassInfo>();
122+
var emittedDtoFullNames = new HashSet<string>();
123+
122124
foreach (var exprGroup in exprGroups)
123125
{
126+
var groupDtos = new List<GenerateDtoClassInfo>();
127+
124128
foreach (var expr in exprGroup.Exprs)
125129
{
126130
var classInfos = expr.Info.GenerateDtoClasses();
127-
allDtoClassInfos.AddRange(classInfos);
131+
foreach (var classInfo in classInfos)
132+
{
133+
// DTOs in hash-named namespaces can stay in the shared file
134+
if (IsHashNamespaceDto(classInfo.Namespace))
135+
{
136+
hashNamespaceDtoClassInfos.Add(classInfo);
137+
continue;
138+
}
139+
140+
// Deduplicate by FullName and keep the first occurrence in this group
141+
if (emittedDtoFullNames.Add(classInfo.FullName))
142+
{
143+
groupDtos.Add(classInfo);
144+
}
145+
}
128146
}
147+
148+
exprGroup.DtoClasses = groupDtos;
129149
}
130150

131-
// Generate all DTOs in a single shared source file
151+
// Generate DTOs that remain in the shared file (hash namespaces)
132152
var dtoCode = GenerateSourceCodeSnippets.BuildGlobalDtoCodeSnippet(
133-
allDtoClassInfos,
153+
hashNamespaceDtoClassInfos,
134154
config
135155
);
136156
if (!string.IsNullOrEmpty(dtoCode))
137157
{
138158
spc.AddSource("GeneratedDtos.g.cs", dtoCode);
139159
}
140160

141-
// Generate code for expression methods (without DTOs)
161+
// Generate code for expression methods (with co-located DTOs)
142162
foreach (var exprGroup in exprGroups)
143163
{
144164
exprGroup.GenerateCodeWithoutDtos(spc);
145165
}
166+
167+
static bool IsHashNamespaceDto(string? namespaceName)
168+
{
169+
const string HashNamespacePrefix = "LinqraftGenerated_";
170+
171+
if (namespaceName is not { Length: > 0 } ns)
172+
{
173+
return false;
174+
}
175+
176+
var parts = ns.Split('.');
177+
return parts.Any(p =>
178+
p.StartsWith(HashNamespacePrefix, StringComparison.Ordinal)
179+
);
180+
}
146181
}
147182
);
148183
}

src/Linqraft.SourceGenerator/SelectExprGroups.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ internal class SelectExprGroups
1111
{
1212
public required List<SelectExprLocations> Exprs { get; set; }
1313

14+
public List<GenerateDtoClassInfo> DtoClasses { get; set; } = [];
15+
1416
public required LinqraftConfiguration Configuration { get; set; }
1517

1618
public required string TargetNamespace
@@ -43,7 +45,7 @@ public string GetUniqueId()
4345
return $"{targetNsReplaced}_{filenameReplaced}";
4446
}
4547

46-
// Generate source code without DTOs (for global DTO generation)
48+
// Generate source code for expressions and co-located DTOs
4749
public virtual void GenerateCodeWithoutDtos(SourceProductionContext context)
4850
{
4951
try
@@ -79,12 +81,24 @@ public virtual void GenerateCodeWithoutDtos(SourceProductionContext context)
7981
}
8082
}
8183

84+
var dtoCode = DtoClasses.Count > 0
85+
? GenerateSourceCodeSnippets.BuildDtoCodeSnippetsGroupedByNamespace(
86+
DtoClasses,
87+
Configuration
88+
)
89+
: string.Empty;
90+
8291
// Generate interceptor-based expression methods
83-
if (selectExprMethods.Count > 0 || staticFields.Count > 0)
92+
if (
93+
selectExprMethods.Count > 0
94+
|| staticFields.Count > 0
95+
|| !string.IsNullOrEmpty(dtoCode)
96+
)
8497
{
8598
var sourceCode = GenerateSourceCodeSnippets.BuildExprCodeSnippetsWithHeaders(
8699
selectExprMethods,
87-
staticFields
100+
staticFields,
101+
dtoCode
88102
);
89103
var uniqueId = GetUniqueId();
90104
context.AddSource($"GeneratedExpression_{uniqueId}.g.cs", sourceCode);
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using System;
2+
using System.IO;
3+
4+
namespace Linqraft.Tests;
5+
6+
public class ExplicitDtoLocationTest
7+
{
8+
[Fact]
9+
public void Explicit_dto_classes_are_emitted_with_expression_file()
10+
{
11+
var projectDir = GetProjectDirectory();
12+
var generatorDir = Path.Combine(
13+
projectDir,
14+
".generated",
15+
"Linqraft.SourceGenerator",
16+
"Linqraft.SelectExprGenerator"
17+
);
18+
19+
var expressionFile = Path.Combine(
20+
generatorDir,
21+
"GeneratedExpression_Linqraft_Tests_ExplicitDtoComprehensiveTest.g.cs"
22+
);
23+
var dtoFile = Path.Combine(generatorDir, "GeneratedDtos.g.cs");
24+
25+
File.Exists(expressionFile).ShouldBeTrue();
26+
27+
var expressionCode = File.ReadAllText(expressionFile);
28+
expressionCode.ShouldContain("partial class SimpleNullableDto");
29+
expressionCode.ShouldContain("partial class NullConditionalDto");
30+
31+
if (File.Exists(dtoFile))
32+
{
33+
var dtoCode = File.ReadAllText(dtoFile);
34+
dtoCode.ShouldNotContain("partial class SimpleNullableDto");
35+
dtoCode.ShouldNotContain("partial class NullConditionalDto");
36+
}
37+
}
38+
39+
private static string GetProjectDirectory()
40+
{
41+
var baseDir = AppContext.BaseDirectory;
42+
return Path.GetFullPath(Path.Combine(baseDir, "..", "..", ".."));
43+
}
44+
}

0 commit comments

Comments
 (0)