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