test(godot-source-generators): 清理源生成器测试项目警告

- 重构 GFramework.Godot.SourceGenerators.Tests 的测试模板与诊断辅助,清除项目内全部 analyzer warning
- 更新 GeneratorTest 异步等待与 analyzer-warning-reduction 跟踪文档,记录批次验证结果与恢复点
This commit is contained in:
gewuyou 2026-04-24 14:06:41 +08:00
parent a439fb8f4e
commit 7e45197698
9 changed files with 1052 additions and 1375 deletions

View File

@ -6,57 +6,61 @@ namespace GFramework.Godot.SourceGenerators.Tests.Behavior;
[TestFixture] [TestFixture]
public class AutoSceneGeneratorTests 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>(T owner, string key)
where T : Node
{
return null!;
}
}
}
""";
[Test] [Test]
public async Task Generates_Scene_Behavior_Boilerplate() public async Task Generates_Scene_Behavior_Boilerplate()
{ {
const string source = """ string source = CreateAutoSceneSource(
using System; AutoSceneAttributeWithKeyDeclaration,
using GFramework.Godot.SourceGenerators.Abstractions.UI; """
using Godot; [AutoScene("Gameplay")]
public partial class GameplayRoot : Node2D
namespace GFramework.Godot.SourceGenerators.Abstractions.UI {
{ }
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] """,
public sealed class AutoSceneAttribute : Attribute includeBehaviorInfrastructure: true);
{
public AutoSceneAttribute(string key) { }
}
}
namespace Godot
{
public class Node { }
public class Node2D : Node { }
}
namespace GFramework.Game.Abstractions.Scene
{
public interface ISceneBehavior { }
}
namespace GFramework.Godot.Scene
{
using GFramework.Game.Abstractions.Scene;
using Godot;
public static class SceneBehaviorFactory
{
public static ISceneBehavior Create<T>(T owner, string key)
where T : Node
{
return null!;
}
}
}
namespace TestApp
{
[AutoScene("Gameplay")]
public partial class GameplayRoot : Node2D
{
}
}
""";
const string expected = """ const string expected = """
// <auto-generated /> // <auto-generated />
@ -80,40 +84,20 @@ public class AutoSceneGeneratorTests
await GeneratorTest<AutoSceneGenerator>.RunAsync( await GeneratorTest<AutoSceneGenerator>.RunAsync(
source, source,
("TestApp_GameplayRoot.AutoScene.g.cs", expected)); ("TestApp_GameplayRoot.AutoScene.g.cs", expected)).ConfigureAwait(false);
} }
[Test] [Test]
public async Task Reports_Diagnostic_When_AutoScene_Arguments_Are_Invalid() public async Task Reports_Diagnostic_When_AutoScene_Arguments_Are_Invalid()
{ {
const string source = """ string source = CreateAutoSceneSource(
using System; AutoSceneAttributeWithoutKeyDeclaration,
using GFramework.Godot.SourceGenerators.Abstractions.UI; """
using Godot; [{|#0:AutoScene|}]
public partial class GameplayRoot : Node2D
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
{
}
}
""";
var test = new CSharpSourceGeneratorTest<AutoSceneGenerator, DefaultVerifier> var test = new CSharpSourceGeneratorTest<AutoSceneGenerator, DefaultVerifier>
{ {
@ -128,65 +112,26 @@ public class AutoSceneGeneratorTests
.WithLocation(0) .WithLocation(0)
.WithArguments("AutoSceneAttribute", "GameplayRoot", "a single string scene key argument")); .WithArguments("AutoSceneAttribute", "GameplayRoot", "a single string scene key argument"));
await test.RunAsync(); await test.RunAsync().ConfigureAwait(false);
} }
[Test] [Test]
public async Task Generates_Type_Constraints_For_Nullable_Reference_NotNull_And_Unmanaged_Parameters() public async Task Generates_Type_Constraints_For_Nullable_Reference_NotNull_And_Unmanaged_Parameters()
{ {
const string source = """ string source = CreateAutoSceneSource(
#nullable enable AutoSceneAttributeWithKeyDeclaration,
using System; """
using GFramework.Godot.SourceGenerators.Abstractions.UI; [AutoScene("Gameplay")]
using Godot; public partial class GameplayRoot<TReference, TNotNull, TValue, TUnmanaged> : Node2D
where TReference : class?
namespace GFramework.Godot.SourceGenerators.Abstractions.UI where TNotNull : notnull
{ where TValue : struct
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] where TUnmanaged : unmanaged
public sealed class AutoSceneAttribute : Attribute {
{ }
public AutoSceneAttribute(string key) { } """,
} includeBehaviorInfrastructure: true,
} nullableEnabled: true);
namespace Godot
{
public class Node { }
public class Node2D : Node { }
}
namespace GFramework.Game.Abstractions.Scene
{
public interface ISceneBehavior { }
}
namespace GFramework.Godot.Scene
{
using GFramework.Game.Abstractions.Scene;
using Godot;
public static class SceneBehaviorFactory
{
public static ISceneBehavior Create<T>(T owner, string key)
where T : Node
{
return null!;
}
}
}
namespace TestApp
{
[AutoScene("Gameplay")]
public partial class GameplayRoot<TReference, TNotNull, TValue, TUnmanaged> : Node2D
where TReference : class?
where TNotNull : notnull
where TValue : struct
where TUnmanaged : unmanaged
{
}
}
""";
const string expected = """ const string expected = """
// <auto-generated /> // <auto-generated />
@ -214,7 +159,7 @@ public class AutoSceneGeneratorTests
await GeneratorTest<AutoSceneGenerator>.RunAsync( await GeneratorTest<AutoSceneGenerator>.RunAsync(
source, source,
("TestApp_GameplayRoot.AutoScene.g.cs", expected)); ("TestApp_GameplayRoot.AutoScene.g.cs", expected)).ConfigureAwait(false);
} }
/// <summary> /// <summary>
@ -267,7 +212,7 @@ public class AutoSceneGeneratorTests
.WithLocation(0) .WithLocation(0)
.WithArguments("GameplayRoot", "SceneKeyStr")); .WithArguments("GameplayRoot", "SceneKeyStr"));
await test.RunAsync(); await test.RunAsync().ConfigureAwait(false);
} }
/// <summary> /// <summary>
@ -326,6 +271,39 @@ public class AutoSceneGeneratorTests
.WithLocation(0) .WithLocation(0)
.WithArguments("GameplayRoot", "__autoSceneBehavior_Generated")); .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}}
}
""";
} }
} }

View File

@ -6,69 +6,85 @@ namespace GFramework.Godot.SourceGenerators.Tests.Behavior;
[TestFixture] [TestFixture]
public class AutoUiPageGeneratorTests 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>(T owner, string key, UiLayer layer)
where T : CanvasItem
{
return null!;
}
}
}
""";
[Test] [Test]
public async Task Generates_Ui_Page_Behavior_Boilerplate() public async Task Generates_Ui_Page_Behavior_Boilerplate()
{ {
const string source = """ string source = CreateAutoUiPageSource(
using System; AutoUiPageAttributeWithLayerDeclaration,
using GFramework.Godot.SourceGenerators.Abstractions.UI; UiLayerFullEnum,
using Godot; """
[AutoUiPage("MainMenu", "Page")]
namespace GFramework.Godot.SourceGenerators.Abstractions.UI public partial class MainMenu : Control
{ {
[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>(T owner, string key, UiLayer layer)
where T : CanvasItem
{
return null!;
}
}
}
namespace TestApp
{
[AutoUiPage("MainMenu", "Page")]
public partial class MainMenu : Control
{
}
}
""";
const string expected = """ const string expected = """
// <auto-generated /> // <auto-generated />
@ -92,70 +108,21 @@ public class AutoUiPageGeneratorTests
await GeneratorTest<AutoUiPageGenerator>.RunAsync( await GeneratorTest<AutoUiPageGenerator>.RunAsync(
source, source,
("TestApp_MainMenu.AutoUiPage.g.cs", expected)); ("TestApp_MainMenu.AutoUiPage.g.cs", expected)).ConfigureAwait(false);
} }
[Test] [Test]
public async Task Reports_Diagnostic_When_AutoUiPage_Attribute_Arguments_Are_Invalid() public async Task Reports_Diagnostic_When_AutoUiPage_Attribute_Arguments_Are_Invalid()
{ {
const string source = """ string source = CreateAutoUiPageSource(
using System; AutoUiPageAttributeWithoutLayerDeclaration,
using GFramework.Godot.SourceGenerators.Abstractions.UI; UiLayerPageOnlyEnum,
using Godot; """
[{|#0:AutoUiPage("MainMenu")|}]
namespace GFramework.Godot.SourceGenerators.Abstractions.UI public partial class MainMenu : Control
{ {
[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>(T owner, string key, UiLayer layer)
where T : CanvasItem
{
return null!;
}
}
}
namespace TestApp
{
[{|#0:AutoUiPage("MainMenu")|}]
public partial class MainMenu : Control
{
}
}
""";
var test = new CSharpSourceGeneratorTest<AutoUiPageGenerator, DefaultVerifier> var test = new CSharpSourceGeneratorTest<AutoUiPageGenerator, DefaultVerifier>
{ {
@ -174,74 +141,25 @@ public class AutoUiPageGeneratorTests
"MainMenu", "MainMenu",
"a string key argument and a string UiLayer name argument")); "a string key argument and a string UiLayer name argument"));
await test.RunAsync(); await test.RunAsync().ConfigureAwait(false);
} }
[Test] [Test]
public async Task Generates_Type_Constraints_For_ClassNullable_NotNull_And_Unmanaged() public async Task Generates_Type_Constraints_For_ClassNullable_NotNull_And_Unmanaged()
{ {
const string source = """ string source = CreateAutoUiPageSource(
#nullable enable AutoUiPageAttributeWithLayerDeclaration,
using System; UiLayerPageOnlyEnum,
using GFramework.Godot.SourceGenerators.Abstractions.UI; """
using Godot; [AutoUiPage("MainMenu", "Page")]
public partial class MainMenu<TReference, TNotNull, TUnmanaged> : Control
namespace GFramework.Godot.SourceGenerators.Abstractions.UI where TReference : class?
{ where TNotNull : notnull
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] where TUnmanaged : unmanaged
public sealed class AutoUiPageAttribute : Attribute {
{ }
public AutoUiPageAttribute(string key, string layerName) { } """,
} nullableEnabled: true);
}
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>(T owner, string key, UiLayer layer)
where T : CanvasItem
{
return null!;
}
}
}
namespace TestApp
{
[AutoUiPage("MainMenu", "Page")]
public partial class MainMenu<TReference, TNotNull, TUnmanaged> : Control
where TReference : class?
where TNotNull : notnull
where TUnmanaged : unmanaged
{
}
}
""";
const string expected = """ const string expected = """
// <auto-generated /> // <auto-generated />
@ -268,6 +186,40 @@ public class AutoUiPageGeneratorTests
await GeneratorTest<AutoUiPageGenerator>.RunAsync( await GeneratorTest<AutoUiPageGenerator>.RunAsync(
source, 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}}
}
""";
} }
} }

View File

@ -8,93 +8,103 @@ namespace GFramework.Godot.SourceGenerators.Tests.BindNodeSignal;
[TestFixture] [TestFixture]
public class BindNodeSignalGeneratorTests 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 {}
}
}
""";
/// <summary> /// <summary>
/// 验证生成器会为已有生命周期调用生成成对的绑定与解绑方法。 /// 验证生成器会为已有生命周期调用生成成对的绑定与解绑方法。
/// </summary> /// </summary>
[Test] [Test]
public async Task Generates_Bind_And_Unbind_Methods_For_Existing_Lifecycle_Hooks() public async Task Generates_Bind_And_Unbind_Methods_For_Existing_Lifecycle_Hooks()
{ {
const string source = """ string source = CreateHudSource(
using System; CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
using GFramework.Godot.SourceGenerators.Abstractions; """
using Godot; private Button _startButton = null!;
private SpinBox _startOreSpinBox = null!;
namespace GFramework.Godot.SourceGenerators.Abstractions [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
{ private void OnStartButtonPressed()
[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; } [BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))]
private void OnStartOreValueChanged(double value)
{
}
public string SignalName { get; } public override void _Ready()
} {
} __BindNodeSignals_Generated();
}
namespace Godot public override void _ExitTree()
{ {
public class Node __UnbindNodeSignals_Generated();
{ }
public virtual void _Ready() {} """,
LifecycleNodeType,
public virtual void _ExitTree() {} ButtonType,
} SpinBoxType);
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();
}
}
}
""";
const string expected = """ const string expected = """
// <auto-generated /> // <auto-generated />
@ -121,7 +131,7 @@ public class BindNodeSignalGeneratorTests
await GeneratorTest<BindNodeSignalGenerator>.RunAsync( await GeneratorTest<BindNodeSignalGenerator>.RunAsync(
source, source,
("TestApp_Hud.BindNodeSignal.g.cs", expected)); ("TestApp_Hud.BindNodeSignal.g.cs", expected)).ConfigureAwait(false);
} }
/// <summary> /// <summary>
@ -130,70 +140,23 @@ public class BindNodeSignalGeneratorTests
[Test] [Test]
public async Task Generates_Multiple_Subscriptions_For_The_Same_Handler_And_Coexists_With_GetNode() public async Task Generates_Multiple_Subscriptions_For_The_Same_Handler_And_Coexists_With_GetNode()
{ {
const string source = """ string source = CreateHudSource(
using System; CreateAbstractionsSource(BindNodeSignalAttributeDeclaration, GetNodeAttributeDeclaration),
using GFramework.Godot.SourceGenerators.Abstractions; """
using Godot; [GetNode]
private Button _startButton = null!;
namespace GFramework.Godot.SourceGenerators.Abstractions [GetNode]
{ private Button _cancelButton = null!;
[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; } [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
[BindNodeSignal(nameof(_cancelButton), nameof(Button.Pressed))]
public string SignalName { get; } private void OnAnyButtonPressed()
} {
}
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] """,
public sealed class GetNodeAttribute : Attribute LifecycleNodeType,
{ ButtonType);
}
}
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()
{
}
}
}
""";
const string expected = """ const string expected = """
// <auto-generated /> // <auto-generated />
@ -220,7 +183,7 @@ public class BindNodeSignalGeneratorTests
await GeneratorTest<BindNodeSignalGenerator>.RunAsync( await GeneratorTest<BindNodeSignalGenerator>.RunAsync(
source, source,
("TestApp_Hud.BindNodeSignal.g.cs", expected)); ("TestApp_Hud.BindNodeSignal.g.cs", expected)).ConfigureAwait(false);
} }
/// <summary> /// <summary>
@ -229,73 +192,24 @@ public class BindNodeSignalGeneratorTests
[Test] [Test]
public async Task Reports_Diagnostic_When_Signal_Does_Not_Exist() public async Task Reports_Diagnostic_When_Signal_Does_Not_Exist()
{ {
const string source = """ string source = CreateHudSource(
using System; CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
using GFramework.Godot.SourceGenerators.Abstractions; """
using Godot; private Button _startButton = null!;
namespace GFramework.Godot.SourceGenerators.Abstractions [{|#0:BindNodeSignal(nameof(_startButton), "Released")|}]
{ private void OnStartButtonPressed()
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] {
public sealed class BindNodeSignalAttribute : Attribute }
{ """,
public BindNodeSignalAttribute(string nodeFieldName, string signalName) EmptyNodeType,
{ ButtonType);
NodeFieldName = nodeFieldName;
SignalName = signalName;
}
public string NodeFieldName { get; } await VerifyDiagnosticsAsync(
source,
public string SignalName { get; } new DiagnosticResult("GF_Godot_BindNodeSignal_006", DiagnosticSeverity.Error)
} .WithLocation(0)
} .WithArguments("_startButton", "Released")).ConfigureAwait(false);
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<BindNodeSignalGenerator, DefaultVerifier>
{
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();
} }
/// <summary> /// <summary>
@ -304,75 +218,24 @@ public class BindNodeSignalGeneratorTests
[Test] [Test]
public async Task Reports_Diagnostic_When_Method_Signature_Does_Not_Match_Event() public async Task Reports_Diagnostic_When_Method_Signature_Does_Not_Match_Event()
{ {
const string source = """ string source = CreateHudSource(
using System; CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
using GFramework.Godot.SourceGenerators.Abstractions; """
using Godot; private SpinBox _startOreSpinBox = null!;
namespace GFramework.Godot.SourceGenerators.Abstractions [{|#0:BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))|}]
{ private void OnStartOreValueChanged()
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] {
public sealed class BindNodeSignalAttribute : Attribute }
{ """,
public BindNodeSignalAttribute(string nodeFieldName, string signalName) EmptyNodeType,
{ SpinBoxType);
NodeFieldName = nodeFieldName;
SignalName = signalName;
}
public string NodeFieldName { get; } await VerifyDiagnosticsAsync(
source,
public string SignalName { get; } new DiagnosticResult("GF_Godot_BindNodeSignal_007", DiagnosticSeverity.Error)
} .WithLocation(0)
} .WithArguments("OnStartOreValueChanged", "ValueChanged", "_startOreSpinBox")).ConfigureAwait(false);
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<BindNodeSignalGenerator, DefaultVerifier>
{
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();
} }
/// <summary> /// <summary>
@ -381,73 +244,24 @@ public class BindNodeSignalGeneratorTests
[Test] [Test]
public async Task Reports_Diagnostic_When_Constructor_Argument_Is_Empty() public async Task Reports_Diagnostic_When_Constructor_Argument_Is_Empty()
{ {
const string source = """ string source = CreateHudSource(
using System; CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
using GFramework.Godot.SourceGenerators.Abstractions; """
using Godot; private Button _startButton = null!;
namespace GFramework.Godot.SourceGenerators.Abstractions [{|#0:BindNodeSignal(nameof(_startButton), "")|}]
{ private void OnStartButtonPressed()
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] {
public sealed class BindNodeSignalAttribute : Attribute }
{ """,
public BindNodeSignalAttribute(string nodeFieldName, string signalName) EmptyNodeType,
{ ButtonType);
NodeFieldName = nodeFieldName;
SignalName = signalName;
}
public string NodeFieldName { get; } await VerifyDiagnosticsAsync(
source,
public string SignalName { get; } new DiagnosticResult("GF_Godot_BindNodeSignal_010", DiagnosticSeverity.Error)
} .WithLocation(0)
} .WithArguments("OnStartButtonPressed", "signalName")).ConfigureAwait(false);
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<BindNodeSignalGenerator, DefaultVerifier>
{
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();
} }
/// <summary> /// <summary>
@ -456,85 +270,35 @@ public class BindNodeSignalGeneratorTests
[Test] [Test]
public async Task Reports_Diagnostic_When_Generated_Method_Names_Already_Exist() public async Task Reports_Diagnostic_When_Generated_Method_Names_Already_Exist()
{ {
const string source = """ string source = CreateHudSource(
using System; CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
using GFramework.Godot.SourceGenerators.Abstractions; """
using Godot; private Button _startButton = null!;
namespace GFramework.Godot.SourceGenerators.Abstractions [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
{ private void OnStartButtonPressed()
[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; } private void {|#0:__BindNodeSignals_Generated|}()
{
}
public string SignalName { get; } private void {|#1:__UnbindNodeSignals_Generated|}()
} {
} }
""",
EmptyNodeType,
ButtonType);
namespace Godot await VerifyDiagnosticsAsync(
{ source,
public class Node new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error)
{ .WithLocation(0)
} .WithArguments("Hud", "__BindNodeSignals_Generated"),
new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error)
public class Button : Node .WithLocation(1)
{ .WithArguments("Hud", "__UnbindNodeSignals_Generated")).ConfigureAwait(false);
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<BindNodeSignalGenerator, DefaultVerifier>
{
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();
} }
/// <summary> /// <summary>
@ -543,69 +307,80 @@ public class BindNodeSignalGeneratorTests
[Test] [Test]
public async Task Reports_Warnings_When_Lifecycle_Methods_Do_Not_Call_Generated_Methods() public async Task Reports_Warnings_When_Lifecycle_Methods_Do_Not_Call_Generated_Methods()
{ {
const string source = """ string source = CreateHudSource(
using System; CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
using GFramework.Godot.SourceGenerators.Abstractions; """
using Godot; private Button _startButton = null!;
namespace GFramework.Godot.SourceGenerators.Abstractions [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
{ private void OnStartButtonPressed()
[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 override void {|#0:_Ready|}()
{
}
public string SignalName { get; } public override void {|#1:_ExitTree|}()
} {
} }
""",
LifecycleNodeType,
ButtonType);
namespace Godot await VerifyDiagnosticsAsync(
{ source,
public class Node new DiagnosticResult("GF_Godot_BindNodeSignal_008", DiagnosticSeverity.Warning)
{ .WithLocation(0)
public virtual void _Ready() {} .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 return $$"""
{ namespace GFramework.Godot.SourceGenerators.Abstractions
public event Action? Pressed {
{ {{declarations}}
add {} }
remove {} """;
} }
}
}
namespace TestApp private static string CreateHudSource(
{ string abstractionsSource,
public partial class Hud : Node string hudMembers,
{ params string[] godotTypes)
private Button _startButton = null!; {
string godotSource = string.Join($"{Environment.NewLine}{Environment.NewLine}", godotTypes);
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] return $$"""
private void OnStartButtonPressed() 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<BindNodeSignalGenerator, DefaultVerifier> var test = new CSharpSourceGeneratorTest<BindNodeSignalGenerator, DefaultVerifier>
{ {
TestState = TestState =
@ -616,14 +391,11 @@ public class BindNodeSignalGeneratorTests
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
}; };
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_008", DiagnosticSeverity.Warning) foreach (DiagnosticResult expectedDiagnostic in expectedDiagnostics)
.WithLocation(0) {
.WithArguments("Hud")); test.ExpectedDiagnostics.Add(expectedDiagnostic);
}
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_009", DiagnosticSeverity.Warning) return test.RunAsync();
.WithLocation(1)
.WithArguments("Hud"));
await test.RunAsync();
} }
} }

View File

@ -29,7 +29,7 @@ public static class GeneratorTest<TGenerator>
test.TestState.GeneratedSources.Add( test.TestState.GeneratedSources.Add(
(typeof(TGenerator), filename, NormalizeLineEndings(content))); (typeof(TGenerator), filename, NormalizeLineEndings(content)));
await test.RunAsync(); await test.RunAsync().ConfigureAwait(false);
} }
/// <summary> /// <summary>

View File

@ -5,61 +5,88 @@ namespace GFramework.Godot.SourceGenerators.Tests.GetNode;
[TestFixture] [TestFixture]
public class GetNodeGeneratorTests 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<T>(string path) where T : Node => throw new InvalidOperationException(path);
public T? GetNodeOrNull<T>(string path) where T : Node => default;
}
""";
private const string HBoxContainerType = """
public class HBoxContainer : Node
{
}
""";
[Test] [Test]
public async Task Generates_InferredUniqueNameBindings_And_ReadyHook_WhenReadyIsMissing() public async Task Generates_InferredUniqueNameBindings_And_ReadyHook_WhenReadyIsMissing()
{ {
const string source = """ string source = CreateGetNodeSource(
using System; FullGetNodeAttributeDeclaration,
using GFramework.Godot.SourceGenerators.Abstractions; """
using Godot; public partial class TopBar : HBoxContainer
{
[GetNode]
private HBoxContainer _leftContainer = null!;
namespace GFramework.Godot.SourceGenerators.Abstractions [GetNode]
{ private HBoxContainer m_rightContainer = null!;
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] }
public sealed class GetNodeAttribute : Attribute """,
{ HBoxContainerType);
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<T>(string path) where T : Node => throw new InvalidOperationException(path);
public T? GetNodeOrNull<T>(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!;
}
}
""";
const string expected = """ const string expected = """
// <auto-generated /> // <auto-generated />
@ -88,69 +115,30 @@ public class GetNodeGeneratorTests
await GeneratorTest<GetNodeGenerator>.RunAsync( await GeneratorTest<GetNodeGenerator>.RunAsync(
source, source,
("TestApp_TopBar.GetNode.g.cs", expected)); ("TestApp_TopBar.GetNode.g.cs", expected)).ConfigureAwait(false);
} }
[Test] [Test]
public async Task Generates_ManualInjectionOnly_WhenReadyAlreadyExists() public async Task Generates_ManualInjectionOnly_WhenReadyAlreadyExists()
{ {
const string source = """ string source = CreateGetNodeSource(
using System; FullGetNodeAttributeDeclaration,
using GFramework.Godot.SourceGenerators.Abstractions; """
using Godot; public partial class TopBar : HBoxContainer
{
[GetNode("%LeftContainer")]
private HBoxContainer _leftContainer = null!;
namespace GFramework.Godot.SourceGenerators.Abstractions [GetNode(Required = false, Lookup = NodeLookupMode.RelativePath)]
{ private HBoxContainer? _rightContainer;
[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 public override void _Ready()
{ {
Auto = 0, __InjectGetNodes_Generated();
UniqueName = 1, }
RelativePath = 2, }
AbsolutePath = 3 """,
} HBoxContainerType);
}
namespace Godot
{
public class Node
{
public virtual void _Ready() {}
public T GetNode<T>(string path) where T : Node => throw new InvalidOperationException(path);
public T? GetNodeOrNull<T>(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();
}
}
}
""";
const string expected = """ const string expected = """
// <auto-generated /> // <auto-generated />
@ -171,7 +159,7 @@ public class GetNodeGeneratorTests
await GeneratorTest<GetNodeGenerator>.RunAsync( await GeneratorTest<GetNodeGenerator>.RunAsync(
source, source,
("TestApp_TopBar.GetNode.g.cs", expected)); ("TestApp_TopBar.GetNode.g.cs", expected)).ConfigureAwait(false);
} }
[Test] [Test]
@ -234,58 +222,26 @@ public class GetNodeGeneratorTests
.WithSpan(39, 24, 39, 38) .WithSpan(39, 24, 39, 38)
.WithArguments("_leftContainer")); .WithArguments("_leftContainer"));
await test.RunAsync(); await test.RunAsync().ConfigureAwait(false);
} }
[Test] [Test]
public async Task Reports_Diagnostic_When_Generated_Injection_Method_Name_Already_Exists() public async Task Reports_Diagnostic_When_Generated_Injection_Method_Name_Already_Exists()
{ {
const string source = """ string source = CreateGetNodeSource(
using System; MinimalGetNodeAttributeDeclaration,
using GFramework.Godot.SourceGenerators.Abstractions; """
using Godot; public partial class TopBar : HBoxContainer
{
[GetNode]
private HBoxContainer _leftContainer = null!;
namespace GFramework.Godot.SourceGenerators.Abstractions private void {|#0:__InjectGetNodes_Generated|}()
{ {
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] }
public sealed class GetNodeAttribute : Attribute }
{ """,
public GetNodeAttribute() {} HBoxContainerType);
}
public enum NodeLookupMode
{
Auto = 0
}
}
namespace Godot
{
public class Node
{
public virtual void _Ready() {}
public T GetNode<T>(string path) where T : Node => throw new InvalidOperationException(path);
public T? GetNodeOrNull<T>(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|}()
{
}
}
}
""";
var test = new CSharpSourceGeneratorTest<GetNodeGenerator, DefaultVerifier> var test = new CSharpSourceGeneratorTest<GetNodeGenerator, DefaultVerifier>
{ {
@ -301,6 +257,39 @@ public class GetNodeGeneratorTests
.WithLocation(0) .WithLocation(0)
.WithArguments("TopBar", "__InjectGetNodes_Generated")); .WithArguments("TopBar", "__InjectGetNodes_Generated"));
await test.RunAsync(); await test.RunAsync().ConfigureAwait(false);
}
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}}
}
""";
} }
} }

