From a9d8baa8b22a5f0741c8c376365636bffa1a9cce Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 27 Jan 2026 05:51:53 +0000
Subject: [PATCH 01/14] Initial plan
From 17da0ad867912bd0233feebd3cc39d19863c1687 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 27 Jan 2026 05:58:50 +0000
Subject: [PATCH 02/14] Implement PrototypeGenerator.cs with full diagnostic
support and cloning strategies
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
.../Prototype/PrototypeAttribute.cs | 127 +++
.../AnalyzerReleases.Unshipped.md | 6 +
.../PrototypeGenerator.cs | 793 ++++++++++++++++++
3 files changed, 926 insertions(+)
create mode 100644 src/PatternKit.Generators.Abstractions/Prototype/PrototypeAttribute.cs
create mode 100644 src/PatternKit.Generators/PrototypeGenerator.cs
diff --git a/src/PatternKit.Generators.Abstractions/Prototype/PrototypeAttribute.cs b/src/PatternKit.Generators.Abstractions/Prototype/PrototypeAttribute.cs
new file mode 100644
index 0000000..1e7658f
--- /dev/null
+++ b/src/PatternKit.Generators.Abstractions/Prototype/PrototypeAttribute.cs
@@ -0,0 +1,127 @@
+namespace PatternKit.Generators.Prototype;
+
+///
+/// Marks a type (class/struct/record class/record struct) for Prototype pattern code generation.
+/// Generates a Clone() method with configurable cloning strategies for safe, deterministic object duplication.
+///
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
+public sealed class PrototypeAttribute : Attribute
+{
+ ///
+ /// The cloning mode that determines default behavior for reference types.
+ /// Default is ShallowWithWarnings (safe-by-default with diagnostics for mutable references).
+ ///
+ public PrototypeMode Mode { get; set; } = PrototypeMode.ShallowWithWarnings;
+
+ ///
+ /// The name of the generated clone method.
+ /// Default is "Clone".
+ ///
+ public string CloneMethodName { get; set; } = "Clone";
+
+ ///
+ /// When true, only members explicitly marked with [PrototypeInclude] will be cloned.
+ /// When false (default), all eligible members are cloned unless marked with [PrototypeIgnore].
+ ///
+ public bool IncludeExplicit { get; set; }
+}
+
+///
+/// Determines the default cloning behavior for reference types.
+///
+public enum PrototypeMode
+{
+ ///
+ /// Shallow clone with warnings for mutable reference types.
+ /// This is the safe-by-default mode that alerts developers to potential aliasing issues.
+ ///
+ ShallowWithWarnings = 0,
+
+ ///
+ /// Shallow clone without warnings.
+ /// Use when you explicitly want shallow cloning and understand the implications.
+ ///
+ Shallow = 1,
+
+ ///
+ /// Deep clone when deterministically possible.
+ /// Attempts to deep clone collections and types with known clone mechanisms.
+ /// Falls back to shallow for types without clone support.
+ ///
+ DeepWhenPossible = 2
+}
+
+///
+/// Marks a member to be excluded from the clone operation.
+/// Only applies when IncludeExplicit is false (default mode).
+///
+[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
+public sealed class PrototypeIgnoreAttribute : Attribute
+{
+}
+
+///
+/// Marks a member to be explicitly included in the clone operation.
+/// Only applies when IncludeExplicit is true.
+///
+[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
+public sealed class PrototypeIncludeAttribute : Attribute
+{
+}
+
+///
+/// Specifies the cloning strategy for a specific member.
+/// Overrides the default strategy determined by the PrototypeMode.
+///
+[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
+public sealed class PrototypeStrategyAttribute : Attribute
+{
+ public PrototypeCloneStrategy Strategy { get; }
+
+ public PrototypeStrategyAttribute(PrototypeCloneStrategy strategy)
+ {
+ Strategy = strategy;
+ }
+}
+
+///
+/// Defines how a member's value is cloned.
+///
+public enum PrototypeCloneStrategy
+{
+ ///
+ /// Copy the reference as-is (shallow copy).
+ /// Safe for immutable types and value types.
+ /// WARNING: For mutable reference types, mutations will affect both the original and clone.
+ ///
+ ByReference = 0,
+
+ ///
+ /// Perform a shallow copy of the member.
+ /// For collections, creates a new collection with the same element references.
+ ///
+ ShallowCopy = 1,
+
+ ///
+ /// Clone the value using a known mechanism:
+ /// - ICloneable.Clone()
+ /// - Clone() method returning same type
+ /// - Copy constructor T(T other)
+ /// - For collections like List<T>, creates new collection: new List<T>(original)
+ /// Generator emits an error if no suitable clone mechanism is available.
+ ///
+ Clone = 2,
+
+ ///
+ /// Perform a deep copy of the member value.
+ /// Only available when the generator can safely emit deep copy logic.
+ /// For complex types, requires Clone strategy on nested members.
+ ///
+ DeepCopy = 3,
+
+ ///
+ /// Use a custom clone mechanism provided by the user.
+ /// Requires a partial method: private static partial TMember Clone{MemberName}(TMember value);
+ ///
+ Custom = 4
+}
diff --git a/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md b/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
index 78073c8..c8131a7 100644
--- a/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
+++ b/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
@@ -52,4 +52,10 @@ PKDEC003 | PatternKit.Generators.Decorator | Error | Name conflict for generated
PKDEC004 | PatternKit.Generators.Decorator | Warning | Member is not accessible for decorator generation
PKDEC005 | PatternKit.Generators.Decorator | Error | Generic contracts are not supported for decorator generation
PKDEC006 | PatternKit.Generators.Decorator | Error | Nested types are not supported for decorator generation
+PKPRO001 | PatternKit.Generators.Prototype | Error | Type marked with [Prototype] must be partial
+PKPRO002 | PatternKit.Generators.Prototype | Error | Cannot construct clone target (no supported clone construction path)
+PKPRO003 | PatternKit.Generators.Prototype | Warning | Unsafe reference capture (mutable reference types)
+PKPRO004 | PatternKit.Generators.Prototype | Error | Requested Clone strategy but no clone mechanism found
+PKPRO005 | PatternKit.Generators.Prototype | Error | Custom strategy requires partial clone hook, but none found
+PKPRO006 | PatternKit.Generators.Prototype | Warning | Include/Ignore attribute misuse
diff --git a/src/PatternKit.Generators/PrototypeGenerator.cs b/src/PatternKit.Generators/PrototypeGenerator.cs
new file mode 100644
index 0000000..208ab82
--- /dev/null
+++ b/src/PatternKit.Generators/PrototypeGenerator.cs
@@ -0,0 +1,793 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using System.Text;
+using System.Linq;
+using System.Collections.Generic;
+
+namespace PatternKit.Generators;
+
+///
+/// Source generator for the Prototype pattern.
+/// Generates Clone methods with configurable cloning strategies for safe object duplication.
+///
+[Generator]
+public sealed class PrototypeGenerator : IIncrementalGenerator
+{
+ // Diagnostic IDs
+ private const string DiagIdTypeNotPartial = "PKPRO001";
+ private const string DiagIdNoConstructionPath = "PKPRO002";
+ private const string DiagIdUnsafeReferenceCapture = "PKPRO003";
+ private const string DiagIdCloneMechanismMissing = "PKPRO004";
+ private const string DiagIdCustomStrategyMissing = "PKPRO005";
+ private const string DiagIdAttributeMisuse = "PKPRO006";
+
+ private static readonly DiagnosticDescriptor TypeNotPartialDescriptor = new(
+ id: DiagIdTypeNotPartial,
+ title: "Type marked with [Prototype] must be partial",
+ messageFormat: "Type '{0}' is marked with [Prototype] but is not declared as partial. Add the 'partial' keyword to the type declaration.",
+ category: "PatternKit.Generators.Prototype",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor NoConstructionPathDescriptor = new(
+ id: DiagIdNoConstructionPath,
+ title: "Cannot construct clone target",
+ messageFormat: "Cannot construct clone for type '{0}'. No supported clone construction path found (no parameterless constructor, copy constructor, or record with-expression support).",
+ category: "PatternKit.Generators.Prototype",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor UnsafeReferenceCaptureDescriptor = new(
+ id: DiagIdUnsafeReferenceCapture,
+ title: "Unsafe reference capture",
+ messageFormat: "Member '{0}' is a mutable reference type copied by reference. Mutations will affect both the original and clone. Consider using [PrototypeStrategy(Clone)] or [PrototypeStrategy(ShallowCopy)].",
+ category: "PatternKit.Generators.Prototype",
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor CloneMechanismMissingDescriptor = new(
+ id: DiagIdCloneMechanismMissing,
+ title: "Requested Clone strategy but no clone mechanism found",
+ messageFormat: "Member '{0}' has [PrototypeStrategy(Clone)] but no suitable clone mechanism is available. Implement ICloneable, provide a Clone() method, copy constructor, or use a collection type with copy constructor support.",
+ category: "PatternKit.Generators.Prototype",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor CustomStrategyMissingDescriptor = new(
+ id: DiagIdCustomStrategyMissing,
+ title: "Custom strategy requires partial clone hook, but none found",
+ messageFormat: "Member '{0}' has [PrototypeStrategy(Custom)] but no partial method 'private static partial {1} Clone{0}({1} value)' was found. Declare this method in your partial type.",
+ category: "PatternKit.Generators.Prototype",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor AttributeMisuseDescriptor = new(
+ id: DiagIdAttributeMisuse,
+ title: "Include/Ignore attribute misuse",
+ messageFormat: "{0}",
+ category: "PatternKit.Generators.Prototype",
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ // Find all type declarations with [Prototype] attribute
+ var prototypeTypes = context.SyntaxProvider.ForAttributeWithMetadataName(
+ fullyQualifiedMetadataName: "PatternKit.Generators.Prototype.PrototypeAttribute",
+ predicate: static (node, _) => node is TypeDeclarationSyntax,
+ transform: static (ctx, _) => ctx
+ );
+
+ // Generate for each type
+ context.RegisterSourceOutput(prototypeTypes, (spc, typeContext) =>
+ {
+ if (typeContext.TargetSymbol is not INamedTypeSymbol typeSymbol)
+ return;
+
+ var attr = typeContext.Attributes.FirstOrDefault(a =>
+ a.AttributeClass?.ToDisplayString() == "PatternKit.Generators.Prototype.PrototypeAttribute");
+ if (attr is null)
+ return;
+
+ GeneratePrototypeForType(spc, typeSymbol, attr, typeContext.TargetNode);
+ });
+ }
+
+ private void GeneratePrototypeForType(
+ SourceProductionContext context,
+ INamedTypeSymbol typeSymbol,
+ AttributeData attribute,
+ SyntaxNode node)
+ {
+ // Check if type is partial
+ if (!IsPartialType(node))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ TypeNotPartialDescriptor,
+ node.GetLocation(),
+ typeSymbol.Name));
+ return;
+ }
+
+ // Parse attribute arguments
+ var config = ParsePrototypeConfig(attribute);
+
+ // Analyze type and members
+ var typeInfo = AnalyzeType(typeSymbol, config, context);
+ if (typeInfo is null)
+ return;
+
+ // Generate clone method
+ var cloneSource = GenerateCloneMethod(typeInfo, config, context);
+ if (!string.IsNullOrEmpty(cloneSource))
+ {
+ var fileName = $"{typeSymbol.Name}.Prototype.g.cs";
+ context.AddSource(fileName, cloneSource);
+ }
+ }
+
+ private static bool IsPartialType(SyntaxNode node)
+ {
+ return node switch
+ {
+ ClassDeclarationSyntax classDecl => classDecl.Modifiers.Any(SyntaxKind.PartialKeyword),
+ StructDeclarationSyntax structDecl => structDecl.Modifiers.Any(SyntaxKind.PartialKeyword),
+ RecordDeclarationSyntax recordDecl => recordDecl.Modifiers.Any(SyntaxKind.PartialKeyword),
+ _ => false
+ };
+ }
+
+ private PrototypeConfig ParsePrototypeConfig(AttributeData attribute)
+ {
+ var config = new PrototypeConfig();
+
+ foreach (var named in attribute.NamedArguments)
+ {
+ switch (named.Key)
+ {
+ case "Mode":
+ config.Mode = (int)named.Value.Value!;
+ break;
+ case "CloneMethodName":
+ config.CloneMethodName = (string)named.Value.Value!;
+ break;
+ case "IncludeExplicit":
+ config.IncludeExplicit = (bool)named.Value.Value!;
+ break;
+ }
+ }
+
+ return config;
+ }
+
+ private TypeInfo? AnalyzeType(
+ INamedTypeSymbol typeSymbol,
+ PrototypeConfig config,
+ SourceProductionContext context)
+ {
+ var typeInfo = new TypeInfo
+ {
+ TypeSymbol = typeSymbol,
+ TypeName = typeSymbol.Name,
+ Namespace = typeSymbol.ContainingNamespace.IsGlobalNamespace
+ ? string.Empty
+ : typeSymbol.ContainingNamespace.ToDisplayString(),
+ IsClass = typeSymbol.TypeKind == TypeKind.Class && !typeSymbol.IsRecord,
+ IsStruct = typeSymbol.TypeKind == TypeKind.Struct && !typeSymbol.IsRecord,
+ IsRecordClass = typeSymbol.TypeKind == TypeKind.Class && typeSymbol.IsRecord,
+ IsRecordStruct = typeSymbol.TypeKind == TypeKind.Struct && typeSymbol.IsRecord,
+ Members = new List()
+ };
+
+ // Collect members based on inclusion mode
+ var members = GetMembersForClone(typeSymbol, config, context);
+ typeInfo.Members.AddRange(members);
+
+ // Determine construction strategy
+ typeInfo.ConstructionStrategy = DetermineConstructionStrategy(typeSymbol, typeInfo);
+ if (typeInfo.ConstructionStrategy == ConstructionStrategy.None)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ NoConstructionPathDescriptor,
+ typeSymbol.Locations.FirstOrDefault(),
+ typeSymbol.Name));
+ return null;
+ }
+
+ return typeInfo;
+ }
+
+ private ConstructionStrategy DetermineConstructionStrategy(INamedTypeSymbol typeSymbol, TypeInfo typeInfo)
+ {
+ // For records, prefer with-expression if all members are init/readonly
+ if (typeInfo.IsRecordClass || typeInfo.IsRecordStruct)
+ {
+ bool allInit = typeInfo.Members.All(m => m.IsInitOnly || m.IsReadOnly);
+ if (allInit)
+ return ConstructionStrategy.RecordWith;
+
+ // Otherwise try copy constructor or parameterless
+ if (HasCopyConstructor(typeSymbol))
+ return ConstructionStrategy.CopyConstructor;
+
+ if (HasParameterlessConstructor(typeSymbol))
+ return ConstructionStrategy.ParameterlessConstructor;
+
+ return ConstructionStrategy.RecordWith; // Fall back to with-expression
+ }
+
+ // For classes/structs, try copy constructor first, then parameterless
+ if (HasCopyConstructor(typeSymbol))
+ return ConstructionStrategy.CopyConstructor;
+
+ if (HasParameterlessConstructor(typeSymbol))
+ return ConstructionStrategy.ParameterlessConstructor;
+
+ return ConstructionStrategy.None;
+ }
+
+ private bool HasCopyConstructor(INamedTypeSymbol typeSymbol)
+ {
+ return typeSymbol.Constructors.Any(c =>
+ c.Parameters.Length == 1 &&
+ SymbolEqualityComparer.Default.Equals(c.Parameters[0].Type, typeSymbol));
+ }
+
+ private bool HasParameterlessConstructor(INamedTypeSymbol typeSymbol)
+ {
+ // Structs always have an implicit parameterless constructor
+ if (typeSymbol.TypeKind == TypeKind.Struct)
+ return true;
+
+ return typeSymbol.Constructors.Any(c =>
+ c.Parameters.Length == 0 &&
+ c.DeclaredAccessibility >= Accessibility.Internal);
+ }
+
+ private List GetMembersForClone(
+ INamedTypeSymbol typeSymbol,
+ PrototypeConfig config,
+ SourceProductionContext context)
+ {
+ var members = new List();
+ var includeAll = !config.IncludeExplicit;
+
+ // Get all instance properties and fields
+ var candidateMembers = typeSymbol.GetMembers()
+ .Where(m => (m is IPropertySymbol || m is IFieldSymbol) &&
+ !m.IsStatic &&
+ m.DeclaredAccessibility == Accessibility.Public);
+
+ foreach (var member in candidateMembers)
+ {
+ // Check for attributes
+ var hasIgnore = HasAttribute(member, "PatternKit.Generators.Prototype.PrototypeIgnoreAttribute");
+ var hasInclude = HasAttribute(member, "PatternKit.Generators.Prototype.PrototypeIncludeAttribute");
+ var strategyAttr = GetAttribute(member, "PatternKit.Generators.Prototype.PrototypeStrategyAttribute");
+
+ // Check for attribute misuse
+ if (includeAll && hasInclude)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ AttributeMisuseDescriptor,
+ member.Locations.FirstOrDefault(),
+ $"[PrototypeInclude] on member '{member.Name}' has no effect when IncludeExplicit is false. Remove the attribute or set IncludeExplicit=true."));
+ }
+ else if (!includeAll && hasIgnore)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ AttributeMisuseDescriptor,
+ member.Locations.FirstOrDefault(),
+ $"[PrototypeIgnore] on member '{member.Name}' has no effect when IncludeExplicit is true. Remove the attribute or set IncludeExplicit=false."));
+ }
+
+ // Determine if this member should be included
+ bool shouldInclude = includeAll ? !hasIgnore : hasInclude;
+ if (!shouldInclude)
+ continue;
+
+ // Extract member type and characteristics
+ ITypeSymbol? memberType = null;
+ bool isReadOnly = false;
+ bool isInitOnly = false;
+
+ if (member is IPropertySymbol prop)
+ {
+ // Must have a getter
+ if (prop.GetMethod is null || prop.GetMethod.DeclaredAccessibility != Accessibility.Public)
+ continue;
+
+ memberType = prop.Type;
+ isReadOnly = prop.SetMethod is null;
+ isInitOnly = prop.SetMethod?.IsInitOnly ?? false;
+ }
+ else if (member is IFieldSymbol fld)
+ {
+ memberType = fld.Type;
+ isReadOnly = fld.IsReadOnly;
+ }
+
+ if (memberType is null)
+ continue;
+
+ // Determine clone strategy
+ var strategy = DetermineCloneStrategy(member, memberType, strategyAttr, config, context);
+ if (strategy is null)
+ continue; // Error already reported
+
+ members.Add(new MemberInfo
+ {
+ Name = member.Name,
+ Type = memberType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
+ TypeSymbol = memberType,
+ IsProperty = member is IPropertySymbol,
+ IsField = member is IFieldSymbol,
+ IsReadOnly = isReadOnly,
+ IsInitOnly = isInitOnly,
+ CloneStrategy = strategy.Value,
+ Symbol = member
+ });
+ }
+
+ // Sort by member name for deterministic output
+ members.Sort((a, b) => string.CompareOrdinal(a.Name, b.Name));
+
+ return members;
+ }
+
+ private CloneStrategy? DetermineCloneStrategy(
+ ISymbol member,
+ ITypeSymbol memberType,
+ AttributeData? strategyAttr,
+ PrototypeConfig config,
+ SourceProductionContext context)
+ {
+ // If explicit strategy provided, validate and use it
+ if (strategyAttr is not null)
+ {
+ var ctorArg = strategyAttr.ConstructorArguments.FirstOrDefault();
+ if (ctorArg.Value is int strategyValue)
+ {
+ var strategy = (CloneStrategy)strategyValue;
+
+ // Validate strategy
+ if (strategy == CloneStrategy.Clone)
+ {
+ if (!HasCloneMechanism(memberType))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ CloneMechanismMissingDescriptor,
+ member.Locations.FirstOrDefault(),
+ member.Name));
+ return null;
+ }
+ }
+ else if (strategy == CloneStrategy.Custom)
+ {
+ // Check for custom partial method
+ var containingType = member.ContainingType;
+ var methodName = $"Clone{member.Name}";
+ var hasCustomMethod = containingType.GetMembers(methodName)
+ .OfType()
+ .Any(m => m.IsStatic && m.IsPartialDefinition && m.Parameters.Length == 1);
+
+ if (!hasCustomMethod)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ CustomStrategyMissingDescriptor,
+ member.Locations.FirstOrDefault(),
+ member.Name,
+ memberType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)));
+ return null;
+ }
+ }
+ else if (strategy == CloneStrategy.DeepCopy)
+ {
+ // DeepCopy is v2 - not yet supported
+ var deepCopyNotImplementedDescriptor = new DiagnosticDescriptor(
+ id: "PKPRO999",
+ title: "DeepCopy strategy not yet implemented",
+ messageFormat: "DeepCopy strategy for member '{0}' is not yet implemented. Use Clone or Custom strategy instead.",
+ category: "PatternKit.Generators.Prototype",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+ context.ReportDiagnostic(Diagnostic.Create(
+ deepCopyNotImplementedDescriptor,
+ member.Locations.FirstOrDefault(),
+ member.Name));
+ return null;
+ }
+
+ return strategy;
+ }
+ }
+
+ // Otherwise, infer based on mode
+ return InferCloneStrategy(member, memberType, config, context);
+ }
+
+ private CloneStrategy InferCloneStrategy(
+ ISymbol member,
+ ITypeSymbol memberType,
+ PrototypeConfig config,
+ SourceProductionContext context)
+ {
+ // Value types and string: always copy (safe)
+ if (memberType.IsValueType || memberType.SpecialType == SpecialType.System_String)
+ return CloneStrategy.ByReference;
+
+ // Reference types depend on mode
+ switch (config.Mode)
+ {
+ case 0: // ShallowWithWarnings
+ // Warn about mutable reference types
+ if (!IsImmutableReferenceType(memberType))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ UnsafeReferenceCaptureDescriptor,
+ member.Locations.FirstOrDefault(),
+ member.Name));
+ }
+ return CloneStrategy.ByReference;
+
+ case 1: // Shallow
+ return CloneStrategy.ByReference;
+
+ case 2: // DeepWhenPossible
+ if (HasCloneMechanism(memberType))
+ return CloneStrategy.Clone;
+ return CloneStrategy.ByReference;
+
+ default:
+ return CloneStrategy.ByReference;
+ }
+ }
+
+ private bool IsImmutableReferenceType(ITypeSymbol type)
+ {
+ // String is immutable
+ if (type.SpecialType == SpecialType.System_String)
+ return true;
+
+ // Value types are always safe (but caller should check for this first)
+ if (type.IsValueType)
+ return true;
+
+ // Check for known immutable collections (basic check)
+ var typeName = type.ToDisplayString();
+ if (typeName.StartsWith("System.Collections.Immutable."))
+ return true;
+
+ // Conservative: assume mutable
+ return false;
+ }
+
+ private bool HasCloneMechanism(ITypeSymbol type)
+ {
+ // Check for ICloneable
+ if (ImplementsICloneable(type))
+ return true;
+
+ // Check for Clone() method returning same type
+ var cloneMethod = type.GetMembers("Clone")
+ .OfType()
+ .FirstOrDefault(m => m.Parameters.Length == 0 &&
+ SymbolEqualityComparer.Default.Equals(m.ReturnType, type));
+ if (cloneMethod is not null)
+ return true;
+
+ // Check for copy constructor
+ if (type is INamedTypeSymbol namedType)
+ {
+ if (HasCopyConstructor(namedType))
+ return true;
+ }
+
+ // Check for List or similar collections with copy constructors
+ if (IsCollectionWithCopyConstructor(type))
+ return true;
+
+ return false;
+ }
+
+ private bool ImplementsICloneable(ITypeSymbol type)
+ {
+ return type.AllInterfaces.Any(i =>
+ i.ToDisplayString() == "System.ICloneable");
+ }
+
+ private bool IsCollectionWithCopyConstructor(ITypeSymbol type)
+ {
+ if (type is not INamedTypeSymbol namedType)
+ return false;
+
+ var typeName = namedType.ConstructedFrom.ToDisplayString();
+
+ // List, HashSet, Queue, Stack, etc.
+ return typeName == "System.Collections.Generic.List" ||
+ typeName == "System.Collections.Generic.HashSet" ||
+ typeName == "System.Collections.Generic.Queue" ||
+ typeName == "System.Collections.Generic.Stack" ||
+ typeName == "System.Collections.Generic.LinkedList" ||
+ typeName == "System.Collections.Generic.Dictionary" ||
+ typeName == "System.Collections.Generic.SortedSet" ||
+ typeName == "System.Collections.Generic.SortedDictionary";
+ }
+
+ private static bool HasAttribute(ISymbol symbol, string attributeName)
+ {
+ return symbol.GetAttributes().Any(a =>
+ a.AttributeClass?.ToDisplayString() == attributeName);
+ }
+
+ private static AttributeData? GetAttribute(ISymbol symbol, string attributeName)
+ {
+ return symbol.GetAttributes().FirstOrDefault(a =>
+ a.AttributeClass?.ToDisplayString() == attributeName);
+ }
+
+ private string GenerateCloneMethod(TypeInfo typeInfo, PrototypeConfig config, SourceProductionContext context)
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("#nullable enable");
+ sb.AppendLine("// ");
+ sb.AppendLine();
+
+ // Only add namespace declaration if not in global namespace
+ if (!string.IsNullOrEmpty(typeInfo.Namespace))
+ {
+ sb.AppendLine($"namespace {typeInfo.Namespace};");
+ sb.AppendLine();
+ }
+
+ // Determine type declaration keyword
+ string typeKeyword;
+ if (typeInfo.IsRecordClass)
+ typeKeyword = "partial record class";
+ else if (typeInfo.IsRecordStruct)
+ typeKeyword = "partial record struct";
+ else if (typeInfo.IsClass)
+ typeKeyword = "partial class";
+ else
+ typeKeyword = "partial struct";
+
+ sb.AppendLine($"{typeKeyword} {typeInfo.TypeName}");
+ sb.AppendLine("{");
+
+ // Generate custom clone hook declarations
+ GenerateCustomCloneHooks(sb, typeInfo);
+
+ // Generate Clone method
+ GenerateCloneMethodBody(sb, typeInfo, config, context);
+
+ sb.AppendLine("}");
+
+ return sb.ToString();
+ }
+
+ private void GenerateCustomCloneHooks(StringBuilder sb, TypeInfo typeInfo)
+ {
+ var customMembers = typeInfo.Members.Where(m => m.CloneStrategy == CloneStrategy.Custom).ToList();
+ if (customMembers.Count == 0)
+ return;
+
+ foreach (var member in customMembers)
+ {
+ sb.AppendLine($" /// Custom clone hook for {member.Name}.");
+ sb.AppendLine($" private static partial {member.Type} Clone{member.Name}({member.Type} value);");
+ sb.AppendLine();
+ }
+ }
+
+ private void GenerateCloneMethodBody(StringBuilder sb, TypeInfo typeInfo, PrototypeConfig config, SourceProductionContext context)
+ {
+ sb.AppendLine($" /// Creates a clone of this instance.");
+ sb.AppendLine($" public {typeInfo.TypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} {config.CloneMethodName}()");
+ sb.AppendLine(" {");
+
+ // Generate member cloning expressions
+ var cloneExprs = new Dictionary();
+ foreach (var member in typeInfo.Members)
+ {
+ var expr = GenerateCloneExpression(member, typeInfo, context);
+ cloneExprs[member.Name] = expr;
+ }
+
+ // Generate construction based on strategy
+ switch (typeInfo.ConstructionStrategy)
+ {
+ case ConstructionStrategy.RecordWith:
+ GenerateRecordWithConstruction(sb, typeInfo, cloneExprs);
+ break;
+
+ case ConstructionStrategy.CopyConstructor:
+ GenerateCopyConstructorConstruction(sb, typeInfo, cloneExprs);
+ break;
+
+ case ConstructionStrategy.ParameterlessConstructor:
+ GenerateParameterlessConstructorConstruction(sb, typeInfo, cloneExprs);
+ break;
+ }
+
+ sb.AppendLine(" }");
+ }
+
+ private string GenerateCloneExpression(MemberInfo member, TypeInfo typeInfo, SourceProductionContext context)
+ {
+ switch (member.CloneStrategy)
+ {
+ case CloneStrategy.ByReference:
+ return $"this.{member.Name}";
+
+ case CloneStrategy.ShallowCopy:
+ return GenerateShallowCopyExpression(member);
+
+ case CloneStrategy.Clone:
+ return GenerateCloneCallExpression(member);
+
+ case CloneStrategy.Custom:
+ return $"Clone{member.Name}(this.{member.Name})";
+
+ default:
+ return $"this.{member.Name}";
+ }
+ }
+
+ private string GenerateShallowCopyExpression(MemberInfo member)
+ {
+ var typeName = member.TypeSymbol.ToDisplayString();
+
+ // For collections, create a new collection with the same elements
+ if (IsCollectionWithCopyConstructor(member.TypeSymbol))
+ {
+ return $"new {member.Type}(this.{member.Name})";
+ }
+
+ // For arrays
+ if (member.TypeSymbol is IArrayTypeSymbol)
+ {
+ return $"(({member.Type})this.{member.Name}.Clone())";
+ }
+
+ // Default: just copy reference
+ return $"this.{member.Name}";
+ }
+
+ private string GenerateCloneCallExpression(MemberInfo member)
+ {
+ // Check for ICloneable
+ if (ImplementsICloneable(member.TypeSymbol))
+ {
+ return $"({member.Type})this.{member.Name}.Clone()";
+ }
+
+ // Check for Clone() method
+ var cloneMethod = member.TypeSymbol.GetMembers("Clone")
+ .OfType()
+ .FirstOrDefault(m => m.Parameters.Length == 0);
+ if (cloneMethod is not null)
+ {
+ return $"this.{member.Name}.Clone()";
+ }
+
+ // Check for copy constructor
+ if (member.TypeSymbol is INamedTypeSymbol namedType && HasCopyConstructor(namedType))
+ {
+ return $"new {member.Type}(this.{member.Name})";
+ }
+
+ // For collections with copy constructors
+ if (IsCollectionWithCopyConstructor(member.TypeSymbol))
+ {
+ return $"new {member.Type}(this.{member.Name})";
+ }
+
+ // Fallback
+ return $"this.{member.Name}";
+ }
+
+ private void GenerateRecordWithConstruction(StringBuilder sb, TypeInfo typeInfo, Dictionary cloneExprs)
+ {
+ // For records with init properties, use with-expression
+ sb.Append(" return this with { ");
+
+ var assignments = typeInfo.Members
+ .Where(m => !m.IsReadOnly) // Can't set readonly members in with-expression
+ .Select(m => $"{m.Name} = {cloneExprs[m.Name]}")
+ .ToList();
+
+ if (assignments.Count > 0)
+ {
+ sb.Append(string.Join(", ", assignments));
+ }
+
+ sb.AppendLine(" };");
+ }
+
+ private void GenerateCopyConstructorConstruction(StringBuilder sb, TypeInfo typeInfo, Dictionary cloneExprs)
+ {
+ // Create using copy constructor, then set members that need custom cloning
+ sb.AppendLine($" var clone = new {typeInfo.TypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}(this);");
+
+ // Override members that need special cloning
+ foreach (var member in typeInfo.Members.Where(m => m.CloneStrategy != CloneStrategy.ByReference && !m.IsReadOnly && !m.IsInitOnly))
+ {
+ sb.AppendLine($" clone.{member.Name} = {cloneExprs[member.Name]};");
+ }
+
+ sb.AppendLine(" return clone;");
+ }
+
+ private void GenerateParameterlessConstructorConstruction(StringBuilder sb, TypeInfo typeInfo, Dictionary cloneExprs)
+ {
+ // Use object initializer syntax if possible
+ var settableMembers = typeInfo.Members.Where(m => !m.IsReadOnly && !m.IsInitOnly).ToList();
+
+ if (settableMembers.Count > 0)
+ {
+ sb.AppendLine($" return new {typeInfo.TypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}");
+ sb.AppendLine(" {");
+ foreach (var member in settableMembers)
+ {
+ sb.AppendLine($" {member.Name} = {cloneExprs[member.Name]},");
+ }
+ sb.AppendLine(" };");
+ }
+ else
+ {
+ sb.AppendLine($" return new {typeInfo.TypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}();");
+ }
+ }
+
+ // Helper classes
+ private class PrototypeConfig
+ {
+ public int Mode { get; set; } // 0=ShallowWithWarnings, 1=Shallow, 2=DeepWhenPossible
+ public string CloneMethodName { get; set; } = "Clone";
+ public bool IncludeExplicit { get; set; }
+ }
+
+ private enum ConstructionStrategy
+ {
+ None,
+ RecordWith,
+ CopyConstructor,
+ ParameterlessConstructor
+ }
+
+ private enum CloneStrategy
+ {
+ ByReference = 0,
+ ShallowCopy = 1,
+ Clone = 2,
+ DeepCopy = 3,
+ Custom = 4
+ }
+
+ private class TypeInfo
+ {
+ public INamedTypeSymbol TypeSymbol { get; set; } = null!;
+ public string TypeName { get; set; } = "";
+ public string Namespace { get; set; } = "";
+ public bool IsClass { get; set; }
+ public bool IsStruct { get; set; }
+ public bool IsRecordClass { get; set; }
+ public bool IsRecordStruct { get; set; }
+ public List Members { get; set; } = new();
+ public ConstructionStrategy ConstructionStrategy { get; set; }
+ }
+
+ private class MemberInfo
+ {
+ public string Name { get; set; } = "";
+ public string Type { get; set; } = "";
+ public ITypeSymbol TypeSymbol { get; set; } = null!;
+ public bool IsProperty { get; set; }
+ public bool IsField { get; set; }
+ public bool IsReadOnly { get; set; }
+ public bool IsInitOnly { get; set; }
+ public CloneStrategy CloneStrategy { get; set; }
+ public ISymbol Symbol { get; set; } = null!;
+ }
+}
From 7dfc87f5e2c3041f55e545f945e92c7007518748 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 27 Jan 2026 06:00:25 +0000
Subject: [PATCH 03/14] Address code review feedback: improve type safety and
code consistency
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
.../AnalyzerReleases.Unshipped.md | 1 +
.../PrototypeGenerator.cs | 47 +++++++++++++------
2 files changed, 34 insertions(+), 14 deletions(-)
diff --git a/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md b/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
index c8131a7..b53c207 100644
--- a/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
+++ b/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
@@ -58,4 +58,5 @@ PKPRO003 | PatternKit.Generators.Prototype | Warning | Unsafe reference capture
PKPRO004 | PatternKit.Generators.Prototype | Error | Requested Clone strategy but no clone mechanism found
PKPRO005 | PatternKit.Generators.Prototype | Error | Custom strategy requires partial clone hook, but none found
PKPRO006 | PatternKit.Generators.Prototype | Warning | Include/Ignore attribute misuse
+PKPRO999 | PatternKit.Generators.Prototype | Error | DeepCopy strategy not yet implemented
diff --git a/src/PatternKit.Generators/PrototypeGenerator.cs b/src/PatternKit.Generators/PrototypeGenerator.cs
index 208ab82..c423052 100644
--- a/src/PatternKit.Generators/PrototypeGenerator.cs
+++ b/src/PatternKit.Generators/PrototypeGenerator.cs
@@ -21,6 +21,7 @@ public sealed class PrototypeGenerator : IIncrementalGenerator
private const string DiagIdCloneMechanismMissing = "PKPRO004";
private const string DiagIdCustomStrategyMissing = "PKPRO005";
private const string DiagIdAttributeMisuse = "PKPRO006";
+ private const string DiagIdDeepCopyNotImplemented = "PKPRO999";
private static readonly DiagnosticDescriptor TypeNotPartialDescriptor = new(
id: DiagIdTypeNotPartial,
@@ -70,6 +71,14 @@ public sealed class PrototypeGenerator : IIncrementalGenerator
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
+ private static readonly DiagnosticDescriptor DeepCopyNotImplementedDescriptor = new(
+ id: DiagIdDeepCopyNotImplemented,
+ title: "DeepCopy strategy not yet implemented",
+ messageFormat: "DeepCopy strategy for member '{0}' is not yet implemented. Use Clone or Custom strategy instead.",
+ category: "PatternKit.Generators.Prototype",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Find all type declarations with [Prototype] attribute
@@ -147,7 +156,7 @@ private PrototypeConfig ParsePrototypeConfig(AttributeData attribute)
switch (named.Key)
{
case "Mode":
- config.Mode = (int)named.Value.Value!;
+ config.Mode = (PrototypeMode)(int)named.Value.Value!;
break;
case "CloneMethodName":
config.CloneMethodName = (string)named.Value.Value!;
@@ -203,7 +212,17 @@ private ConstructionStrategy DetermineConstructionStrategy(INamedTypeSymbol type
// For records, prefer with-expression if all members are init/readonly
if (typeInfo.IsRecordClass || typeInfo.IsRecordStruct)
{
- bool allInit = typeInfo.Members.All(m => m.IsInitOnly || m.IsReadOnly);
+ // Check if all members are init-only or readonly
+ bool allInit = true;
+ foreach (var member in typeInfo.Members)
+ {
+ if (!member.IsInitOnly && !member.IsReadOnly)
+ {
+ allInit = false;
+ break;
+ }
+ }
+
if (allInit)
return ConstructionStrategy.RecordWith;
@@ -385,15 +404,8 @@ private List GetMembersForClone(
else if (strategy == CloneStrategy.DeepCopy)
{
// DeepCopy is v2 - not yet supported
- var deepCopyNotImplementedDescriptor = new DiagnosticDescriptor(
- id: "PKPRO999",
- title: "DeepCopy strategy not yet implemented",
- messageFormat: "DeepCopy strategy for member '{0}' is not yet implemented. Use Clone or Custom strategy instead.",
- category: "PatternKit.Generators.Prototype",
- defaultSeverity: DiagnosticSeverity.Error,
- isEnabledByDefault: true);
context.ReportDiagnostic(Diagnostic.Create(
- deepCopyNotImplementedDescriptor,
+ DeepCopyNotImplementedDescriptor,
member.Locations.FirstOrDefault(),
member.Name));
return null;
@@ -420,7 +432,7 @@ private CloneStrategy InferCloneStrategy(
// Reference types depend on mode
switch (config.Mode)
{
- case 0: // ShallowWithWarnings
+ case PrototypeMode.ShallowWithWarnings:
// Warn about mutable reference types
if (!IsImmutableReferenceType(memberType))
{
@@ -431,10 +443,10 @@ private CloneStrategy InferCloneStrategy(
}
return CloneStrategy.ByReference;
- case 1: // Shallow
+ case PrototypeMode.Shallow:
return CloneStrategy.ByReference;
- case 2: // DeepWhenPossible
+ case PrototypeMode.DeepWhenPossible:
if (HasCloneMechanism(memberType))
return CloneStrategy.Clone;
return CloneStrategy.ByReference;
@@ -743,11 +755,18 @@ private void GenerateParameterlessConstructorConstruction(StringBuilder sb, Type
// Helper classes
private class PrototypeConfig
{
- public int Mode { get; set; } // 0=ShallowWithWarnings, 1=Shallow, 2=DeepWhenPossible
+ public PrototypeMode Mode { get; set; } = PrototypeMode.ShallowWithWarnings;
public string CloneMethodName { get; set; } = "Clone";
public bool IncludeExplicit { get; set; }
}
+ private enum PrototypeMode
+ {
+ ShallowWithWarnings = 0,
+ Shallow = 1,
+ DeepWhenPossible = 2
+ }
+
private enum ConstructionStrategy
{
None,
From 36ed5250276718e1ed6d51c71419abca38a89f30 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 27 Jan 2026 06:05:44 +0000
Subject: [PATCH 04/14] Fix record type support and add comprehensive tests
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
.../PrototypeGenerator.cs | 8 +
.../PrototypeGeneratorTests.cs | 306 ++++++++++++++++++
2 files changed, 314 insertions(+)
create mode 100644 test/PatternKit.Generators.Tests/PrototypeGeneratorTests.cs
diff --git a/src/PatternKit.Generators/PrototypeGenerator.cs b/src/PatternKit.Generators/PrototypeGenerator.cs
index c423052..ff60a64 100644
--- a/src/PatternKit.Generators/PrototypeGenerator.cs
+++ b/src/PatternKit.Generators/PrototypeGenerator.cs
@@ -127,6 +127,12 @@ private void GeneratePrototypeForType(
if (typeInfo is null)
return;
+ // For records, default to "Duplicate" instead of "Clone" (which is reserved)
+ if (!config.CloneMethodNameExplicit && (typeInfo.IsRecordClass || typeInfo.IsRecordStruct))
+ {
+ config.CloneMethodName = "Duplicate";
+ }
+
// Generate clone method
var cloneSource = GenerateCloneMethod(typeInfo, config, context);
if (!string.IsNullOrEmpty(cloneSource))
@@ -160,6 +166,7 @@ private PrototypeConfig ParsePrototypeConfig(AttributeData attribute)
break;
case "CloneMethodName":
config.CloneMethodName = (string)named.Value.Value!;
+ config.CloneMethodNameExplicit = true;
break;
case "IncludeExplicit":
config.IncludeExplicit = (bool)named.Value.Value!;
@@ -757,6 +764,7 @@ private class PrototypeConfig
{
public PrototypeMode Mode { get; set; } = PrototypeMode.ShallowWithWarnings;
public string CloneMethodName { get; set; } = "Clone";
+ public bool CloneMethodNameExplicit { get; set; }
public bool IncludeExplicit { get; set; }
}
diff --git a/test/PatternKit.Generators.Tests/PrototypeGeneratorTests.cs b/test/PatternKit.Generators.Tests/PrototypeGeneratorTests.cs
new file mode 100644
index 0000000..fca1083
--- /dev/null
+++ b/test/PatternKit.Generators.Tests/PrototypeGeneratorTests.cs
@@ -0,0 +1,306 @@
+using Microsoft.CodeAnalysis;
+using PatternKit.Common;
+
+namespace PatternKit.Generators.Tests;
+
+public class PrototypeGeneratorTests
+{
+ [Fact]
+ public void GenerateCloneForClass()
+ {
+ const string source = """
+ using PatternKit.Generators.Prototype;
+
+ namespace TestNamespace;
+
+ [Prototype]
+ public partial class Person
+ {
+ public string Name { get; set; } = "";
+ public int Age { get; set; }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateCloneForClass));
+ var gen = new PrototypeGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Clone method is generated
+ var names = result.Results.SelectMany(r => r.GeneratedSources).Select(gs => gs.HintName).ToArray();
+ Assert.Contains("Person.Prototype.g.cs", names);
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void GenerateCloneForRecordClass()
+ {
+ const string source = """
+ using PatternKit.Generators.Prototype;
+
+ namespace TestNamespace;
+
+ [Prototype]
+ public partial record class Person(string Name, int Age);
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateCloneForRecordClass));
+ var gen = new PrototypeGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Clone method is generated
+ var names = result.Results.SelectMany(r => r.GeneratedSources).Select(gs => gs.HintName).ToArray();
+ Assert.Contains("Person.Prototype.g.cs", names);
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+
+ // Records get "Duplicate" method by default
+ var generatedSource = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .First(gs => gs.HintName == "Person.Prototype.g.cs")
+ .SourceText.ToString();
+ Assert.Contains("Duplicate()", generatedSource);
+ }
+
+ [Fact]
+ public void GenerateCloneForRecordStruct()
+ {
+ const string source = """
+ using PatternKit.Generators.Prototype;
+
+ namespace TestNamespace;
+
+ [Prototype]
+ public partial record struct Point(int X, int Y);
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateCloneForRecordStruct));
+ var gen = new PrototypeGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Clone method is generated
+ var names = result.Results.SelectMany(r => r.GeneratedSources).Select(gs => gs.HintName).ToArray();
+ Assert.Contains("Point.Prototype.g.cs", names);
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+
+ // Records get "Duplicate" method by default
+ var generatedSource = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .First(gs => gs.HintName == "Point.Prototype.g.cs")
+ .SourceText.ToString();
+ Assert.Contains("Duplicate()", generatedSource);
+ }
+
+ [Fact]
+ public void GenerateCloneForStruct()
+ {
+ const string source = """
+ using PatternKit.Generators.Prototype;
+
+ namespace TestNamespace;
+
+ [Prototype]
+ public partial struct Vector
+ {
+ public double X { get; set; }
+ public double Y { get; set; }
+ public double Z { get; set; }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateCloneForStruct));
+ var gen = new PrototypeGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Clone method is generated
+ var names = result.Results.SelectMany(r => r.GeneratedSources).Select(gs => gs.HintName).ToArray();
+ Assert.Contains("Vector.Prototype.g.cs", names);
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void ErrorIfNotPartial()
+ {
+ const string source = """
+ using PatternKit.Generators.Prototype;
+
+ namespace TestNamespace;
+
+ [Prototype]
+ public class NonPartialClass
+ {
+ public int Value { get; set; }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(ErrorIfNotPartial));
+ var gen = new PrototypeGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out _);
+
+ // Should have PKPRO001 diagnostic
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKPRO001");
+ }
+
+ [Fact]
+ public void GenerateCloneWithCustomMethodName()
+ {
+ const string source = """
+ using PatternKit.Generators.Prototype;
+
+ namespace TestNamespace;
+
+ [Prototype(CloneMethodName = "Duplicate")]
+ public partial class Item
+ {
+ public string Name { get; set; } = "";
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateCloneWithCustomMethodName));
+ var gen = new PrototypeGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Clone method is generated
+ var names = result.Results.SelectMany(r => r.GeneratedSources).Select(gs => gs.HintName).ToArray();
+ Assert.Contains("Item.Prototype.g.cs", names);
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+
+ // Check that the custom method name is used
+ var generatedSource = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .First(gs => gs.HintName == "Item.Prototype.g.cs")
+ .SourceText.ToString();
+ Assert.Contains("Duplicate()", generatedSource);
+ Assert.DoesNotContain("Clone()", generatedSource);
+ }
+
+ [Fact]
+ public void GenerateCloneWithIgnoreAttribute()
+ {
+ const string source = """
+ using PatternKit.Generators.Prototype;
+
+ namespace TestNamespace;
+
+ [Prototype]
+ public partial class User
+ {
+ public string Username { get; set; } = "";
+
+ [PrototypeIgnore]
+ public string Password { get; set; } = "";
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateCloneWithIgnoreAttribute));
+ var gen = new PrototypeGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+
+ // Check that Password is not cloned
+ var generatedSource = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .First(gs => gs.HintName == "User.Prototype.g.cs")
+ .SourceText.ToString();
+ Assert.Contains("Username", generatedSource);
+ Assert.DoesNotContain("Password", generatedSource);
+ }
+
+ [Fact]
+ public void GenerateCloneWithExplicitInclude()
+ {
+ const string source = """
+ using PatternKit.Generators.Prototype;
+
+ namespace TestNamespace;
+
+ [Prototype(IncludeExplicit = true)]
+ public partial class Config
+ {
+ [PrototypeInclude]
+ public string ApiKey { get; set; } = "";
+
+ public string Internal { get; set; } = "";
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateCloneWithExplicitInclude));
+ var gen = new PrototypeGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+
+ // Check that only ApiKey is cloned
+ var generatedSource = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .First(gs => gs.HintName == "Config.Prototype.g.cs")
+ .SourceText.ToString();
+ Assert.Contains("ApiKey", generatedSource);
+ Assert.DoesNotContain("Internal", generatedSource);
+ }
+
+ [Fact]
+ public void WarnOnMutableReferenceType()
+ {
+ const string source = """
+ using PatternKit.Generators.Prototype;
+ using System.Collections.Generic;
+
+ namespace TestNamespace;
+
+ [Prototype]
+ public partial class Container
+ {
+ public List Items { get; set; } = new();
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(WarnOnMutableReferenceType));
+ var gen = new PrototypeGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out _);
+
+ // Should have PKPRO003 warning for mutable reference type
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKPRO003" && d.Severity == DiagnosticSeverity.Warning);
+ }
+}
From 74cb964e76f915fc74b1944f84acf137f669b539 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 27 Jan 2026 06:07:40 +0000
Subject: [PATCH 05/14] Final code review fixes: improve null safety and remove
unused code
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
src/PatternKit.Generators/PrototypeGenerator.cs | 15 +++++++++------
1 file changed, 9 insertions(+), 6 deletions(-)
diff --git a/src/PatternKit.Generators/PrototypeGenerator.cs b/src/PatternKit.Generators/PrototypeGenerator.cs
index ff60a64..7a3f7e5 100644
--- a/src/PatternKit.Generators/PrototypeGenerator.cs
+++ b/src/PatternKit.Generators/PrototypeGenerator.cs
@@ -162,14 +162,19 @@ private PrototypeConfig ParsePrototypeConfig(AttributeData attribute)
switch (named.Key)
{
case "Mode":
- config.Mode = (PrototypeMode)(int)named.Value.Value!;
+ if (named.Value.Value is int modeValue)
+ config.Mode = (PrototypeMode)modeValue;
break;
case "CloneMethodName":
- config.CloneMethodName = (string)named.Value.Value!;
- config.CloneMethodNameExplicit = true;
+ if (named.Value.Value is string methodName)
+ {
+ config.CloneMethodName = methodName;
+ config.CloneMethodNameExplicit = true;
+ }
break;
case "IncludeExplicit":
- config.IncludeExplicit = (bool)named.Value.Value!;
+ if (named.Value.Value is bool includeExplicit)
+ config.IncludeExplicit = includeExplicit;
break;
}
}
@@ -655,8 +660,6 @@ private string GenerateCloneExpression(MemberInfo member, TypeInfo typeInfo, Sou
private string GenerateShallowCopyExpression(MemberInfo member)
{
- var typeName = member.TypeSymbol.ToDisplayString();
-
// For collections, create a new collection with the same elements
if (IsCollectionWithCopyConstructor(member.TypeSymbol))
{
From acf5b81f8f9884328e42c0ade103a4a041b4690b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 27 Jan 2026 06:13:38 +0000
Subject: [PATCH 06/14] docs: Add comprehensive documentation for Prototype
pattern generator
- Created docs/generators/prototype.md with complete generator documentation
- Created docs/examples/prototype-demo.md with real-world game character example
- Follows existing PatternKit documentation patterns and style
- Includes quickstart, configuration, diagnostics, best practices
- Demonstrates deep cloning, mutation chains, and prototype registry
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
docs/examples/prototype-demo.md | 844 ++++++++++++++++++++++++++++
docs/generators/prototype.md | 969 ++++++++++++++++++++++++++++++++
2 files changed, 1813 insertions(+)
create mode 100644 docs/examples/prototype-demo.md
create mode 100644 docs/generators/prototype.md
diff --git a/docs/examples/prototype-demo.md b/docs/examples/prototype-demo.md
new file mode 100644
index 0000000..a9fe03b
--- /dev/null
+++ b/docs/examples/prototype-demo.md
@@ -0,0 +1,844 @@
+# Game Character Factory with Prototype Pattern
+
+This demo shows how to build an efficient game entity spawning system using the Prototype pattern, demonstrating complex object cloning with nested structures, collections, and runtime mutations.
+
+## What it demonstrates
+
+- **Prototype registry** - store and retrieve named character templates
+- **Deep cloning** - create independent copies of complex objects
+- **Mutation chains** - customize clones at creation time
+- **Performance** - avoid expensive initialization via cloning
+- **Independence verification** - clones don't affect originals
+
+## Where to look
+
+- Code: `src/PatternKit.Examples/PrototypeDemo/PrototypeDemo.cs`
+- Tests: `test/PatternKit.Tests/Creational/Prototype/PrototypeTests.cs`
+- Generator: [Prototype Generator Documentation](../generators/prototype.md)
+
+## Quick start
+
+```csharp
+using PatternKit.Creational.Prototype;
+
+// Define your domain model
+public sealed class GameCharacter
+{
+ public required string Id { get; set; }
+ public required string Name { get; set; }
+ public required string Class { get; set; }
+ public int Level { get; set; } = 1;
+ public CharacterStats Stats { get; set; } = new();
+ public Equipment Equipment { get; set; } = new();
+
+ public static GameCharacter DeepClone(in GameCharacter source) => new()
+ {
+ Id = $"{source.Id}-{Guid.NewGuid():N}"[..16],
+ Name = source.Name,
+ Class = source.Class,
+ Level = source.Level,
+ Stats = source.Stats.Clone(),
+ Equipment = source.Equipment.Clone()
+ };
+}
+
+// Create a prototype registry
+var factory = Prototype
+ .Create()
+ .Map("warrior", CreateWarriorPrototype(), GameCharacter.DeepClone)
+ .Map("mage", CreateMagePrototype(), GameCharacter.DeepClone)
+ .Build();
+
+// Clone with optional mutations
+var warrior = factory.Create("warrior");
+var customWarrior = factory.Create("warrior", c =>
+{
+ c.Name = "Sir Galahad";
+ c.Level = 25;
+});
+```
+
+## The Problem
+
+In game development, you often need to spawn many similar entities (NPCs, enemies, projectiles) with:
+
+1. **Complex initialization** - nested objects (stats, equipment, abilities)
+2. **Expensive setup** - loading assets, computing derived values
+3. **Variations needed** - similar but not identical instances
+4. **Performance critical** - spawn hundreds per frame
+
+Traditional solutions have drawbacks:
+
+```csharp
+// ❌ Factory functions - run full initialization every time
+public GameCharacter CreateWarrior()
+{
+ return new GameCharacter
+ {
+ Id = Guid.NewGuid().ToString(),
+ Name = "Warrior",
+ Stats = new CharacterStats { Health = 150, Strength = 15, /* ... */ },
+ Equipment = new Equipment { Weapon = "Sword", /* ... */ },
+ Abilities = ["Slash", "Block", "Charge"],
+ // ... expensive initialization
+ };
+}
+
+// ❌ Inheritance - class explosion for variants
+public class Warrior : GameCharacter { }
+public class EliteWarrior : Warrior { }
+public class BossWarrior : Warrior { }
+// ... one class per variant
+```
+
+## The Solution: Prototype Pattern
+
+The Prototype pattern solves this by:
+
+1. **Pre-configuring base prototypes** once
+2. **Cloning them** (cheap memory copy)
+3. **Mutating the clone** as needed
+
+```csharp
+// ✅ Configure once
+var warriorPrototype = CreateWarriorPrototype();
+
+// ✅ Clone many times (fast)
+var w1 = warriorPrototype.Clone();
+var w2 = warriorPrototype.Clone();
+
+// ✅ Customize each clone
+w1.Name = "Goblin Fighter #1";
+w2.Name = "Goblin Fighter #2";
+```
+
+## Domain Model
+
+### Character Statistics
+
+```csharp
+public sealed class CharacterStats
+{
+ public int Health { get; set; }
+ public int Mana { get; set; }
+ public int Strength { get; set; }
+ public int Agility { get; set; }
+ public int Intelligence { get; set; }
+
+ public CharacterStats Clone() => new()
+ {
+ Health = Health,
+ Mana = Mana,
+ Strength = Strength,
+ Agility = Agility,
+ Intelligence = Intelligence
+ };
+}
+```
+
+### Equipment System
+
+```csharp
+public sealed class Equipment
+{
+ public string Weapon { get; set; } = "Fists";
+ public string Armor { get; set; } = "Cloth";
+ public List Accessories { get; set; } = [];
+
+ public Equipment Clone() => new()
+ {
+ Weapon = Weapon,
+ Armor = Armor,
+ Accessories = new List(Accessories) // New list, same items
+ };
+}
+```
+
+### Game Character
+
+```csharp
+public sealed class GameCharacter
+{
+ public required string Id { get; set; }
+ public required string Name { get; set; }
+ public required string Class { get; set; }
+ public int Level { get; set; } = 1;
+ public CharacterStats Stats { get; set; } = new();
+ public Equipment Equipment { get; set; } = new();
+ public List Abilities { get; set; } = [];
+ public Dictionary Resistances { get; set; } = new();
+
+ // Deep clone - recursively clones all nested objects
+ public static GameCharacter DeepClone(in GameCharacter source) => new()
+ {
+ Id = $"{source.Id}-{Guid.NewGuid():N}"[..16], // Generate new ID
+ Name = source.Name,
+ Class = source.Class,
+ Level = source.Level,
+ Stats = source.Stats.Clone(), // Clone nested object
+ Equipment = source.Equipment.Clone(), // Clone nested object
+ Abilities = new List(source.Abilities), // Clone collection
+ Resistances = new Dictionary(source.Resistances) // Clone dict
+ };
+}
+```
+
+**Key points:**
+- `DeepClone` creates truly independent copies
+- Each nested object is also cloned (not just the reference)
+- Collections are copied (new collection, elements may be shared if immutable)
+- IDs are regenerated to ensure uniqueness
+
+## Base Prototypes
+
+### Warrior Prototype
+
+```csharp
+public static GameCharacter CreateWarriorPrototype() => new()
+{
+ Id = "warrior-base",
+ Name = "Warrior",
+ Class = "Warrior",
+ Level = 1,
+ Stats = new CharacterStats
+ {
+ Health = 150,
+ Mana = 30,
+ Strength = 15,
+ Agility = 8,
+ Intelligence = 5
+ },
+ Equipment = new Equipment
+ {
+ Weapon = "Iron Sword",
+ Armor = "Chainmail",
+ Accessories = ["Shield"]
+ },
+ Abilities = ["Slash", "Block", "Charge"],
+ Resistances = new Dictionary
+ {
+ ["Physical"] = 20,
+ ["Magic"] = -10
+ }
+};
+```
+
+**Design:**
+- High health and strength
+- Low intelligence and mana
+- Physical resistance, magic weakness
+
+### Mage Prototype
+
+```csharp
+public static GameCharacter CreateMagePrototype() => new()
+{
+ Id = "mage-base",
+ Name = "Mage",
+ Class = "Mage",
+ Level = 1,
+ Stats = new CharacterStats
+ {
+ Health = 80,
+ Mana = 150,
+ Strength = 5,
+ Agility = 7,
+ Intelligence = 18
+ },
+ Equipment = new Equipment
+ {
+ Weapon = "Oak Staff",
+ Armor = "Silk Robes",
+ Accessories = ["Spellbook", "Amulet"]
+ },
+ Abilities = ["Fireball", "Ice Shield", "Teleport"],
+ Resistances = new Dictionary
+ {
+ ["Physical"] = -10,
+ ["Magic"] = 25
+ }
+};
+```
+
+**Design:**
+- High mana and intelligence
+- Low health and strength
+- Magic resistance, physical weakness
+
+### Rogue Prototype
+
+```csharp
+public static GameCharacter CreateRoguePrototype() => new()
+{
+ Id = "rogue-base",
+ Name = "Rogue",
+ Class = "Rogue",
+ Level = 1,
+ Stats = new CharacterStats
+ {
+ Health = 100,
+ Mana = 60,
+ Strength = 10,
+ Agility = 16,
+ Intelligence = 10
+ },
+ Equipment = new Equipment
+ {
+ Weapon = "Twin Daggers",
+ Armor = "Leather Vest",
+ Accessories = ["Lockpicks", "Smoke Bombs"]
+ },
+ Abilities = ["Backstab", "Stealth", "Poison"],
+ Resistances = new Dictionary
+ {
+ ["Physical"] = 5,
+ ["Magic"] = 5,
+ ["Poison"] = 50
+ }
+};
+```
+
+**Design:**
+- High agility for speed/evasion
+- Balanced stats
+- Poison immunity
+
+## Prototype Registry Setup
+
+### Basic Character Factory
+
+```csharp
+public static Prototype CreateCharacterFactory()
+{
+ return Prototype
+ .Create()
+
+ // Register base class prototypes
+ .Map("warrior", CreateWarriorPrototype(), GameCharacter.DeepClone)
+ .Map("mage", CreateMagePrototype(), GameCharacter.DeepClone)
+ .Map("rogue", CreateRoguePrototype(), GameCharacter.DeepClone)
+
+ // Register elite variants with pre-configured mutations
+ .Map("elite-warrior", CreateWarriorPrototype(), GameCharacter.DeepClone)
+ .Mutate("elite-warrior", c =>
+ {
+ c.Name = "Elite Warrior";
+ c.Level = 10;
+ c.Stats.Health *= 2;
+ })
+
+ .Map("elite-mage", CreateMagePrototype(), GameCharacter.DeepClone)
+ .Mutate("elite-mage", c =>
+ {
+ c.Name = "Archmage";
+ c.Level = 15;
+ c.Stats.Mana *= 3;
+ })
+
+ // Register boss variant
+ .Map("boss-dragon-knight", CreateWarriorPrototype(), GameCharacter.DeepClone)
+ .Mutate("boss-dragon-knight", c =>
+ {
+ c.Name = "Dragon Knight";
+ c.Level = 50;
+ c.Stats.Health = 5000;
+ c.Stats.Strength = 80;
+ c.Equipment.Weapon = "Dragonbone Greatsword";
+ c.Equipment.Armor = "Dragon Scale Plate";
+ c.Resistances["Fire"] = 100;
+ })
+
+ // Set default fallback
+ .Default(CreateWarriorPrototype(), GameCharacter.DeepClone)
+
+ .Build();
+}
+```
+
+**Key features:**
+- **Named templates** - retrieve by key ("warrior", "mage", etc.)
+- **Pre-configured mutations** - elite variants have baked-in transformations
+- **Default fallback** - returned if key not found
+- **Fluent API** - chain configuration calls
+
+### Single Prototype Spawner
+
+For mass-spawning many copies of the same entity:
+
+```csharp
+public static Prototype CreateNpcSpawner(GameCharacter baseNpc)
+{
+ return Prototype
+ .Create(baseNpc, GameCharacter.DeepClone)
+ .Build();
+}
+```
+
+## Usage Scenarios
+
+### Scenario 1: Clone Base Classes
+
+```csharp
+var factory = CreateCharacterFactory();
+
+var warrior = factory.Create("warrior");
+Console.WriteLine(warrior);
+// Output: Warrior (Lv.1 Warrior) - HP:150 MP:30 STR:15 AGI:8 INT:5
+
+var mage = factory.Create("mage");
+Console.WriteLine(mage);
+// Output: Mage (Lv.1 Mage) - HP:80 MP:150 STR:5 AGI:7 INT:18
+```
+
+**What happens:**
+1. Registry looks up "warrior" key
+2. Calls `GameCharacter.DeepClone()` on the stored prototype
+3. Returns independent copy
+4. Each clone gets a new unique ID
+
+### Scenario 2: Clone with Runtime Mutations
+
+```csharp
+var customWarrior = factory.Create("warrior", c =>
+{
+ c.Name = "Sir Galahad";
+ c.Level = 25;
+ c.Stats.Strength += 20;
+ c.Equipment.Weapon = "Excalibur";
+ c.Abilities.Add("Holy Strike");
+});
+
+Console.WriteLine(customWarrior);
+// Output: Sir Galahad (Lv.25 Warrior) - HP:150 MP:30 STR:35 AGI:8 INT:5
+// Weapon: Excalibur | Armor: Chainmail
+// Abilities: Slash, Block, Charge, Holy Strike
+```
+
+**What happens:**
+1. Clone the base "warrior" prototype
+2. Apply the mutation lambda to the clone
+3. Return the customized instance
+
+**Benefits:**
+- Start from a base template
+- Customize only what's needed
+- No need to set every property manually
+
+### Scenario 3: Clone Pre-configured Elite Variants
+
+```csharp
+var eliteWarrior = factory.Create("elite-warrior");
+Console.WriteLine(eliteWarrior);
+// Output: Elite Warrior (Lv.10 Warrior) - HP:300 MP:30 STR:15 AGI:8 INT:5
+
+var archmage = factory.Create("elite-mage");
+Console.WriteLine(archmage);
+// Output: Archmage (Lv.15 Mage) - HP:80 MP:450 STR:5 AGI:7 INT:18
+```
+
+**What happens:**
+1. Elite variants are stored with pre-applied mutations
+2. When you clone "elite-warrior", you get the mutated version
+3. No runtime mutation cost - it's baked into the prototype
+
+**When to use:**
+- Common variations you spawn frequently
+- Avoid repeating the same mutations
+- Performance optimization
+
+### Scenario 4: Clone Boss Characters
+
+```csharp
+var boss = factory.Create("boss-dragon-knight");
+Console.WriteLine(boss);
+// Output: Dragon Knight (Lv.50 Warrior) - HP:5000 MP:30 STR:80 AGI:8 INT:5
+// Weapon: Dragonbone Greatsword | Armor: Dragon Scale Plate
+// Resistances: Physical:20%, Magic:-10%, Fire:100%
+```
+
+**Design pattern:**
+- Bosses are heavily modified variants
+- Start from a base class prototype (Warrior)
+- Transform stats, equipment, abilities
+- Still benefit from cloning mechanism
+
+### Scenario 5: Mass Spawn NPCs
+
+```csharp
+// Create a single prototype
+var goblinPrototype = new GameCharacter
+{
+ Id = "goblin",
+ Name = "Goblin",
+ Class = "Monster",
+ Level = 3,
+ Stats = new CharacterStats
+ {
+ Health = 30,
+ Mana = 10,
+ Strength = 8,
+ Agility = 12,
+ Intelligence = 3
+ },
+ Equipment = new Equipment { Weapon = "Rusty Dagger", Armor = "Rags" },
+ Abilities = ["Scratch", "Flee"]
+};
+
+var spawner = CreateNpcSpawner(goblinPrototype);
+
+// Spawn many with slight variations
+Console.WriteLine("Spawning 5 goblins with variations...");
+for (int i = 0; i < 5; i++)
+{
+ var goblin = spawner.Create(g =>
+ {
+ g.Name = $"Goblin #{i + 1}";
+ g.Stats.Health += Random.Shared.Next(-5, 10); // Randomize
+ });
+ Console.WriteLine($"[{goblin.Id[..8]}] {goblin.Name} HP:{goblin.Stats.Health}");
+}
+
+// Output:
+// [a7b2c3d4] Goblin #1 HP:27
+// [e5f6g7h8] Goblin #2 HP:35
+// [i9j0k1l2] Goblin #3 HP:32
+// [m3n4o5p6] Goblin #4 HP:29
+// [q7r8s9t0] Goblin #5 HP:38
+```
+
+**Performance benefits:**
+- Clone operation is O(1) for value types, O(n) for collections
+- No expensive initialization logic
+- No asset loading or computation
+- Can spawn hundreds per frame
+
+### Scenario 6: Verify Clone Independence
+
+```csharp
+var original = factory.Create("warrior");
+var clone = factory.Create("warrior", c => c.Name = "Clone Warrior");
+
+Console.WriteLine($"Original: {original.Name}"); // "Warrior"
+Console.WriteLine($"Clone: {clone.Name}"); // "Clone Warrior"
+Console.WriteLine($"Are same object? {ReferenceEquals(original, clone)}"); // False
+Console.WriteLine($"Original unchanged? {original.Name == "Warrior"}"); // True
+
+// Verify deep independence
+clone.Stats.Health = 999;
+Console.WriteLine($"Original health: {original.Stats.Health}"); // Still 150
+Console.WriteLine($"Clone health: {clone.Stats.Health}"); // 999
+
+clone.Abilities.Add("New Ability");
+Console.WriteLine($"Original abilities: {original.Abilities.Count}"); // Still 3
+Console.WriteLine($"Clone abilities: {clone.Abilities.Count}"); // 4
+```
+
+**What this proves:**
+- Clones are separate instances (different references)
+- Mutations to the clone don't affect the original
+- Nested objects (Stats, Equipment) are also independent
+- Collections (Abilities, Resistances) are independent
+
+## Before/After Comparison
+
+### Before: Manual Creation
+
+```csharp
+// ❌ Manual creation - verbose, error-prone, expensive
+public void SpawnEnemies()
+{
+ for (int i = 0; i < 100; i++)
+ {
+ var goblin = new GameCharacter
+ {
+ Id = Guid.NewGuid().ToString(),
+ Name = $"Goblin {i}",
+ Class = "Monster",
+ Level = 3,
+ Stats = new CharacterStats
+ {
+ Health = 30,
+ Mana = 10,
+ Strength = 8,
+ Agility = 12,
+ Intelligence = 3
+ },
+ Equipment = new Equipment
+ {
+ Weapon = "Rusty Dagger",
+ Armor = "Rags",
+ Accessories = []
+ },
+ Abilities = ["Scratch", "Flee"],
+ Resistances = new Dictionary()
+ };
+
+ // ... expensive initialization
+ LoadAssets(goblin);
+ ComputeDerivedStats(goblin);
+
+ _entities.Add(goblin);
+ }
+}
+```
+
+**Problems:**
+- 🐌 **Slow** - runs full initialization 100 times
+- 📝 **Verbose** - lots of repetitive code
+- 🐛 **Error-prone** - easy to miss a property
+- 🚫 **Not reusable** - can't easily create variants
+
+### After: Prototype Pattern
+
+```csharp
+// ✅ Prototype pattern - fast, concise, flexible
+public void SpawnEnemies()
+{
+ // Configure prototype once
+ var goblinPrototype = CreateGoblinPrototype();
+ LoadAssets(goblinPrototype); // Load once
+ ComputeDerivedStats(goblinPrototype); // Compute once
+
+ var spawner = CreateNpcSpawner(goblinPrototype);
+
+ // Clone 100 times (fast)
+ for (int i = 0; i < 100; i++)
+ {
+ var goblin = spawner.Create(g => g.Name = $"Goblin {i}");
+ _entities.Add(goblin);
+ }
+}
+```
+
+**Benefits:**
+- ⚡ **Fast** - clone is much cheaper than initialization
+- 📦 **Concise** - configuration in one place
+- ✅ **Safe** - single source of truth
+- 🔄 **Reusable** - clone with variations easily
+
+## Pattern Benefits Demonstrated
+
+### 1. Fast Object Creation
+
+Cloning is significantly faster than running initialization logic:
+
+```csharp
+// Initialization: O(complexity)
+var character = CreateAndInitializeCharacter(); // Slow
+
+// Cloning: O(size)
+var clone = prototype.Create(); // Fast
+```
+
+**When initialization is expensive:**
+- Loading assets from disk
+- Database queries
+- Complex computations
+- Network calls
+
+**Cloning just copies memory** - much faster.
+
+### 2. Named Prototype Registry
+
+Store common configurations with semantic names:
+
+```csharp
+factory.Create("warrior"); // Get a warrior
+factory.Create("elite-warrior"); // Get elite variant
+factory.Create("boss-dragon-knight"); // Get boss
+```
+
+**Benefits:**
+- **Self-documenting** - name describes what you get
+- **Centralized** - all prototypes in one place
+- **Easy to extend** - add new variants without changing calling code
+
+### 3. Mutation Chains
+
+Customize clones at creation time:
+
+```csharp
+var custom = factory.Create("warrior", c =>
+{
+ c.Name = "Custom Name";
+ c.Level = 25;
+ c.Stats.Strength *= 2;
+});
+```
+
+**Benefits:**
+- **Fluent** - readable customization
+- **Flexible** - override any property
+- **Composable** - chain multiple mutations
+
+### 4. Deep Cloning Ensures Independence
+
+Clones don't share mutable state with the original:
+
+```csharp
+var c1 = factory.Create("warrior");
+var c2 = factory.Create("warrior");
+
+c1.Stats.Health = 999;
+Console.WriteLine(c2.Stats.Health); // Still 150, not affected
+```
+
+**Critical for:**
+- Game entities (independent behavior)
+- Undo/redo systems (save/restore state)
+- Concurrent processing (no shared mutations)
+
+### 5. Runtime Flexibility
+
+Create variants without defining new classes:
+
+```csharp
+// No need for EliteWarrior, BossWarrior classes
+// Just mutate the base prototype
+
+factory.Map("custom-variant", basePrototype, clone)
+ .Mutate("custom-variant", c => { /* custom logic */ });
+```
+
+**Reduces:**
+- Class explosion
+- Inheritance hierarchies
+- Coupling between types
+
+## Use Cases
+
+### Game Development
+
+- **Enemy spawning** - clone enemy templates
+- **Particle systems** - clone particle configurations
+- **Item generation** - clone base items with modifications
+- **Character presets** - clone character templates
+
+### Document Processing
+
+- **Document templates** - clone base documents
+- **Form pre-filling** - clone form configurations
+- **Report generation** - clone report structures
+
+### Configuration Management
+
+- **Environment configs** - clone base configs per environment
+- **User preferences** - clone default preferences
+- **Feature flags** - clone flag sets
+
+### Testing
+
+- **Test fixtures** - clone base test data
+- **Mock objects** - clone mock configurations
+- **Test scenarios** - clone scenario templates
+
+## Performance Notes
+
+### Cloning is Fast
+
+For the game character example:
+
+- **Prototype creation:** Once per base type (~5-10 types)
+- **Cloning cost:** O(n) where n = number of properties + collection sizes
+- **vs. Initialization:** Often 10-100x faster when initialization involves I/O
+
+### Memory Efficiency
+
+```csharp
+// Prototype stored once in registry
+var prototype = CreateWarriorPrototype(); // ~1KB memory
+
+// Each clone is independent but shallow for immutable data
+var clone1 = prototype.Clone(); // ~1KB (deep copy)
+var clone2 = prototype.Clone(); // ~1KB (deep copy)
+
+// Strings are interned, so "Warrior" string is shared (but immutable)
+```
+
+### When NOT to Use Prototype
+
+❌ **Simple value objects** - just use constructors:
+```csharp
+var point = new Point(x: 10, y: 20); // Simpler than cloning
+```
+
+❌ **Immutable types** - use `with` expressions (records):
+```csharp
+var p2 = p1 with { X = 20 }; // Built-in cloning
+```
+
+❌ **Very large objects** - cloning might be expensive:
+```csharp
+// If object is 100MB, cloning copies 100MB
+// Consider lazy loading or copy-on-write strategies
+```
+
+## Code Generator Support
+
+The Prototype pattern generator can automate clone method generation:
+
+```csharp
+using PatternKit.Generators.Prototype;
+
+[Prototype]
+public partial class GameCharacter
+{
+ public required string Id { get; set; }
+ public required string Name { get; set; }
+
+ [PrototypeStrategy(PrototypeCloneStrategy.Clone)]
+ public CharacterStats Stats { get; set; } = new();
+
+ [PrototypeStrategy(PrototypeCloneStrategy.Clone)]
+ public Equipment Equipment { get; set; } = new();
+
+ [PrototypeStrategy(PrototypeCloneStrategy.Clone)]
+ public List Abilities { get; set; } = [];
+}
+
+// Generated automatically:
+// public GameCharacter Clone() { /* implementation */ }
+```
+
+**Benefits:**
+- No manual clone method writing
+- Configurable per-member strategies
+- Compile-time diagnostics for unsafe cloning
+- Supports classes, structs, records
+
+See [Prototype Generator Documentation](../generators/prototype.md) for details.
+
+## Key Takeaways
+
+✅ **Use prototypes when:**
+- Object creation is expensive (I/O, computation)
+- You need many similar but not identical instances
+- Runtime flexibility is important (no new classes)
+- You want a registry of common configurations
+
+✅ **Remember:**
+- Deep clone for independence (copy nested objects)
+- Shallow clone may share references (understand implications)
+- Mutations apply to the clone, not the prototype
+- Verify clone independence in tests
+
+✅ **Performance wins:**
+- Initialization once, clone many times
+- Memory copy is faster than logic execution
+- Registry lookup is O(1)
+
+## Run the Demo
+
+```bash
+# From the repo root
+dotnet build PatternKit.slnx -c Debug
+dotnet run --project src/PatternKit.Examples --framework net9.0
+
+# Select "Prototype Demo" from the menu
+```
+
+## Further Reading
+
+- [Prototype Generator](../generators/prototype.md) - Automated clone method generation
+- [PatternKit.Creational.Prototype](../../src/PatternKit/Creational/Prototype/) - Runtime prototype API
+- [Gang of Four Design Patterns](https://en.wikipedia.org/wiki/Design_Patterns) - Original pattern catalog
diff --git a/docs/generators/prototype.md b/docs/generators/prototype.md
new file mode 100644
index 0000000..9242ccb
--- /dev/null
+++ b/docs/generators/prototype.md
@@ -0,0 +1,969 @@
+# Prototype Generator
+
+The Prototype generator creates GoF-aligned clone methods with configurable strategies for safe object duplication. It requires only the `PatternKit.Generators` package at compile time—no runtime dependency on PatternKit.
+
+> Modes: **Shallow with Warnings** (safe-by-default with diagnostics), **Shallow** (explicit shallow cloning), and **DeepWhenPossible** (attempts deep cloning for known types).
+
+## Quickstart: Class Cloning
+
+```csharp
+using PatternKit.Generators.Prototype;
+
+[Prototype]
+public partial class Person
+{
+ public string Name { get; set; } = "";
+ public int Age { get; set; }
+ public List Hobbies { get; set; } = new();
+}
+```
+
+Generated shape (essentials):
+
+- `Person Clone()` — creates a shallow clone of the instance
+- Warnings for mutable reference types (e.g., `List` copied by reference)
+- Works with classes, structs, record classes, and record structs
+
+Usage:
+
+```csharp
+var original = new Person { Name = "Alice", Age = 30 };
+var clone = original.Clone();
+
+clone.Name = "Bob";
+Console.WriteLine(original.Name); // Still "Alice"
+```
+
+## Quickstart: Record Cloning
+
+```csharp
+using PatternKit.Generators.Prototype;
+
+[Prototype]
+public partial record class Person(string Name, int Age);
+```
+
+Generated shape:
+
+- `Person Duplicate()` — for records, default method name is "Duplicate" to avoid conflicts with compiler-generated `Clone()`
+- Uses record `with` expressions for efficient cloning
+- Override method name with `CloneMethodName` if needed
+
+Usage:
+
+```csharp
+var original = new Person("Alice", 30);
+var clone = original.Duplicate();
+
+Console.WriteLine(clone.Name); // "Alice"
+Console.WriteLine(ReferenceEquals(original, clone)); // False
+```
+
+## Basic Usage Examples
+
+### Class with Value Types
+
+```csharp
+[Prototype]
+public partial class Vector
+{
+ public double X { get; set; }
+ public double Y { get; set; }
+ public double Z { get; set; }
+}
+
+var v1 = new Vector { X = 1, Y = 2, Z = 3 };
+var v2 = v1.Clone(); // Deep copy - all value types
+```
+
+### Struct
+
+```csharp
+[Prototype]
+public partial struct Point
+{
+ public int X { get; set; }
+ public int Y { get; set; }
+}
+
+var p1 = new Point { X = 10, Y = 20 };
+var p2 = p1.Clone();
+```
+
+### Record Class
+
+```csharp
+[Prototype]
+public partial record class Customer(string Id, string Name, int Credits);
+
+var c1 = new Customer("C001", "Alice", 100);
+var c2 = c1.Duplicate(); // Uses record with-expression
+```
+
+### Record Struct
+
+```csharp
+[Prototype]
+public partial record struct Temperature(double Celsius)
+{
+ public double Fahrenheit => Celsius * 9.0 / 5.0 + 32.0;
+}
+
+var t1 = new Temperature(25.0);
+var t2 = t1.Duplicate();
+```
+
+## Configuration Options
+
+### PrototypeMode
+
+Controls default cloning behavior for reference types:
+
+```csharp
+// ShallowWithWarnings (default) - warns about mutable reference types
+[Prototype(Mode = PrototypeMode.ShallowWithWarnings)]
+public partial class Container
+{
+ public List Items { get; set; } = new(); // WARNING: PKPRO003
+}
+
+// Shallow - no warnings, explicit shallow cloning
+[Prototype(Mode = PrototypeMode.Shallow)]
+public partial class Container
+{
+ public List Items { get; set; } = new(); // No warning
+}
+
+// DeepWhenPossible - attempts deep cloning for known types
+[Prototype(Mode = PrototypeMode.DeepWhenPossible)]
+public partial class Container
+{
+ public List Items { get; set; } = new(); // Creates new List(original)
+}
+```
+
+### CloneMethodName
+
+Customize the generated method name:
+
+```csharp
+[Prototype(CloneMethodName = "Copy")]
+public partial class Document
+{
+ public string Title { get; set; } = "";
+}
+
+var doc = new Document { Title = "Report" };
+var copy = doc.Copy(); // Not Clone()
+```
+
+For records, the default is automatically "Duplicate" to avoid conflicts with compiler-generated `Clone()`:
+
+```csharp
+[Prototype] // CloneMethodName defaults to "Duplicate" for records
+public partial record class Item(string Name);
+
+[Prototype(CloneMethodName = "MakeCopy")] // Override if desired
+public partial record class Item2(string Name);
+```
+
+### IncludeExplicit
+
+Control member selection mode:
+
+```csharp
+// Default: IncludeAll mode - all members cloned unless marked [PrototypeIgnore]
+[Prototype]
+public partial class User
+{
+ public string Username { get; set; } = "";
+
+ [PrototypeIgnore] // Excluded from clone
+ public string Password { get; set; } = "";
+}
+
+// ExplicitOnly mode - only members marked [PrototypeInclude] are cloned
+[Prototype(IncludeExplicit = true)]
+public partial class Config
+{
+ [PrototypeInclude] // Included in clone
+ public string ApiKey { get; set; } = "";
+
+ public string InternalState { get; set; } = ""; // Not cloned
+}
+```
+
+## Per-Member Cloning Strategies
+
+Override the default strategy for specific members using `[PrototypeStrategy]`:
+
+### ByReference
+
+Copy the reference as-is (shallow copy):
+
+```csharp
+[Prototype]
+public partial class Logger
+{
+ [PrototypeStrategy(PrototypeCloneStrategy.ByReference)]
+ public ILogSink Sink { get; set; } = null!;
+}
+```
+
+**When to use:**
+- Immutable types (strings, records with readonly members)
+- Shared services (loggers, database connections)
+- Flyweight instances
+
+**Warning:** For mutable types, changes affect both original and clone.
+
+### ShallowCopy
+
+Create a new collection with the same element references:
+
+```csharp
+[Prototype]
+public partial class Playlist
+{
+ [PrototypeStrategy(PrototypeCloneStrategy.ShallowCopy)]
+ public List Songs { get; set; } = new();
+}
+
+// Generated:
+// Songs = new List(original.Songs)
+```
+
+**When to use:**
+- Collections where you want independent collection instances
+- Element references can be shared (immutable elements)
+
+**Limitation:** Elements themselves are not cloned.
+
+### Clone
+
+Use a known clone mechanism (ICloneable, Clone() method, copy constructor, or collection copy constructor):
+
+```csharp
+public class Address
+{
+ public string Street { get; set; } = "";
+ public string City { get; set; } = "";
+
+ public Address Clone() => new() { Street = Street, City = City };
+}
+
+[Prototype]
+public partial class Person
+{
+ public string Name { get; set; } = "";
+
+ [PrototypeStrategy(PrototypeCloneStrategy.Clone)]
+ public Address HomeAddress { get; set; } = new();
+
+ [PrototypeStrategy(PrototypeCloneStrategy.Clone)]
+ public List PhoneNumbers { get; set; } = new();
+}
+
+// Generated:
+// HomeAddress = original.HomeAddress.Clone()
+// PhoneNumbers = new List(original.PhoneNumbers)
+```
+
+**When to use:**
+- Types with explicit Clone() methods
+- Types implementing ICloneable
+- Collections (List, Dictionary, HashSet, etc.)
+
+**Generator checks:**
+- For custom types: must have `Clone()` method or `ICloneable`
+- For collections: uses copy constructor `new List(original)`
+- Emits **PKPRO004** error if no suitable mechanism found
+
+### DeepCopy
+
+**Status:** Not yet implemented (PKPRO999)
+
+Planned for recursive deep cloning of complex object graphs.
+
+### Custom
+
+Provide your own cloning logic via partial method:
+
+```csharp
+[Prototype]
+public partial class GameEntity
+{
+ [PrototypeStrategy(PrototypeCloneStrategy.Custom)]
+ public EntityStats Stats { get; set; } = new();
+
+ // Partial method hook - implement in your code
+ private static partial EntityStats CloneStats(EntityStats value);
+}
+
+// In your partial class implementation:
+public partial class GameEntity
+{
+ private static partial EntityStats CloneStats(EntityStats value)
+ {
+ return new EntityStats
+ {
+ Health = value.Health,
+ Mana = value.Mana,
+ // Custom logic here
+ };
+ }
+}
+```
+
+**When to use:**
+- Complex cloning logic not expressible with other strategies
+- Integration with existing clone systems
+- Performance-critical custom implementations
+
+**Generator checks:**
+- Emits **PKPRO005** error if partial method `private static partial TMember Clone{MemberName}(TMember value)` not found
+
+## Member Selection
+
+### Default: IncludeAll Mode
+
+All eligible members are cloned unless marked `[PrototypeIgnore]`:
+
+```csharp
+[Prototype]
+public partial class Session
+{
+ public string SessionId { get; set; } = ""; // Cloned
+ public DateTime CreatedAt { get; set; } // Cloned
+
+ [PrototypeIgnore]
+ public DateTime LastAccess { get; set; } // NOT cloned
+
+ [PrototypeIgnore]
+ public int AccessCount { get; set; } // NOT cloned
+}
+```
+
+### ExplicitOnly Mode
+
+Only members marked `[PrototypeInclude]` are cloned:
+
+```csharp
+[Prototype(IncludeExplicit = true)]
+public partial class SecureConfig
+{
+ [PrototypeInclude]
+ public string ApiEndpoint { get; set; } = ""; // Cloned
+
+ [PrototypeInclude]
+ public int Timeout { get; set; } // Cloned
+
+ public string ApiSecret { get; set; } = ""; // NOT cloned
+ public byte[] EncryptionKey { get; set; } = Array.Empty(); // NOT cloned
+}
+```
+
+**When to use ExplicitOnly:**
+- Security-sensitive types (secrets, keys)
+- Large types where most members shouldn't be cloned
+- Explicit opt-in clarity
+
+## Clone Construction Strategies
+
+The generator uses different strategies based on the type:
+
+### Record with-expression (Highest Priority)
+
+For record classes and record structs:
+
+```csharp
+[Prototype]
+public partial record class Person(string Name, int Age);
+
+// Generated:
+public Person Duplicate()
+{
+ return this with { };
+}
+```
+
+**Advantages:**
+- Most efficient for records
+- Preserves record semantics
+- Compiler-optimized
+
+### Copy Constructor
+
+If a constructor accepting the same type exists:
+
+```csharp
+[Prototype]
+public partial class Point
+{
+ public int X { get; set; }
+ public int Y { get; set; }
+
+ public Point() { }
+ public Point(Point other) // Copy constructor detected
+ {
+ X = other.X;
+ Y = other.Y;
+ }
+}
+
+// Generated:
+public Point Clone()
+{
+ return new Point(this);
+}
+```
+
+### Parameterless Constructor + Assignment (Fallback)
+
+When no copy constructor exists:
+
+```csharp
+[Prototype]
+public partial class Person
+{
+ public string Name { get; set; } = "";
+ public int Age { get; set; }
+}
+
+// Generated:
+public Person Clone()
+{
+ var clone = new Person();
+ clone.Name = this.Name;
+ clone.Age = this.Age;
+ return clone;
+}
+```
+
+**Diagnostic:**
+- Emits **PKPRO002** error if no construction path is available (no parameterless constructor, no copy constructor, not a record)
+
+## Diagnostics Reference
+
+### PKPRO001: Type Not Partial
+
+**Severity:** Error
+
+Type marked with `[Prototype]` must be declared as `partial`.
+
+```csharp
+// ❌ Error
+[Prototype]
+public class Person { }
+
+// ✅ Fixed
+[Prototype]
+public partial class Person { }
+```
+
+### PKPRO002: No Construction Path
+
+**Severity:** Error
+
+Cannot construct clone - no supported construction mechanism found.
+
+```csharp
+// ❌ Error: No parameterless constructor
+[Prototype]
+public partial class Person
+{
+ public Person(string name) { } // Only parameterized constructor
+}
+
+// ✅ Fixed: Add parameterless constructor
+[Prototype]
+public partial class Person
+{
+ public Person() { }
+ public Person(string name) { }
+}
+```
+
+### PKPRO003: Unsafe Reference Capture
+
+**Severity:** Warning
+
+Member is a mutable reference type copied by reference - mutations affect both original and clone.
+
+```csharp
+// ⚠️ Warning
+[Prototype(Mode = PrototypeMode.ShallowWithWarnings)]
+public partial class Container
+{
+ public List Items { get; set; } = new(); // PKPRO003
+}
+
+// ✅ Fixed: Use Clone strategy
+[Prototype(Mode = PrototypeMode.ShallowWithWarnings)]
+public partial class Container
+{
+ [PrototypeStrategy(PrototypeCloneStrategy.Clone)]
+ public List Items { get; set; } = new();
+}
+
+// ✅ Alternative: Use Shallow mode (acknowledges shallow cloning)
+[Prototype(Mode = PrototypeMode.Shallow)]
+public partial class Container
+{
+ public List Items { get; set; } = new(); // No warning
+}
+```
+
+### PKPRO004: Clone Mechanism Missing
+
+**Severity:** Error
+
+Member has `[PrototypeStrategy(Clone)]` but no suitable clone mechanism is available.
+
+```csharp
+public class CustomType
+{
+ public int Value { get; set; }
+ // No Clone() method, no ICloneable
+}
+
+// ❌ Error
+[Prototype]
+public partial class Container
+{
+ [PrototypeStrategy(PrototypeCloneStrategy.Clone)]
+ public CustomType Data { get; set; } = new(); // PKPRO004
+}
+
+// ✅ Fixed: Add Clone() method to CustomType
+public class CustomType
+{
+ public int Value { get; set; }
+ public CustomType Clone() => new() { Value = Value };
+}
+```
+
+### PKPRO005: Custom Strategy Missing Hook
+
+**Severity:** Error
+
+Member has `[PrototypeStrategy(Custom)]` but no partial clone hook found.
+
+```csharp
+// ❌ Error
+[Prototype]
+public partial class Entity
+{
+ [PrototypeStrategy(PrototypeCloneStrategy.Custom)]
+ public EntityData Data { get; set; } = new(); // PKPRO005
+}
+
+// ✅ Fixed: Add partial method
+[Prototype]
+public partial class Entity
+{
+ [PrototypeStrategy(PrototypeCloneStrategy.Custom)]
+ public EntityData Data { get; set; } = new();
+
+ private static partial EntityData CloneData(EntityData value);
+}
+
+// Implement in another partial file:
+public partial class Entity
+{
+ private static partial EntityData CloneData(EntityData value)
+ {
+ return new EntityData { /* custom logic */ };
+ }
+}
+```
+
+### PKPRO006: Attribute Misuse
+
+**Severity:** Warning
+
+`[PrototypeInclude]` or `[PrototypeIgnore]` used incorrectly.
+
+```csharp
+// ⚠️ Warning: PrototypeInclude in IncludeAll mode (default)
+[Prototype] // IncludeExplicit = false by default
+public partial class Config
+{
+ [PrototypeInclude] // PKPRO006: ignored in this mode
+ public string Name { get; set; } = "";
+}
+
+// ✅ Fixed: Use IncludeExplicit = true
+[Prototype(IncludeExplicit = true)]
+public partial class Config
+{
+ [PrototypeInclude]
+ public string Name { get; set; } = "";
+}
+```
+
+### PKPRO999: DeepCopy Not Implemented
+
+**Severity:** Error
+
+The `DeepCopy` strategy is not yet implemented.
+
+```csharp
+// ❌ Error
+[Prototype]
+public partial class Container
+{
+ [PrototypeStrategy(PrototypeCloneStrategy.DeepCopy)]
+ public ComplexObject Data { get; set; } = new(); // PKPRO999
+}
+
+// ✅ Workaround: Use Clone or Custom
+[Prototype]
+public partial class Container
+{
+ [PrototypeStrategy(PrototypeCloneStrategy.Clone)]
+ public ComplexObject Data { get; set; } = new();
+}
+```
+
+## Best Practices and Tips
+
+### 1. Start with ShallowWithWarnings Mode
+
+The default mode is safe-by-default and alerts you to potential issues:
+
+```csharp
+[Prototype] // Default: ShallowWithWarnings
+public partial class MyClass
+{
+ public List Items { get; set; } = new(); // You'll get a warning
+}
+```
+
+Address warnings by choosing the appropriate strategy for each mutable reference type.
+
+### 2. Use ByReference for Shared Services
+
+```csharp
+[Prototype]
+public partial class RequestHandler
+{
+ [PrototypeStrategy(PrototypeCloneStrategy.ByReference)]
+ public ILogger Logger { get; set; } = null!; // Shared, don't clone
+
+ [PrototypeStrategy(PrototypeCloneStrategy.ByReference)]
+ public IDatabase Database { get; set; } = null!; // Shared, don't clone
+}
+```
+
+### 3. Clone Collections When Elements Are Immutable
+
+```csharp
+[Prototype]
+public partial class Report
+{
+ [PrototypeStrategy(PrototypeCloneStrategy.Clone)]
+ public List Tags { get; set; } = new(); // Strings are immutable
+
+ [PrototypeStrategy(PrototypeCloneStrategy.Clone)]
+ public Dictionary Metrics { get; set; } = new();
+}
+```
+
+### 4. Use Custom Strategy for Complex Scenarios
+
+```csharp
+[Prototype]
+public partial class GameState
+{
+ [PrototypeStrategy(PrototypeCloneStrategy.Custom)]
+ public EntityRegistry Entities { get; set; } = new();
+
+ private static partial EntityRegistry CloneEntities(EntityRegistry value)
+ {
+ var clone = new EntityRegistry();
+ foreach (var entity in value.GetAll())
+ {
+ // Deep clone each entity with custom logic
+ clone.Add(entity.DeepClone());
+ }
+ return clone;
+ }
+}
+```
+
+### 5. Ignore Transient/Computed Members
+
+```csharp
+[Prototype]
+public partial class CachedData
+{
+ public byte[] Data { get; set; } = Array.Empty();
+
+ [PrototypeIgnore] // Don't clone cache
+ public DateTime LastAccess { get; set; }
+
+ [PrototypeIgnore] // Don't clone derived value
+ public int CachedSize => Data.Length;
+}
+```
+
+### 6. Use ExplicitOnly for Security-Sensitive Types
+
+```csharp
+[Prototype(IncludeExplicit = true)]
+public partial class Credentials
+{
+ [PrototypeInclude]
+ public string Username { get; set; } = "";
+
+ // Secrets NOT cloned by default
+ public string Password { get; set; } = "";
+ public string ApiKey { get; set; } = "";
+}
+```
+
+### 7. Prefer Records for Immutable Data
+
+Records get efficient cloning via `with` expressions:
+
+```csharp
+[Prototype]
+public partial record class User(string Id, string Name, DateTime Created);
+
+// Efficient: uses record with-expression
+var clone = user.Duplicate();
+```
+
+### 8. Test Clone Independence
+
+Always verify clones are truly independent:
+
+```csharp
+[Prototype]
+public partial class Document
+{
+ public string Title { get; set; } = "";
+
+ [PrototypeStrategy(PrototypeCloneStrategy.Clone)]
+ public List Tags { get; set; } = new();
+}
+
+var original = new Document { Title = "Report", Tags = ["draft"] };
+var clone = original.Clone();
+
+// Verify independence
+clone.Title = "Copy";
+clone.Tags.Add("final");
+
+Assert.Equal("Report", original.Title); // Original unchanged
+Assert.Single(original.Tags); // Original list unchanged
+```
+
+## Real-World Scenarios
+
+### Scenario 1: Configuration Snapshots
+
+```csharp
+[Prototype]
+public partial class AppConfig
+{
+ public string Environment { get; set; } = "development";
+ public int MaxConnections { get; set; } = 10;
+
+ [PrototypeStrategy(PrototypeCloneStrategy.Clone)]
+ public Dictionary Settings { get; set; } = new();
+}
+
+// Take snapshot before changes
+var originalConfig = GetCurrentConfig();
+var snapshot = originalConfig.Clone();
+
+try
+{
+ ApplyNewSettings(originalConfig);
+ ValidateConfig(originalConfig);
+}
+catch
+{
+ // Restore from snapshot
+ RestoreConfig(snapshot);
+}
+```
+
+### Scenario 2: Document Versioning
+
+```csharp
+[Prototype]
+public partial class Document
+{
+ public string Id { get; set; } = Guid.NewGuid().ToString();
+ public string Title { get; set; } = "";
+ public string Content { get; set; } = "";
+ public int Version { get; set; } = 1;
+
+ [PrototypeIgnore] // Don't copy timestamps
+ public DateTime Created { get; set; }
+}
+
+public Document CreateNewVersion(Document current)
+{
+ var newVersion = current.Clone();
+ newVersion.Id = Guid.NewGuid().ToString(); // New ID
+ newVersion.Version++; // Increment version
+ return newVersion;
+}
+```
+
+### Scenario 3: Game Entity Spawning
+
+```csharp
+[Prototype]
+public partial class Enemy
+{
+ public string Id { get; set; } = "";
+ public string Type { get; set; } = "";
+ public int Health { get; set; }
+ public int Damage { get; set; }
+
+ [PrototypeStrategy(PrototypeCloneStrategy.Clone)]
+ public List Abilities { get; set; } = new();
+
+ [PrototypeStrategy(PrototypeCloneStrategy.ByReference)]
+ public IEnemyAI AI { get; set; } = null!; // Shared behavior
+}
+
+// Prototype registry
+var goblinPrototype = new Enemy
+{
+ Type = "Goblin",
+ Health = 50,
+ Abilities = ["Scratch", "Flee"]
+};
+
+// Spawn many enemies efficiently
+for (int i = 0; i < 100; i++)
+{
+ var enemy = goblinPrototype.Clone();
+ enemy.Id = $"goblin-{i}";
+ SpawnInWorld(enemy);
+}
+```
+
+### Scenario 4: Test Data Builders
+
+```csharp
+[Prototype]
+public partial class TestUser
+{
+ public string Username { get; set; } = "test-user";
+ public string Email { get; set; } = "test@example.com";
+ public bool IsActive { get; set; } = true;
+
+ [PrototypeStrategy(PrototypeCloneStrategy.Clone)]
+ public List Roles { get; set; } = new() { "user" };
+}
+
+// Base test fixtures
+var baseUser = new TestUser();
+var adminUser = baseUser.Clone();
+adminUser.Roles.Add("admin");
+
+var inactiveUser = baseUser.Clone();
+inactiveUser.IsActive = false;
+```
+
+### Scenario 5: Form State Management
+
+```csharp
+[Prototype]
+public partial class FormState
+{
+ [PrototypeStrategy(PrototypeCloneStrategy.Clone)]
+ public Dictionary Values { get; set; } = new();
+
+ [PrototypeStrategy(PrototypeCloneStrategy.Clone)]
+ public Dictionary Errors { get; set; } = new();
+
+ public bool IsDirty { get; set; }
+}
+
+// Undo/Redo implementation
+var history = new Stack();
+
+void SaveState(FormState current)
+{
+ history.Push(current.Clone());
+}
+
+FormState Undo()
+{
+ return history.Count > 0 ? history.Pop() : null;
+}
+```
+
+## Performance Considerations
+
+### Cloning is Fast for Value Types
+
+```csharp
+[Prototype]
+public partial struct Point3D
+{
+ public double X { get; set; }
+ public double Y { get; set; }
+ public double Z { get; set; }
+}
+
+// Very fast - just copies bytes
+var clone = point.Clone();
+```
+
+### Record with-expression is Optimized
+
+```csharp
+[Prototype]
+public partial record class Customer(string Id, string Name);
+
+// Compiler-optimized shallow copy
+var clone = customer.Duplicate();
+```
+
+### Collection Cloning Has Overhead
+
+```csharp
+[Prototype]
+public partial class Container
+{
+ [PrototypeStrategy(PrototypeCloneStrategy.Clone)]
+ public List Items { get; set; } = new(); // O(n) copy
+}
+
+// Consider ByReference for read-only scenarios or immutable collections
+```
+
+### Custom Strategy for Optimization
+
+```csharp
+[Prototype]
+public partial class LargeObject
+{
+ [PrototypeStrategy(PrototypeCloneStrategy.Custom)]
+ public byte[] Data { get; set; } = Array.Empty();
+
+ private static partial byte[] CloneData(byte[] value)
+ {
+ if (value.Length == 0) return Array.Empty();
+
+ var clone = new byte[value.Length];
+ Buffer.BlockCopy(value, 0, clone, 0, value.Length); // Fast copy
+ return clone;
+ }
+}
+```
+
+## See Also
+
+- [Prototype Demo Example](../examples/prototype-demo.md) - Real-world usage with game entities
+- [PatternKit.Creational.Prototype](../../src/PatternKit/Creational/Prototype/) - Runtime prototype registry
+- [Builder Generator](builder.md) - For fluent object construction
From 4293f216974135c30519d085aef6bc2148fecb0e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 27 Jan 2026 06:15:26 +0000
Subject: [PATCH 07/14] docs: Fix path reference to PatternKit.Core in
Prototype documentation
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
docs/examples/prototype-demo.md | 2 +-
docs/generators/prototype.md | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/examples/prototype-demo.md b/docs/examples/prototype-demo.md
index a9fe03b..473d5c9 100644
--- a/docs/examples/prototype-demo.md
+++ b/docs/examples/prototype-demo.md
@@ -840,5 +840,5 @@ dotnet run --project src/PatternKit.Examples --framework net9.0
## Further Reading
- [Prototype Generator](../generators/prototype.md) - Automated clone method generation
-- [PatternKit.Creational.Prototype](../../src/PatternKit/Creational/Prototype/) - Runtime prototype API
+- [PatternKit.Core.Creational.Prototype](../../src/PatternKit.Core/Creational/Prototype/) - Runtime prototype API
- [Gang of Four Design Patterns](https://en.wikipedia.org/wiki/Design_Patterns) - Original pattern catalog
diff --git a/docs/generators/prototype.md b/docs/generators/prototype.md
index 9242ccb..773875c 100644
--- a/docs/generators/prototype.md
+++ b/docs/generators/prototype.md
@@ -965,5 +965,5 @@ public partial class LargeObject
## See Also
- [Prototype Demo Example](../examples/prototype-demo.md) - Real-world usage with game entities
-- [PatternKit.Creational.Prototype](../../src/PatternKit/Creational/Prototype/) - Runtime prototype registry
+- [PatternKit.Core.Creational.Prototype](../../src/PatternKit.Core/Creational/Prototype/) - Runtime prototype registry
- [Builder Generator](builder.md) - For fluent object construction
From ea559e29675db300ad73866e855fa053d7138ce5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 27 Jan 2026 06:16:28 +0000
Subject: [PATCH 08/14] Add Prototype pattern generator with complete
implementation, tests, and documentation
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
docs/examples/toc.yml | 4 ++++
docs/generators/toc.yml | 3 +++
2 files changed, 7 insertions(+)
diff --git a/docs/examples/toc.yml b/docs/examples/toc.yml
index 20aa962..900aba6 100644
--- a/docs/examples/toc.yml
+++ b/docs/examples/toc.yml
@@ -42,6 +42,10 @@
- name: Patterns Showcase — Integrated Order Processing
href: patterns-showcase.md
+
+- name: Prototype — Game Character Factory with Clone Registry
+ href: prototype-demo.md
+
- name: Proxy Pattern Demonstrations — Virtual, Protection, Caching, Logging, Mocking, Remote
href: proxy-demo.md
diff --git a/docs/generators/toc.yml b/docs/generators/toc.yml
index 8885c33..50f2159 100644
--- a/docs/generators/toc.yml
+++ b/docs/generators/toc.yml
@@ -10,6 +10,9 @@
- name: Builder
href: builder.md
+- name: Prototype
+ href: prototype.md
+
- name: Proxy
href: proxy.md
From 8a547200f0600e59d6c716ad736596a04a47221b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 27 Jan 2026 06:20:43 +0000
Subject: [PATCH 09/14] Address code review feedback: improve diagnostics
numbering, collection checking, and custom method validation
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
.../AnalyzerReleases.Unshipped.md | 2 +-
.../PrototypeGenerator.cs | 40 ++++++++++---------
2 files changed, 22 insertions(+), 20 deletions(-)
diff --git a/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md b/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
index b53c207..364a77c 100644
--- a/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
+++ b/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
@@ -58,5 +58,5 @@ PKPRO003 | PatternKit.Generators.Prototype | Warning | Unsafe reference capture
PKPRO004 | PatternKit.Generators.Prototype | Error | Requested Clone strategy but no clone mechanism found
PKPRO005 | PatternKit.Generators.Prototype | Error | Custom strategy requires partial clone hook, but none found
PKPRO006 | PatternKit.Generators.Prototype | Warning | Include/Ignore attribute misuse
-PKPRO999 | PatternKit.Generators.Prototype | Error | DeepCopy strategy not yet implemented
+PKPRO007 | PatternKit.Generators.Prototype | Error | DeepCopy strategy not yet implemented
diff --git a/src/PatternKit.Generators/PrototypeGenerator.cs b/src/PatternKit.Generators/PrototypeGenerator.cs
index 7a3f7e5..9e50056 100644
--- a/src/PatternKit.Generators/PrototypeGenerator.cs
+++ b/src/PatternKit.Generators/PrototypeGenerator.cs
@@ -21,7 +21,7 @@ public sealed class PrototypeGenerator : IIncrementalGenerator
private const string DiagIdCloneMechanismMissing = "PKPRO004";
private const string DiagIdCustomStrategyMissing = "PKPRO005";
private const string DiagIdAttributeMisuse = "PKPRO006";
- private const string DiagIdDeepCopyNotImplemented = "PKPRO999";
+ private const string DiagIdDeepCopyNotImplemented = "PKPRO007";
private static readonly DiagnosticDescriptor TypeNotPartialDescriptor = new(
id: DiagIdTypeNotPartial,
@@ -396,12 +396,15 @@ private List GetMembersForClone(
}
else if (strategy == CloneStrategy.Custom)
{
- // Check for custom partial method
+ // Check for custom partial method with correct signature
var containingType = member.ContainingType;
var methodName = $"Clone{member.Name}";
var hasCustomMethod = containingType.GetMembers(methodName)
.OfType()
- .Any(m => m.IsStatic && m.IsPartialDefinition && m.Parameters.Length == 1);
+ .Any(m => m.IsStatic &&
+ m.IsPartialDefinition &&
+ m.Parameters.Length == 1 &&
+ SymbolEqualityComparer.Default.Equals(m.Parameters[0].Type, memberType));
if (!hasCustomMethod)
{
@@ -521,22 +524,25 @@ private bool ImplementsICloneable(ITypeSymbol type)
i.ToDisplayString() == "System.ICloneable");
}
+ private static readonly HashSet CollectionsWithCopyConstructor = new HashSet
+ {
+ "System.Collections.Generic.List",
+ "System.Collections.Generic.HashSet",
+ "System.Collections.Generic.Queue",
+ "System.Collections.Generic.Stack",
+ "System.Collections.Generic.LinkedList",
+ "System.Collections.Generic.Dictionary",
+ "System.Collections.Generic.SortedSet",
+ "System.Collections.Generic.SortedDictionary"
+ };
+
private bool IsCollectionWithCopyConstructor(ITypeSymbol type)
{
if (type is not INamedTypeSymbol namedType)
return false;
var typeName = namedType.ConstructedFrom.ToDisplayString();
-
- // List, HashSet, Queue, Stack, etc.
- return typeName == "System.Collections.Generic.List" ||
- typeName == "System.Collections.Generic.HashSet" ||
- typeName == "System.Collections.Generic.Queue" ||
- typeName == "System.Collections.Generic.Stack" ||
- typeName == "System.Collections.Generic.LinkedList" ||
- typeName == "System.Collections.Generic.Dictionary" ||
- typeName == "System.Collections.Generic.SortedSet" ||
- typeName == "System.Collections.Generic.SortedDictionary";
+ return CollectionsWithCopyConstructor.Contains(typeName);
}
private static bool HasAttribute(ISymbol symbol, string attributeName)
@@ -716,13 +722,9 @@ private void GenerateRecordWithConstruction(StringBuilder sb, TypeInfo typeInfo,
var assignments = typeInfo.Members
.Where(m => !m.IsReadOnly) // Can't set readonly members in with-expression
- .Select(m => $"{m.Name} = {cloneExprs[m.Name]}")
- .ToList();
+ .Select(m => $"{m.Name} = {cloneExprs[m.Name]}");
- if (assignments.Count > 0)
- {
- sb.Append(string.Join(", ", assignments));
- }
+ sb.Append(string.Join(", ", assignments));
sb.AppendLine(" };");
}
From 009929613f0f9045fcb7528b66502bb0f44b215d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 27 Jan 2026 17:38:13 +0000
Subject: [PATCH 10/14] Address PR review comments: fix docs, simplify code,
add comprehensive tests for all diagnostics and strategies
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
docs/generators/prototype.md | 14 +-
.../PrototypeGenerator.cs | 17 +-
.../PrototypeGeneratorTests.cs | 341 ++++++++++++++++++
3 files changed, 350 insertions(+), 22 deletions(-)
diff --git a/docs/generators/prototype.md b/docs/generators/prototype.md
index 773875c..f5dd7ec 100644
--- a/docs/generators/prototype.md
+++ b/docs/generators/prototype.md
@@ -281,13 +281,13 @@ public partial class Person
### DeepCopy
-**Status:** Not yet implemented (PKPRO999)
+**Status:** Not yet implemented – selecting this strategy will emit diagnostic **PKPRO007** ("DeepCopy strategy not yet implemented").
Planned for recursive deep cloning of complex object graphs.
### Custom
-Provide your own cloning logic via partial method:
+Provide your own cloning logic via partial method. The generator will emit a partial method declaration; you only need to provide the implementation:
```csharp
[Prototype]
@@ -295,12 +295,10 @@ public partial class GameEntity
{
[PrototypeStrategy(PrototypeCloneStrategy.Custom)]
public EntityStats Stats { get; set; } = new();
-
- // Partial method hook - implement in your code
- private static partial EntityStats CloneStats(EntityStats value);
}
-// In your partial class implementation:
+// The generator emits the declaration automatically.
+// You only need to provide the implementation in your partial class:
public partial class GameEntity
{
private static partial EntityStats CloneStats(EntityStats value)
@@ -602,7 +600,7 @@ public partial class Config
}
```
-### PKPRO999: DeepCopy Not Implemented
+### PKPRO007: DeepCopy Not Implemented
**Severity:** Error
@@ -614,7 +612,7 @@ The `DeepCopy` strategy is not yet implemented.
public partial class Container
{
[PrototypeStrategy(PrototypeCloneStrategy.DeepCopy)]
- public ComplexObject Data { get; set; } = new(); // PKPRO999
+ public ComplexObject Data { get; set; } = new(); // PKPRO007
}
// ✅ Workaround: Use Clone or Custom
diff --git a/src/PatternKit.Generators/PrototypeGenerator.cs b/src/PatternKit.Generators/PrototypeGenerator.cs
index 9e50056..1eaaf0d 100644
--- a/src/PatternKit.Generators/PrototypeGenerator.cs
+++ b/src/PatternKit.Generators/PrototypeGenerator.cs
@@ -225,15 +225,7 @@ private ConstructionStrategy DetermineConstructionStrategy(INamedTypeSymbol type
if (typeInfo.IsRecordClass || typeInfo.IsRecordStruct)
{
// Check if all members are init-only or readonly
- bool allInit = true;
- foreach (var member in typeInfo.Members)
- {
- if (!member.IsInitOnly && !member.IsReadOnly)
- {
- allInit = false;
- break;
- }
- }
+ bool allInit = typeInfo.Members.All(member => member.IsInitOnly || member.IsReadOnly);
if (allInit)
return ConstructionStrategy.RecordWith;
@@ -505,11 +497,8 @@ private bool HasCloneMechanism(ITypeSymbol type)
return true;
// Check for copy constructor
- if (type is INamedTypeSymbol namedType)
- {
- if (HasCopyConstructor(namedType))
- return true;
- }
+ if (type is INamedTypeSymbol namedType && HasCopyConstructor(namedType))
+ return true;
// Check for List or similar collections with copy constructors
if (IsCollectionWithCopyConstructor(type))
diff --git a/test/PatternKit.Generators.Tests/PrototypeGeneratorTests.cs b/test/PatternKit.Generators.Tests/PrototypeGeneratorTests.cs
index fca1083..a3b5daa 100644
--- a/test/PatternKit.Generators.Tests/PrototypeGeneratorTests.cs
+++ b/test/PatternKit.Generators.Tests/PrototypeGeneratorTests.cs
@@ -303,4 +303,345 @@ public partial class Container
var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
Assert.Contains(diagnostics, d => d.Id == "PKPRO003" && d.Severity == DiagnosticSeverity.Warning);
}
+
+ [Fact]
+ public void ErrorOnCloneStrategyWithoutMechanism()
+ {
+ const string source = """
+ using PatternKit.Generators.Prototype;
+
+ namespace TestNamespace;
+
+ public class NonCloneable
+ {
+ public string Value { get; set; } = "";
+ }
+
+ [Prototype]
+ public partial class Container
+ {
+ [PrototypeStrategy(PrototypeCloneStrategy.Clone)]
+ public NonCloneable Data { get; set; } = new();
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(ErrorOnCloneStrategyWithoutMechanism));
+ var gen = new PrototypeGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out _);
+
+ // Should have PKPRO004 error for missing clone mechanism
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKPRO004" && d.Severity == DiagnosticSeverity.Error);
+ }
+
+ [Fact]
+ public void ErrorOnCustomStrategyWithoutPartialMethod()
+ {
+ const string source = """
+ using PatternKit.Generators.Prototype;
+
+ namespace TestNamespace;
+
+ public class CustomData
+ {
+ public string Value { get; set; } = "";
+ }
+
+ [Prototype]
+ public partial class Container
+ {
+ [PrototypeStrategy(PrototypeCloneStrategy.Custom)]
+ public CustomData Data { get; set; } = new();
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(ErrorOnCustomStrategyWithoutPartialMethod));
+ var gen = new PrototypeGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out _);
+
+ // Should have PKPRO005 error for missing custom hook
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKPRO005" && d.Severity == DiagnosticSeverity.Error);
+ }
+
+ [Fact]
+ public void WarnOnAttributeMisuseIncludeInIncludeAllMode()
+ {
+ const string source = """
+ using PatternKit.Generators.Prototype;
+
+ namespace TestNamespace;
+
+ [Prototype]
+ public partial class Container
+ {
+ [PrototypeInclude]
+ public string Value { get; set; } = "";
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(WarnOnAttributeMisuseIncludeInIncludeAllMode));
+ var gen = new PrototypeGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out _);
+
+ // Should have PKPRO006 warning for attribute misuse
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKPRO006" && d.Severity == DiagnosticSeverity.Warning);
+ }
+
+ [Fact]
+ public void WarnOnAttributeMisuseIgnoreInExplicitMode()
+ {
+ const string source = """
+ using PatternKit.Generators.Prototype;
+
+ namespace TestNamespace;
+
+ [Prototype(IncludeExplicit = true)]
+ public partial class Container
+ {
+ [PrototypeIgnore]
+ public string Value { get; set; } = "";
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(WarnOnAttributeMisuseIgnoreInExplicitMode));
+ var gen = new PrototypeGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out _);
+
+ // Should have PKPRO006 warning for attribute misuse
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKPRO006" && d.Severity == DiagnosticSeverity.Warning);
+ }
+
+ [Fact]
+ public void ErrorOnDeepCopyStrategy()
+ {
+ const string source = """
+ using PatternKit.Generators.Prototype;
+
+ namespace TestNamespace;
+
+ public class ComplexData
+ {
+ public string Value { get; set; } = "";
+ }
+
+ [Prototype]
+ public partial class Container
+ {
+ [PrototypeStrategy(PrototypeCloneStrategy.DeepCopy)]
+ public ComplexData Data { get; set; } = new();
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(ErrorOnDeepCopyStrategy));
+ var gen = new PrototypeGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out _);
+
+ // Should have PKPRO007 error for DeepCopy not implemented
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKPRO007" && d.Severity == DiagnosticSeverity.Error);
+ }
+
+ [Fact]
+ public void GenerateCloneWithShallowCopyStrategy()
+ {
+ const string source = """
+ using PatternKit.Generators.Prototype;
+ using System.Collections.Generic;
+
+ namespace TestNamespace;
+
+ [Prototype(Mode = PrototypeMode.Shallow)]
+ public partial class Container
+ {
+ [PrototypeStrategy(PrototypeCloneStrategy.ShallowCopy)]
+ public List Items { get; set; } = new();
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateCloneWithShallowCopyStrategy));
+ var gen = new PrototypeGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No errors
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error)));
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+
+ // Check that new List is created (using fully qualified name)
+ var generatedSource = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .First(gs => gs.HintName == "Container.Prototype.g.cs")
+ .SourceText.ToString();
+ Assert.Contains("new global::System.Collections.Generic.List(this.Items)", generatedSource);
+ }
+
+ [Fact]
+ public void GenerateCloneWithCloneStrategyUsingICloneable()
+ {
+ const string source = """
+ using PatternKit.Generators.Prototype;
+ using System;
+
+ namespace TestNamespace;
+
+ public class CloneableData : ICloneable
+ {
+ public string Value { get; set; } = "";
+ public object Clone() => new CloneableData { Value = this.Value };
+ }
+
+ [Prototype]
+ public partial class Container
+ {
+ [PrototypeStrategy(PrototypeCloneStrategy.Clone)]
+ public CloneableData Data { get; set; } = new();
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateCloneWithCloneStrategyUsingICloneable));
+ var gen = new PrototypeGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No errors
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error)));
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+
+ // Check that Clone() is called
+ var generatedSource = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .First(gs => gs.HintName == "Container.Prototype.g.cs")
+ .SourceText.ToString();
+ Assert.Contains("Clone()", generatedSource);
+ }
+
+ [Fact]
+ public void GenerateCloneWithCloneStrategyUsingCloneMethod()
+ {
+ const string source = """
+ using PatternKit.Generators.Prototype;
+
+ namespace TestNamespace;
+
+ public class DataWithClone
+ {
+ public string Value { get; set; } = "";
+ public DataWithClone Clone() => new DataWithClone { Value = this.Value };
+ }
+
+ [Prototype]
+ public partial class Container
+ {
+ [PrototypeStrategy(PrototypeCloneStrategy.Clone)]
+ public DataWithClone Data { get; set; } = new();
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateCloneWithCloneStrategyUsingCloneMethod));
+ var gen = new PrototypeGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No errors
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error)));
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+
+ // Check that Clone() is called
+ var generatedSource = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .First(gs => gs.HintName == "Container.Prototype.g.cs")
+ .SourceText.ToString();
+ Assert.Contains("Clone()", generatedSource);
+ }
+
+ [Fact]
+ public void GenerateCloneWithCloneStrategyUsingCopyConstructor()
+ {
+ const string source = """
+ using PatternKit.Generators.Prototype;
+
+ namespace TestNamespace;
+
+ public class DataWithCopyCtor
+ {
+ public string Value { get; set; } = "";
+
+ public DataWithCopyCtor() { }
+ public DataWithCopyCtor(DataWithCopyCtor other)
+ {
+ Value = other.Value;
+ }
+ }
+
+ [Prototype]
+ public partial class Container
+ {
+ [PrototypeStrategy(PrototypeCloneStrategy.Clone)]
+ public DataWithCopyCtor Data { get; set; } = new();
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateCloneWithCloneStrategyUsingCopyConstructor));
+ var gen = new PrototypeGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No errors
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error)));
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+
+ // Check that copy constructor is called
+ var generatedSource = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .First(gs => gs.HintName == "Container.Prototype.g.cs")
+ .SourceText.ToString();
+ Assert.Contains("new global::TestNamespace.DataWithCopyCtor(this.Data)", generatedSource);
+ }
+
+ [Fact]
+ public void GenerateCloneWithCloneStrategyForListCollection()
+ {
+ const string source = """
+ using PatternKit.Generators.Prototype;
+ using System.Collections.Generic;
+
+ namespace TestNamespace;
+
+ [Prototype]
+ public partial class Container
+ {
+ [PrototypeStrategy(PrototypeCloneStrategy.Clone)]
+ public List Items { get; set; } = new();
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateCloneWithCloneStrategyForListCollection));
+ var gen = new PrototypeGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No errors
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error)));
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+
+ // Check that List copy constructor is used (using fully qualified name)
+ var generatedSource = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .First(gs => gs.HintName == "Container.Prototype.g.cs")
+ .SourceText.ToString();
+ Assert.Contains("new global::System.Collections.Generic.List(this.Items)", generatedSource);
+ }
}
From 7d7314d86f0ac5bba06af026068f08db460571be Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 28 Jan 2026 01:06:49 +0000
Subject: [PATCH 11/14] Address PR review: add generic/nested type diagnostics,
fix static Clone() detection, remove duplicate partial methods, add PKPRO002
test
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
docs/generators/prototype.md | 10 ++-
.../AnalyzerReleases.Unshipped.md | 2 +
.../PrototypeGenerator.cs | 64 ++++++++++-----
.../PrototypeGeneratorTests.cs | 80 +++++++++++++++++++
4 files changed, 131 insertions(+), 25 deletions(-)
diff --git a/docs/generators/prototype.md b/docs/generators/prototype.md
index f5dd7ec..891d12c 100644
--- a/docs/generators/prototype.md
+++ b/docs/generators/prototype.md
@@ -287,7 +287,7 @@ Planned for recursive deep cloning of complex object graphs.
### Custom
-Provide your own cloning logic via partial method. The generator will emit a partial method declaration; you only need to provide the implementation:
+Provide your own cloning logic via partial method. You must declare and implement the partial method yourself:
```csharp
[Prototype]
@@ -295,10 +295,12 @@ public partial class GameEntity
{
[PrototypeStrategy(PrototypeCloneStrategy.Custom)]
public EntityStats Stats { get; set; } = new();
+
+ // Declare the partial method
+ private static partial EntityStats CloneStats(EntityStats value);
}
-// The generator emits the declaration automatically.
-// You only need to provide the implementation in your partial class:
+// Provide the implementation in your partial class:
public partial class GameEntity
{
private static partial EntityStats CloneStats(EntityStats value)
@@ -319,7 +321,7 @@ public partial class GameEntity
- Performance-critical custom implementations
**Generator checks:**
-- Emits **PKPRO005** error if partial method `private static partial TMember Clone{MemberName}(TMember value)` not found
+- Emits **PKPRO005** error if partial method `private static partial TMember Clone{MemberName}(TMember value)` not declared and implemented
## Member Selection
diff --git a/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md b/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
index 364a77c..635c0be 100644
--- a/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
+++ b/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
@@ -59,4 +59,6 @@ PKPRO004 | PatternKit.Generators.Prototype | Error | Requested Clone strategy bu
PKPRO005 | PatternKit.Generators.Prototype | Error | Custom strategy requires partial clone hook, but none found
PKPRO006 | PatternKit.Generators.Prototype | Warning | Include/Ignore attribute misuse
PKPRO007 | PatternKit.Generators.Prototype | Error | DeepCopy strategy not yet implemented
+PKPRO008 | PatternKit.Generators.Prototype | Error | Generic types not supported for Prototype pattern
+PKPRO009 | PatternKit.Generators.Prototype | Error | Nested types not supported for Prototype pattern
diff --git a/src/PatternKit.Generators/PrototypeGenerator.cs b/src/PatternKit.Generators/PrototypeGenerator.cs
index 1eaaf0d..b344bbe 100644
--- a/src/PatternKit.Generators/PrototypeGenerator.cs
+++ b/src/PatternKit.Generators/PrototypeGenerator.cs
@@ -22,6 +22,8 @@ public sealed class PrototypeGenerator : IIncrementalGenerator
private const string DiagIdCustomStrategyMissing = "PKPRO005";
private const string DiagIdAttributeMisuse = "PKPRO006";
private const string DiagIdDeepCopyNotImplemented = "PKPRO007";
+ private const string DiagIdGenericTypeNotSupported = "PKPRO008";
+ private const string DiagIdNestedTypeNotSupported = "PKPRO009";
private static readonly DiagnosticDescriptor TypeNotPartialDescriptor = new(
id: DiagIdTypeNotPartial,
@@ -79,6 +81,22 @@ public sealed class PrototypeGenerator : IIncrementalGenerator
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
+ private static readonly DiagnosticDescriptor GenericTypeNotSupportedDescriptor = new(
+ id: DiagIdGenericTypeNotSupported,
+ title: "Generic types not supported for Prototype pattern",
+ messageFormat: "Type '{0}' is generic, which is not currently supported by the Prototype generator. Remove the [Prototype] attribute or use a non-generic type.",
+ category: "PatternKit.Generators.Prototype",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor NestedTypeNotSupportedDescriptor = new(
+ id: DiagIdNestedTypeNotSupported,
+ title: "Nested types not supported for Prototype pattern",
+ messageFormat: "Type '{0}' is nested inside another type, which is not currently supported by the Prototype generator. Remove the [Prototype] attribute or move the type to the top level.",
+ category: "PatternKit.Generators.Prototype",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Find all type declarations with [Prototype] attribute
@@ -119,6 +137,26 @@ private void GeneratePrototypeForType(
return;
}
+ // Check for generic types
+ if (typeSymbol.IsGenericType)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ GenericTypeNotSupportedDescriptor,
+ node.GetLocation(),
+ typeSymbol.Name));
+ return;
+ }
+
+ // Check for nested types
+ if (typeSymbol.ContainingType is not null)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ NestedTypeNotSupportedDescriptor,
+ node.GetLocation(),
+ typeSymbol.Name));
+ return;
+ }
+
// Parse attribute arguments
var config = ParsePrototypeConfig(attribute);
@@ -488,10 +526,11 @@ private bool HasCloneMechanism(ITypeSymbol type)
if (ImplementsICloneable(type))
return true;
- // Check for Clone() method returning same type
+ // Check for Clone() method returning same type (instance methods only)
var cloneMethod = type.GetMembers("Clone")
.OfType()
- .FirstOrDefault(m => m.Parameters.Length == 0 &&
+ .FirstOrDefault(m => !m.IsStatic &&
+ m.Parameters.Length == 0 &&
SymbolEqualityComparer.Default.Equals(m.ReturnType, type));
if (cloneMethod is not null)
return true;
@@ -574,9 +613,6 @@ private string GenerateCloneMethod(TypeInfo typeInfo, PrototypeConfig config, So
sb.AppendLine($"{typeKeyword} {typeInfo.TypeName}");
sb.AppendLine("{");
- // Generate custom clone hook declarations
- GenerateCustomCloneHooks(sb, typeInfo);
-
// Generate Clone method
GenerateCloneMethodBody(sb, typeInfo, config, context);
@@ -585,20 +621,6 @@ private string GenerateCloneMethod(TypeInfo typeInfo, PrototypeConfig config, So
return sb.ToString();
}
- private void GenerateCustomCloneHooks(StringBuilder sb, TypeInfo typeInfo)
- {
- var customMembers = typeInfo.Members.Where(m => m.CloneStrategy == CloneStrategy.Custom).ToList();
- if (customMembers.Count == 0)
- return;
-
- foreach (var member in customMembers)
- {
- sb.AppendLine($" /// Custom clone hook for {member.Name}.");
- sb.AppendLine($" private static partial {member.Type} Clone{member.Name}({member.Type} value);");
- sb.AppendLine();
- }
- }
-
private void GenerateCloneMethodBody(StringBuilder sb, TypeInfo typeInfo, PrototypeConfig config, SourceProductionContext context)
{
sb.AppendLine($" /// Creates a clone of this instance.");
@@ -679,10 +701,10 @@ private string GenerateCloneCallExpression(MemberInfo member)
return $"({member.Type})this.{member.Name}.Clone()";
}
- // Check for Clone() method
+ // Check for Clone() method (instance methods only)
var cloneMethod = member.TypeSymbol.GetMembers("Clone")
.OfType()
- .FirstOrDefault(m => m.Parameters.Length == 0);
+ .FirstOrDefault(m => !m.IsStatic && m.Parameters.Length == 0);
if (cloneMethod is not null)
{
return $"this.{member.Name}.Clone()";
diff --git a/test/PatternKit.Generators.Tests/PrototypeGeneratorTests.cs b/test/PatternKit.Generators.Tests/PrototypeGeneratorTests.cs
index a3b5daa..4f78f68 100644
--- a/test/PatternKit.Generators.Tests/PrototypeGeneratorTests.cs
+++ b/test/PatternKit.Generators.Tests/PrototypeGeneratorTests.cs
@@ -644,4 +644,84 @@ public partial class Container
.SourceText.ToString();
Assert.Contains("new global::System.Collections.Generic.List(this.Items)", generatedSource);
}
+
+ [Fact]
+ public void ErrorOnNoConstructionPath()
+ {
+ const string source = """
+ using PatternKit.Generators.Prototype;
+
+ namespace TestNamespace;
+
+ [Prototype]
+ public partial class Container
+ {
+ public string Value { get; }
+
+ public Container(string value)
+ {
+ Value = value;
+ }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(ErrorOnNoConstructionPath));
+ var gen = new PrototypeGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out _);
+
+ // Should have PKPRO002 error for no construction path
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKPRO002" && d.Severity == DiagnosticSeverity.Error);
+ }
+
+ [Fact]
+ public void ErrorOnGenericType()
+ {
+ const string source = """
+ using PatternKit.Generators.Prototype;
+
+ namespace TestNamespace;
+
+ [Prototype]
+ public partial class GenericContainer
+ {
+ public T Value { get; set; } = default!;
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(ErrorOnGenericType));
+ var gen = new PrototypeGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out _);
+
+ // Should have PKPRO008 error for generic type
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKPRO008" && d.Severity == DiagnosticSeverity.Error);
+ }
+
+ [Fact]
+ public void ErrorOnNestedType()
+ {
+ const string source = """
+ using PatternKit.Generators.Prototype;
+
+ namespace TestNamespace;
+
+ public class OuterClass
+ {
+ [Prototype]
+ public partial class InnerClass
+ {
+ public string Value { get; set; } = "";
+ }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(ErrorOnNestedType));
+ var gen = new PrototypeGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out _);
+
+ // Should have PKPRO009 error for nested type
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKPRO009" && d.Severity == DiagnosticSeverity.Error);
+ }
}
From 46c1ebbfe69bc279e0846dc14a58dc161242ba82 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 28 Jan 2026 02:22:30 +0000
Subject: [PATCH 12/14] Address PR review: verify Custom return types, relax
constructor accessibility, add static Clone() test, document enum sync
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
.../PrototypeGenerator.cs | 15 ++--
.../PrototypeGeneratorTests.cs | 70 +++++++++++++++++++
2 files changed, 80 insertions(+), 5 deletions(-)
diff --git a/src/PatternKit.Generators/PrototypeGenerator.cs b/src/PatternKit.Generators/PrototypeGenerator.cs
index b344bbe..cb35d95 100644
--- a/src/PatternKit.Generators/PrototypeGenerator.cs
+++ b/src/PatternKit.Generators/PrototypeGenerator.cs
@@ -301,9 +301,9 @@ private bool HasParameterlessConstructor(INamedTypeSymbol typeSymbol)
if (typeSymbol.TypeKind == TypeKind.Struct)
return true;
- return typeSymbol.Constructors.Any(c =>
- c.Parameters.Length == 0 &&
- c.DeclaredAccessibility >= Accessibility.Internal);
+ // Any parameterless constructor is usable from the generated clone method,
+ // which is emitted into the same partial type, regardless of accessibility.
+ return typeSymbol.Constructors.Any(c => c.Parameters.Length == 0);
}
private List GetMembersForClone(
@@ -426,7 +426,7 @@ private List GetMembersForClone(
}
else if (strategy == CloneStrategy.Custom)
{
- // Check for custom partial method with correct signature
+ // Check for custom partial method with correct signature (parameter and return type must match member type)
var containingType = member.ContainingType;
var methodName = $"Clone{member.Name}";
var hasCustomMethod = containingType.GetMembers(methodName)
@@ -434,7 +434,8 @@ private List GetMembersForClone(
.Any(m => m.IsStatic &&
m.IsPartialDefinition &&
m.Parameters.Length == 1 &&
- SymbolEqualityComparer.Default.Equals(m.Parameters[0].Type, memberType));
+ SymbolEqualityComparer.Default.Equals(m.Parameters[0].Type, memberType) &&
+ SymbolEqualityComparer.Default.Equals(m.ReturnType, memberType));
if (!hasCustomMethod)
{
@@ -784,6 +785,8 @@ private class PrototypeConfig
public bool IncludeExplicit { get; set; }
}
+ // Internal enums that mirror the public attribute enums
+ // These must stay in sync with PatternKit.Generators.Prototype.PrototypeMode
private enum PrototypeMode
{
ShallowWithWarnings = 0,
@@ -799,6 +802,8 @@ private enum ConstructionStrategy
ParameterlessConstructor
}
+ // Internal enum that mirrors PatternKit.Generators.Prototype.PrototypeCloneStrategy
+ // Values must stay in sync with the public enum
private enum CloneStrategy
{
ByReference = 0,
diff --git a/test/PatternKit.Generators.Tests/PrototypeGeneratorTests.cs b/test/PatternKit.Generators.Tests/PrototypeGeneratorTests.cs
index 4f78f68..50f26e2 100644
--- a/test/PatternKit.Generators.Tests/PrototypeGeneratorTests.cs
+++ b/test/PatternKit.Generators.Tests/PrototypeGeneratorTests.cs
@@ -724,4 +724,74 @@ public partial class InnerClass
var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
Assert.Contains(diagnostics, d => d.Id == "PKPRO009" && d.Severity == DiagnosticSeverity.Error);
}
+
+ [Fact]
+ public void ErrorOnStaticCloneMethod()
+ {
+ const string source = """
+ using PatternKit.Generators.Prototype;
+
+ namespace TestNamespace;
+
+ public class DataWithStaticClone
+ {
+ public string Value { get; set; } = "";
+
+ // Static Clone method - should NOT be treated as valid
+ public static DataWithStaticClone Clone()
+ {
+ return new DataWithStaticClone();
+ }
+ }
+
+ [Prototype]
+ public partial class Container
+ {
+ [PrototypeStrategy(PrototypeCloneStrategy.Clone)]
+ public DataWithStaticClone Data { get; set; } = new();
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(ErrorOnStaticCloneMethod));
+ var gen = new PrototypeGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out _);
+
+ // Should have PKPRO004 error because static Clone() is not a valid mechanism
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKPRO004" && d.Severity == DiagnosticSeverity.Error);
+ }
+
+ [Fact]
+ public void SucceedWithPrivateParameterlessConstructor()
+ {
+ const string source = """
+ using PatternKit.Generators.Prototype;
+
+ namespace TestNamespace;
+
+ [Prototype]
+ public partial class SecureContainer
+ {
+ public string Value { get; set; } = "";
+
+ private SecureContainer()
+ {
+ }
+
+ public static SecureContainer Create() => new SecureContainer();
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(SucceedWithPrivateParameterlessConstructor));
+ var gen = new PrototypeGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // Should succeed - private parameterless constructor is accessible from generated code
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.DoesNotContain(diagnostics, d => d.Id == "PKPRO002");
+
+ // Compilation should succeed
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
}
From a7c93c5c83182f81bbdf791b54a7cfc69166c2d4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 28 Jan 2026 05:03:21 +0000
Subject: [PATCH 13/14] Address PR review: fix init-only property cloning, add
abstract class diagnostic, add comprehensive tests
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
.../AnalyzerReleases.Unshipped.md | 1 +
.../PrototypeGenerator.cs | 22 +++-
.../PrototypeGeneratorTests.cs | 105 ++++++++++++++++++
3 files changed, 127 insertions(+), 1 deletion(-)
diff --git a/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md b/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
index 635c0be..ba46011 100644
--- a/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
+++ b/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
@@ -61,4 +61,5 @@ PKPRO006 | PatternKit.Generators.Prototype | Warning | Include/Ignore attribute
PKPRO007 | PatternKit.Generators.Prototype | Error | DeepCopy strategy not yet implemented
PKPRO008 | PatternKit.Generators.Prototype | Error | Generic types not supported for Prototype pattern
PKPRO009 | PatternKit.Generators.Prototype | Error | Nested types not supported for Prototype pattern
+PKPRO010 | PatternKit.Generators.Prototype | Error | Abstract types not supported for Prototype pattern
diff --git a/src/PatternKit.Generators/PrototypeGenerator.cs b/src/PatternKit.Generators/PrototypeGenerator.cs
index cb35d95..3559ceb 100644
--- a/src/PatternKit.Generators/PrototypeGenerator.cs
+++ b/src/PatternKit.Generators/PrototypeGenerator.cs
@@ -24,6 +24,7 @@ public sealed class PrototypeGenerator : IIncrementalGenerator
private const string DiagIdDeepCopyNotImplemented = "PKPRO007";
private const string DiagIdGenericTypeNotSupported = "PKPRO008";
private const string DiagIdNestedTypeNotSupported = "PKPRO009";
+ private const string DiagIdAbstractTypeNotSupported = "PKPRO010";
private static readonly DiagnosticDescriptor TypeNotPartialDescriptor = new(
id: DiagIdTypeNotPartial,
@@ -97,6 +98,14 @@ public sealed class PrototypeGenerator : IIncrementalGenerator
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
+ private static readonly DiagnosticDescriptor AbstractTypeNotSupportedDescriptor = new(
+ id: DiagIdAbstractTypeNotSupported,
+ title: "Abstract types not supported for Prototype pattern",
+ messageFormat: "Type '{0}' is abstract, which is not supported by the Prototype generator. Abstract types cannot be instantiated for cloning.",
+ category: "PatternKit.Generators.Prototype",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Find all type declarations with [Prototype] attribute
@@ -157,6 +166,16 @@ private void GeneratePrototypeForType(
return;
}
+ // Check for abstract types (non-record abstract classes cannot be instantiated)
+ if (typeSymbol.IsAbstract && !typeSymbol.IsRecord)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ AbstractTypeNotSupportedDescriptor,
+ node.GetLocation(),
+ typeSymbol.Name));
+ return;
+ }
+
// Parse attribute arguments
var config = ParsePrototypeConfig(attribute);
@@ -758,7 +777,8 @@ private void GenerateCopyConstructorConstruction(StringBuilder sb, TypeInfo type
private void GenerateParameterlessConstructorConstruction(StringBuilder sb, TypeInfo typeInfo, Dictionary cloneExprs)
{
// Use object initializer syntax if possible
- var settableMembers = typeInfo.Members.Where(m => !m.IsReadOnly && !m.IsInitOnly).ToList();
+ // Init-only properties can be set in object initializers
+ var settableMembers = typeInfo.Members.Where(m => !m.IsReadOnly).ToList();
if (settableMembers.Count > 0)
{
diff --git a/test/PatternKit.Generators.Tests/PrototypeGeneratorTests.cs b/test/PatternKit.Generators.Tests/PrototypeGeneratorTests.cs
index 50f26e2..15a96a7 100644
--- a/test/PatternKit.Generators.Tests/PrototypeGeneratorTests.cs
+++ b/test/PatternKit.Generators.Tests/PrototypeGeneratorTests.cs
@@ -794,4 +794,109 @@ private SecureContainer()
var emit = updated.Emit(Stream.Null);
Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
}
+
+ [Fact]
+ public void SucceedWithInitOnlyPropertiesOnClass()
+ {
+ const string source = """
+ using PatternKit.Generators.Prototype;
+
+ namespace TestNamespace;
+
+ [Prototype]
+ public partial class ImmutableData
+ {
+ public string Name { get; init; } = "";
+ public int Value { get; init; }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(SucceedWithInitOnlyPropertiesOnClass));
+ var gen = new PrototypeGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // Should succeed - init-only properties can be set in object initializers
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.DoesNotContain(diagnostics, d => d.Severity == DiagnosticSeverity.Error);
+
+ // Compilation should succeed
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+
+ // Verify init-only properties are cloned
+ var generatedSource = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .First(gs => gs.HintName == "ImmutableData.Prototype.g.cs")
+ .SourceText.ToString();
+ Assert.Contains("Name = this.Name", generatedSource);
+ Assert.Contains("Value = this.Value", generatedSource);
+ }
+
+ [Fact]
+ public void SucceedWithInitOnlyPropertiesWithCopyConstructor()
+ {
+ const string source = """
+ using PatternKit.Generators.Prototype;
+
+ namespace TestNamespace;
+
+ [Prototype]
+ public partial class DataWithCtor
+ {
+ public string Name { get; init; } = "";
+ public int Value { get; set; }
+
+ public DataWithCtor() { }
+
+ public DataWithCtor(DataWithCtor other)
+ {
+ Name = other.Name;
+ Value = other.Value;
+ }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(SucceedWithInitOnlyPropertiesWithCopyConstructor));
+ var gen = new PrototypeGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // Should succeed - copy constructor handles init-only properties
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.DoesNotContain(diagnostics, d => d.Severity == DiagnosticSeverity.Error);
+
+ // Compilation should succeed
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+
+ // Verify copy constructor is used
+ var generatedSource = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .First(gs => gs.HintName == "DataWithCtor.Prototype.g.cs")
+ .SourceText.ToString();
+ Assert.Contains("new global::TestNamespace.DataWithCtor(this)", generatedSource);
+ }
+
+ [Fact]
+ public void ErrorOnAbstractClass()
+ {
+ const string source = """
+ using PatternKit.Generators.Prototype;
+
+ namespace TestNamespace;
+
+ [Prototype]
+ public abstract partial class AbstractBase
+ {
+ public string Value { get; set; } = "";
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(ErrorOnAbstractClass));
+ var gen = new PrototypeGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out _);
+
+ // Should have PKPRO010 error for abstract type
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKPRO010" && d.Severity == DiagnosticSeverity.Error);
+ }
}
From c960f9ad9b478e34ef4723e47197aa4d61685927 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 28 Jan 2026 05:47:47 +0000
Subject: [PATCH 14/14] Address PR review: add missing diagnostic docs, fix
message clarity, add Custom/DeepWhenPossible tests
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
docs/generators/prototype.md | 69 ++++++++++++
.../PrototypeGenerator.cs | 2 +-
.../PrototypeGeneratorTests.cs | 106 ++++++++++++++++++
3 files changed, 176 insertions(+), 1 deletion(-)
diff --git a/docs/generators/prototype.md b/docs/generators/prototype.md
index 891d12c..fad0d40 100644
--- a/docs/generators/prototype.md
+++ b/docs/generators/prototype.md
@@ -626,6 +626,75 @@ public partial class Container
}
```
+### PKPRO008: Generic Types Not Supported
+
+**Severity:** Error
+
+Generic types are not currently supported by the Prototype generator.
+
+```csharp
+// ❌ Error
+[Prototype]
+public partial class Container // PKPRO008
+{
+ public T Value { get; set; } = default!;
+}
+
+// ✅ Workaround: Use non-generic type
+[Prototype]
+public partial class StringContainer
+{
+ public string Value { get; set; } = "";
+}
+```
+
+### PKPRO009: Nested Types Not Supported
+
+**Severity:** Error
+
+Nested types (types declared inside other types) are not currently supported.
+
+```csharp
+// ❌ Error
+public class OuterClass
+{
+ [Prototype]
+ public partial class InnerClass // PKPRO009
+ {
+ public string Value { get; set; } = "";
+ }
+}
+
+// ✅ Workaround: Move type to top level
+[Prototype]
+public partial class InnerClass
+{
+ public string Value { get; set; } = "";
+}
+```
+
+### PKPRO010: Abstract Types Not Supported
+
+**Severity:** Error
+
+Abstract non-record classes cannot be instantiated for cloning.
+
+```csharp
+// ❌ Error
+[Prototype]
+public abstract partial class AbstractBase // PKPRO010
+{
+ public string Value { get; set; } = "";
+}
+
+// ✅ Workaround: Use concrete class or record
+[Prototype]
+public partial class ConcreteClass
+{
+ public string Value { get; set; } = "";
+}
+```
+
## Best Practices and Tips
### 1. Start with ShallowWithWarnings Mode
diff --git a/src/PatternKit.Generators/PrototypeGenerator.cs b/src/PatternKit.Generators/PrototypeGenerator.cs
index 3559ceb..29f72b1 100644
--- a/src/PatternKit.Generators/PrototypeGenerator.cs
+++ b/src/PatternKit.Generators/PrototypeGenerator.cs
@@ -61,7 +61,7 @@ public sealed class PrototypeGenerator : IIncrementalGenerator
private static readonly DiagnosticDescriptor CustomStrategyMissingDescriptor = new(
id: DiagIdCustomStrategyMissing,
title: "Custom strategy requires partial clone hook, but none found",
- messageFormat: "Member '{0}' has [PrototypeStrategy(Custom)] but no partial method 'private static partial {1} Clone{0}({1} value)' was found. Declare this method in your partial type.",
+ messageFormat: "Member '{0}' has [PrototypeStrategy(Custom)] but no static partial method '{1} Clone{0}({1} value)' was found. Declare this method in your partial type.",
category: "PatternKit.Generators.Prototype",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
diff --git a/test/PatternKit.Generators.Tests/PrototypeGeneratorTests.cs b/test/PatternKit.Generators.Tests/PrototypeGeneratorTests.cs
index 15a96a7..5c9728f 100644
--- a/test/PatternKit.Generators.Tests/PrototypeGeneratorTests.cs
+++ b/test/PatternKit.Generators.Tests/PrototypeGeneratorTests.cs
@@ -899,4 +899,110 @@ public abstract partial class AbstractBase
var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
Assert.Contains(diagnostics, d => d.Id == "PKPRO010" && d.Severity == DiagnosticSeverity.Error);
}
+
+ [Fact]
+ public void SucceedWithCustomStrategy()
+ {
+ const string source = """
+ using PatternKit.Generators.Prototype;
+
+ namespace TestNamespace;
+
+ public class CustomData
+ {
+ public string Value { get; set; } = "";
+ }
+
+ [Prototype]
+ public partial class Container
+ {
+ [PrototypeStrategy(PrototypeCloneStrategy.Custom)]
+ public CustomData Data { get; set; } = new();
+
+ private static partial CustomData CloneData(CustomData value);
+ }
+
+ public partial class Container
+ {
+ private static partial CustomData CloneData(CustomData value)
+ {
+ return new CustomData { Value = value.Value + "_cloned" };
+ }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(SucceedWithCustomStrategy));
+ var gen = new PrototypeGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // Should succeed - custom partial method is provided
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.DoesNotContain(diagnostics, d => d.Id == "PKPRO005");
+ Assert.DoesNotContain(diagnostics, d => d.Severity == DiagnosticSeverity.Error);
+
+ // Compilation should succeed
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+
+ // Verify custom method is called
+ var generatedSource = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .First(gs => gs.HintName == "Container.Prototype.g.cs")
+ .SourceText.ToString();
+ Assert.Contains("CloneData(this.Data)", generatedSource);
+ }
+
+ [Fact]
+ public void SucceedWithDeepWhenPossibleMode()
+ {
+ const string source = """
+ using PatternKit.Generators.Prototype;
+ using System.Collections.Generic;
+
+ namespace TestNamespace;
+
+ public class CloneableData
+ {
+ public string Value { get; set; } = "";
+ public CloneableData Clone() => new CloneableData { Value = this.Value };
+ }
+
+ public class NonCloneableData
+ {
+ public string Value { get; set; } = "";
+ }
+
+ [Prototype(Mode = PrototypeMode.DeepWhenPossible)]
+ public partial class Container
+ {
+ // Should use Clone strategy automatically
+ public CloneableData Cloneable { get; set; } = new();
+
+ // Should fall back to by-reference (no warning in DeepWhenPossible mode)
+ public NonCloneableData NonCloneable { get; set; } = new();
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(SucceedWithDeepWhenPossibleMode));
+ var gen = new PrototypeGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // Should succeed - DeepWhenPossible mode clones what it can
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.DoesNotContain(diagnostics, d => d.Severity == DiagnosticSeverity.Error);
+ // No warnings in DeepWhenPossible mode
+ Assert.DoesNotContain(diagnostics, d => d.Id == "PKPRO003");
+
+ // Compilation should succeed
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+
+ // Verify cloneable uses Clone() and non-cloneable uses by-reference
+ var generatedSource = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .First(gs => gs.HintName == "Container.Prototype.g.cs")
+ .SourceText.ToString();
+ Assert.Contains("Cloneable.Clone()", generatedSource);
+ Assert.Contains("this.NonCloneable", generatedSource);
+ }
}