feat(generator): 添加导出集合自动注册生成器

- 实现了 AutoRegisterExportedCollectionsGenerator 源生成器
- 支持扫描标记了 AutoRegisterExportedCollectionsAttribute 的 partial 类型
- 为使用 RegisterExportedCollectionAttribute 声明的集合成员生成集中注册方法
- 添加了类型验证和诊断报告功能
- 实现了集合元素类型推导和注册方法兼容性检查
- 生成批量注册样板代码以简化手动注册流程
- 添加了完整的单元测试覆盖各种使用场景
This commit is contained in:
GeWuYou 2026-04-13 12:27:27 +08:00
parent d21fac42b0
commit 3fadba2d79
2 changed files with 93 additions and 2 deletions

View File

@ -133,4 +133,70 @@ public class AutoRegisterExportedCollectionsGeneratorTests
await test.RunAsync();
}
[Test]
public async Task Generates_Batch_Registration_Method_When_Register_Method_Uses_Array_Parameter()
{
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 ArrayRegistry
{
public void Register(int[] value) { }
}
[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
private readonly ArrayRegistry _registry = new();
[RegisterExportedCollection(nameof(_registry), nameof(ArrayRegistry.Register))]
public List<int[]> Values { get; } = new();
}
}
""";
const string expected = """
// <auto-generated />
#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<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
}
}

View File

@ -104,6 +104,7 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
var registrations = CollectRegistrations(
context,
compilation,
candidate.TypeSymbol,
registerCollectionAttribute,
enumerableType);
@ -138,6 +139,7 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
private static List<RegistrationSpec> CollectRegistrations(
SourceProductionContext context,
Compilation compilation,
INamedTypeSymbol typeSymbol,
INamedTypeSymbol registerCollectionAttribute,
INamedTypeSymbol enumerableType)
@ -156,8 +158,17 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
if (attribute is null)
continue;
if (!TryCreateRegistration(context, typeSymbol, member, attribute, enumerableType, out var registration))
if (!TryCreateRegistration(
context,
compilation,
typeSymbol,
member,
attribute,
enumerableType,
out var registration))
{
continue;
}
registrations.Add(registration);
}
@ -167,6 +178,7 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
private static bool TryCreateRegistration(
SourceProductionContext context,
Compilation compilation,
INamedTypeSymbol ownerType,
ISymbol collectionMember,
AttributeData attribute,
@ -238,7 +250,7 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
.Any(method =>
!method.IsStatic &&
method.Parameters.Length == 1 &&
elementType.IsAssignableTo(method.Parameters[0].Type as INamedTypeSymbol));
CanAcceptElementType(compilation, elementType, method.Parameters[0].Type));
if (!hasCompatibleMethod)
{
@ -255,6 +267,19 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
return true;
}
private static bool CanAcceptElementType(
Compilation compilation,
ITypeSymbol elementType,
ITypeSymbol parameterType)
{
if (elementType.IsAssignableTo(parameterType as INamedTypeSymbol))
return true;
// Fall back to Roslyn's conversion rules so arrays and other non-named types are
// validated the same way the generated invocation will be bound by the compiler.
return compilation.ClassifyConversion(elementType, parameterType).IsImplicit;
}
private static bool TryGetRegistrationAttributeArguments(
AttributeData attribute,
out string registryMemberName,