extended polymorphism, il weavers off-the-shelf support - #49
Conversation
There was a problem hiding this comment.
Pull request overview
This PR extends FastCloner’s reflection cloner and source generator to better handle (1) polymorphic cloning through non-abstract base references, (2) assembly-level “external ignore” attributes, and (3) IL-weaver injected runtime state (e.g., PostSharp/Metalama patterns) so cloning remains safe and consistent across both cloning modes.
Changes:
- Add
[FastClonerPolymorphic]support (including generic roots) with subtype dispatch and validation diagnostics. - Add
[assembly: FastClonerExternalIgnore(...)]support so framework ignore attributes can be honored without per-member FastCloner attributes. - Detect and special-case weaver-injected state fields and prefer weaver-declared
MemberwiseClone()when present; add regression tests and documentation.
Reviewed changes
Copilot reviewed 32 out of 32 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/FastCloner/Code/FastClonerWeaverState.cs | Detect weaver state fields and discover weaver-declared MemberwiseClone(). |
| src/FastCloner/Code/FastClonerExternalIgnoreRegistry.cs | Reflection-side cache/lookup for assembly-registered external ignore attributes. |
| src/FastCloner/Code/FastClonerExternalIgnoreAttribute.cs | Assembly-level attribute to register external ignore marker types. |
| src/FastCloner/Code/FastClonerExprGenerator.cs | Apply external ignore + weaver-state handling in reflection expression generation. |
| src/FastCloner/Code/FastClonerCache.cs | Extend TypeShape with HasWeaverStateFields. |
| src/FastCloner/Code/ClonerToExprGenerator.cs | Skip copying weaver state fields during DeepCloneTo deep member population. |
| src/FastCloner.Tests/PostSharpAspectTests.cs | Regression tests simulating PostSharp IL shapes (issue #48). |
| src/FastCloner.Tests/PolymorphicCloneTests.cs | End-to-end tests for polymorphic dispatch, including generic roots and fallback. |
| src/FastCloner.Tests/ExternalIgnoreAttributeTests.cs | Tests for [assembly: FastClonerExternalIgnore] behavior and precedence. |
| src/FastCloner.Tests/DiagnosticTests.cs | Generator diagnostics + generated-source structural assertions for polymorphic behavior. |
| src/FastCloner.Tests/AbstractClassTests.cs | Verify abstract root dispatch to derived clonable generators. |
| src/FastCloner.SourceGenerator/TypeModelFactory.cs | Thread external ignore registry; add polymorphic root modeling and derived collection. |
| src/FastCloner.SourceGenerator/TypeModel.cs | Add IsPolymorphicRoot flag to the model. |
| src/FastCloner.SourceGenerator/SubtypeUsageCollector.cs | Collect closed generic subtype constructions from usages for dispatch lists. |
| src/FastCloner.SourceGenerator/PolymorphicValidationInfo.cs | Add validation diagnostics for [FastClonerPolymorphic] misuse. |
| src/FastCloner.SourceGenerator/MemberModel.cs | Track weaver-state members and backing-field storage for safe population. |
| src/FastCloner.SourceGenerator/MemberCollector.cs | Apply external-ignore matching (by declaring assembly) during member collection. |
| src/FastCloner.SourceGenerator/MemberCloneGenerator.cs | Adapt member population for uninitialized instances + weaver state handling. |
| src/FastCloner.SourceGenerator/IncludeAttributeCollector.cs | Avoid treating included subtypes as generic-arg usages; thread external ignores. |
| src/FastCloner.SourceGenerator/ImplicitTypeAnalyzer.cs | Thread external ignore registry through implicit analysis. |
| src/FastCloner.SourceGenerator/GenericUsageCollector.cs | Thread external ignore registry through usage analysis. |
| src/FastCloner.SourceGenerator/GenericTypeAnalyzer.cs | Skip unbound type arguments; thread external ignores into implicit analysis. |
| src/FastCloner.SourceGenerator/FastClonerIncrementalGenerator.cs | Integrate external ignore registry + subtype usage pipeline + polymorphic validation. |
| src/FastCloner.SourceGenerator/ExternalIgnoreCollector.cs | Collect per-assembly external ignore registrations into an equatable registry. |
| src/FastCloner.SourceGenerator/DerivedTypeCollector.cs | Support generic roots + external ignore threading for derived type models. |
| src/FastCloner.SourceGenerator/ContextCollector.cs | Thread external ignore registry through context collection. |
| src/FastCloner.SourceGenerator/CloneGeneratorContext.cs | Ensure derived-type helper method names are unique (generic constructions). |
| src/FastCloner.SourceGenerator/CloneCodeGenerator.cs | Implement polymorphic root dispatch + subtype-usage-derived dispatch branches. |
| src/FastCloner.SourceGenerator/ClassCloneBodyGenerator.cs | Pass “created without ctor” flag to avoid invoking setters and handle weaver state. |
| src/FastCloner.SourceGenerator.Shared/FastClonerPolymorphicAttribute.cs | New attribute enabling polymorphic dispatch on concrete roots. |
| src/FastCloner.Benchmark/BenchPolymorphic.cs | Benchmark for polymorphic root overhead and dispatch cost. |
| README.md | Document external ignore registration and [FastClonerPolymorphic] usage and precedence. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| bool nullability = context.SemanticModel.GetNullableContext(node.SpanStart).HasFlag(NullableContext.Enabled); | ||
| TypeModel? model = DerivedTypeCollector.CreateTypeModelForDerived( | ||
| symbol, context.SemanticModel.Compilation, nullability, targetFramework); | ||
| if (model == null) |
| /// Attributes are matched by exact attribute type on presence only; constructors and properties | ||
| /// of the external attribute (e.g. <c>JsonIgnore(Condition = ...)</c>) are not evaluated. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 32 out of 32 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/FastCloner.SourceGenerator/MemberCollector.cs:57
- ResolveExternalIgnores only takes the first matching ExternalIgnoreAssemblyRegistration and breaks. Since FastClonerExternalIgnoreAttribute is AllowMultiple=true, multiple registrations on the same assembly will be ignored here, diverging from the reflection cloner (and from the attribute contract). Merge all matching registrations for the assembly instead of stopping at the first one.
foreach (ExternalIgnoreAssemblyRegistration registration in externalIgnores.Registrations)
{
if (registration.AssemblyName == displayName)
{
resolved = [.. registration.AttributeTypeFqns];
src/FastCloner.SourceGenerator/SubtypeUsageCollector.cs:58
- SubtypeUsageCollector.Collect computes FindAllDispatchRoots(symbol) twice; the second call can reuse the already-computed 'roots' list to avoid repeating the base-chain walk.
foreach (INamedTypeSymbol chainRoot in FindAllDispatchRoots(symbol))
{
usages.Add(new ClosedSubtypeUsage(
chainRoot.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
model));
Deep Clone Benchmarks
Current FastCloner vs DeepCloner
FastCloner vs latest
|
| Status | Benchmark | Delta Time | Delta Alloc |
|---|---|---|---|
| 🔴 | DynamicWithArray | +13% slower | ~same |
| 🔴 | DynamicWithDictionary | +5% slower | ~same |
| ⚪ | DynamicWithNestedObject | ~same | ~same |
| ⚪ | FileSpec | -4% faster | ~same |
| 🟢 | LargeEventDocument_10MB | -5% faster | ~same |
| ⚪ | LargeLogBatch_10MB | -3% faster | ~same |
| 🔴 | MediumNestedObject | +6% slower | ~same |
| ⚪ | ObjectDictionary_50 | ~same | ~same |
| ⚪ | ObjectList_100 | ~same | ~same |
| ⚪ | SmallObject | ~same | ~same |
| ⚪ | SmallObjectWithCollections | +3% slower | ~same |
| ⚪ | StringArray_1000 | -3% faster | ~same |
Regressions
DynamicWithArray: time +13% slower, alloc ~sameDynamicWithDictionary: time +5% slower, alloc ~sameMediumNestedObject: time +6% slower, alloc ~same
Improvements
LargeEventDocument_10MB: time -5% faster, alloc ~same
Mixed changes
- none
No description provided.