From 049102bea87c60b9a72f84b1309dd8ed0def2bb4 Mon Sep 17 00:00:00 2001 From: Morten Holt Date: Fri, 19 Jun 2026 14:20:50 +0200 Subject: [PATCH 1/2] Fix: detect handler methods inherited from base interfaces GetAllMethodsIncludingInherited only walked the BaseType chain, which is null for interfaces. Inherited interface members are exposed via AllInterfaces, so a handler method declared on a base interface was not detected and falsely triggered XPC4001. Also iterate AllInterfaces (transitive) so multi-level interface inheritance is covered. This fixes XPC4001, XPC4002, and the related code-fix providers, which all route through this helper. Co-Authored-By: Claude via Conducktor --- .../DiagnosticReportingTests.cs | 58 +++++++++++++++++++ .../Helpers/TypeHelper.cs | 20 ++++++- 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/XrmPluginCore.SourceGenerator.Tests/DiagnosticTests/DiagnosticReportingTests.cs b/XrmPluginCore.SourceGenerator.Tests/DiagnosticTests/DiagnosticReportingTests.cs index b5caa78..f139c9b 100644 --- a/XrmPluginCore.SourceGenerator.Tests/DiagnosticTests/DiagnosticReportingTests.cs +++ b/XrmPluginCore.SourceGenerator.Tests/DiagnosticTests/DiagnosticReportingTests.cs @@ -160,6 +160,64 @@ public void Process() { } errorDiagnostics.Should().OnlyContain(d => d.Severity == DiagnosticSeverity.Error); } + [Fact] + public async Task Should_Not_Report_XPC4001_When_Handler_Method_Is_Inherited_From_Base_Interface() + { + // Arrange - the handler method is declared on a base interface that the + // service interface inherits from. The method must still be detected. + const string pluginSource = """ + + using XrmPluginCore; + using XrmPluginCore.Enums; + using Microsoft.Extensions.DependencyInjection; + using TestNamespace; + + namespace TestNamespace + { + public class TestPlugin : Plugin + { + public TestPlugin() + { + RegisterStep(EventOperation.Update, ExecutionStage.PostOperation, + service => service.Process) + .WithPreImage(x => x.Name); + } + + protected override IServiceCollection OnBeforeBuildServiceProvider(IServiceCollection services) + { + return services.AddScoped(); + } + } + + public interface IBaseService + { + void Process(); + } + + public interface ITestService : IBaseService + { + } + + public class TestService : ITestService + { + public void Process() { } + } + } + """; + + var source = TestFixtures.GetCompleteSource(pluginSource); + + // Act - Run analyzer instead of generator + var diagnostics = await GetAnalyzerDiagnosticsAsync(source, new HandlerMethodNotFoundAnalyzer()); + + // Assert + var errorDiagnostics = diagnostics + .Where(d => d.Id == "XPC4001") + .ToArray(); + + errorDiagnostics.Should().BeEmpty("XPC4001 should not be reported when the handler method is inherited from a base interface"); + } + [Fact] public async Task Should_Report_XPC4002_When_Handler_Missing_PreImage_Parameter() { diff --git a/XrmPluginCore.SourceGenerator/Helpers/TypeHelper.cs b/XrmPluginCore.SourceGenerator/Helpers/TypeHelper.cs index f8998e6..2c42975 100644 --- a/XrmPluginCore.SourceGenerator/Helpers/TypeHelper.cs +++ b/XrmPluginCore.SourceGenerator/Helpers/TypeHelper.cs @@ -11,24 +11,38 @@ internal static class TypeHelper { /// /// Gets all methods with the specified name, including inherited methods. + /// For classes this walks the base type chain; for interfaces it also includes + /// members inherited from base interfaces (which are exposed via AllInterfaces, not BaseType). /// public static IMethodSymbol[] GetAllMethodsIncludingInherited(ITypeSymbol type, string methodName) { var methods = new List(); - var currentType = type; - while (currentType != null) + + void AddMethods(ITypeSymbol from) { - foreach (var member in currentType.GetMembers(methodName)) + foreach (var member in from.GetMembers(methodName)) { if (member is IMethodSymbol method) { methods.Add(method); } } + } + var currentType = type; + while (currentType != null) + { + AddMethods(currentType); currentType = currentType.BaseType; } + // Interfaces don't expose inherited members via BaseType; their base + // interfaces (transitively) are available through AllInterfaces. + foreach (var inheritedInterface in type.AllInterfaces) + { + AddMethods(inheritedInterface); + } + return methods.ToArray(); } From 6f30aacdb8ca2e3ea319342e78c49e7e803be865 Mon Sep 17 00:00:00 2001 From: Morten Holt Date: Fri, 19 Jun 2026 14:32:19 +0200 Subject: [PATCH 2/2] Address review: restrict interface walk to interface inputs, test 2-level chain - Only walk AllInterfaces when the input type is itself an interface, so FindImplementingMethods (called with class symbols) no longer surfaces unimplemented interface declaration methods. - Extend the regression test to a multi-level interface chain (ITestService : IMidService : IBaseService) to lock in the transitive AllInterfaces behavior. Co-Authored-By: Claude via Conducktor --- .../DiagnosticTests/DiagnosticReportingTests.cs | 11 ++++++++--- XrmPluginCore.SourceGenerator/Helpers/TypeHelper.cs | 12 +++++++++--- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/XrmPluginCore.SourceGenerator.Tests/DiagnosticTests/DiagnosticReportingTests.cs b/XrmPluginCore.SourceGenerator.Tests/DiagnosticTests/DiagnosticReportingTests.cs index f139c9b..1c46df4 100644 --- a/XrmPluginCore.SourceGenerator.Tests/DiagnosticTests/DiagnosticReportingTests.cs +++ b/XrmPluginCore.SourceGenerator.Tests/DiagnosticTests/DiagnosticReportingTests.cs @@ -163,8 +163,9 @@ public void Process() { } [Fact] public async Task Should_Not_Report_XPC4001_When_Handler_Method_Is_Inherited_From_Base_Interface() { - // Arrange - the handler method is declared on a base interface that the - // service interface inherits from. The method must still be detected. + // Arrange - the handler method is declared on a grandparent interface in a + // multi-level inheritance chain (ITestService : IMidService : IBaseService). + // The method must still be detected via the transitive AllInterfaces walk. const string pluginSource = """ using XrmPluginCore; @@ -194,7 +195,11 @@ public interface IBaseService void Process(); } - public interface ITestService : IBaseService + public interface IMidService : IBaseService + { + } + + public interface ITestService : IMidService { } diff --git a/XrmPluginCore.SourceGenerator/Helpers/TypeHelper.cs b/XrmPluginCore.SourceGenerator/Helpers/TypeHelper.cs index 2c42975..ffddbdf 100644 --- a/XrmPluginCore.SourceGenerator/Helpers/TypeHelper.cs +++ b/XrmPluginCore.SourceGenerator/Helpers/TypeHelper.cs @@ -37,10 +37,16 @@ void AddMethods(ITypeSymbol from) } // Interfaces don't expose inherited members via BaseType; their base - // interfaces (transitively) are available through AllInterfaces. - foreach (var inheritedInterface in type.AllInterfaces) + // interfaces (transitively) are available through AllInterfaces. We only + // do this for interface inputs - for classes the implementing methods live + // on the class/base chain, and walking AllInterfaces would surface + // (unimplemented) interface declaration methods. + if (type.TypeKind == TypeKind.Interface) { - AddMethods(inheritedInterface); + foreach (var inheritedInterface in type.AllInterfaces) + { + AddMethods(inheritedInterface); + } } return methods.ToArray();