33
44namespace FastCloner . SourceGenerator ;
55
6+ /// <summary>
7+ /// Represents the member-level clone behavior as determined by attributes.
8+ /// Mirrors FastCloner.Code.CloneBehavior enum values.
9+ /// </summary>
10+ internal enum MemberCloneBehavior
11+ {
12+ Clone = 0 , // Default: deep clone
13+ Reference = 1 , // Copy reference directly
14+ Shallow = 2 , // MemberwiseClone (treated same as Reference for members)
15+ Ignore = 3 // Skip, set to default
16+ }
17+
618internal record struct MemberAnalysis ( MemberModel Model , ITypeSymbol Type ) ;
719
820internal static class MemberCollector
@@ -46,10 +58,10 @@ public static List<MemberAnalysis> GetMembers(
4658 // Include property if it has an accessible setter OR it's a getter-only populatable collection
4759 if ( hasAccessibleSetter || isPopulatableCollection )
4860 {
49- if ( ! HasIgnoreAttribute ( property , compilation ) )
61+ MemberCloneBehavior behavior = GetMemberBehavior ( property , compilation ) ;
62+ if ( behavior != MemberCloneBehavior . Ignore )
5063 {
51- bool isShallow = HasShallowAttribute ( property , compilation ) ;
52- members . Add ( new MemberAnalysis ( MemberModel . Create ( property , nullabilityEnabled , compilation , isShallow ) , property . Type ) ) ;
64+ members . Add ( new MemberAnalysis ( MemberModel . Create ( property , nullabilityEnabled , compilation , behavior ) , property . Type ) ) ;
5365 }
5466 }
5567 }
@@ -58,10 +70,10 @@ public static List<MemberAnalysis> GetMembers(
5870 {
5971 if ( field . IsConst ) continue ; // Skip const fields
6072
61- if ( ! HasIgnoreAttribute ( field , compilation ) )
73+ MemberCloneBehavior behavior = GetMemberBehavior ( field , compilation ) ;
74+ if ( behavior != MemberCloneBehavior . Ignore )
6275 {
63- bool isShallow = HasShallowAttribute ( field , compilation ) ;
64- members . Add ( new MemberAnalysis ( MemberModel . Create ( field , nullabilityEnabled , compilation , isShallow ) , field . Type ) ) ;
76+ members . Add ( new MemberAnalysis ( MemberModel . Create ( field , nullabilityEnabled , compilation , behavior ) , field . Type ) ) ;
6577 }
6678 }
6779 }
@@ -138,46 +150,136 @@ private static bool IsPopulatableCollectionType(ITypeSymbol type)
138150 return false ;
139151 }
140152
141- private static bool HasIgnoreAttribute ( ISymbol member , Compilation compilation )
153+ /// <summary>
154+ /// Gets the clone behavior for a type by checking for FastClonerBehaviorAttribute on the type definition.
155+ /// </summary>
156+ private static MemberCloneBehavior ? GetTypeBehavior ( ITypeSymbol type , Compilation compilation )
142157 {
158+ INamedTypeSymbol ? behaviorAttribute = compilation . GetTypeByMetadataName ( "FastCloner.Code.FastClonerBehaviorAttribute" ) ;
143159 INamedTypeSymbol ? ignoreAttribute = compilation . GetTypeByMetadataName ( "FastCloner.Code.FastClonerIgnoreAttribute" ) ;
144- INamedTypeSymbol ? nonSerializedAttribute = compilation . GetTypeByMetadataName ( "System.NonSerializedAttribute" ) ;
160+ INamedTypeSymbol ? shallowAttribute = compilation . GetTypeByMetadataName ( "FastCloner.Code.FastClonerShallowAttribute" ) ;
161+ INamedTypeSymbol ? referenceAttribute = compilation . GetTypeByMetadataName ( "FastCloner.Code.FastClonerReferenceAttribute" ) ;
145162
146- foreach ( AttributeData ? attr in member . GetAttributes ( ) )
163+ foreach ( AttributeData attr in type . GetAttributes ( ) )
147164 {
148- if ( SymbolEqualityComparer . Default . Equals ( attr . AttributeClass , ignoreAttribute ) )
165+ INamedTypeSymbol ? attrClass = attr . AttributeClass ;
166+ if ( attrClass == null )
167+ continue ;
168+
169+ if ( ignoreAttribute != null && SymbolEqualityComparer . Default . Equals ( attrClass , ignoreAttribute ) )
149170 {
150- // Check if Ignored property is true (default is true)
151- if ( attr . ConstructorArguments . Length == 0 )
152- return true ;
153- if ( attr . ConstructorArguments . Length > 0 && attr . ConstructorArguments [ 0 ] . Value is bool ignored )
154- return ignored ;
155- return true ;
171+ return attr . ConstructorArguments . Length switch
172+ {
173+ 0 => MemberCloneBehavior . Ignore ,
174+ > 0 when attr . ConstructorArguments [ 0 ] . Value is bool ignored => ignored ? MemberCloneBehavior . Ignore : null ,
175+ _ => MemberCloneBehavior . Ignore
176+ } ;
177+ }
178+
179+ if ( shallowAttribute != null && SymbolEqualityComparer . Default . Equals ( attrClass , shallowAttribute ) )
180+ {
181+ return MemberCloneBehavior . Shallow ;
156182 }
157183
158- if ( SymbolEqualityComparer . Default . Equals ( attr . AttributeClass , nonSerializedAttribute ) )
184+ if ( referenceAttribute != null && SymbolEqualityComparer . Default . Equals ( attrClass , referenceAttribute ) )
159185 {
160- return true ;
186+ return MemberCloneBehavior . Reference ;
187+ }
188+
189+ if ( behaviorAttribute != null && SymbolEqualityComparer . Default . Equals ( attrClass , behaviorAttribute ) )
190+ {
191+ if ( attr . ConstructorArguments . Length > 0 && attr . ConstructorArguments [ 0 ] . Value is int behaviorInt )
192+ {
193+ return ( MemberCloneBehavior ) behaviorInt ;
194+ }
161195 }
162196 }
163197
164- return false ;
198+ return null ;
165199 }
166200
167- private static bool HasShallowAttribute ( ISymbol member , Compilation compilation )
201+ /// <summary>
202+ /// Gets the clone behavior for a member by checking:
203+ /// 1. Member-level FastClonerBehaviorAttribute (highest priority)
204+ /// 2. [NonSerialized] attribute (treat as Ignore)
205+ /// 3. Type-level FastClonerBehaviorAttribute on the member's type (lowest priority)
206+ /// </summary>
207+ private static MemberCloneBehavior GetMemberBehavior ( ISymbol member , ITypeSymbol memberType , Compilation compilation )
168208 {
209+ // Get all relevant attribute types
210+ INamedTypeSymbol ? behaviorAttribute = compilation . GetTypeByMetadataName ( "FastCloner.Code.FastClonerBehaviorAttribute" ) ;
211+ INamedTypeSymbol ? ignoreAttribute = compilation . GetTypeByMetadataName ( "FastCloner.Code.FastClonerIgnoreAttribute" ) ;
169212 INamedTypeSymbol ? shallowAttribute = compilation . GetTypeByMetadataName ( "FastCloner.Code.FastClonerShallowAttribute" ) ;
170- if ( shallowAttribute == null )
171- return false ;
213+ INamedTypeSymbol ? referenceAttribute = compilation . GetTypeByMetadataName ( "FastCloner.Code.FastClonerReferenceAttribute" ) ;
214+ INamedTypeSymbol ? nonSerializedAttribute = compilation . GetTypeByMetadataName ( "System.NonSerializedAttribute" ) ;
172215
173- foreach ( AttributeData ? attr in member . GetAttributes ( ) )
216+ // 1. Check for member-level attributes first (highest priority)
217+ foreach ( AttributeData attr in member . GetAttributes ( ) )
174218 {
175- if ( SymbolEqualityComparer . Default . Equals ( attr . AttributeClass , shallowAttribute ) )
219+ INamedTypeSymbol ? attrClass = attr . AttributeClass ;
220+ if ( attrClass == null )
221+ continue ;
222+
223+ // Check for specific derived attributes first (shorthand attributes)
224+ if ( ignoreAttribute != null && SymbolEqualityComparer . Default . Equals ( attrClass , ignoreAttribute ) )
176225 {
177- return true ;
226+ return attr . ConstructorArguments . Length switch
227+ {
228+ // Check if Ignored property is true (default is true)
229+ 0 => MemberCloneBehavior . Ignore ,
230+ > 0 when attr . ConstructorArguments [ 0 ] . Value is bool ignored => ignored ? MemberCloneBehavior . Ignore : MemberCloneBehavior . Clone ,
231+ _ => MemberCloneBehavior . Ignore
232+ } ;
233+ }
234+
235+ if ( shallowAttribute != null && SymbolEqualityComparer . Default . Equals ( attrClass , shallowAttribute ) )
236+ {
237+ return MemberCloneBehavior . Shallow ;
238+ }
239+
240+ if ( referenceAttribute != null && SymbolEqualityComparer . Default . Equals ( attrClass , referenceAttribute ) )
241+ {
242+ return MemberCloneBehavior . Reference ;
243+ }
244+
245+ // Check for base FastClonerBehaviorAttribute with explicit behavior parameter
246+ if ( behaviorAttribute != null && SymbolEqualityComparer . Default . Equals ( attrClass , behaviorAttribute ) )
247+ {
248+ if ( attr . ConstructorArguments . Length > 0 && attr . ConstructorArguments [ 0 ] . Value is int behaviorInt )
249+ {
250+ return ( MemberCloneBehavior ) behaviorInt ;
251+ }
252+ }
253+
254+ // Check for [NonSerialized] - treat as Ignore
255+ if ( nonSerializedAttribute != null && SymbolEqualityComparer . Default . Equals ( attrClass , nonSerializedAttribute ) )
256+ {
257+ return MemberCloneBehavior . Ignore ;
178258 }
179259 }
180260
181- return false ;
261+ // 2. Check for type-level attribute on the member's type
262+ MemberCloneBehavior ? typeBehavior = GetTypeBehavior ( memberType , compilation ) ;
263+
264+ return typeBehavior ?? MemberCloneBehavior . Clone ;
265+ }
266+
267+ /// <summary>
268+ /// Gets the clone behavior for a member (overload for backward compatibility).
269+ /// </summary>
270+ private static MemberCloneBehavior GetMemberBehavior ( ISymbol member , Compilation compilation )
271+ {
272+ ITypeSymbol ? memberType = member switch
273+ {
274+ IFieldSymbol f => f . Type ,
275+ IPropertySymbol p => p . Type ,
276+ IEventSymbol e => e . Type ,
277+ _ => null
278+ } ;
279+
280+ return memberType != null
281+ ? GetMemberBehavior ( member , memberType , compilation )
282+ : MemberCloneBehavior . Clone ;
182283 }
183284}
285+
0 commit comments