Skip to content

Commit d14d6dc

Browse files
mkholtClaude <noreply@anthropic.com> via Conducktor
andauthored
Fix: detect handler methods inherited from base interfaces (#11)
* 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 <noreply@anthropic.com> via Conducktor <conducktor@contextand.com> * 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 <noreply@anthropic.com> via Conducktor <conducktor@contextand.com> --------- Co-authored-by: Claude <noreply@anthropic.com> via Conducktor <conducktor@contextand.com>
1 parent 8244f8b commit d14d6dc

2 files changed

Lines changed: 86 additions & 3 deletions

File tree

XrmPluginCore.SourceGenerator.Tests/DiagnosticTests/DiagnosticReportingTests.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,69 @@ public void Process() { }
160160
errorDiagnostics.Should().OnlyContain(d => d.Severity == DiagnosticSeverity.Error);
161161
}
162162

163+
[Fact]
164+
public async Task Should_Not_Report_XPC4001_When_Handler_Method_Is_Inherited_From_Base_Interface()
165+
{
166+
// Arrange - the handler method is declared on a grandparent interface in a
167+
// multi-level inheritance chain (ITestService : IMidService : IBaseService).
168+
// The method must still be detected via the transitive AllInterfaces walk.
169+
const string pluginSource = """
170+
171+
using XrmPluginCore;
172+
using XrmPluginCore.Enums;
173+
using Microsoft.Extensions.DependencyInjection;
174+
using TestNamespace;
175+
176+
namespace TestNamespace
177+
{
178+
public class TestPlugin : Plugin
179+
{
180+
public TestPlugin()
181+
{
182+
RegisterStep<Account, ITestService>(EventOperation.Update, ExecutionStage.PostOperation,
183+
service => service.Process)
184+
.WithPreImage(x => x.Name);
185+
}
186+
187+
protected override IServiceCollection OnBeforeBuildServiceProvider(IServiceCollection services)
188+
{
189+
return services.AddScoped<ITestService, TestService>();
190+
}
191+
}
192+
193+
public interface IBaseService
194+
{
195+
void Process();
196+
}
197+
198+
public interface IMidService : IBaseService
199+
{
200+
}
201+
202+
public interface ITestService : IMidService
203+
{
204+
}
205+
206+
public class TestService : ITestService
207+
{
208+
public void Process() { }
209+
}
210+
}
211+
""";
212+
213+
var source = TestFixtures.GetCompleteSource(pluginSource);
214+
215+
// Act - Run analyzer instead of generator
216+
var diagnostics = await GetAnalyzerDiagnosticsAsync(source, new HandlerMethodNotFoundAnalyzer());
217+
218+
// Assert
219+
var errorDiagnostics = diagnostics
220+
.Where(d => d.Id == "XPC4001")
221+
.ToArray();
222+
223+
errorDiagnostics.Should().BeEmpty("XPC4001 should not be reported when the handler method is inherited from a base interface");
224+
}
225+
163226
[Fact]
164227
public async Task Should_Report_XPC4002_When_Handler_Missing_PreImage_Parameter()
165228
{

XrmPluginCore.SourceGenerator/Helpers/TypeHelper.cs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,44 @@ internal static class TypeHelper
1111
{
1212
/// <summary>
1313
/// Gets all methods with the specified name, including inherited methods.
14+
/// For classes this walks the base type chain; for interfaces it also includes
15+
/// members inherited from base interfaces (which are exposed via AllInterfaces, not BaseType).
1416
/// </summary>
1517
public static IMethodSymbol[] GetAllMethodsIncludingInherited(ITypeSymbol type, string methodName)
1618
{
1719
var methods = new List<IMethodSymbol>();
18-
var currentType = type;
19-
while (currentType != null)
20+
21+
void AddMethods(ITypeSymbol from)
2022
{
21-
foreach (var member in currentType.GetMembers(methodName))
23+
foreach (var member in from.GetMembers(methodName))
2224
{
2325
if (member is IMethodSymbol method)
2426
{
2527
methods.Add(method);
2628
}
2729
}
30+
}
2831

32+
var currentType = type;
33+
while (currentType != null)
34+
{
35+
AddMethods(currentType);
2936
currentType = currentType.BaseType;
3037
}
3138

39+
// Interfaces don't expose inherited members via BaseType; their base
40+
// interfaces (transitively) are available through AllInterfaces. We only
41+
// do this for interface inputs - for classes the implementing methods live
42+
// on the class/base chain, and walking AllInterfaces would surface
43+
// (unimplemented) interface declaration methods.
44+
if (type.TypeKind == TypeKind.Interface)
45+
{
46+
foreach (var inheritedInterface in type.AllInterfaces)
47+
{
48+
AddMethods(inheritedInterface);
49+
}
50+
}
51+
3252
return methods.ToArray();
3353
}
3454

0 commit comments

Comments
 (0)