Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<Account, ITestService>(EventOperation.Update, ExecutionStage.PostOperation,
service => service.Process)
.WithPreImage(x => x.Name);
}

protected override IServiceCollection OnBeforeBuildServiceProvider(IServiceCollection services)
{
return services.AddScoped<ITestService, TestService>();
}
}

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()
{
Expand Down
26 changes: 23 additions & 3 deletions XrmPluginCore.SourceGenerator/Helpers/TypeHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,44 @@ internal static class TypeHelper
{
/// <summary>
/// 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).
/// </summary>
public static IMethodSymbol[] GetAllMethodsIncludingInherited(ITypeSymbol type, string methodName)
{
var methods = new List<IMethodSymbol>();
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();
}

Expand Down
Loading