diff --git a/src/PatternKit.Examples/Decorators/StorageDecoratorExample.cs b/src/PatternKit.Examples/Decorators/StorageDecoratorExample.cs
new file mode 100644
index 0000000..7403838
--- /dev/null
+++ b/src/PatternKit.Examples/Decorators/StorageDecoratorExample.cs
@@ -0,0 +1,231 @@
+using PatternKit.Generators.Decorator;
+
+namespace PatternKit.Examples.Decorators;
+
+///
+/// Example demonstrating the Decorator pattern with a storage interface.
+/// Shows how to use generated decorator base classes to implement caching,
+/// logging, and retry logic in a composable way.
+///
+[GenerateDecorator]
+public interface IFileStorage
+{
+ string ReadFile(string path);
+ void WriteFile(string path, string content);
+ bool FileExists(string path);
+ void DeleteFile(string path);
+}
+
+///
+/// Simple in-memory file storage implementation.
+///
+public class InMemoryFileStorage : IFileStorage
+{
+ private readonly Dictionary _files = new();
+
+ public string ReadFile(string path)
+ {
+ if (!_files.TryGetValue(path, out var content))
+ throw new FileNotFoundException($"File not found: {path}");
+ return content;
+ }
+
+ public void WriteFile(string path, string content)
+ {
+ _files[path] = content;
+ }
+
+ public bool FileExists(string path)
+ {
+ return _files.ContainsKey(path);
+ }
+
+ public void DeleteFile(string path)
+ {
+ _files.Remove(path);
+ }
+}
+
+///
+/// Caching decorator that caches file reads.
+///
+public class CachingFileStorage : FileStorageDecoratorBase
+{
+ private readonly Dictionary _cache = new();
+
+ public CachingFileStorage(IFileStorage inner) : base(inner) { }
+
+ public override string ReadFile(string path)
+ {
+ if (_cache.TryGetValue(path, out var cached))
+ {
+ Console.WriteLine($"[Cache] Hit for {path}");
+ return cached;
+ }
+
+ Console.WriteLine($"[Cache] Miss for {path}");
+ var content = base.ReadFile(path);
+ _cache[path] = content;
+ return content;
+ }
+
+ public override void WriteFile(string path, string content)
+ {
+ _cache.Remove(path); // Invalidate cache
+ base.WriteFile(path, content);
+ }
+
+ public override void DeleteFile(string path)
+ {
+ _cache.Remove(path); // Invalidate cache
+ base.DeleteFile(path);
+ }
+}
+
+///
+/// Logging decorator that logs all operations.
+///
+public class LoggingFileStorage : FileStorageDecoratorBase
+{
+ public LoggingFileStorage(IFileStorage inner) : base(inner) { }
+
+ public override string ReadFile(string path)
+ {
+ Console.WriteLine($"[Log] Reading file: {path}");
+ try
+ {
+ var result = base.ReadFile(path);
+ Console.WriteLine($"[Log] Successfully read {result.Length} characters from {path}");
+ return result;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[Log] Error reading {path}: {ex.Message}");
+ throw;
+ }
+ }
+
+ public override void WriteFile(string path, string content)
+ {
+ Console.WriteLine($"[Log] Writing {content.Length} characters to {path}");
+ base.WriteFile(path, content);
+ Console.WriteLine($"[Log] Successfully wrote to {path}");
+ }
+
+ public override bool FileExists(string path)
+ {
+ Console.WriteLine($"[Log] Checking existence of {path}");
+ var exists = base.FileExists(path);
+ Console.WriteLine($"[Log] File {path} exists: {exists}");
+ return exists;
+ }
+
+ public override void DeleteFile(string path)
+ {
+ Console.WriteLine($"[Log] Deleting file: {path}");
+ base.DeleteFile(path);
+ Console.WriteLine($"[Log] Successfully deleted {path}");
+ }
+}
+
+///
+/// Retry decorator that retries failed operations.
+///
+public class RetryFileStorage : FileStorageDecoratorBase
+{
+ private readonly int _maxRetries;
+ private readonly int _retryDelayMs;
+
+ public RetryFileStorage(IFileStorage inner, int maxRetries = 3, int retryDelayMs = 100)
+ : base(inner)
+ {
+ _maxRetries = maxRetries;
+ _retryDelayMs = retryDelayMs;
+ }
+
+ public override string ReadFile(string path)
+ {
+ return RetryOperation(() => base.ReadFile(path), $"ReadFile({path})");
+ }
+
+ public override void WriteFile(string path, string content)
+ {
+ RetryOperation(() => { base.WriteFile(path, content); return true; }, $"WriteFile({path})");
+ }
+
+ public override void DeleteFile(string path)
+ {
+ RetryOperation(() => { base.DeleteFile(path); return true; }, $"DeleteFile({path})");
+ }
+
+ private T RetryOperation(Func operation, string operationName)
+ {
+ for (int i = 0; i < _maxRetries; i++)
+ {
+ try
+ {
+ return operation();
+ }
+ catch (Exception ex) when (i < _maxRetries - 1)
+ {
+ Console.WriteLine($"[Retry] Attempt {i + 1}/{_maxRetries} failed for {operationName}: {ex.Message}");
+ Thread.Sleep(_retryDelayMs);
+ }
+ }
+ // Last attempt - let exception propagate
+ return operation();
+ }
+}
+
+///
+/// Demonstrates using the decorator pattern with the generated base class.
+///
+public static class StorageDecoratorDemo
+{
+ public static void Run()
+ {
+ Console.WriteLine("=== Decorator Pattern Demo ===\n");
+
+ // Base storage
+ var baseStorage = new InMemoryFileStorage();
+
+ // Compose decorators: Logging -> Caching -> Retry -> Base
+ // Order matters: outermost decorator is applied first
+ var storage = FileStorageDecorators.Compose(
+ baseStorage,
+ inner => new LoggingFileStorage(inner),
+ inner => new CachingFileStorage(inner),
+ inner => new RetryFileStorage(inner)
+ );
+
+ Console.WriteLine("--- Writing a file ---");
+ storage.WriteFile("test.txt", "Hello, Decorators!");
+
+ Console.WriteLine("\n--- Reading the file (cache miss) ---");
+ var content1 = storage.ReadFile("test.txt");
+ Console.WriteLine($"Content: {content1}");
+
+ Console.WriteLine("\n--- Reading the file again (cache hit) ---");
+ var content2 = storage.ReadFile("test.txt");
+ Console.WriteLine($"Content: {content2}");
+
+ Console.WriteLine("\n--- Checking file existence ---");
+ var exists = storage.FileExists("test.txt");
+ Console.WriteLine($"Exists: {exists}");
+
+ Console.WriteLine("\n--- Deleting the file ---");
+ storage.DeleteFile("test.txt");
+
+ Console.WriteLine("\n--- Trying to read deleted file (will fail with retries) ---");
+ try
+ {
+ storage.ReadFile("test.txt");
+ }
+ catch (FileNotFoundException ex)
+ {
+ Console.WriteLine($"Expected error: {ex.Message}");
+ }
+
+ Console.WriteLine("\n=== Demo Complete ===");
+ }
+}
diff --git a/src/PatternKit.Generators.Abstractions/Decorator/DecoratorIgnoreAttribute.cs b/src/PatternKit.Generators.Abstractions/Decorator/DecoratorIgnoreAttribute.cs
new file mode 100644
index 0000000..77225e7
--- /dev/null
+++ b/src/PatternKit.Generators.Abstractions/Decorator/DecoratorIgnoreAttribute.cs
@@ -0,0 +1,13 @@
+namespace PatternKit.Generators.Decorator;
+
+///
+/// Marks a member that should not participate in decoration.
+/// The generator will still emit a forwarding member in the decorator. For concrete
+/// members it strips virtual/override so the member is not decorated,
+/// but when required to satisfy an abstract or virtual contract it emits a
+/// sealed override instead.
+///
+[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
+public sealed class DecoratorIgnoreAttribute : Attribute
+{
+}
diff --git a/src/PatternKit.Generators.Abstractions/Decorator/GenerateDecoratorAttribute.cs b/src/PatternKit.Generators.Abstractions/Decorator/GenerateDecoratorAttribute.cs
new file mode 100644
index 0000000..7f53955
--- /dev/null
+++ b/src/PatternKit.Generators.Abstractions/Decorator/GenerateDecoratorAttribute.cs
@@ -0,0 +1,65 @@
+namespace PatternKit.Generators.Decorator;
+
+///
+/// Marks an interface or abstract class for Decorator pattern code generation.
+/// Generates a base decorator class that forwards all members to an inner instance,
+/// with optional composition helpers for building decorator chains.
+///
+[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
+public sealed class GenerateDecoratorAttribute : Attribute
+{
+ ///
+ /// Name of the generated base decorator class.
+ /// Default is {ContractName}DecoratorBase (e.g., IStorage -> StorageDecoratorBase).
+ ///
+ public string? BaseTypeName { get; set; }
+
+ ///
+ /// Name of the generated helpers/composition class.
+ /// Default is {ContractName}Decorators (e.g., IStorage -> StorageDecorators).
+ ///
+ public string? HelpersTypeName { get; set; }
+
+ ///
+ /// Determines what composition helpers are generated.
+ /// Default is HelpersOnly (generates a Compose method for chaining decorators).
+ ///
+ public DecoratorCompositionMode Composition { get; set; } = DecoratorCompositionMode.HelpersOnly;
+
+ ///
+ /// Reserved for future use.
+ /// Currently ignored by the Decorator generator and has no effect on generated code.
+ /// Default is false.
+ ///
+ public bool GenerateAsync { get; set; }
+
+ ///
+ /// Reserved for future use.
+ /// Currently ignored by the Decorator generator and has no effect on generated code.
+ /// Default is false.
+ ///
+ public bool ForceAsync { get; set; }
+}
+
+///
+/// Determines what composition utilities are generated with the decorator.
+///
+public enum DecoratorCompositionMode
+{
+ ///
+ /// Do not generate any composition helpers.
+ ///
+ None = 0,
+
+ ///
+ /// Generate a static Compose method for chaining decorators in order.
+ /// Decorators are applied in array order (first element is outermost).
+ ///
+ HelpersOnly = 1,
+
+ ///
+ /// Generate pipeline-style composition with explicit "next" parameter.
+ /// (Reserved for future use - v2 feature)
+ ///
+ PipelineNextStyle = 2
+}
diff --git a/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md b/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
index e530359..1f27f05 100644
--- a/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
+++ b/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
@@ -40,3 +40,9 @@ PKMEM006 | PatternKit.Generators.Memento | Info | Init-only or readonly restrict
PKVIS001 | PatternKit.Generators.Visitor | Warning | No concrete types found for visitor generation
PKVIS002 | PatternKit.Generators.Visitor | Error | Type must be partial for Accept method generation
PKVIS004 | PatternKit.Generators.Visitor | Error | Derived type must be partial for Accept method generation
+PKDEC001 | PatternKit.Generators.Decorator | Error | Unsupported target type for decorator generation
+PKDEC002 | PatternKit.Generators.Decorator | Error | Unsupported member kind for decorator generation
+PKDEC003 | PatternKit.Generators.Decorator | Error | Name conflict for generated decorator types
+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
diff --git a/src/PatternKit.Generators/DecoratorGenerator.cs b/src/PatternKit.Generators/DecoratorGenerator.cs
new file mode 100644
index 0000000..fc852a9
--- /dev/null
+++ b/src/PatternKit.Generators/DecoratorGenerator.cs
@@ -0,0 +1,993 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using PatternKit.Generators.Decorator;
+using System.Text;
+
+namespace PatternKit.Generators;
+
+///
+/// Source generator for the Decorator pattern.
+/// Generates base decorator classes that forward all members to an inner instance,
+/// with optional composition helpers for building decorator chains.
+///
+[Generator]
+public sealed class DecoratorGenerator : IIncrementalGenerator
+{
+ // Symbol display format that preserves nullable annotations
+ private static readonly SymbolDisplayFormat TypeFormat = new(
+ globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included,
+ typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
+ genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
+ miscellaneousOptions: SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier | SymbolDisplayMiscellaneousOptions.UseSpecialTypes);
+
+ // Diagnostic IDs
+ private const string DiagIdUnsupportedTargetType = "PKDEC001";
+ private const string DiagIdNestedType = "PKDEC006";
+ private const string DiagIdUnsupportedMember = "PKDEC002";
+ private const string DiagIdNameConflict = "PKDEC003";
+ private const string DiagIdInaccessibleMember = "PKDEC004";
+ private const string DiagIdGenericContract = "PKDEC005";
+
+ private static readonly DiagnosticDescriptor UnsupportedTargetTypeDescriptor = new(
+ id: DiagIdUnsupportedTargetType,
+ title: "Unsupported target type for decorator generation",
+ messageFormat: "Type '{0}' cannot be used as a decorator contract. Only interfaces and abstract classes are supported.",
+ category: "PatternKit.Generators.Decorator",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor UnsupportedMemberDescriptor = new(
+ id: DiagIdUnsupportedMember,
+ title: "Unsupported member kind for decorator generation",
+ messageFormat: "Member '{0}' of kind '{1}' is not supported in decorator generation (v1). Only methods and properties are supported.",
+ category: "PatternKit.Generators.Decorator",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor NameConflictDescriptor = new(
+ id: DiagIdNameConflict,
+ title: "Name conflict for generated decorator types",
+ messageFormat: "Generated type name '{0}' conflicts with an existing type. Use BaseTypeName or HelpersTypeName to specify a different name.",
+ category: "PatternKit.Generators.Decorator",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor InaccessibleMemberDescriptor = new(
+ id: DiagIdInaccessibleMember,
+ title: "Member is not accessible for decorator generation",
+ messageFormat: "Member '{0}' cannot be forwarded by the generated decorator. Only members accessible from the decorator type (public, internal, or protected internal) can be forwarded; purely protected or private protected members on the inner type are not accessible.",
+ category: "PatternKit.Generators.Decorator",
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor GenericContractDescriptor = new(
+ id: DiagIdGenericContract,
+ title: "Generic contracts are not supported for decorator generation",
+ messageFormat: "Generic type '{0}' cannot be used as a decorator contract. Generic contracts are not supported in v1.",
+ category: "PatternKit.Generators.Decorator",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor NestedTypeDescriptor = new(
+ id: DiagIdNestedType,
+ title: "Nested types are not supported for decorator generation",
+ messageFormat: "Nested type '{0}' cannot be used as a decorator contract. Only top-level types are supported.",
+ category: "PatternKit.Generators.Decorator",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ // Find all types (interfaces, abstract classes, or records) marked with [GenerateDecorator]
+ var decoratorContracts = context.SyntaxProvider.ForAttributeWithMetadataName(
+ fullyQualifiedMetadataName: "PatternKit.Generators.Decorator.GenerateDecoratorAttribute",
+ predicate: static (node, _) => node is InterfaceDeclarationSyntax or ClassDeclarationSyntax or RecordDeclarationSyntax,
+ transform: static (ctx, _) => ctx
+ );
+
+ // Generate for each contract
+ context.RegisterSourceOutput(decoratorContracts, (spc, contractContext) =>
+ {
+ if (contractContext.TargetSymbol is not INamedTypeSymbol contractSymbol)
+ return;
+
+ var attr = contractContext.Attributes.FirstOrDefault(a =>
+ a.AttributeClass?.ToDisplayString() == "PatternKit.Generators.Decorator.GenerateDecoratorAttribute");
+ if (attr is null)
+ return;
+
+ GenerateDecoratorForContract(spc, contractSymbol, attr, contractContext.TargetNode);
+ });
+ }
+
+ private void GenerateDecoratorForContract(
+ SourceProductionContext context,
+ INamedTypeSymbol contractSymbol,
+ AttributeData attribute,
+ SyntaxNode node)
+ {
+ // Check if type is interface or abstract class
+ if (contractSymbol.TypeKind == TypeKind.Interface)
+ {
+ // Interfaces are supported
+ }
+ else if (contractSymbol.TypeKind == TypeKind.Class && contractSymbol.IsAbstract)
+ {
+ // Abstract classes are supported
+ }
+ else
+ {
+ // Not an interface or abstract class - unsupported target type
+ context.ReportDiagnostic(Diagnostic.Create(
+ UnsupportedTargetTypeDescriptor,
+ node.GetLocation(),
+ contractSymbol.Name));
+ return;
+ }
+
+ // Check for nested types (not supported - accessibility issues)
+ if (contractSymbol.ContainingType != null)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ NestedTypeDescriptor,
+ node.GetLocation(),
+ contractSymbol.Name));
+ return;
+ }
+
+ // Check for generic contracts (not supported in v1)
+ if (contractSymbol.TypeParameters.Length > 0)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ GenericContractDescriptor,
+ node.GetLocation(),
+ contractSymbol.Name));
+ return;
+ }
+
+ // Parse attribute arguments
+ var config = ParseDecoratorConfig(attribute, contractSymbol);
+
+ // Analyze contract and members
+ var contractInfo = AnalyzeContract(contractSymbol, config, context);
+ if (contractInfo is null)
+ return;
+
+ // Check for name conflicts
+ if (HasNameConflict(contractSymbol, config.BaseTypeName))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ NameConflictDescriptor,
+ node.GetLocation(),
+ config.BaseTypeName));
+ return;
+ }
+
+ // Check for helpers name conflict only if composition helpers will be generated
+ if ((DecoratorCompositionMode)config.Composition == DecoratorCompositionMode.HelpersOnly &&
+ HasNameConflict(contractSymbol, config.HelpersTypeName))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ NameConflictDescriptor,
+ node.GetLocation(),
+ config.HelpersTypeName));
+ return;
+ }
+
+ // Generate base decorator class
+ var decoratorSource = GenerateBaseDecorator(contractInfo, config, context);
+ if (!string.IsNullOrEmpty(decoratorSource))
+ {
+ // Use namespace + simple name to avoid collisions while keeping it readable
+ var ns = contractSymbol.ContainingNamespace.IsGlobalNamespace
+ ? ""
+ : contractSymbol.ContainingNamespace.ToDisplayString().Replace(".", "_") + "_";
+ var fileName = $"{ns}{contractSymbol.Name}.Decorator.g.cs";
+ context.AddSource(fileName, decoratorSource);
+ }
+ }
+
+ private DecoratorConfig ParseDecoratorConfig(AttributeData attribute, INamedTypeSymbol contractSymbol)
+ {
+ var config = new DecoratorConfig
+ {
+ ContractName = contractSymbol.Name,
+ Namespace = contractSymbol.ContainingNamespace.IsGlobalNamespace
+ ? string.Empty
+ : contractSymbol.ContainingNamespace.ToDisplayString()
+ };
+
+ // Determine default base type name
+ var baseName = contractSymbol.Name;
+ if (baseName.StartsWith("I") && baseName.Length > 1 && char.IsUpper(baseName[1]))
+ {
+ // Interface with I prefix: IStorage -> StorageDecoratorBase
+ baseName = baseName.Substring(1);
+ }
+ config.BaseTypeName = $"{baseName}DecoratorBase";
+ config.HelpersTypeName = $"{baseName}Decorators";
+
+ foreach (var named in attribute.NamedArguments)
+ {
+ switch (named.Key)
+ {
+ case nameof(GenerateDecoratorAttribute.BaseTypeName):
+ if (named.Value.Value is string baseTypeName && !string.IsNullOrWhiteSpace(baseTypeName))
+ config.BaseTypeName = baseTypeName;
+ break;
+ case nameof(GenerateDecoratorAttribute.HelpersTypeName):
+ if (named.Value.Value is string helpersTypeName && !string.IsNullOrWhiteSpace(helpersTypeName))
+ config.HelpersTypeName = helpersTypeName;
+ break;
+ case nameof(GenerateDecoratorAttribute.Composition):
+ config.Composition = (int)named.Value.Value!;
+ break;
+ // GenerateAsync and ForceAsync are reserved for future use - not parsed
+ }
+ }
+
+ return config;
+ }
+
+ private ContractInfo? AnalyzeContract(
+ INamedTypeSymbol contractSymbol,
+ DecoratorConfig config,
+ SourceProductionContext context)
+ {
+ var contractInfo = new ContractInfo
+ {
+ ContractSymbol = contractSymbol,
+ ContractName = contractSymbol.Name,
+ Namespace = config.Namespace,
+ IsInterface = contractSymbol.TypeKind == TypeKind.Interface,
+ IsAbstractClass = contractSymbol.TypeKind == TypeKind.Class && contractSymbol.IsAbstract,
+ Members = new List()
+ };
+
+ // Collect members based on contract type
+ var members = GetMembersForDecorator(contractSymbol, contractInfo, context);
+ contractInfo.Members.AddRange(members);
+
+ if (contractInfo.Members.Count == 0)
+ {
+ // No members to forward - skip generation to avoid emitting invalid decorators
+ // (Could be empty due to hasErrors=true in GetMembersForDecorator, or truly no members)
+ return null;
+ }
+
+ // Detect if any async members exist
+ contractInfo.HasAsyncMembers = contractInfo.Members.Any(m => m.IsAsync);
+
+ return contractInfo;
+ }
+
+ private List GetMembersForDecorator(
+ INamedTypeSymbol contractSymbol,
+ ContractInfo contractInfo,
+ SourceProductionContext context)
+ {
+ var members = new List();
+ var hasErrors = false;
+
+ // Get all members from the contract and its base types
+ var allMembers = GetAllInterfaceMembers(contractSymbol);
+
+ foreach (var member in allMembers)
+ {
+ // Check for ignore attribute - these will still be forwarded but marked as non-virtual
+ var isIgnored = HasAttribute(member, "PatternKit.Generators.Decorator.DecoratorIgnoreAttribute");
+
+ // Only process methods and properties
+ if (member is IMethodSymbol method)
+ {
+ // Skip special methods (constructors, operators, property accessors, etc.)
+ if (method.MethodKind != MethodKind.Ordinary)
+ continue;
+
+ // Skip static methods; decorators only forward instance members
+ if (method.IsStatic)
+ continue;
+
+ // Generic methods are not supported in v1
+ if (method.TypeParameters.Length > 0)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ UnsupportedMemberDescriptor,
+ member.Locations.FirstOrDefault() ?? Location.None,
+ method.Name,
+ "Generic method"));
+ hasErrors = true;
+ continue;
+ }
+
+ // For abstract classes, only include virtual or abstract methods
+ if (contractInfo.IsAbstractClass && !method.IsVirtual && !method.IsAbstract)
+ continue;
+
+ // Skip inaccessible methods
+ if (!IsAccessibleForDecorator(method.DeclaredAccessibility))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ InaccessibleMemberDescriptor,
+ member.Locations.FirstOrDefault() ?? Location.None,
+ member.Name));
+ // PKDEC004 is a warning, not an error - don't set hasErrors
+ continue;
+ }
+
+ var isAsync = IsAsyncMethod(method);
+ var baseReturnType = method.ReturnType.ToDisplayString(TypeFormat);
+ var returnType = method.ReturnsByRef
+ ? "ref " + baseReturnType
+ : method.ReturnsByRefReadonly
+ ? "ref readonly " + baseReturnType
+ : baseReturnType;
+
+ members.Add(new MemberInfo
+ {
+ Name = method.Name,
+ MemberType = MemberType.Method,
+ ReturnType = returnType,
+ IsAsync = isAsync,
+ IsVoid = method.ReturnsVoid,
+ IsIgnored = isIgnored,
+ Accessibility = method.DeclaredAccessibility,
+ OriginalSymbol = method,
+ ReturnsByRef = method.ReturnsByRef,
+ ReturnsByRefReadonly = method.ReturnsByRefReadonly,
+ Parameters = method.Parameters.Select(p => new ParameterInfo
+ {
+ Name = p.Name,
+ Type = p.Type.ToDisplayString(TypeFormat),
+ HasDefaultValue = p.HasExplicitDefaultValue,
+ DefaultValue = p.HasExplicitDefaultValue ? FormatDefaultValue(p) : null,
+ RefKind = p.RefKind,
+ IsParams = p.IsParams,
+ IsThis = p.IsThis
+ }).ToList()
+ });
+ }
+ else if (member is IPropertySymbol property)
+ {
+ // Indexer properties are not supported in v1
+ if (property.IsIndexer)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ UnsupportedMemberDescriptor,
+ member.Locations.FirstOrDefault() ?? Location.None,
+ member.Name,
+ "Indexer"));
+ hasErrors = true;
+ continue;
+ }
+
+ // Skip static properties; decorators only forward instance members
+ if (property.IsStatic)
+ continue;
+
+ // For abstract classes, only include virtual or abstract properties
+ if (contractInfo.IsAbstractClass && !property.IsVirtual && !property.IsAbstract)
+ continue;
+
+ // Check accessor-level accessibility
+ // Property may be public but have protected/private accessors
+ bool getterAccessible = property.GetMethod == null || IsAccessibleForDecorator(property.GetMethod.DeclaredAccessibility);
+ bool setterAccessible = property.SetMethod == null || IsAccessibleForDecorator(property.SetMethod.DeclaredAccessibility);
+
+ if (!getterAccessible || !setterAccessible)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ InaccessibleMemberDescriptor,
+ member.Locations.FirstOrDefault() ?? Location.None,
+ member.Name));
+ // PKDEC004 is a warning, not an error - don't set hasErrors
+ continue;
+ }
+
+ // Skip inaccessible properties
+ if (!IsAccessibleForDecorator(property.DeclaredAccessibility))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ InaccessibleMemberDescriptor,
+ member.Locations.FirstOrDefault() ?? Location.None,
+ member.Name));
+ // PKDEC004 is a warning, not an error - don't set hasErrors
+ continue;
+ }
+
+ // Properties with init-only setters are not supported
+ // The decorator pattern is incompatible with init setters because
+ // you cannot assign to init-only properties after object construction
+ if (property.SetMethod?.IsInitOnly ?? false)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ UnsupportedMemberDescriptor,
+ member.Locations.FirstOrDefault() ?? Location.None,
+ property.Name,
+ "Init-only property"));
+ hasErrors = true;
+ continue;
+ }
+
+ var propInfo = new MemberInfo
+ {
+ Name = property.Name,
+ MemberType = MemberType.Property,
+ ReturnType = property.Type.ToDisplayString(TypeFormat),
+ HasGetter = property.GetMethod is not null,
+ HasSetter = property.SetMethod is not null,
+ IsInitOnly = property.SetMethod?.IsInitOnly ?? false,
+ IsAsync = false,
+ IsIgnored = isIgnored,
+ Accessibility = property.DeclaredAccessibility,
+ GetterAccessibility = property.GetMethod?.DeclaredAccessibility ?? property.DeclaredAccessibility,
+ SetterAccessibility = property.SetMethod?.DeclaredAccessibility ?? property.DeclaredAccessibility,
+ OriginalSymbol = property
+ };
+
+ members.Add(propInfo);
+ }
+ else if (member is IEventSymbol)
+ {
+ // Events not supported in v1
+ context.ReportDiagnostic(Diagnostic.Create(
+ UnsupportedMemberDescriptor,
+ member.Locations.FirstOrDefault() ?? Location.None,
+ member.Name,
+ "Event"));
+ hasErrors = true;
+ }
+ else if (member is IFieldSymbol fieldSymbol && IsAccessibleForDecorator(fieldSymbol.DeclaredAccessibility))
+ {
+ // Fields are not supported; only report for forwardable API-surface members
+ context.ReportDiagnostic(Diagnostic.Create(
+ UnsupportedMemberDescriptor,
+ member.Locations.FirstOrDefault() ?? Location.None,
+ member.Name,
+ member.Kind.ToString()));
+ hasErrors = true;
+ }
+ else if (member is INamedTypeSymbol typeSymbol && IsAccessibleForDecorator(typeSymbol.DeclaredAccessibility))
+ {
+ // Nested types are not supported; only report for forwardable API-surface members
+ context.ReportDiagnostic(Diagnostic.Create(
+ UnsupportedMemberDescriptor,
+ member.Locations.FirstOrDefault() ?? Location.None,
+ member.Name,
+ member.Kind.ToString()));
+ hasErrors = true;
+ }
+ }
+
+ // If any errors were reported, return empty list to skip generation
+ // This prevents generating incomplete decorator bases that won't compile
+ if (hasErrors)
+ {
+ return new List();
+ }
+
+ // Sort members for deterministic ordering by kind, name, and signature
+ return members.OrderBy(m => GetMemberSortKey(m), StringComparer.Ordinal).ToList();
+ }
+
+ private static string GetMemberSortKey(MemberInfo member)
+ {
+ // Create a stable sort key: kind + name + parameter signature
+ var sb = new StringBuilder();
+ sb.Append((int)member.MemberType); // 0 for Method, 1 for Property
+ sb.Append('_');
+ sb.Append(member.Name);
+
+ if (member.MemberType == MemberType.Method && member.Parameters.Count > 0)
+ {
+ sb.Append('(');
+ for (int i = 0; i < member.Parameters.Count; i++)
+ {
+ if (i > 0) sb.Append(',');
+ var param = member.Parameters[i];
+ sb.Append(param.RefKind switch
+ {
+ RefKind.Ref => "ref ",
+ RefKind.Out => "out ",
+ RefKind.In => "in ",
+ _ => ""
+ });
+ sb.Append(param.Type);
+ }
+ sb.Append(')');
+ }
+
+ return sb.ToString();
+ }
+
+ private IEnumerable GetAllInterfaceMembers(INamedTypeSymbol type)
+ {
+ var members = new List();
+ var seenSignatures = new HashSet();
+
+ void AddMember(ISymbol symbol)
+ {
+ // Create a signature key for deduplication
+ var signature = GetMemberSignature(symbol);
+ if (seenSignatures.Add(signature))
+ {
+ members.Add(symbol);
+ }
+ }
+
+ if (type.TypeKind == TypeKind.Interface)
+ {
+ // For interfaces, collect from this interface and all base interfaces
+ foreach (var member in type.GetMembers())
+ {
+ AddMember(member);
+ }
+ foreach (var baseInterface in type.AllInterfaces)
+ {
+ foreach (var member in baseInterface.GetMembers())
+ {
+ AddMember(member);
+ }
+ }
+ }
+ else if (type.TypeKind == TypeKind.Class && type.IsAbstract)
+ {
+ // For abstract classes, collect members from this type and all base types
+ // (we'll filter virtual/abstract later). We walk the BaseType chain to ensure
+ // inherited virtual/abstract members are also considered part of the contract.
+ INamedTypeSymbol? current = type;
+ while (current != null && current.SpecialType != SpecialType.System_Object)
+ {
+ foreach (var member in current.GetMembers())
+ {
+ AddMember(member);
+ }
+ current = current.BaseType;
+ }
+ }
+
+ return members;
+ }
+
+ private static string GetMemberSignature(ISymbol symbol)
+ {
+ var sb = new StringBuilder();
+ sb.Append(symbol.Kind);
+ sb.Append('_');
+ sb.Append(symbol.Name);
+
+ if (symbol is IMethodSymbol method)
+ {
+ sb.Append('(');
+ for (int i = 0; i < method.Parameters.Length; i++)
+ {
+ if (i > 0) sb.Append(',');
+ var param = method.Parameters[i];
+ sb.Append(param.RefKind switch
+ {
+ RefKind.Ref => "ref ",
+ RefKind.Out => "out ",
+ RefKind.In => "in ",
+ _ => ""
+ });
+ sb.Append(param.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
+ }
+ sb.Append(')');
+ sb.Append(':');
+ sb.Append(method.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
+ }
+ else if (symbol is IPropertySymbol property)
+ {
+ sb.Append(':');
+ sb.Append(property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
+ }
+ else if (symbol is IEventSymbol eventSymbol)
+ {
+ sb.Append(':');
+ sb.Append(eventSymbol.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
+ }
+
+ return sb.ToString();
+ }
+
+ private static bool IsAsyncMethod(IMethodSymbol method)
+ {
+ var returnType = method.ReturnType;
+ var typeName = returnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+
+ return typeName.StartsWith("global::System.Threading.Tasks.Task") ||
+ typeName.StartsWith("global::System.Threading.Tasks.ValueTask");
+ }
+
+ private static bool CanTypeAcceptNull(ITypeSymbol type)
+ {
+ if (type is null)
+ return false;
+
+ // All reference types can accept null
+ if (type.IsReferenceType)
+ return true;
+
+ // Nullable reference types / annotated types can accept null
+ if (type.NullableAnnotation == NullableAnnotation.Annotated)
+ return true;
+
+ // Type parameters without a value type constraint can accept null
+ if (type is ITypeParameterSymbol typeParam)
+ return !typeParam.HasValueTypeConstraint;
+
+ // Nullable value types can accept null
+ if (type is INamedTypeSymbol named &&
+ named.IsGenericType &&
+ named.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T)
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ private static string FormatDefaultValue(IParameterSymbol param)
+ {
+ if (param.ExplicitDefaultValue is null)
+ {
+ // Preserve 'null' for types that can accept null (including Nullable and unconstrained type parameters)
+ if (CanTypeAcceptNull(param.Type))
+ return "null";
+
+ // For non-nullable value types (structs), use 'default'
+ if (param.Type.IsValueType)
+ return "default";
+
+ // Fallback to 'null' for any remaining cases
+ return "null";
+ }
+
+ if (param.Type.TypeKind == TypeKind.Enum && param.Type is INamedTypeSymbol enumType)
+ {
+ // Try to resolve the enum field name corresponding to the default value
+ var enumField = enumType.GetMembers()
+ .OfType()
+ .FirstOrDefault(f => f.HasConstantValue && Equals(f.ConstantValue, param.ExplicitDefaultValue));
+
+ if (enumField != null)
+ {
+ return $"{enumType.ToDisplayString(TypeFormat)}.{enumField.Name}";
+ }
+
+ // Fallback: cast the numeric value
+ return $"({enumType.ToDisplayString(TypeFormat)}){param.ExplicitDefaultValue}";
+ }
+
+ // Use Roslyn's culture-invariant literal formatting for all other types
+ return SymbolDisplay.FormatPrimitive(param.ExplicitDefaultValue, quoteStrings: true, useHexadecimalNumbers: false);
+ }
+
+ private static bool HasAttribute(ISymbol symbol, string attributeName)
+ {
+ return symbol.GetAttributes().Any(a =>
+ a.AttributeClass?.ToDisplayString() == attributeName);
+ }
+
+ private static bool IsAccessibleForDecorator(Accessibility accessibility)
+ {
+ // Allow public/internal/protected internal
+ // Pure protected can't be forwarded because Inner.Member() isn't accessible through the base type reference
+ return accessibility == Accessibility.Public ||
+ accessibility == Accessibility.Internal ||
+ accessibility == Accessibility.ProtectedOrInternal;
+ }
+
+ private static string GetAccessibilityKeyword(Accessibility accessibility)
+ {
+ return accessibility switch
+ {
+ Accessibility.Public => "public",
+ Accessibility.Internal => "internal",
+ Accessibility.Protected => "protected",
+ Accessibility.ProtectedOrInternal => "protected internal",
+ Accessibility.ProtectedAndInternal => "private protected",
+ Accessibility.Private => "private",
+ _ => "public"
+ };
+ }
+
+ private static bool HasNameConflict(INamedTypeSymbol contractSymbol, string generatedName)
+ {
+ // Check if there's already a type with the generated name in the same namespace
+ var containingNamespace = contractSymbol.ContainingNamespace;
+ var existingTypes = containingNamespace.GetTypeMembers(generatedName);
+ return existingTypes.Length > 0;
+ }
+
+ private string GenerateBaseDecorator(ContractInfo contractInfo, DecoratorConfig 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(contractInfo.Namespace))
+ {
+ sb.AppendLine($"namespace {contractInfo.Namespace};");
+ sb.AppendLine();
+ }
+
+ // Generate base decorator class
+ var contractFullName = contractInfo.ContractSymbol.ToDisplayString(TypeFormat);
+ var accessibility = GetAccessibilityKeyword(contractInfo.ContractSymbol.DeclaredAccessibility);
+ sb.AppendLine($"/// Base decorator for {contractInfo.ContractName}. All members forward to Inner.");
+ sb.AppendLine($"{accessibility} abstract partial class {config.BaseTypeName} : {contractFullName}");
+ sb.AppendLine("{");
+
+ // Constructor
+ sb.AppendLine($" /// Initializes the decorator with an inner instance.");
+ sb.AppendLine($" protected {config.BaseTypeName}({contractFullName} inner)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" Inner = inner ?? throw new System.ArgumentNullException(nameof(inner));");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+
+ // Inner property
+ sb.AppendLine($" /// Gets the inner {contractInfo.ContractName} instance.");
+ sb.AppendLine($" protected {contractFullName} Inner {{ get; }}");
+ sb.AppendLine();
+
+ // Generate forwarding members
+ foreach (var member in contractInfo.Members)
+ {
+ if (member.MemberType == MemberType.Method)
+ {
+ GenerateForwardingMethod(sb, member, contractInfo, config);
+ }
+ else if (member.MemberType == MemberType.Property)
+ {
+ GenerateForwardingProperty(sb, member, contractInfo, config);
+ }
+ }
+
+ sb.AppendLine("}");
+
+ // Generate composition helpers if requested
+ if ((DecoratorCompositionMode)config.Composition == DecoratorCompositionMode.HelpersOnly)
+ {
+ sb.AppendLine();
+ GenerateCompositionHelpers(sb, contractInfo, config);
+ }
+
+ return sb.ToString();
+ }
+
+ private void GenerateForwardingMethod(StringBuilder sb, MemberInfo member, ContractInfo contractInfo, DecoratorConfig config)
+ {
+ // For async methods, use direct forwarding (return Inner.X()) instead of async/await
+ // to avoid unnecessary state machine allocation
+
+ // Determine the modifier keyword
+ string modifierKeyword = contractInfo.IsAbstractClass
+ // For abstract class contracts, always override the contract member.
+ // If the member is ignored, seal the override to prevent further overriding
+ // while still satisfying the abstract/virtual contract.
+ ? (member.IsIgnored ? "sealed override " : "override ")
+ // For non-abstract contracts (e.g., interfaces), only non-ignored members are virtual.
+ : (member.IsIgnored ? "" : "virtual ");
+
+ // Preserve the original member's accessibility to avoid widening on overrides
+ var accessibilityKeyword = GetAccessibilityKeyword(member.Accessibility);
+
+ sb.AppendLine($" /// Forwards to Inner.{member.Name}.");
+ sb.Append($" {accessibilityKeyword} {modifierKeyword}{member.ReturnType} {member.Name}(");
+
+ // Generate parameters
+ var paramList = string.Join(", ", member.Parameters.Select(p =>
+ {
+ var refKind = p.RefKind switch
+ {
+ RefKind.Ref => "ref ",
+ RefKind.Out => "out ",
+ RefKind.In => "in ",
+ _ => ""
+ };
+ // Preserve additional parameter modifiers (params, this)
+ var paramsModifier = p.IsParams ? "params " : "";
+ var thisModifier = p.IsThis ? "this " : "";
+ var defaultVal = p.HasDefaultValue ? $" = {p.DefaultValue}" : "";
+ return $"{thisModifier}{paramsModifier}{refKind}{p.Type} {p.Name}{defaultVal}";
+ }));
+
+ sb.Append(paramList);
+ sb.AppendLine(")");
+
+ // Generate method body - use direct forwarding for all methods including async
+ if (member.IsVoid)
+ {
+ sb.AppendLine(" {");
+ sb.Append($" Inner.{member.Name}(");
+ sb.Append(string.Join(", ", member.Parameters.Select(p =>
+ {
+ var refKind = p.RefKind switch
+ {
+ RefKind.Ref => "ref ",
+ RefKind.Out => "out ",
+ RefKind.In => "in ",
+ _ => ""
+ };
+ return $"{refKind}{p.Name}";
+ })));
+ sb.AppendLine(");");
+ sb.AppendLine(" }");
+ }
+ else
+ {
+ // For ref returns, we need to put "ref" before the invocation
+ var refModifier = member.ReturnsByRef || member.ReturnsByRefReadonly ? "ref " : "";
+ sb.Append($" => {refModifier}Inner.{member.Name}(");
+ sb.Append(string.Join(", ", member.Parameters.Select(p =>
+ {
+ var refKind = p.RefKind switch
+ {
+ RefKind.Ref => "ref ",
+ RefKind.Out => "out ",
+ RefKind.In => "in ",
+ _ => ""
+ };
+ return $"{refKind}{p.Name}";
+ })));
+ sb.AppendLine(");");
+ }
+
+ sb.AppendLine();
+ }
+
+ private void GenerateForwardingProperty(StringBuilder sb, MemberInfo member, ContractInfo contractInfo, DecoratorConfig config)
+ {
+ // Determine the modifier keyword
+ string modifierKeyword = contractInfo.IsAbstractClass
+ ? (member.IsIgnored ? "sealed override " : "override ")
+ : (member.IsIgnored ? "" : "virtual ");
+
+ // Preserve the original member's accessibility
+ var accessibilityKeyword = GetAccessibilityKeyword(member.Accessibility);
+
+ sb.AppendLine($" /// Forwards to Inner.{member.Name}.");
+ sb.Append($" {accessibilityKeyword} {modifierKeyword}{member.ReturnType} {member.Name}");
+
+ // Determine accessor-level modifiers
+ string getterModifier = "";
+ string setterModifier = "";
+
+ if (contractInfo.IsAbstractClass)
+ {
+ // For abstract classes, apply accessor modifiers when accessibility differs from property
+ if (member.HasGetter && member.GetterAccessibility != member.Accessibility)
+ {
+ getterModifier = GetAccessibilityKeyword(member.GetterAccessibility) + " ";
+ }
+ if (member.HasSetter && member.SetterAccessibility != member.Accessibility)
+ {
+ setterModifier = GetAccessibilityKeyword(member.SetterAccessibility) + " ";
+ }
+ }
+
+ if (member.HasGetter && member.HasSetter)
+ {
+ sb.AppendLine();
+ sb.AppendLine(" {");
+ sb.AppendLine($" {getterModifier}get => Inner.{member.Name};");
+ // Note: Init setters cannot be properly forwarded in decorators
+ // Always use 'set' for forwarding since we can't assign to init-only properties
+ sb.AppendLine($" {setterModifier}set => Inner.{member.Name} = value;");
+ sb.AppendLine(" }");
+ }
+ else if (member.HasGetter)
+ {
+ sb.AppendLine($" => Inner.{member.Name};");
+ }
+ else if (member.HasSetter)
+ {
+ sb.AppendLine();
+ sb.AppendLine(" {");
+ // Always use 'set' for forwarding
+ sb.AppendLine($" {setterModifier}set => Inner.{member.Name} = value;");
+ sb.AppendLine(" }");
+ }
+
+ sb.AppendLine();
+ }
+
+ private void GenerateCompositionHelpers(StringBuilder sb, ContractInfo contractInfo, DecoratorConfig config)
+ {
+ var contractFullName = contractInfo.ContractSymbol.ToDisplayString(TypeFormat);
+ var accessibility = GetAccessibilityKeyword(contractInfo.ContractSymbol.DeclaredAccessibility);
+
+ sb.AppendLine($"/// Composition helpers for {contractInfo.ContractName} decorators.");
+ sb.AppendLine($"{accessibility} static partial class {config.HelpersTypeName}");
+ sb.AppendLine("{");
+ sb.AppendLine($" /// ");
+ sb.AppendLine($" /// Composes multiple decorators in order.");
+ sb.AppendLine($" /// Decorators are applied in array order: decorators[0] is the outermost decorator,");
+ sb.AppendLine($" /// decorators[^1] is the innermost decorator (closest to the inner instance).");
+ sb.AppendLine($" /// ");
+ sb.AppendLine($" /// The inner instance to wrap.");
+ sb.AppendLine($" /// Factory functions that create decorators.");
+ sb.AppendLine($" /// The fully decorated instance.");
+ sb.AppendLine($" public static {contractFullName} Compose(");
+ sb.AppendLine($" {contractFullName} inner,");
+ sb.AppendLine($" params System.Func<{contractFullName}, {contractFullName}>[] decorators)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" if (inner == null) throw new System.ArgumentNullException(nameof(inner));");
+ sb.AppendLine(" if (decorators == null) throw new System.ArgumentNullException(nameof(decorators));");
+ sb.AppendLine();
+ sb.AppendLine(" var current = inner;");
+ sb.AppendLine(" // Apply decorators in reverse order so first decorator is outermost");
+ sb.AppendLine(" for (int i = decorators.Length - 1; i >= 0; i--)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" if (decorators[i] != null)");
+ sb.AppendLine(" current = decorators[i](current);");
+ sb.AppendLine(" }");
+ sb.AppendLine(" return current;");
+ sb.AppendLine(" }");
+ sb.AppendLine("}");
+ }
+
+ // Helper classes
+ private class DecoratorConfig
+ {
+ public string ContractName { get; set; } = "";
+ public string Namespace { get; set; } = "";
+ public string BaseTypeName { get; set; } = "";
+ public string HelpersTypeName { get; set; } = "";
+ public int Composition { get; set; } = 1; // HelpersOnly
+ // GenerateAsync and ForceAsync are reserved for future use - not stored in config
+ }
+
+ private class ContractInfo
+ {
+ public INamedTypeSymbol ContractSymbol { get; set; } = null!;
+ public string ContractName { get; set; } = "";
+ public string Namespace { get; set; } = "";
+ public bool IsInterface { get; set; }
+ public bool IsAbstractClass { get; set; }
+ public List Members { get; set; } = new();
+ public bool HasAsyncMembers { get; set; }
+ }
+
+ private class MemberInfo
+ {
+ public string Name { get; set; } = "";
+ public MemberType MemberType { get; set; }
+ public string ReturnType { get; set; } = "";
+ public bool IsAsync { get; set; }
+ public bool IsVoid { get; set; }
+ public bool IsIgnored { get; set; }
+ public List Parameters { get; set; } = new();
+ public bool HasGetter { get; set; }
+ public bool HasSetter { get; set; }
+ public bool IsInitOnly { get; set; }
+ public Accessibility Accessibility { get; set; } = Accessibility.Public;
+ public Accessibility GetterAccessibility { get; set; } = Accessibility.Public;
+ public Accessibility SetterAccessibility { get; set; } = Accessibility.Public;
+ public ISymbol? OriginalSymbol { get; set; }
+ public bool ReturnsByRef { get; set; }
+ public bool ReturnsByRefReadonly { get; set; }
+ }
+
+ private class ParameterInfo
+ {
+ public string Name { get; set; } = "";
+ public string Type { get; set; } = "";
+ public bool HasDefaultValue { get; set; }
+ public string? DefaultValue { get; set; }
+ public RefKind RefKind { get; set; }
+ public bool IsParams { get; set; }
+ public bool IsThis { get; set; }
+ }
+
+ private enum MemberType
+ {
+ Method,
+ Property
+ }
+}
diff --git a/test/PatternKit.Generators.Tests/DecoratorGeneratorTests.cs b/test/PatternKit.Generators.Tests/DecoratorGeneratorTests.cs
new file mode 100644
index 0000000..a3ed29a
--- /dev/null
+++ b/test/PatternKit.Generators.Tests/DecoratorGeneratorTests.cs
@@ -0,0 +1,1044 @@
+using Microsoft.CodeAnalysis;
+
+namespace PatternKit.Generators.Tests;
+
+public class DecoratorGeneratorTests
+{
+ [Fact]
+ public void GenerateDecoratorForInterface_BasicContract()
+ {
+ const string source = """
+ using PatternKit.Generators.Decorator;
+
+ namespace TestNamespace;
+
+ [GenerateDecorator]
+ public interface IStorage
+ {
+ string ReadFile(string path);
+ void WriteFile(string path, string content);
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateDecoratorForInterface_BasicContract));
+ var gen = new DecoratorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Decorator class is generated
+ var names = result.Results.SelectMany(r => r.GeneratedSources).Select(gs => gs.HintName).ToArray();
+ Assert.Contains("TestNamespace_IStorage.Decorator.g.cs", names);
+
+ // Verify generated content contains expected elements
+ var generatedSource = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .First(gs => gs.HintName == "TestNamespace_IStorage.Decorator.g.cs")
+ .SourceText.ToString();
+
+ Assert.True(generatedSource.Length > 100, $"Generated source is too short ({generatedSource.Length} chars): {generatedSource}");
+
+ // The Inner property will use fully qualified names
+ Assert.Contains("StorageDecoratorBase", generatedSource);
+ Assert.Contains("protected", generatedSource);
+ Assert.Contains(" Inner ", generatedSource);
+ Assert.Contains("public virtual", generatedSource);
+ Assert.Contains("ReadFile", generatedSource);
+ Assert.Contains("WriteFile", generatedSource);
+ Assert.Contains("StorageDecorators", generatedSource);
+ Assert.Contains("Compose", generatedSource);
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void GenerateDecoratorForInterface_WithAsyncMethods()
+ {
+ const string source = """
+ using PatternKit.Generators.Decorator;
+ using System.Threading;
+ using System.Threading.Tasks;
+
+ namespace TestNamespace;
+
+ [GenerateDecorator]
+ public interface IAsyncStorage
+ {
+ Task ReadFileAsync(string path, CancellationToken ct = default);
+ ValueTask WriteFileAsync(string path, string content, CancellationToken ct = default);
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateDecoratorForInterface_WithAsyncMethods));
+ var gen = new DecoratorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Verify generated content contains async method forwarding (direct forwarding without async/await)
+ var generatedSource = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .First(gs => gs.HintName == "TestNamespace_IAsyncStorage.Decorator.g.cs")
+ .SourceText.ToString();
+
+ Assert.Contains("public", generatedSource);
+ Assert.Contains("virtual", generatedSource);
+ Assert.Contains("ReadFileAsync", generatedSource);
+ Assert.Contains("=> Inner.ReadFileAsync", generatedSource);
+ Assert.Contains("WriteFileAsync", generatedSource);
+ Assert.Contains("=> Inner.WriteFileAsync", generatedSource);
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void GenerateDecoratorForInterface_WithProperties()
+ {
+ const string source = """
+ using PatternKit.Generators.Decorator;
+
+ namespace TestNamespace;
+
+ [GenerateDecorator]
+ public interface IConfiguration
+ {
+ string ApiKey { get; set; }
+ int Timeout { get; }
+ bool IsEnabled { get; set; }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateDecoratorForInterface_WithProperties));
+ var gen = new DecoratorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Verify generated content contains properties
+ var generatedSource = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .First(gs => gs.HintName == "TestNamespace_IConfiguration.Decorator.g.cs")
+ .SourceText.ToString();
+
+ Assert.Contains("public virtual string ApiKey", generatedSource);
+ Assert.Contains("get => Inner.ApiKey", generatedSource);
+ Assert.Contains("set => Inner.ApiKey = value", generatedSource);
+ Assert.Contains("public virtual int Timeout => Inner.Timeout", generatedSource);
+ Assert.Contains("public virtual bool IsEnabled", generatedSource);
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void GenerateDecoratorForInterface_WithDecoratorIgnore()
+ {
+ const string source = """
+ using PatternKit.Generators.Decorator;
+
+ namespace TestNamespace;
+
+ [GenerateDecorator]
+ public interface IRepository
+ {
+ void Save(string data);
+
+ [DecoratorIgnore]
+ void InternalMethod();
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateDecoratorForInterface_WithDecoratorIgnore));
+ var gen = new DecoratorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Verify generated content - ignored methods are still forwarded but not virtual
+ var generatedSource = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .First(gs => gs.HintName == "TestNamespace_IRepository.Decorator.g.cs")
+ .SourceText.ToString();
+
+ Assert.Contains("void Save", generatedSource);
+ Assert.Contains("virtual", generatedSource); // Save should be virtual
+ Assert.Contains("InternalMethod", generatedSource); // Still present, forwarded to Inner
+
+ // InternalMethod should be present and non-virtual (sealed)
+ // We can't easily check "public void InternalMethod" vs "public virtual void InternalMethod"
+ // so we'll just verify it compiles and InternalMethod exists
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void GenerateDecoratorForInterface_CustomNames()
+ {
+ const string source = """
+ using PatternKit.Generators.Decorator;
+
+ namespace TestNamespace;
+
+ [GenerateDecorator(BaseTypeName = "CustomStorageDecorator", HelpersTypeName = "CustomHelpers")]
+ public interface IStorage
+ {
+ string ReadFile(string path);
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateDecoratorForInterface_CustomNames));
+ var gen = new DecoratorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Verify custom names are used
+ var generatedSource = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .First(gs => gs.HintName == "TestNamespace_IStorage.Decorator.g.cs")
+ .SourceText.ToString();
+
+ Assert.Contains("class CustomStorageDecorator", generatedSource);
+ Assert.Contains("class CustomHelpers", generatedSource);
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void GenerateDecoratorForInterface_NoCompositionHelpers()
+ {
+ const string source = """
+ using PatternKit.Generators.Decorator;
+
+ namespace TestNamespace;
+
+ [GenerateDecorator(Composition = DecoratorCompositionMode.None)]
+ public interface IStorage
+ {
+ string ReadFile(string path);
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateDecoratorForInterface_NoCompositionHelpers));
+ var gen = new DecoratorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Verify composition helpers are NOT generated
+ var generatedSource = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .First(gs => gs.HintName == "TestNamespace_IStorage.Decorator.g.cs")
+ .SourceText.ToString();
+
+ Assert.Contains("class StorageDecoratorBase", generatedSource);
+ Assert.DoesNotContain("class StorageDecorators", generatedSource);
+ Assert.DoesNotContain("public static IStorage Compose", generatedSource);
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void GenerateDecoratorForAbstractClass_VirtualMembersOnly()
+ {
+ const string source = """
+ using PatternKit.Generators.Decorator;
+
+ namespace TestNamespace;
+
+ [GenerateDecorator]
+ public abstract partial class StorageBase
+ {
+ public abstract string ReadFile(string path);
+ public virtual void WriteFile(string path, string content) { }
+ public void NonVirtualMethod() { } // Should be excluded
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateDecoratorForAbstractClass_VirtualMembersOnly));
+ var gen = new DecoratorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Verify only virtual/abstract members are included
+ var generatedSource = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .First(gs => gs.HintName == "TestNamespace_StorageBase.Decorator.g.cs")
+ .SourceText.ToString();
+
+ // For abstract classes, methods use "override" not "virtual"
+ Assert.Contains("public override", generatedSource);
+ Assert.Contains("ReadFile", generatedSource);
+ Assert.Contains("WriteFile", generatedSource);
+ Assert.DoesNotContain("NonVirtualMethod", generatedSource);
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void GenerateDecoratorForInterface_WithDefaultParameters()
+ {
+ const string source = """
+ using PatternKit.Generators.Decorator;
+ using System.Threading;
+
+ namespace TestNamespace;
+
+ [GenerateDecorator]
+ public interface IStorage
+ {
+ string ReadFile(string path, int bufferSize = 4096);
+ void WriteFile(string path, string content, bool append = false);
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateDecoratorForInterface_WithDefaultParameters));
+ var gen = new DecoratorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Verify default parameters are preserved
+ var generatedSource = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .First(gs => gs.HintName == "TestNamespace_IStorage.Decorator.g.cs")
+ .SourceText.ToString();
+
+ Assert.Contains("int bufferSize = 4096", generatedSource);
+ Assert.Contains("bool append = false", generatedSource);
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void GenerateDecoratorForInterface_DeterministicOrdering()
+ {
+ const string source = """
+ using PatternKit.Generators.Decorator;
+
+ namespace TestNamespace;
+
+ [GenerateDecorator]
+ public interface IStorage
+ {
+ void Zebra();
+ void Apple();
+ void Mango();
+ void Banana();
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateDecoratorForInterface_DeterministicOrdering));
+ var gen = new DecoratorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Verify members are ordered alphabetically
+ var generatedSource = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .First(gs => gs.HintName == "TestNamespace_IStorage.Decorator.g.cs")
+ .SourceText.ToString();
+
+ var appleIndex = generatedSource.IndexOf("void Apple");
+ var bananaIndex = generatedSource.IndexOf("void Banana");
+ var mangoIndex = generatedSource.IndexOf("void Mango");
+ var zebraIndex = generatedSource.IndexOf("void Zebra");
+
+ Assert.True(appleIndex < bananaIndex);
+ Assert.True(bananaIndex < mangoIndex);
+ Assert.True(mangoIndex < zebraIndex);
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void GenerateDecoratorForInterface_WithRefParameters()
+ {
+ const string source = """
+ using PatternKit.Generators.Decorator;
+
+ namespace TestNamespace;
+
+ [GenerateDecorator]
+ public interface ICalculator
+ {
+ void Calculate(ref int value);
+ void TryParse(string input, out int result);
+ void Process(in int value);
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateDecoratorForInterface_WithRefParameters));
+ var gen = new DecoratorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Verify ref/out/in parameters are preserved
+ var generatedSource = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .First(gs => gs.HintName == "TestNamespace_ICalculator.Decorator.g.cs")
+ .SourceText.ToString();
+
+ Assert.Contains("ref int value", generatedSource);
+ Assert.Contains("out int result", generatedSource);
+ Assert.Contains("in int value", generatedSource);
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void GenerateDecoratorForInterface_ComplexExample()
+ {
+ const string source = """
+ using PatternKit.Generators.Decorator;
+ using System.IO;
+ using System.Threading;
+ using System.Threading.Tasks;
+
+ namespace TestNamespace;
+
+ [GenerateDecorator]
+ public interface IStorage
+ {
+ Stream OpenRead(string path);
+ ValueTask OpenReadAsync(string path, CancellationToken ct = default);
+ void Write(string path, byte[] data);
+ Task WriteAsync(string path, byte[] data, CancellationToken ct = default);
+ bool Exists(string path);
+ void Delete(string path);
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateDecoratorForInterface_ComplexExample));
+ var gen = new DecoratorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Verify all methods are generated correctly
+ var generatedSource = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .First(gs => gs.HintName == "TestNamespace_IStorage.Decorator.g.cs")
+ .SourceText.ToString();
+
+ Assert.Contains("OpenRead", generatedSource);
+ Assert.Contains("OpenReadAsync", generatedSource);
+ Assert.Contains("Write", generatedSource);
+ Assert.Contains("WriteAsync", generatedSource);
+ Assert.Contains("Exists", generatedSource);
+ Assert.Contains("Delete", generatedSource);
+ Assert.Contains("virtual", generatedSource);
+ // Async methods use direct forwarding (no async/await keywords)
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void GenerateDecoratorForInterface_InheritedMembers()
+ {
+ const string source = """
+ using PatternKit.Generators.Decorator;
+
+ namespace TestNamespace;
+
+ public interface IReadable
+ {
+ string Read();
+ }
+
+ [GenerateDecorator]
+ public interface IStorage : IReadable
+ {
+ void Write(string data);
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateDecoratorForInterface_InheritedMembers));
+ var gen = new DecoratorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Verify inherited members are included
+ var generatedSource = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .First(gs => gs.HintName == "TestNamespace_IStorage.Decorator.g.cs")
+ .SourceText.ToString();
+
+ Assert.Contains("public virtual string Read", generatedSource);
+ Assert.Contains("public virtual void Write", generatedSource);
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void DecoratorComposition_AppliesInCorrectOrder()
+ {
+ const string source = """
+ using PatternKit.Generators.Decorator;
+
+ namespace TestNamespace;
+
+ [GenerateDecorator]
+ public interface IService
+ {
+ string Execute(string input);
+ }
+
+ public class LoggingDecorator : ServiceDecoratorBase
+ {
+ public LoggingDecorator(IService inner) : base(inner) { }
+
+ public override string Execute(string input)
+ {
+ return "Logging: " + base.Execute(input);
+ }
+ }
+
+ public class CachingDecorator : ServiceDecoratorBase
+ {
+ public CachingDecorator(IService inner) : base(inner) { }
+
+ public override string Execute(string input)
+ {
+ return "Caching: " + base.Execute(input);
+ }
+ }
+
+ public class BaseService : IService
+ {
+ public string Execute(string input) => "Base: " + input;
+ }
+
+ public class TestRunner
+ {
+ public static string Test()
+ {
+ var service = new BaseService();
+ var decorated = ServiceDecorators.Compose(
+ service,
+ s => new LoggingDecorator(s),
+ s => new CachingDecorator(s)
+ );
+ return decorated.Execute("test");
+ }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(DecoratorComposition_AppliesInCorrectOrder));
+ var gen = new DecoratorGenerator();
+ _ = 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));
+ }
+
+ [Fact]
+ public void Diagnostic_PKDEC001_UnsupportedTargetType()
+ {
+ const string source = """
+ using PatternKit.Generators.Decorator;
+
+ namespace TestNamespace;
+
+ [GenerateDecorator]
+ public class ConcreteClass // Not abstract
+ {
+ public void Method() { }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(Diagnostic_PKDEC001_UnsupportedTargetType));
+ var gen = new DecoratorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // Should have PKDEC001 diagnostic
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKDEC001");
+ Assert.Contains(diagnostics, d => d.GetMessage().Contains("ConcreteClass"));
+ }
+
+ [Fact]
+ public void Diagnostic_PKDEC002_UnsupportedMemberKind_Event()
+ {
+ const string source = """
+ using PatternKit.Generators.Decorator;
+ using System;
+
+ namespace TestNamespace;
+
+ [GenerateDecorator]
+ public interface IWithEvent
+ {
+ event EventHandler Changed;
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(Diagnostic_PKDEC002_UnsupportedMemberKind_Event));
+ var gen = new DecoratorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // Should have PKDEC002 diagnostic for the event
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKDEC002" && d.GetMessage().Contains("Changed"));
+
+ // Generation should be skipped when PKDEC002 (error) is reported
+ var generatedSources = result.Results.SelectMany(r => r.GeneratedSources).ToArray();
+ Assert.Empty(generatedSources);
+ }
+
+ [Fact]
+ public void Diagnostic_PKDEC003_NameConflict_BaseType()
+ {
+ const string source = """
+ using PatternKit.Generators.Decorator;
+
+ namespace TestNamespace;
+
+ [GenerateDecorator]
+ public interface IService
+ {
+ void Execute();
+ }
+
+ // This conflicts with the generated name
+ public class ServiceDecoratorBase
+ {
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(Diagnostic_PKDEC003_NameConflict_BaseType));
+ var gen = new DecoratorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // Should have PKDEC003 diagnostic
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKDEC003" && d.GetMessage().Contains("ServiceDecoratorBase"));
+ }
+
+ [Fact]
+ public void Diagnostic_PKDEC003_NameConflict_HelpersType()
+ {
+ const string source = """
+ using PatternKit.Generators.Decorator;
+
+ namespace TestNamespace;
+
+ [GenerateDecorator]
+ public interface IService
+ {
+ void Execute();
+ }
+
+ // This conflicts with the generated helpers name
+ public static class ServiceDecorators
+ {
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(Diagnostic_PKDEC003_NameConflict_HelpersType));
+ var gen = new DecoratorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // Should have PKDEC003 diagnostic
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKDEC003" && d.GetMessage().Contains("ServiceDecorators"));
+ }
+
+ [Fact]
+ public void Diagnostic_PKDEC002_Indexer()
+ {
+ const string source = """
+ using PatternKit.Generators.Decorator;
+
+ namespace TestNamespace;
+
+ [GenerateDecorator]
+ public interface IIndexable
+ {
+ string this[int index] { get; set; }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(Diagnostic_PKDEC002_Indexer));
+ var gen = new DecoratorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // Should have PKDEC002 diagnostic for the indexer
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKDEC002" && d.GetMessage().Contains("Indexer"));
+
+ // Generation should be skipped when PKDEC002 (error) is reported
+ var generatedSources = result.Results.SelectMany(r => r.GeneratedSources).ToArray();
+ Assert.Empty(generatedSources);
+ }
+
+ [Fact]
+ public void Diagnostic_PKDEC005_GenericContract()
+ {
+ const string source = """
+ using PatternKit.Generators.Decorator;
+
+ namespace TestNamespace;
+
+ [GenerateDecorator]
+ public interface IGenericService
+ {
+ T Process(T input);
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(Diagnostic_PKDEC005_GenericContract));
+ var gen = new DecoratorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // Should have PKDEC005 diagnostic for the generic contract
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKDEC005" && d.GetMessage().Contains("IGenericService"));
+ }
+
+ [Fact]
+ public void Diagnostic_PKDEC002_InitOnlyProperty()
+ {
+ const string source = """
+ using PatternKit.Generators.Decorator;
+
+ namespace TestNamespace;
+
+ [GenerateDecorator]
+ public interface IConfig
+ {
+ string Name { get; init; }
+ int Value { get; set; }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(Diagnostic_PKDEC002_InitOnlyProperty));
+ var gen = new DecoratorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // Should have PKDEC002 diagnostic for the init-only property
+ // Init setters are incompatible with the decorator pattern
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKDEC002" && d.GetMessage().Contains("Name"));
+
+ // Generation should be skipped when PKDEC002 (error) is reported
+ var generatedSources = result.Results.SelectMany(r => r.GeneratedSources).ToArray();
+ Assert.Empty(generatedSources);
+ }
+
+ [Fact]
+ public void GenerateDecoratorForAbstractClass_WithInternalProtectedMembers()
+ {
+ const string source = """
+ using PatternKit.Generators.Decorator;
+
+ namespace TestNamespace;
+
+ [GenerateDecorator]
+ public abstract class ServiceBase
+ {
+ public abstract void PublicMethod();
+ protected internal abstract void ProtectedInternalMethod();
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateDecoratorForAbstractClass_WithInternalProtectedMembers));
+ var gen = new DecoratorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Verify generated content contains correct accessibility
+ var generatedSource = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .First(gs => gs.HintName == "TestNamespace_ServiceBase.Decorator.g.cs")
+ .SourceText.ToString();
+
+ Assert.Contains("public override", generatedSource);
+ Assert.Contains("PublicMethod", generatedSource);
+ Assert.Contains("protected internal override", generatedSource);
+ Assert.Contains("ProtectedInternalMethod", generatedSource);
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void Diagnostic_PKDEC002_GenericMethod()
+ {
+ const string source = """
+ using PatternKit.Generators.Decorator;
+
+ namespace TestNamespace;
+
+ [GenerateDecorator]
+ public interface IGenericService
+ {
+ T Process(T input);
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(Diagnostic_PKDEC002_GenericMethod));
+ var gen = new DecoratorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // Should have PKDEC002 diagnostic for the generic method
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKDEC002" && d.GetMessage().Contains("Generic method"));
+
+ // Generation should be skipped when PKDEC002 (error) is reported
+ var generatedSources = result.Results.SelectMany(r => r.GeneratedSources).ToArray();
+ Assert.Empty(generatedSources);
+ }
+
+ [Fact]
+ public void GenerateDecoratorForInterface_IgnoresStaticMembers()
+ {
+ const string source = """
+ using PatternKit.Generators.Decorator;
+
+ namespace TestNamespace;
+
+ [GenerateDecorator]
+ public interface IService
+ {
+ void InstanceMethod();
+ static void StaticMethod() { }
+ string InstanceProperty { get; set; }
+ static string StaticProperty { get; set; }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateDecoratorForInterface_IgnoresStaticMembers));
+ var gen = new DecoratorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics for static members
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Verify generated content contains only instance members
+ var generatedSource = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .First(gs => gs.HintName == "TestNamespace_IService.Decorator.g.cs")
+ .SourceText.ToString();
+
+ Assert.Contains("InstanceMethod", generatedSource);
+ Assert.Contains("InstanceProperty", generatedSource);
+ Assert.DoesNotContain("StaticMethod", generatedSource);
+ Assert.DoesNotContain("StaticProperty", generatedSource);
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void Diagnostic_PKDEC004_PropertyWithProtectedSetter()
+ {
+ const string source = """
+ using PatternKit.Generators.Decorator;
+
+ namespace TestNamespace;
+
+ [GenerateDecorator]
+ public abstract class ServiceBase
+ {
+ public abstract string Name { get; protected set; }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(Diagnostic_PKDEC004_PropertyWithProtectedSetter));
+ var gen = new DecoratorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // Should have PKDEC004 diagnostic for the property with protected setter
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKDEC004" && d.GetMessage().Contains("Name"));
+ }
+
+ [Fact]
+ public void GenerateDecoratorForInterface_SupportsParamsModifier()
+ {
+ const string source = """
+ using PatternKit.Generators.Decorator;
+
+ namespace TestNamespace;
+
+ [GenerateDecorator]
+ public interface IService
+ {
+ void ProcessMany(params string[] items);
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateDecoratorForInterface_SupportsParamsModifier));
+ var gen = new DecoratorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Verify generated content preserves params modifier
+ var generatedSource = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .First(gs => gs.HintName == "TestNamespace_IService.Decorator.g.cs")
+ .SourceText.ToString();
+
+ Assert.Contains("params", generatedSource);
+ Assert.Contains("params string[] items", generatedSource);
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void GenerateDecoratorForAbstractClass_InheritsVirtualMembers()
+ {
+ const string source = """
+ using PatternKit.Generators.Decorator;
+
+ namespace TestNamespace;
+
+ public abstract class BaseClass
+ {
+ public abstract void BaseMethod();
+ }
+
+ [GenerateDecorator]
+ public abstract class DerivedClass : BaseClass
+ {
+ public abstract void DerivedMethod();
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateDecoratorForAbstractClass_InheritsVirtualMembers));
+ var gen = new DecoratorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Verify generated content includes both base and derived methods
+ var generatedSource = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .First(gs => gs.HintName == "TestNamespace_DerivedClass.Decorator.g.cs")
+ .SourceText.ToString();
+
+ Assert.Contains("BaseMethod", generatedSource);
+ Assert.Contains("DerivedMethod", generatedSource);
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void Diagnostic_PKDEC006_NestedType()
+ {
+ const string source = """
+ using PatternKit.Generators.Decorator;
+
+ namespace TestNamespace;
+
+ public class OuterClass
+ {
+ [GenerateDecorator]
+ public interface INestedService
+ {
+ void DoWork();
+ }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(Diagnostic_PKDEC006_NestedType));
+ var gen = new DecoratorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // Should have PKDEC006 diagnostic for nested type
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKDEC006");
+ }
+
+ [Fact]
+ public void GenerateDecoratorForInterface_SupportsRefReturns()
+ {
+ const string source = """
+ using PatternKit.Generators.Decorator;
+
+ namespace TestNamespace;
+
+ [GenerateDecorator]
+ public interface IRefService
+ {
+ ref int GetRef();
+ ref readonly int GetRefReadonly();
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateDecoratorForInterface_SupportsRefReturns));
+ var gen = new DecoratorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Verify generated content includes ref/ref readonly modifiers
+ var generatedSource = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .First(gs => gs.HintName == "TestNamespace_IRefService.Decorator.g.cs")
+ .SourceText.ToString();
+
+ Assert.Contains("ref int GetRef()", generatedSource);
+ Assert.Contains("ref readonly int GetRefReadonly()", generatedSource);
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+}