mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
feat(generator): 添加 AutoScene 和 AutoRegisterExportedCollections 源代码生成器
- 实现 AutoSceneGenerator 为标记了 [AutoScene] 的 Godot 节点生成场景行为样板 - 实现 AutoRegisterExportedCollectionsGenerator 为导出集合生成批量注册方法 - 添加完整的单元测试覆盖两种源代码生成器的功能和诊断 - 支持泛型类型参数约束的正确生成 - 提供详细的诊断信息帮助用户修复配置错误
This commit is contained in:
parent
ca1214f47f
commit
d21fac42b0
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 =>
|
||||
|
||||
@ -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 =>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user