From 42179be5fb0f5aef54b589972cc1c77d536db46c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 02:48:09 +0000 Subject: [PATCH 1/2] Initial plan From 48738074fe1fb64bb6efe655b80764322edd35be Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 02:58:10 +0000 Subject: [PATCH 2/2] fix(adapter): Address PR review feedback - diagnostics, validation, and docs Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com> --- docs/generators/adapter.md | 41 ++++++++ .../Adapter/AdapterGenerator.cs | 99 ++++++++++++++++++- .../AnalyzerReleases.Unshipped.md | 4 + .../AdapterGeneratorTests.cs | 4 +- 4 files changed, 142 insertions(+), 6 deletions(-) diff --git a/docs/generators/adapter.md b/docs/generators/adapter.md index 9b68ebe..7b5980a 100644 --- a/docs/generators/adapter.md +++ b/docs/generators/adapter.md @@ -239,6 +239,47 @@ public static partial class Adapters | **PKADP006** | Error | Adapter type name conflicts with existing type | | **PKADP007** | Error | Adaptee must be a concrete class or struct | | **PKADP008** | Error | Mapping method must be static | +| **PKADP009** | Error | Events are not supported | +| **PKADP010** | Error | Generic methods are not supported | +| **PKADP011** | Error | Overloaded methods are not supported | +| **PKADP012** | Error | Abstract class target requires accessible parameterless constructor | +| **PKADP013** | Error | Settable properties are not supported | +| **PKADP014** | Error | Nested or generic host not supported | +| **PKADP015** | Error | Mapping method must be accessible (public or internal) | +| **PKADP016** | Error | Static members are not supported | +| **PKADP017** | Error | Ref-return members are not supported | + +## Limitations + +### Multiple Adapters with Shared Adaptee + +When defining multiple `[GenerateAdapter]` attributes within the same host class that share the same adaptee type, mapping ambiguity can occur. The generator matches `[AdapterMap]` methods to adapters solely by adaptee type and then by `TargetMember` name. If two target types have overlapping member names (both use `nameof(...)` resulting in the same string), mappings become ambiguous and may trigger false `PKADP004` duplicate mapping diagnostics. + +**Workaround:** Define separate host classes for each adapter when they share the same adaptee type: + +```csharp +// ✅ Good: Separate hosts avoid ambiguity +[GenerateAdapter(Target = typeof(IServiceA), Adaptee = typeof(LegacyService))] +public static partial class ServiceAAdapters +{ + [AdapterMap(TargetMember = nameof(IServiceA.DoWork))] + public static void MapDoWork(LegacyService adaptee) => adaptee.Execute(); +} + +[GenerateAdapter(Target = typeof(IServiceB), Adaptee = typeof(LegacyService))] +public static partial class ServiceBAdapters +{ + [AdapterMap(TargetMember = nameof(IServiceB.DoWork))] + public static void MapDoWork(LegacyService adaptee) => adaptee.Run(); +} + +// ⚠️ Problematic: Multiple adapters with same adaptee in one host +public static partial class AllAdapters +{ + // Both IServiceA and IServiceB have DoWork() members + // The generator cannot distinguish which mapping is for which target +} +``` ## Best Practices diff --git a/src/PatternKit.Generators/Adapter/AdapterGenerator.cs b/src/PatternKit.Generators/Adapter/AdapterGenerator.cs index 97bd340..6cf28b1 100644 --- a/src/PatternKit.Generators/Adapter/AdapterGenerator.cs +++ b/src/PatternKit.Generators/Adapter/AdapterGenerator.cs @@ -1,7 +1,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using System.Collections.Immutable; using System.Text; namespace PatternKit.Generators.Adapter; @@ -35,6 +34,10 @@ public sealed class AdapterGenerator : IIncrementalGenerator private const string DiagIdOverloadedMethodsNotSupported = "PKADP011"; private const string DiagIdAbstractClassNoParameterlessCtor = "PKADP012"; private const string DiagIdSettablePropertiesNotSupported = "PKADP013"; + private const string DiagIdNestedOrGenericHost = "PKADP014"; + private const string DiagIdMappingMethodNotAccessible = "PKADP015"; + private const string DiagIdStaticMembersNotSupported = "PKADP016"; + private const string DiagIdRefReturnNotSupported = "PKADP017"; private static readonly DiagnosticDescriptor HostNotStaticPartialDescriptor = new( id: DiagIdHostNotStaticPartial, @@ -140,6 +143,38 @@ public sealed class AdapterGenerator : IIncrementalGenerator defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true); + private static readonly DiagnosticDescriptor NestedOrGenericHostDescriptor = new( + id: DiagIdNestedOrGenericHost, + title: "Nested or generic host not supported", + messageFormat: "Adapter host '{0}' cannot be nested or generic", + category: "PatternKit.Generators.Adapter", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor MappingMethodNotAccessibleDescriptor = new( + id: DiagIdMappingMethodNotAccessible, + title: "Mapping method must be accessible", + messageFormat: "Mapping method '{0}' must be public or internal to be accessible from generated adapter", + category: "PatternKit.Generators.Adapter", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor StaticMembersNotSupportedDescriptor = new( + id: DiagIdStaticMembersNotSupported, + title: "Static members are not supported", + messageFormat: "Target type '{0}' contains static member '{1}' which is not supported by the adapter generator", + category: "PatternKit.Generators.Adapter", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor RefReturnNotSupportedDescriptor = new( + id: DiagIdRefReturnNotSupported, + title: "Ref-return members are not supported", + messageFormat: "Target type '{0}' contains ref-return member '{1}' which is not supported by the adapter generator", + category: "PatternKit.Generators.Adapter", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + public void Initialize(IncrementalGeneratorInitializationContext context) { // Find all class declarations with [GenerateAdapter] attribute @@ -183,6 +218,16 @@ private void GenerateAdapterForAttribute( return; } + // Validate host is not nested or generic + if (hostSymbol.ContainingType is not null || hostSymbol.TypeParameters.Length > 0) + { + context.ReportDiagnostic(Diagnostic.Create( + NestedOrGenericHostDescriptor, + node.GetLocation(), + hostSymbol.Name)); + return; + } + // Parse attribute arguments var config = ParseAdapterConfig(attribute); if (config.TargetType is null || config.AdapteeType is null) @@ -240,7 +285,7 @@ private void GenerateAdapterForAttribute( // Get all mapping methods from host var mappingMethods = GetMappingMethods(hostSymbol, config.AdapteeType); - // Validate mapping methods are static + // Validate mapping methods are static and accessible foreach (var (method, _) in mappingMethods) { if (!method.IsStatic) @@ -251,6 +296,17 @@ private void GenerateAdapterForAttribute( method.Name)); return; } + + // Validate method is accessible (public or internal) + if (method.DeclaredAccessibility != Accessibility.Public && + method.DeclaredAccessibility != Accessibility.Internal) + { + context.ReportDiagnostic(Diagnostic.Create( + MappingMethodNotAccessibleDescriptor, + method.Locations.FirstOrDefault() ?? node.GetLocation(), + method.Name)); + return; + } } // Get target members that need mapping @@ -403,6 +459,16 @@ private List ValidateTargetMembers(INamedTypeSymbol targetType, Loca foreach (var member in membersToCheck) { + // Check for static members (not supported) + if (member.IsStatic) + { + diagnostics.Add(Diagnostic.Create( + StaticMembersNotSupportedDescriptor, + location, + targetType.Name, + member.Name)); + } + // Check for events (not supported) if (member is IEventSymbol evt) { @@ -423,6 +489,16 @@ private List ValidateTargetMembers(INamedTypeSymbol targetType, Loca prop.Name)); } + // Check for ref-return properties (not supported) + if (member is IPropertySymbol refProp && refProp.ReturnsByRef) + { + diagnostics.Add(Diagnostic.Create( + RefReturnNotSupportedDescriptor, + location, + targetType.Name, + refProp.Name)); + } + // Check for generic methods (not supported) if (member is IMethodSymbol method && method.MethodKind == MethodKind.Ordinary) { @@ -435,6 +511,16 @@ private List ValidateTargetMembers(INamedTypeSymbol targetType, Loca method.Name)); } + // Check for ref-return methods (not supported) + if (method.ReturnsByRef || method.ReturnsByRefReadonly) + { + diagnostics.Add(Diagnostic.Create( + RefReturnNotSupportedDescriptor, + location, + targetType.Name, + method.Name)); + } + // Track full method signature for overload detection var sig = GetMemberSignature(method); if (!methodSignatures.TryGetValue(method.Name, out var sigs)) @@ -557,6 +643,7 @@ private static List GetTargetMembers(INamedTypeSymbol targetType) continue; var membersToProcess = type.GetMembers() + .Where(m => !m.IsStatic) // Exclude static members .Where(m => !isAbstractClass || m.IsAbstract); foreach (var member in membersToProcess) @@ -592,8 +679,12 @@ private static List GetTargetMembers(INamedTypeSymbol targetType) } } - // Return in declaration order (members already added in traversal order) - return members; + // Ensure stable, deterministic ordering by kind+name+signature + // This provides a predictable output order even if member traversal is non-deterministic + return members.OrderBy(m => m.Kind) + .ThenBy(m => m.Name) + .ThenBy(m => m.ToDisplayString(FullyQualifiedFormat)) + .ToList(); } private static string GetMemberSignature(IMethodSymbol method) diff --git a/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md b/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md index 558eed1..947541b 100644 --- a/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md +++ b/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md @@ -111,3 +111,7 @@ PKADP010 | PatternKit.Generators.Adapter | Error | Generic methods are not suppo PKADP011 | PatternKit.Generators.Adapter | Error | Overloaded methods are not supported PKADP012 | PatternKit.Generators.Adapter | Error | Abstract class target requires accessible parameterless constructor PKADP013 | PatternKit.Generators.Adapter | Error | Settable properties are not supported +PKADP014 | PatternKit.Generators.Adapter | Error | Nested or generic host not supported +PKADP015 | PatternKit.Generators.Adapter | Error | Mapping method must be accessible +PKADP016 | PatternKit.Generators.Adapter | Error | Static members are not supported +PKADP017 | PatternKit.Generators.Adapter | Error | Ref-return members are not supported diff --git a/test/PatternKit.Generators.Tests/AdapterGeneratorTests.cs b/test/PatternKit.Generators.Tests/AdapterGeneratorTests.cs index 31a45ae..c8b0aa9 100644 --- a/test/PatternKit.Generators.Tests/AdapterGeneratorTests.cs +++ b/test/PatternKit.Generators.Tests/AdapterGeneratorTests.cs @@ -184,7 +184,7 @@ public class NotStaticPartial } [Fact] - public void ErrorWhenTargetNotInterface() + public void ErrorWhenTargetNotInterfaceOrAbstract() { const string source = """ using PatternKit.Generators.Adapter; @@ -202,7 +202,7 @@ public class LegacyClock { } public static partial class Adapters { } """; - var comp = RoslynTestHelpers.CreateCompilation(source, nameof(ErrorWhenTargetNotInterface)); + var comp = RoslynTestHelpers.CreateCompilation(source, nameof(ErrorWhenTargetNotInterfaceOrAbstract)); var gen = new AdapterGenerator(); _ = RoslynTestHelpers.Run(comp, gen, out var result, out _);