Skip to content

Commit 310dcb6

Browse files
committed
sg: abstract classes, records
1 parent 426f6e9 commit 310dcb6

File tree

12 files changed

+1766
-70
lines changed

12 files changed

+1766
-70
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System;
2+
3+
namespace FastCloner.SourceGenerator.Shared;
4+
5+
/// <summary>
6+
/// When applied to an abstract class with [FastClonerClonable], disables automatic discovery
7+
/// of derived types in the compilation. Only types explicitly registered via [FastClonerInclude]
8+
/// will be used for the type dispatcher.
9+
/// </summary>
10+
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
11+
internal class FastClonerDisableAutoDiscoveryAttribute : Attribute
12+
{
13+
}
14+

src/FastCloner.SourceGenerator/ClassCloneBodyGenerator.cs

Lines changed: 85 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ internal static class ClassCloneBodyGenerator
1111
{
1212
/// <summary>
1313
/// Checks if FormatterServices using statement is needed for the given type.
14+
/// Records don't need FormatterServices since they have copy constructors.
1415
/// </summary>
1516
public static bool NeedsFormatterServices(TypeModel model)
1617
{
17-
return !model.HasParameterlessConstructor && !model.IsStruct;
18+
return !model.HasParameterlessConstructor && !model.IsStruct && !model.IsRecord;
1819
}
1920

2021
/// <summary>
@@ -32,25 +33,6 @@ public static bool NeedsFormatterServices(IEnumerable<TypeModel> types)
3233
return false;
3334
}
3435

