feat(generator): 添加 AutoScene 和 AutoRegisterExportedCollections 源代码生成器

- 实现 AutoSceneGenerator 为标记了 [AutoScene] 的 Godot 节点生成场景行为样板
- 实现 AutoRegisterExportedCollectionsGenerator 为导出集合生成批量注册方法
- 添加完整的单元测试覆盖两种源代码生成器的功能和诊断
- 支持泛型类型参数约束的正确生成
- 提供详细的诊断信息帮助用户修复配置错误
This commit is contained in:
GeWuYou 2026-04-13 11:25:49 +08:00
parent ca1214f47f
commit d21fac42b0
4 changed files with 170 additions and 14 deletions

View File

@ -130,4 +130,90 @@ public class AutoSceneGeneratorTests
await test.RunAsync();
}
[Test]
public async Task Generates_Type_Constraints_For_Nullable_Reference_NotNull_And_Unmanaged_Parameters()
{
const string source = """
#nullable enable
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoSceneAttribute : Attribute
{
public AutoSceneAttribute(string key) { }
}
}
namespace Godot
{
public class Node { }
public class Node2D : Node { }
}
namespace GFramework.Game.Abstractions.Scene
{
public interface ISceneBehavior { }
}
namespace GFramework.Godot.Scene
{
using GFramework.Game.Abstractions.Scene;
using Godot;
public static class SceneBehaviorFactory
{
public static ISceneBehavior Create<T>(T owner, string key)
where T : Node
{
return null!;
}
}
}
namespace TestApp
{
[AutoScene("Gameplay")]
public partial class GameplayRoot<TReference, TNotNull, TValue, TUnmanaged> : Node2D
where TReference : class?
where TNotNull : notnull
where TValue : struct
where TUnmanaged : unmanaged
{
}
}
""";
const string expected = """
// <auto-generated />
#nullable enable
namespace TestApp;
partial class GameplayRoot<TReference, TNotNull, TValue, TUnmanaged>
where TReference : class?
where TNotNull : notnull
where TValue : struct
where TUnmanaged : unmanaged
{
private global::GFramework.Game.Abstractions.Scene.ISceneBehavior? __autoSceneBehavior_Generated;
public static string SceneKeyStr => "Gameplay";
public global::GFramework.Game.Abstractions.Scene.ISceneBehavior GetScene()
{
return __autoSceneBehavior_Generated ??= global::GFramework.Godot.Scene.SceneBehaviorFactory.Create(this, SceneKeyStr);
}
}
""";
await GeneratorTest<AutoSceneGenerator>.RunAsync(
source,
("TestApp_GameplayRoot.AutoScene.g.cs", expected));
}
}

View File

