mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-12 22:03:30 +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]
|
[TestFixture]
|
||||||
public class AutoSceneGeneratorTests
|
public class AutoSceneGeneratorTests
|
||||||
{
|
{
|
||||||
|
private const string AutoSceneAttributeWithKeyDeclaration = """
|
||||||
|
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||||
|
public sealed class AutoSceneAttribute : Attribute
|
||||||
|
{
|
||||||
|
public AutoSceneAttribute(string key) { }
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string AutoSceneAttributeWithoutKeyDeclaration = """
|
||||||
|
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||||
|
public sealed class AutoSceneAttribute : Attribute
|
||||||
|
{
|
||||||
|
public AutoSceneAttribute() { }
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string NodeTypes = """
|
||||||
|
public class Node { }
|
||||||
|
public class Node2D : Node { }
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string SceneBehaviorInfrastructure = """
|
||||||
|
namespace GFramework.Game.Abstractions.Scene
|
||||||
|
{
|
||||||
|
public interface ISceneBehavior { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace GFramework.Godot.Scene
|
||||||
|
{
|
||||||
|
using GFramework.Game.Abstractions.Scene;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
public static class SceneBehaviorFactory
|
||||||
|
{
|
||||||
|
public static ISceneBehavior Create<T>(T owner, string key)
|
||||||
|
where T : Node
|
||||||
|
{
|
||||||
|
return null!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task Generates_Scene_Behavior_Boilerplate()
|
public async Task Generates_Scene_Behavior_Boilerplate()
|
||||||
{
|
{
|
||||||
const string source = """
|
string source = CreateAutoSceneSource(
|
||||||
using System;
|
AutoSceneAttributeWithKeyDeclaration,
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
"""
|
||||||
using Godot;
|
[AutoScene("Gameplay")]
|
||||||
|
public partial class GameplayRoot : Node2D
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
|
{
|
||||||
{
|
}
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
""",
|
||||||
public sealed class AutoSceneAttribute : Attribute
|
includeBehaviorInfrastructure: true);
|
||||||
{
|
|
||||||
public AutoSceneAttribute(string key) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace Godot
|
|
||||||
{
|
|
||||||
public class Node { }
|
|
||||||
public class Node2D : Node { }
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace GFramework.Game.Abstractions.Scene
|
|
||||||
{
|
|
||||||
public interface ISceneBehavior { }
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace GFramework.Godot.Scene
|
|
||||||
{
|
|
||||||
using GFramework.Game.Abstractions.Scene;
|
|
||||||
using Godot;
|
|
||||||
|
|
||||||
public static class SceneBehaviorFactory
|
|
||||||
{
|
|
||||||
public static ISceneBehavior Create<T>(T owner, string key)
|
|
||||||
where T : Node
|
|
||||||
{
|
|
||||||
return null!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
[AutoScene("Gameplay")]
|
|
||||||
public partial class GameplayRoot : Node2D
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
const string expected = """
|
const string expected = """
|
||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
@ -80,40 +84,20 @@ public class AutoSceneGeneratorTests
|
|||||||
|
|
||||||
await GeneratorTest<AutoSceneGenerator>.RunAsync(
|
await GeneratorTest<AutoSceneGenerator>.RunAsync(
|
||||||
source,
|
source,
|
||||||
("TestApp_GameplayRoot.AutoScene.g.cs", expected));
|
("TestApp_GameplayRoot.AutoScene.g.cs", expected)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task Reports_Diagnostic_When_AutoScene_Arguments_Are_Invalid()
|
public async Task Reports_Diagnostic_When_AutoScene_Arguments_Are_Invalid()
|
||||||
{
|
{
|
||||||
const string source = """
|
string source = CreateAutoSceneSource(
|
||||||
using System;
|
AutoSceneAttributeWithoutKeyDeclaration,
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
"""
|
||||||
using Godot;
|
[{|#0:AutoScene|}]
|
||||||
|
public partial class GameplayRoot : Node2D
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
|
{
|
||||||
{
|
}
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
""");
|
||||||
public sealed class AutoSceneAttribute : Attribute
|
|
||||||
{
|
|
||||||
public AutoSceneAttribute() { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace Godot
|
|
||||||
{
|
|
||||||
public class Node { }
|
|
||||||
public class Node2D : Node { }
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
[{|#0:AutoScene|}]
|
|
||||||
public partial class GameplayRoot : Node2D
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
var test = new CSharpSourceGeneratorTest<AutoSceneGenerator, DefaultVerifier>
|
var test = new CSharpSourceGeneratorTest<AutoSceneGenerator, DefaultVerifier>
|
||||||
{
|
{
|
||||||
@ -128,65 +112,26 @@ public class AutoSceneGeneratorTests
|
|||||||
.WithLocation(0)
|
.WithLocation(0)
|
||||||
.WithArguments("AutoSceneAttribute", "GameplayRoot", "a single string scene key argument"));
|
.WithArguments("AutoSceneAttribute", "GameplayRoot", "a single string scene key argument"));
|
||||||
|
|
||||||
await test.RunAsync();
|
await test.RunAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task Generates_Type_Constraints_For_Nullable_Reference_NotNull_And_Unmanaged_Parameters()
|
public async Task Generates_Type_Constraints_For_Nullable_Reference_NotNull_And_Unmanaged_Parameters()
|
||||||
{
|
{
|
||||||
const string source = """
|
string source = CreateAutoSceneSource(
|
||||||
#nullable enable
|
AutoSceneAttributeWithKeyDeclaration,
|
||||||
using System;
|
"""
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
[AutoScene("Gameplay")]
|
||||||
using Godot;
|
public partial class GameplayRoot<TReference, TNotNull, TValue, TUnmanaged> : Node2D
|
||||||
|
where TReference : class?
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
|
where TNotNull : notnull
|
||||||
{
|
where TValue : struct
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
where TUnmanaged : unmanaged
|
||||||
public sealed class AutoSceneAttribute : Attribute
|
{
|
||||||
{
|
}
|
||||||
public AutoSceneAttribute(string key) { }
|
""",
|
||||||
}
|
includeBehaviorInfrastructure: true,
|
||||||
}
|
nullableEnabled: true);
|
||||||
|
|
||||||
namespace Godot
|
|
||||||
{
|
|
||||||
public class Node { }
|
|
||||||
public class Node2D : Node { }
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace GFramework.Game.Abstractions.Scene
|
|
||||||
{
|
|
||||||
public interface ISceneBehavior { }
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace GFramework.Godot.Scene
|
|
||||||
{
|
|
||||||
using GFramework.Game.Abstractions.Scene;
|
|
||||||
using Godot;
|
|
||||||
|
|
||||||
public static class SceneBehaviorFactory
|
|
||||||
{
|
|
||||||
public static ISceneBehavior Create<T>(T owner, string key)
|
|
||||||
where T : Node
|
|
||||||
{
|
|
||||||
return null!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
[AutoScene("Gameplay")]
|
|
||||||
public partial class GameplayRoot<TReference, TNotNull, TValue, TUnmanaged> : Node2D
|
|
||||||
where TReference : class?
|
|
||||||
where TNotNull : notnull
|
|
||||||
where TValue : struct
|
|
||||||
where TUnmanaged : unmanaged
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
const string expected = """
|
const string expected = """
|
||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
@ -214,7 +159,7 @@ public class AutoSceneGeneratorTests
|
|||||||
|
|
||||||
await GeneratorTest<AutoSceneGenerator>.RunAsync(
|
await GeneratorTest<AutoSceneGenerator>.RunAsync(
|
||||||
source,
|
source,
|
||||||
("TestApp_GameplayRoot.AutoScene.g.cs", expected));
|
("TestApp_GameplayRoot.AutoScene.g.cs", expected)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -267,7 +212,7 @@ public class AutoSceneGeneratorTests
|
|||||||
.WithLocation(0)
|
.WithLocation(0)
|
||||||
.WithArguments("GameplayRoot", "SceneKeyStr"));
|
.WithArguments("GameplayRoot", "SceneKeyStr"));
|
||||||
|
|
||||||
await test.RunAsync();
|
await test.RunAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -326,6 +271,39 @@ public class AutoSceneGeneratorTests
|
|||||||
.WithLocation(0)
|
.WithLocation(0)
|
||||||
.WithArguments("GameplayRoot", "__autoSceneBehavior_Generated"));
|
.WithArguments("GameplayRoot", "__autoSceneBehavior_Generated"));
|
||||||
|
|
||||||
await test.RunAsync();
|
await test.RunAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateAutoSceneSource(
|
||||||
|
string attributeDeclaration,
|
||||||
|
string testAppSource,
|
||||||
|
bool includeBehaviorInfrastructure = false,
|
||||||
|
bool nullableEnabled = false)
|
||||||
|
{
|
||||||
|
string nullableDirective = nullableEnabled ? "#nullable enable\n" : string.Empty;
|
||||||
|
string infrastructure = includeBehaviorInfrastructure
|
||||||
|
? $"{Environment.NewLine}{Environment.NewLine}{SceneBehaviorInfrastructure}"
|
||||||
|
: string.Empty;
|
||||||
|
|
||||||
|
return $$"""
|
||||||
|
{{nullableDirective}}using System;
|
||||||
|
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
|
||||||
|
{
|
||||||
|
{{attributeDeclaration}}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Godot
|
||||||
|
{
|
||||||
|
{{NodeTypes}}
|
||||||
|
}{{infrastructure}}
|
||||||
|
|
||||||
|
namespace TestApp
|
||||||
|
{
|
||||||
|
{{testAppSource}}
|
||||||
|
}
|
||||||
|
""";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,69 +6,85 @@ namespace GFramework.Godot.SourceGenerators.Tests.Behavior;
|
|||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class AutoUiPageGeneratorTests
|
public class AutoUiPageGeneratorTests
|
||||||
{
|
{
|
||||||
|
private const string AutoUiPageAttributeWithLayerDeclaration = """
|
||||||
|
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||||
|
public sealed class AutoUiPageAttribute : Attribute
|
||||||
|
{
|
||||||
|
public AutoUiPageAttribute(string key, string layerName) { }
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string AutoUiPageAttributeWithoutLayerDeclaration = """
|
||||||
|
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||||
|
public sealed class AutoUiPageAttribute : Attribute
|
||||||
|
{
|
||||||
|
public AutoUiPageAttribute(string key) { }
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string CanvasNodeTypes = """
|
||||||
|
public class Node { }
|
||||||
|
public class CanvasItem : Node { }
|
||||||
|
public class Control : CanvasItem { }
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string UiLayerFullEnum = """
|
||||||
|
namespace GFramework.Game.Abstractions.Enums
|
||||||
|
{
|
||||||
|
public enum UiLayer
|
||||||
|
{
|
||||||
|
Page,
|
||||||
|
Overlay,
|
||||||
|
Modal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string UiLayerPageOnlyEnum = """
|
||||||
|
namespace GFramework.Game.Abstractions.Enums
|
||||||
|
{
|
||||||
|
public enum UiLayer
|
||||||
|
{
|
||||||
|
Page
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string UiBehaviorInfrastructure = """
|
||||||
|
namespace GFramework.Game.Abstractions.UI
|
||||||
|
{
|
||||||
|
public interface IUiPageBehavior { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace GFramework.Godot.UI
|
||||||
|
{
|
||||||
|
using GFramework.Game.Abstractions.Enums;
|
||||||
|
using GFramework.Game.Abstractions.UI;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
public static class UiPageBehaviorFactory
|
||||||
|
{
|
||||||
|
public static IUiPageBehavior Create<T>(T owner, string key, UiLayer layer)
|
||||||
|
where T : CanvasItem
|
||||||
|
{
|
||||||
|
return null!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task Generates_Ui_Page_Behavior_Boilerplate()
|
public async Task Generates_Ui_Page_Behavior_Boilerplate()
|
||||||
{
|
{
|
||||||
const string source = """
|
string source = CreateAutoUiPageSource(
|
||||||
using System;
|
AutoUiPageAttributeWithLayerDeclaration,
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
UiLayerFullEnum,
|
||||||
using Godot;
|
"""
|
||||||
|
[AutoUiPage("MainMenu", "Page")]
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
|
public partial class MainMenu : Control
|
||||||
{
|
{
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
}
|
||||||
public sealed class AutoUiPageAttribute : Attribute
|
""");
|
||||||
{
|
|
||||||
public AutoUiPageAttribute(string key, string layerName) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace Godot
|
|
||||||
{
|
|
||||||
public class Node { }
|
|
||||||
public class CanvasItem : Node { }
|
|
||||||
public class Control : CanvasItem { }
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace GFramework.Game.Abstractions.Enums
|
|
||||||
{
|
|
||||||
public enum UiLayer
|
|
||||||
{
|
|
||||||
Page,
|
|
||||||
Overlay,
|
|
||||||
Modal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace GFramework.Game.Abstractions.UI
|
|
||||||
{
|
|
||||||
public interface IUiPageBehavior { }
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace GFramework.Godot.UI
|
|
||||||
{
|
|
||||||
using GFramework.Game.Abstractions.Enums;
|
|
||||||
using GFramework.Game.Abstractions.UI;
|
|
||||||
using Godot;
|
|
||||||
|
|
||||||
public static class UiPageBehaviorFactory
|
|
||||||
{
|
|
||||||
public static IUiPageBehavior Create<T>(T owner, string key, UiLayer layer)
|
|
||||||
where T : CanvasItem
|
|
||||||
{
|
|
||||||
return null!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
[AutoUiPage("MainMenu", "Page")]
|
|
||||||
public partial class MainMenu : Control
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
const string expected = """
|
const string expected = """
|
||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
@ -92,70 +108,21 @@ public class AutoUiPageGeneratorTests
|
|||||||
|
|
||||||
await GeneratorTest<AutoUiPageGenerator>.RunAsync(
|
await GeneratorTest<AutoUiPageGenerator>.RunAsync(
|
||||||
source,
|
source,
|
||||||
("TestApp_MainMenu.AutoUiPage.g.cs", expected));
|
("TestApp_MainMenu.AutoUiPage.g.cs", expected)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task Reports_Diagnostic_When_AutoUiPage_Attribute_Arguments_Are_Invalid()
|
public async Task Reports_Diagnostic_When_AutoUiPage_Attribute_Arguments_Are_Invalid()
|
||||||
{
|
{
|
||||||
const string source = """
|
string source = CreateAutoUiPageSource(
|
||||||
using System;
|
AutoUiPageAttributeWithoutLayerDeclaration,
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
UiLayerPageOnlyEnum,
|
||||||
using Godot;
|
"""
|
||||||
|
[{|#0:AutoUiPage("MainMenu")|}]
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
|
public partial class MainMenu : Control
|
||||||
{
|
{
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
}
|
||||||
public sealed class AutoUiPageAttribute : Attribute
|
""");
|
||||||
{
|
|
||||||
public AutoUiPageAttribute(string key) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace Godot
|
|
||||||
{
|
|
||||||
public class Node { }
|
|
||||||
public class CanvasItem : Node { }
|
|
||||||
public class Control : CanvasItem { }
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace GFramework.Game.Abstractions.Enums
|
|
||||||
{
|
|
||||||
public enum UiLayer
|
|
||||||
{
|
|
||||||
Page
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace GFramework.Game.Abstractions.UI
|
|
||||||
{
|
|
||||||
public interface IUiPageBehavior { }
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace GFramework.Godot.UI
|
|
||||||
{
|
|
||||||
using GFramework.Game.Abstractions.Enums;
|
|
||||||
using GFramework.Game.Abstractions.UI;
|
|
||||||
using Godot;
|
|
||||||
|
|
||||||
public static class UiPageBehaviorFactory
|
|
||||||
{
|
|
||||||
public static IUiPageBehavior Create<T>(T owner, string key, UiLayer layer)
|
|
||||||
where T : CanvasItem
|
|
||||||
{
|
|
||||||
return null!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
[{|#0:AutoUiPage("MainMenu")|}]
|
|
||||||
public partial class MainMenu : Control
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
var test = new CSharpSourceGeneratorTest<AutoUiPageGenerator, DefaultVerifier>
|
var test = new CSharpSourceGeneratorTest<AutoUiPageGenerator, DefaultVerifier>
|
||||||
{
|
{
|
||||||
@ -174,74 +141,25 @@ public class AutoUiPageGeneratorTests
|
|||||||
"MainMenu",
|
"MainMenu",
|
||||||
"a string key argument and a string UiLayer name argument"));
|
"a string key argument and a string UiLayer name argument"));
|
||||||
|
|
||||||
await test.RunAsync();
|
await test.RunAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task Generates_Type_Constraints_For_ClassNullable_NotNull_And_Unmanaged()
|
public async Task Generates_Type_Constraints_For_ClassNullable_NotNull_And_Unmanaged()
|
||||||
{
|
{
|
||||||
const string source = """
|
string source = CreateAutoUiPageSource(
|
||||||
#nullable enable
|
AutoUiPageAttributeWithLayerDeclaration,
|
||||||
using System;
|
UiLayerPageOnlyEnum,
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
"""
|
||||||
using Godot;
|
[AutoUiPage("MainMenu", "Page")]
|
||||||
|
public partial class MainMenu<TReference, TNotNull, TUnmanaged> : Control
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
|
where TReference : class?
|
||||||
{
|
where TNotNull : notnull
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
where TUnmanaged : unmanaged
|
||||||
public sealed class AutoUiPageAttribute : Attribute
|
{
|
||||||
{
|
}
|
||||||
public AutoUiPageAttribute(string key, string layerName) { }
|
""",
|
||||||
}
|
nullableEnabled: true);
|
||||||
}
|
|
||||||
|
|
||||||
namespace Godot
|
|
||||||
{
|
|
||||||
public class Node { }
|
|
||||||
public class CanvasItem : Node { }
|
|
||||||
public class Control : CanvasItem { }
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace GFramework.Game.Abstractions.Enums
|
|
||||||
{
|
|
||||||
public enum UiLayer
|
|
||||||
{
|
|
||||||
Page
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace GFramework.Game.Abstractions.UI
|
|
||||||
{
|
|
||||||
public interface IUiPageBehavior { }
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace GFramework.Godot.UI
|
|
||||||
{
|
|
||||||
using GFramework.Game.Abstractions.Enums;
|
|
||||||
using GFramework.Game.Abstractions.UI;
|
|
||||||
using Godot;
|
|
||||||
|
|
||||||
public static class UiPageBehaviorFactory
|
|
||||||
{
|
|
||||||
public static IUiPageBehavior Create<T>(T owner, string key, UiLayer layer)
|
|
||||||
where T : CanvasItem
|
|
||||||
{
|
|
||||||
return null!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
[AutoUiPage("MainMenu", "Page")]
|
|
||||||
public partial class MainMenu<TReference, TNotNull, TUnmanaged> : Control
|
|
||||||
where TReference : class?
|
|
||||||
where TNotNull : notnull
|
|
||||||
where TUnmanaged : unmanaged
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
const string expected = """
|
const string expected = """
|
||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
@ -268,6 +186,40 @@ public class AutoUiPageGeneratorTests
|
|||||||
|
|
||||||
await GeneratorTest<AutoUiPageGenerator>.RunAsync(
|
await GeneratorTest<AutoUiPageGenerator>.RunAsync(
|
||||||
source,
|
source,
|
||||||
("TestApp_MainMenu.AutoUiPage.g.cs", expected));
|
("TestApp_MainMenu.AutoUiPage.g.cs", expected)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateAutoUiPageSource(
|
||||||
|
string attributeDeclaration,
|
||||||
|
string uiLayerDeclaration,
|
||||||
|
string testAppSource,
|
||||||
|
bool nullableEnabled = false)
|
||||||
|
{
|
||||||
|
string nullableDirective = nullableEnabled ? "#nullable enable\n" : string.Empty;
|
||||||
|
|
||||||
|
return $$"""
|
||||||
|
{{nullableDirective}}using System;
|
||||||
|
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
|
||||||
|
{
|
||||||
|
{{attributeDeclaration}}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Godot
|
||||||
|
{
|
||||||
|
{{CanvasNodeTypes}}
|
||||||
|
}
|
||||||
|
|
||||||
|
{{uiLayerDeclaration}}
|
||||||
|
|
||||||
|
{{UiBehaviorInfrastructure}}
|
||||||
|
|
||||||
|
namespace TestApp
|
||||||
|
{
|
||||||
|
{{testAppSource}}
|
||||||
|
}
|
||||||
|
""";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,93 +8,103 @@ namespace GFramework.Godot.SourceGenerators.Tests.BindNodeSignal;
|
|||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class BindNodeSignalGeneratorTests
|
public class BindNodeSignalGeneratorTests
|
||||||
{
|
{
|
||||||
|
private const string BindNodeSignalAttributeDeclaration = """
|
||||||
|
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
|
||||||
|
public sealed class BindNodeSignalAttribute : Attribute
|
||||||
|
{
|
||||||
|
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
|
||||||
|
{
|
||||||
|
NodeFieldName = nodeFieldName;
|
||||||
|
SignalName = signalName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string NodeFieldName { get; }
|
||||||
|
|
||||||
|
public string SignalName { get; }
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string GetNodeAttributeDeclaration = """
|
||||||
|
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
|
||||||
|
public sealed class GetNodeAttribute : Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string EmptyNodeType = """
|
||||||
|
public class Node
|
||||||
|
{
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string LifecycleNodeType = """
|
||||||
|
public class Node
|
||||||
|
{
|
||||||
|
public virtual void _Ready() {}
|
||||||
|
|
||||||
|
public virtual void _ExitTree() {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string ButtonType = """
|
||||||
|
public class Button : Node
|
||||||
|
{
|
||||||
|
public event Action? Pressed
|
||||||
|
{
|
||||||
|
add {}
|
||||||
|
remove {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string SpinBoxType = """
|
||||||
|
public class SpinBox : Node
|
||||||
|
{
|
||||||
|
public delegate void ValueChangedEventHandler(double value);
|
||||||
|
|
||||||
|
public event ValueChangedEventHandler? ValueChanged
|
||||||
|
{
|
||||||
|
add {}
|
||||||
|
remove {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证生成器会为已有生命周期调用生成成对的绑定与解绑方法。
|
/// 验证生成器会为已有生命周期调用生成成对的绑定与解绑方法。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Test]
|
[Test]
|
||||||
public async Task Generates_Bind_And_Unbind_Methods_For_Existing_Lifecycle_Hooks()
|
public async Task Generates_Bind_And_Unbind_Methods_For_Existing_Lifecycle_Hooks()
|
||||||
{
|
{
|
||||||
const string source = """
|
string source = CreateHudSource(
|
||||||
using System;
|
CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
"""
|
||||||
using Godot;
|
private Button _startButton = null!;
|
||||||
|
private SpinBox _startOreSpinBox = null!;
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
|
||||||
{
|
private void OnStartButtonPressed()
|
||||||
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
|
{
|
||||||
public sealed class BindNodeSignalAttribute : Attribute
|
}
|
||||||
{
|
|
||||||
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
|
|
||||||
{
|
|
||||||
NodeFieldName = nodeFieldName;
|
|
||||||
SignalName = signalName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string NodeFieldName { get; }
|
[BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))]
|
||||||
|
private void OnStartOreValueChanged(double value)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public string SignalName { get; }
|
public override void _Ready()
|
||||||
}
|
{
|
||||||
}
|
__BindNodeSignals_Generated();
|
||||||
|
}
|
||||||
|
|
||||||
namespace Godot
|
public override void _ExitTree()
|
||||||
{
|
{
|
||||||
public class Node
|
__UnbindNodeSignals_Generated();
|
||||||
{
|
}
|
||||||
public virtual void _Ready() {}
|
""",
|
||||||
|
LifecycleNodeType,
|
||||||
public virtual void _ExitTree() {}
|
ButtonType,
|
||||||
}
|
SpinBoxType);
|
||||||
|
|
||||||
public class Button : Node
|
|
||||||
{
|
|
||||||
public event Action? Pressed
|
|
||||||
{
|
|
||||||
add {}
|
|
||||||
remove {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SpinBox : Node
|
|
||||||
{
|
|
||||||
public delegate void ValueChangedEventHandler(double value);
|
|
||||||
|
|
||||||
public event ValueChangedEventHandler? ValueChanged
|
|
||||||
{
|
|
||||||
add {}
|
|
||||||
remove {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
public partial class Hud : Node
|
|
||||||
{
|
|
||||||
private Button _startButton = null!;
|
|
||||||
private SpinBox _startOreSpinBox = null!;
|
|
||||||
|
|
||||||
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
|
|
||||||
private void OnStartButtonPressed()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
[BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))]
|
|
||||||
private void OnStartOreValueChanged(double value)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void _Ready()
|
|
||||||
{
|
|
||||||
__BindNodeSignals_Generated();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void _ExitTree()
|
|
||||||
{
|
|
||||||
__UnbindNodeSignals_Generated();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
const string expected = """
|
const string expected = """
|
||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
@ -121,7 +131,7 @@ public class BindNodeSignalGeneratorTests
|
|||||||
|
|
||||||
await GeneratorTest<BindNodeSignalGenerator>.RunAsync(
|
await GeneratorTest<BindNodeSignalGenerator>.RunAsync(
|
||||||
source,
|
source,
|
||||||
("TestApp_Hud.BindNodeSignal.g.cs", expected));
|
("TestApp_Hud.BindNodeSignal.g.cs", expected)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -130,70 +140,23 @@ public class BindNodeSignalGeneratorTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task Generates_Multiple_Subscriptions_For_The_Same_Handler_And_Coexists_With_GetNode()
|
public async Task Generates_Multiple_Subscriptions_For_The_Same_Handler_And_Coexists_With_GetNode()
|
||||||
{
|
{
|
||||||
const string source = """
|
string source = CreateHudSource(
|
||||||
using System;
|
CreateAbstractionsSource(BindNodeSignalAttributeDeclaration, GetNodeAttributeDeclaration),
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
"""
|
||||||
using Godot;
|
[GetNode]
|
||||||
|
private Button _startButton = null!;
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
[GetNode]
|
||||||
{
|
private Button _cancelButton = null!;
|
||||||
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
|
|
||||||
public sealed class BindNodeSignalAttribute : Attribute
|
|
||||||
{
|
|
||||||
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
|
|
||||||
{
|
|
||||||
NodeFieldName = nodeFieldName;
|
|
||||||
SignalName = signalName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string NodeFieldName { get; }
|
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
|
||||||
|
[BindNodeSignal(nameof(_cancelButton), nameof(Button.Pressed))]
|
||||||
public string SignalName { get; }
|
private void OnAnyButtonPressed()
|
||||||
}
|
{
|
||||||
|
}
|
||||||
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
|
""",
|
||||||
public sealed class GetNodeAttribute : Attribute
|
LifecycleNodeType,
|
||||||
{
|
ButtonType);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace Godot
|
|
||||||
{
|
|
||||||
public class Node
|
|
||||||
{
|
|
||||||
public virtual void _Ready() {}
|
|
||||||
|
|
||||||
public virtual void _ExitTree() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Button : Node
|
|
||||||
{
|
|
||||||
public event Action? Pressed
|
|
||||||
{
|
|
||||||
add {}
|
|
||||||
remove {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
public partial class Hud : Node
|
|
||||||
{
|
|
||||||
[GetNode]
|
|
||||||
private Button _startButton = null!;
|
|
||||||
|
|
||||||
[GetNode]
|
|
||||||
private Button _cancelButton = null!;
|
|
||||||
|
|
||||||
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
|
|
||||||
[BindNodeSignal(nameof(_cancelButton), nameof(Button.Pressed))]
|
|
||||||
private void OnAnyButtonPressed()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
const string expected = """
|
const string expected = """
|
||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
@ -220,7 +183,7 @@ public class BindNodeSignalGeneratorTests
|
|||||||
|
|
||||||
await GeneratorTest<BindNodeSignalGenerator>.RunAsync(
|
await GeneratorTest<BindNodeSignalGenerator>.RunAsync(
|
||||||
source,
|
source,
|
||||||
("TestApp_Hud.BindNodeSignal.g.cs", expected));
|
("TestApp_Hud.BindNodeSignal.g.cs", expected)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -229,73 +192,24 @@ public class BindNodeSignalGeneratorTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task Reports_Diagnostic_When_Signal_Does_Not_Exist()
|
public async Task Reports_Diagnostic_When_Signal_Does_Not_Exist()
|
||||||
{
|
{
|
||||||
const string source = """
|
string source = CreateHudSource(
|
||||||
using System;
|
CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
"""
|
||||||
using Godot;
|
private Button _startButton = null!;
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
[{|#0:BindNodeSignal(nameof(_startButton), "Released")|}]
|
||||||
{
|
private void OnStartButtonPressed()
|
||||||
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
|
{
|
||||||
public sealed class BindNodeSignalAttribute : Attribute
|
}
|
||||||
{
|
""",
|
||||||
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
|
EmptyNodeType,
|
||||||
{
|
ButtonType);
|
||||||
NodeFieldName = nodeFieldName;
|
|
||||||
SignalName = signalName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string NodeFieldName { get; }
|
await VerifyDiagnosticsAsync(
|
||||||
|
source,
|
||||||
public string SignalName { get; }
|
new DiagnosticResult("GF_Godot_BindNodeSignal_006", DiagnosticSeverity.Error)
|
||||||
}
|
.WithLocation(0)
|
||||||
}
|
.WithArguments("_startButton", "Released")).ConfigureAwait(false);
|
||||||
|
|
||||||
namespace Godot
|
|
||||||
{
|
|
||||||
public class Node
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Button : Node
|
|
||||||
{
|
|
||||||
public event Action? Pressed
|
|
||||||
{
|
|
||||||
add {}
|
|
||||||
remove {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
public partial class Hud : Node
|
|
||||||
{
|
|
||||||
private Button _startButton = null!;
|
|
||||||
|
|
||||||
[{|#0:BindNodeSignal(nameof(_startButton), "Released")|}]
|
|
||||||
private void OnStartButtonPressed()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
var test = new CSharpSourceGeneratorTest<BindNodeSignalGenerator, DefaultVerifier>
|
|
||||||
{
|
|
||||||
TestState =
|
|
||||||
{
|
|
||||||
Sources = { source }
|
|
||||||
},
|
|
||||||
DisabledDiagnostics = { "GF_Common_Trace_001" },
|
|
||||||
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
|
|
||||||
};
|
|
||||||
|
|
||||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_006", DiagnosticSeverity.Error)
|
|
||||||
.WithLocation(0)
|
|
||||||
.WithArguments("_startButton", "Released"));
|
|
||||||
|
|
||||||
await test.RunAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -304,75 +218,24 @@ public class BindNodeSignalGeneratorTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task Reports_Diagnostic_When_Method_Signature_Does_Not_Match_Event()
|
public async Task Reports_Diagnostic_When_Method_Signature_Does_Not_Match_Event()
|
||||||
{
|
{
|
||||||
const string source = """
|
string source = CreateHudSource(
|
||||||
using System;
|
CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
"""
|
||||||
using Godot;
|
private SpinBox _startOreSpinBox = null!;
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
[{|#0:BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))|}]
|
||||||
{
|
private void OnStartOreValueChanged()
|
||||||
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
|
{
|
||||||
public sealed class BindNodeSignalAttribute : Attribute
|
}
|
||||||
{
|
""",
|
||||||
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
|
EmptyNodeType,
|
||||||
{
|
SpinBoxType);
|
||||||
NodeFieldName = nodeFieldName;
|
|
||||||
SignalName = signalName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string NodeFieldName { get; }
|
await VerifyDiagnosticsAsync(
|
||||||
|
source,
|
||||||
public string SignalName { get; }
|
new DiagnosticResult("GF_Godot_BindNodeSignal_007", DiagnosticSeverity.Error)
|
||||||
}
|
.WithLocation(0)
|
||||||
}
|
.WithArguments("OnStartOreValueChanged", "ValueChanged", "_startOreSpinBox")).ConfigureAwait(false);
|
||||||
|
|
||||||
namespace Godot
|
|
||||||
{
|
|
||||||
public class Node
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SpinBox : Node
|
|
||||||
{
|
|
||||||
public delegate void ValueChangedEventHandler(double value);
|
|
||||||
|
|
||||||
public event ValueChangedEventHandler? ValueChanged
|
|
||||||
{
|
|
||||||
add {}
|
|
||||||
remove {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
public partial class Hud : Node
|
|
||||||
{
|
|
||||||
private SpinBox _startOreSpinBox = null!;
|
|
||||||
|
|
||||||
[{|#0:BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))|}]
|
|
||||||
private void OnStartOreValueChanged()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
var test = new CSharpSourceGeneratorTest<BindNodeSignalGenerator, DefaultVerifier>
|
|
||||||
{
|
|
||||||
TestState =
|
|
||||||
{
|
|
||||||
Sources = { source }
|
|
||||||
},
|
|
||||||
DisabledDiagnostics = { "GF_Common_Trace_001" },
|
|
||||||
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
|
|
||||||
};
|
|
||||||
|
|
||||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_007", DiagnosticSeverity.Error)
|
|
||||||
.WithLocation(0)
|
|
||||||
.WithArguments("OnStartOreValueChanged", "ValueChanged", "_startOreSpinBox"));
|
|
||||||
|
|
||||||
await test.RunAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -381,73 +244,24 @@ public class BindNodeSignalGeneratorTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task Reports_Diagnostic_When_Constructor_Argument_Is_Empty()
|
public async Task Reports_Diagnostic_When_Constructor_Argument_Is_Empty()
|
||||||
{
|
{
|
||||||
const string source = """
|
string source = CreateHudSource(
|
||||||
using System;
|
CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
"""
|
||||||
using Godot;
|
private Button _startButton = null!;
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
[{|#0:BindNodeSignal(nameof(_startButton), "")|}]
|
||||||
{
|
private void OnStartButtonPressed()
|
||||||
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
|
{
|
||||||
public sealed class BindNodeSignalAttribute : Attribute
|
}
|
||||||
{
|
""",
|
||||||
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
|
EmptyNodeType,
|
||||||
{
|
ButtonType);
|
||||||
NodeFieldName = nodeFieldName;
|
|
||||||
SignalName = signalName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string NodeFieldName { get; }
|
await VerifyDiagnosticsAsync(
|
||||||
|
source,
|
||||||
public string SignalName { get; }
|
new DiagnosticResult("GF_Godot_BindNodeSignal_010", DiagnosticSeverity.Error)
|
||||||
}
|
.WithLocation(0)
|
||||||
}
|
.WithArguments("OnStartButtonPressed", "signalName")).ConfigureAwait(false);
|
||||||
|
|
||||||
namespace Godot
|
|
||||||
{
|
|
||||||
public class Node
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Button : Node
|
|
||||||
{
|
|
||||||
public event Action? Pressed
|
|
||||||
{
|
|
||||||
add {}
|
|
||||||
remove {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
public partial class Hud : Node
|
|
||||||
{
|
|
||||||
private Button _startButton = null!;
|
|
||||||
|
|
||||||
[{|#0:BindNodeSignal(nameof(_startButton), "")|}]
|
|
||||||
private void OnStartButtonPressed()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
var test = new CSharpSourceGeneratorTest<BindNodeSignalGenerator, DefaultVerifier>
|
|
||||||
{
|
|
||||||
TestState =
|
|
||||||
{
|
|
||||||
Sources = { source }
|
|
||||||
},
|
|
||||||
DisabledDiagnostics = { "GF_Common_Trace_001" },
|
|
||||||
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
|
|
||||||
};
|
|
||||||
|
|
||||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_010", DiagnosticSeverity.Error)
|
|
||||||
.WithLocation(0)
|
|
||||||
.WithArguments("OnStartButtonPressed", "signalName"));
|
|
||||||
|
|
||||||
await test.RunAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -456,85 +270,35 @@ public class BindNodeSignalGeneratorTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task Reports_Diagnostic_When_Generated_Method_Names_Already_Exist()
|
public async Task Reports_Diagnostic_When_Generated_Method_Names_Already_Exist()
|
||||||
{
|
{
|
||||||
const string source = """
|
string source = CreateHudSource(
|
||||||
using System;
|
CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
"""
|
||||||
using Godot;
|
private Button _startButton = null!;
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
|
||||||
{
|
private void OnStartButtonPressed()
|
||||||
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
|
{
|
||||||
public sealed class BindNodeSignalAttribute : Attribute
|
}
|
||||||
{
|
|
||||||
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
|
|
||||||
{
|
|
||||||
NodeFieldName = nodeFieldName;
|
|
||||||
SignalName = signalName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string NodeFieldName { get; }
|
private void {|#0:__BindNodeSignals_Generated|}()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public string SignalName { get; }
|
private void {|#1:__UnbindNodeSignals_Generated|}()
|
||||||
}
|
{
|
||||||
}
|
}
|
||||||
|
""",
|
||||||
|
EmptyNodeType,
|
||||||
|
ButtonType);
|
||||||
|
|
||||||
namespace Godot
|
await VerifyDiagnosticsAsync(
|
||||||
{
|
source,
|
||||||
public class Node
|
new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error)
|
||||||
{
|
.WithLocation(0)
|
||||||
}
|
.WithArguments("Hud", "__BindNodeSignals_Generated"),
|
||||||
|
new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error)
|
||||||
public class Button : Node
|
.WithLocation(1)
|
||||||
{
|
.WithArguments("Hud", "__UnbindNodeSignals_Generated")).ConfigureAwait(false);
|
||||||
public event Action? Pressed
|
|
||||||
{
|
|
||||||
add {}
|
|
||||||
remove {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
public partial class Hud : Node
|
|
||||||
{
|
|
||||||
private Button _startButton = null!;
|
|
||||||
|
|
||||||
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
|
|
||||||
private void OnStartButtonPressed()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
private void {|#0:__BindNodeSignals_Generated|}()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
private void {|#1:__UnbindNodeSignals_Generated|}()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
var test = new CSharpSourceGeneratorTest<BindNodeSignalGenerator, DefaultVerifier>
|
|
||||||
{
|
|
||||||
TestState =
|
|
||||||
{
|
|
||||||
Sources = { source }
|
|
||||||
},
|
|
||||||
DisabledDiagnostics = { "GF_Common_Trace_001" },
|
|
||||||
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
|
|
||||||
};
|
|
||||||
|
|
||||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error)
|
|
||||||
.WithLocation(0)
|
|
||||||
.WithArguments("Hud", "__BindNodeSignals_Generated"));
|
|
||||||
|
|
||||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error)
|
|
||||||
.WithLocation(1)
|
|
||||||
.WithArguments("Hud", "__UnbindNodeSignals_Generated"));
|
|
||||||
|
|
||||||
await test.RunAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -543,69 +307,80 @@ public class BindNodeSignalGeneratorTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task Reports_Warnings_When_Lifecycle_Methods_Do_Not_Call_Generated_Methods()
|
public async Task Reports_Warnings_When_Lifecycle_Methods_Do_Not_Call_Generated_Methods()
|
||||||
{
|
{
|
||||||
const string source = """
|
string source = CreateHudSource(
|
||||||
using System;
|
CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
"""
|
||||||
using Godot;
|
private Button _startButton = null!;
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
|
||||||
{
|
private void OnStartButtonPressed()
|
||||||
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
|
{
|
||||||
public sealed class BindNodeSignalAttribute : Attribute
|
}
|
||||||
{
|
|
||||||
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
|
|
||||||
{
|
|
||||||
NodeFieldName = nodeFieldName;
|
|
||||||
SignalName = signalName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string NodeFieldName { get; }
|
public override void {|#0:_Ready|}()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public string SignalName { get; }
|
public override void {|#1:_ExitTree|}()
|
||||||
}
|
{
|
||||||
}
|
}
|
||||||
|
""",
|
||||||
|
LifecycleNodeType,
|
||||||
|
ButtonType);
|
||||||
|
|
||||||
namespace Godot
|
await VerifyDiagnosticsAsync(
|
||||||
{
|
source,
|
||||||
public class Node
|
new DiagnosticResult("GF_Godot_BindNodeSignal_008", DiagnosticSeverity.Warning)
|
||||||
{
|
.WithLocation(0)
|
||||||
public virtual void _Ready() {}
|
.WithArguments("Hud"),
|
||||||
|
new DiagnosticResult("GF_Godot_BindNodeSignal_009", DiagnosticSeverity.Warning)
|
||||||
|
.WithLocation(1)
|
||||||
|
.WithArguments("Hud")).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
public virtual void _ExitTree() {}
|
private static string CreateAbstractionsSource(params string[] attributeDeclarations)
|
||||||
}
|
{
|
||||||
|
string declarations = string.Join($"{Environment.NewLine}{Environment.NewLine}", attributeDeclarations);
|
||||||
|
|
||||||
public class Button : Node
|
return $$"""
|
||||||
{
|
namespace GFramework.Godot.SourceGenerators.Abstractions
|
||||||
public event Action? Pressed
|
{
|
||||||
{
|
{{declarations}}
|
||||||
add {}
|
}
|
||||||
remove {}
|
""";
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
private static string CreateHudSource(
|
||||||
{
|
string abstractionsSource,
|
||||||
public partial class Hud : Node
|
string hudMembers,
|
||||||
{
|
params string[] godotTypes)
|
||||||
private Button _startButton = null!;
|
{
|
||||||
|
string godotSource = string.Join($"{Environment.NewLine}{Environment.NewLine}", godotTypes);
|
||||||
|
|
||||||
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
|
return $$"""
|
||||||
private void OnStartButtonPressed()
|
using System;
|
||||||
{
|
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||||
}
|
using Godot;
|
||||||
|
|
||||||
public override void {|#0:_Ready|}()
|
{{abstractionsSource}}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void {|#1:_ExitTree|}()
|
namespace Godot
|
||||||
{
|
{
|
||||||
}
|
{{godotSource}}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
|
namespace TestApp
|
||||||
|
{
|
||||||
|
public partial class Hud : Node
|
||||||
|
{
|
||||||
|
{{hudMembers}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Task VerifyDiagnosticsAsync(string source, params DiagnosticResult[] expectedDiagnostics)
|
||||||
|
{
|
||||||
var test = new CSharpSourceGeneratorTest<BindNodeSignalGenerator, DefaultVerifier>
|
var test = new CSharpSourceGeneratorTest<BindNodeSignalGenerator, DefaultVerifier>
|
||||||
{
|
{
|
||||||
TestState =
|
TestState =
|
||||||
@ -616,14 +391,11 @@ public class BindNodeSignalGeneratorTests
|
|||||||
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
|
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
|
||||||
};
|
};
|
||||||
|
|
||||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_008", DiagnosticSeverity.Warning)
|
foreach (DiagnosticResult expectedDiagnostic in expectedDiagnostics)
|
||||||
.WithLocation(0)
|
{
|
||||||
.WithArguments("Hud"));
|
test.ExpectedDiagnostics.Add(expectedDiagnostic);
|
||||||
|
}
|
||||||
|
|
||||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_009", DiagnosticSeverity.Warning)
|
return test.RunAsync();
|
||||||
.WithLocation(1)
|
|
||||||
.WithArguments("Hud"));
|
|
||||||
|
|
||||||
await test.RunAsync();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
@ -44,4 +44,4 @@ public static class GeneratorTest<TGenerator>
|
|||||||
.Replace("\r", "\n", StringComparison.Ordinal)
|
.Replace("\r", "\n", StringComparison.Ordinal)
|
||||||
.Replace("\n", Environment.NewLine, StringComparison.Ordinal);
|
.Replace("\n", Environment.NewLine, StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,61 +5,88 @@ namespace GFramework.Godot.SourceGenerators.Tests.GetNode;
|
|||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class GetNodeGeneratorTests
|
public class GetNodeGeneratorTests
|
||||||
{
|
{
|
||||||
|
private const string FullGetNodeAttributeDeclaration = """
|
||||||
|
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
|
||||||
|
public sealed class GetNodeAttribute : Attribute
|
||||||
|
{
|
||||||
|
public GetNodeAttribute() {}
|
||||||
|
public GetNodeAttribute(string path) { Path = path; }
|
||||||
|
public string? Path { get; set; }
|
||||||
|
public bool Required { get; set; } = true;
|
||||||
|
public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum NodeLookupMode
|
||||||
|
{
|
||||||
|
Auto = 0,
|
||||||
|
UniqueName = 1,
|
||||||
|
RelativePath = 2,
|
||||||
|
AbsolutePath = 3
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string MinimalGetNodeAttributeDeclaration = """
|
||||||
|
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
|
||||||
|
public sealed class GetNodeAttribute : Attribute
|
||||||
|
{
|
||||||
|
public GetNodeAttribute() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum NodeLookupMode
|
||||||
|
{
|
||||||
|
Auto = 0
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string PropertyOnlyGetNodeAttributeDeclaration = """
|
||||||
|
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
|
||||||
|
public sealed class GetNodeAttribute : Attribute
|
||||||
|
{
|
||||||
|
public string? Path { get; set; }
|
||||||
|
public bool Required { get; set; } = true;
|
||||||
|
public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum NodeLookupMode
|
||||||
|
{
|
||||||
|
Auto = 0,
|
||||||
|
UniqueName = 1,
|
||||||
|
RelativePath = 2,
|
||||||
|
AbsolutePath = 3
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string NodeWithReadyAndLookupMethods = """
|
||||||
|
public class Node
|
||||||
|
{
|
||||||
|
public virtual void _Ready() {}
|
||||||
|
public T GetNode<T>(string path) where T : Node => throw new InvalidOperationException(path);
|
||||||
|
public T? GetNodeOrNull<T>(string path) where T : Node => default;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string HBoxContainerType = """
|
||||||
|
public class HBoxContainer : Node
|
||||||
|
{
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task Generates_InferredUniqueNameBindings_And_ReadyHook_WhenReadyIsMissing()
|
public async Task Generates_InferredUniqueNameBindings_And_ReadyHook_WhenReadyIsMissing()
|
||||||
{
|
{
|
||||||
const string source = """
|
string source = CreateGetNodeSource(
|
||||||
using System;
|
FullGetNodeAttributeDeclaration,
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
"""
|
||||||
using Godot;
|
public partial class TopBar : HBoxContainer
|
||||||
|
{
|
||||||
|
[GetNode]
|
||||||
|
private HBoxContainer _leftContainer = null!;
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
[GetNode]
|
||||||
{
|
private HBoxContainer m_rightContainer = null!;
|
||||||
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
|
}
|
||||||
public sealed class GetNodeAttribute : Attribute
|
""",
|
||||||
{
|
HBoxContainerType);
|
||||||
public GetNodeAttribute() {}
|
|
||||||
public GetNodeAttribute(string path) { Path = path; }
|
|
||||||
public string? Path { get; set; }
|
|
||||||
public bool Required { get; set; } = true;
|
|
||||||
public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum NodeLookupMode
|
|
||||||
{
|
|
||||||
Auto = 0,
|
|
||||||
UniqueName = 1,
|
|
||||||
RelativePath = 2,
|
|
||||||
AbsolutePath = 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace Godot
|
|
||||||
{
|
|
||||||
public class Node
|
|
||||||
{
|
|
||||||
public virtual void _Ready() {}
|
|
||||||
public T GetNode<T>(string path) where T : Node => throw new InvalidOperationException(path);
|
|
||||||
public T? GetNodeOrNull<T>(string path) where T : Node => default;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class HBoxContainer : Node
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
public partial class TopBar : HBoxContainer
|
|
||||||
{
|
|
||||||
[GetNode]
|
|
||||||
private HBoxContainer _leftContainer = null!;
|
|
||||||
|
|
||||||
[GetNode]
|
|
||||||
private HBoxContainer m_rightContainer = null!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
const string expected = """
|
const string expected = """
|
||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
@ -88,69 +115,30 @@ public class GetNodeGeneratorTests
|
|||||||
|
|
||||||
await GeneratorTest<GetNodeGenerator>.RunAsync(
|
await GeneratorTest<GetNodeGenerator>.RunAsync(
|
||||||
source,
|
source,
|
||||||
("TestApp_TopBar.GetNode.g.cs", expected));
|
("TestApp_TopBar.GetNode.g.cs", expected)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task Generates_ManualInjectionOnly_WhenReadyAlreadyExists()
|
public async Task Generates_ManualInjectionOnly_WhenReadyAlreadyExists()
|
||||||
{
|
{
|
||||||
const string source = """
|
string source = CreateGetNodeSource(
|
||||||
using System;
|
FullGetNodeAttributeDeclaration,
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
"""
|
||||||
using Godot;
|
public partial class TopBar : HBoxContainer
|
||||||
|
{
|
||||||
|
[GetNode("%LeftContainer")]
|
||||||
|
private HBoxContainer _leftContainer = null!;
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
[GetNode(Required = false, Lookup = NodeLookupMode.RelativePath)]
|
||||||
{
|
private HBoxContainer? _rightContainer;
|
||||||
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class GetNodeAttribute : Attribute
|
|
||||||
{
|
|
||||||
public GetNodeAttribute() {}
|
|
||||||
public GetNodeAttribute(string path) { Path = path; }
|
|
||||||
public string? Path { get; set; }
|
|
||||||
public bool Required { get; set; } = true;
|
|
||||||
public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum NodeLookupMode
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
Auto = 0,
|
__InjectGetNodes_Generated();
|
||||||
UniqueName = 1,
|
}
|
||||||
RelativePath = 2,
|
}
|
||||||
AbsolutePath = 3
|
""",
|
||||||
}
|
HBoxContainerType);
|
||||||
}
|
|
||||||
|
|
||||||
namespace Godot
|
|
||||||
{
|
|
||||||
public class Node
|
|
||||||
{
|
|
||||||
public virtual void _Ready() {}
|
|
||||||
public T GetNode<T>(string path) where T : Node => throw new InvalidOperationException(path);
|
|
||||||
public T? GetNodeOrNull<T>(string path) where T : Node => default;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class HBoxContainer : Node
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
public partial class TopBar : HBoxContainer
|
|
||||||
{
|
|
||||||
[GetNode("%LeftContainer")]
|
|
||||||
private HBoxContainer _leftContainer = null!;
|
|
||||||
|
|
||||||
[GetNode(Required = false, Lookup = NodeLookupMode.RelativePath)]
|
|
||||||
private HBoxContainer? _rightContainer;
|
|
||||||
|
|
||||||
public override void _Ready()
|
|
||||||
{
|
|
||||||
__InjectGetNodes_Generated();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
const string expected = """
|
const string expected = """
|
||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
@ -171,7 +159,7 @@ public class GetNodeGeneratorTests
|
|||||||
|
|
||||||
await GeneratorTest<GetNodeGenerator>.RunAsync(
|
await GeneratorTest<GetNodeGenerator>.RunAsync(
|
||||||
source,
|
source,
|
||||||
("TestApp_TopBar.GetNode.g.cs", expected));
|
("TestApp_TopBar.GetNode.g.cs", expected)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -234,58 +222,26 @@ public class GetNodeGeneratorTests
|
|||||||
.WithSpan(39, 24, 39, 38)
|
.WithSpan(39, 24, 39, 38)
|
||||||
.WithArguments("_leftContainer"));
|
.WithArguments("_leftContainer"));
|
||||||
|
|
||||||
await test.RunAsync();
|
await test.RunAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task Reports_Diagnostic_When_Generated_Injection_Method_Name_Already_Exists()
|
public async Task Reports_Diagnostic_When_Generated_Injection_Method_Name_Already_Exists()
|
||||||
{
|
{
|
||||||
const string source = """
|
string source = CreateGetNodeSource(
|
||||||
using System;
|
MinimalGetNodeAttributeDeclaration,
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
"""
|
||||||
using Godot;
|
public partial class TopBar : HBoxContainer
|
||||||
|
{
|
||||||
|
[GetNode]
|
||||||
|
private HBoxContainer _leftContainer = null!;
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
private void {|#0:__InjectGetNodes_Generated|}()
|
||||||
{
|
{
|
||||||
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
|
}
|
||||||
public sealed class GetNodeAttribute : Attribute
|
}
|
||||||
{
|
""",
|
||||||
public GetNodeAttribute() {}
|
HBoxContainerType);
|
||||||
}
|
|
||||||
|
|
||||||
public enum NodeLookupMode
|
|
||||||
{
|
|
||||||
Auto = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace Godot
|
|
||||||
{
|
|
||||||
public class Node
|
|
||||||
{
|
|
||||||
public virtual void _Ready() {}
|
|
||||||
public T GetNode<T>(string path) where T : Node => throw new InvalidOperationException(path);
|
|
||||||
public T? GetNodeOrNull<T>(string path) where T : Node => default;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class HBoxContainer : Node
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
public partial class TopBar : HBoxContainer
|
|
||||||
{
|
|
||||||
[GetNode]
|
|
||||||
private HBoxContainer _leftContainer = null!;
|
|
||||||
|
|
||||||
private void {|#0:__InjectGetNodes_Generated|}()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
var test = new CSharpSourceGeneratorTest<GetNodeGenerator, DefaultVerifier>
|
var test = new CSharpSourceGeneratorTest<GetNodeGenerator, DefaultVerifier>
|
||||||
{
|
{
|
||||||
@ -301,6 +257,39 @@ public class GetNodeGeneratorTests
|
|||||||
.WithLocation(0)
|
.WithLocation(0)
|
||||||
.WithArguments("TopBar", "__InjectGetNodes_Generated"));
|
.WithArguments("TopBar", "__InjectGetNodes_Generated"));
|
||||||
|
|
||||||
await test.RunAsync();
|
await test.RunAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private static string CreateGetNodeSource(
|
||||||
|
string attributeDeclaration,
|
||||||
|
string testAppSource,
|
||||||
|
params string[] godotTypes)
|
||||||
|
{
|
||||||
|
string[] allGodotTypes = new string[godotTypes.Length + 1];
|
||||||
|
allGodotTypes[0] = NodeWithReadyAndLookupMethods;
|
||||||
|
Array.Copy(godotTypes, 0, allGodotTypes, 1, godotTypes.Length);
|
||||||
|
|
||||||
|
string godotSource = string.Join($"{Environment.NewLine}{Environment.NewLine}", allGodotTypes);
|
||||||
|
|
||||||
|
return $$"""
|
||||||
|
using System;
|
||||||
|
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace GFramework.Godot.SourceGenerators.Abstractions
|
||||||
|
{
|
||||||
|
{{attributeDeclaration}}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Godot
|
||||||
|
{
|
||||||
|
{{godotSource}}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace TestApp
|
||||||
|
{
|
||||||
|
{{testAppSource}}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -8,6 +8,131 @@ namespace GFramework.Godot.SourceGenerators.Tests.Project;
|
|||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class GodotProjectMetadataGeneratorTests
|
public class GodotProjectMetadataGeneratorTests
|
||||||
{
|
{
|
||||||
|
private const string AutoLoadProjectFile = """
|
||||||
|
[autoload]
|
||||||
|
GameServices="*res://autoload/game_services.tscn"
|
||||||
|
AudioBus="*res://autoload/audio_bus.gd"
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string InputActionsProjectFile = """
|
||||||
|
[input]
|
||||||
|
move_up={
|
||||||
|
"deadzone": 0.5
|
||||||
|
}
|
||||||
|
ui_cancel={
|
||||||
|
"deadzone": 0.5
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string ExpectedAutoLoads = """
|
||||||
|
// <auto-generated />
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace GFramework.Godot.Generated;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 提供 project.godot 中 AutoLoad 单例的强类型访问入口。
|
||||||
|
/// </summary>
|
||||||
|
public static partial class AutoLoads
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取 AutoLoad <c>GameServices</c>。
|
||||||
|
/// </summary>
|
||||||
|
public static global::TestApp.GameServices GameServices => GetRequiredNode<global::TestApp.GameServices>("GameServices");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 尝试获取 AutoLoad <c>GameServices</c>。
|
||||||
|
/// </summary>
|
||||||
|
public static bool TryGetGameServices(out global::TestApp.GameServices? value)
|
||||||
|
{
|
||||||
|
return TryGetNode("GameServices", out value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取 AutoLoad <c>AudioBus</c>。
|
||||||
|
/// </summary>
|
||||||
|
public static global::Godot.Node AudioBus => GetRequiredNode<global::Godot.Node>("AudioBus");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 尝试获取 AutoLoad <c>AudioBus</c>。
|
||||||
|
/// </summary>
|
||||||
|
public static bool TryGetAudioBus(out global::Godot.Node? value)
|
||||||
|
{
|
||||||
|
return TryGetNode("AudioBus", out value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取一个必填的 AutoLoad 节点;缺失时抛出异常。
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TNode">节点类型。</typeparam>
|
||||||
|
/// <param name="autoLoadName">AutoLoad 名称。</param>
|
||||||
|
/// <returns>已解析的 AutoLoad 节点。</returns>
|
||||||
|
private static TNode GetRequiredNode<TNode>(string autoLoadName)
|
||||||
|
where TNode : global::Godot.Node
|
||||||
|
{
|
||||||
|
if (TryGetNode(autoLoadName, out TNode? value))
|
||||||
|
{
|
||||||
|
return value!;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new global::System.InvalidOperationException($"AutoLoad '{autoLoadName}' is not available on the active SceneTree root.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 尝试从当前 SceneTree 根节点解析 AutoLoad。
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TNode">节点类型。</typeparam>
|
||||||
|
/// <param name="autoLoadName">AutoLoad 名称。</param>
|
||||||
|
/// <param name="value">解析到的节点实例。</param>
|
||||||
|
/// <returns>若当前进程存在 SceneTree 且根节点中能解析到该 AutoLoad,则返回 <c>true</c>。</returns>
|
||||||
|
private static bool TryGetNode<TNode>(string autoLoadName, out TNode? value)
|
||||||
|
where TNode : global::Godot.Node
|
||||||
|
{
|
||||||
|
value = default;
|
||||||
|
|
||||||
|
if (global::Godot.Engine.GetMainLoop() is not global::Godot.SceneTree sceneTree)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var root = sceneTree.Root;
|
||||||
|
if (root is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = root.GetNodeOrNull<TNode>($"/root/{autoLoadName}");
|
||||||
|
return value is not null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string ExpectedInputActions = """
|
||||||
|
// <auto-generated />
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace GFramework.Godot.Generated;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 提供 project.godot 中 Input Action 名称的强类型常量。
|
||||||
|
/// </summary>
|
||||||
|
public static partial class InputActions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Input Action <c>move_up</c> 的稳定名称。
|
||||||
|
/// </summary>
|
||||||
|
public const string MoveUp = "move_up";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Input Action <c>ui_cancel</c> 的稳定名称。
|
||||||
|
/// </summary>
|
||||||
|
public const string UiCancel = "ui_cancel";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
""";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证会根据 AutoLoad 与 Input Action 生成稳定的强类型入口。
|
/// 验证会根据 AutoLoad 与 Input Action 生成稳定的强类型入口。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -29,142 +154,19 @@ public class GodotProjectMetadataGeneratorTests
|
|||||||
""",
|
""",
|
||||||
includeAutoLoadAttribute: true);
|
includeAutoLoadAttribute: true);
|
||||||
|
|
||||||
const string projectFile = """
|
|
||||||
[autoload]
|
|
||||||
GameServices="*res://autoload/game_services.tscn"
|
|
||||||
AudioBus="*res://autoload/audio_bus.gd"
|
|
||||||
|
|
||||||
[input]
|
|
||||||
move_up={
|
|
||||||
"deadzone": 0.5
|
|
||||||
}
|
|
||||||
ui_cancel={
|
|
||||||
"deadzone": 0.5
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
const string expectedAutoLoads = """
|
|
||||||
// <auto-generated />
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace GFramework.Godot.Generated;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 提供 project.godot 中 AutoLoad 单例的强类型访问入口。
|
|
||||||
/// </summary>
|
|
||||||
public static partial class AutoLoads
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 获取 AutoLoad <c>GameServices</c>。
|
|
||||||
/// </summary>
|
|
||||||
public static global::TestApp.GameServices GameServices => GetRequiredNode<global::TestApp.GameServices>("GameServices");
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 尝试获取 AutoLoad <c>GameServices</c>。
|
|
||||||
/// </summary>
|
|
||||||
public static bool TryGetGameServices(out global::TestApp.GameServices? value)
|
|
||||||
{
|
|
||||||
return TryGetNode("GameServices", out value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取 AutoLoad <c>AudioBus</c>。
|
|
||||||
/// </summary>
|
|
||||||
public static global::Godot.Node AudioBus => GetRequiredNode<global::Godot.Node>("AudioBus");
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 尝试获取 AutoLoad <c>AudioBus</c>。
|
|
||||||
/// </summary>
|
|
||||||
public static bool TryGetAudioBus(out global::Godot.Node? value)
|
|
||||||
{
|
|
||||||
return TryGetNode("AudioBus", out value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取一个必填的 AutoLoad 节点;缺失时抛出异常。
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TNode">节点类型。</typeparam>
|
|
||||||
/// <param name="autoLoadName">AutoLoad 名称。</param>
|
|
||||||
/// <returns>已解析的 AutoLoad 节点。</returns>
|
|
||||||
private static TNode GetRequiredNode<TNode>(string autoLoadName)
|
|
||||||
where TNode : global::Godot.Node
|
|
||||||
{
|
|
||||||
if (TryGetNode(autoLoadName, out TNode? value))
|
|
||||||
{
|
|
||||||
return value!;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new global::System.InvalidOperationException($"AutoLoad '{autoLoadName}' is not available on the active SceneTree root.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 尝试从当前 SceneTree 根节点解析 AutoLoad。
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TNode">节点类型。</typeparam>
|
|
||||||
/// <param name="autoLoadName">AutoLoad 名称。</param>
|
|
||||||
/// <param name="value">解析到的节点实例。</param>
|
|
||||||
/// <returns>若当前进程存在 SceneTree 且根节点中能解析到该 AutoLoad,则返回 <c>true</c>。</returns>
|
|
||||||
private static bool TryGetNode<TNode>(string autoLoadName, out TNode? value)
|
|
||||||
where TNode : global::Godot.Node
|
|
||||||
{
|
|
||||||
value = default;
|
|
||||||
|
|
||||||
if (global::Godot.Engine.GetMainLoop() is not global::Godot.SceneTree sceneTree)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var root = sceneTree.Root;
|
|
||||||
if (root is null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = root.GetNodeOrNull<TNode>($"/root/{autoLoadName}");
|
|
||||||
return value is not null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
""";
|
|
||||||
|
|
||||||
const string expectedInputActions = """
|
|
||||||
// <auto-generated />
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace GFramework.Godot.Generated;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 提供 project.godot 中 Input Action 名称的强类型常量。
|
|
||||||
/// </summary>
|
|
||||||
public static partial class InputActions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Input Action <c>move_up</c> 的稳定名称。
|
|
||||||
/// </summary>
|
|
||||||
public const string MoveUp = "move_up";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Input Action <c>ui_cancel</c> 的稳定名称。
|
|
||||||
/// </summary>
|
|
||||||
public const string UiCancel = "ui_cancel";
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
""";
|
|
||||||
|
|
||||||
var result = AdditionalTextGeneratorTestDriver.Run<GodotProjectMetadataGenerator>(
|
var result = AdditionalTextGeneratorTestDriver.Run<GodotProjectMetadataGenerator>(
|
||||||
source,
|
source,
|
||||||
("project.godot", projectFile));
|
("project.godot", $"{AutoLoadProjectFile}\n\n{InputActionsProjectFile}"));
|
||||||
|
|
||||||
var generatedSources = AdditionalTextGeneratorTestDriver.ToGeneratedSourceMap(result);
|
var generatedSources = AdditionalTextGeneratorTestDriver.ToGeneratedSourceMap(result);
|
||||||
|
|
||||||
Assert.That(result.Results.Single().Diagnostics, Is.Empty);
|
Assert.That(result.Results.Single().Diagnostics, Is.Empty);
|
||||||
Assert.That(
|
Assert.That(
|
||||||
generatedSources["GFramework_Godot_Generated_AutoLoads.g.cs"],
|
generatedSources["GFramework_Godot_Generated_AutoLoads.g.cs"],
|
||||||
Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(expectedAutoLoads)));
|
Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(ExpectedAutoLoads)));
|
||||||
Assert.That(
|
Assert.That(
|
||||||
generatedSources["GFramework_Godot_Generated_InputActions.g.cs"],
|
generatedSources["GFramework_Godot_Generated_InputActions.g.cs"],
|
||||||
Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(expectedInputActions)));
|
Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(ExpectedInputActions)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -6,48 +6,52 @@ namespace GFramework.Godot.SourceGenerators.Tests.Registration;
|
|||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class AutoRegisterExportedCollectionsGeneratorTests
|
public class AutoRegisterExportedCollectionsGeneratorTests
|
||||||
{
|
{
|
||||||
|
private const string StandardAttributeDeclarations = """
|
||||||
|
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||||
|
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
||||||
|
public sealed class RegisterExportedCollectionAttribute : Attribute
|
||||||
|
{
|
||||||
|
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string MultiDeclarationAttributeDeclarations = """
|
||||||
|
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
||||||
|
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
||||||
|
public sealed class RegisterExportedCollectionAttribute : Attribute
|
||||||
|
{
|
||||||
|
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task Generates_Batch_Registration_Method_For_Annotated_Collections()
|
public async Task Generates_Batch_Registration_Method_For_Annotated_Collections()
|
||||||
{
|
{
|
||||||
const string source = """
|
string source = CreateSource(
|
||||||
#nullable enable
|
"""
|
||||||
using System;
|
public sealed class IntRegistry
|
||||||
using System.Collections.Generic;
|
{
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
public void Register(int value) { }
|
||||||
|
}
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
|
[AutoRegisterExportedCollections]
|
||||||
{
|
public partial class Bootstrapper<TReference, TNotNull, TValue, TUnmanaged>
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
where TReference : class?
|
||||||
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
|
where TNotNull : notnull
|
||||||
|
where TValue : struct
|
||||||
|
where TUnmanaged : unmanaged
|
||||||
|
{
|
||||||
|
private readonly IntRegistry? _registry = new();
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
||||||
public sealed class RegisterExportedCollectionAttribute : Attribute
|
public List<int>? Values { get; } = new();
|
||||||
{
|
}
|
||||||
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
|
""",
|
||||||
}
|
nullableEnabled: true);
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
public sealed class IntRegistry
|
|
||||||
{
|
|
||||||
public void Register(int value) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
[AutoRegisterExportedCollections]
|
|
||||||
public partial class Bootstrapper<TReference, TNotNull, TValue, TUnmanaged>
|
|
||||||
where TReference : class?
|
|
||||||
where TNotNull : notnull
|
|
||||||
where TValue : struct
|
|
||||||
where TUnmanaged : unmanaged
|
|
||||||
{
|
|
||||||
private readonly IntRegistry? _registry = new();
|
|
||||||
|
|
||||||
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
|
||||||
public List<int>? Values { get; } = new();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
const string expected = """
|
const string expected = """
|
||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
@ -77,7 +81,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests
|
|||||||
|
|
||||||
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
|
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
|
||||||
source,
|
source,
|
||||||
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
|
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -137,41 +141,23 @@ public class AutoRegisterExportedCollectionsGeneratorTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task Generates_Batch_Registration_Method_When_Register_Method_Uses_Array_Parameter()
|
public async Task Generates_Batch_Registration_Method_When_Register_Method_Uses_Array_Parameter()
|
||||||
{
|
{
|
||||||
const string source = """
|
string source = CreateSource(
|
||||||
#nullable enable
|
"""
|
||||||
using System;
|
public sealed class ArrayRegistry
|
||||||
using System.Collections.Generic;
|
{
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
public void Register(int[] value) { }
|
||||||
|
}
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
|
[AutoRegisterExportedCollections]
|
||||||
{
|
public partial class Bootstrapper
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
{
|
||||||
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
|
private readonly ArrayRegistry _registry = new();
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
[RegisterExportedCollection(nameof(_registry), nameof(ArrayRegistry.Register))]
|
||||||
public sealed class RegisterExportedCollectionAttribute : Attribute
|
public List<int[]> Values { get; } = new();
|
||||||
{
|
}
|
||||||
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
|
""",
|
||||||
}
|
nullableEnabled: true);
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
public sealed class ArrayRegistry
|
|
||||||
{
|
|
||||||
public void Register(int[] value) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
[AutoRegisterExportedCollections]
|
|
||||||
public partial class Bootstrapper
|
|
||||||
{
|
|
||||||
private readonly ArrayRegistry _registry = new();
|
|
||||||
|
|
||||||
[RegisterExportedCollection(nameof(_registry), nameof(ArrayRegistry.Register))]
|
|
||||||
public List<int[]> Values { get; } = new();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
const string expected = """
|
const string expected = """
|
||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
@ -197,59 +183,41 @@ public class AutoRegisterExportedCollectionsGeneratorTests
|
|||||||
|
|
||||||
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
|
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
|
||||||
source,
|
source,
|
||||||
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
|
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Inherited_Interface()
|
public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Inherited_Interface()
|
||||||
{
|
{
|
||||||
const string source = """
|
string source = CreateSource(
|
||||||
#nullable enable
|
"""
|
||||||
using System;
|
public interface IKeyValue<TKey, TValue>
|
||||||
using System.Collections.Generic;
|
{
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
}
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
|
public interface IRegistry<TKey, TValue>
|
||||||
{
|
{
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
void Registry(IKeyValue<TKey, TValue> mapping);
|
||||||
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
|
}
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
public interface IAssetRegistry<TValue> : IRegistry<string, TValue>
|
||||||
public sealed class RegisterExportedCollectionAttribute : Attribute
|
{
|
||||||
{
|
}
|
||||||
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
public sealed class IntConfig : IKeyValue<string, int>
|
||||||
{
|
{
|
||||||
public interface IKeyValue<TKey, TValue>
|
}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IRegistry<TKey, TValue>
|
[AutoRegisterExportedCollections]
|
||||||
{
|
public partial class Bootstrapper
|
||||||
void Registry(IKeyValue<TKey, TValue> mapping);
|
{
|
||||||
}
|
private readonly IAssetRegistry<int>? _registry = null;
|
||||||
|
|
||||||
public interface IAssetRegistry<TValue> : IRegistry<string, TValue>
|
[RegisterExportedCollection(nameof(_registry), "Registry")]
|
||||||
{
|
public List<IntConfig>? Values { get; } = new();
|
||||||
}
|
}
|
||||||
|
""",
|
||||||
public sealed class IntConfig : IKeyValue<string, int>
|
nullableEnabled: true);
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
[AutoRegisterExportedCollections]
|
|
||||||
public partial class Bootstrapper
|
|
||||||
{
|
|
||||||
private readonly IAssetRegistry<int>? _registry = null;
|
|
||||||
|
|
||||||
[RegisterExportedCollection(nameof(_registry), "Registry")]
|
|
||||||
public List<IntConfig>? Values { get; } = new();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
const string expected = """
|
const string expected = """
|
||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
@ -275,7 +243,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests
|
|||||||
|
|
||||||
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
|
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
|
||||||
source,
|
source,
|
||||||
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
|
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -340,45 +308,27 @@ public class AutoRegisterExportedCollectionsGeneratorTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Base_Class()
|
public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Base_Class()
|
||||||
{
|
{
|
||||||
const string source = """
|
string source = CreateSource(
|
||||||
#nullable enable
|
"""
|
||||||
using System;
|
public class BaseRegistry
|
||||||
using System.Collections.Generic;
|
{
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
public void Register(int value) { }
|
||||||
|
}
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
|
public sealed class DerivedRegistry : BaseRegistry
|
||||||
{
|
{
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
}
|
||||||
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
[AutoRegisterExportedCollections]
|
||||||
public sealed class RegisterExportedCollectionAttribute : Attribute
|
public partial class Bootstrapper
|
||||||
{
|
{
|
||||||
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
|
private readonly DerivedRegistry? _registry = new();
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
[RegisterExportedCollection(nameof(_registry), nameof(BaseRegistry.Register))]
|
||||||
{
|
public List<int>? Values { get; } = new();
|
||||||
public class BaseRegistry
|
}
|
||||||
{
|
""",
|
||||||
public void Register(int value) { }
|
nullableEnabled: true);
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class DerivedRegistry : BaseRegistry
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
[AutoRegisterExportedCollections]
|
|
||||||
public partial class Bootstrapper
|
|
||||||
{
|
|
||||||
private readonly DerivedRegistry? _registry = new();
|
|
||||||
|
|
||||||
[RegisterExportedCollection(nameof(_registry), nameof(BaseRegistry.Register))]
|
|
||||||
public List<int>? Values { get; } = new();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
const string expected = """
|
const string expected = """
|
||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
@ -404,50 +354,32 @@ public class AutoRegisterExportedCollectionsGeneratorTests
|
|||||||
|
|
||||||
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
|
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
|
||||||
source,
|
source,
|
||||||
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
|
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task Generates_Batch_Registration_Method_When_Registry_Member_Comes_From_Base_Class()
|
public async Task Generates_Batch_Registration_Method_When_Registry_Member_Comes_From_Base_Class()
|
||||||
{
|
{
|
||||||
const string source = """
|
string source = CreateSource(
|
||||||
#nullable enable
|
"""
|
||||||
using System;
|
public sealed class IntRegistry
|
||||||
using System.Collections.Generic;
|
{
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
public void Register(int value) { }
|
||||||
|
}
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
|
public abstract class BootstrapperBase
|
||||||
{
|
{
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
protected readonly IntRegistry? _registry = new();
|
||||||
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
|
}
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
[AutoRegisterExportedCollections]
|
||||||
public sealed class RegisterExportedCollectionAttribute : Attribute
|
public partial class Bootstrapper : BootstrapperBase
|
||||||
{
|
{
|
||||||
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
|
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
||||||
}
|
public List<int>? Values { get; } = new();
|
||||||
}
|
}
|
||||||
|
""",
|
||||||
namespace TestApp
|
nullableEnabled: true);
|
||||||
{
|
|
||||||
public sealed class IntRegistry
|
|
||||||
{
|
|
||||||
public void Register(int value) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract class BootstrapperBase
|
|
||||||
{
|
|
||||||
protected readonly IntRegistry? _registry = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
[AutoRegisterExportedCollections]
|
|
||||||
public partial class Bootstrapper : BootstrapperBase
|
|
||||||
{
|
|
||||||
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
|
||||||
public List<int>? Values { get; } = new();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
const string expected = """
|
const string expected = """
|
||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
@ -473,74 +405,47 @@ public class AutoRegisterExportedCollectionsGeneratorTests
|
|||||||
|
|
||||||
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
|
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
|
||||||
source,
|
source,
|
||||||
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
|
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task Reports_Diagnostic_When_Collection_Member_Is_Not_Instance_Readable()
|
public async Task Reports_Diagnostic_When_Collection_Member_Is_Not_Instance_Readable()
|
||||||
{
|
{
|
||||||
const string source = """
|
string source = CreateSource(
|
||||||
using System;
|
"""
|
||||||
using System.Collections.Generic;
|
public sealed class IntRegistry
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
{
|
||||||
|
public void Register(int value) { }
|
||||||
|
}
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
|
[AutoRegisterExportedCollections]
|
||||||
{
|
public partial class Bootstrapper
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
{
|
||||||
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
|
private readonly IntRegistry _registry = new();
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
||||||
public sealed class RegisterExportedCollectionAttribute : Attribute
|
public static List<int> {|#0:StaticValues|} = new();
|
||||||
{
|
|
||||||
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
||||||
{
|
public static List<int> {|#1:StaticPropertyValues|} { get; } = new();
|
||||||
public sealed class IntRegistry
|
|
||||||
{
|
|
||||||
public void Register(int value) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
[AutoRegisterExportedCollections]
|
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
||||||
public partial class Bootstrapper
|
public List<int> {|#2:WriteOnlyValues|} { set { } }
|
||||||
{
|
}
|
||||||
private readonly IntRegistry _registry = new();
|
""");
|
||||||
|
|
||||||
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
await VerifyDiagnosticsAsync(
|
||||||
public static List<int> {|#0:StaticValues|} = new();
|
source,
|
||||||
|
skipGeneratedSourcesCheck: true,
|
||||||
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
|
||||||
public static List<int> {|#1:StaticPropertyValues|} { get; } = new();
|
.WithLocation(0)
|
||||||
|
.WithArguments("StaticValues"),
|
||||||
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
|
||||||
public List<int> {|#2:WriteOnlyValues|} { set { } }
|
.WithLocation(1)
|
||||||
}
|
.WithArguments("StaticPropertyValues"),
|
||||||
}
|
new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
|
||||||
""";
|
.WithLocation(2)
|
||||||
|
.WithArguments("WriteOnlyValues")).ConfigureAwait(false);
|
||||||
var test = new CSharpSourceGeneratorTest<AutoRegisterExportedCollectionsGenerator, DefaultVerifier>
|
|
||||||
{
|
|
||||||
TestState =
|
|
||||||
{
|
|
||||||
Sources = { source }
|
|
||||||
},
|
|
||||||
DisabledDiagnostics = { "GF_Common_Trace_001" },
|
|
||||||
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
|
|
||||||
};
|
|
||||||
|
|
||||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
|
|
||||||
.WithLocation(0)
|
|
||||||
.WithArguments("StaticValues"));
|
|
||||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
|
|
||||||
.WithLocation(1)
|
|
||||||
.WithArguments("StaticPropertyValues"));
|
|
||||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
|
|
||||||
.WithLocation(2)
|
|
||||||
.WithArguments("WriteOnlyValues"));
|
|
||||||
|
|
||||||
await test.RunAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -711,45 +616,28 @@ public class AutoRegisterExportedCollectionsGeneratorTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task Generates_Only_One_Source_When_Multiple_Partial_Declarations_Are_Annotated()
|
public async Task Generates_Only_One_Source_When_Multiple_Partial_Declarations_Are_Annotated()
|
||||||
{
|
{
|
||||||
const string source = """
|
string source = CreateSource(
|
||||||
#nullable enable
|
"""
|
||||||
using System;
|
public sealed class IntRegistry
|
||||||
using System.Collections.Generic;
|
{
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
public void Register(int value) { }
|
||||||
|
}
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
|
[AutoRegisterExportedCollections]
|
||||||
{
|
public partial class Bootstrapper
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
{
|
||||||
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
|
private readonly IntRegistry? _registry = new();
|
||||||
|
}
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
[AutoRegisterExportedCollections]
|
||||||
public sealed class RegisterExportedCollectionAttribute : Attribute
|
public partial class Bootstrapper
|
||||||
{
|
{
|
||||||
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
|
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
||||||
}
|
public List<int>? Values { get; } = new();
|
||||||
}
|
}
|
||||||
|
""",
|
||||||
namespace TestApp
|
nullableEnabled: true,
|
||||||
{
|
allowMultipleDeclarations: true);
|
||||||
public sealed class IntRegistry
|
|
||||||
{
|
|
||||||
public void Register(int value) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
[AutoRegisterExportedCollections]
|
|
||||||
public partial class Bootstrapper
|
|
||||||
{
|
|
||||||
private readonly IntRegistry? _registry = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
[AutoRegisterExportedCollections]
|
|
||||||
public partial class Bootstrapper
|
|
||||||
{
|
|
||||||
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
|
||||||
public List<int>? Values { get; } = new();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
const string expected = """
|
const string expected = """
|
||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
@ -775,6 +663,61 @@ public class AutoRegisterExportedCollectionsGeneratorTests
|
|||||||
|
|
||||||
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
|
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
|
||||||
source,
|
source,
|
||||||
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
|
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateSource(
|
||||||
|
string applicationSource,
|
||||||
|
bool nullableEnabled = false,
|
||||||
|
bool allowMultipleDeclarations = false)
|
||||||
|
{
|
||||||
|
string nullableDirective = nullableEnabled ? "#nullable enable\n" : string.Empty;
|
||||||
|
string attributeDeclarations = allowMultipleDeclarations
|
||||||
|
? MultiDeclarationAttributeDeclarations
|
||||||
|
: StandardAttributeDeclarations;
|
||||||
|
|
||||||
|
return $$"""
|
||||||
|
{{nullableDirective}}using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
||||||
|
|
||||||
|
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
|
||||||
|
{
|
||||||
|
{{attributeDeclarations}}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace TestApp
|
||||||
|
{
|
||||||
|
{{applicationSource}}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Task VerifyDiagnosticsAsync(
|
||||||
|
string source,
|
||||||
|
bool skipGeneratedSourcesCheck = false,
|
||||||
|
params DiagnosticResult[] expectedDiagnostics)
|
||||||
|
{
|
||||||
|
var test = new CSharpSourceGeneratorTest<AutoRegisterExportedCollectionsGenerator, DefaultVerifier>
|
||||||
|
{
|
||||||
|
TestState =
|
||||||
|
{
|
||||||
|
Sources = { source }
|
||||||
|
},
|
||||||
|
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
||||||
|
};
|
||||||
|
|
||||||
|
if (skipGeneratedSourcesCheck)
|
||||||
|
{
|
||||||
|
test.TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (DiagnosticResult expectedDiagnostic in expectedDiagnostics)
|
||||||
|
{
|
||||||
|
test.ExpectedDiagnostics.Add(expectedDiagnostic);
|
||||||
|
}
|
||||||
|
|
||||||
|
return test.RunAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 基线采集问题
|
||||||
|
|||||||
@ -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` 切片清零
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user