diff --git a/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs index 1c4927a4..3a67d3c5 100644 --- a/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs @@ -200,6 +200,282 @@ public class AutoRegisterExportedCollectionsGeneratorTests ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)); } + [Test] + public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Inherited_Interface() + { + 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 interface IKeyValue + { + } + + public interface IRegistry + { + void Registry(IKeyValue mapping); + } + + public interface IAssetRegistry : IRegistry + { + } + + public sealed class IntConfig : IKeyValue + { + } + + [AutoRegisterExportedCollections] + public partial class Bootstrapper + { + private readonly IAssetRegistry? _registry = null; + + [RegisterExportedCollection(nameof(_registry), "Registry")] + 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.Registry(__generatedItem); + } + } + } + } + + """; + + await GeneratorTest.RunAsync( + source, + ("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" } + }; + + 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() + { + 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 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 df0c433b..12589767 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, @@ -268,8 +268,7 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener return false; } - var hasCompatibleMethod = registryType.GetMembers(registerMethodName) - .OfType() + var hasCompatibleMethod = EnumerateCandidateMethods(registryType, registerMethodName) .Any(method => !method.IsStatic && method.Parameters.Length == 1 && @@ -319,6 +318,72 @@ 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; + } + + /// + /// 枚举给定注册表类型上可能承载批量注册入口的候选实例方法。 + /// + /// 声明注册表成员的静态类型。 + /// 特性参数中声明的注册方法名称。 + /// + /// 按“当前类型 -> 基类链 -> 接口继承链(仅当静态类型本身是接口)”顺序返回所有同名方法, + /// 供后续签名和可访问性筛选使用。 + /// + /// + /// 生成器需要沿当前类型和基类链查找方法,因为用户代码可能通过派生类字段引用基类实现; + /// 当注册表成员本身声明为接口类型时,还要继续沿接口继承链查找由父接口声明的契约方法。 + /// 对类或结构体不遍历 ,避免把仅能通过接口调用的显式实现 + /// 误判为可由 this.<registry>.<method>(...) 直接访问的方法。 + /// 这里故意不做去重:同一个语义方法可能同时经由覆盖链、接口继承或显式声明被枚举多次,但当前调用方只使用 + /// Any 判断“是否存在至少一个可用候选”,因此重复项只会带来额外的符号检查成本,不会改变生成结果或诊断边界。 + /// + private static IEnumerable EnumerateCandidateMethods( + INamedTypeSymbol registryType, + string registerMethodName) + { + // Start from the declared registry type so directly declared overloads win the cheap checks + // before we expand into inherited declarations. + foreach (var method in registryType.GetMembers(registerMethodName).OfType()) + yield return method; + + // Concrete registry types can inherit callable implementations from base classes. When the + // registry itself is an interface, BaseType is null and this phase intentionally yields nothing. + for (var baseType = registryType.BaseType; baseType is not null; baseType = baseType.BaseType) + { + foreach (var method in baseType.GetMembers(registerMethodName).OfType()) + yield return method; + } + + // Only interface-typed registry members should search interface inheritance. For classes or + // structs this avoids accepting explicit interface implementations that generated code cannot + // call through `this..(...)`. AllInterfaces is already transitive, so the + // same semantic contract may appear multiple times; that is safe because the caller only uses Any(). + if (registryType.TypeKind != TypeKind.Interface) + yield break; + + foreach (var interfaceType in registryType.AllInterfaces) + { + foreach (var method in interfaceType.GetMembers(registerMethodName).OfType()) + yield return method; + } + } + private static bool TryGetRegistrationAttributeArguments( SourceProductionContext context, ISymbol collectionMember, @@ -434,11 +499,15 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener private static string GetTypeDeclarationKeyword(INamedTypeSymbol typeSymbol) { - return typeSymbol.IsRecord - ? typeSymbol.TypeKind == TypeKind.Struct ? "partial record struct" : "partial record" - : typeSymbol.TypeKind == TypeKind.Struct - ? "partial struct" - : "partial class"; + return typeSymbol switch + { + { IsRecord: true, TypeKind: TypeKind.Struct } => "partial record struct", + { IsRecord: true } => "partial record", + { TypeKind: TypeKind.Struct } => "partial struct", + { TypeKind: TypeKind.Class } => "partial class", + { TypeKind: TypeKind.Interface } => "partial interface", + _ => throw new NotSupportedException($"Unsupported type: {typeSymbol.TypeKind}") + }; } private static string GetTypeDeclarationName(INamedTypeSymbol typeSymbol)