From ad9db8efc51d2fff3fc7f19b954f80c54540ccd2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 22 Jan 2026 02:35:20 +0000
Subject: [PATCH 1/8] Initial plan
From 4a5922cb5c9110cb04c4cee493fe2e9dd7044769 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 22 Jan 2026 02:44:25 +0000
Subject: [PATCH 2/8] feat: Add interface and struct support to Visitor
generator
- Support interface-based hierarchies (e.g., IShape)
- Support struct types that implement visitor interfaces
- Intelligent visitor interface naming (IShape -> IShapeVisitor, not IIShapeVisitor)
- Enhanced type discovery to find classes, structs, records, and interfaces
- Add comprehensive tests for interface and struct hierarchies
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
.../Visitors/VisitorAttributes.cs | 11 +-
src/PatternKit.Generators/VisitorGenerator.cs | 100 ++++++-
.../VisitorGeneratorTests.cs | 249 ++++++++++++++++++
3 files changed, 345 insertions(+), 15 deletions(-)
diff --git a/src/PatternKit.Generators.Abstractions/Visitors/VisitorAttributes.cs b/src/PatternKit.Generators.Abstractions/Visitors/VisitorAttributes.cs
index 522987a..20bb1b4 100644
--- a/src/PatternKit.Generators.Abstractions/Visitors/VisitorAttributes.cs
+++ b/src/PatternKit.Generators.Abstractions/Visitors/VisitorAttributes.cs
@@ -12,6 +12,7 @@ namespace PatternKit.Generators.Visitors;
/// - Fluent builder APIs for composing visitors
///
///
+/// Class-based hierarchy:
///
/// [GenerateVisitor]
/// public partial class AstNode { }
@@ -19,9 +20,17 @@ namespace PatternKit.Generators.Visitors;
/// public partial class Expression : AstNode { }
/// public partial class Statement : AstNode { }
///
+/// Interface-based hierarchy:
+///
+/// [GenerateVisitor]
+/// public partial interface IShape { }
+///
+/// public partial class Circle : IShape { }
+/// public partial class Rectangle : IShape { }
+///
///
///
-[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
public sealed class GenerateVisitorAttribute : Attribute
{
///
diff --git a/src/PatternKit.Generators/VisitorGenerator.cs b/src/PatternKit.Generators/VisitorGenerator.cs
index 470b5ca..eb1f040 100644
--- a/src/PatternKit.Generators/VisitorGenerator.cs
+++ b/src/PatternKit.Generators/VisitorGenerator.cs
@@ -15,10 +15,13 @@ public sealed class VisitorGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
- // Find all classes marked with [GenerateVisitor]
+ // Find all types (classes, interfaces, structs, records) marked with [GenerateVisitor]
var visitorRoots = context.SyntaxProvider.ForAttributeWithMetadataName(
fullyQualifiedMetadataName: "PatternKit.Generators.Visitors.GenerateVisitorAttribute",
- predicate: static (node, _) => node is ClassDeclarationSyntax,
+ predicate: static (node, _) => node is ClassDeclarationSyntax
+ or InterfaceDeclarationSyntax
+ or StructDeclarationSyntax
+ or RecordDeclarationSyntax,
transform: static (gasc, ct) => GetVisitorRoot(gasc, ct)
).Where(static x => x is not null);
@@ -52,6 +55,20 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
var baseName = baseType.Name;
var baseFullName = baseType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+
+ // Generate visitor interface name intelligently
+ // If base name starts with "I" and is an interface, don't add another "I"
+ string defaultVisitorName;
+ if (baseType.TypeKind == TypeKind.Interface && baseName.StartsWith("I") && baseName.Length > 1 && char.IsUpper(baseName[1]))
+ {
+ // Interface name like "IShape" -> "IShapeVisitor"
+ defaultVisitorName = $"{baseName}Visitor";
+ }
+ else
+ {
+ // Class name like "Shape" -> "IShapeVisitor"
+ defaultVisitorName = $"I{baseName}Visitor";
+ }
// Discover derived types in the same assembly
var derivedTypes = autoDiscover
@@ -63,7 +80,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
BaseName: baseName,
BaseFullName: baseFullName,
BaseType: baseType,
- VisitorInterfaceName: visitorInterfaceName ?? $"I{baseName}Visitor",
+ VisitorInterfaceName: visitorInterfaceName ?? defaultVisitorName,
GenerateAsync: generateAsync,
GenerateActions: generateActions,
DerivedTypes: derivedTypes
@@ -89,27 +106,74 @@ private static ImmutableArray DiscoverDerivedTypes(
var semanticModel = compilation.GetSemanticModel(tree);
var root = tree.GetRoot();
+ // Discover classes
foreach (var classDecl in root.DescendantNodes().OfType())
{
var symbol = semanticModel.GetDeclaredSymbol(classDecl) as INamedTypeSymbol;
- if (symbol is null) continue;
+ if (symbol is null || SymbolEqualityComparer.Default.Equals(symbol, baseType)) continue;
+
+ if (IsDerivedFrom(symbol, baseType))
+ {
+ derived.Add(symbol);
+ }
+ }
+
+ // Discover structs
+ foreach (var structDecl in root.DescendantNodes().OfType())
+ {
+ var symbol = semanticModel.GetDeclaredSymbol(structDecl) as INamedTypeSymbol;
+ if (symbol is null || SymbolEqualityComparer.Default.Equals(symbol, baseType)) continue;
+
+ if (ImplementsInterface(symbol, baseType))
+ {
+ derived.Add(symbol);
+ }
+ }
+
+ // Discover records
+ foreach (var recordDecl in root.DescendantNodes().OfType())
+ {
+ var symbol = semanticModel.GetDeclaredSymbol(recordDecl) as INamedTypeSymbol;
+ if (symbol is null || SymbolEqualityComparer.Default.Equals(symbol, baseType)) continue;
- // Check if this type derives from baseType
- var current = symbol.BaseType;
- while (current is not null)
+ if (IsDerivedFrom(symbol, baseType))
{
- if (SymbolEqualityComparer.Default.Equals(current, baseType))
- {
- derived.Add(symbol);
- break;
- }
- current = current.BaseType;
+ derived.Add(symbol);
}
}
}
return derived.ToImmutableArray();
}
+
+ private static bool IsDerivedFrom(INamedTypeSymbol type, INamedTypeSymbol baseType)
+ {
+ // Check class inheritance
+ var current = type.BaseType;
+ while (current is not null)
+ {
+ if (SymbolEqualityComparer.Default.Equals(current, baseType))
+ return true;
+ current = current.BaseType;
+ }
+
+ // Check interface implementation
+ return ImplementsInterface(type, baseType);
+ }
+
+ private static bool ImplementsInterface(INamedTypeSymbol type, INamedTypeSymbol interfaceType)
+ {
+ if (interfaceType.TypeKind != TypeKind.Interface)
+ return false;
+
+ foreach (var iface in type.AllInterfaces)
+ {
+ if (SymbolEqualityComparer.Default.Equals(iface, interfaceType))
+ return true;
+ }
+
+ return false;
+ }
private static void GenerateVisitorInfrastructure(SourceProductionContext context, VisitorRootInfo root)
{
@@ -230,7 +294,15 @@ private static void GenerateAcceptMethods(SourceProductionContext context, Visit
sb.AppendLine();
}
- sb.AppendLine($"public partial class {type.Name}");
+ // Determine the type keyword (class, struct, interface, record)
+ string typeKeyword = type.TypeKind switch
+ {
+ TypeKind.Interface => "interface",
+ TypeKind.Struct => "struct",
+ _ => "class"
+ };
+
+ sb.AppendLine($"public partial {typeKeyword} {type.Name}");
sb.AppendLine("{");
// Sync Accept with result
diff --git a/test/PatternKit.Generators.Tests/VisitorGeneratorTests.cs b/test/PatternKit.Generators.Tests/VisitorGeneratorTests.cs
index c1b3990..23082e3 100644
--- a/test/PatternKit.Generators.Tests/VisitorGeneratorTests.cs
+++ b/test/PatternKit.Generators.Tests/VisitorGeneratorTests.cs
@@ -466,4 +466,253 @@ public partial class ResultLeaf : ResultNode
var emit = updated.Emit(Stream.Null);
Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
}
+
+ [Fact]
+ public void Generates_Visitor_For_Interface_Hierarchy()
+ {
+ const string interfaceHierarchy = """
+ using PatternKit.Generators.Visitors;
+
+ namespace PatternKit.Examples.Shapes;
+
+ [GenerateVisitor]
+ public partial interface IShape { }
+
+ public partial class Circle : IShape
+ {
+ public double Radius { get; init; }
+ }
+
+ public partial class Rectangle : IShape
+ {
+ public double Width { get; init; }
+ public double Height { get; init; }
+ }
+
+ public partial class Triangle : IShape
+ {
+ public double Base { get; init; }
+ public double Height { get; init; }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(
+ interfaceHierarchy,
+ assemblyName: nameof(Generates_Visitor_For_Interface_Hierarchy));
+
+ var gen = new VisitorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var run, out var updated);
+
+ // No generator diagnostics
+ Assert.All(run.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Confirm we generated expected files
+ var names = run.Results.SelectMany(r => r.GeneratedSources).Select(gs => gs.HintName).ToArray();
+
+ // Interfaces
+ Assert.Contains("IShapeVisitor.Interfaces.g.cs", names);
+
+ // Accept methods for each type (interface + concrete classes)
+ Assert.Contains("IShape.Accept.g.cs", names);
+ Assert.Contains("Circle.Accept.g.cs", names);
+ Assert.Contains("Rectangle.Accept.g.cs", names);
+ Assert.Contains("Triangle.Accept.g.cs", names);
+
+ // Builders
+ Assert.Contains("IShapeVisitorBuilder.g.cs", names);
+
+ // Verify compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void Interface_Hierarchy_Visitor_Dispatches_Correctly()
+ {
+ const string interfaceHierarchyWithUsage = """
+ using PatternKit.Generators.Visitors;
+
+ namespace PatternKit.Examples.Shapes;
+
+ [GenerateVisitor]
+ public partial interface IShape { }
+
+ public partial class Circle : IShape
+ {
+ public double Radius { get; init; }
+ }
+
+ public partial class Rectangle : IShape
+ {
+ public double Width { get; init; }
+ public double Height { get; init; }
+ }
+
+ public static class Demo
+ {
+ public static double Run()
+ {
+ var areaCalculator = new IShapeVisitorBuilder()
+ .When(c => 3.14159 * c.Radius * c.Radius)
+ .When(r => r.Width * r.Height)
+ .Default(_ => 0.0)
+ .Build();
+
+ var circle = new Circle { Radius = 5.0 };
+ var rectangle = new Rectangle { Width = 4.0, Height = 6.0 };
+
+ var circleArea = circle.Accept(areaCalculator);
+ var rectangleArea = rectangle.Accept(areaCalculator);
+
+ return circleArea + rectangleArea;
+ }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(
+ interfaceHierarchyWithUsage,
+ assemblyName: nameof(Interface_Hierarchy_Visitor_Dispatches_Correctly));
+
+ var gen = new VisitorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var run, out var updated);
+
+ Assert.All(run.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Emit and execute
+ using var pe = new MemoryStream();
+ using var pdb = new MemoryStream();
+ var res = updated.Emit(pe, pdb);
+ Assert.True(res.Success, string.Join("\n", res.Diagnostics));
+ pe.Position = 0;
+
+ var asm = AssemblyLoadContext.Default.LoadFromStream(pe, pdb);
+ var demo = asm.GetType("PatternKit.Examples.Shapes.Demo")!;
+ var runMethod = demo.GetMethod("Run")!;
+ var result = (double)runMethod.Invoke(null, null)!;
+
+ // Circle area: π * 5^2 ≈ 78.54
+ // Rectangle area: 4 * 6 = 24
+ // Total ≈ 102.54
+ Assert.True(result > 100 && result < 105, $"Expected ~102.54, got {result}");
+ }
+
+ [Fact]
+ public void Generates_Visitor_For_Struct_Hierarchy()
+ {
+ const string structHierarchy = """
+ using PatternKit.Generators.Visitors;
+
+ namespace PatternKit.Examples.Values;
+
+ [GenerateVisitor]
+ public partial interface IValue { }
+
+ public partial struct IntValue : IValue
+ {
+ public int Value { get; init; }
+ }
+
+ public partial struct DoubleValue : IValue
+ {
+ public double Value { get; init; }
+ }
+
+ public partial struct StringValue : IValue
+ {
+ public string Value { get; init; }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(
+ structHierarchy,
+ assemblyName: nameof(Generates_Visitor_For_Struct_Hierarchy));
+
+ var gen = new VisitorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var run, out var updated);
+
+ // No generator diagnostics
+ Assert.All(run.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Confirm we generated expected files
+ var names = run.Results.SelectMany(r => r.GeneratedSources).Select(gs => gs.HintName).ToArray();
+
+ // Interfaces
+ Assert.Contains("IValueVisitor.Interfaces.g.cs", names);
+
+ // Accept methods for interface and structs
+ Assert.Contains("IValue.Accept.g.cs", names);
+ Assert.Contains("IntValue.Accept.g.cs", names);
+ Assert.Contains("DoubleValue.Accept.g.cs", names);
+ Assert.Contains("StringValue.Accept.g.cs", names);
+
+ // Verify compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void Struct_Visitor_Dispatches_Without_Boxing()
+ {
+ const string structHierarchyWithUsage = """
+ using PatternKit.Generators.Visitors;
+
+ namespace PatternKit.Examples.Values;
+
+ [GenerateVisitor]
+ public partial interface IValue { }
+
+ public partial struct IntValue : IValue
+ {
+ public int Value { get; init; }
+ }
+
+ public partial struct DoubleValue : IValue
+ {
+ public double Value { get; init; }
+ }
+
+ public static class Demo
+ {
+ public static string Run()
+ {
+ var formatter = new IValueVisitorBuilder()
+ .When(i => $"Int:{i.Value}")
+ .When(d => $"Double:{d.Value:F2}")
+ .Default(_ => "Unknown")
+ .Build();
+
+ var intVal = new IntValue { Value = 42 };
+ var doubleVal = new DoubleValue { Value = 3.14159 };
+
+ var intStr = intVal.Accept(formatter);
+ var doubleStr = doubleVal.Accept(formatter);
+
+ return $"{intStr},{doubleStr}";
+ }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(
+ structHierarchyWithUsage,
+ assemblyName: nameof(Struct_Visitor_Dispatches_Without_Boxing));
+
+ var gen = new VisitorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var run, out var updated);
+
+ Assert.All(run.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Emit and execute
+ using var pe = new MemoryStream();
+ using var pdb = new MemoryStream();
+ var res = updated.Emit(pe, pdb);
+ Assert.True(res.Success, string.Join("\n", res.Diagnostics));
+ pe.Position = 0;
+
+ var asm = AssemblyLoadContext.Default.LoadFromStream(pe, pdb);
+ var demo = asm.GetType("PatternKit.Examples.Values.Demo")!;
+ var runMethod = demo.GetMethod("Run")!;
+ var result = (string)runMethod.Invoke(null, null)!;
+
+ Assert.Equal("Int:42,Double:3.14", result);
+ }
}
From 3a5c3e231e4463be1bb9ed3f3bfb92d146c025f8 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 22 Jan 2026 02:47:32 +0000
Subject: [PATCH 3/8] feat: Add comprehensive diagnostics to Visitor generator
- Add PKVIS001: Warning for no concrete types found
- Add PKVIS002: Error for non-partial base types
- Add PKVIS003: Warning for inaccessible types
- Add PKVIS004: Error for non-partial derived types
- Update AnalyzerReleases.Unshipped.md with new diagnostic IDs
- Add IsPartial helper method for checking partial keyword
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
.../AnalyzerReleases.Unshipped.md | 4 +
src/PatternKit.Generators/VisitorGenerator.cs | 103 ++++++++++++++++++
2 files changed, 107 insertions(+)
diff --git a/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md b/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
index 90f116b..87767b0 100644
--- a/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
+++ b/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
@@ -37,3 +37,7 @@ PKMEM003 | PatternKit.Generators.Memento | Warning | Unsafe reference capture
PKMEM004 | PatternKit.Generators.Memento | Error | Clone strategy requested but mechanism missing
PKMEM005 | PatternKit.Generators.Memento | Error | Record restore generation failed
PKMEM006 | PatternKit.Generators.Memento | Info | Init-only or readonly restrictions prevent in-place restore
+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
+PKVIS003 | PatternKit.Generators.Visitor | Warning | Type not accessible for visitor generation
+PKVIS004 | PatternKit.Generators.Visitor | Error | Derived type must be partial for Accept method generation
diff --git a/src/PatternKit.Generators/VisitorGenerator.cs b/src/PatternKit.Generators/VisitorGenerator.cs
index eb1f040..47b1888 100644
--- a/src/PatternKit.Generators/VisitorGenerator.cs
+++ b/src/PatternKit.Generators/VisitorGenerator.cs
@@ -177,6 +177,40 @@ private static bool ImplementsInterface(INamedTypeSymbol type, INamedTypeSymbol
private static void GenerateVisitorInfrastructure(SourceProductionContext context, VisitorRootInfo root)
{
+ // Check if we have any concrete types to visit
+ if (root.DerivedTypes.IsEmpty)
+ {
+ var location = root.BaseType.Locations.FirstOrDefault();
+ context.ReportDiagnostic(Diagnostic.Create(
+ Diagnostics.NoConcretTypes,
+ location,
+ root.BaseName));
+ }
+
+ // Check if base type is partial (only for classes and structs, not interfaces)
+ if (root.BaseType.TypeKind != TypeKind.Interface && !IsPartial(root.BaseType))
+ {
+ var location = root.BaseType.Locations.FirstOrDefault();
+ context.ReportDiagnostic(Diagnostic.Create(
+ Diagnostics.TypeMustBePartial,
+ location,
+ root.BaseName));
+ return; // Can't generate if not partial
+ }
+
+ // Check if derived types are partial
+ foreach (var derivedType in root.DerivedTypes)
+ {
+ if (!IsPartial(derivedType))
+ {
+ var location = derivedType.Locations.FirstOrDefault();
+ context.ReportDiagnostic(Diagnostic.Create(
+ Diagnostics.DerivedTypeNotPartial,
+ location,
+ derivedType.Name));
+ }
+ }
+
// Generate visitor interfaces
GenerateVisitorInterfaces(context, root);
@@ -186,6 +220,23 @@ private static void GenerateVisitorInfrastructure(SourceProductionContext contex
// Generate fluent builders
GenerateFluentBuilders(context, root);
}
+
+ private static bool IsPartial(INamedTypeSymbol type)
+ {
+ // Check if any of the declarations is marked as partial
+ foreach (var syntaxRef in type.DeclaringSyntaxReferences)
+ {
+ var syntax = syntaxRef.GetSyntax();
+ if (syntax is TypeDeclarationSyntax typeDecl)
+ {
+ if (typeDecl.Modifiers.Any(m => m.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.PartialKeyword)))
+ {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
private static void GenerateVisitorInterfaces(SourceProductionContext context, VisitorRootInfo root)
{
@@ -758,4 +809,56 @@ private readonly record struct VisitorRootInfo(
bool GenerateActions,
ImmutableArray DerivedTypes
);
+
+ ///
+ /// Diagnostic descriptors for visitor pattern generation.
+ ///
+ private static class Diagnostics
+ {
+ private const string Category = "PatternKit.Generators.Visitor";
+
+ ///
+ /// PKVIS001: No concrete types found for a marked base type.
+ ///
+ public static readonly DiagnosticDescriptor NoConcretTypes = new(
+ "PKVIS001",
+ "No concrete types found",
+ "No concrete types implementing or deriving from '{0}' were found. Add derived types or set AutoDiscoverDerivedTypes = false",
+ Category,
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
+ ///
+ /// PKVIS002: Type must be partial for Accept method generation.
+ ///
+ public static readonly DiagnosticDescriptor TypeMustBePartial = new(
+ "PKVIS002",
+ "Type must be partial",
+ "Type '{0}' must be declared as partial to allow Accept method generation",
+ Category,
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ ///
+ /// PKVIS003: Type not accessible for visitor generation.
+ ///
+ public static readonly DiagnosticDescriptor TypeNotAccessible = new(
+ "PKVIS003",
+ "Type not accessible",
+ "Type '{0}' is not accessible. Visitor generation requires public or internal types",
+ Category,
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
+ ///
+ /// PKVIS004: Derived type is not partial.
+ ///
+ public static readonly DiagnosticDescriptor DerivedTypeNotPartial = new(
+ "PKVIS004",
+ "Derived type must be partial",
+ "Derived type '{0}' must be declared as partial to allow Accept method generation",
+ Category,
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+ }
}
From c8858153109db99998d45947faaef0e32503d727 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 22 Jan 2026 02:48:54 +0000
Subject: [PATCH 4/8] docs: Update visitor generator documentation with new
features
- Add comprehensive section on supported hierarchy types
- Document interface-based hierarchies with examples
- Document struct-based hierarchies for allocation-free patterns
- Document record types support
- Add Diagnostics section with all PKVIS001-004 errors
- Update overview to mention support for all type kinds
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
docs/generators/visitor-generator.md | 196 ++++++++++++++++++++++++++-
1 file changed, 195 insertions(+), 1 deletion(-)
diff --git a/docs/generators/visitor-generator.md b/docs/generators/visitor-generator.md
index c749470..494fdbb 100644
--- a/docs/generators/visitor-generator.md
+++ b/docs/generators/visitor-generator.md
@@ -1,6 +1,6 @@
# Visitor Pattern Generator
-The Visitor Pattern Generator automatically generates fluent, type-safe visitor infrastructure for class hierarchies marked with the `[GenerateVisitor]` attribute. This eliminates boilerplate code and provides modern C# ergonomics including async/await support, ValueTask, and generic type inference.
+The Visitor Pattern Generator automatically generates fluent, type-safe visitor infrastructure for hierarchies marked with the `[GenerateVisitor]` attribute. It supports class, interface, struct, and record hierarchies, eliminating boilerplate code and providing modern C# ergonomics including async/await support, ValueTask, and generic type inference.
## Overview
@@ -118,6 +118,129 @@ var asyncLogger = new AstNodeAsyncActionVisitorBuilder()
await myExpression.AcceptAsync(asyncLogger);
```
+## Supported Hierarchy Types
+
+The visitor generator supports multiple types of hierarchies, providing flexibility in design:
+
+### Class-Based Hierarchies
+
+Traditional class inheritance hierarchies are fully supported:
+
+```csharp
+[GenerateVisitor]
+public abstract partial class Animal
+{
+}
+
+public partial class Dog : Animal
+{
+ public string Breed { get; init; }
+}
+
+public partial class Cat : Animal
+{
+ public bool IsIndoor { get; init; }
+}
+```
+
+### Interface-Based Hierarchies
+
+Hierarchies based on interfaces work seamlessly:
+
+```csharp
+[GenerateVisitor]
+public partial interface IShape
+{
+}
+
+public partial class Circle : IShape
+{
+ public double Radius { get; init; }
+}
+
+public partial class Rectangle : IShape
+{
+ public double Width { get; init; }
+ public double Height { get; init; }
+}
+
+public partial class Triangle : IShape
+{
+ public double Base { get; init; }
+ public double Height { get; init; }
+}
+```
+
+**Note:** For interface base types, the generated visitor interface name is intelligently derived. `IShape` generates `IShapeVisitor` (not `IIShapeVisitor`).
+
+### Struct-Based Hierarchies
+
+Value types can implement visitable interfaces for allocation-free visitor patterns:
+
+```csharp
+[GenerateVisitor]
+public partial interface IValue
+{
+}
+
+public partial struct IntValue : IValue
+{
+ public int Value { get; init; }
+}
+
+public partial struct DoubleValue : IValue
+{
+ public double Value { get; init; }
+}
+
+// No boxing occurs during visitation
+var visitor = new IValueVisitorBuilder()
+ .When(i => $"Int:{i.Value}")
+ .When(d => $"Double:{d.Value:F2}")
+ .Build();
+
+var intVal = new IntValue { Value = 42 };
+var result = intVal.Accept(visitor); // "Int:42"
+```
+
+### Record Types
+
+Records are also supported:
+
+```csharp
+[GenerateVisitor]
+public abstract partial record Message;
+
+public partial record TextMessage(string Content) : Message;
+public partial record ImageMessage(byte[] Data, string Format) : Message;
+```
+
+### Mixed Hierarchies
+
+You can mix interfaces, classes, and structs in complex hierarchies:
+
+```csharp
+[GenerateVisitor]
+public partial interface INode
+{
+}
+
+public abstract partial class Expression : INode
+{
+}
+
+public partial class Literal : Expression
+{
+ public object Value { get; init; }
+}
+
+public partial struct Position : INode
+{
+ public int Line { get; init; }
+ public int Column { get; init; }
+}
+```
+
## Attribute Options
The `[GenerateVisitor]` attribute supports several options:
@@ -391,6 +514,77 @@ var validator = new DocumentVisitorBuilder()
.Build();
```
+## Diagnostics
+
+The generator provides helpful diagnostics to catch common issues:
+
+### PKVIS001: No concrete types found
+
+**Severity:** Warning
+
+This warning appears when the generator cannot find any concrete types implementing or deriving from the marked base type.
+
+```csharp
+[GenerateVisitor]
+public partial interface IEmptyHierarchy { }
+
+// Warning PKVIS001: No concrete types implementing or deriving from 'IEmptyHierarchy' were found
+```
+
+**Solutions:**
+- Add concrete types that implement the interface or derive from the class
+- Set `AutoDiscoverDerivedTypes = false` if you're building types manually
+
+### PKVIS002: Type must be partial
+
+**Severity:** Error
+
+The base type (class or struct, not interface) must be declared as `partial` to allow Accept method generation.
+
+```csharp
+[GenerateVisitor]
+public class NonPartialBase { } // Error!
+
+// Fix:
+[GenerateVisitor]
+public partial class PartialBase { } // Correct
+```
+
+**Solution:** Add the `partial` keyword to the type declaration.
+
+### PKVIS003: Type not accessible
+
+**Severity:** Warning
+
+Types must be `public` or `internal` for visitor generation to work properly.
+
+```csharp
+[GenerateVisitor]
+private partial class PrivateBase { } // Warning!
+
+// Fix:
+[GenerateVisitor]
+public partial class PublicBase { } // Correct
+```
+
+### PKVIS004: Derived type must be partial
+
+**Severity:** Error
+
+All derived types must be `partial` to allow Accept method generation.
+
+```csharp
+[GenerateVisitor]
+public partial class Base { }
+
+public class Derived : Base { } // Error!
+
+// Fix:
+public partial class Derived : Base { } // Correct
+```
+
+**Solution:** Add the `partial` keyword to all derived types in the hierarchy.
+
## Troubleshooting
### "No handler registered for type X"
From 43b2271d7a771fde2ce62b3f08c47140f4cefc02 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 22 Jan 2026 02:50:33 +0000
Subject: [PATCH 5/8] test: Add comprehensive diagnostic tests for Visitor
generator
- Add test for PKVIS001 (no concrete types found)
- Add test for PKVIS002 (base type not partial)
- Add test for PKVIS004 (derived type not partial)
- Add test for valid hierarchy with no diagnostics
- All 34 visitor generator tests passing
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
.../VisitorGeneratorTests.cs | 113 ++++++++++++++++++
1 file changed, 113 insertions(+)
diff --git a/test/PatternKit.Generators.Tests/VisitorGeneratorTests.cs b/test/PatternKit.Generators.Tests/VisitorGeneratorTests.cs
index 23082e3..a7b3f3e 100644
--- a/test/PatternKit.Generators.Tests/VisitorGeneratorTests.cs
+++ b/test/PatternKit.Generators.Tests/VisitorGeneratorTests.cs
@@ -715,4 +715,117 @@ public static string Run()
Assert.Equal("Int:42,Double:3.14", result);
}
+
+ [Fact]
+ public void Diagnostic_PKVIS001_EmittedWhenNoConcretTypesFound()
+ {
+ const string noDerivedTypes = """
+ using PatternKit.Generators.Visitors;
+
+ namespace PatternKit.Examples;
+
+ [GenerateVisitor]
+ public partial interface IEmptyHierarchy { }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(
+ noDerivedTypes,
+ assemblyName: nameof(Diagnostic_PKVIS001_EmittedWhenNoConcretTypesFound));
+
+ var gen = new VisitorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var run, out var updated);
+
+ // Should have PKVIS001 warning
+ var diagnostics = run.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKVIS001");
+
+ var pkvis001 = diagnostics.First(d => d.Id == "PKVIS001");
+ Assert.Contains("IEmptyHierarchy", pkvis001.GetMessage());
+ }
+
+ [Fact]
+ public void Diagnostic_PKVIS002_EmittedWhenBaseTypeNotPartial()
+ {
+ const string nonPartialBase = """
+ using PatternKit.Generators.Visitors;
+
+ namespace PatternKit.Examples;
+
+ [GenerateVisitor]
+ public class NonPartialBase { }
+
+ public partial class DerivedType : NonPartialBase { }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(
+ nonPartialBase,
+ assemblyName: nameof(Diagnostic_PKVIS002_EmittedWhenBaseTypeNotPartial));
+
+ var gen = new VisitorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var run, out var updated);
+
+ // Should have PKVIS002 error
+ var diagnostics = run.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKVIS002");
+
+ var pkvis002 = diagnostics.First(d => d.Id == "PKVIS002");
+ Assert.Contains("NonPartialBase", pkvis002.GetMessage());
+ }
+
+ [Fact]
+ public void Diagnostic_PKVIS004_EmittedWhenDerivedTypeNotPartial()
+ {
+ const string nonPartialDerived = """
+ using PatternKit.Generators.Visitors;
+
+ namespace PatternKit.Examples;
+
+ [GenerateVisitor]
+ public partial class Base { }
+
+ public class NonPartialDerived : Base { }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(
+ nonPartialDerived,
+ assemblyName: nameof(Diagnostic_PKVIS004_EmittedWhenDerivedTypeNotPartial));
+
+ var gen = new VisitorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var run, out var updated);
+
+ // Should have PKVIS004 error
+ var diagnostics = run.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKVIS004");
+
+ var pkvis004 = diagnostics.First(d => d.Id == "PKVIS004");
+ Assert.Contains("NonPartialDerived", pkvis004.GetMessage());
+ }
+
+ [Fact]
+ public void No_Diagnostics_For_Valid_Hierarchy()
+ {
+ const string validHierarchy = """
+ using PatternKit.Generators.Visitors;
+
+ namespace PatternKit.Examples;
+
+ [GenerateVisitor]
+ public partial class ValidBase { }
+
+ public partial class ValidDerived : ValidBase { }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(
+ validHierarchy,
+ assemblyName: nameof(No_Diagnostics_For_Valid_Hierarchy));
+
+ var gen = new VisitorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var run, out var updated);
+
+ // Should have no generator diagnostics
+ var diagnostics = run.Results.SelectMany(r => r.Diagnostics)
+ .Where(d => d.Id.StartsWith("PKVIS"))
+ .ToArray();
+ Assert.Empty(diagnostics);
+ }
}
From 194a29ba6dca68daebbd171b1ed8336c46bfe4a3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 22 Jan 2026 02:58:21 +0000
Subject: [PATCH 6/8] refactor: Address code review feedback for Visitor
generator
- Add using statement for Microsoft.CodeAnalysis.CSharp
- Extract interface naming logic into GetDefaultVisitorInterfaceName method
- Improve code maintainability and testability
- All tests passing, CodeQL clean
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
src/PatternKit.Generators/VisitorGenerator.cs | 41 ++++++++++++-------
1 file changed, 27 insertions(+), 14 deletions(-)
diff --git a/src/PatternKit.Generators/VisitorGenerator.cs b/src/PatternKit.Generators/VisitorGenerator.cs
index 47b1888..4eadbb8 100644
--- a/src/PatternKit.Generators/VisitorGenerator.cs
+++ b/src/PatternKit.Generators/VisitorGenerator.cs
@@ -1,4 +1,5 @@
using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Immutable;
using System.Text;
@@ -56,19 +57,8 @@ or StructDeclarationSyntax
var baseName = baseType.Name;
var baseFullName = baseType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
- // Generate visitor interface name intelligently
- // If base name starts with "I" and is an interface, don't add another "I"
- string defaultVisitorName;
- if (baseType.TypeKind == TypeKind.Interface && baseName.StartsWith("I") && baseName.Length > 1 && char.IsUpper(baseName[1]))
- {
- // Interface name like "IShape" -> "IShapeVisitor"
- defaultVisitorName = $"{baseName}Visitor";
- }
- else
- {
- // Class name like "Shape" -> "IShapeVisitor"
- defaultVisitorName = $"I{baseName}Visitor";
- }
+ // Generate default visitor interface name
+ var defaultVisitorName = GetDefaultVisitorInterfaceName(baseType);
// Discover derived types in the same assembly
var derivedTypes = autoDiscover
@@ -86,6 +76,29 @@ or StructDeclarationSyntax
DerivedTypes: derivedTypes
);
}
+
+ ///
+ /// Generates a default visitor interface name based on the base type.
+ /// For interfaces with I-prefix (e.g., IShape), generates IShapeVisitor.
+ /// For other types (e.g., Shape), generates IShapeVisitor.
+ ///
+ private static string GetDefaultVisitorInterfaceName(INamedTypeSymbol baseType)
+ {
+ var baseName = baseType.Name;
+
+ // If base is an interface with I-prefix (Hungarian notation), don't add another I
+ if (baseType.TypeKind == TypeKind.Interface &&
+ baseName.StartsWith("I") &&
+ baseName.Length > 1 &&
+ char.IsUpper(baseName[1]))
+ {
+ // Interface name like "IShape" -> "IShapeVisitor"
+ return $"{baseName}Visitor";
+ }
+
+ // Class name like "Shape" -> "IShapeVisitor"
+ return $"I{baseName}Visitor";
+ }
private static T? GetAttributeProperty(AttributeData attr, string propertyName)
{
@@ -229,7 +242,7 @@ private static bool IsPartial(INamedTypeSymbol type)
var syntax = syntaxRef.GetSyntax();
if (syntax is TypeDeclarationSyntax typeDecl)
{
- if (typeDecl.Modifiers.Any(m => m.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.PartialKeyword)))
+ if (typeDecl.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)))
{
return true;
}
From 99942b4d87519a678a18edf519e8832210b0c1ce Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 22 Jan 2026 03:36:23 +0000
Subject: [PATCH 7/8] fix: Address PR review feedback
- Remove interface exclusion from partial check (fixes PKVIS002 for interfaces)
- Add proper record type keyword detection (record/record struct)
- Remove unused PKVIS003 diagnostic
- Add explicit .Where() and .Select() for better readability
- Simplify IsPartial method with LINQ
- Add tests for record hierarchies
- Add test for non-partial interface base types
- Update documentation to remove PKVIS003
All 37 tests passing.
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
docs/generators/visitor-generator.md | 15 --
.../AnalyzerReleases.Unshipped.md | 1 -
src/PatternKit.Generators/VisitorGenerator.cs | 68 ++++-----
.../VisitorGeneratorTests.cs | 137 ++++++++++++++++++
4 files changed, 165 insertions(+), 56 deletions(-)
diff --git a/docs/generators/visitor-generator.md b/docs/generators/visitor-generator.md
index 494fdbb..741f132 100644
--- a/docs/generators/visitor-generator.md
+++ b/docs/generators/visitor-generator.md
@@ -552,21 +552,6 @@ public partial class PartialBase { } // Correct
**Solution:** Add the `partial` keyword to the type declaration.
-### PKVIS003: Type not accessible
-
-**Severity:** Warning
-
-Types must be `public` or `internal` for visitor generation to work properly.
-
-```csharp
-[GenerateVisitor]
-private partial class PrivateBase { } // Warning!
-
-// Fix:
-[GenerateVisitor]
-public partial class PublicBase { } // Correct
-```
-
### PKVIS004: Derived type must be partial
**Severity:** Error
diff --git a/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md b/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
index 87767b0..e530359 100644
--- a/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
+++ b/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
@@ -39,5 +39,4 @@ PKMEM005 | PatternKit.Generators.Memento | Error | Record restore generation fai
PKMEM006 | PatternKit.Generators.Memento | Info | Init-only or readonly restrictions prevent in-place restore
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
-PKVIS003 | PatternKit.Generators.Visitor | Warning | Type not accessible for visitor generation
PKVIS004 | PatternKit.Generators.Visitor | Error | Derived type must be partial for Accept method generation
diff --git a/src/PatternKit.Generators/VisitorGenerator.cs b/src/PatternKit.Generators/VisitorGenerator.cs
index 4eadbb8..99bbc0d 100644
--- a/src/PatternKit.Generators/VisitorGenerator.cs
+++ b/src/PatternKit.Generators/VisitorGenerator.cs
@@ -200,8 +200,8 @@ private static void GenerateVisitorInfrastructure(SourceProductionContext contex
root.BaseName));
}
- // Check if base type is partial (only for classes and structs, not interfaces)
- if (root.BaseType.TypeKind != TypeKind.Interface && !IsPartial(root.BaseType))
+ // Check if base type is partial (all types need to be partial for Accept method generation)
+ if (!IsPartial(root.BaseType))
{
var location = root.BaseType.Locations.FirstOrDefault();
context.ReportDiagnostic(Diagnostic.Create(
@@ -212,16 +212,13 @@ private static void GenerateVisitorInfrastructure(SourceProductionContext contex
}
// Check if derived types are partial
- foreach (var derivedType in root.DerivedTypes)
+ foreach (var derivedType in root.DerivedTypes.Where(dt => !IsPartial(dt)))
{
- if (!IsPartial(derivedType))
- {
- var location = derivedType.Locations.FirstOrDefault();
- context.ReportDiagnostic(Diagnostic.Create(
- Diagnostics.DerivedTypeNotPartial,
- location,
- derivedType.Name));
- }
+ var location = derivedType.Locations.FirstOrDefault();
+ context.ReportDiagnostic(Diagnostic.Create(
+ Diagnostics.DerivedTypeNotPartial,
+ location,
+ derivedType.Name));
}
// Generate visitor interfaces
@@ -237,18 +234,10 @@ private static void GenerateVisitorInfrastructure(SourceProductionContext contex
private static bool IsPartial(INamedTypeSymbol type)
{
// Check if any of the declarations is marked as partial
- foreach (var syntaxRef in type.DeclaringSyntaxReferences)
- {
- var syntax = syntaxRef.GetSyntax();
- if (syntax is TypeDeclarationSyntax typeDecl)
- {
- if (typeDecl.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)))
- {
- return true;
- }
- }
- }
- return false;
+ return type.DeclaringSyntaxReferences
+ .Select(syntaxRef => syntaxRef.GetSyntax())
+ .OfType()
+ .Any(typeDecl => typeDecl.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)));
}
private static void GenerateVisitorInterfaces(SourceProductionContext context, VisitorRootInfo root)
@@ -358,13 +347,23 @@ private static void GenerateAcceptMethods(SourceProductionContext context, Visit
sb.AppendLine();
}
- // Determine the type keyword (class, struct, interface, record)
- string typeKeyword = type.TypeKind switch
+ // Determine the type keyword (class, struct, interface, record, record struct)
+ string typeKeyword;
+ if (type.IsRecord)
{
- TypeKind.Interface => "interface",
- TypeKind.Struct => "struct",
- _ => "class"
- };
+ typeKeyword = type.TypeKind == TypeKind.Struct
+ ? "record struct"
+ : "record";
+ }
+ else
+ {
+ typeKeyword = type.TypeKind switch
+ {
+ TypeKind.Interface => "interface",
+ TypeKind.Struct => "struct",
+ _ => "class"
+ };
+ }
sb.AppendLine($"public partial {typeKeyword} {type.Name}");
sb.AppendLine("{");
@@ -852,17 +851,6 @@ private static class Diagnostics
DiagnosticSeverity.Error,
isEnabledByDefault: true);
- ///
- /// PKVIS003: Type not accessible for visitor generation.
- ///
- public static readonly DiagnosticDescriptor TypeNotAccessible = new(
- "PKVIS003",
- "Type not accessible",
- "Type '{0}' is not accessible. Visitor generation requires public or internal types",
- Category,
- DiagnosticSeverity.Warning,
- isEnabledByDefault: true);
-
///
/// PKVIS004: Derived type is not partial.
///
diff --git a/test/PatternKit.Generators.Tests/VisitorGeneratorTests.cs b/test/PatternKit.Generators.Tests/VisitorGeneratorTests.cs
index a7b3f3e..4e60439 100644
--- a/test/PatternKit.Generators.Tests/VisitorGeneratorTests.cs
+++ b/test/PatternKit.Generators.Tests/VisitorGeneratorTests.cs
@@ -828,4 +828,141 @@ public partial class ValidDerived : ValidBase { }
.ToArray();
Assert.Empty(diagnostics);
}
+
+ [Fact]
+ public void Generates_Visitor_For_Record_Hierarchy()
+ {
+ const string recordHierarchy = """
+ using PatternKit.Generators.Visitors;
+
+ namespace PatternKit.Examples.Records;
+
+ [GenerateVisitor]
+ public abstract partial record Message;
+
+ public partial record TextMessage(string Content) : Message;
+
+ public partial record ImageMessage(byte[] Data, string Format) : Message;
+
+ public partial record AudioMessage(string Url, int DurationSeconds) : Message;
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(
+ recordHierarchy,
+ assemblyName: nameof(Generates_Visitor_For_Record_Hierarchy));
+
+ var gen = new VisitorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var run, out var updated);
+
+ // No generator diagnostics
+ Assert.All(run.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Confirm we generated expected files
+ var names = run.Results.SelectMany(r => r.GeneratedSources).Select(gs => gs.HintName).ToArray();
+
+ // Interfaces
+ Assert.Contains("IMessageVisitor.Interfaces.g.cs", names);
+
+ // Accept methods for each record type
+ Assert.Contains("Message.Accept.g.cs", names);
+ Assert.Contains("TextMessage.Accept.g.cs", names);
+ Assert.Contains("ImageMessage.Accept.g.cs", names);
+ Assert.Contains("AudioMessage.Accept.g.cs", names);
+
+ // Builders
+ Assert.Contains("MessageVisitorBuilder.g.cs", names);
+
+ // Verify compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void Record_Visitor_Dispatches_Correctly()
+ {
+ const string recordHierarchyWithUsage = """
+ using PatternKit.Generators.Visitors;
+
+ namespace PatternKit.Examples.Records;
+
+ [GenerateVisitor]
+ public abstract partial record Message;
+
+ public partial record TextMessage(string Content) : Message;
+
+ public partial record ImageMessage(byte[] Data, string Format) : Message;
+
+ public static class Demo
+ {
+ public static string Run()
+ {
+ var formatter = new MessageVisitorBuilder()
+ .When(m => $"Text: {m.Content}")
+ .When(m => $"Image: {m.Format}")
+ .Default(_ => "Unknown")
+ .Build();
+
+ var text = new TextMessage("Hello World");
+ var image = new ImageMessage(new byte[] { 1, 2, 3 }, "PNG");
+
+ var textStr = text.Accept(formatter);
+ var imageStr = image.Accept(formatter);
+
+ return $"{textStr}|{imageStr}";
+ }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(
+ recordHierarchyWithUsage,
+ assemblyName: nameof(Record_Visitor_Dispatches_Correctly));
+
+ var gen = new VisitorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var run, out var updated);
+
+ Assert.All(run.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Emit and execute
+ using var pe = new MemoryStream();
+ using var pdb = new MemoryStream();
+ var res = updated.Emit(pe, pdb);
+ Assert.True(res.Success, string.Join("\n", res.Diagnostics));
+ pe.Position = 0;
+
+ var asm = AssemblyLoadContext.Default.LoadFromStream(pe, pdb);
+ var demo = asm.GetType("PatternKit.Examples.Records.Demo")!;
+ var runMethod = demo.GetMethod("Run")!;
+ var result = (string)runMethod.Invoke(null, null)!;
+
+ Assert.Equal("Text: Hello World|Image: PNG", result);
+ }
+
+ [Fact]
+ public void Diagnostic_PKVIS002_EmittedWhenInterfaceBaseTypeNotPartial()
+ {
+ const string nonPartialInterface = """
+ using PatternKit.Generators.Visitors;
+
+ namespace PatternKit.Examples;
+
+ [GenerateVisitor]
+ public interface INotPartial { }
+
+ public partial class DerivedType : INotPartial { }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(
+ nonPartialInterface,
+ assemblyName: nameof(Diagnostic_PKVIS002_EmittedWhenInterfaceBaseTypeNotPartial));
+
+ var gen = new VisitorGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var run, out var updated);
+
+ // Should have PKVIS002 error
+ var diagnostics = run.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKVIS002");
+
+ var pkvis002 = diagnostics.First(d => d.Id == "PKVIS002");
+ Assert.Contains("INotPartial", pkvis002.GetMessage());
+ }
}
From eb05aaafff6bd07d3d6f22a69a0ea0a8af8018da Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 22 Jan 2026 03:55:23 +0000
Subject: [PATCH 8/8] fix: Use IsDerivedFrom for structs and update
documentation
- Use IsDerivedFrom instead of ImplementsInterface for struct discovery (consistency)
- Update documentation to clarify interfaces also require partial keyword
- All 37 tests passing
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
docs/generators/visitor-generator.md | 2 +-
src/PatternKit.Generators/VisitorGenerator.cs | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/generators/visitor-generator.md b/docs/generators/visitor-generator.md
index 741f132..c0afabf 100644
--- a/docs/generators/visitor-generator.md
+++ b/docs/generators/visitor-generator.md
@@ -539,7 +539,7 @@ public partial interface IEmptyHierarchy { }
**Severity:** Error
-The base type (class or struct, not interface) must be declared as `partial` to allow Accept method generation.
+The base type (class, struct, or interface) must be declared as `partial` to allow Accept method generation.
```csharp
[GenerateVisitor]
diff --git a/src/PatternKit.Generators/VisitorGenerator.cs b/src/PatternKit.Generators/VisitorGenerator.cs
index 99bbc0d..7971246 100644
--- a/src/PatternKit.Generators/VisitorGenerator.cs
+++ b/src/PatternKit.Generators/VisitorGenerator.cs
@@ -137,7 +137,7 @@ private static ImmutableArray DiscoverDerivedTypes(
var symbol = semanticModel.GetDeclaredSymbol(structDecl) as INamedTypeSymbol;
if (symbol is null || SymbolEqualityComparer.Default.Equals(symbol, baseType)) continue;
- if (ImplementsInterface(symbol, baseType))
+ if (IsDerivedFrom(symbol, baseType))
{
derived.Add(symbol);
}