From 7e45197698705e184e4c3348d4986d5129de6aaf Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Fri, 24 Apr 2026 14:06:41 +0800 Subject: [PATCH] =?UTF-8?q?test(godot-source-generators):=20=E6=B8=85?= =?UTF-8?q?=E7=90=86=E6=BA=90=E7=94=9F=E6=88=90=E5=99=A8=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E8=AD=A6=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构 GFramework.Godot.SourceGenerators.Tests 的测试模板与诊断辅助,清除项目内全部 analyzer warning - 更新 GeneratorTest 异步等待与 analyzer-warning-reduction 跟踪文档,记录批次验证结果与恢复点 --- .../Behavior/AutoSceneGeneratorTests.cs | 246 +++--- .../Behavior/AutoUiPageGeneratorTests.cs | 318 ++++---- .../BindNodeSignalGeneratorTests.cs | 720 ++++++------------ .../Core/GeneratorTest.cs | 4 +- .../GetNode/GetNodeGeneratorTests.cs | 301 ++++---- .../GodotProjectMetadataGeneratorTests.cs | 254 +++--- ...gisterExportedCollectionsGeneratorTests.cs | 511 ++++++------- .../analyzer-warning-reduction-tracking.md | 42 +- .../analyzer-warning-reduction-trace.md | 31 + 9 files changed, 1052 insertions(+), 1375 deletions(-) diff --git a/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs index 2c6dc972..a4bb59cf 100644 --- a/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs @@ -6,57 +6,61 @@ namespace GFramework.Godot.SourceGenerators.Tests.Behavior; [TestFixture] public class AutoSceneGeneratorTests { + private const string AutoSceneAttributeWithKeyDeclaration = """ + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + public sealed class AutoSceneAttribute : Attribute + { + public AutoSceneAttribute(string key) { } + } + """; + + private const string AutoSceneAttributeWithoutKeyDeclaration = """ + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + public sealed class AutoSceneAttribute : Attribute + { + public AutoSceneAttribute() { } + } + """; + + private const string NodeTypes = """ + public class Node { } + public class Node2D : Node { } + """; + + private const string SceneBehaviorInfrastructure = """ + 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!; + } + } + } + """; + [Test] public async Task Generates_Scene_Behavior_Boilerplate() { - const string source = """ - using System; - using GFramework.Godot.SourceGenerators.Abstractions.UI; - using Godot; - - namespace GFramework.Godot.SourceGenerators.Abstractions.UI - { - [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 - { - } - } - """; + string source = CreateAutoSceneSource( + AutoSceneAttributeWithKeyDeclaration, + """ + [AutoScene("Gameplay")] + public partial class GameplayRoot : Node2D + { + } + """, + includeBehaviorInfrastructure: true); const string expected = """ // @@ -80,40 +84,20 @@ public class AutoSceneGeneratorTests await GeneratorTest.RunAsync( source, - ("TestApp_GameplayRoot.AutoScene.g.cs", expected)); + ("TestApp_GameplayRoot.AutoScene.g.cs", expected)).ConfigureAwait(false); } [Test] public async Task Reports_Diagnostic_When_AutoScene_Arguments_Are_Invalid() { - const string source = """ - using System; - using GFramework.Godot.SourceGenerators.Abstractions.UI; - using Godot; - - namespace GFramework.Godot.SourceGenerators.Abstractions.UI - { - [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 - { - } - } - """; + string source = CreateAutoSceneSource( + AutoSceneAttributeWithoutKeyDeclaration, + """ + [{|#0:AutoScene|}] + public partial class GameplayRoot : Node2D + { + } + """); var test = new CSharpSourceGeneratorTest { @@ -128,65 +112,26 @@ public class AutoSceneGeneratorTests .WithLocation(0) .WithArguments("AutoSceneAttribute", "GameplayRoot", "a single string scene key argument")); - await test.RunAsync(); + await test.RunAsync().ConfigureAwait(false); } [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.UI; - using Godot; - - namespace GFramework.Godot.SourceGenerators.Abstractions.UI - { - [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 - { - } - } - """; + string source = CreateAutoSceneSource( + AutoSceneAttributeWithKeyDeclaration, + """ + [AutoScene("Gameplay")] + public partial class GameplayRoot : Node2D + where TReference : class? + where TNotNull : notnull + where TValue : struct + where TUnmanaged : unmanaged + { + } + """, + includeBehaviorInfrastructure: true, + nullableEnabled: true); const string expected = """ // @@ -214,7 +159,7 @@ public class AutoSceneGeneratorTests await GeneratorTest.RunAsync( source, - ("TestApp_GameplayRoot.AutoScene.g.cs", expected)); + ("TestApp_GameplayRoot.AutoScene.g.cs", expected)).ConfigureAwait(false); } /// @@ -267,7 +212,7 @@ public class AutoSceneGeneratorTests .WithLocation(0) .WithArguments("GameplayRoot", "SceneKeyStr")); - await test.RunAsync(); + await test.RunAsync().ConfigureAwait(false); } /// @@ -326,6 +271,39 @@ public class AutoSceneGeneratorTests .WithLocation(0) .WithArguments("GameplayRoot", "__autoSceneBehavior_Generated")); - await test.RunAsync(); + await test.RunAsync().ConfigureAwait(false); + } + + private static string CreateAutoSceneSource( + string attributeDeclaration, + string testAppSource, + bool includeBehaviorInfrastructure = false, + bool nullableEnabled = false) + { + string nullableDirective = nullableEnabled ? "#nullable enable\n" : string.Empty; + string infrastructure = includeBehaviorInfrastructure + ? $"{Environment.NewLine}{Environment.NewLine}{SceneBehaviorInfrastructure}" + : string.Empty; + + return $$""" + {{nullableDirective}}using System; + using GFramework.Godot.SourceGenerators.Abstractions.UI; + using Godot; + + namespace GFramework.Godot.SourceGenerators.Abstractions.UI + { + {{attributeDeclaration}} + } + + namespace Godot + { + {{NodeTypes}} + }{{infrastructure}} + + namespace TestApp + { + {{testAppSource}} + } + """; } } diff --git a/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoUiPageGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoUiPageGeneratorTests.cs index 2b8aca67..5d667309 100644 --- a/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoUiPageGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoUiPageGeneratorTests.cs @@ -6,69 +6,85 @@ namespace GFramework.Godot.SourceGenerators.Tests.Behavior; [TestFixture] public class AutoUiPageGeneratorTests { + private const string AutoUiPageAttributeWithLayerDeclaration = """ + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + public sealed class AutoUiPageAttribute : Attribute + { + public AutoUiPageAttribute(string key, string layerName) { } + } + """; + + private const string AutoUiPageAttributeWithoutLayerDeclaration = """ + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + public sealed class AutoUiPageAttribute : Attribute + { + public AutoUiPageAttribute(string key) { } + } + """; + + private const string CanvasNodeTypes = """ + public class Node { } + public class CanvasItem : Node { } + public class Control : CanvasItem { } + """; + + private const string UiLayerFullEnum = """ + namespace GFramework.Game.Abstractions.Enums + { + public enum UiLayer + { + Page, + Overlay, + Modal + } + } + """; + + private const string UiLayerPageOnlyEnum = """ + namespace GFramework.Game.Abstractions.Enums + { + public enum UiLayer + { + Page + } + } + """; + + private const string UiBehaviorInfrastructure = """ + namespace GFramework.Game.Abstractions.UI + { + public interface IUiPageBehavior { } + } + + namespace GFramework.Godot.UI + { + using GFramework.Game.Abstractions.Enums; + using GFramework.Game.Abstractions.UI; + using Godot; + + public static class UiPageBehaviorFactory + { + public static IUiPageBehavior Create(T owner, string key, UiLayer layer) + where T : CanvasItem + { + return null!; + } + } + } + """; + [Test] public async Task Generates_Ui_Page_Behavior_Boilerplate() { - const string source = """ - using System; - using GFramework.Godot.SourceGenerators.Abstractions.UI; - using Godot; - - namespace GFramework.Godot.SourceGenerators.Abstractions.UI - { - [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - public sealed class AutoUiPageAttribute : Attribute - { - public AutoUiPageAttribute(string key, string layerName) { } - } - } - - namespace Godot - { - public class Node { } - public class CanvasItem : Node { } - public class Control : CanvasItem { } - } - - namespace GFramework.Game.Abstractions.Enums - { - public enum UiLayer - { - Page, - Overlay, - Modal - } - } - - namespace GFramework.Game.Abstractions.UI - { - public interface IUiPageBehavior { } - } - - namespace GFramework.Godot.UI - { - using GFramework.Game.Abstractions.Enums; - using GFramework.Game.Abstractions.UI; - using Godot; - - public static class UiPageBehaviorFactory - { - public static IUiPageBehavior Create(T owner, string key, UiLayer layer) - where T : CanvasItem - { - return null!; - } - } - } - - namespace TestApp - { - [AutoUiPage("MainMenu", "Page")] - public partial class MainMenu : Control - { - } - } - """; + string source = CreateAutoUiPageSource( + AutoUiPageAttributeWithLayerDeclaration, + UiLayerFullEnum, + """ + [AutoUiPage("MainMenu", "Page")] + public partial class MainMenu : Control + { + } + """); const string expected = """ // @@ -92,70 +108,21 @@ public class AutoUiPageGeneratorTests await GeneratorTest.RunAsync( source, - ("TestApp_MainMenu.AutoUiPage.g.cs", expected)); + ("TestApp_MainMenu.AutoUiPage.g.cs", expected)).ConfigureAwait(false); } [Test] public async Task Reports_Diagnostic_When_AutoUiPage_Attribute_Arguments_Are_Invalid() { - const string source = """ - using System; - using GFramework.Godot.SourceGenerators.Abstractions.UI; - using Godot; - - namespace GFramework.Godot.SourceGenerators.Abstractions.UI - { - [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - public sealed class AutoUiPageAttribute : Attribute - { - public AutoUiPageAttribute(string key) { } - } - } - - namespace Godot - { - public class Node { } - public class CanvasItem : Node { } - public class Control : CanvasItem { } - } - - namespace GFramework.Game.Abstractions.Enums - { - public enum UiLayer - { - Page - } - } - - namespace GFramework.Game.Abstractions.UI - { - public interface IUiPageBehavior { } - } - - namespace GFramework.Godot.UI - { - using GFramework.Game.Abstractions.Enums; - using GFramework.Game.Abstractions.UI; - using Godot; - - public static class UiPageBehaviorFactory - { - public static IUiPageBehavior Create(T owner, string key, UiLayer layer) - where T : CanvasItem - { - return null!; - } - } - } - - namespace TestApp - { - [{|#0:AutoUiPage("MainMenu")|}] - public partial class MainMenu : Control - { - } - } - """; + string source = CreateAutoUiPageSource( + AutoUiPageAttributeWithoutLayerDeclaration, + UiLayerPageOnlyEnum, + """ + [{|#0:AutoUiPage("MainMenu")|}] + public partial class MainMenu : Control + { + } + """); var test = new CSharpSourceGeneratorTest { @@ -174,74 +141,25 @@ public class AutoUiPageGeneratorTests "MainMenu", "a string key argument and a string UiLayer name argument")); - await test.RunAsync(); + await test.RunAsync().ConfigureAwait(false); } [Test] public async Task Generates_Type_Constraints_For_ClassNullable_NotNull_And_Unmanaged() { - const string source = """ - #nullable enable - using System; - using GFramework.Godot.SourceGenerators.Abstractions.UI; - using Godot; - - namespace GFramework.Godot.SourceGenerators.Abstractions.UI - { - [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - public sealed class AutoUiPageAttribute : Attribute - { - public AutoUiPageAttribute(string key, string layerName) { } - } - } - - namespace Godot - { - public class Node { } - public class CanvasItem : Node { } - public class Control : CanvasItem { } - } - - namespace GFramework.Game.Abstractions.Enums - { - public enum UiLayer - { - Page - } - } - - namespace GFramework.Game.Abstractions.UI - { - public interface IUiPageBehavior { } - } - - namespace GFramework.Godot.UI - { - using GFramework.Game.Abstractions.Enums; - using GFramework.Game.Abstractions.UI; - using Godot; - - public static class UiPageBehaviorFactory - { - public static IUiPageBehavior Create(T owner, string key, UiLayer layer) - where T : CanvasItem - { - return null!; - } - } - } - - namespace TestApp - { - [AutoUiPage("MainMenu", "Page")] - public partial class MainMenu : Control - where TReference : class? - where TNotNull : notnull - where TUnmanaged : unmanaged - { - } - } - """; + string source = CreateAutoUiPageSource( + AutoUiPageAttributeWithLayerDeclaration, + UiLayerPageOnlyEnum, + """ + [AutoUiPage("MainMenu", "Page")] + public partial class MainMenu : Control + where TReference : class? + where TNotNull : notnull + where TUnmanaged : unmanaged + { + } + """, + nullableEnabled: true); const string expected = """ // @@ -268,6 +186,40 @@ public class AutoUiPageGeneratorTests await GeneratorTest.RunAsync( source, - ("TestApp_MainMenu.AutoUiPage.g.cs", expected)); + ("TestApp_MainMenu.AutoUiPage.g.cs", expected)).ConfigureAwait(false); + } + + private static string CreateAutoUiPageSource( + string attributeDeclaration, + string uiLayerDeclaration, + string testAppSource, + bool nullableEnabled = false) + { + string nullableDirective = nullableEnabled ? "#nullable enable\n" : string.Empty; + + return $$""" + {{nullableDirective}}using System; + using GFramework.Godot.SourceGenerators.Abstractions.UI; + using Godot; + + namespace GFramework.Godot.SourceGenerators.Abstractions.UI + { + {{attributeDeclaration}} + } + + namespace Godot + { + {{CanvasNodeTypes}} + } + + {{uiLayerDeclaration}} + + {{UiBehaviorInfrastructure}} + + namespace TestApp + { + {{testAppSource}} + } + """; } } diff --git a/GFramework.Godot.SourceGenerators.Tests/BindNodeSignal/BindNodeSignalGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/BindNodeSignal/BindNodeSignalGeneratorTests.cs index 7f71f210..661023f7 100644 --- a/GFramework.Godot.SourceGenerators.Tests/BindNodeSignal/BindNodeSignalGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/BindNodeSignal/BindNodeSignalGeneratorTests.cs @@ -8,93 +8,103 @@ namespace GFramework.Godot.SourceGenerators.Tests.BindNodeSignal; [TestFixture] public class BindNodeSignalGeneratorTests { + private const string BindNodeSignalAttributeDeclaration = """ + [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + public sealed class BindNodeSignalAttribute : Attribute + { + public BindNodeSignalAttribute(string nodeFieldName, string signalName) + { + NodeFieldName = nodeFieldName; + SignalName = signalName; + } + + public string NodeFieldName { get; } + + public string SignalName { get; } + } + """; + + private const string GetNodeAttributeDeclaration = """ + [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] + public sealed class GetNodeAttribute : Attribute + { + } + """; + + private const string EmptyNodeType = """ + public class Node + { + } + """; + + private const string LifecycleNodeType = """ + public class Node + { + public virtual void _Ready() {} + + public virtual void _ExitTree() {} + } + """; + + private const string ButtonType = """ + public class Button : Node + { + public event Action? Pressed + { + add {} + remove {} + } + } + """; + + private const string SpinBoxType = """ + public class SpinBox : Node + { + public delegate void ValueChangedEventHandler(double value); + + public event ValueChangedEventHandler? ValueChanged + { + add {} + remove {} + } + } + """; + /// /// 验证生成器会为已有生命周期调用生成成对的绑定与解绑方法。 /// [Test] public async Task Generates_Bind_And_Unbind_Methods_For_Existing_Lifecycle_Hooks() { - const string source = """ - using System; - using GFramework.Godot.SourceGenerators.Abstractions; - using Godot; + string source = CreateHudSource( + CreateAbstractionsSource(BindNodeSignalAttributeDeclaration), + """ + private Button _startButton = null!; + private SpinBox _startOreSpinBox = null!; - namespace GFramework.Godot.SourceGenerators.Abstractions - { - [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - public sealed class BindNodeSignalAttribute : Attribute - { - public BindNodeSignalAttribute(string nodeFieldName, string signalName) - { - NodeFieldName = nodeFieldName; - SignalName = signalName; - } + [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] + private void OnStartButtonPressed() + { + } - public string NodeFieldName { get; } + [BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))] + private void OnStartOreValueChanged(double value) + { + } - public string SignalName { get; } - } - } + public override void _Ready() + { + __BindNodeSignals_Generated(); + } - namespace Godot - { - public class Node - { - public virtual void _Ready() {} - - public virtual void _ExitTree() {} - } - - public class Button : Node - { - public event Action? Pressed - { - add {} - remove {} - } - } - - public class SpinBox : Node - { - public delegate void ValueChangedEventHandler(double value); - - public event ValueChangedEventHandler? ValueChanged - { - add {} - remove {} - } - } - } - - namespace TestApp - { - public partial class Hud : Node - { - private Button _startButton = null!; - private SpinBox _startOreSpinBox = null!; - - [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] - private void OnStartButtonPressed() - { - } - - [BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))] - private void OnStartOreValueChanged(double value) - { - } - - public override void _Ready() - { - __BindNodeSignals_Generated(); - } - - public override void _ExitTree() - { - __UnbindNodeSignals_Generated(); - } - } - } - """; + public override void _ExitTree() + { + __UnbindNodeSignals_Generated(); + } + """, + LifecycleNodeType, + ButtonType, + SpinBoxType); const string expected = """ // @@ -121,7 +131,7 @@ public class BindNodeSignalGeneratorTests await GeneratorTest.RunAsync( source, - ("TestApp_Hud.BindNodeSignal.g.cs", expected)); + ("TestApp_Hud.BindNodeSignal.g.cs", expected)).ConfigureAwait(false); } /// @@ -130,70 +140,23 @@ public class BindNodeSignalGeneratorTests [Test] public async Task Generates_Multiple_Subscriptions_For_The_Same_Handler_And_Coexists_With_GetNode() { - const string source = """ - using System; - using GFramework.Godot.SourceGenerators.Abstractions; - using Godot; + string source = CreateHudSource( + CreateAbstractionsSource(BindNodeSignalAttributeDeclaration, GetNodeAttributeDeclaration), + """ + [GetNode] + private Button _startButton = null!; - namespace GFramework.Godot.SourceGenerators.Abstractions - { - [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - public sealed class BindNodeSignalAttribute : Attribute - { - public BindNodeSignalAttribute(string nodeFieldName, string signalName) - { - NodeFieldName = nodeFieldName; - SignalName = signalName; - } + [GetNode] + private Button _cancelButton = null!; - public string NodeFieldName { get; } - - public string SignalName { get; } - } - - [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] - public sealed class GetNodeAttribute : Attribute - { - } - } - - namespace Godot - { - public class Node - { - public virtual void _Ready() {} - - public virtual void _ExitTree() {} - } - - public class Button : Node - { - public event Action? Pressed - { - add {} - remove {} - } - } - } - - namespace TestApp - { - public partial class Hud : Node - { - [GetNode] - private Button _startButton = null!; - - [GetNode] - private Button _cancelButton = null!; - - [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] - [BindNodeSignal(nameof(_cancelButton), nameof(Button.Pressed))] - private void OnAnyButtonPressed() - { - } - } - } - """; + [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] + [BindNodeSignal(nameof(_cancelButton), nameof(Button.Pressed))] + private void OnAnyButtonPressed() + { + } + """, + LifecycleNodeType, + ButtonType); const string expected = """ // @@ -220,7 +183,7 @@ public class BindNodeSignalGeneratorTests await GeneratorTest.RunAsync( source, - ("TestApp_Hud.BindNodeSignal.g.cs", expected)); + ("TestApp_Hud.BindNodeSignal.g.cs", expected)).ConfigureAwait(false); } /// @@ -229,73 +192,24 @@ public class BindNodeSignalGeneratorTests [Test] public async Task Reports_Diagnostic_When_Signal_Does_Not_Exist() { - const string source = """ - using System; - using GFramework.Godot.SourceGenerators.Abstractions; - using Godot; + string source = CreateHudSource( + CreateAbstractionsSource(BindNodeSignalAttributeDeclaration), + """ + private Button _startButton = null!; - namespace GFramework.Godot.SourceGenerators.Abstractions - { - [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - public sealed class BindNodeSignalAttribute : Attribute - { - public BindNodeSignalAttribute(string nodeFieldName, string signalName) - { - NodeFieldName = nodeFieldName; - SignalName = signalName; - } + [{|#0:BindNodeSignal(nameof(_startButton), "Released")|}] + private void OnStartButtonPressed() + { + } + """, + EmptyNodeType, + ButtonType); - public string NodeFieldName { get; } - - public string SignalName { get; } - } - } - - namespace Godot - { - public class Node - { - } - - public class Button : Node - { - public event Action? Pressed - { - add {} - remove {} - } - } - } - - namespace TestApp - { - public partial class Hud : Node - { - private Button _startButton = null!; - - [{|#0:BindNodeSignal(nameof(_startButton), "Released")|}] - private void OnStartButtonPressed() - { - } - } - } - """; - - var test = new CSharpSourceGeneratorTest - { - TestState = - { - Sources = { source } - }, - DisabledDiagnostics = { "GF_Common_Trace_001" }, - TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck - }; - - test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_006", DiagnosticSeverity.Error) - .WithLocation(0) - .WithArguments("_startButton", "Released")); - - await test.RunAsync(); + await VerifyDiagnosticsAsync( + source, + new DiagnosticResult("GF_Godot_BindNodeSignal_006", DiagnosticSeverity.Error) + .WithLocation(0) + .WithArguments("_startButton", "Released")).ConfigureAwait(false); } /// @@ -304,75 +218,24 @@ public class BindNodeSignalGeneratorTests [Test] public async Task Reports_Diagnostic_When_Method_Signature_Does_Not_Match_Event() { - const string source = """ - using System; - using GFramework.Godot.SourceGenerators.Abstractions; - using Godot; + string source = CreateHudSource( + CreateAbstractionsSource(BindNodeSignalAttributeDeclaration), + """ + private SpinBox _startOreSpinBox = null!; - namespace GFramework.Godot.SourceGenerators.Abstractions - { - [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - public sealed class BindNodeSignalAttribute : Attribute - { - public BindNodeSignalAttribute(string nodeFieldName, string signalName) - { - NodeFieldName = nodeFieldName; - SignalName = signalName; - } + [{|#0:BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))|}] + private void OnStartOreValueChanged() + { + } + """, + EmptyNodeType, + SpinBoxType); - public string NodeFieldName { get; } - - public string SignalName { get; } - } - } - - namespace Godot - { - public class Node - { - } - - public class SpinBox : Node - { - public delegate void ValueChangedEventHandler(double value); - - public event ValueChangedEventHandler? ValueChanged - { - add {} - remove {} - } - } - } - - namespace TestApp - { - public partial class Hud : Node - { - private SpinBox _startOreSpinBox = null!; - - [{|#0:BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))|}] - private void OnStartOreValueChanged() - { - } - } - } - """; - - var test = new CSharpSourceGeneratorTest - { - TestState = - { - Sources = { source } - }, - DisabledDiagnostics = { "GF_Common_Trace_001" }, - TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck - }; - - test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_007", DiagnosticSeverity.Error) - .WithLocation(0) - .WithArguments("OnStartOreValueChanged", "ValueChanged", "_startOreSpinBox")); - - await test.RunAsync(); + await VerifyDiagnosticsAsync( + source, + new DiagnosticResult("GF_Godot_BindNodeSignal_007", DiagnosticSeverity.Error) + .WithLocation(0) + .WithArguments("OnStartOreValueChanged", "ValueChanged", "_startOreSpinBox")).ConfigureAwait(false); } /// @@ -381,73 +244,24 @@ public class BindNodeSignalGeneratorTests [Test] public async Task Reports_Diagnostic_When_Constructor_Argument_Is_Empty() { - const string source = """ - using System; - using GFramework.Godot.SourceGenerators.Abstractions; - using Godot; + string source = CreateHudSource( + CreateAbstractionsSource(BindNodeSignalAttributeDeclaration), + """ + private Button _startButton = null!; - namespace GFramework.Godot.SourceGenerators.Abstractions - { - [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - public sealed class BindNodeSignalAttribute : Attribute - { - public BindNodeSignalAttribute(string nodeFieldName, string signalName) - { - NodeFieldName = nodeFieldName; - SignalName = signalName; - } + [{|#0:BindNodeSignal(nameof(_startButton), "")|}] + private void OnStartButtonPressed() + { + } + """, + EmptyNodeType, + ButtonType); - public string NodeFieldName { get; } - - public string SignalName { get; } - } - } - - namespace Godot - { - public class Node - { - } - - public class Button : Node - { - public event Action? Pressed - { - add {} - remove {} - } - } - } - - namespace TestApp - { - public partial class Hud : Node - { - private Button _startButton = null!; - - [{|#0:BindNodeSignal(nameof(_startButton), "")|}] - private void OnStartButtonPressed() - { - } - } - } - """; - - var test = new CSharpSourceGeneratorTest - { - TestState = - { - Sources = { source } - }, - DisabledDiagnostics = { "GF_Common_Trace_001" }, - TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck - }; - - test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_010", DiagnosticSeverity.Error) - .WithLocation(0) - .WithArguments("OnStartButtonPressed", "signalName")); - - await test.RunAsync(); + await VerifyDiagnosticsAsync( + source, + new DiagnosticResult("GF_Godot_BindNodeSignal_010", DiagnosticSeverity.Error) + .WithLocation(0) + .WithArguments("OnStartButtonPressed", "signalName")).ConfigureAwait(false); } /// @@ -456,85 +270,35 @@ public class BindNodeSignalGeneratorTests [Test] public async Task Reports_Diagnostic_When_Generated_Method_Names_Already_Exist() { - const string source = """ - using System; - using GFramework.Godot.SourceGenerators.Abstractions; - using Godot; + string source = CreateHudSource( + CreateAbstractionsSource(BindNodeSignalAttributeDeclaration), + """ + private Button _startButton = null!; - namespace GFramework.Godot.SourceGenerators.Abstractions - { - [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - public sealed class BindNodeSignalAttribute : Attribute - { - public BindNodeSignalAttribute(string nodeFieldName, string signalName) - { - NodeFieldName = nodeFieldName; - SignalName = signalName; - } + [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] + private void OnStartButtonPressed() + { + } - public string NodeFieldName { get; } + private void {|#0:__BindNodeSignals_Generated|}() + { + } - public string SignalName { get; } - } - } + private void {|#1:__UnbindNodeSignals_Generated|}() + { + } + """, + EmptyNodeType, + ButtonType); - namespace Godot - { - public class Node - { - } - - public class Button : Node - { - public event Action? Pressed - { - add {} - remove {} - } - } - } - - namespace TestApp - { - public partial class Hud : Node - { - private Button _startButton = null!; - - [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] - private void OnStartButtonPressed() - { - } - - private void {|#0:__BindNodeSignals_Generated|}() - { - } - - private void {|#1:__UnbindNodeSignals_Generated|}() - { - } - } - } - """; - - var test = new CSharpSourceGeneratorTest - { - TestState = - { - Sources = { source } - }, - DisabledDiagnostics = { "GF_Common_Trace_001" }, - TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck - }; - - test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error) - .WithLocation(0) - .WithArguments("Hud", "__BindNodeSignals_Generated")); - - test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error) - .WithLocation(1) - .WithArguments("Hud", "__UnbindNodeSignals_Generated")); - - await test.RunAsync(); + await VerifyDiagnosticsAsync( + source, + new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error) + .WithLocation(0) + .WithArguments("Hud", "__BindNodeSignals_Generated"), + new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error) + .WithLocation(1) + .WithArguments("Hud", "__UnbindNodeSignals_Generated")).ConfigureAwait(false); } /// @@ -543,69 +307,80 @@ public class BindNodeSignalGeneratorTests [Test] public async Task Reports_Warnings_When_Lifecycle_Methods_Do_Not_Call_Generated_Methods() { - const string source = """ - using System; - using GFramework.Godot.SourceGenerators.Abstractions; - using Godot; + string source = CreateHudSource( + CreateAbstractionsSource(BindNodeSignalAttributeDeclaration), + """ + private Button _startButton = null!; - namespace GFramework.Godot.SourceGenerators.Abstractions - { - [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - public sealed class BindNodeSignalAttribute : Attribute - { - public BindNodeSignalAttribute(string nodeFieldName, string signalName) - { - NodeFieldName = nodeFieldName; - SignalName = signalName; - } + [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] + private void OnStartButtonPressed() + { + } - public string NodeFieldName { get; } + public override void {|#0:_Ready|}() + { + } - public string SignalName { get; } - } - } + public override void {|#1:_ExitTree|}() + { + } + """, + LifecycleNodeType, + ButtonType); - namespace Godot - { - public class Node - { - public virtual void _Ready() {} + await VerifyDiagnosticsAsync( + source, + new DiagnosticResult("GF_Godot_BindNodeSignal_008", DiagnosticSeverity.Warning) + .WithLocation(0) + .WithArguments("Hud"), + new DiagnosticResult("GF_Godot_BindNodeSignal_009", DiagnosticSeverity.Warning) + .WithLocation(1) + .WithArguments("Hud")).ConfigureAwait(false); + } - public virtual void _ExitTree() {} - } + private static string CreateAbstractionsSource(params string[] attributeDeclarations) + { + string declarations = string.Join($"{Environment.NewLine}{Environment.NewLine}", attributeDeclarations); - public class Button : Node - { - public event Action? Pressed - { - add {} - remove {} - } - } - } + return $$""" + namespace GFramework.Godot.SourceGenerators.Abstractions + { + {{declarations}} + } + """; + } - namespace TestApp - { - public partial class Hud : Node - { - private Button _startButton = null!; + private static string CreateHudSource( + string abstractionsSource, + string hudMembers, + params string[] godotTypes) + { + string godotSource = string.Join($"{Environment.NewLine}{Environment.NewLine}", godotTypes); - [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] - private void OnStartButtonPressed() - { - } + return $$""" + using System; + using GFramework.Godot.SourceGenerators.Abstractions; + using Godot; - public override void {|#0:_Ready|}() - { - } + {{abstractionsSource}} - public override void {|#1:_ExitTree|}() - { - } - } - } - """; + namespace Godot + { + {{godotSource}} + } + namespace TestApp + { + public partial class Hud : Node + { + {{hudMembers}} + } + } + """; + } + + private static Task VerifyDiagnosticsAsync(string source, params DiagnosticResult[] expectedDiagnostics) + { var test = new CSharpSourceGeneratorTest { TestState = @@ -616,14 +391,11 @@ public class BindNodeSignalGeneratorTests TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck }; - test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_008", DiagnosticSeverity.Warning) - .WithLocation(0) - .WithArguments("Hud")); + foreach (DiagnosticResult expectedDiagnostic in expectedDiagnostics) + { + test.ExpectedDiagnostics.Add(expectedDiagnostic); + } - test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_009", DiagnosticSeverity.Warning) - .WithLocation(1) - .WithArguments("Hud")); - - await test.RunAsync(); + return test.RunAsync(); } -} \ No newline at end of file +} diff --git a/GFramework.Godot.SourceGenerators.Tests/Core/GeneratorTest.cs b/GFramework.Godot.SourceGenerators.Tests/Core/GeneratorTest.cs index cca85465..c5cb4608 100644 --- a/GFramework.Godot.SourceGenerators.Tests/Core/GeneratorTest.cs +++ b/GFramework.Godot.SourceGenerators.Tests/Core/GeneratorTest.cs @@ -29,7 +29,7 @@ public static class GeneratorTest test.TestState.GeneratedSources.Add( (typeof(TGenerator), filename, NormalizeLineEndings(content))); - await test.RunAsync(); + await test.RunAsync().ConfigureAwait(false); } /// @@ -44,4 +44,4 @@ public static class GeneratorTest .Replace("\r", "\n", StringComparison.Ordinal) .Replace("\n", Environment.NewLine, StringComparison.Ordinal); } -} \ No newline at end of file +} diff --git a/GFramework.Godot.SourceGenerators.Tests/GetNode/GetNodeGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/GetNode/GetNodeGeneratorTests.cs index 266ff983..d6e7cd5e 100644 --- a/GFramework.Godot.SourceGenerators.Tests/GetNode/GetNodeGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/GetNode/GetNodeGeneratorTests.cs @@ -5,61 +5,88 @@ namespace GFramework.Godot.SourceGenerators.Tests.GetNode; [TestFixture] public class GetNodeGeneratorTests { + private const string FullGetNodeAttributeDeclaration = """ + [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] + public sealed class GetNodeAttribute : Attribute + { + public GetNodeAttribute() {} + public GetNodeAttribute(string path) { Path = path; } + public string? Path { get; set; } + public bool Required { get; set; } = true; + public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto; + } + + public enum NodeLookupMode + { + Auto = 0, + UniqueName = 1, + RelativePath = 2, + AbsolutePath = 3 + } + """; + + private const string MinimalGetNodeAttributeDeclaration = """ + [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] + public sealed class GetNodeAttribute : Attribute + { + public GetNodeAttribute() {} + } + + public enum NodeLookupMode + { + Auto = 0 + } + """; + + private const string PropertyOnlyGetNodeAttributeDeclaration = """ + [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] + public sealed class GetNodeAttribute : Attribute + { + public string? Path { get; set; } + public bool Required { get; set; } = true; + public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto; + } + + public enum NodeLookupMode + { + Auto = 0, + UniqueName = 1, + RelativePath = 2, + AbsolutePath = 3 + } + """; + + private const string NodeWithReadyAndLookupMethods = """ + public class Node + { + public virtual void _Ready() {} + public T GetNode(string path) where T : Node => throw new InvalidOperationException(path); + public T? GetNodeOrNull(string path) where T : Node => default; + } + """; + + private const string HBoxContainerType = """ + public class HBoxContainer : Node + { + } + """; + [Test] public async Task Generates_InferredUniqueNameBindings_And_ReadyHook_WhenReadyIsMissing() { - const string source = """ - using System; - using GFramework.Godot.SourceGenerators.Abstractions; - using Godot; + string source = CreateGetNodeSource( + FullGetNodeAttributeDeclaration, + """ + public partial class TopBar : HBoxContainer + { + [GetNode] + private HBoxContainer _leftContainer = null!; - namespace GFramework.Godot.SourceGenerators.Abstractions - { - [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] - public sealed class GetNodeAttribute : Attribute - { - public GetNodeAttribute() {} - public GetNodeAttribute(string path) { Path = path; } - public string? Path { get; set; } - public bool Required { get; set; } = true; - public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto; - } - - public enum NodeLookupMode - { - Auto = 0, - UniqueName = 1, - RelativePath = 2, - AbsolutePath = 3 - } - } - - namespace Godot - { - public class Node - { - public virtual void _Ready() {} - public T GetNode(string path) where T : Node => throw new InvalidOperationException(path); - public T? GetNodeOrNull(string path) where T : Node => default; - } - - public class HBoxContainer : Node - { - } - } - - namespace TestApp - { - public partial class TopBar : HBoxContainer - { - [GetNode] - private HBoxContainer _leftContainer = null!; - - [GetNode] - private HBoxContainer m_rightContainer = null!; - } - } - """; + [GetNode] + private HBoxContainer m_rightContainer = null!; + } + """, + HBoxContainerType); const string expected = """ // @@ -88,69 +115,30 @@ public class GetNodeGeneratorTests await GeneratorTest.RunAsync( source, - ("TestApp_TopBar.GetNode.g.cs", expected)); + ("TestApp_TopBar.GetNode.g.cs", expected)).ConfigureAwait(false); } [Test] public async Task Generates_ManualInjectionOnly_WhenReadyAlreadyExists() { - const string source = """ - using System; - using GFramework.Godot.SourceGenerators.Abstractions; - using Godot; + string source = CreateGetNodeSource( + FullGetNodeAttributeDeclaration, + """ + public partial class TopBar : HBoxContainer + { + [GetNode("%LeftContainer")] + private HBoxContainer _leftContainer = null!; - namespace GFramework.Godot.SourceGenerators.Abstractions - { - [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] - public sealed class GetNodeAttribute : Attribute - { - public GetNodeAttribute() {} - public GetNodeAttribute(string path) { Path = path; } - public string? Path { get; set; } - public bool Required { get; set; } = true; - public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto; - } + [GetNode(Required = false, Lookup = NodeLookupMode.RelativePath)] + private HBoxContainer? _rightContainer; - public enum NodeLookupMode - { - Auto = 0, - UniqueName = 1, - RelativePath = 2, - AbsolutePath = 3 - } - } - - namespace Godot - { - public class Node - { - public virtual void _Ready() {} - public T GetNode(string path) where T : Node => throw new InvalidOperationException(path); - public T? GetNodeOrNull(string path) where T : Node => default; - } - - public class HBoxContainer : Node - { - } - } - - namespace TestApp - { - public partial class TopBar : HBoxContainer - { - [GetNode("%LeftContainer")] - private HBoxContainer _leftContainer = null!; - - [GetNode(Required = false, Lookup = NodeLookupMode.RelativePath)] - private HBoxContainer? _rightContainer; - - public override void _Ready() - { - __InjectGetNodes_Generated(); - } - } - } - """; + public override void _Ready() + { + __InjectGetNodes_Generated(); + } + } + """, + HBoxContainerType); const string expected = """ // @@ -171,7 +159,7 @@ public class GetNodeGeneratorTests await GeneratorTest.RunAsync( source, - ("TestApp_TopBar.GetNode.g.cs", expected)); + ("TestApp_TopBar.GetNode.g.cs", expected)).ConfigureAwait(false); } [Test] @@ -234,58 +222,26 @@ public class GetNodeGeneratorTests .WithSpan(39, 24, 39, 38) .WithArguments("_leftContainer")); - await test.RunAsync(); + await test.RunAsync().ConfigureAwait(false); } [Test] public async Task Reports_Diagnostic_When_Generated_Injection_Method_Name_Already_Exists() { - const string source = """ - using System; - using GFramework.Godot.SourceGenerators.Abstractions; - using Godot; + string source = CreateGetNodeSource( + MinimalGetNodeAttributeDeclaration, + """ + public partial class TopBar : HBoxContainer + { + [GetNode] + private HBoxContainer _leftContainer = null!; - namespace GFramework.Godot.SourceGenerators.Abstractions - { - [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] - public sealed class GetNodeAttribute : Attribute - { - public GetNodeAttribute() {} - } - - public enum NodeLookupMode - { - Auto = 0 - } - } - - namespace Godot - { - public class Node - { - public virtual void _Ready() {} - public T GetNode(string path) where T : Node => throw new InvalidOperationException(path); - public T? GetNodeOrNull(string path) where T : Node => default; - } - - public class HBoxContainer : Node - { - } - } - - namespace TestApp - { - public partial class TopBar : HBoxContainer - { - [GetNode] - private HBoxContainer _leftContainer = null!; - - private void {|#0:__InjectGetNodes_Generated|}() - { - } - } - } - """; + private void {|#0:__InjectGetNodes_Generated|}() + { + } + } + """, + HBoxContainerType); var test = new CSharpSourceGeneratorTest { @@ -301,6 +257,39 @@ public class GetNodeGeneratorTests .WithLocation(0) .WithArguments("TopBar", "__InjectGetNodes_Generated")); - await test.RunAsync(); + await test.RunAsync().ConfigureAwait(false); } -} \ No newline at end of file + + private static string CreateGetNodeSource( + string attributeDeclaration, + string testAppSource, + params string[] godotTypes) + { + string[] allGodotTypes = new string[godotTypes.Length + 1]; + allGodotTypes[0] = NodeWithReadyAndLookupMethods; + Array.Copy(godotTypes, 0, allGodotTypes, 1, godotTypes.Length); + + string godotSource = string.Join($"{Environment.NewLine}{Environment.NewLine}", allGodotTypes); + + return $$""" + using System; + using GFramework.Godot.SourceGenerators.Abstractions; + using Godot; + + namespace GFramework.Godot.SourceGenerators.Abstractions + { + {{attributeDeclaration}} + } + + namespace Godot + { + {{godotSource}} + } + + namespace TestApp + { + {{testAppSource}} + } + """; + } +} diff --git a/GFramework.Godot.SourceGenerators.Tests/Project/GodotProjectMetadataGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Project/GodotProjectMetadataGeneratorTests.cs index b033a865..508273d7 100644 --- a/GFramework.Godot.SourceGenerators.Tests/Project/GodotProjectMetadataGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/Project/GodotProjectMetadataGeneratorTests.cs @@ -8,6 +8,131 @@ namespace GFramework.Godot.SourceGenerators.Tests.Project; [TestFixture] public class GodotProjectMetadataGeneratorTests { + private const string AutoLoadProjectFile = """ + [autoload] + GameServices="*res://autoload/game_services.tscn" + AudioBus="*res://autoload/audio_bus.gd" + """; + + private const string InputActionsProjectFile = """ + [input] + move_up={ + "deadzone": 0.5 + } + ui_cancel={ + "deadzone": 0.5 + } + """; + + private const string ExpectedAutoLoads = """ + // + #nullable enable + + namespace GFramework.Godot.Generated; + + /// + /// 提供 project.godot 中 AutoLoad 单例的强类型访问入口。 + /// + public static partial class AutoLoads + { + /// + /// 获取 AutoLoad GameServices。 + /// + public static global::TestApp.GameServices GameServices => GetRequiredNode("GameServices"); + + /// + /// 尝试获取 AutoLoad GameServices。 + /// + public static bool TryGetGameServices(out global::TestApp.GameServices? value) + { + return TryGetNode("GameServices", out value); + } + + /// + /// 获取 AutoLoad AudioBus。 + /// + public static global::Godot.Node AudioBus => GetRequiredNode("AudioBus"); + + /// + /// 尝试获取 AutoLoad AudioBus。 + /// + public static bool TryGetAudioBus(out global::Godot.Node? value) + { + return TryGetNode("AudioBus", out value); + } + + /// + /// 获取一个必填的 AutoLoad 节点;缺失时抛出异常。 + /// + /// 节点类型。 + /// AutoLoad 名称。 + /// 已解析的 AutoLoad 节点。 + private static TNode GetRequiredNode(string autoLoadName) + where TNode : global::Godot.Node + { + if (TryGetNode(autoLoadName, out TNode? value)) + { + return value!; + } + + throw new global::System.InvalidOperationException($"AutoLoad '{autoLoadName}' is not available on the active SceneTree root."); + } + + /// + /// 尝试从当前 SceneTree 根节点解析 AutoLoad。 + /// + /// 节点类型。 + /// AutoLoad 名称。 + /// 解析到的节点实例。 + /// 若当前进程存在 SceneTree 且根节点中能解析到该 AutoLoad,则返回 true + private static bool TryGetNode(string autoLoadName, out TNode? value) + where TNode : global::Godot.Node + { + value = default; + + if (global::Godot.Engine.GetMainLoop() is not global::Godot.SceneTree sceneTree) + { + return false; + } + + var root = sceneTree.Root; + if (root is null) + { + return false; + } + + value = root.GetNodeOrNull($"/root/{autoLoadName}"); + return value is not null; + } + } + + """; + + private const string ExpectedInputActions = """ + // + #nullable enable + + namespace GFramework.Godot.Generated; + + /// + /// 提供 project.godot 中 Input Action 名称的强类型常量。 + /// + public static partial class InputActions + { + /// + /// Input Action move_up 的稳定名称。 + /// + public const string MoveUp = "move_up"; + + /// + /// Input Action ui_cancel 的稳定名称。 + /// + public const string UiCancel = "ui_cancel"; + + } + + """; + /// /// 验证会根据 AutoLoad 与 Input Action 生成稳定的强类型入口。 /// @@ -29,142 +154,19 @@ public class GodotProjectMetadataGeneratorTests """, includeAutoLoadAttribute: true); - const string projectFile = """ - [autoload] - GameServices="*res://autoload/game_services.tscn" - AudioBus="*res://autoload/audio_bus.gd" - - [input] - move_up={ - "deadzone": 0.5 - } - ui_cancel={ - "deadzone": 0.5 - } - """; - - const string expectedAutoLoads = """ - // - #nullable enable - - namespace GFramework.Godot.Generated; - - /// - /// 提供 project.godot 中 AutoLoad 单例的强类型访问入口。 - /// - public static partial class AutoLoads - { - /// - /// 获取 AutoLoad GameServices。 - /// - public static global::TestApp.GameServices GameServices => GetRequiredNode("GameServices"); - - /// - /// 尝试获取 AutoLoad GameServices。 - /// - public static bool TryGetGameServices(out global::TestApp.GameServices? value) - { - return TryGetNode("GameServices", out value); - } - - /// - /// 获取 AutoLoad AudioBus。 - /// - public static global::Godot.Node AudioBus => GetRequiredNode("AudioBus"); - - /// - /// 尝试获取 AutoLoad AudioBus。 - /// - public static bool TryGetAudioBus(out global::Godot.Node? value) - { - return TryGetNode("AudioBus", out value); - } - - /// - /// 获取一个必填的 AutoLoad 节点;缺失时抛出异常。 - /// - /// 节点类型。 - /// AutoLoad 名称。 - /// 已解析的 AutoLoad 节点。 - private static TNode GetRequiredNode(string autoLoadName) - where TNode : global::Godot.Node - { - if (TryGetNode(autoLoadName, out TNode? value)) - { - return value!; - } - - throw new global::System.InvalidOperationException($"AutoLoad '{autoLoadName}' is not available on the active SceneTree root."); - } - - /// - /// 尝试从当前 SceneTree 根节点解析 AutoLoad。 - /// - /// 节点类型。 - /// AutoLoad 名称。 - /// 解析到的节点实例。 - /// 若当前进程存在 SceneTree 且根节点中能解析到该 AutoLoad,则返回 true - private static bool TryGetNode(string autoLoadName, out TNode? value) - where TNode : global::Godot.Node - { - value = default; - - if (global::Godot.Engine.GetMainLoop() is not global::Godot.SceneTree sceneTree) - { - return false; - } - - var root = sceneTree.Root; - if (root is null) - { - return false; - } - - value = root.GetNodeOrNull($"/root/{autoLoadName}"); - return value is not null; - } - } - - """; - - const string expectedInputActions = """ - // - #nullable enable - - namespace GFramework.Godot.Generated; - - /// - /// 提供 project.godot 中 Input Action 名称的强类型常量。 - /// - public static partial class InputActions - { - /// - /// Input Action move_up 的稳定名称。 - /// - public const string MoveUp = "move_up"; - - /// - /// Input Action ui_cancel 的稳定名称。 - /// - public const string UiCancel = "ui_cancel"; - - } - - """; - var result = AdditionalTextGeneratorTestDriver.Run( source, - ("project.godot", projectFile)); + ("project.godot", $"{AutoLoadProjectFile}\n\n{InputActionsProjectFile}")); var generatedSources = AdditionalTextGeneratorTestDriver.ToGeneratedSourceMap(result); Assert.That(result.Results.Single().Diagnostics, Is.Empty); Assert.That( generatedSources["GFramework_Godot_Generated_AutoLoads.g.cs"], - Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(expectedAutoLoads))); + Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(ExpectedAutoLoads))); Assert.That( generatedSources["GFramework_Godot_Generated_InputActions.g.cs"], - Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(expectedInputActions))); + Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(ExpectedInputActions))); } /// diff --git a/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs index befdcf59..d57a74c6 100644 --- a/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs @@ -6,48 +6,52 @@ namespace GFramework.Godot.SourceGenerators.Tests.Registration; [TestFixture] public class AutoRegisterExportedCollectionsGeneratorTests { + private const string StandardAttributeDeclarations = """ + [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) { } + } + """; + + private const string MultiDeclarationAttributeDeclarations = """ + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] + 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) { } + } + """; + [Test] 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.UI; + string source = CreateSource( + """ + public sealed class IntRegistry + { + public void Register(int value) { } + } - namespace GFramework.Godot.SourceGenerators.Abstractions.UI - { - [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } + [AutoRegisterExportedCollections] + public partial class Bootstrapper + where TReference : class? + where TNotNull : notnull + where TValue : struct + where TUnmanaged : unmanaged + { + private readonly IntRegistry? _registry = new(); - [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 - where TReference : class? - where TNotNull : notnull - where TValue : struct - where TUnmanaged : unmanaged - { - private readonly IntRegistry? _registry = new(); - - [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] - public List? Values { get; } = new(); - } - } - """; + [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] + public List? Values { get; } = new(); + } + """, + nullableEnabled: true); const string expected = """ // @@ -77,7 +81,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests await GeneratorTest.RunAsync( source, - ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)); + ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false); } [Test] @@ -137,41 +141,23 @@ public class AutoRegisterExportedCollectionsGeneratorTests [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.UI; + string source = CreateSource( + """ + public sealed class ArrayRegistry + { + public void Register(int[] value) { } + } - namespace GFramework.Godot.SourceGenerators.Abstractions.UI - { - [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } + [AutoRegisterExportedCollections] + public partial class Bootstrapper + { + private readonly ArrayRegistry _registry = new(); - [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 Values { get; } = new(); - } - } - """; + [RegisterExportedCollection(nameof(_registry), nameof(ArrayRegistry.Register))] + public List Values { get; } = new(); + } + """, + nullableEnabled: true); const string expected = """ // @@ -197,59 +183,41 @@ public class AutoRegisterExportedCollectionsGeneratorTests await GeneratorTest.RunAsync( source, - ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)); + ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false); } [Test] public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Inherited_Interface() { - const string source = """ - #nullable enable - using System; - using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions.UI; + string source = CreateSource( + """ + public interface IKeyValue + { + } - namespace GFramework.Godot.SourceGenerators.Abstractions.UI - { - [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } + public interface IRegistry + { + void Registry(IKeyValue mapping); + } - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] - public sealed class RegisterExportedCollectionAttribute : Attribute - { - public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { } - } - } + public interface IAssetRegistry : IRegistry + { + } - namespace TestApp - { - public interface IKeyValue - { - } + public sealed class IntConfig : IKeyValue + { + } - public interface IRegistry - { - void Registry(IKeyValue mapping); - } + [AutoRegisterExportedCollections] + public partial class Bootstrapper + { + private readonly IAssetRegistry? _registry = null; - public interface IAssetRegistry : IRegistry - { - } - - public sealed class IntConfig : IKeyValue - { - } - - [AutoRegisterExportedCollections] - public partial class Bootstrapper - { - private readonly IAssetRegistry? _registry = null; - - [RegisterExportedCollection(nameof(_registry), "Registry")] - public List? Values { get; } = new(); - } - } - """; + [RegisterExportedCollection(nameof(_registry), "Registry")] + public List? Values { get; } = new(); + } + """, + nullableEnabled: true); const string expected = """ // @@ -275,7 +243,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests await GeneratorTest.RunAsync( source, - ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)); + ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false); } [Test] @@ -340,45 +308,27 @@ public class AutoRegisterExportedCollectionsGeneratorTests [Test] public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Base_Class() { - const string source = """ - #nullable enable - using System; - using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions.UI; + string source = CreateSource( + """ + public class BaseRegistry + { + public void Register(int value) { } + } - namespace GFramework.Godot.SourceGenerators.Abstractions.UI - { - [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } + public sealed class DerivedRegistry : BaseRegistry + { + } - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] - public sealed class RegisterExportedCollectionAttribute : Attribute - { - public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { } - } - } + [AutoRegisterExportedCollections] + public partial class Bootstrapper + { + private readonly DerivedRegistry? _registry = new(); - namespace TestApp - { - public class BaseRegistry - { - public void Register(int value) { } - } - - public sealed class DerivedRegistry : BaseRegistry - { - } - - [AutoRegisterExportedCollections] - public partial class Bootstrapper - { - private readonly DerivedRegistry? _registry = new(); - - [RegisterExportedCollection(nameof(_registry), nameof(BaseRegistry.Register))] - public List? Values { get; } = new(); - } - } - """; + [RegisterExportedCollection(nameof(_registry), nameof(BaseRegistry.Register))] + public List? Values { get; } = new(); + } + """, + nullableEnabled: true); const string expected = """ // @@ -404,50 +354,32 @@ public class AutoRegisterExportedCollectionsGeneratorTests await GeneratorTest.RunAsync( source, - ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)); + ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false); } [Test] public async Task Generates_Batch_Registration_Method_When_Registry_Member_Comes_From_Base_Class() { - const string source = """ - #nullable enable - using System; - using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions.UI; + string source = CreateSource( + """ + public sealed class IntRegistry + { + public void Register(int value) { } + } - namespace GFramework.Godot.SourceGenerators.Abstractions.UI - { - [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } + public abstract class BootstrapperBase + { + protected readonly IntRegistry? _registry = new(); + } - [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) { } - } - - public abstract class BootstrapperBase - { - protected readonly IntRegistry? _registry = new(); - } - - [AutoRegisterExportedCollections] - public partial class Bootstrapper : BootstrapperBase - { - [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] - public List? Values { get; } = new(); - } - } - """; + [AutoRegisterExportedCollections] + public partial class Bootstrapper : BootstrapperBase + { + [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] + public List? Values { get; } = new(); + } + """, + nullableEnabled: true); const string expected = """ // @@ -473,74 +405,47 @@ public class AutoRegisterExportedCollectionsGeneratorTests await GeneratorTest.RunAsync( source, - ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)); + ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false); } [Test] public async Task Reports_Diagnostic_When_Collection_Member_Is_Not_Instance_Readable() { - const string source = """ - using System; - using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions.UI; + string source = CreateSource( + """ + public sealed class IntRegistry + { + public void Register(int value) { } + } - namespace GFramework.Godot.SourceGenerators.Abstractions.UI - { - [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } + [AutoRegisterExportedCollections] + public partial class Bootstrapper + { + private readonly IntRegistry _registry = new(); - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] - public sealed class RegisterExportedCollectionAttribute : Attribute - { - public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { } - } - } + [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] + public static List {|#0:StaticValues|} = new(); - namespace TestApp - { - public sealed class IntRegistry - { - public void Register(int value) { } - } + [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] + public static List {|#1:StaticPropertyValues|} { get; } = new(); - [AutoRegisterExportedCollections] - public partial class Bootstrapper - { - private readonly IntRegistry _registry = new(); + [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] + public List {|#2:WriteOnlyValues|} { set { } } + } + """); - [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] - public static List {|#0:StaticValues|} = new(); - - [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] - public static List {|#1:StaticPropertyValues|} { get; } = new(); - - [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] - public List {|#2:WriteOnlyValues|} { set { } } - } - } - """; - - var test = new CSharpSourceGeneratorTest - { - TestState = - { - Sources = { source } - }, - DisabledDiagnostics = { "GF_Common_Trace_001" }, - TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck - }; - - test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error) - .WithLocation(0) - .WithArguments("StaticValues")); - test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error) - .WithLocation(1) - .WithArguments("StaticPropertyValues")); - test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error) - .WithLocation(2) - .WithArguments("WriteOnlyValues")); - - await test.RunAsync(); + await VerifyDiagnosticsAsync( + source, + skipGeneratedSourcesCheck: true, + new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error) + .WithLocation(0) + .WithArguments("StaticValues"), + new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error) + .WithLocation(1) + .WithArguments("StaticPropertyValues"), + new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error) + .WithLocation(2) + .WithArguments("WriteOnlyValues")).ConfigureAwait(false); } [Test] @@ -711,45 +616,28 @@ public class AutoRegisterExportedCollectionsGeneratorTests [Test] public async Task Generates_Only_One_Source_When_Multiple_Partial_Declarations_Are_Annotated() { - const string source = """ - #nullable enable - using System; - using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions.UI; + string source = CreateSource( + """ + public sealed class IntRegistry + { + public void Register(int value) { } + } - namespace GFramework.Godot.SourceGenerators.Abstractions.UI - { - [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] - public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } + [AutoRegisterExportedCollections] + public partial class Bootstrapper + { + private readonly IntRegistry? _registry = new(); + } - [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(); - } - - [AutoRegisterExportedCollections] - public partial class Bootstrapper - { - [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] - public List? Values { get; } = new(); - } - } - """; + [AutoRegisterExportedCollections] + public partial class Bootstrapper + { + [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] + public List? Values { get; } = new(); + } + """, + nullableEnabled: true, + allowMultipleDeclarations: true); const string expected = """ // @@ -775,6 +663,61 @@ public class AutoRegisterExportedCollectionsGeneratorTests await GeneratorTest.RunAsync( source, - ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)); + ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false); + } + + private static string CreateSource( + string applicationSource, + bool nullableEnabled = false, + bool allowMultipleDeclarations = false) + { + string nullableDirective = nullableEnabled ? "#nullable enable\n" : string.Empty; + string attributeDeclarations = allowMultipleDeclarations + ? MultiDeclarationAttributeDeclarations + : StandardAttributeDeclarations; + + return $$""" + {{nullableDirective}}using System; + using System.Collections; + using System.Collections.Generic; + using GFramework.Godot.SourceGenerators.Abstractions.UI; + + namespace GFramework.Godot.SourceGenerators.Abstractions.UI + { + {{attributeDeclarations}} + } + + namespace TestApp + { + {{applicationSource}} + } + """; + } + + private static Task VerifyDiagnosticsAsync( + string source, + bool skipGeneratedSourcesCheck = false, + params DiagnosticResult[] expectedDiagnostics) + { + var test = new CSharpSourceGeneratorTest + { + TestState = + { + Sources = { source } + }, + DisabledDiagnostics = { "GF_Common_Trace_001" } + }; + + if (skipGeneratedSourcesCheck) + { + test.TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck; + } + + foreach (DiagnosticResult expectedDiagnostic in expectedDiagnostics) + { + test.ExpectedDiagnostics.Add(expectedDiagnostic); + } + + return test.RunAsync(); } } diff --git a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md index dbc281af..1ee133e9 100644 --- a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md +++ b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md @@ -6,27 +6,32 @@ ## 当前恢复点 -- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-050` -- 当前阶段:`Phase 50` +- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-051` +- 当前阶段:`Phase 51` - 当前焦点: - - warning 基线已修正为仓库根目录执行 `dotnet clean` 后再执行 `dotnet build` - - `2026-04-24` 用户确认的 clean solution build 结果为 `Build succeeded with 1193 warning(s)` - - 当前主线程切片为 `GFramework.Godot.SourceGenerators` - - 当前工作树除未跟踪的 `.codex` 目录外,存在待提交的 source generator / `AGENTS.md` / `ai-plan` 修改 + - `2026-04-24` 本轮已完成 `GFramework.Godot.SourceGenerators.Tests` warning 清理 + - 当前主线程切片从生成器实现转到对应测试项目,并已把 `GFramework.Godot.SourceGenerators.Tests` 从 `24` 个 warning 降到 `0` + - 当前批次按 `origin/main` merge-base 计算的累计分支 diff 预计为 `23` 个文件,仍低于 `$gframework-batch-boot 75` 的主阈值 + - 当前工作树除未跟踪的 `.codex` 目录外,还存在与本批次无关的既有文档 / 跟踪文件修改;提交当前批次时必须只包含本 topic 相关文件 ## 当前活跃事实 - 之前记录的 plain `dotnet build` `0 Warning(s)` 属于增量构建假阴性,不能再作为 warning 检查真值 - 本轮已完成 `GFramework.Godot.SourceGenerators` warning 清理:clean `Release` build 从 9 个 warning 降至 0 个 warning - 当前已确认解决的文件包括 `BindNodeSignalGenerator.cs`、`GetNodeGenerator.cs`、`GodotProjectMetadataGenerator.cs`、`Registration/AutoRegisterExportedCollectionsGenerator.cs` -- 后续 warning-reduction 仍应以 clean solution build 的真实输出为切片来源 +- 本轮直接执行仓库根目录 `dotnet clean` 仍在 `ValidateSolutionConfiguration` 阶段失败,输出未提供具体 error 文本 +- 本轮直接执行仓库根目录 `dotnet build` 成功,并给出 `1184 warning(s)` 的真实输出 +- `GFramework.Godot.SourceGenerators.Tests` 已通过测试辅助模板抽取与 `ConfigureAwait(false)` 修正,当前 `Debug` / `Release` 构建均为 `0 Warning(s)` +- 本轮已验证 `dotnet test GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release --no-build`,结果为 `Passed: 48` ## 当前风险 - 如果后续继续依赖增量 `dotnet build`,容易再次把 warning 数量误判为 0 - 缓解措施:每轮 warning 检查前先执行 `dotnet clean`,再执行目标 `dotnet build` -- 当前只验证了受影响项目 `GFramework.Godot.SourceGenerators`;整仓库 warning 总量仍应以用户确认的 clean solution build 为基线 - - 缓解措施:下一轮从 clean solution build 输出里选择新的低风险 warning 热点继续切片 +- 仓库根目录 `dotnet clean` 目前仍然无法给出新的 clean 基线 + - 缓解措施:若下一轮继续做整仓 warning reduction,先定位 `dotnet clean` 的 solution-level 失败原因,或明确继续沿用用户确认的 `1193 warning(s)` clean 基线与本轮 `1184 warning(s)` direct build 观测值 +- 当前 worktree 已存在与本批次无关的未提交改动 + - 缓解措施:提交当前批次时只暂存 `GFramework.Godot.SourceGenerators.Tests` 与对应 `ai-plan` 文件,避免混入其他 topic 变更 ## 活跃文档 @@ -42,14 +47,19 @@ ## 验证说明 -- `dotnet clean GFramework.Godot.SourceGenerators/GFramework.Godot.SourceGenerators.csproj -c Release` - - 结果:成功;`0 Warning(s)`、`0 Error(s)` -- `dotnet build GFramework.Godot.SourceGenerators/GFramework.Godot.SourceGenerators.csproj -c Release` - - 结果:成功;`0 Warning(s)`、`0 Error(s)` +- `dotnet clean` + - 结果:失败;停在 solution `ValidateSolutionConfiguration`,`0 Warning(s)`、`0 Error(s)`,未输出更具体的 error 文本 - `dotnet build` - - 结果:此前被误记为 `0 Warning(s)`;现已确认这是增量构建假阴性,不再作为有效基线 + - 结果:成功;`1184 Warning(s)`、`0 Error(s)` +- `dotnet build GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj` + - 初始结果:成功;`24 Warning(s)`、`0 Error(s)` + - 本轮收尾结果:成功;`0 Warning(s)`、`0 Error(s)` +- `dotnet build GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release` + - 结果:成功;`0 Warning(s)`、`0 Error(s)` +- `dotnet test GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release --no-build` + - 结果:成功;`Passed: 48`、`Failed: 0` ## 下一步建议 -1. 在仓库根目录先执行 `dotnet clean`、再执行 `dotnet build`,重新采集当前 solution 的真实 warning 列表 -2. 以 clean build 输出中的下一个低风险热点作为新切片,优先继续 source generator、测试或单模块可局部验证的问题 +1. 提交当前 `GFramework.Godot.SourceGenerators.Tests` 清理批次,并确认提交只包含本 topic 相关文件 +2. 如果继续 warning reduction,优先重新评估仓库根目录 `dotnet clean` 的 solution-level 失败,再决定是继续从整仓 `dotnet build` 输出挑热点,还是先修复 clean 基线采集问题 diff --git a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md index 725b2f08..b9393bd8 100644 --- a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md +++ b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md @@ -1,5 +1,36 @@ # Analyzer Warning Reduction 追踪 +## 2026-04-24 — RP-051 + +### 阶段:`GFramework.Godot.SourceGenerators.Tests` warning 清零 + +- 触发背景: + - 用户要求直接运行 `dotnet clean`,不再添加额外 shell 包装;solution-level `dotnet clean` 仍然在 `ValidateSolutionConfiguration` 阶段失败 + - 直接执行仓库根目录 `dotnet build` 成功,并输出 `1184 warning(s)`,说明当前真实热点已从 `GFramework.Godot.SourceGenerators` 转移到对应测试项目 +- 主线程实施: + - 以 `GFramework.Godot.SourceGenerators.Tests` 为独立批次,先确认该项目本地基线为 `24 warning(s)` + - 在 `BindNodeSignalGeneratorTests.cs`、`AutoSceneGeneratorTests.cs`、`AutoUiPageGeneratorTests.cs`、`GetNodeGeneratorTests.cs`、`AutoRegisterExportedCollectionsGeneratorTests.cs`、`GodotProjectMetadataGeneratorTests.cs` 中抽取共享 source / diagnostic helper,压缩重复长方法 + - 在 `Core/GeneratorTest.cs` 中补充 `ConfigureAwait(false)`,清除项目内唯一 `MA0004` + - 把 `GFramework.Godot.SourceGenerators.Tests` 项目 warning 从 `24` 降到 `0` +- 验证里程碑: + - `dotnet build` + - 结果:成功;`1184 Warning(s)`、`0 Error(s)` + - `dotnet build GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj` + - 初始结果:成功;`24 Warning(s)`、`0 Error(s)` + - 第一批(`BindNodeSignal` + `GeneratorTest`)后:`16 Warning(s)` + - 第二批(`AutoScene` / `AutoUiPage` / `GetNode`)后:`8 Warning(s)` + - 第三批(`Registration` / `Project`)后:`1 Warning(s)` + - 收尾修复后:成功;`0 Warning(s)`、`0 Error(s)` + - `dotnet build GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release` + - 结果:成功;`0 Warning(s)`、`0 Error(s)` + - `dotnet test GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release --no-build` + - 结果:成功;`Passed: 48`、`Failed: 0` +- 当前结论: + - `GFramework.Godot.SourceGenerators.Tests` 已在 `Debug` / `Release` 构建下达到 `0 warning(s)` + - 按 `origin/main` merge-base 计算并只纳入当前暂存批次时,累计分支 diff 为 `23` 个文件,低于 `$gframework-batch-boot 75` 的主停止阈值 + - 仓库根目录 `dotnet clean` 仍无法稳定产出新的 clean 基线,需要在下一轮单独排查 + - 当前 worktree 已有与本批次无关的既有改动;提交时必须只暂存 analyzer warning reduction 相关文件 + ## 2026-04-24 — RP-050 ### 阶段:clean-build 基线修正与 `GFramework.Godot.SourceGenerators` 切片清零