@@ -15,15 +15,15 @@ internal static class ClassCloneBodyGenerator
1515 /// </summary>
1616 public static bool NeedsFormatterServices ( TypeModel model )
1717 {
18- return ! model . HasParameterlessConstructor && ! model . IsStruct && ! model . IsRecord ;
18+ return model is { HasParameterlessConstructor : false , IsStruct : false , IsRecord : false } ;
1919 }
2020
2121 /// <summary>
2222 /// Checks if any of the given types need FormatterServices.
2323 /// </summary>
2424 public static bool NeedsFormatterServices ( IEnumerable < TypeModel > types )
2525 {
26- foreach ( var type in types )
26+ foreach ( TypeModel ? type in types )
2727 {
2828 if ( NeedsFormatterServices ( type ) )
2929 {
@@ -50,9 +50,9 @@ public static void WriteClassCloneBody(
5050 bool useNullConditional = false ,
5151 string sourceVarName = "source" )
5252 {
53- var sb = ctx . Source ;
54- var hasParameterlessConstructor = ctx . Model . HasParameterlessConstructor ;
55- var isRecord = ctx . Model . IsRecord ;
53+ StringBuilder sb = ctx . Source ;
54+ bool hasParameterlessConstructor = ctx . Model . HasParameterlessConstructor ;
55+ bool isRecord = ctx . Model . IsRecord ;
5656
5757 // For records without circular references, use the idiomatic 'with' expression
5858 if ( isRecord && ! useState )
@@ -68,12 +68,12 @@ public static void WriteClassCloneBody(
6868 // This requires us to instantiate first, then register, then assign members.
6969 WriteInstanceCreation ( ctx , sb , typeName , hasParameterlessConstructor , isRecord , sourceVarName ) ;
7070
71- var stateVarForAdd = stateVarName ?? "state" ;
72- var nullConditional = useNullConditional ? "?" : "" ;
71+ string stateVarForAdd = stateVarName ?? "state" ;
72+ string nullConditional = useNullConditional ? "?" : "" ;
7373 sb . AppendLine ( $ " { stateVarForAdd } { nullConditional } .AddKnownRef({ sourceVarName } , result);") ;
7474 sb . AppendLine ( ) ;
7575
76- foreach ( var member in ctx . Model . Members )
76+ foreach ( MemberModel member in ctx . Model . Members )
7777 {
7878 MemberCloneGenerator . WriteMemberCloning ( ctx , member , "result" , sourceVarName , stateVarForAdd ) ;
7979 }
@@ -90,12 +90,12 @@ public static void WriteClassCloneBody(
9090 sb . Append ( $ " var result = new { typeName } ") ;
9191
9292 // Collect init-only and required properties for object initializer
93- var initOnlyMembers = new List < string > ( ) ;
94- foreach ( var member in ctx . Model . Members )
93+ List < string > initOnlyMembers = [ ] ;
94+ foreach ( MemberModel member in ctx . Model . Members )
9595 {
96- if ( ( member . IsProperty && member . IsInitOnly ) || member . IsRequired )
96+ if ( member is { IsProperty : true , IsInitOnly : true } || member . IsRequired )
9797 {
98- var assignment = MemberCloneGenerator . GetMemberAssignment ( ctx , member , sourceVarName , "null" , " " ) ;
98+ string assignment = MemberCloneGenerator . GetMemberAssignment ( ctx , member , sourceVarName , "null" , " " ) ;
9999 if ( ! string . IsNullOrEmpty ( assignment ) )
100100 {
101101 initOnlyMembers . Add ( $ " { assignment } ") ;
@@ -116,10 +116,10 @@ public static void WriteClassCloneBody(
116116 }
117117
118118 // Use statements for everything else (better for JIT and null handling)
119- foreach ( var member in ctx . Model . Members )
119+ foreach ( MemberModel member in ctx . Model . Members )
120120 {
121121 // Skip if already handled in initializer (init-only or required)
122- if ( ( member . IsProperty && member . IsInitOnly ) || member . IsRequired )
122+ if ( member is { IsProperty : true , IsInitOnly : true } || member . IsRequired )
123123 continue ;
124124
125125 if ( ! member . IsProperty || ! member . IsInitOnly )
@@ -134,7 +134,7 @@ public static void WriteClassCloneBody(
134134 WriteInstanceCreation ( ctx , sb , typeName , hasParameterlessConstructor , isRecord , sourceVarName ) ;
135135
136136 // Then assign members individually (no state needed for non-circular types)
137- foreach ( var member in ctx . Model . Members )
137+ foreach ( MemberModel member in ctx . Model . Members )
138138 {
139139 MemberCloneGenerator . WriteMemberCloning ( ctx , member , "result" , sourceVarName , "null" ) ;
140140 }
@@ -160,8 +160,8 @@ private static void WriteInstanceCreation(CloneGeneratorContext ctx, StringBuild
160160 else if ( hasParameterlessConstructor )
161161 {
162162 // Check for required members
163- var requiredMembers = new List < string > ( ) ;
164- foreach ( var member in ctx . Model . Members )
163+ List < string > requiredMembers = [ ] ;
164+ foreach ( MemberModel member in ctx . Model . Members )
165165 {
166166 if ( member . IsRequired )
167167 {
@@ -193,32 +193,72 @@ private static void WriteInstanceCreation(CloneGeneratorContext ctx, StringBuild
193193 /// <summary>
194194 /// Writes the clone body for records using the 'with' expression.
195195 /// Only includes members that need deep cloning in the 'with' expression.
196+ /// Getter-only collections are handled separately via population.
196197 /// </summary>
197198 /// <param name="sourceVarName">The name of the source variable (usually "source" for classes, "src" for structs after null check)</param>
198199 private static void WriteRecordCloneBody ( CloneGeneratorContext ctx , string typeName , string sourceVarName = "source" )
199200 {
200- var sb = ctx . Source ;
201+ StringBuilder sb = ctx . Source ;
201202
202- // Collect members that need deep cloning (not safe types)
203- var deepCloneAssignments = new List < string > ( ) ;
204- foreach ( var member in ctx . Model . Members )
203+ // Collect members that need deep cloning (not safe types) and can be assigned
204+ List < string > deepCloneAssignments = [ ] ;
205+ // Collect getter-only collections that need population
206+ List < MemberModel > getterOnlyCollections = [ ] ;
207+
208+ foreach ( MemberModel member in ctx . Model . Members )
205209 {
210+ // Check for getter-only collection properties
211+ if ( member is { IsProperty : true , HasGetter : true , HasSetter : false , IsInitOnly : false } )
212+ {
213+ // These need to be handled via population, not 'with' expression
214+ if ( member . TypeKind == MemberTypeKind . Collection || member . TypeKind == MemberTypeKind . Dictionary )
215+ {
216+ getterOnlyCollections . Add ( member ) ;
217+ }
218+ continue ;
219+ }
220+
206221 // Skip safe types - they're already shallow copied by 'with'
207222 if ( member . TypeKind == MemberTypeKind . Safe )
208223 continue ;
209224
210- // Skip read-only members
225+ // Skip read-only members (that aren't getter-only collections, which we handled above)
211226 if ( member . IsReadOnly )
212227 continue ;
213228
214- var assignment = MemberCloneGenerator . GetMemberAssignment ( ctx , member , sourceVarName , "null" , " " ) ;
229+ string assignment = MemberCloneGenerator . GetMemberAssignment ( ctx , member , sourceVarName , "null" , " " ) ;
215230 if ( ! string . IsNullOrEmpty ( assignment ) )
216231 {
217232 deepCloneAssignments . Add ( $ " { assignment } ") ;
218233 }
219234 }
220235
221- if ( deepCloneAssignments . Count == 0 )
236+ // If we have getter-only collections, we need to use statement-based approach
237+ if ( getterOnlyCollections . Count > 0 )
238+ {
239+ // Create result using 'with' expression
240+ if ( deepCloneAssignments . Count == 0 )
241+ {
242+ sb . AppendLine ( $ " var result = { sourceVarName } with {{ }};") ;
243+ }
244+ else
245+ {
246+ sb . AppendLine ( $ " var result = { sourceVarName } with") ;
247+ sb . AppendLine ( " {" ) ;
248+ sb . AppendLine ( string . Join ( ",\n " , deepCloneAssignments ) ) ;
249+ sb . AppendLine ( " };" ) ;
250+ }
251+
252+ // Populate getter-only collections
253+ foreach ( MemberModel member in getterOnlyCollections )
254+ {
255+ MemberCloneGenerator . WriteMemberCloning ( ctx , member , "result" , sourceVarName , "null" ) ;
256+ }
257+
258+ sb . AppendLine ( ) ;
259+ sb . AppendLine ( " return result;" ) ;
260+ }
261+ else if ( deepCloneAssignments . Count == 0 )
222262 {
223263 // All members are safe - simple shallow copy
224264 sb . AppendLine ( $ " return { sourceVarName } with {{ }};") ;
0 commit comments