diff --git a/XrmPluginCore.SourceGenerator.Tests/DiagnosticTests/DiagnosticReportingTests.cs b/XrmPluginCore.SourceGenerator.Tests/DiagnosticTests/DiagnosticReportingTests.cs index b5caa78..1c46df4 100644 --- a/XrmPluginCore.SourceGenerator.Tests/DiagnosticTests/DiagnosticReportingTests.cs +++ b/XrmPluginCore.SourceGenerator.Tests/DiagnosticTests/DiagnosticReportingTests.cs @@ -160,6 +160,69 @@ 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 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; + 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 IMidService : IBaseService + { + } + + public interface ITestService : IMidService + { + } + + 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..ffddbdf 100644 --- a/XrmPluginCore.SourceGenerator/Helpers/TypeHelper.cs +++ b/XrmPluginCore.SourceGenerator/Helpers/TypeHelper.cs @@ -11,24 +11,44 @@ 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. 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) + { + foreach (var inheritedInterface in type.AllInterfaces) + { + AddMethods(inheritedInterface); + } + } + return methods.ToArray(); }