diff --git a/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs index d5117332..1a9afec6 100644 --- a/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs @@ -278,6 +278,66 @@ public class AutoRegisterExportedCollectionsGeneratorTests ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)); } + [Test] + public async Task Reports_Diagnostic_When_Register_Method_Is_Only_Explicitly_Implemented_Interface_Member() + { + const string source = """ + using System; + using System.Collections.Generic; + using GFramework.Godot.SourceGenerators.Abstractions; + + namespace GFramework.Godot.SourceGenerators.Abstractions + { + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] + public sealed class RegisterExportedCollectionAttribute : Attribute + { + public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { } + } + } + + namespace TestApp + { + public interface IRegistry + { + void Register(int value); + } + + public sealed class ExplicitRegistry : IRegistry + { + void IRegistry.Register(int value) { } + } + + [AutoRegisterExportedCollections] + public partial class Bootstrapper + { + private readonly ExplicitRegistry _registry = new(); + + [RegisterExportedCollection(nameof(_registry), "Register")] + public List {|#0:Values|} { get; } = new(); + } + } + """; + + var test = new CSharpSourceGeneratorTest + { + TestState = + { + Sources = { source } + }, + DisabledDiagnostics = { "GF_Common_Trace_001" }, + TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck + }; + + test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_003", DiagnosticSeverity.Error) + .WithLocation(0) + .WithArguments("Register", "_registry", "Values")); + + await test.RunAsync(); + } + [Test] public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Base_Class() { @@ -348,6 +408,75 @@ public class AutoRegisterExportedCollectionsGeneratorTests ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)); } + [Test] + public async Task Generates_Batch_Registration_Method_When_Registry_Member_Comes_From_Base_Class() + { + const string source = """ + #nullable enable + using System; + using System.Collections.Generic; + using GFramework.Godot.SourceGenerators.Abstractions; + + namespace GFramework.Godot.SourceGenerators.Abstractions + { + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] + public sealed class RegisterExportedCollectionAttribute : Attribute + { + public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { } + } + } + + namespace TestApp + { + public sealed class IntRegistry + { + public void Register(int value) { } + } + + public abstract class BootstrapperBase + { + protected readonly IntRegistry? _registry = new(); + } + + [AutoRegisterExportedCollections] + public partial class Bootstrapper : BootstrapperBase + { + [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] + public List? Values { get; } = new(); + } + } + """; + + const string expected = """ + // + #nullable enable + + namespace TestApp; + + partial class Bootstrapper + { + private void __RegisterExportedCollections_Generated() + { + if (this.Values is not null && this._registry is not null) + { + foreach (var __generatedItem in this.Values) + { + this._registry.Register(__generatedItem); + } + } + } + } + + """; + + await GeneratorTest.RunAsync( + source, + ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)); + } + [Test] public async Task Reports_Diagnostic_When_Collection_Member_Is_Not_Instance_Readable() { diff --git a/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs b/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs index c790c709..1db4f290 100644 --- a/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs +++ b/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs @@ -222,8 +222,7 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener out var registerMethodName)) return false; - var registryMember = ownerType.GetMembers(registryMemberName) - .FirstOrDefault(member => member is IFieldSymbol or IPropertySymbol); + var registryMember = FindRegistryMember(ownerType, registryMemberName); if (registryMember is null) { @@ -236,7 +235,8 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener return false; } - if (!IsInstanceReadableMember(registryMember)) + if (!IsInstanceReadableMember(registryMember) || + !compilation.IsSymbolAccessibleWithin(registryMember, ownerType)) { context.ReportDiagnostic(Diagnostic.Create( AutoRegisterExportedCollectionsDiagnostics.RegistryMemberMustBeInstanceReadable, @@ -318,19 +318,40 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener return compilation.ClassifyConversion(elementType, parameterType).IsImplicit; } + private static ISymbol? FindRegistryMember( + INamedTypeSymbol ownerType, + string registryMemberName) + { + for (var currentType = ownerType; currentType is not null; currentType = currentType.BaseType) + { + // Search the owner hierarchy one level at a time so the generator follows the same + // name-hiding order as `this.` in generated code. + var candidateMember = currentType.GetMembers(registryMemberName) + .FirstOrDefault(static member => member is IFieldSymbol or IPropertySymbol); + + if (candidateMember is not null) + return candidateMember; + } + + return null; + } + /// /// 枚举给定注册表类型上可能承载批量注册入口的候选实例方法。 /// /// 声明注册表成员的静态类型。 /// 特性参数中声明的注册方法名称。 /// - /// 按“当前类型 -> 基类链 -> 已实现接口”顺序返回所有同名方法,供后续签名和可访问性筛选使用。 + /// 按“当前类型 -> 基类链 -> 接口继承链(仅当静态类型本身是接口)”顺序返回所有同名方法, + /// 供后续签名和可访问性筛选使用。 /// /// - /// 生成器需要沿这三条继承路径查找方法,因为用户代码可能通过派生类字段引用基类实现, - /// 或通过接口类型引用由上层接口声明的契约方法。这里故意不做去重:同一个语义方法可能同时经由 - /// 覆盖链、接口继承或显式声明被枚举多次,但当前调用方只使用 Any 判断“是否存在至少一个可用候选”, - /// 因此重复项只会带来额外的符号检查成本,不会改变生成结果或诊断边界。 + /// 生成器需要沿当前类型和基类链查找方法,因为用户代码可能通过派生类字段引用基类实现; + /// 当注册表成员本身声明为接口类型时,还要继续沿接口继承链查找由父接口声明的契约方法。 + /// 对类或结构体不遍历 ,避免把仅能通过接口调用的显式实现 + /// 误判为可由 this.<registry>.<method>(...) 直接访问的方法。 + /// 这里故意不做去重:同一个语义方法可能同时经由覆盖链、接口继承或显式声明被枚举多次,但当前调用方只使用 + /// Any 判断“是否存在至少一个可用候选”,因此重复项只会带来额外的符号检查成本,不会改变生成结果或诊断边界。 /// private static IEnumerable EnumerateCandidateMethods( INamedTypeSymbol registryType, @@ -345,6 +366,9 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener yield return method; } + if (registryType.TypeKind != TypeKind.Interface) + yield break; + foreach (var interfaceType in registryType.AllInterfaces) { foreach (var method in interfaceType.GetMembers(registerMethodName).OfType())