35-
/// <summary>
36-
/// Writes the code to instantiate a class, handling both parameterless and non-parameterless constructors.
37-
/// </summary>
38-
/// <param name="sb">StringBuilder to write to</param>
39-
/// <param name="typeName">Name of the type to instantiate</param>
40-
/// <param name="hasParameterlessConstructor">Whether the type has a parameterless constructor</param>
41-
public static void WriteInstanceCreation(StringBuilder sb, string typeName, bool hasParameterlessConstructor)
42-
{
43-
if (hasParameterlessConstructor)
44-
{
45-
sb.AppendLine($" var result = new {typeName}();");
46-
}
47-
else
48-
{
49-
// Use FormatterServices.GetUninitializedObject to create instance without calling constructor
50-
sb.AppendLine($" var result = ({typeName})FormatterServices.GetUninitializedObject(typeof({typeName}));");
51-
}
52-
}
53-
5436
/// <summary>
5537
/// Writes the complete class clone body code.
5638
/// </summary>
@@ -59,31 +41,41 @@ public static void WriteInstanceCreation(StringBuilder sb, string typeName, bool
5941
/// <param name="useState">Whether to use state tracking for circular references</param>
6042
/// <param name="stateVarName">Name of the state variable (null if not using state)</param>
6143
/// <param name="useNullConditional">Whether to use null-conditional operator (?.) when calling AddKnownRef</param>
44+
/// <param name="sourceVarName">Name of the source variable (usually "source" for classes, "src" for structs after null check)</param>
6245
public static void WriteClassCloneBody(
6346
CloneGeneratorContext ctx,
6447
string typeName,
6548
bool useState,
6649
string? stateVarName = null,
67-
bool useNullConditional = false)
50+
bool useNullConditional = false,
51+
string sourceVarName = "source")
6852
{
6953
var sb = ctx.Source;
7054
var hasParameterlessConstructor = ctx.Model.HasParameterlessConstructor;
55+
var isRecord = ctx.Model.IsRecord;
56+
57+
// For records without circular references, use the idiomatic 'with' expression
58+
if (isRecord && !useState)
59+
{
60+
WriteRecordCloneBody(ctx, typeName, sourceVarName);
61+
return;
62+
}
7163

7264
if (useState)
7365
{
7466
// When tracking circular references, we must register the instance BEFORE cloning members
7567
// to avoid infinite recursion (StackOverflowException) in case of cycles.
7668
// This requires us to instantiate first, then register, then assign members.
77-
WriteInstanceCreation(sb, typeName, hasParameterlessConstructor);
69+
WriteInstanceCreation(sb, typeName, hasParameterlessConstructor, isRecord, sourceVarName);
7870

7971
var stateVarForAdd = stateVarName ?? "state";
8072
var nullConditional = useNullConditional ? "?" : "";
81-
sb.AppendLine($" {stateVarForAdd}{nullConditional}.AddKnownRef(source, result);");
73+
sb.AppendLine($" {stateVarForAdd}{nullConditional}.AddKnownRef({sourceVarName}, result);");
8274
sb.AppendLine();
8375

8476
foreach (var member in ctx.Model.Members)
8577
{
86-
MemberCloneGenerator.WriteMemberCloning(ctx, member, "result", "source", stateVarForAdd);
78+
MemberCloneGenerator.WriteMemberCloning(ctx, member, "result", sourceVarName, stateVarForAdd);
8779
}
8880

8981
sb.AppendLine();
@@ -100,7 +92,7 @@ public static void WriteClassCloneBody(
10092
var memberAssignments = new List<string>();
10193
foreach (var member in ctx.Model.Members)
10294
{
103-
var assignment = MemberCloneGenerator.GetMemberAssignment(ctx, member, "source", "null");
95+
var assignment = MemberCloneGenerator.GetMemberAssignment(ctx, member, sourceVarName, "null");
10496
if (!string.IsNullOrEmpty(assignment))
10597
{
10698
memberAssignments.Add($" {assignment}");
@@ -117,18 +109,84 @@ public static void WriteClassCloneBody(
117109
else
118110
{
119111
// Use FormatterServices.GetUninitializedObject to create instance without calling constructor
120-
WriteInstanceCreation(sb, typeName, hasParameterlessConstructor);
112+
WriteInstanceCreation(sb, typeName, hasParameterlessConstructor, isRecord, sourceVarName);
121113

122114
// Then assign members individually (no state needed for non-circular types)
123115
foreach (var member in ctx.Model.Members)
124116
{
125-
MemberCloneGenerator.WriteMemberCloning(ctx, member, "result", "source", "null");
117+
MemberCloneGenerator.WriteMemberCloning(ctx, member, "result", sourceVarName, "null");
126118
}
127119
}
128120

129121
sb.AppendLine();
130122
sb.AppendLine(" return result;");
131123
}
132124
}
125+
126+
/// <summary>
127+
/// Writes the code to instantiate a class, handling both parameterless and non-parameterless constructors.
128+
/// For records, uses the 'with' expression for shallow copy.
129+
/// </summary>
130+
/// <param name="sourceVarName">The name of the source variable (usually "source" for classes, "src" for structs after null check)</param>
131+
private static void WriteInstanceCreation(StringBuilder sb, string typeName, bool hasParameterlessConstructor, bool isRecord, string sourceVarName = "source")
132+
{
133+
if (isRecord)
134+
{
135+
// Records use 'with' expression for shallow copy (modifiable later)
136+
sb.AppendLine($" var result = {sourceVarName} with {{ }};");
137+
}
138+
else if (hasParameterlessConstructor)
139+
{
140+
sb.AppendLine($" var result = new {typeName}();");
141+
}
142+
else
143+
{
144+
// Use FormatterServices.GetUninitializedObject to create instance without calling constructor
145+
sb.AppendLine($" var result = ({typeName})FormatterServices.GetUninitializedObject(typeof({typeName}));");
146+
}
147+
}
148+
149+
/// <summary>
150+
/// Writes the clone body for records using the 'with' expression.
151+
/// Only includes members that need deep cloning in the 'with' expression.
152+
/// </summary>
153+
/// <param name="sourceVarName">The name of the source variable (usually "source" for classes, "src" for structs after null check)</param>
154+
private static void WriteRecordCloneBody(CloneGeneratorContext ctx, string typeName, string sourceVarName = "source")
155+
{
156+
var sb = ctx.Source;
157+
158+
// Collect members that need deep cloning (not safe types)
159+
var deepCloneAssignments = new List<string>();
160+
foreach (var member in ctx.Model.Members)
161+
{
162+
// Skip safe types - they're already shallow copied by 'with'
163+
if (member.TypeKind == MemberTypeKind.Safe)
164+
continue;
165+
166+
// Skip read-only members
167+
if (member.IsReadOnly)
168+
continue;
169+
170+
var assignment = MemberCloneGenerator.GetMemberAssignment(ctx, member, sourceVarName, "null");
171+
if (!string.IsNullOrEmpty(assignment))
172+
{
173+
deepCloneAssignments.Add($" {assignment}");
174+
}
175+
}
176+
177+
if (deepCloneAssignments.Count == 0)
178+
{
179+
// All members are safe - simple shallow copy
180+
sb.AppendLine($" return {sourceVarName} with {{ }};");
181+
}
182+
else
183+
{
184+
// Use 'with' expression with only the members that need deep cloning
185+
sb.AppendLine($" return {sourceVarName} with");
186+
sb.AppendLine(" {");
187+
sb.AppendLine(string.Join(",\n", deepCloneAssignments));
188+
sb.AppendLine(" };");
189+
}
190+
}
133191
}
134192

0 commit comments

Comments
 (0)