View File

@ -8,6 +8,131 @@ namespace GFramework.Godot.SourceGenerators.Tests.Project;
[TestFixture] [TestFixture]
public class GodotProjectMetadataGeneratorTests 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 = """
// <auto-generated />
#nullable enable
namespace GFramework.Godot.Generated;
/// <summary>
/// 提供 project.godot 中 AutoLoad 单例的强类型访问入口。
/// </summary>
public static partial class AutoLoads
{
/// <summary>
/// 获取 AutoLoad <c>GameServices</c>。
/// </summary>
public static global::TestApp.GameServices GameServices => GetRequiredNode<global::TestApp.GameServices>("GameServices");
/// <summary>
/// 尝试获取 AutoLoad <c>GameServices</c>。
/// </summary>
public static bool TryGetGameServices(out global::TestApp.GameServices? value)
{
return TryGetNode("GameServices", out value);
}
/// <summary>
/// 获取 AutoLoad <c>AudioBus</c>。
/// </summary>
public static global::Godot.Node AudioBus => GetRequiredNode<global::Godot.Node>("AudioBus");
/// <summary>
/// 尝试获取 AutoLoad <c>AudioBus</c>。
/// </summary>
public static bool TryGetAudioBus(out global::Godot.Node? value)
{
return TryGetNode("AudioBus", out value);
}
/// <summary>
/// 获取一个必填的 AutoLoad 节点;缺失时抛出异常。
/// </summary>
/// <typeparam name="TNode">节点类型。</typeparam>
/// <param name="autoLoadName">AutoLoad 名称。</param>
/// <returns>已解析的 AutoLoad 节点。</returns>
private static TNode GetRequiredNode<TNode>(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.");
}
/// <summary>
/// 尝试从当前 SceneTree 根节点解析 AutoLoad。
/// </summary>
/// <typeparam name="TNode">节点类型。</typeparam>
/// <param name="autoLoadName">AutoLoad 名称。</param>
/// <param name="value">解析到的节点实例。</param>
/// <returns>若当前进程存在 SceneTree 且根节点中能解析到该 AutoLoad则返回 <c>true</c>。</returns>
private static bool TryGetNode<TNode>(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<TNode>($"/root/{autoLoadName}");
return value is not null;
}
}
""";
private const string ExpectedInputActions = """
// <auto-generated />
#nullable enable
namespace GFramework.Godot.Generated;
/// <summary>
/// 提供 project.godot 中 Input Action 名称的强类型常量。
/// </summary>
public static partial class InputActions
{
/// <summary>
/// Input Action <c>move_up</c> 的稳定名称。
/// </summary>
public const string MoveUp = "move_up";
/// <summary>
/// Input Action <c>ui_cancel</c> 的稳定名称。
/// </summary>
public const string UiCancel = "ui_cancel";
}
""";
/// <summary> /// <summary>
/// 验证会根据 AutoLoad 与 Input Action 生成稳定的强类型入口。 /// 验证会根据 AutoLoad 与 Input Action 生成稳定的强类型入口。
/// </summary> /// </summary>
@ -29,142 +154,19 @@ public class GodotProjectMetadataGeneratorTests
""", """,
includeAutoLoadAttribute: true); 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 = """
// <auto-generated />
#nullable enable
namespace GFramework.Godot.Generated;
/// <summary>
/// 提供 project.godot 中 AutoLoad 单例的强类型访问入口。
/// </summary>
public static partial class AutoLoads
{
/// <summary>
/// 获取 AutoLoad <c>GameServices</c>。
/// </summary>
public static global::TestApp.GameServices GameServices => GetRequiredNode<global::TestApp.GameServices>("GameServices");
/// <summary>
/// 尝试获取 AutoLoad <c>GameServices</c>。
/// </summary>
public static bool TryGetGameServices(out global::TestApp.GameServices? value)
{
return TryGetNode("GameServices", out value);
}
/// <summary>
/// 获取 AutoLoad <c>AudioBus</c>。
/// </summary>
public static global::Godot.Node AudioBus => GetRequiredNode<global::Godot.Node>("AudioBus");
/// <summary>
/// 尝试获取 AutoLoad <c>AudioBus</c>。
/// </summary>
public static bool TryGetAudioBus(out global::Godot.Node? value)
{
return TryGetNode("AudioBus", out value);
}
/// <summary>
/// 获取一个必填的 AutoLoad 节点;缺失时抛出异常。
/// </summary>
/// <typeparam name="TNode">节点类型。</typeparam>
/// <param name="autoLoadName">AutoLoad 名称。</param>
/// <returns>已解析的 AutoLoad 节点。</returns>
private static TNode GetRequiredNode<TNode>(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.");
}
/// <summary>
/// 尝试从当前 SceneTree 根节点解析 AutoLoad。
/// </summary>
/// <typeparam name="TNode">节点类型。</typeparam>
/// <param name="autoLoadName">AutoLoad 名称。</param>
/// <param name="value">解析到的节点实例。</param>
/// <returns>若当前进程存在 SceneTree 且根节点中能解析到该 AutoLoad则返回 <c>true</c>。</returns>
private static bool TryGetNode<TNode>(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<TNode>($"/root/{autoLoadName}");
return value is not null;
}
}
""";
const string expectedInputActions = """
// <auto-generated />
#nullable enable
namespace GFramework.Godot.Generated;
/// <summary>
/// 提供 project.godot 中 Input Action 名称的强类型常量。
/// </summary>
public static partial class InputActions
{
/// <summary>
/// Input Action <c>move_up</c> 的稳定名称。
/// </summary>
public const string MoveUp = "move_up";
/// <summary>
/// Input Action <c>ui_cancel</c> 的稳定名称。
/// </summary>
public const string UiCancel = "ui_cancel";
}
""";
var result = AdditionalTextGeneratorTestDriver.Run<GodotProjectMetadataGenerator>( var result = AdditionalTextGeneratorTestDriver.Run<GodotProjectMetadataGenerator>(
source, source,
("project.godot", projectFile)); ("project.godot", $"{AutoLoadProjectFile}\n\n{InputActionsProjectFile}"));
var generatedSources = AdditionalTextGeneratorTestDriver.ToGeneratedSourceMap(result); var generatedSources = AdditionalTextGeneratorTestDriver.ToGeneratedSourceMap(result);
Assert.That(result.Results.Single().Diagnostics, Is.Empty); Assert.That(result.Results.Single().Diagnostics, Is.Empty);
Assert.That( Assert.That(
generatedSources["GFramework_Godot_Generated_AutoLoads.g.cs"], generatedSources["GFramework_Godot_Generated_AutoLoads.g.cs"],
Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(expectedAutoLoads))); Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(ExpectedAutoLoads)));
Assert.That( Assert.That(
generatedSources["GFramework_Godot_Generated_InputActions.g.cs"], generatedSources["GFramework_Godot_Generated_InputActions.g.cs"],
Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(expectedInputActions))); Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(ExpectedInputActions)));
} }
/// <summary> /// <summary>

