From d21fac42b02efcc8ae695a3286d353c9b0813a5d Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Mon, 13 Apr 2026 11:25:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(generator):=20=E6=B7=BB=E5=8A=A0=20AutoSce?= =?UTF-8?q?ne=20=E5=92=8C=20AutoRegisterExportedCollections=20=E6=BA=90?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=94=9F=E6=88=90=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现 AutoSceneGenerator 为标记了 [AutoScene] 的 Godot 节点生成场景行为样板 - 实现 AutoRegisterExportedCollectionsGenerator 为导出集合生成批量注册方法 - 添加完整的单元测试覆盖两种源代码生成器的功能和诊断 - 支持泛型类型参数约束的正确生成 - 提供详细的诊断信息帮助用户修复配置错误 --- .../Behavior/AutoSceneGeneratorTests.cs | 86 +++++++++++++++++++ ...gisterExportedCollectionsGeneratorTests.cs | 24 ++++-- .../Behavior/AutoSceneGenerator.cs | 30 ++++++- ...utoRegisterExportedCollectionsGenerator.cs | 44 ++++++++-- 4 files changed, 170 insertions(+), 14 deletions(-) diff --git a/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs index d6b73929..0e429cbc 100644 --- a/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs @@ -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 owner, string key) + where T : Node + { + return null!; + } + } + } + + namespace TestApp + { + [AutoScene("Gameplay")] + public partial class GameplayRoot : Node2D + where TReference : class? + where TNotNull : notnull + where TValue : struct + where TUnmanaged : unmanaged + { + } + } + """; + + const string expected = """ + // + #nullable enable + + namespace TestApp; + + partial class GameplayRoot + 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.RunAsync( + source, + ("TestApp_GameplayRoot.AutoScene.g.cs", expected)); + } } diff --git a/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs index 013be561..cd255d1d 100644 --- a/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs @@ -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 + 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 Values { get; } = new(); + public List? Values { get; } = new(); } } """; @@ -50,13 +55,20 @@ public class AutoRegisterExportedCollectionsGeneratorTests namespace TestApp; - partial class Bootstrapper + partial class Bootstrapper + 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); + } } } } diff --git a/GFramework.Godot.SourceGenerators/Behavior/AutoSceneGenerator.cs b/GFramework.Godot.SourceGenerators/Behavior/AutoSceneGenerator.cs index a0b791f5..d505222b 100644 --- a/GFramework.Godot.SourceGenerators/Behavior/AutoSceneGenerator.cs +++ b/GFramework.Godot.SourceGenerators/Behavior/AutoSceneGenerator.cs @@ -8,12 +8,27 @@ namespace GFramework.Godot.SourceGenerators.Behavior; /// /// 为标记了 [AutoScene] 的 Godot 节点生成场景行为样板。 /// +/// +/// 该生成器会为兼容的非嵌套 partial Godot 节点类型生成 SceneKeyStrGetScene, +/// 以便通过 SceneBehaviorFactory 延迟创建并缓存场景行为实例。 +/// 生成管线仅处理显式标记了 AutoSceneAttribute 的类,并在类型不满足基类、partial、 +/// 成员冲突或属性参数约束时通过诊断停止生成,而不是静默回退到不完整输出。 +/// [Generator] public sealed class AutoSceneGenerator : IIncrementalGenerator { private const string AutoSceneAttributeMetadataName = $"{PathContests.GodotSourceGeneratorsAbstractionsPath}.AutoSceneAttribute"; + /// + /// 配置 AutoScene 的增量生成管线。 + /// + /// 用于注册语法筛选、语义转换和源输出阶段的增量生成上下文。 + /// + /// 管线首先通过语法节点名称快速筛选潜在候选,再结合语义模型确认类型符号。 + /// 最终输出阶段仅在 AutoSceneAttributeGodot.Node 等依赖可解析且目标类型满足生成约束时产出源码; + /// 否则会报告对应诊断,或在宿主依赖缺失时直接跳过生成。 + /// public void Initialize(IncrementalGeneratorInitializationContext context) { var candidates = context.SyntaxProvider.CreateSyntaxProvider( @@ -226,9 +241,20 @@ public sealed class AutoSceneGenerator : IIncrementalGenerator var constraints = new List(); 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 => diff --git a/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs b/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs index 07e6757b..b22bc003 100644 --- a/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs +++ b/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs @@ -8,6 +8,12 @@ namespace GFramework.Godot.SourceGenerators.Registration; /// /// 为导出集合生成批量注册样板方法。 /// +/// +/// 该生成器会扫描标记了 AutoRegisterExportedCollectionsAttributepartial 类型, +/// 为其中使用 RegisterExportedCollectionAttribute 声明的集合成员生成集中注册方法。 +/// 仅当集合可枚举、元素类型可推导、注册表成员存在且可找到兼容的实例注册方法时才会输出代码; +/// 否则通过 GF_AutoExport_001GF_AutoExport_005 以及公共 ClassMustBePartial 诊断显式阻止生成。 +/// [Generator] public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGenerator { @@ -19,6 +25,14 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener private const string GeneratedMethodName = "__RegisterExportedCollections_Generated"; + /// + /// 配置导出集合自动注册的增量生成管线。 + /// + /// 用于注册候选筛选、语义转换和最终源输出的增量生成上下文。 + /// + /// 管线先通过语法名称筛选减少分析范围,再在输出阶段验证特性、集合形状、注册目标与方法签名。 + /// 当依赖类型无法解析时,生成器不会报告噪声诊断而是直接跳过;当用户代码违反生成约束时,会报告明确诊断并停止该类型的生成。 + /// 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(); 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 =>