diff --git a/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs index 612529c3..d5117332 100644 --- a/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs @@ -278,6 +278,76 @@ public class AutoRegisterExportedCollectionsGeneratorTests ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)); } + [Test] + public async Task Generates_Batch_Registration_Method_When_Register_Method_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 class BaseRegistry + { + public void Register(int value) { } + } + + public sealed class DerivedRegistry : BaseRegistry + { + } + + [AutoRegisterExportedCollections] + public partial class Bootstrapper + { + private readonly DerivedRegistry? _registry = new(); + + [RegisterExportedCollection(nameof(_registry), nameof(BaseRegistry.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 16effd91..c790c709 100644 --- a/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs +++ b/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs @@ -318,6 +318,20 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener return compilation.ClassifyConversion(elementType, parameterType).IsImplicit; } + /// + /// 枚举给定注册表类型上可能承载批量注册入口的候选实例方法。 + /// + /// 声明注册表成员的静态类型。 + /// 特性参数中声明的注册方法名称。 + /// + /// 按“当前类型 -> 基类链 -> 已实现接口”顺序返回所有同名方法,供后续签名和可访问性筛选使用。 + /// + /// + /// 生成器需要沿这三条继承路径查找方法,因为用户代码可能通过派生类字段引用基类实现, + /// 或通过接口类型引用由上层接口声明的契约方法。这里故意不做去重:同一个语义方法可能同时经由 + /// 覆盖链、接口继承或显式声明被枚举多次,但当前调用方只使用 Any 判断“是否存在至少一个可用候选”, + /// 因此重复项只会带来额外的符号检查成本,不会改变生成结果或诊断边界。 + /// private static IEnumerable EnumerateCandidateMethods( INamedTypeSymbol registryType, string registerMethodName)