@ -10,6 +10,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests
public async Task Generates_Batch_Registration_Method_For_Annotated_Collections()
{
const string source = """
#nullable enable
using System;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions;
@ -34,12 +35,16 @@ public class AutoRegisterExportedCollectionsGeneratorTests
}
[AutoRegisterExportedCollections]
public partial class Bootstrapper
public partial class Bootstrapper<TReference, TNotNull, TValue, TUnmanaged>
where TReference : class?
where TNotNull : notnull
where TValue : struct
where TUnmanaged : unmanaged
{
private readonly IntRegistry _registry = new();
private readonly IntRegistry? _registry = new();
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public List<int> Values { get; } = new();
public List<int>? Values { get; } = new();
}
}
""";
@ -50,13 +55,20 @@ public class AutoRegisterExportedCollectionsGeneratorTests
namespace TestApp;
partial class Bootstrapper
partial class Bootstrapper<TReference, TNotNull, TValue, TUnmanaged>
where TReference : class?
where TNotNull : notnull
where TValue : struct
where TUnmanaged : unmanaged
{
private void __RegisterExportedCollections_Generated()
{
foreach (var item in Values)
if (this.Values is not null && this._registry is not null)
{
_registry.Register(item);
foreach (var __generatedItem in this.Values)
{
this._registry.Register(__generatedItem);
}
}
}
}

View File

@ -8,12 +8,27 @@ namespace GFramework.Godot.SourceGenerators.Behavior;
/// <summary>
/// 为标记了 <c>[AutoScene]</c> 的 Godot 节点生成场景行为样板。
/// </summary>
/// <remarks>
/// 该生成器会为兼容的非嵌套 <c>partial</c> Godot 节点类型生成 <c>SceneKeyStr</c> 与 <c>GetScene</c>
/// 以便通过 <c>SceneBehaviorFactory</c> 延迟创建并缓存场景行为实例。
/// 生成管线仅处理显式标记了 <c>AutoSceneAttribute</c> 的类,并在类型不满足基类、<c>partial</c>、
/// 成员冲突或属性参数约束时通过诊断停止生成,而不是静默回退到不完整输出。
/// </remarks>
[Generator]
public sealed class AutoSceneGenerator : IIncrementalGenerator
{
private const string AutoSceneAttributeMetadataName =
$"{PathContests.GodotSourceGeneratorsAbstractionsPath}.AutoSceneAttribute";
/// <summary>
/// 配置 <c>AutoScene</c> 的增量生成管线。
/// </summary>
/// <param name="context">用于注册语法筛选、语义转换和源输出阶段的增量生成上下文。</param>
/// <remarks>
/// 管线首先通过语法节点名称快速筛选潜在候选,再结合语义模型确认类型符号。
/// 最终输出阶段仅在 <c>AutoSceneAttribute</c>、<c>Godot.Node</c> 等依赖可解析且目标类型满足生成约束时产出源码;
/// 否则会报告对应诊断,或在宿主依赖缺失时直接跳过生成。
/// </remarks>
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var candidates = context.SyntaxProvider.CreateSyntaxProvider(
@ -226,9 +241,20 @@ public sealed class AutoSceneGenerator : IIncrementalGenerator
var constraints = new List<string>();
if (typeParameter.HasReferenceTypeConstraint)
constraints.Add("class");
{
constraints.Add(
typeParameter.ReferenceTypeConstraintNullableAnnotation == NullableAnnotation.Annotated
? "class?"
: "class");
}
if (typeParameter.HasValueTypeConstraint)
if (typeParameter.HasNotNullConstraint)
constraints.Add("notnull");
// unmanaged implies the value-type constraint and must replace struct in generated constraints.
if (typeParameter.HasUnmanagedTypeConstraint)
constraints.Add("unmanaged");
else if (typeParameter.HasValueTypeConstraint)
constraints.Add("struct");
constraints.AddRange(typeParameter.ConstraintTypes.Select(static constraint =>

View File

@ -8,6 +8,12 @@ namespace GFramework.Godot.SourceGenerators.Registration;
/// <summary>
/// 为导出集合生成批量注册样板方法。
/// </summary>
/// <remarks>
/// 该生成器会扫描标记了 <c>AutoRegisterExportedCollectionsAttribute</c> 的 <c>partial</c> 类型,
/// 为其中使用 <c>RegisterExportedCollectionAttribute</c> 声明的集合成员生成集中注册方法。
/// 仅当集合可枚举、元素类型可推导、注册表成员存在且可找到兼容的实例注册方法时才会输出代码;
/// 否则通过 <c>GF_AutoExport_001</c> 到 <c>GF_AutoExport_005</c> 以及公共 <c>ClassMustBePartial</c> 诊断显式阻止生成。
/// </remarks>
[Generator]
public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGenerator
{
@ -19,6 +25,14 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
private const string GeneratedMethodName = "__RegisterExportedCollections_Generated";
/// <summary>
/// 配置导出集合自动注册的增量生成管线。
/// </summary>
/// <param name="context">用于注册候选筛选、语义转换和最终源输出的增量生成上下文。</param>
/// <remarks>
/// 管线先通过语法名称筛选减少分析范围,再在输出阶段验证特性、集合形状、注册目标与方法签名。
/// 当依赖类型无法解析时,生成器不会报告噪声诊断而是直接跳过;当用户代码违反生成约束时,会报告明确诊断并停止该类型的生成。
/// </remarks>
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var candidates = context.SyntaxProvider.CreateSyntaxProvider(
@ -305,15 +319,22 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
foreach (var registration in registrations)
{
builder.Append(" foreach (var item in ");
builder.Append(" if (this.");
builder.Append(registration.CollectionMemberName);
builder.Append(" is not null && this.");
builder.Append(registration.RegistryMemberName);
builder.AppendLine(" is not null)");
builder.AppendLine(" {");
builder.Append(" foreach (var __generatedItem in this.");
builder.Append(registration.CollectionMemberName);
builder.AppendLine(")");
builder.AppendLine(" {");
builder.Append(" ");
builder.AppendLine(" {");
builder.Append(" this.");
builder.Append(registration.RegistryMemberName);
builder.Append('.');
builder.Append(registration.RegisterMethodName);
builder.AppendLine("(item);");
builder.AppendLine("(__generatedItem);");
builder.AppendLine(" }");
builder.AppendLine(" }");
}
@ -364,9 +385,20 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
var constraints = new List<string>();
if (typeParameter.HasReferenceTypeConstraint)
constraints.Add("class");
{
constraints.Add(
typeParameter.ReferenceTypeConstraintNullableAnnotation == NullableAnnotation.Annotated
? "class?"
: "class");
}
if (typeParameter.HasValueTypeConstraint)
if (typeParameter.HasNotNullConstraint)
constraints.Add("notnull");
// unmanaged implies the value-type constraint and must replace struct in generated constraints.
if (typeParameter.HasUnmanagedTypeConstraint)
constraints.Add("unmanaged");
else if (typeParameter.HasValueTypeConstraint)
constraints.Add("struct");
constraints.AddRange(typeParameter.ConstraintTypes.Select(static constraint =>