View File

@ -6,48 +6,52 @@ namespace GFramework.Godot.SourceGenerators.Tests.Registration;
[TestFixture] [TestFixture]
public class AutoRegisterExportedCollectionsGeneratorTests 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] [Test]
public async Task Generates_Batch_Registration_Method_For_Annotated_Collections() public async Task Generates_Batch_Registration_Method_For_Annotated_Collections()
{ {
const string source = """ string source = CreateSource(
#nullable enable """
using System; public sealed class IntRegistry
using System.Collections.Generic; {
using GFramework.Godot.SourceGenerators.Abstractions.UI; public void Register(int value) { }
}
namespace GFramework.Godot.SourceGenerators.Abstractions.UI [AutoRegisterExportedCollections]
{ public partial class Bootstrapper<TReference, TNotNull, TValue, TUnmanaged>
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] where TReference : class?
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } where TNotNull : notnull
where TValue : struct
where TUnmanaged : unmanaged
{
private readonly IntRegistry? _registry = new();
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public sealed class RegisterExportedCollectionAttribute : Attribute public List<int>? Values { get; } = new();
{ }
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { } """,
} nullableEnabled: true);
}
namespace TestApp
{
public sealed class IntRegistry
{
public void Register(int value) { }
}
[AutoRegisterExportedCollections]
public partial class Bootstrapper<TReference, TNotNull, TValue, TUnmanaged>
where TReference : class?
where TNotNull : notnull
where TValue : struct
where TUnmanaged : unmanaged
{
private readonly IntRegistry? _registry = new();
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public List<int>? Values { get; } = new();
}
}
""";
const string expected = """ const string expected = """
// <auto-generated /> // <auto-generated />
@ -77,7 +81,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync( await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source, source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)); ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
} }
[Test] [Test]
@ -137,41 +141,23 @@ public class AutoRegisterExportedCollectionsGeneratorTests
[Test] [Test]
public async Task Generates_Batch_Registration_Method_When_Register_Method_Uses_Array_Parameter() public async Task Generates_Batch_Registration_Method_When_Register_Method_Uses_Array_Parameter()
{ {
const string source = """ string source = CreateSource(
#nullable enable """
using System; public sealed class ArrayRegistry
using System.Collections.Generic; {
using GFramework.Godot.SourceGenerators.Abstractions.UI; public void Register(int[] value) { }
}
namespace GFramework.Godot.SourceGenerators.Abstractions.UI [AutoRegisterExportedCollections]
{ public partial class Bootstrapper
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] {
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } private readonly ArrayRegistry _registry = new();
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] [RegisterExportedCollection(nameof(_registry), nameof(ArrayRegistry.Register))]
public sealed class RegisterExportedCollectionAttribute : Attribute public List<int[]> Values { get; } = new();
{ }
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { } """,
} nullableEnabled: true);
}
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<int[]> Values { get; } = new();
}
}
""";
const string expected = """ const string expected = """
// <auto-generated /> // <auto-generated />
@ -197,59 +183,41 @@ public class AutoRegisterExportedCollectionsGeneratorTests
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync( await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source, source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)); ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
} }
[Test] [Test]
public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Inherited_Interface() public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Inherited_Interface()
{ {
const string source = """ string source = CreateSource(
#nullable enable """
using System; public interface IKeyValue<TKey, TValue>
using System.Collections.Generic; {
using GFramework.Godot.SourceGenerators.Abstractions.UI; }
namespace GFramework.Godot.SourceGenerators.Abstractions.UI public interface IRegistry<TKey, TValue>
{ {
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] void Registry(IKeyValue<TKey, TValue> mapping);
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] public interface IAssetRegistry<TValue> : IRegistry<string, TValue>
public sealed class RegisterExportedCollectionAttribute : Attribute {
{ }
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
}
namespace TestApp public sealed class IntConfig : IKeyValue<string, int>
{ {
public interface IKeyValue<TKey, TValue> }
{
}
public interface IRegistry<TKey, TValue> [AutoRegisterExportedCollections]
{ public partial class Bootstrapper
void Registry(IKeyValue<TKey, TValue> mapping); {
} private readonly IAssetRegistry<int>? _registry = null;
public interface IAssetRegistry<TValue> : IRegistry<string, TValue> [RegisterExportedCollection(nameof(_registry), "Registry")]
{ public List<IntConfig>? Values { get; } = new();
} }
""",
public sealed class IntConfig : IKeyValue<string, int> nullableEnabled: true);
{
}
[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
private readonly IAssetRegistry<int>? _registry = null;
[RegisterExportedCollection(nameof(_registry), "Registry")]
public List<IntConfig>? Values { get; } = new();
}
}
""";
const string expected = """ const string expected = """
// <auto-generated /> // <auto-generated />
@ -275,7 +243,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync( await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source, source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)); ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
} }
[Test] [Test]
@ -340,45 +308,27 @@ public class AutoRegisterExportedCollectionsGeneratorTests
[Test] [Test]
public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Base_Class() public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Base_Class()
{ {
const string source = """ string source = CreateSource(
#nullable enable """
using System; public class BaseRegistry
using System.Collections.Generic; {
using GFramework.Godot.SourceGenerators.Abstractions.UI; public void Register(int value) { }
}
namespace GFramework.Godot.SourceGenerators.Abstractions.UI public sealed class DerivedRegistry : BaseRegistry
{ {
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] }
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] [AutoRegisterExportedCollections]
public sealed class RegisterExportedCollectionAttribute : Attribute public partial class Bootstrapper
{ {
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { } private readonly DerivedRegistry? _registry = new();
}
}
namespace TestApp [RegisterExportedCollection(nameof(_registry), nameof(BaseRegistry.Register))]
{ public List<int>? Values { get; } = new();
public class BaseRegistry }
{ """,
public void Register(int value) { } nullableEnabled: true);
}
public sealed class DerivedRegistry : BaseRegistry
{
}
[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
private readonly DerivedRegistry? _registry = new();
[RegisterExportedCollection(nameof(_registry), nameof(BaseRegistry.Register))]
public List<int>? Values { get; } = new();
}
}
""";
const string expected = """ const string expected = """
// <auto-generated /> // <auto-generated />
@ -404,50 +354,32 @@ public class AutoRegisterExportedCollectionsGeneratorTests
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync( await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source, source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)); ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
} }
[Test] [Test]
public async Task Generates_Batch_Registration_Method_When_Registry_Member_Comes_From_Base_Class() public async Task Generates_Batch_Registration_Method_When_Registry_Member_Comes_From_Base_Class()
{ {
const string source = """ string source = CreateSource(
#nullable enable """
using System; public sealed class IntRegistry
using System.Collections.Generic; {
using GFramework.Godot.SourceGenerators.Abstractions.UI; public void Register(int value) { }
}
namespace GFramework.Godot.SourceGenerators.Abstractions.UI public abstract class BootstrapperBase
{ {
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] protected readonly IntRegistry? _registry = new();
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] [AutoRegisterExportedCollections]
public sealed class RegisterExportedCollectionAttribute : Attribute public partial class Bootstrapper : BootstrapperBase
{ {
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { } [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
} public List<int>? Values { get; } = new();
} }
""",
namespace TestApp nullableEnabled: true);
{
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<int>? Values { get; } = new();
}
}
""";
const string expected = """ const string expected = """
// <auto-generated /> // <auto-generated />
@ -473,74 +405,47 @@ public class AutoRegisterExportedCollectionsGeneratorTests
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync( await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source, source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)); ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
} }
[Test] [Test]
public async Task Reports_Diagnostic_When_Collection_Member_Is_Not_Instance_Readable() public async Task Reports_Diagnostic_When_Collection_Member_Is_Not_Instance_Readable()
{ {
const string source = """ string source = CreateSource(
using System; """
using System.Collections.Generic; public sealed class IntRegistry
using GFramework.Godot.SourceGenerators.Abstractions.UI; {
public void Register(int value) { }
}
namespace GFramework.Godot.SourceGenerators.Abstractions.UI [AutoRegisterExportedCollections]
{ public partial class Bootstrapper
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] {
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } private readonly IntRegistry _registry = new();
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public sealed class RegisterExportedCollectionAttribute : Attribute public static List<int> {|#0:StaticValues|} = new();
{
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
}
namespace TestApp [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
{ public static List<int> {|#1:StaticPropertyValues|} { get; } = new();
public sealed class IntRegistry
{
public void Register(int value) { }
}
[AutoRegisterExportedCollections] [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public partial class Bootstrapper public List<int> {|#2:WriteOnlyValues|} { set { } }
{ }
private readonly IntRegistry _registry = new(); """);
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] await VerifyDiagnosticsAsync(
public static List<int> {|#0:StaticValues|} = new(); source,
skipGeneratedSourcesCheck: true,
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
public static List<int> {|#1:StaticPropertyValues|} { get; } = new(); .WithLocation(0)
.WithArguments("StaticValues"),
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
public List<int> {|#2:WriteOnlyValues|} { set { } } .WithLocation(1)
} .WithArguments("StaticPropertyValues"),
} new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
"""; .WithLocation(2)
.WithArguments("WriteOnlyValues")).ConfigureAwait(false);
var test = new CSharpSourceGeneratorTest<AutoRegisterExportedCollectionsGenerator, DefaultVerifier>
{
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();
} }
[Test] [Test]
@ -711,45 +616,28 @@ public class AutoRegisterExportedCollectionsGeneratorTests
[Test] [Test]
public async Task Generates_Only_One_Source_When_Multiple_Partial_Declarations_Are_Annotated() public async Task Generates_Only_One_Source_When_Multiple_Partial_Declarations_Are_Annotated()
{ {
const string source = """ string source = CreateSource(
#nullable enable """
using System; public sealed class IntRegistry
using System.Collections.Generic; {
using GFramework.Godot.SourceGenerators.Abstractions.UI; public void Register(int value) { }
}
namespace GFramework.Godot.SourceGenerators.Abstractions.UI [AutoRegisterExportedCollections]
{ public partial class Bootstrapper
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] {
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } private readonly IntRegistry? _registry = new();
}
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] [AutoRegisterExportedCollections]
public sealed class RegisterExportedCollectionAttribute : Attribute public partial class Bootstrapper
{ {
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { } [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
} public List<int>? Values { get; } = new();
} }
""",
namespace TestApp nullableEnabled: true,
{ allowMultipleDeclarations: true);
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<int>? Values { get; } = new();
}
}
""";
const string expected = """ const string expected = """
// <auto-generated /> // <auto-generated />
@ -775,6 +663,61 @@ public class AutoRegisterExportedCollectionsGeneratorTests
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync( await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source, 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<AutoRegisterExportedCollectionsGenerator, DefaultVerifier>
{
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();
} }
} }

View File

@ -6,27 +6,32 @@
## 当前恢复点 ## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-050` - 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-051`
- 当前阶段:`Phase 50` - 当前阶段:`Phase 51`
- 当前焦点: - 当前焦点:
- warning 基线已修正为仓库根目录执行 `dotnet clean` 后再执行 `dotnet build` - `2026-04-24` 本轮已完成 `GFramework.Godot.SourceGenerators.Tests` warning 清理
- `2026-04-24` 用户确认的 clean solution build 结果为 `Build succeeded with 1193 warning(s)` - 当前主线程切片从生成器实现转到对应测试项目,并已把 `GFramework.Godot.SourceGenerators.Tests``24` 个 warning 降到 `0`
- 当前主线程切片为 `GFramework.Godot.SourceGenerators` - 当前批次按 `origin/main` merge-base 计算的累计分支 diff 预计为 `23` 个文件,仍低于 `$gframework-batch-boot 75` 的主阈值
- 当前工作树除未跟踪的 `.codex` 目录外,存在待提交的 source generator / `AGENTS.md` / `ai-plan` 修改 - 当前工作树除未跟踪的 `.codex` 目录外,还存在与本批次无关的既有文档 / 跟踪文件修改;提交当前批次时必须只包含本 topic 相关文件
## 当前活跃事实 ## 当前活跃事实
- 之前记录的 plain `dotnet build` `0 Warning(s)` 属于增量构建假阴性,不能再作为 warning 检查真值 - 之前记录的 plain `dotnet build` `0 Warning(s)` 属于增量构建假阴性,不能再作为 warning 检查真值
- 本轮已完成 `GFramework.Godot.SourceGenerators` warning 清理clean `Release` build 从 9 个 warning 降至 0 个 warning - 本轮已完成 `GFramework.Godot.SourceGenerators` warning 清理clean `Release` build 从 9 个 warning 降至 0 个 warning
- 当前已确认解决的文件包括 `BindNodeSignalGenerator.cs``GetNodeGenerator.cs``GodotProjectMetadataGenerator.cs``Registration/AutoRegisterExportedCollectionsGenerator.cs` - 当前已确认解决的文件包括 `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 - 如果后续继续依赖增量 `dotnet build`,容易再次把 warning 数量误判为 0
- 缓解措施:每轮 warning 检查前先执行 `dotnet clean`,再执行目标 `dotnet build` - 缓解措施:每轮 warning 检查前先执行 `dotnet clean`,再执行目标 `dotnet build`
- 当前只验证了受影响项目 `GFramework.Godot.SourceGenerators`;整仓库 warning 总量仍应以用户确认的 clean solution build 为基线 - 仓库根目录 `dotnet clean` 目前仍然无法给出新的 clean 基线
- 缓解措施:下一轮从 clean solution build 输出里选择新的低风险 warning 热点继续切片 - 缓解措施:若下一轮继续做整仓 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` - `dotnet clean`
- 结果:成功;`0 Warning(s)``0 Error(s)` - 结果:失败;停在 solution `ValidateSolutionConfiguration``0 Warning(s)``0 Error(s)`,未输出更具体的 error 文本
- `dotnet build GFramework.Godot.SourceGenerators/GFramework.Godot.SourceGenerators.csproj -c Release`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet build` - `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 列表 1. 提交当前 `GFramework.Godot.SourceGenerators.Tests` 清理批次,并确认提交只包含本 topic 相关文件
2. 以 clean build 输出中的下一个低风险热点作为新切片,优先继续 source generator、测试或单模块可局部验证的问题 2. 如果继续 warning reduction优先重新评估仓库根目录 `dotnet clean` 的 solution-level 失败,再决定是继续从整仓 `dotnet build` 输出挑热点,还是先修复 clean 基线采集问题

View File

@ -1,5 +1,36 @@
# Analyzer Warning Reduction 追踪 # 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 ## 2026-04-24 — RP-050
### 阶段clean-build 基线修正与 `GFramework.Godot.SourceGenerators` 切片清零 ### 阶段clean-build 基线修正与 `GFramework.Godot.SourceGenerators` 切片清零