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,29 +6,28 @@ namespace GFramework.Godot.SourceGenerators.Tests.Behavior;
[TestFixture] [TestFixture]
public class AutoSceneGeneratorTests public class AutoSceneGeneratorTests
{ {
[Test] private const string AutoSceneAttributeWithKeyDeclaration = """
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)] [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoSceneAttribute : Attribute public sealed class AutoSceneAttribute : Attribute
{ {
public AutoSceneAttribute(string key) { } public AutoSceneAttribute(string key) { }
} }
} """;
namespace Godot 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 Node { }
public class Node2D : Node { } public class Node2D : Node { }
} """;
private const string SceneBehaviorInfrastructure = """
namespace GFramework.Game.Abstractions.Scene namespace GFramework.Game.Abstractions.Scene
{ {
public interface ISceneBehavior { } public interface ISceneBehavior { }
@ -48,15 +47,20 @@ public class AutoSceneGeneratorTests
} }
} }
} }
""";
namespace TestApp [Test]
public async Task Generates_Scene_Behavior_Boilerplate()
{ {
string source = CreateAutoSceneSource(
AutoSceneAttributeWithKeyDeclaration,
"""
[AutoScene("Gameplay")] [AutoScene("Gameplay")]
public partial class GameplayRoot : Node2D public partial class GameplayRoot : Node2D
{ {
} }
} """,
"""; includeBehaviorInfrastructure: true);
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;
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|}] [{|#0:AutoScene|}]
public partial class GameplayRoot : Node2D public partial class GameplayRoot : Node2D
{ {
} }
} """);
""";
var test = new CSharpSourceGeneratorTest<AutoSceneGenerator, DefaultVerifier> var test = new CSharpSourceGeneratorTest<AutoSceneGenerator, DefaultVerifier>
{ {
@ -128,55 +112,15 @@ 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;
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>(T owner, string key)
where T : Node
{
return null!;
}
}
}
namespace TestApp
{
[AutoScene("Gameplay")] [AutoScene("Gameplay")]
public partial class GameplayRoot<TReference, TNotNull, TValue, TUnmanaged> : Node2D public partial class GameplayRoot<TReference, TNotNull, TValue, TUnmanaged> : Node2D
where TReference : class? where TReference : class?
@ -185,8 +129,9 @@ public class AutoSceneGeneratorTests
where TUnmanaged : unmanaged where TUnmanaged : unmanaged
{ {
} }
} """,
"""; includeBehaviorInfrastructure: true,
nullableEnabled: true);
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,30 +6,29 @@ namespace GFramework.Godot.SourceGenerators.Tests.Behavior;
[TestFixture] [TestFixture]
public class AutoUiPageGeneratorTests public class AutoUiPageGeneratorTests
{ {
[Test] private const string AutoUiPageAttributeWithLayerDeclaration = """
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)] [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoUiPageAttribute : Attribute public sealed class AutoUiPageAttribute : Attribute
{ {
public AutoUiPageAttribute(string key, string layerName) { } public AutoUiPageAttribute(string key, string layerName) { }
} }
} """;
namespace Godot 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 Node { }
public class CanvasItem : Node { } public class CanvasItem : Node { }
public class Control : CanvasItem { } public class Control : CanvasItem { }
} """;
private const string UiLayerFullEnum = """
namespace GFramework.Game.Abstractions.Enums namespace GFramework.Game.Abstractions.Enums
{ {
public enum UiLayer public enum UiLayer
@ -39,7 +38,19 @@ public class AutoUiPageGeneratorTests
Modal Modal
} }
} }
""";
private const string UiLayerPageOnlyEnum = """
namespace GFramework.Game.Abstractions.Enums
{
public enum UiLayer
{
Page
}
}
""";
private const string UiBehaviorInfrastructure = """
namespace GFramework.Game.Abstractions.UI namespace GFramework.Game.Abstractions.UI
{ {
public interface IUiPageBehavior { } public interface IUiPageBehavior { }
@ -60,15 +71,20 @@ public class AutoUiPageGeneratorTests
} }
} }
} }
""";
namespace TestApp [Test]
public async Task Generates_Ui_Page_Behavior_Boilerplate()
{ {
string source = CreateAutoUiPageSource(
AutoUiPageAttributeWithLayerDeclaration,
UiLayerFullEnum,
"""
[AutoUiPage("MainMenu", "Page")] [AutoUiPage("MainMenu", "Page")]
public partial class MainMenu : Control 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; """
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>(T owner, string key, UiLayer layer)
where T : CanvasItem
{
return null!;
}
}
}
namespace TestApp
{
[{|#0:AutoUiPage("MainMenu")|}] [{|#0:AutoUiPage("MainMenu")|}]
public partial class MainMenu : Control public partial class MainMenu : Control
{ {
} }
} """);
""";
var test = new CSharpSourceGeneratorTest<AutoUiPageGenerator, DefaultVerifier> var test = new CSharpSourceGeneratorTest<AutoUiPageGenerator, DefaultVerifier>
{ {
@ -174,65 +141,16 @@ 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;
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>(T owner, string key, UiLayer layer)
where T : CanvasItem
{
return null!;
}
}
}
namespace TestApp
{
[AutoUiPage("MainMenu", "Page")] [AutoUiPage("MainMenu", "Page")]
public partial class MainMenu<TReference, TNotNull, TUnmanaged> : Control public partial class MainMenu<TReference, TNotNull, TUnmanaged> : Control
where TReference : class? where TReference : class?
@ -240,8 +158,8 @@ public class AutoUiPageGeneratorTests
where TUnmanaged : unmanaged where TUnmanaged : unmanaged
{ {
} }
} """,
"""; nullableEnabled: true);
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,19 +8,7 @@ namespace GFramework.Godot.SourceGenerators.Tests.BindNodeSignal;
[TestFixture] [TestFixture]
public class BindNodeSignalGeneratorTests public class BindNodeSignalGeneratorTests
{ {
/// <summary> private const string BindNodeSignalAttributeDeclaration = """
/// 验证生成器会为已有生命周期调用生成成对的绑定与解绑方法。
/// </summary>
[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;
namespace GFramework.Godot.SourceGenerators.Abstractions
{
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public sealed class BindNodeSignalAttribute : Attribute public sealed class BindNodeSignalAttribute : Attribute
{ {
@ -34,17 +22,31 @@ public class BindNodeSignalGeneratorTests
public string SignalName { get; } public string SignalName { get; }
} }
} """;
namespace Godot 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 class Node
{ {
public virtual void _Ready() {} public virtual void _Ready() {}
public virtual void _ExitTree() {} public virtual void _ExitTree() {}
} }
""";
private const string ButtonType = """
public class Button : Node public class Button : Node
{ {
public event Action? Pressed public event Action? Pressed
@ -53,7 +55,9 @@ public class BindNodeSignalGeneratorTests
remove {} remove {}
} }
} }
""";
private const string SpinBoxType = """
public class SpinBox : Node public class SpinBox : Node
{ {
public delegate void ValueChangedEventHandler(double value); public delegate void ValueChangedEventHandler(double value);
@ -64,12 +68,17 @@ public class BindNodeSignalGeneratorTests
remove {} remove {}
} }
} }
} """;
namespace TestApp /// <summary>
{ /// 验证生成器会为已有生命周期调用生成成对的绑定与解绑方法。
public partial class Hud : Node /// </summary>
[Test]
public async Task Generates_Bind_And_Unbind_Methods_For_Existing_Lifecycle_Hooks()
{ {
string source = CreateHudSource(
CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
"""
private Button _startButton = null!; private Button _startButton = null!;
private SpinBox _startOreSpinBox = null!; private SpinBox _startOreSpinBox = null!;
@ -92,9 +101,10 @@ public class BindNodeSignalGeneratorTests
{ {
__UnbindNodeSignals_Generated(); __UnbindNodeSignals_Generated();
} }
} """,
} LifecycleNodeType,
"""; ButtonType,
SpinBoxType);
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,56 +140,9 @@ 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;
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;
}
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] [GetNode]
private Button _startButton = null!; private Button _startButton = null!;
@ -191,9 +154,9 @@ public class BindNodeSignalGeneratorTests
private void OnAnyButtonPressed() private void OnAnyButtonPressed()
{ {
} }
} """,
} LifecycleNodeType,
"""; ButtonType);
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;
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;
}
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!; private Button _startButton = null!;
[{|#0:BindNodeSignal(nameof(_startButton), "Released")|}] [{|#0:BindNodeSignal(nameof(_startButton), "Released")|}]
private void OnStartButtonPressed() private void OnStartButtonPressed()
{ {
} }
} """,
} EmptyNodeType,
"""; ButtonType);
var test = new CSharpSourceGeneratorTest<BindNodeSignalGenerator, DefaultVerifier> await VerifyDiagnosticsAsync(
{ source,
TestState = new DiagnosticResult("GF_Godot_BindNodeSignal_006", DiagnosticSeverity.Error)
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" },
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
};
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_006", DiagnosticSeverity.Error)
.WithLocation(0) .WithLocation(0)
.WithArguments("_startButton", "Released")); .WithArguments("_startButton", "Released")).ConfigureAwait(false);
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;
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;
}
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!; private SpinBox _startOreSpinBox = null!;
[{|#0:BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))|}] [{|#0:BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))|}]
private void OnStartOreValueChanged() private void OnStartOreValueChanged()
{ {
} }
} """,
} EmptyNodeType,
"""; SpinBoxType);
var test = new CSharpSourceGeneratorTest<BindNodeSignalGenerator, DefaultVerifier> await VerifyDiagnosticsAsync(
{ source,
TestState = new DiagnosticResult("GF_Godot_BindNodeSignal_007", DiagnosticSeverity.Error)
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" },
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
};
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_007", DiagnosticSeverity.Error)
.WithLocation(0) .WithLocation(0)
.WithArguments("OnStartOreValueChanged", "ValueChanged", "_startOreSpinBox")); .WithArguments("OnStartOreValueChanged", "ValueChanged", "_startOreSpinBox")).ConfigureAwait(false);
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;
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;
}
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!; private Button _startButton = null!;
[{|#0:BindNodeSignal(nameof(_startButton), "")|}] [{|#0:BindNodeSignal(nameof(_startButton), "")|}]
private void OnStartButtonPressed() private void OnStartButtonPressed()
{ {
} }
} """,
} EmptyNodeType,
"""; ButtonType);
var test = new CSharpSourceGeneratorTest<BindNodeSignalGenerator, DefaultVerifier> await VerifyDiagnosticsAsync(
{ source,
TestState = new DiagnosticResult("GF_Godot_BindNodeSignal_010", DiagnosticSeverity.Error)
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" },
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
};
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_010", DiagnosticSeverity.Error)
.WithLocation(0) .WithLocation(0)
.WithArguments("OnStartButtonPressed", "signalName")); .WithArguments("OnStartButtonPressed", "signalName")).ConfigureAwait(false);
await test.RunAsync();
} }
/// <summary> /// <summary>
@ -456,48 +270,9 @@ 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;
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;
}
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!; private Button _startButton = null!;
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
@ -512,29 +287,18 @@ public class BindNodeSignalGeneratorTests
private void {|#1:__UnbindNodeSignals_Generated|}() private void {|#1:__UnbindNodeSignals_Generated|}()
{ {
} }
} """,
} EmptyNodeType,
"""; ButtonType);
var test = new CSharpSourceGeneratorTest<BindNodeSignalGenerator, DefaultVerifier> await VerifyDiagnosticsAsync(
{ source,
TestState = new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error)
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" },
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
};
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error)
.WithLocation(0) .WithLocation(0)
.WithArguments("Hud", "__BindNodeSignals_Generated")); .WithArguments("Hud", "__BindNodeSignals_Generated"),
new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error)
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error)
.WithLocation(1) .WithLocation(1)
.WithArguments("Hud", "__UnbindNodeSignals_Generated")); .WithArguments("Hud", "__UnbindNodeSignals_Generated")).ConfigureAwait(false);
await test.RunAsync();
} }
/// <summary> /// <summary>
@ -543,51 +307,9 @@ 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;
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;
}
public string NodeFieldName { get; }
public string SignalName { get; }
}
}
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
{
private Button _startButton = null!; private Button _startButton = null!;
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
@ -602,10 +324,63 @@ public class BindNodeSignalGeneratorTests
public override void {|#1:_ExitTree|}() public override void {|#1:_ExitTree|}()
{ {
} }
""",
LifecycleNodeType,
ButtonType);
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);
}
private static string CreateAbstractionsSource(params string[] attributeDeclarations)
{
string declarations = string.Join($"{Environment.NewLine}{Environment.NewLine}", attributeDeclarations);
return $$"""
namespace GFramework.Godot.SourceGenerators.Abstractions
{
{{declarations}}
}
""";
}
private static string CreateHudSource(
string abstractionsSource,
string hudMembers,
params string[] godotTypes)
{
string godotSource = string.Join($"{Environment.NewLine}{Environment.NewLine}", godotTypes);
return $$"""
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
{{abstractionsSource}}
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,16 +5,7 @@ namespace GFramework.Godot.SourceGenerators.Tests.GetNode;
[TestFixture] [TestFixture]
public class GetNodeGeneratorTests public class GetNodeGeneratorTests
{ {
[Test] private const string FullGetNodeAttributeDeclaration = """
public async Task Generates_InferredUniqueNameBindings_And_ReadyHook_WhenReadyIsMissing()
{
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions
{
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class GetNodeAttribute : Attribute public sealed class GetNodeAttribute : Attribute
{ {
@ -32,24 +23,60 @@ public class GetNodeGeneratorTests
RelativePath = 2, RelativePath = 2,
AbsolutePath = 3 AbsolutePath = 3
} }
""";
private const string MinimalGetNodeAttributeDeclaration = """
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class GetNodeAttribute : Attribute
{
public GetNodeAttribute() {}
} }
namespace Godot 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 class Node
{ {
public virtual void _Ready() {} public virtual void _Ready() {}
public T GetNode<T>(string path) where T : Node => throw new InvalidOperationException(path); public T GetNode<T>(string path) where T : Node => throw new InvalidOperationException(path);
public T? GetNodeOrNull<T>(string path) where T : Node => default; public T? GetNodeOrNull<T>(string path) where T : Node => default;
} }
""";
private const string HBoxContainerType = """
public class HBoxContainer : Node public class HBoxContainer : Node
{ {
} }
} """;
namespace TestApp [Test]
public async Task Generates_InferredUniqueNameBindings_And_ReadyHook_WhenReadyIsMissing()
{ {
string source = CreateGetNodeSource(
FullGetNodeAttributeDeclaration,
"""
public partial class TopBar : HBoxContainer public partial class TopBar : HBoxContainer
{ {
[GetNode] [GetNode]
@ -58,8 +85,8 @@ public class GetNodeGeneratorTests
[GetNode] [GetNode]
private HBoxContainer m_rightContainer = null!; private HBoxContainer m_rightContainer = null!;
} }
} """,
"""; HBoxContainerType);
const string expected = """ const string expected = """
// <auto-generated /> // <auto-generated />
@ -88,54 +115,15 @@ 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;
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<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 public partial class TopBar : HBoxContainer
{ {
[GetNode("%LeftContainer")] [GetNode("%LeftContainer")]
@ -149,8 +137,8 @@ public class GetNodeGeneratorTests
__InjectGetNodes_Generated(); __InjectGetNodes_Generated();
} }
} }
} """,
"""; HBoxContainerType);
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,47 +222,15 @@ 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;
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<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 public partial class TopBar : HBoxContainer
{ {
[GetNode] [GetNode]
@ -284,8 +240,8 @@ public class GetNodeGeneratorTests
{ {
} }
} }
} """,
"""; HBoxContainerType);
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,32 +8,13 @@ namespace GFramework.Godot.SourceGenerators.Tests.Project;
[TestFixture] [TestFixture]
public class GodotProjectMetadataGeneratorTests public class GodotProjectMetadataGeneratorTests
{ {
/// <summary> private const string AutoLoadProjectFile = """
/// 验证会根据 AutoLoad 与 Input Action 生成稳定的强类型入口。
/// </summary>
[Test]
public void Run_Should_Generate_AutoLoads_And_InputActions()
{
var source = CreateSource(
"""
namespace TestApp
{
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
[AutoLoad("GameServices")]
public partial class GameServices : Node
{
}
}
""",
includeAutoLoadAttribute: true);
const string projectFile = """
[autoload] [autoload]
GameServices="*res://autoload/game_services.tscn" GameServices="*res://autoload/game_services.tscn"
AudioBus="*res://autoload/audio_bus.gd" AudioBus="*res://autoload/audio_bus.gd"
""";
private const string InputActionsProjectFile = """
[input] [input]
move_up={ move_up={
"deadzone": 0.5 "deadzone": 0.5
@ -43,7 +24,7 @@ public class GodotProjectMetadataGeneratorTests
} }
"""; """;
const string expectedAutoLoads = """ private const string ExpectedAutoLoads = """
// <auto-generated /> // <auto-generated />
#nullable enable #nullable enable
@ -127,7 +108,7 @@ public class GodotProjectMetadataGeneratorTests
"""; """;
const string expectedInputActions = """ private const string ExpectedInputActions = """
// <auto-generated /> // <auto-generated />
#nullable enable #nullable enable
@ -152,19 +133,40 @@ public class GodotProjectMetadataGeneratorTests
"""; """;
/// <summary>
/// 验证会根据 AutoLoad 与 Input Action 生成稳定的强类型入口。
/// </summary>
[Test]
public void Run_Should_Generate_AutoLoads_And_InputActions()
{
var source = CreateSource(
"""
namespace TestApp
{
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
[AutoLoad("GameServices")]
public partial class GameServices : Node
{
}
}
""",
includeAutoLoadAttribute: true);
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,17 +6,7 @@ namespace GFramework.Godot.SourceGenerators.Tests.Registration;
[TestFixture] [TestFixture]
public class AutoRegisterExportedCollectionsGeneratorTests public class AutoRegisterExportedCollectionsGeneratorTests
{ {
[Test] private const string StandardAttributeDeclarations = """
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;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
@ -25,10 +15,24 @@ public class AutoRegisterExportedCollectionsGeneratorTests
{ {
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { } public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
} }
} """;
namespace TestApp 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()
{
string source = CreateSource(
"""
public sealed class IntRegistry public sealed class IntRegistry
{ {
public void Register(int value) { } public void Register(int value) { }
@ -46,8 +50,8 @@ public class AutoRegisterExportedCollectionsGeneratorTests
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public List<int>? Values { get; } = new(); public List<int>? Values { get; } = new();
} }
} """,
"""; nullableEnabled: true);
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,26 +141,8 @@ 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;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
}
namespace TestApp
{
public sealed class ArrayRegistry public sealed class ArrayRegistry
{ {
public void Register(int[] value) { } public void Register(int[] value) { }
@ -170,8 +156,8 @@ public class AutoRegisterExportedCollectionsGeneratorTests
[RegisterExportedCollection(nameof(_registry), nameof(ArrayRegistry.Register))] [RegisterExportedCollection(nameof(_registry), nameof(ArrayRegistry.Register))]
public List<int[]> Values { get; } = new(); public List<int[]> Values { get; } = new();
} }
} """,
"""; nullableEnabled: true);
const string expected = """ const string expected = """
// <auto-generated /> // <auto-generated />
@ -197,32 +183,14 @@ 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;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
}
namespace TestApp
{
public interface IKeyValue<TKey, TValue> public interface IKeyValue<TKey, TValue>
{ {
} }
@ -248,8 +216,8 @@ public class AutoRegisterExportedCollectionsGeneratorTests
[RegisterExportedCollection(nameof(_registry), "Registry")] [RegisterExportedCollection(nameof(_registry), "Registry")]
public List<IntConfig>? Values { get; } = new(); public List<IntConfig>? Values { get; } = new();
} }
} """,
"""; nullableEnabled: true);
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,26 +308,8 @@ 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;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
}
namespace TestApp
{
public class BaseRegistry public class BaseRegistry
{ {
public void Register(int value) { } public void Register(int value) { }
@ -377,8 +327,8 @@ public class AutoRegisterExportedCollectionsGeneratorTests
[RegisterExportedCollection(nameof(_registry), nameof(BaseRegistry.Register))] [RegisterExportedCollection(nameof(_registry), nameof(BaseRegistry.Register))]
public List<int>? Values { get; } = new(); public List<int>? Values { get; } = new();
} }
} """,
"""; nullableEnabled: true);
const string expected = """ const string expected = """
// <auto-generated /> // <auto-generated />
@ -404,32 +354,14 @@ 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;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
}
namespace TestApp
{
public sealed class IntRegistry public sealed class IntRegistry
{ {
public void Register(int value) { } public void Register(int value) { }
@ -446,8 +378,8 @@ public class AutoRegisterExportedCollectionsGeneratorTests
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public List<int>? Values { get; } = new(); public List<int>? Values { get; } = new();
} }
} """,
"""; nullableEnabled: true);
const string expected = """ const string expected = """
// <auto-generated /> // <auto-generated />
@ -473,31 +405,14 @@ 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;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
}
namespace TestApp
{
public sealed class IntRegistry public sealed class IntRegistry
{ {
public void Register(int value) { } public void Register(int value) { }
@ -517,30 +432,20 @@ public class AutoRegisterExportedCollectionsGeneratorTests
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public List<int> {|#2:WriteOnlyValues|} { set { } } public List<int> {|#2:WriteOnlyValues|} { set { } }
} }
} """);
""";
var test = new CSharpSourceGeneratorTest<AutoRegisterExportedCollectionsGenerator, DefaultVerifier> await VerifyDiagnosticsAsync(
{ source,
TestState = skipGeneratedSourcesCheck: true,
{ new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" },
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
};
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
.WithLocation(0) .WithLocation(0)
.WithArguments("StaticValues")); .WithArguments("StaticValues"),
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error) new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
.WithLocation(1) .WithLocation(1)
.WithArguments("StaticPropertyValues")); .WithArguments("StaticPropertyValues"),
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error) new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
.WithLocation(2) .WithLocation(2)
.WithArguments("WriteOnlyValues")); .WithArguments("WriteOnlyValues")).ConfigureAwait(false);
await test.RunAsync();
} }
[Test] [Test]
@ -711,26 +616,8 @@ 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;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[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) { }
}
}
namespace TestApp
{
public sealed class IntRegistry public sealed class IntRegistry
{ {
public void Register(int value) { } public void Register(int value) { }
@ -748,8 +635,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public List<int>? Values { get; } = new(); public List<int>? Values { get; } = new();
} }
} """,
"""; nullableEnabled: true,
allowMultipleDeclarations: true);
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` 切片清零