@@ -85,21 +85,23 @@ internal readonly record struct MemberModel(
8585 bool HasSetter , // Whether the property has a setter (regular, not init-only)
8686 bool SetterIsAccessible , // Whether the setter is publicly accessible (not private/protected)
8787 MemberCloneBehavior MemberBehavior , // The clone behavior for this member (Clone, Reference, Shallow, Ignore)
88- bool ? PreserveIdentity = null // null=inherit from type, true=preserve identity for this member's subgraph, false=disabled
88+ bool ? PreserveIdentity = null , // null=inherit from type, true=preserve identity for this member's subgraph, false=disabled
89+ bool CollectionHasCount = true , // Whether the source collection type has Count property
90+ bool CollectionHasIndexer = true // Whether the source collection type supports [i] indexing
8991) : IEquatable < MemberModel >
9092{
9193 /// <summary>
9294 /// Returns true if the member should have its reference copied directly without deep cloning.
9395 /// This applies to both Shallow and Reference behaviors.
9496 /// </summary>
95- public bool ShouldCopyReference => MemberBehavior == MemberCloneBehavior . Shallow || MemberBehavior == MemberCloneBehavior . Reference ;
97+ private bool ShouldCopyReference => MemberBehavior is MemberCloneBehavior . Shallow or MemberCloneBehavior . Reference ;
9698
9799 // Legacy property for backward compatibility
98100 public bool IsShallowClone => ShouldCopyReference ;
99101
100102 public static MemberModel Create ( IPropertySymbol property , bool nullabilityEnabled , Compilation compilation , MemberCloneBehavior memberBehavior = MemberCloneBehavior . Clone )
101103 {
102- ( MemberTypeKind typeKind , string ? elementName , string ? keyName , string ? valueName , bool elementSafe , bool elementClonable , bool keySafe , bool keyClonable , bool valSafe , bool valClonable , bool requiresFastCloner , CollectionKind collectionKind , string ? concreteType , int arrayRank )
104+ ( MemberTypeKind typeKind , string ? elementName , string ? keyName , string ? valueName , bool elementSafe , bool elementClonable , bool keySafe , bool keyClonable , bool valSafe , bool valClonable , bool requiresFastCloner , CollectionKind collectionKind , string ? concreteType , int arrayRank , bool collHasCount , bool collHasIndexer )
103105 = AnalyzeType ( property . Type , compilation ) ;
104106
105107 // Check if the property has an init-only setter (C# 9+)
@@ -147,12 +149,14 @@ public static MemberModel Create(IPropertySymbol property, bool nullabilityEnabl
147149 hasSetter ,
148150 setterIsAccessible ,
149151 memberBehavior ,
150- preserveIdentity ) ;
152+ preserveIdentity ,
153+ collHasCount ,
154+ collHasIndexer ) ;
151155 }
152156
153157 public static MemberModel Create ( IFieldSymbol field , bool nullabilityEnabled , Compilation compilation , MemberCloneBehavior memberBehavior = MemberCloneBehavior . Clone )
154158 {
155- ( MemberTypeKind typeKind , string ? elementName , string ? keyName , string ? valueName , bool elementSafe , bool elementClonable , bool keySafe , bool keyClonable , bool valSafe , bool valClonable , bool requiresFastCloner , CollectionKind collectionKind , string ? concreteType , int arrayRank )
159+ ( MemberTypeKind typeKind , string ? elementName , string ? keyName , string ? valueName , bool elementSafe , bool elementClonable , bool keySafe , bool keyClonable , bool valSafe , bool valClonable , bool requiresFastCloner , CollectionKind collectionKind , string ? concreteType , int arrayRank , bool collHasCount , bool collHasIndexer )
156160 = AnalyzeType ( field . Type , compilation ) ;
157161
158162 // Check nullability
@@ -194,7 +198,9 @@ public static MemberModel Create(IFieldSymbol field, bool nullabilityEnabled, Co
194198 hasSetter ,
195199 setterIsAccessible ,
196200 memberBehavior ,
197- preserveIdentity ) ;
201+ preserveIdentity ,
202+ collHasCount ,
203+ collHasIndexer ) ;
198204 }
199205
200206 /// <summary>
@@ -209,7 +215,7 @@ public static MemberModel Create(IFieldSymbol field, bool nullabilityEnabled, Co
209215 // Check named argument first (Enabled = true/false)
210216 foreach ( KeyValuePair < string , TypedConstant > namedArg in attr . NamedArguments )
211217 {
212- if ( namedArg . Key == "Enabled" && namedArg . Value . Value is bool namedEnabled )
218+ if ( namedArg is { Key : "Enabled" , Value . Value : bool namedEnabled } )
213219 return namedEnabled ;
214220 }
215221
@@ -224,33 +230,33 @@ public static MemberModel Create(IFieldSymbol field, bool nullabilityEnabled, Co
224230 return null ; // No attribute found
225231 }
226232
227- private static ( MemberTypeKind kind , string ? elem , string ? key , string ? val , bool elemSafe , bool elemClon , bool keySafe , bool keyClon , bool valSafe , bool valClon , bool requiresFastCloner , CollectionKind collKind , string ? concreteType , int arrayRank )
233+ private static ( MemberTypeKind kind , string ? elem , string ? key , string ? val , bool elemSafe , bool elemClon , bool keySafe , bool keyClon , bool valSafe , bool valClon , bool requiresFastCloner , CollectionKind collKind , string ? concreteType , int arrayRank , bool collHasCount , bool collHasIndexer )
228234 AnalyzeType ( ITypeSymbol type , Compilation compilation )
229235 {
230236 // Check if safe type (primitives, strings, etc.)
231237 if ( TypeAnalyzer . IsSafeType ( type , compilation ) )
232- return ( MemberTypeKind . Safe , null , null , null , false , false , false , false , false , false , false , CollectionKind . None , null , 0 ) ;
238+ return ( MemberTypeKind . Safe , null , null , null , false , false , false , false , false , false , false , CollectionKind . None , null , 0 , true , true ) ;
233239
234240 // Check if this is a "do not clone" type (delegates, Lazy, Task, etc.)
235241 // These are treated as Safe to prevent deep cloning (shallow copy semantics)
236242 if ( TypeAnalyzer . IsDoNotCloneType ( type ) )
237- return ( MemberTypeKind . Safe , null , null , null , false , false , false , false , false , false , false , CollectionKind . None , null , 0 ) ;
243+ return ( MemberTypeKind . Safe , null , null , null , false , false , false , false , false , false , false , CollectionKind . None , null , 0 , true , true ) ;
238244
239245 // Check if this is a ref struct type (Span<T>, ReadOnlySpan<T>, etc.)
240246 // Ref structs cannot be boxed and cannot be used with state tracking dictionary.
241247 // Treat them as Safe to use shallow copy (which is the correct semantics for ref structs anyway).
242248 if ( TypeAnalyzer . IsRefStructType ( type ) )
243- return ( MemberTypeKind . Safe , null , null , null , false , false , false , false , false , false , false , CollectionKind . None , null , 0 ) ;
249+ return ( MemberTypeKind . Safe , null , null , null , false , false , false , false , false , false , false , CollectionKind . None , null , 0 , true , true ) ;
244250
245251 // Check if has clonable attribute
246252 if ( TypeAnalyzer . HasClonableAttribute ( type ) )
247- return ( MemberTypeKind . Clonable , null , null , null , false , false , false , false , false , false , false , CollectionKind . None , null , 0 ) ;
253+ return ( MemberTypeKind . Clonable , null , null , null , false , false , false , false , false , false , false , CollectionKind . None , null , 0 , true , true ) ;
248254
249255 // Check if System.Object or Type Parameter (generic T)
250256 // For generics, we don't know at compile time if it's clonable.
251257 // We generate a smart fallback that handles safe types at runtime.
252258 if ( type . SpecialType == SpecialType . System_Object || type . TypeKind == Microsoft . CodeAnalysis . TypeKind . TypeParameter )
253- return ( MemberTypeKind . Object , null , null , null , false , false , false , false , false , false , false , CollectionKind . None , null , 0 ) ;
259+ return ( MemberTypeKind . Object , null , null , null , false , false , false , false , false , false , false , CollectionKind . None , null , 0 , true , true ) ;
254260
255261 // IMPORTANT: Check array BEFORE collection (arrays implement ICollection<T>)
256262 if ( type is IArrayTypeSymbol arrayType )
@@ -265,12 +271,12 @@ private static (MemberTypeKind kind, string? elem, string? key, string? val, boo
265271 {
266272 // Multi-dimensional arrays: if element is not safe and not clonable, we need FastCloner to deep clone elements
267273 bool requiresFastCloner = ! elemSafe && ! elemClon ;
268- return ( MemberTypeKind . MultiDimArray , elemName , null , null , elemSafe , elemClon , false , false , false , false , requiresFastCloner , CollectionKind . None , null , rank ) ;
274+ return ( MemberTypeKind . MultiDimArray , elemName , null , null , elemSafe , elemClon , false , false , false , false , requiresFastCloner , CollectionKind . None , null , rank , true , true ) ;
269275 }
270276
271277 // Single-dimensional arrays: if element is not safe and not clonable, we need FastCloner to deep clone it
272278 bool requiresFastClonerSingle = ! elemSafe && ! elemClon ;
273- return ( MemberTypeKind . Array , elemName , null , null , elemSafe , elemClon , false , false , false , false , requiresFastClonerSingle , CollectionKind . None , null , 1 ) ;
279+ return ( MemberTypeKind . Array , elemName , null , null , elemSafe , elemClon , false , false , false , false , requiresFastClonerSingle , CollectionKind . None , null , 1 , true , true ) ;
274280 }
275281
276282 // Check dictionary BEFORE collection (dictionaries implement ICollection<KeyValuePair<K,V>>)
@@ -293,7 +299,7 @@ private static (MemberTypeKind kind, string? elem, string? key, string? val, boo
293299 CollectionKind collKind = TypeAnalyzer . GetCollectionKind ( type ) ;
294300 string concreteType = TypeAnalyzer . GetConcreteTypeForCollection ( type , collKind , $ "{ keyName } , { valName } ") ;
295301
296- return ( MemberTypeKind . Dictionary , null , keyName , valName , false , false , keySafe , keyClon , valSafe , valClon , requiresFastCloner , collKind , concreteType , 0 ) ;
302+ return ( MemberTypeKind . Dictionary , null , keyName , valName , false , false , keySafe , keyClon , valSafe , valClon , requiresFastCloner , collKind , concreteType , 0 , true , true ) ;
297303 }
298304 }
299305
@@ -310,18 +316,21 @@ private static (MemberTypeKind kind, string? elem, string? key, string? val, boo
310316 CollectionKind collKind = TypeAnalyzer . GetCollectionKind ( type ) ;
311317 string concreteType = TypeAnalyzer . GetConcreteTypeForCollection ( type , collKind , elemName ! ) ;
312318
313- return ( MemberTypeKind . Collection , elemName , null , null , elemSafe , elemClon , false , false , false , false , requiresFastCloner , collKind , concreteType , 0 ) ;
319+ bool collHasCount = TypeAnalyzer . CollectionHasCountProperty ( type ) ;
320+ bool collHasIndexer = TypeAnalyzer . CollectionHasIndexer ( type ) ;
321+
322+ return ( MemberTypeKind . Collection , elemName , null , null , elemSafe , elemClon , false , false , false , false , requiresFastCloner , collKind , concreteType , 0 , collHasCount , collHasIndexer ) ;
314323 }
315324
316325 // Check for implicit candidate (must be after collection)
317326 if ( TypeAnalyzer . IsImplicitCandidate ( type ) )
318327 {
319328 // It's a candidate for implicit cloning (generated recursively)
320- return ( MemberTypeKind . Implicit , null , null , null , false , false , false , false , false , false , false , CollectionKind . None , null , 0 ) ;
329+ return ( MemberTypeKind . Implicit , null , null , null , false , false , false , false , false , false , false , CollectionKind . None , null , 0 , true , true ) ;
321330 }
322331
323332 // Everything else - shallow copy fallback
324333 // If it's "Other", it's an unknown type. We definitely need FastCloner to deep clone it.
325- return ( MemberTypeKind . Other , null , null , null , false , false , false , false , false , false , true , CollectionKind . None , null , 0 ) ;
334+ return ( MemberTypeKind . Other , null , null , null , false , false , false , false , false , false , true , CollectionKind . None , null , 0 , true , true ) ;
326335 }
327336}
0 commit comments