From 80acf84e957b92c6dbd3542255df202790bf2c64 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Mon, 13 Apr 2026 10:51:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(godot):=20=E6=B7=BB=E5=8A=A0AutoScene?= =?UTF-8?q?=E5=92=8CAutoRegisterExportedCollections=E6=BA=90=E4=BB=A3?= =?UTF-8?q?=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为导出集合生成批量注册样板方法 - 添加AutoBehaviorDiagnostics和AutoRegisterExportedCollectionsDiagnostics诊断描述符 - 创建AnalyzerReleases.Unshipped.md文件跟踪新的分析器规则 - 添加完整的单元测试覆盖两个生成器的功能和错误情况 - 更新.gitignore文件排除dotnet-home和脚本缓存目录 --- .gitignore | 4 +- .../Behavior/AutoSceneGeneratorTests.cs | 48 +++++++++++++++++ ...gisterExportedCollectionsGeneratorTests.cs | 54 +++++++++++++++++++ .../AnalyzerReleases.Unshipped.md | 2 + .../Behavior/AutoSceneGenerator.cs | 29 +++++++++- .../Diagnostics/AutoBehaviorDiagnostics.cs | 27 ++++++++++ ...oRegisterExportedCollectionsDiagnostics.cs | 30 +++++++++++ ...utoRegisterExportedCollectionsGenerator.cs | 14 ++++- 8 files changed, 203 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 315acfbf..d6abff17 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ riderModule.iml /_ReSharper.Caches/ GFramework.sln.DotSettings.user .idea/ +dotnet-home/ +scripts/__pycache__/ # ai opencode.json .claude/settings.local.json @@ -14,4 +16,4 @@ docs/.omc/ docs/.vitepress/cache/ local-plan/ # tool -.venv/ \ No newline at end of file +.venv/ diff --git a/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs index c60d1f18..d6b73929 100644 --- a/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs @@ -82,4 +82,52 @@ public class AutoSceneGeneratorTests source, ("TestApp_GameplayRoot.AutoScene.g.cs", expected)); } + + [Test] + public async Task Reports_Diagnostic_When_AutoScene_Arguments_Are_Invalid() + { + const string source = """ + 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() { } + } + } + + namespace Godot + { + public class Node { } + public class Node2D : Node { } + } + + namespace TestApp + { + [{|#0:AutoScene|}] + public partial class GameplayRoot : Node2D + { + } + } + """; + + var test = new CSharpSourceGeneratorTest + { + TestState = + { + Sources = { source } + }, + DisabledDiagnostics = { "GF_Common_Trace_001" } + }; + + test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoBehavior_004", DiagnosticSeverity.Error) + .WithLocation(0) + .WithArguments("AutoSceneAttribute", "GameplayRoot", "a single string scene key argument")); + + await test.RunAsync(); + } } diff --git a/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs index 0202b824..013be561 100644 --- a/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs @@ -67,4 +67,58 @@ public class AutoRegisterExportedCollectionsGeneratorTests source, ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)); } + + [Test] + public async Task Reports_Diagnostic_When_Collection_Element_Type_Cannot_Be_Inferred() + { + const string source = """ + using System; + using System.Collections; + 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) { } + } + + [AutoRegisterExportedCollections] + public partial class Bootstrapper + { + private readonly IntRegistry _registry = new(); + + [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] + public IEnumerable {|#0:Values|} { get; } = new ArrayList(); + } + } + """; + + var test = new CSharpSourceGeneratorTest + { + TestState = + { + Sources = { source } + }, + DisabledDiagnostics = { "GF_Common_Trace_001" } + }; + + test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_005", DiagnosticSeverity.Error) + .WithLocation(0) + .WithArguments("Values")); + + await test.RunAsync(); + } } diff --git a/GFramework.Godot.SourceGenerators/AnalyzerReleases.Unshipped.md b/GFramework.Godot.SourceGenerators/AnalyzerReleases.Unshipped.md index 42e69dc7..2c852e8d 100644 --- a/GFramework.Godot.SourceGenerators/AnalyzerReleases.Unshipped.md +++ b/GFramework.Godot.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -24,7 +24,9 @@ GF_AutoBehavior_001 | GFramework.Godot.SourceGenerators.Behavior | Error | AutoBehaviorDiagnostics GF_AutoBehavior_002 | GFramework.Godot.SourceGenerators.Behavior | Error | AutoBehaviorDiagnostics GF_AutoBehavior_003 | GFramework.Godot.SourceGenerators.Behavior | Error | AutoBehaviorDiagnostics + GF_AutoBehavior_004 | GFramework.Godot.SourceGenerators.Behavior | Error | AutoBehaviorDiagnostics GF_AutoExport_001 | GFramework.Godot.SourceGenerators.Registration | Error | AutoRegisterExportedCollectionsDiagnostics GF_AutoExport_002 | GFramework.Godot.SourceGenerators.Registration | Error | AutoRegisterExportedCollectionsDiagnostics GF_AutoExport_003 | GFramework.Godot.SourceGenerators.Registration | Error | AutoRegisterExportedCollectionsDiagnostics GF_AutoExport_004 | GFramework.Godot.SourceGenerators.Registration | Error | AutoRegisterExportedCollectionsDiagnostics + GF_AutoExport_005 | GFramework.Godot.SourceGenerators.Registration | Error | AutoRegisterExportedCollectionsDiagnostics diff --git a/GFramework.Godot.SourceGenerators/Behavior/AutoSceneGenerator.cs b/GFramework.Godot.SourceGenerators/Behavior/AutoSceneGenerator.cs index 4fc9f874..179ef8a9 100644 --- a/GFramework.Godot.SourceGenerators/Behavior/AutoSceneGenerator.cs +++ b/GFramework.Godot.SourceGenerators/Behavior/AutoSceneGenerator.cs @@ -1,7 +1,6 @@ using GFramework.Godot.SourceGenerators.Diagnostics; using GFramework.SourceGenerators.Common.Constants; using GFramework.SourceGenerators.Common.Diagnostics; -using GFramework.SourceGenerators.Common.Extensions; namespace GFramework.Godot.SourceGenerators.Behavior; @@ -79,7 +78,7 @@ public sealed class AutoSceneGenerator : IIncrementalGenerator continue; } - if (attribute.ConstructorArguments.Length != 1 || attribute.ConstructorArguments[0].Value is not string key) + if (!TryGetSceneKey(context, candidate.TypeSymbol, attribute, out var key)) continue; context.AddSource(GetHintName(candidate.TypeSymbol), GenerateSource(candidate.TypeSymbol, key)); @@ -122,6 +121,32 @@ public sealed class AutoSceneGenerator : IIncrementalGenerator return false; } + private static bool TryGetSceneKey( + SourceProductionContext context, + INamedTypeSymbol typeSymbol, + AttributeData attribute, + out string key) + { + key = string.Empty; + + if (attribute.ConstructorArguments.Length == 1 && + attribute.ConstructorArguments[0].Value is string sceneKey) + { + key = sceneKey; + return true; + } + + context.ReportDiagnostic(Diagnostic.Create( + AutoBehaviorDiagnostics.InvalidAttributeArguments, + attribute.ApplicationSyntaxReference?.GetSyntax().GetLocation() ?? + typeSymbol.Locations.FirstOrDefault() ?? + Location.None, + "AutoSceneAttribute", + typeSymbol.Name, + "a single string scene key argument")); + return false; + } + private static string GenerateSource(INamedTypeSymbol typeSymbol, string key) { var builder = new StringBuilder(); diff --git a/GFramework.Godot.SourceGenerators/Diagnostics/AutoBehaviorDiagnostics.cs b/GFramework.Godot.SourceGenerators/Diagnostics/AutoBehaviorDiagnostics.cs index fd78d27a..42c9dbec 100644 --- a/GFramework.Godot.SourceGenerators/Diagnostics/AutoBehaviorDiagnostics.cs +++ b/GFramework.Godot.SourceGenerators/Diagnostics/AutoBehaviorDiagnostics.cs @@ -2,10 +2,20 @@ using GFramework.SourceGenerators.Common.Constants; namespace GFramework.Godot.SourceGenerators.Diagnostics; +/// +/// 定义行为类自动生成器使用的诊断描述符。 +/// +/// +/// 这些规则覆盖 AutoSceneAutoUiPage 等行为生成器的常见使用约束, +/// 以便在生成被跳过前向调用方报告明确的失败原因。 +/// internal static class AutoBehaviorDiagnostics { private const string Category = $"{PathContests.GodotNamespace}.SourceGenerators.Behavior"; + /// + /// 报告行为生成器不支持在嵌套类型上运行。 + /// public static readonly DiagnosticDescriptor NestedClassNotSupported = new( "GF_AutoBehavior_001", "Auto behavior generators do not support nested classes", @@ -14,6 +24,9 @@ internal static class AutoBehaviorDiagnostics DiagnosticSeverity.Error, true); + /// + /// 报告目标类型没有继承生成器要求的 Godot 基类。 + /// public static readonly DiagnosticDescriptor MissingBaseType = new( "GF_AutoBehavior_002", "Auto behavior generators require a compatible base type", @@ -22,6 +35,9 @@ internal static class AutoBehaviorDiagnostics DiagnosticSeverity.Error, true); + /// + /// 报告 UI 页面声明中使用了不存在的 UiLayer 名称。 + /// public static readonly DiagnosticDescriptor InvalidUiLayerName = new( "GF_AutoBehavior_003", "Unknown UiLayer name", @@ -29,4 +45,15 @@ internal static class AutoBehaviorDiagnostics Category, DiagnosticSeverity.Error, true); + + /// + /// 报告行为生成器特性参数不满足约定签名,导致生成器无法推导所需元数据。 + /// + public static readonly DiagnosticDescriptor InvalidAttributeArguments = new( + "GF_AutoBehavior_004", + "Auto behavior attribute arguments are invalid", + "Attribute '{0}' on '{1}' must provide {2}", + Category, + DiagnosticSeverity.Error, + true); } diff --git a/GFramework.Godot.SourceGenerators/Diagnostics/AutoRegisterExportedCollectionsDiagnostics.cs b/GFramework.Godot.SourceGenerators/Diagnostics/AutoRegisterExportedCollectionsDiagnostics.cs index bb449e0f..9c7463cc 100644 --- a/GFramework.Godot.SourceGenerators/Diagnostics/AutoRegisterExportedCollectionsDiagnostics.cs +++ b/GFramework.Godot.SourceGenerators/Diagnostics/AutoRegisterExportedCollectionsDiagnostics.cs @@ -2,10 +2,20 @@ using GFramework.SourceGenerators.Common.Constants; namespace GFramework.Godot.SourceGenerators.Diagnostics; +/// +/// 定义导出集合自动注册生成器使用的诊断描述符。 +/// +/// +/// 这些规则用于在源生成阶段验证集合成员、注册目标以及元素类型推导, +/// 避免把配置错误延后到生成代码编译或运行时才暴露。 +/// internal static class AutoRegisterExportedCollectionsDiagnostics { private const string Category = $"{PathContests.GodotNamespace}.SourceGenerators.Registration"; + /// + /// 报告自动注册生成器不支持嵌套类型。 + /// public static readonly DiagnosticDescriptor NestedClassNotSupported = new( "GF_AutoExport_001", "AutoRegisterExportedCollections does not support nested classes", @@ -14,6 +24,9 @@ internal static class AutoRegisterExportedCollectionsDiagnostics DiagnosticSeverity.Error, true); + /// + /// 报告特性引用的注册表成员在宿主类型上不存在。 + /// public static readonly DiagnosticDescriptor RegistryMemberNotFound = new( "GF_AutoExport_002", "Registry member was not found", @@ -22,6 +35,9 @@ internal static class AutoRegisterExportedCollectionsDiagnostics DiagnosticSeverity.Error, true); + /// + /// 报告注册表上未找到与集合元素类型兼容的注册方法。 + /// public static readonly DiagnosticDescriptor RegisterMethodNotFound = new( "GF_AutoExport_003", "Register method was not found", @@ -30,6 +46,9 @@ internal static class AutoRegisterExportedCollectionsDiagnostics DiagnosticSeverity.Error, true); + /// + /// 报告被标记成员不是可枚举集合,因此无法执行批量注册。 + /// public static readonly DiagnosticDescriptor CollectionTypeMustBeEnumerable = new( "GF_AutoExport_004", "Exported collection must be enumerable", @@ -37,4 +56,15 @@ internal static class AutoRegisterExportedCollectionsDiagnostics Category, DiagnosticSeverity.Error, true); + + /// + /// 报告集合元素类型无法在编译期推导,因此无法安全匹配注册方法。 + /// + public static readonly DiagnosticDescriptor CollectionElementTypeCouldNotBeInferred = new( + "GF_AutoExport_005", + "Exported collection element type could not be inferred", + "Member '{0}' must expose a generic enumerable element type to use RegisterExportedCollection safely", + Category, + DiagnosticSeverity.Error, + true); } diff --git a/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs b/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs index a051ff2d..802f4c26 100644 --- a/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs +++ b/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs @@ -1,7 +1,6 @@ using GFramework.Godot.SourceGenerators.Diagnostics; using GFramework.SourceGenerators.Common.Constants; using GFramework.SourceGenerators.Common.Diagnostics; -using GFramework.SourceGenerators.Common.Extensions; namespace GFramework.Godot.SourceGenerators.Registration; @@ -208,12 +207,23 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener return false; var elementType = TryGetElementType(collectionType); + if (elementType is null) + { + // Non-generic IEnumerable exposes elements as object at compile time, which is not safe + // for validating or generating a strongly typed registry call. + context.ReportDiagnostic(Diagnostic.Create( + AutoRegisterExportedCollectionsDiagnostics.CollectionElementTypeCouldNotBeInferred, + collectionMember.Locations.FirstOrDefault() ?? Location.None, + collectionMember.Name)); + return false; + } + var hasCompatibleMethod = registryType.GetMembers(registerMethodName) .OfType() .Any(method => !method.IsStatic && method.Parameters.Length == 1 && - (elementType is null || elementType.IsAssignableTo(method.Parameters[0].Type as INamedTypeSymbol))); + elementType.IsAssignableTo(method.Parameters[0].Type as INamedTypeSymbol)); if (!hasCompatibleMethod) {