Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,24 @@ Animal pet = new Dog { Name = "Buddy", Breed = "Labrador" };
Animal clone = pet.FastDeepClone(); // Returns a cloned Dog
```

For non-abstract base classes, you can opt into the same runtime subtype dispatch behavior:

```cs
[FastClonerClonable(IncludeSubtypes = true)]
public class Device
{
public string Name { get; set; }
}

public class Phone : Device
{
public string OS { get; set; }
}
Comment thread
MizardX marked this conversation as resolved.
Comment thread
MizardX marked this conversation as resolved.

Device device = new Phone { Name = "Pixel", OS = "Android" };
Device clone = device.FastDeepClone(); // Returns a cloned Phone
```

### Explicitly Including Types

When a type is only used dynamically (not visible at compile time), use `[FastClonerInclude]` to ensure the generator creates cloning code for it:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,21 @@ namespace FastCloner.SourceGenerator.Shared;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class FastClonerClonableAttribute : Attribute
{
/// <summary>
/// Gets or sets whether subtype dispatch should be generated for this type.
/// When true, generated clone code dispatches by runtime type and clones known subtypes similarly to abstract roots.
/// This is ignored for abstract types, which always have subtype dispatch.
/// This is ignored for structs, which cannot have subtypes.
/// This is ignored for sealed classes, which cannot be subclassed.
Comment thread
MizardX marked this conversation as resolved.
/// </summary>
/// <example>
/// <code>
/// [FastClonerClonable(IncludeSubtypes = true)]
/// public class BaseType { }
/// </code>
/// </example>
public bool IncludeSubtypes { get; set; }

/// <summary>
/// Initializes a new instance of the FastClonerClonableAttribute.
/// </summary>
Expand Down
50 changes: 37 additions & 13 deletions src/FastCloner.SourceGenerator/CloneCodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,9 @@ private void WritePublicFastDeepCloneMethod(string typeName, string fullTypeName
return;
}

if (_context.Model.IsAbstract)
bool needsSubtypeDispatcher = !_context.Model.IsStruct && (_context.Model.IsAbstract || _context.Model.IncludeSubtypes);
Comment thread
MizardX marked this conversation as resolved.
Comment thread
MizardX marked this conversation as resolved.

if (needsSubtypeDispatcher)
{
sb.AppendLine(" return InternalFastDeepClone(source, null);");
}
Expand Down Expand Up @@ -274,11 +276,19 @@ private void WritePrivateFastDeepCloneMethod(string typeName, string fullTypeNam
sb.AppendLine(" if (source == null) return null;");
}

if (_context.Model.IsAbstract)
bool needsSubtypeDispatcher = !_context.Model.IsStruct && (_context.Model.IsAbstract || _context.Model.IncludeSubtypes);
Comment thread
MizardX marked this conversation as resolved.
if (needsSubtypeDispatcher)
Comment thread
MizardX marked this conversation as resolved.
{
WriteAbstractTypeDispatcher(typeName);
WriteSubtypeDispatcher(typeName, fullTypeName, includeRootTypeGuard: !_context.Model.IsAbstract);
if (_context.Model.IsAbstract)
{
sb.AppendLine(" }");
sb.AppendLine();
return;
}
}
else if (_context.NeedsStateTracking)

if (_context.NeedsStateTracking)
{
_context.NeedsStateClass = true;
sb.AppendLine(" var localState = state ?? new FcGeneratedCloneState();");
Expand Down Expand Up @@ -309,7 +319,7 @@ private void WritePrivateFastDeepCloneMethod(string typeName, string fullTypeNam
sb.AppendLine();
}

private void WriteAbstractTypeDispatcher(string typeName)
private void WriteSubtypeDispatcher(string typeName, string fullTypeName, bool includeRootTypeGuard)
{
StringBuilder sb = _context.Source;
EquatableArray<TypeModel> derivedTypes = _context.Model.DerivedTypes;
Expand All @@ -319,6 +329,14 @@ private void WriteAbstractTypeDispatcher(string typeName)
sb.AppendLine(" var runtimeType = source.GetType();");
sb.AppendLine();

string indent = " ";
if (includeRootTypeGuard)
{
sb.AppendLine($" if (runtimeType != typeof({fullTypeName}))");
sb.AppendLine(" {");
indent = " ";
}

foreach (TypeModel? derivedType in derivedTypes)
{
string derivedTypeName = derivedType.FullyQualifiedName;
Expand All @@ -331,29 +349,35 @@ private void WriteAbstractTypeDispatcher(string typeName)
extensionClassName = $"{derivedType.Name}FastDeepCloneExtensions";
}

sb.AppendLine($" if (runtimeType == typeof({derivedTypeName}))");
sb.AppendLine($" return ({typeName}){extensionClassName}.InternalFastDeepClone(({derivedTypeName})source, state);");
sb.AppendLine($"{indent}if (runtimeType == typeof({derivedTypeName}))");
sb.AppendLine($"{indent} return ({typeName}){extensionClassName}.InternalFastDeepClone(({derivedTypeName})source, state);");
}
else
{
string helperName = $"Clone{GetSafeTypeName(derivedType.Name)}";
_context.RegisterDerivedTypeHelper(derivedType, helperName);

sb.AppendLine($" if (runtimeType == typeof({derivedTypeName}))");
sb.AppendLine($" return ({typeName}){helperName}(({derivedTypeName})source, state);");
sb.AppendLine($"{indent}if (runtimeType == typeof({derivedTypeName}))");
sb.AppendLine($"{indent} return ({typeName}){helperName}(({derivedTypeName})source, state);");
}
sb.AppendLine();
}

if (_context.IsFastClonerAvailable)
{
sb.AppendLine($" return ({typeName}){CloneGeneratorContext.FastClonerDeepCloneCall("source")}!;");
sb.AppendLine($"{indent}return ({typeName}){CloneGeneratorContext.FastClonerDeepCloneCall("source")}!;");
}
else
{
sb.AppendLine($" throw new InvalidOperationException($\"Cannot clone unknown derived type {{runtimeType.FullName}} of {_context.Model.Name}. \" +");
sb.AppendLine(" \"Either add the derived type to this assembly, use [FastClonerInclude] to register it, \" +");
sb.AppendLine(" \"or install the FastCloner NuGet package for runtime fallback.\");");
sb.AppendLine($"{indent}throw new InvalidOperationException($\"Cannot clone unknown derived type {{runtimeType.FullName}} of {_context.Model.Name}. \" +");
sb.AppendLine($"{indent} \"Either add the derived type to this assembly, use [FastClonerInclude] to register it, \" +");
sb.AppendLine($"{indent} \"or install the FastCloner NuGet package for runtime fallback.\");");
}

if (includeRootTypeGuard)
{
sb.AppendLine(" }");
sb.AppendLine();
}
}

Expand Down
10 changes: 5 additions & 5 deletions src/FastCloner.SourceGenerator/DerivedTypeCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,11 @@ private static TypeModel CreateMinimalTypeModel(
RelatedTypes: EquatableArray<TypeModel>.Empty,
NestedTypes: EquatableArray<MemberModel>.Empty,
DerivedTypes: EquatableArray<TypeModel>.Empty,
nullabilityEnabled,
trustNullability,
NullabilityEnabled: nullabilityEnabled,
TrustNullability: trustNullability,
PreserveIdentity: null,
IsRefLikeType: false,
hasParameterlessConstructor,
HasParameterlessConstructor: hasParameterlessConstructor,
CodeAnalysisAvailable: compilation.GetTypeByMetadataName("System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute") != null,
TargetFramework: targetFramework);
}
Expand Down Expand Up @@ -332,10 +332,10 @@ private static TypeModel CreateMinimalTypeModel(
trustNullability,
preserveIdentity,
IsRefLikeType: false,
hasParameterlessConstructor,
HasParameterlessConstructor: hasParameterlessConstructor,
CodeAnalysisAvailable: compilation.GetTypeByMetadataName("System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute") != null,
TargetFramework: targetFramework,
new EquatableArray<string>(circRefLog.ToArray()));
CircularAnalysisLog: new EquatableArray<string>(circRefLog.ToArray()));
}

private static bool? GetPreserveIdentityFromType(INamedTypeSymbol symbol)
Expand Down
2 changes: 1 addition & 1 deletion src/FastCloner.SourceGenerator/ImplicitTypeAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ bool TryHandleComponent(ITypeSymbol componentType, out MemberModel? componentMem
trustNullability,
PreserveIdentity: null,
IsRefLikeType: false,
hasParameterlessConstructor,
HasParameterlessConstructor: hasParameterlessConstructor,
CodeAnalysisAvailable: compilation.GetTypeByMetadataName("System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute") != null,
TargetFramework: targetFramework);

Expand Down
1 change: 1 addition & 0 deletions src/FastCloner.SourceGenerator/TypeModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@ internal sealed record TypeModel(
bool IsRefLikeType = false, // Whether the type is a ref struct (cannot be boxed/used as generic)
bool HasParameterlessConstructor = true, // Whether the type has a public parameterless constructor (defaults to true for safety)
bool CodeAnalysisAvailable = false, // Whether System.Diagnostics.CodeAnalysis attributes are available
bool IncludeSubtypes = false, // Whether subtype dispatch should be generated for this type
TargetFramework TargetFramework = TargetFramework.NetStandard20, // Detected target framework for TFM-specific optimizations
EquatableArray<string> CircularAnalysisLog = default) : IEquatable<TypeModel>;
26 changes: 24 additions & 2 deletions src/FastCloner.SourceGenerator/TypeModelFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public static bool TryCreate(
.Any(a => a.AttributeClass?.ToDisplayString() == "FastCloner.SourceGenerator.Shared.FastClonerSimulateNoRuntimeAttribute");
bool trustNullability = symbol.GetAttributes()
.Any(a => a.AttributeClass?.ToDisplayString() == "FastCloner.SourceGenerator.Shared.FastClonerTrustNullabilityAttribute");
bool requestedIncludeSubtypes = GetIncludeSubtypesFromType(symbol);
bool effectiveIncludeSubtypes = requestedIncludeSubtypes && !symbol.IsValueType && !symbol.IsSealed;
bool? preserveIdentity = GetPreserveIdentityFromType(symbol);
bool codeAnalysisAvailable = compilation.GetTypeByMetadataName("System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute") != null;

Expand Down Expand Up @@ -211,12 +213,12 @@ public static bool TryCreate(
bool isRefLikeType = TypeAnalyzer.IsRefStructType(symbol);
EquatableArray<TypeModel> derivedTypes = EquatableArray<TypeModel>.Empty;

if (symbol.IsAbstract)
if (symbol.IsAbstract || effectiveIncludeSubtypes)
{
List<TypeModel> derivedTypesList = DerivedTypeCollector.Collect(symbol, compilation, nullabilityEnabled, targetFramework);
derivedTypes = new EquatableArray<TypeModel>(derivedTypesList.ToArray());

if (derivedTypesList.Count == 0 && !isFastClonerAvailable)
if (symbol.IsAbstract && derivedTypesList.Count == 0 && !isFastClonerAvailable)
{
error = Diagnostic.Create(
new DiagnosticDescriptor(
Expand Down Expand Up @@ -257,6 +259,7 @@ public static bool TryCreate(
isRefLikeType,
hasParameterlessConstructor,
codeAnalysisAvailable,
effectiveIncludeSubtypes,
targetFramework,
new EquatableArray<string>(circRefLog.ToArray()));

Expand All @@ -283,4 +286,23 @@ public static bool TryCreate(
}
return null;
}

private static bool GetIncludeSubtypesFromType(INamedTypeSymbol symbol)
{
foreach (AttributeData attr in symbol.GetAttributes())
{
if (attr.AttributeClass?.ToDisplayString() != "FastCloner.SourceGenerator.Shared.FastClonerClonableAttribute")
continue;

foreach (KeyValuePair<string, TypedConstant> namedArg in attr.NamedArguments)
{
if (namedArg is { Key: "IncludeSubtypes", Value.Value: bool includeSubtypes })
return includeSubtypes;
}

return false;
}

return false;
}
}
Loading
Loading