diff --git a/AGENTS.md b/AGENTS.md
index 082bc7ed..8ecbd160 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -29,6 +29,10 @@ All AI agents and contributors must follow these rules when writing, reviewing,
## Git Workflow Rules
- Every completed task MUST pass at least one build validation before it is considered done.
+- When the goal is to inspect or reduce warnings printed during project build, contributors MUST establish the warning
+ baseline from a non-incremental repository-root build by running `dotnet clean` and then `dotnet build`.
+- Contributors MUST NOT treat a repeated incremental `dotnet build` result as authoritative for warning inspection when
+ a clean baseline has not been captured in the same round.
- If the task changes multiple projects or shared abstractions, prefer a solution-level or affected-project
`dotnet build ... -c Release`; otherwise use the smallest build command that still proves the result compiles.
- When a task adds a feature or modifies code, contributors MUST run a Release build for every directly affected
@@ -233,6 +237,10 @@ All generated or modified code MUST include clear and meaningful comments where
Use the smallest command set that proves the change, then expand if the change is cross-cutting.
```bash
+# Check warnings from the default repository build entrypoint
+dotnet clean
+dotnet build
+
# Build the full solution
dotnet build GFramework.sln -c Release
diff --git a/GFramework.Core.Tests/GFramework.Core.Tests.csproj b/GFramework.Core.Tests/GFramework.Core.Tests.csproj
index efea29cf..968fedd2 100644
--- a/GFramework.Core.Tests/GFramework.Core.Tests.csproj
+++ b/GFramework.Core.Tests/GFramework.Core.Tests.csproj
@@ -7,7 +7,6 @@
enable
false
true
- 0
diff --git a/GFramework.Game/Data/UnifiedSettingsDataRepository.cs b/GFramework.Game/Data/UnifiedSettingsDataRepository.cs
index 1cd13eb1..99672301 100644
--- a/GFramework.Game/Data/UnifiedSettingsDataRepository.cs
+++ b/GFramework.Game/Data/UnifiedSettingsDataRepository.cs
@@ -283,15 +283,21 @@ public class UnifiedSettingsDataRepository(
/// 复制当前统一文件快照,确保未提交修改不会污染内存中的已提交状态。
///
/// 要复制的统一文件快照。
- /// 包含独立 section 字典的新快照。
+ /// 包含独立 section 映射副本的新快照。
private static UnifiedSettingsFile CloneFile(UnifiedSettingsFile source)
{
ArgumentNullException.ThrowIfNull(source);
+ // 反序列化后的运行时类型可能只是 IDictionary 实现;若底层仍是 Dictionary,则保留其 comparer。
+ // 若 comparer 已因接口抽象而不可恢复,则显式回退到 Ordinal,避免让默认 comparer 语义继续隐式存在。
+ var sections = source.Sections is Dictionary dictionary
+ ? new Dictionary(dictionary, dictionary.Comparer)
+ : new Dictionary(source.Sections, StringComparer.Ordinal);
+
return new UnifiedSettingsFile
{
Version = source.Version,
- Sections = new Dictionary(source.Sections, source.Sections.Comparer)
+ Sections = sections
};
}
diff --git a/GFramework.Game/Data/UnifiedSettingsFile.cs b/GFramework.Game/Data/UnifiedSettingsFile.cs
index 694dcc28..02fd66b6 100644
--- a/GFramework.Game/Data/UnifiedSettingsFile.cs
+++ b/GFramework.Game/Data/UnifiedSettingsFile.cs
@@ -11,6 +11,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+using System;
+using System.Collections.Generic;
using GFramework.Core.Abstractions.Versioning;
namespace GFramework.Game.Data;
@@ -22,13 +24,19 @@ namespace GFramework.Game.Data;
internal sealed class UnifiedSettingsFile : IVersioned
{
///
- /// 配置节集合,存储不同类型的配置数据
- /// 键为配置节名称,值为配置对象
+ /// 配置节映射,存储不同类型的配置数据。
///
- public Dictionary Sections { get; set; } = new();
+ ///
+ /// 这里公开为 而不是具体的 ,
+ /// 以避免暴露可替换的具体集合实现,同时继续兼容 Newtonsoft.Json 对字典对象的序列化与反序列化。
+ /// 默认实例使用 ;若调用方提供其他实现,仓库在可以识别底层
+ /// comparer 时会保留原语义,否则克隆快照时会显式回退到
+ /// 。
+ ///
+ public IDictionary Sections { get; set; } = new Dictionary(StringComparer.Ordinal);
///
/// 配置文件版本号,用于版本控制和兼容性检查
///
public int Version { get; set; }
-}
\ No newline at end of file
+}
diff --git a/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs
index 2c6dc972..a4bb59cf 100644
--- a/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs
+++ b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs
@@ -6,57 +6,61 @@ namespace GFramework.Godot.SourceGenerators.Tests.Behavior;
[TestFixture]
public class AutoSceneGeneratorTests
{
+ private const string AutoSceneAttributeWithKeyDeclaration = """
+ [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
+ public sealed class AutoSceneAttribute : Attribute
+ {
+ public AutoSceneAttribute(string key) { }
+ }
+ """;
+
+ private const string AutoSceneAttributeWithoutKeyDeclaration = """
+ [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
+ public sealed class AutoSceneAttribute : Attribute
+ {
+ public AutoSceneAttribute() { }
+ }
+ """;
+
+ private const string NodeTypes = """
+ public class Node { }
+ public class Node2D : Node { }
+ """;
+
+ private const string SceneBehaviorInfrastructure = """
+ namespace GFramework.Game.Abstractions.Scene
+ {
+ public interface ISceneBehavior { }
+ }
+
+ namespace GFramework.Godot.Scene
+ {
+ using GFramework.Game.Abstractions.Scene;
+ using Godot;
+
+ public static class SceneBehaviorFactory
+ {
+ public static ISceneBehavior Create(T owner, string key)
+ where T : Node
+ {
+ return null!;
+ }
+ }
+ }
+ """;
+
[Test]
public async Task Generates_Scene_Behavior_Boilerplate()
{
- const string source = """
- using System;
- using GFramework.Godot.SourceGenerators.Abstractions.UI;
- using Godot;
-
- namespace GFramework.Godot.SourceGenerators.Abstractions.UI
- {
- [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
- public sealed class AutoSceneAttribute : Attribute
- {
- public AutoSceneAttribute(string key) { }
- }
- }
-
- namespace Godot
- {
- public class Node { }
- public class Node2D : Node { }
- }
-
- namespace GFramework.Game.Abstractions.Scene
- {
- public interface ISceneBehavior { }
- }
-
- namespace GFramework.Godot.Scene
- {
- using GFramework.Game.Abstractions.Scene;
- using Godot;
-
- public static class SceneBehaviorFactory
- {
- public static ISceneBehavior Create(T owner, string key)
- where T : Node
- {
- return null!;
- }
- }
- }
-
- namespace TestApp
- {
- [AutoScene("Gameplay")]
- public partial class GameplayRoot : Node2D
- {
- }
- }
- """;
+ string source = CreateAutoSceneSource(
+ AutoSceneAttributeWithKeyDeclaration,
+ """
+ [AutoScene("Gameplay")]
+ public partial class GameplayRoot : Node2D
+ {
+ }
+ """,
+ includeBehaviorInfrastructure: true);
const string expected = """
//
@@ -80,40 +84,20 @@ public class AutoSceneGeneratorTests
await GeneratorTest.RunAsync(
source,
- ("TestApp_GameplayRoot.AutoScene.g.cs", expected));
+ ("TestApp_GameplayRoot.AutoScene.g.cs", expected)).ConfigureAwait(false);
}
[Test]
public async Task Reports_Diagnostic_When_AutoScene_Arguments_Are_Invalid()
{
- const string source = """
- using System;
- using GFramework.Godot.SourceGenerators.Abstractions.UI;
- using Godot;
-
- namespace GFramework.Godot.SourceGenerators.Abstractions.UI
- {
- [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
- public sealed class AutoSceneAttribute : Attribute
- {
- public AutoSceneAttribute() { }
- }
- }
-
- namespace Godot
- {
- public class Node { }
- public class Node2D : Node { }
- }
-
- namespace TestApp
- {
- [{|#0:AutoScene|}]
- public partial class GameplayRoot : Node2D
- {
- }
- }
- """;
+ string source = CreateAutoSceneSource(
+ AutoSceneAttributeWithoutKeyDeclaration,
+ """
+ [{|#0:AutoScene|}]
+ public partial class GameplayRoot : Node2D
+ {
+ }
+ """);
var test = new CSharpSourceGeneratorTest
{
@@ -128,65 +112,26 @@ public class AutoSceneGeneratorTests
.WithLocation(0)
.WithArguments("AutoSceneAttribute", "GameplayRoot", "a single string scene key argument"));
- await test.RunAsync();
+ await test.RunAsync().ConfigureAwait(false);
}
[Test]
public async Task Generates_Type_Constraints_For_Nullable_Reference_NotNull_And_Unmanaged_Parameters()
{
- const string source = """
- #nullable enable
- using System;
- using GFramework.Godot.SourceGenerators.Abstractions.UI;
- using Godot;
-
- namespace GFramework.Godot.SourceGenerators.Abstractions.UI
- {
- [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
- public sealed class AutoSceneAttribute : Attribute
- {
- public AutoSceneAttribute(string key) { }
- }
- }
-
- namespace Godot
- {
- public class Node { }
- public class Node2D : Node { }
- }
-
- namespace GFramework.Game.Abstractions.Scene
- {
- public interface ISceneBehavior { }
- }
-
- namespace GFramework.Godot.Scene
- {
- using GFramework.Game.Abstractions.Scene;
- using Godot;
-
- public static class SceneBehaviorFactory
- {
- public static ISceneBehavior Create(T owner, string key)
- where T : Node
- {
- return null!;
- }
- }
- }
-
- namespace TestApp
- {
- [AutoScene("Gameplay")]
- public partial class GameplayRoot : Node2D
- where TReference : class?
- where TNotNull : notnull
- where TValue : struct
- where TUnmanaged : unmanaged
- {
- }
- }
- """;
+ string source = CreateAutoSceneSource(
+ AutoSceneAttributeWithKeyDeclaration,
+ """
+ [AutoScene("Gameplay")]
+ public partial class GameplayRoot : Node2D
+ where TReference : class?
+ where TNotNull : notnull
+ where TValue : struct
+ where TUnmanaged : unmanaged
+ {
+ }
+ """,
+ includeBehaviorInfrastructure: true,
+ nullableEnabled: true);
const string expected = """
//
@@ -214,7 +159,7 @@ public class AutoSceneGeneratorTests
await GeneratorTest.RunAsync(
source,
- ("TestApp_GameplayRoot.AutoScene.g.cs", expected));
+ ("TestApp_GameplayRoot.AutoScene.g.cs", expected)).ConfigureAwait(false);
}
///
@@ -267,7 +212,7 @@ public class AutoSceneGeneratorTests
.WithLocation(0)
.WithArguments("GameplayRoot", "SceneKeyStr"));
- await test.RunAsync();
+ await test.RunAsync().ConfigureAwait(false);
}
///
@@ -326,6 +271,39 @@ public class AutoSceneGeneratorTests
.WithLocation(0)
.WithArguments("GameplayRoot", "__autoSceneBehavior_Generated"));
- await test.RunAsync();
+ await test.RunAsync().ConfigureAwait(false);
+ }
+
+ private static string CreateAutoSceneSource(
+ string attributeDeclaration,
+ string testAppSource,
+ bool includeBehaviorInfrastructure = false,
+ bool nullableEnabled = false)
+ {
+ string nullableDirective = nullableEnabled ? "#nullable enable\n" : string.Empty;
+ string infrastructure = includeBehaviorInfrastructure
+ ? $"{Environment.NewLine}{Environment.NewLine}{SceneBehaviorInfrastructure}"
+ : string.Empty;
+
+ return $$"""
+ {{nullableDirective}}using System;
+ using GFramework.Godot.SourceGenerators.Abstractions.UI;
+ using Godot;
+
+ namespace GFramework.Godot.SourceGenerators.Abstractions.UI
+ {
+ {{attributeDeclaration}}
+ }
+
+ namespace Godot
+ {
+ {{NodeTypes}}
+ }{{infrastructure}}
+
+ namespace TestApp
+ {
+ {{testAppSource}}
+ }
+ """;
}
}
diff --git a/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoUiPageGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoUiPageGeneratorTests.cs
index 2b8aca67..5d667309 100644
--- a/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoUiPageGeneratorTests.cs
+++ b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoUiPageGeneratorTests.cs
@@ -6,69 +6,85 @@ namespace GFramework.Godot.SourceGenerators.Tests.Behavior;
[TestFixture]
public class AutoUiPageGeneratorTests
{
+ private const string AutoUiPageAttributeWithLayerDeclaration = """
+ [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
+ public sealed class AutoUiPageAttribute : Attribute
+ {
+ public AutoUiPageAttribute(string key, string layerName) { }
+ }
+ """;
+
+ private const string AutoUiPageAttributeWithoutLayerDeclaration = """
+ [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
+ public sealed class AutoUiPageAttribute : Attribute
+ {
+ public AutoUiPageAttribute(string key) { }
+ }
+ """;
+
+ private const string CanvasNodeTypes = """
+ public class Node { }
+ public class CanvasItem : Node { }
+ public class Control : CanvasItem { }
+ """;
+
+ private const string UiLayerFullEnum = """
+ namespace GFramework.Game.Abstractions.Enums
+ {
+ public enum UiLayer
+ {
+ Page,
+ Overlay,
+ Modal
+ }
+ }
+ """;
+
+ private const string UiLayerPageOnlyEnum = """
+ namespace GFramework.Game.Abstractions.Enums
+ {
+ public enum UiLayer
+ {
+ Page
+ }
+ }
+ """;
+
+ private const string UiBehaviorInfrastructure = """
+ namespace GFramework.Game.Abstractions.UI
+ {
+ public interface IUiPageBehavior { }
+ }
+
+ namespace GFramework.Godot.UI
+ {
+ using GFramework.Game.Abstractions.Enums;
+ using GFramework.Game.Abstractions.UI;
+ using Godot;
+
+ public static class UiPageBehaviorFactory
+ {
+ public static IUiPageBehavior Create(T owner, string key, UiLayer layer)
+ where T : CanvasItem
+ {
+ return null!;
+ }
+ }
+ }
+ """;
+
[Test]
public async Task Generates_Ui_Page_Behavior_Boilerplate()
{
- const string source = """
- using System;
- using GFramework.Godot.SourceGenerators.Abstractions.UI;
- using Godot;
-
- namespace GFramework.Godot.SourceGenerators.Abstractions.UI
- {
- [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
- public sealed class AutoUiPageAttribute : Attribute
- {
- public AutoUiPageAttribute(string key, string layerName) { }
- }
- }
-
- namespace Godot
- {
- public class Node { }
- public class CanvasItem : Node { }
- public class Control : CanvasItem { }
- }
-
- namespace GFramework.Game.Abstractions.Enums
- {
- public enum UiLayer
- {
- Page,
- Overlay,
- Modal
- }
- }
-
- namespace GFramework.Game.Abstractions.UI
- {
- public interface IUiPageBehavior { }
- }
-
- namespace GFramework.Godot.UI
- {
- using GFramework.Game.Abstractions.Enums;
- using GFramework.Game.Abstractions.UI;
- using Godot;
-
- public static class UiPageBehaviorFactory
- {
- public static IUiPageBehavior Create(T owner, string key, UiLayer layer)
- where T : CanvasItem
- {
- return null!;
- }
- }
- }
-
- namespace TestApp
- {
- [AutoUiPage("MainMenu", "Page")]
- public partial class MainMenu : Control
- {
- }
- }
- """;
+ string source = CreateAutoUiPageSource(
+ AutoUiPageAttributeWithLayerDeclaration,
+ UiLayerFullEnum,
+ """
+ [AutoUiPage("MainMenu", "Page")]
+ public partial class MainMenu : Control
+ {
+ }
+ """);
const string expected = """
//
@@ -92,70 +108,21 @@ public class AutoUiPageGeneratorTests
await GeneratorTest.RunAsync(
source,
- ("TestApp_MainMenu.AutoUiPage.g.cs", expected));
+ ("TestApp_MainMenu.AutoUiPage.g.cs", expected)).ConfigureAwait(false);
}
[Test]
public async Task Reports_Diagnostic_When_AutoUiPage_Attribute_Arguments_Are_Invalid()
{
- const string source = """
- using System;
- using GFramework.Godot.SourceGenerators.Abstractions.UI;
- using Godot;
-
- namespace GFramework.Godot.SourceGenerators.Abstractions.UI
- {
- [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
- public sealed class AutoUiPageAttribute : Attribute
- {
- public AutoUiPageAttribute(string key) { }
- }
- }
-
- namespace Godot
- {
- public class Node { }
- public class CanvasItem : Node { }
- public class Control : CanvasItem { }
- }
-
- namespace GFramework.Game.Abstractions.Enums
- {
- public enum UiLayer
- {
- Page
- }
- }
-
- namespace GFramework.Game.Abstractions.UI
- {
- public interface IUiPageBehavior { }
- }
-
- namespace GFramework.Godot.UI
- {
- using GFramework.Game.Abstractions.Enums;
- using GFramework.Game.Abstractions.UI;
- using Godot;
-
- public static class UiPageBehaviorFactory
- {
- public static IUiPageBehavior Create(T owner, string key, UiLayer layer)
- where T : CanvasItem
- {
- return null!;
- }
- }
- }
-
- namespace TestApp
- {
- [{|#0:AutoUiPage("MainMenu")|}]
- public partial class MainMenu : Control
- {
- }
- }
- """;
+ string source = CreateAutoUiPageSource(
+ AutoUiPageAttributeWithoutLayerDeclaration,
+ UiLayerPageOnlyEnum,
+ """
+ [{|#0:AutoUiPage("MainMenu")|}]
+ public partial class MainMenu : Control
+ {
+ }
+ """);
var test = new CSharpSourceGeneratorTest
{
@@ -174,74 +141,25 @@ public class AutoUiPageGeneratorTests
"MainMenu",
"a string key argument and a string UiLayer name argument"));
- await test.RunAsync();
+ await test.RunAsync().ConfigureAwait(false);
}
[Test]
public async Task Generates_Type_Constraints_For_ClassNullable_NotNull_And_Unmanaged()
{
- const string source = """
- #nullable enable
- using System;
- using GFramework.Godot.SourceGenerators.Abstractions.UI;
- using Godot;
-
- namespace GFramework.Godot.SourceGenerators.Abstractions.UI
- {
- [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
- public sealed class AutoUiPageAttribute : Attribute
- {
- public AutoUiPageAttribute(string key, string layerName) { }
- }
- }
-
- namespace Godot
- {
- public class Node { }
- public class CanvasItem : Node { }
- public class Control : CanvasItem { }
- }
-
- namespace GFramework.Game.Abstractions.Enums
- {
- public enum UiLayer
- {
- Page
- }
- }
-
- namespace GFramework.Game.Abstractions.UI
- {
- public interface IUiPageBehavior { }
- }
-
- namespace GFramework.Godot.UI
- {
- using GFramework.Game.Abstractions.Enums;
- using GFramework.Game.Abstractions.UI;
- using Godot;
-
- public static class UiPageBehaviorFactory
- {
- public static IUiPageBehavior Create(T owner, string key, UiLayer layer)
- where T : CanvasItem
- {
- return null!;
- }
- }
- }
-
- namespace TestApp
- {
- [AutoUiPage("MainMenu", "Page")]
- public partial class MainMenu : Control
- where TReference : class?
- where TNotNull : notnull
- where TUnmanaged : unmanaged
- {
- }
- }
- """;
+ string source = CreateAutoUiPageSource(
+ AutoUiPageAttributeWithLayerDeclaration,
+ UiLayerPageOnlyEnum,
+ """
+ [AutoUiPage("MainMenu", "Page")]
+ public partial class MainMenu : Control
+ where TReference : class?
+ where TNotNull : notnull
+ where TUnmanaged : unmanaged
+ {
+ }
+ """,
+ nullableEnabled: true);
const string expected = """
//
@@ -268,6 +186,40 @@ public class AutoUiPageGeneratorTests
await GeneratorTest.RunAsync(
source,
- ("TestApp_MainMenu.AutoUiPage.g.cs", expected));
+ ("TestApp_MainMenu.AutoUiPage.g.cs", expected)).ConfigureAwait(false);
+ }
+
+ private static string CreateAutoUiPageSource(
+ string attributeDeclaration,
+ string uiLayerDeclaration,
+ string testAppSource,
+ bool nullableEnabled = false)
+ {
+ string nullableDirective = nullableEnabled ? "#nullable enable\n" : string.Empty;
+
+ return $$"""
+ {{nullableDirective}}using System;
+ using GFramework.Godot.SourceGenerators.Abstractions.UI;
+ using Godot;
+
+ namespace GFramework.Godot.SourceGenerators.Abstractions.UI
+ {
+ {{attributeDeclaration}}
+ }
+
+ namespace Godot
+ {
+ {{CanvasNodeTypes}}
+ }
+
+ {{uiLayerDeclaration}}
+
+ {{UiBehaviorInfrastructure}}
+
+ namespace TestApp
+ {
+ {{testAppSource}}
+ }
+ """;
}
}
diff --git a/GFramework.Godot.SourceGenerators.Tests/BindNodeSignal/BindNodeSignalGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/BindNodeSignal/BindNodeSignalGeneratorTests.cs
index 7f71f210..661023f7 100644
--- a/GFramework.Godot.SourceGenerators.Tests/BindNodeSignal/BindNodeSignalGeneratorTests.cs
+++ b/GFramework.Godot.SourceGenerators.Tests/BindNodeSignal/BindNodeSignalGeneratorTests.cs
@@ -8,93 +8,103 @@ namespace GFramework.Godot.SourceGenerators.Tests.BindNodeSignal;
[TestFixture]
public class BindNodeSignalGeneratorTests
{
+ private const string BindNodeSignalAttributeDeclaration = """
+ [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
+ public sealed class BindNodeSignalAttribute : Attribute
+ {
+ public BindNodeSignalAttribute(string nodeFieldName, string signalName)
+ {
+ NodeFieldName = nodeFieldName;
+ SignalName = signalName;
+ }
+
+ public string NodeFieldName { get; }
+
+ public string SignalName { get; }
+ }
+ """;
+
+ private const string GetNodeAttributeDeclaration = """
+ [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
+ public sealed class GetNodeAttribute : Attribute
+ {
+ }
+ """;
+
+ private const string EmptyNodeType = """
+ public class Node
+ {
+ }
+ """;
+
+ private const string LifecycleNodeType = """
+ public class Node
+ {
+ public virtual void _Ready() {}
+
+ public virtual void _ExitTree() {}
+ }
+ """;
+
+ private const string ButtonType = """
+ public class Button : Node
+ {
+ public event Action? Pressed
+ {
+ add {}
+ remove {}
+ }
+ }
+ """;
+
+ private const string SpinBoxType = """
+ public class SpinBox : Node
+ {
+ public delegate void ValueChangedEventHandler(double value);
+
+ public event ValueChangedEventHandler? ValueChanged
+ {
+ add {}
+ remove {}
+ }
+ }
+ """;
+
///
/// 验证生成器会为已有生命周期调用生成成对的绑定与解绑方法。
///
[Test]
public async Task Generates_Bind_And_Unbind_Methods_For_Existing_Lifecycle_Hooks()
{
- const string source = """
- using System;
- using GFramework.Godot.SourceGenerators.Abstractions;
- using Godot;
+ string source = CreateHudSource(
+ CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
+ """
+ private Button _startButton = null!;
+ private SpinBox _startOreSpinBox = null!;
- namespace GFramework.Godot.SourceGenerators.Abstractions
- {
- [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
- public sealed class BindNodeSignalAttribute : Attribute
- {
- public BindNodeSignalAttribute(string nodeFieldName, string signalName)
- {
- NodeFieldName = nodeFieldName;
- SignalName = signalName;
- }
+ [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
+ private void OnStartButtonPressed()
+ {
+ }
- public string NodeFieldName { get; }
+ [BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))]
+ private void OnStartOreValueChanged(double value)
+ {
+ }
- public string SignalName { get; }
- }
- }
+ public override void _Ready()
+ {
+ __BindNodeSignals_Generated();
+ }
- namespace Godot
- {
- public class Node
- {
- public virtual void _Ready() {}
-
- public virtual void _ExitTree() {}
- }
-
- public class Button : Node
- {
- public event Action? Pressed
- {
- add {}
- remove {}
- }
- }
-
- public class SpinBox : Node
- {
- public delegate void ValueChangedEventHandler(double value);
-
- public event ValueChangedEventHandler? ValueChanged
- {
- add {}
- remove {}
- }
- }
- }
-
- namespace TestApp
- {
- public partial class Hud : Node
- {
- private Button _startButton = null!;
- private SpinBox _startOreSpinBox = null!;
-
- [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
- private void OnStartButtonPressed()
- {
- }
-
- [BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))]
- private void OnStartOreValueChanged(double value)
- {
- }
-
- public override void _Ready()
- {
- __BindNodeSignals_Generated();
- }
-
- public override void _ExitTree()
- {
- __UnbindNodeSignals_Generated();
- }
- }
- }
- """;
+ public override void _ExitTree()
+ {
+ __UnbindNodeSignals_Generated();
+ }
+ """,
+ LifecycleNodeType,
+ ButtonType,
+ SpinBoxType);
const string expected = """
//
@@ -121,7 +131,7 @@ public class BindNodeSignalGeneratorTests
await GeneratorTest.RunAsync(
source,
- ("TestApp_Hud.BindNodeSignal.g.cs", expected));
+ ("TestApp_Hud.BindNodeSignal.g.cs", expected)).ConfigureAwait(false);
}
///
@@ -130,70 +140,23 @@ public class BindNodeSignalGeneratorTests
[Test]
public async Task Generates_Multiple_Subscriptions_For_The_Same_Handler_And_Coexists_With_GetNode()
{
- const string source = """
- using System;
- using GFramework.Godot.SourceGenerators.Abstractions;
- using Godot;
+ string source = CreateHudSource(
+ CreateAbstractionsSource(BindNodeSignalAttributeDeclaration, GetNodeAttributeDeclaration),
+ """
+ [GetNode]
+ private Button _startButton = null!;
- namespace GFramework.Godot.SourceGenerators.Abstractions
- {
- [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
- public sealed class BindNodeSignalAttribute : Attribute
- {
- public BindNodeSignalAttribute(string nodeFieldName, string signalName)
- {
- NodeFieldName = nodeFieldName;
- SignalName = signalName;
- }
+ [GetNode]
+ private Button _cancelButton = null!;
- public string NodeFieldName { get; }
-
- public string SignalName { get; }
- }
-
- [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
- public sealed class GetNodeAttribute : Attribute
- {
- }
- }
-
- namespace Godot
- {
- public class Node
- {
- public virtual void _Ready() {}
-
- public virtual void _ExitTree() {}
- }
-
- public class Button : Node
- {
- public event Action? Pressed
- {
- add {}
- remove {}
- }
- }
- }
-
- namespace TestApp
- {
- public partial class Hud : Node
- {
- [GetNode]
- private Button _startButton = null!;
-
- [GetNode]
- private Button _cancelButton = null!;
-
- [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
- [BindNodeSignal(nameof(_cancelButton), nameof(Button.Pressed))]
- private void OnAnyButtonPressed()
- {
- }
- }
- }
- """;
+ [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
+ [BindNodeSignal(nameof(_cancelButton), nameof(Button.Pressed))]
+ private void OnAnyButtonPressed()
+ {
+ }
+ """,
+ LifecycleNodeType,
+ ButtonType);
const string expected = """
//
@@ -220,7 +183,7 @@ public class BindNodeSignalGeneratorTests
await GeneratorTest.RunAsync(
source,
- ("TestApp_Hud.BindNodeSignal.g.cs", expected));
+ ("TestApp_Hud.BindNodeSignal.g.cs", expected)).ConfigureAwait(false);
}
///
@@ -229,73 +192,24 @@ public class BindNodeSignalGeneratorTests
[Test]
public async Task Reports_Diagnostic_When_Signal_Does_Not_Exist()
{
- const string source = """
- using System;
- using GFramework.Godot.SourceGenerators.Abstractions;
- using Godot;
+ string source = CreateHudSource(
+ CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
+ """
+ private Button _startButton = null!;
- namespace GFramework.Godot.SourceGenerators.Abstractions
- {
- [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
- public sealed class BindNodeSignalAttribute : Attribute
- {
- public BindNodeSignalAttribute(string nodeFieldName, string signalName)
- {
- NodeFieldName = nodeFieldName;
- SignalName = signalName;
- }
+ [{|#0:BindNodeSignal(nameof(_startButton), "Released")|}]
+ private void OnStartButtonPressed()
+ {
+ }
+ """,
+ EmptyNodeType,
+ ButtonType);
- public string NodeFieldName { get; }
-
- public string SignalName { get; }
- }
- }
-
- namespace Godot
- {
- public class Node
- {
- }
-
- public class Button : Node
- {
- public event Action? Pressed
- {
- add {}
- remove {}
- }
- }
- }
-
- namespace TestApp
- {
- public partial class Hud : Node
- {
- private Button _startButton = null!;
-
- [{|#0:BindNodeSignal(nameof(_startButton), "Released")|}]
- private void OnStartButtonPressed()
- {
- }
- }
- }
- """;
-
- var test = new CSharpSourceGeneratorTest
- {
- TestState =
- {
- Sources = { source }
- },
- DisabledDiagnostics = { "GF_Common_Trace_001" },
- TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
- };
-
- test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_006", DiagnosticSeverity.Error)
- .WithLocation(0)
- .WithArguments("_startButton", "Released"));
-
- await test.RunAsync();
+ await VerifyDiagnosticsAsync(
+ source,
+ new DiagnosticResult("GF_Godot_BindNodeSignal_006", DiagnosticSeverity.Error)
+ .WithLocation(0)
+ .WithArguments("_startButton", "Released")).ConfigureAwait(false);
}
///
@@ -304,75 +218,24 @@ public class BindNodeSignalGeneratorTests
[Test]
public async Task Reports_Diagnostic_When_Method_Signature_Does_Not_Match_Event()
{
- const string source = """
- using System;
- using GFramework.Godot.SourceGenerators.Abstractions;
- using Godot;
+ string source = CreateHudSource(
+ CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
+ """
+ private SpinBox _startOreSpinBox = null!;
- namespace GFramework.Godot.SourceGenerators.Abstractions
- {
- [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
- public sealed class BindNodeSignalAttribute : Attribute
- {
- public BindNodeSignalAttribute(string nodeFieldName, string signalName)
- {
- NodeFieldName = nodeFieldName;
- SignalName = signalName;
- }
+ [{|#0:BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))|}]
+ private void OnStartOreValueChanged()
+ {
+ }
+ """,
+ EmptyNodeType,
+ SpinBoxType);
- public string NodeFieldName { get; }
-
- public string SignalName { get; }
- }
- }
-
- namespace Godot
- {
- public class Node
- {
- }
-
- public class SpinBox : Node
- {
- public delegate void ValueChangedEventHandler(double value);
-
- public event ValueChangedEventHandler? ValueChanged
- {
- add {}
- remove {}
- }
- }
- }
-
- namespace TestApp
- {
- public partial class Hud : Node
- {
- private SpinBox _startOreSpinBox = null!;
-
- [{|#0:BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))|}]
- private void OnStartOreValueChanged()
- {
- }
- }
- }
- """;
-
- var test = new CSharpSourceGeneratorTest
- {
- TestState =
- {
- Sources = { source }
- },
- DisabledDiagnostics = { "GF_Common_Trace_001" },
- TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
- };
-
- test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_007", DiagnosticSeverity.Error)
- .WithLocation(0)
- .WithArguments("OnStartOreValueChanged", "ValueChanged", "_startOreSpinBox"));
-
- await test.RunAsync();
+ await VerifyDiagnosticsAsync(
+ source,
+ new DiagnosticResult("GF_Godot_BindNodeSignal_007", DiagnosticSeverity.Error)
+ .WithLocation(0)
+ .WithArguments("OnStartOreValueChanged", "ValueChanged", "_startOreSpinBox")).ConfigureAwait(false);
}
///
@@ -381,73 +244,24 @@ public class BindNodeSignalGeneratorTests
[Test]
public async Task Reports_Diagnostic_When_Constructor_Argument_Is_Empty()
{
- const string source = """
- using System;
- using GFramework.Godot.SourceGenerators.Abstractions;
- using Godot;
+ string source = CreateHudSource(
+ CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
+ """
+ private Button _startButton = null!;
- namespace GFramework.Godot.SourceGenerators.Abstractions
- {
- [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
- public sealed class BindNodeSignalAttribute : Attribute
- {
- public BindNodeSignalAttribute(string nodeFieldName, string signalName)
- {
- NodeFieldName = nodeFieldName;
- SignalName = signalName;
- }
+ [{|#0:BindNodeSignal(nameof(_startButton), "")|}]
+ private void OnStartButtonPressed()
+ {
+ }
+ """,
+ EmptyNodeType,
+ ButtonType);
- public string NodeFieldName { get; }
-
- public string SignalName { get; }
- }
- }
-
- namespace Godot
- {
- public class Node
- {
- }
-
- public class Button : Node
- {
- public event Action? Pressed
- {
- add {}
- remove {}
- }
- }
- }
-
- namespace TestApp
- {
- public partial class Hud : Node
- {
- private Button _startButton = null!;
-
- [{|#0:BindNodeSignal(nameof(_startButton), "")|}]
- private void OnStartButtonPressed()
- {
- }
- }
- }
- """;
-
- var test = new CSharpSourceGeneratorTest
- {
- TestState =
- {
- Sources = { source }
- },
- DisabledDiagnostics = { "GF_Common_Trace_001" },
- TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
- };
-
- test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_010", DiagnosticSeverity.Error)
- .WithLocation(0)
- .WithArguments("OnStartButtonPressed", "signalName"));
-
- await test.RunAsync();
+ await VerifyDiagnosticsAsync(
+ source,
+ new DiagnosticResult("GF_Godot_BindNodeSignal_010", DiagnosticSeverity.Error)
+ .WithLocation(0)
+ .WithArguments("OnStartButtonPressed", "signalName")).ConfigureAwait(false);
}
///
@@ -456,85 +270,35 @@ public class BindNodeSignalGeneratorTests
[Test]
public async Task Reports_Diagnostic_When_Generated_Method_Names_Already_Exist()
{
- const string source = """
- using System;
- using GFramework.Godot.SourceGenerators.Abstractions;
- using Godot;
+ string source = CreateHudSource(
+ CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
+ """
+ private Button _startButton = null!;
- namespace GFramework.Godot.SourceGenerators.Abstractions
- {
- [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
- public sealed class BindNodeSignalAttribute : Attribute
- {
- public BindNodeSignalAttribute(string nodeFieldName, string signalName)
- {
- NodeFieldName = nodeFieldName;
- SignalName = signalName;
- }
+ [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
+ private void OnStartButtonPressed()
+ {
+ }
- public string NodeFieldName { get; }
+ private void {|#0:__BindNodeSignals_Generated|}()
+ {
+ }
- public string SignalName { get; }
- }
- }
+ private void {|#1:__UnbindNodeSignals_Generated|}()
+ {
+ }
+ """,
+ EmptyNodeType,
+ ButtonType);
- namespace Godot
- {
- public class Node
- {
- }
-
- public class Button : Node
- {
- public event Action? Pressed
- {
- add {}
- remove {}
- }
- }
- }
-
- namespace TestApp
- {
- public partial class Hud : Node
- {
- private Button _startButton = null!;
-
- [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
- private void OnStartButtonPressed()
- {
- }
-
- private void {|#0:__BindNodeSignals_Generated|}()
- {
- }
-
- private void {|#1:__UnbindNodeSignals_Generated|}()
- {
- }
- }
- }
- """;
-
- var test = new CSharpSourceGeneratorTest
- {
- TestState =
- {
- Sources = { source }
- },
- DisabledDiagnostics = { "GF_Common_Trace_001" },
- TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
- };
-
- test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error)
- .WithLocation(0)
- .WithArguments("Hud", "__BindNodeSignals_Generated"));
-
- test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error)
- .WithLocation(1)
- .WithArguments("Hud", "__UnbindNodeSignals_Generated"));
-
- await test.RunAsync();
+ await VerifyDiagnosticsAsync(
+ source,
+ new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error)
+ .WithLocation(0)
+ .WithArguments("Hud", "__BindNodeSignals_Generated"),
+ new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error)
+ .WithLocation(1)
+ .WithArguments("Hud", "__UnbindNodeSignals_Generated")).ConfigureAwait(false);
}
///
@@ -543,69 +307,80 @@ public class BindNodeSignalGeneratorTests
[Test]
public async Task Reports_Warnings_When_Lifecycle_Methods_Do_Not_Call_Generated_Methods()
{
- const string source = """
- using System;
- using GFramework.Godot.SourceGenerators.Abstractions;
- using Godot;
+ string source = CreateHudSource(
+ CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
+ """
+ private Button _startButton = null!;
- namespace GFramework.Godot.SourceGenerators.Abstractions
- {
- [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
- public sealed class BindNodeSignalAttribute : Attribute
- {
- public BindNodeSignalAttribute(string nodeFieldName, string signalName)
- {
- NodeFieldName = nodeFieldName;
- SignalName = signalName;
- }
+ [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
+ private void OnStartButtonPressed()
+ {
+ }
- public string NodeFieldName { get; }
+ public override void {|#0:_Ready|}()
+ {
+ }
- public string SignalName { get; }
- }
- }
+ public override void {|#1:_ExitTree|}()
+ {
+ }
+ """,
+ LifecycleNodeType,
+ ButtonType);
- namespace Godot
- {
- public class Node
- {
- public virtual void _Ready() {}
+ await VerifyDiagnosticsAsync(
+ source,
+ new DiagnosticResult("GF_Godot_BindNodeSignal_008", DiagnosticSeverity.Warning)
+ .WithLocation(0)
+ .WithArguments("Hud"),
+ new DiagnosticResult("GF_Godot_BindNodeSignal_009", DiagnosticSeverity.Warning)
+ .WithLocation(1)
+ .WithArguments("Hud")).ConfigureAwait(false);
+ }
- public virtual void _ExitTree() {}
- }
+ private static string CreateAbstractionsSource(params string[] attributeDeclarations)
+ {
+ string declarations = string.Join($"{Environment.NewLine}{Environment.NewLine}", attributeDeclarations);
- public class Button : Node
- {
- public event Action? Pressed
- {
- add {}
- remove {}
- }
- }
- }
+ return $$"""
+ namespace GFramework.Godot.SourceGenerators.Abstractions
+ {
+ {{declarations}}
+ }
+ """;
+ }
- namespace TestApp
- {
- public partial class Hud : Node
- {
- private Button _startButton = null!;
+ private static string CreateHudSource(
+ string abstractionsSource,
+ string hudMembers,
+ params string[] godotTypes)
+ {
+ string godotSource = string.Join($"{Environment.NewLine}{Environment.NewLine}", godotTypes);
- [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
- private void OnStartButtonPressed()
- {
- }
+ return $$"""
+ using System;
+ using GFramework.Godot.SourceGenerators.Abstractions;
+ using Godot;
- public override void {|#0:_Ready|}()
- {
- }
+ {{abstractionsSource}}
- public override void {|#1:_ExitTree|}()
- {
- }
- }
- }
- """;
+ namespace Godot
+ {
+ {{godotSource}}
+ }
+ namespace TestApp
+ {
+ public partial class Hud : Node
+ {
+ {{hudMembers}}
+ }
+ }
+ """;
+ }
+
+ private static Task VerifyDiagnosticsAsync(string source, params DiagnosticResult[] expectedDiagnostics)
+ {
var test = new CSharpSourceGeneratorTest
{
TestState =
@@ -616,14 +391,11 @@ public class BindNodeSignalGeneratorTests
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
};
- test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_008", DiagnosticSeverity.Warning)
- .WithLocation(0)
- .WithArguments("Hud"));
+ foreach (DiagnosticResult expectedDiagnostic in expectedDiagnostics)
+ {
+ test.ExpectedDiagnostics.Add(expectedDiagnostic);
+ }
- test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_009", DiagnosticSeverity.Warning)
- .WithLocation(1)
- .WithArguments("Hud"));
-
- await test.RunAsync();
+ return test.RunAsync();
}
-}
\ No newline at end of file
+}
diff --git a/GFramework.Godot.SourceGenerators.Tests/Core/GeneratorTest.cs b/GFramework.Godot.SourceGenerators.Tests/Core/GeneratorTest.cs
index cca85465..c5cb4608 100644
--- a/GFramework.Godot.SourceGenerators.Tests/Core/GeneratorTest.cs
+++ b/GFramework.Godot.SourceGenerators.Tests/Core/GeneratorTest.cs
@@ -29,7 +29,7 @@ public static class GeneratorTest
test.TestState.GeneratedSources.Add(
(typeof(TGenerator), filename, NormalizeLineEndings(content)));
- await test.RunAsync();
+ await test.RunAsync().ConfigureAwait(false);
}
///
@@ -44,4 +44,4 @@ public static class GeneratorTest
.Replace("\r", "\n", StringComparison.Ordinal)
.Replace("\n", Environment.NewLine, StringComparison.Ordinal);
}
-}
\ No newline at end of file
+}
diff --git a/GFramework.Godot.SourceGenerators.Tests/GetNode/GetNodeGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/GetNode/GetNodeGeneratorTests.cs
index 266ff983..d6e7cd5e 100644
--- a/GFramework.Godot.SourceGenerators.Tests/GetNode/GetNodeGeneratorTests.cs
+++ b/GFramework.Godot.SourceGenerators.Tests/GetNode/GetNodeGeneratorTests.cs
@@ -5,61 +5,88 @@ namespace GFramework.Godot.SourceGenerators.Tests.GetNode;
[TestFixture]
public class GetNodeGeneratorTests
{
+ private const string FullGetNodeAttributeDeclaration = """
+ [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
+ public sealed class GetNodeAttribute : Attribute
+ {
+ public GetNodeAttribute() {}
+ public GetNodeAttribute(string path) { Path = path; }
+ public string? Path { get; set; }
+ public bool Required { get; set; } = true;
+ public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto;
+ }
+
+ public enum NodeLookupMode
+ {
+ Auto = 0,
+ UniqueName = 1,
+ RelativePath = 2,
+ AbsolutePath = 3
+ }
+ """;
+
+ private const string MinimalGetNodeAttributeDeclaration = """
+ [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
+ public sealed class GetNodeAttribute : Attribute
+ {
+ public GetNodeAttribute() {}
+ }
+
+ public enum NodeLookupMode
+ {
+ Auto = 0
+ }
+ """;
+
+ private const string PropertyOnlyGetNodeAttributeDeclaration = """
+ [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
+ public sealed class GetNodeAttribute : Attribute
+ {
+ public string? Path { get; set; }
+ public bool Required { get; set; } = true;
+ public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto;
+ }
+
+ public enum NodeLookupMode
+ {
+ Auto = 0,
+ UniqueName = 1,
+ RelativePath = 2,
+ AbsolutePath = 3
+ }
+ """;
+
+ private const string NodeWithReadyAndLookupMethods = """
+ public class Node
+ {
+ public virtual void _Ready() {}
+ public T GetNode(string path) where T : Node => throw new InvalidOperationException(path);
+ public T? GetNodeOrNull(string path) where T : Node => default;
+ }
+ """;
+
+ private const string HBoxContainerType = """
+ public class HBoxContainer : Node
+ {
+ }
+ """;
+
[Test]
public async Task Generates_InferredUniqueNameBindings_And_ReadyHook_WhenReadyIsMissing()
{
- const string source = """
- using System;
- using GFramework.Godot.SourceGenerators.Abstractions;
- using Godot;
+ string source = CreateGetNodeSource(
+ FullGetNodeAttributeDeclaration,
+ """
+ public partial class TopBar : HBoxContainer
+ {
+ [GetNode]
+ private HBoxContainer _leftContainer = null!;
- namespace GFramework.Godot.SourceGenerators.Abstractions
- {
- [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
- public sealed class GetNodeAttribute : Attribute
- {
- public GetNodeAttribute() {}
- public GetNodeAttribute(string path) { Path = path; }
- public string? Path { get; set; }
- public bool Required { get; set; } = true;
- public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto;
- }
-
- public enum NodeLookupMode
- {
- Auto = 0,
- UniqueName = 1,
- RelativePath = 2,
- AbsolutePath = 3
- }
- }
-
- namespace Godot
- {
- public class Node
- {
- public virtual void _Ready() {}
- public T GetNode(string path) where T : Node => throw new InvalidOperationException(path);
- public T? GetNodeOrNull(string path) where T : Node => default;
- }
-
- public class HBoxContainer : Node
- {
- }
- }
-
- namespace TestApp
- {
- public partial class TopBar : HBoxContainer
- {
- [GetNode]
- private HBoxContainer _leftContainer = null!;
-
- [GetNode]
- private HBoxContainer m_rightContainer = null!;
- }
- }
- """;
+ [GetNode]
+ private HBoxContainer m_rightContainer = null!;
+ }
+ """,
+ HBoxContainerType);
const string expected = """
//
@@ -88,69 +115,30 @@ public class GetNodeGeneratorTests
await GeneratorTest.RunAsync(
source,
- ("TestApp_TopBar.GetNode.g.cs", expected));
+ ("TestApp_TopBar.GetNode.g.cs", expected)).ConfigureAwait(false);
}
[Test]
public async Task Generates_ManualInjectionOnly_WhenReadyAlreadyExists()
{
- const string source = """
- using System;
- using GFramework.Godot.SourceGenerators.Abstractions;
- using Godot;
+ string source = CreateGetNodeSource(
+ FullGetNodeAttributeDeclaration,
+ """
+ public partial class TopBar : HBoxContainer
+ {
+ [GetNode("%LeftContainer")]
+ private HBoxContainer _leftContainer = null!;
- namespace GFramework.Godot.SourceGenerators.Abstractions
- {
- [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
- public sealed class GetNodeAttribute : Attribute
- {
- public GetNodeAttribute() {}
- public GetNodeAttribute(string path) { Path = path; }
- public string? Path { get; set; }
- public bool Required { get; set; } = true;
- public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto;
- }
+ [GetNode(Required = false, Lookup = NodeLookupMode.RelativePath)]
+ private HBoxContainer? _rightContainer;
- public enum NodeLookupMode
- {
- Auto = 0,
- UniqueName = 1,
- RelativePath = 2,
- AbsolutePath = 3
- }
- }
-
- namespace Godot
- {
- public class Node
- {
- public virtual void _Ready() {}
- public T GetNode(string path) where T : Node => throw new InvalidOperationException(path);
- public T? GetNodeOrNull(string path) where T : Node => default;
- }
-
- public class HBoxContainer : Node
- {
- }
- }
-
- namespace TestApp
- {
- public partial class TopBar : HBoxContainer
- {
- [GetNode("%LeftContainer")]
- private HBoxContainer _leftContainer = null!;
-
- [GetNode(Required = false, Lookup = NodeLookupMode.RelativePath)]
- private HBoxContainer? _rightContainer;
-
- public override void _Ready()
- {
- __InjectGetNodes_Generated();
- }
- }
- }
- """;
+ public override void _Ready()
+ {
+ __InjectGetNodes_Generated();
+ }
+ }
+ """,
+ HBoxContainerType);
const string expected = """
//
@@ -171,7 +159,7 @@ public class GetNodeGeneratorTests
await GeneratorTest.RunAsync(
source,
- ("TestApp_TopBar.GetNode.g.cs", expected));
+ ("TestApp_TopBar.GetNode.g.cs", expected)).ConfigureAwait(false);
}
[Test]
@@ -234,58 +222,26 @@ public class GetNodeGeneratorTests
.WithSpan(39, 24, 39, 38)
.WithArguments("_leftContainer"));
- await test.RunAsync();
+ await test.RunAsync().ConfigureAwait(false);
}
[Test]
public async Task Reports_Diagnostic_When_Generated_Injection_Method_Name_Already_Exists()
{
- const string source = """
- using System;
- using GFramework.Godot.SourceGenerators.Abstractions;
- using Godot;
+ string source = CreateGetNodeSource(
+ MinimalGetNodeAttributeDeclaration,
+ """
+ public partial class TopBar : HBoxContainer
+ {
+ [GetNode]
+ private HBoxContainer _leftContainer = null!;
- namespace GFramework.Godot.SourceGenerators.Abstractions
- {
- [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
- public sealed class GetNodeAttribute : Attribute
- {
- public GetNodeAttribute() {}
- }
-
- public enum NodeLookupMode
- {
- Auto = 0
- }
- }
-
- namespace Godot
- {
- public class Node
- {
- public virtual void _Ready() {}
- public T GetNode(string path) where T : Node => throw new InvalidOperationException(path);
- public T? GetNodeOrNull(string path) where T : Node => default;
- }
-
- public class HBoxContainer : Node
- {
- }
- }
-
- namespace TestApp
- {
- public partial class TopBar : HBoxContainer
- {
- [GetNode]
- private HBoxContainer _leftContainer = null!;
-
- private void {|#0:__InjectGetNodes_Generated|}()
- {
- }
- }
- }
- """;
+ private void {|#0:__InjectGetNodes_Generated|}()
+ {
+ }
+ }
+ """,
+ HBoxContainerType);
var test = new CSharpSourceGeneratorTest
{
@@ -301,6 +257,39 @@ public class GetNodeGeneratorTests
.WithLocation(0)
.WithArguments("TopBar", "__InjectGetNodes_Generated"));
- await test.RunAsync();
+ await test.RunAsync().ConfigureAwait(false);
}
-}
\ No newline at end of file
+
+ private static string CreateGetNodeSource(
+ string attributeDeclaration,
+ string testAppSource,
+ params string[] godotTypes)
+ {
+ string[] allGodotTypes = new string[godotTypes.Length + 1];
+ allGodotTypes[0] = NodeWithReadyAndLookupMethods;
+ Array.Copy(godotTypes, 0, allGodotTypes, 1, godotTypes.Length);
+
+ string godotSource = string.Join($"{Environment.NewLine}{Environment.NewLine}", allGodotTypes);
+
+ return $$"""
+ using System;
+ using GFramework.Godot.SourceGenerators.Abstractions;
+ using Godot;
+
+ namespace GFramework.Godot.SourceGenerators.Abstractions
+ {
+ {{attributeDeclaration}}
+ }
+
+ namespace Godot
+ {
+ {{godotSource}}
+ }
+
+ namespace TestApp
+ {
+ {{testAppSource}}
+ }
+ """;
+ }
+}
diff --git a/GFramework.Godot.SourceGenerators.Tests/Project/GodotProjectMetadataGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Project/GodotProjectMetadataGeneratorTests.cs
index b033a865..508273d7 100644
--- a/GFramework.Godot.SourceGenerators.Tests/Project/GodotProjectMetadataGeneratorTests.cs
+++ b/GFramework.Godot.SourceGenerators.Tests/Project/GodotProjectMetadataGeneratorTests.cs
@@ -8,6 +8,131 @@ namespace GFramework.Godot.SourceGenerators.Tests.Project;
[TestFixture]
public class GodotProjectMetadataGeneratorTests
{
+ private const string AutoLoadProjectFile = """
+ [autoload]
+ GameServices="*res://autoload/game_services.tscn"
+ AudioBus="*res://autoload/audio_bus.gd"
+ """;
+
+ private const string InputActionsProjectFile = """
+ [input]
+ move_up={
+ "deadzone": 0.5
+ }
+ ui_cancel={
+ "deadzone": 0.5
+ }
+ """;
+
+ private const string ExpectedAutoLoads = """
+ //
+ #nullable enable
+
+ namespace GFramework.Godot.Generated;
+
+ ///
+ /// 提供 project.godot 中 AutoLoad 单例的强类型访问入口。
+ ///
+ public static partial class AutoLoads
+ {
+ ///
+ /// 获取 AutoLoad GameServices。
+ ///
+ public static global::TestApp.GameServices GameServices => GetRequiredNode("GameServices");
+
+ ///
+ /// 尝试获取 AutoLoad GameServices。
+ ///
+ public static bool TryGetGameServices(out global::TestApp.GameServices? value)
+ {
+ return TryGetNode("GameServices", out value);
+ }
+
+ ///
+ /// 获取 AutoLoad AudioBus。
+ ///
+ public static global::Godot.Node AudioBus => GetRequiredNode("AudioBus");
+
+ ///
+ /// 尝试获取 AutoLoad AudioBus。
+ ///
+ public static bool TryGetAudioBus(out global::Godot.Node? value)
+ {
+ return TryGetNode("AudioBus", out value);
+ }
+
+ ///
+ /// 获取一个必填的 AutoLoad 节点;缺失时抛出异常。
+ ///
+ /// 节点类型。
+ /// AutoLoad 名称。
+ /// 已解析的 AutoLoad 节点。
+ private static TNode GetRequiredNode(string autoLoadName)
+ where TNode : global::Godot.Node
+ {
+ if (TryGetNode(autoLoadName, out TNode? value))
+ {
+ return value!;
+ }
+
+ throw new global::System.InvalidOperationException($"AutoLoad '{autoLoadName}' is not available on the active SceneTree root.");
+ }
+
+ ///
+ /// 尝试从当前 SceneTree 根节点解析 AutoLoad。
+ ///
+ /// 节点类型。
+ /// AutoLoad 名称。
+ /// 解析到的节点实例。
+ /// 若当前进程存在 SceneTree 且根节点中能解析到该 AutoLoad,则返回 true。
+ private static bool TryGetNode(string autoLoadName, out TNode? value)
+ where TNode : global::Godot.Node
+ {
+ value = default;
+
+ if (global::Godot.Engine.GetMainLoop() is not global::Godot.SceneTree sceneTree)
+ {
+ return false;
+ }
+
+ var root = sceneTree.Root;
+ if (root is null)
+ {
+ return false;
+ }
+
+ value = root.GetNodeOrNull($"/root/{autoLoadName}");
+ return value is not null;
+ }
+ }
+
+ """;
+
+ private const string ExpectedInputActions = """
+ //
+ #nullable enable
+
+ namespace GFramework.Godot.Generated;
+
+ ///
+ /// 提供 project.godot 中 Input Action 名称的强类型常量。
+ ///
+ public static partial class InputActions
+ {
+ ///
+ /// Input Action move_up 的稳定名称。
+ ///
+ public const string MoveUp = "move_up";
+
+ ///
+ /// Input Action ui_cancel 的稳定名称。
+ ///
+ public const string UiCancel = "ui_cancel";
+
+ }
+
+ """;
+
///
/// 验证会根据 AutoLoad 与 Input Action 生成稳定的强类型入口。
///
@@ -29,142 +154,19 @@ public class GodotProjectMetadataGeneratorTests
""",
includeAutoLoadAttribute: true);
- const string projectFile = """
- [autoload]
- GameServices="*res://autoload/game_services.tscn"
- AudioBus="*res://autoload/audio_bus.gd"
-
- [input]
- move_up={
- "deadzone": 0.5
- }
- ui_cancel={
- "deadzone": 0.5
- }
- """;
-
- const string expectedAutoLoads = """
- //
- #nullable enable
-
- namespace GFramework.Godot.Generated;
-
- ///
- /// 提供 project.godot 中 AutoLoad 单例的强类型访问入口。
- ///
- public static partial class AutoLoads
- {
- ///
- /// 获取 AutoLoad GameServices。
- ///
- public static global::TestApp.GameServices GameServices => GetRequiredNode("GameServices");
-
- ///
- /// 尝试获取 AutoLoad GameServices。
- ///
- public static bool TryGetGameServices(out global::TestApp.GameServices? value)
- {
- return TryGetNode("GameServices", out value);
- }
-
- ///
- /// 获取 AutoLoad AudioBus。
- ///
- public static global::Godot.Node AudioBus => GetRequiredNode("AudioBus");
-
- ///
- /// 尝试获取 AutoLoad AudioBus。
- ///
- public static bool TryGetAudioBus(out global::Godot.Node? value)
- {
- return TryGetNode("AudioBus", out value);
- }
-
- ///
- /// 获取一个必填的 AutoLoad 节点;缺失时抛出异常。
- ///
- /// 节点类型。
- /// AutoLoad 名称。
- /// 已解析的 AutoLoad 节点。
- private static TNode GetRequiredNode(string autoLoadName)
- where TNode : global::Godot.Node
- {
- if (TryGetNode(autoLoadName, out TNode? value))
- {
- return value!;
- }
-
- throw new global::System.InvalidOperationException($"AutoLoad '{autoLoadName}' is not available on the active SceneTree root.");
- }
-
- ///
- /// 尝试从当前 SceneTree 根节点解析 AutoLoad。
- ///
- /// 节点类型。
- /// AutoLoad 名称。
- /// 解析到的节点实例。
- /// 若当前进程存在 SceneTree 且根节点中能解析到该 AutoLoad,则返回 true。
- private static bool TryGetNode(string autoLoadName, out TNode? value)
- where TNode : global::Godot.Node
- {
- value = default;
-
- if (global::Godot.Engine.GetMainLoop() is not global::Godot.SceneTree sceneTree)
- {
- return false;
- }
-
- var root = sceneTree.Root;
- if (root is null)
- {
- return false;
- }
-
- value = root.GetNodeOrNull($"/root/{autoLoadName}");
- return value is not null;
- }
- }
-
- """;
-
- const string expectedInputActions = """
- //
- #nullable enable
-
- namespace GFramework.Godot.Generated;
-
- ///
- /// 提供 project.godot 中 Input Action 名称的强类型常量。
- ///
- public static partial class InputActions
- {
- ///
- /// Input Action move_up 的稳定名称。
- ///
- public const string MoveUp = "move_up";
-
- ///
- /// Input Action ui_cancel 的稳定名称。
- ///
- public const string UiCancel = "ui_cancel";
-
- }
-
- """;
-
var result = AdditionalTextGeneratorTestDriver.Run(
source,
- ("project.godot", projectFile));
+ ("project.godot", $"{AutoLoadProjectFile}\n\n{InputActionsProjectFile}"));
var generatedSources = AdditionalTextGeneratorTestDriver.ToGeneratedSourceMap(result);
Assert.That(result.Results.Single().Diagnostics, Is.Empty);
Assert.That(
generatedSources["GFramework_Godot_Generated_AutoLoads.g.cs"],
- Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(expectedAutoLoads)));
+ Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(ExpectedAutoLoads)));
Assert.That(
generatedSources["GFramework_Godot_Generated_InputActions.g.cs"],
- Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(expectedInputActions)));
+ Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(ExpectedInputActions)));
}
///
diff --git a/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs
index befdcf59..ded1bb0b 100644
--- a/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs
+++ b/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs
@@ -6,48 +6,52 @@ namespace GFramework.Godot.SourceGenerators.Tests.Registration;
[TestFixture]
public class AutoRegisterExportedCollectionsGeneratorTests
{
+ private const string StandardAttributeDeclarations = """
+ [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
+ public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
+
+ [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
+ public sealed class RegisterExportedCollectionAttribute : Attribute
+ {
+ public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
+ }
+ """;
+
+ private const string MultiDeclarationAttributeDeclarations = """
+ [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
+ public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
+
+ [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
+ public sealed class RegisterExportedCollectionAttribute : Attribute
+ {
+ public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
+ }
+ """;
+
[Test]
public async Task Generates_Batch_Registration_Method_For_Annotated_Collections()
{
- const string source = """
- #nullable enable
- using System;
- using System.Collections.Generic;
- using GFramework.Godot.SourceGenerators.Abstractions.UI;
+ string source = CreateSource(
+ """
+ public sealed class IntRegistry
+ {
+ public void Register(int value) { }
+ }
- namespace GFramework.Godot.SourceGenerators.Abstractions.UI
- {
- [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
- public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
+ [AutoRegisterExportedCollections]
+ public partial class Bootstrapper
+ where TReference : class?
+ where TNotNull : notnull
+ where TValue : struct
+ where TUnmanaged : unmanaged
+ {
+ private readonly IntRegistry? _registry = new();
- [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
- public sealed class RegisterExportedCollectionAttribute : Attribute
- {
- public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
- }
- }
-
- namespace TestApp
- {
- public sealed class IntRegistry
- {
- public void Register(int value) { }
- }
-
- [AutoRegisterExportedCollections]
- public partial class Bootstrapper
- where TReference : class?
- where TNotNull : notnull
- where TValue : struct
- where TUnmanaged : unmanaged
- {
- private readonly IntRegistry? _registry = new();
-
- [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
- public List? Values { get; } = new();
- }
- }
- """;
+ [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
+ public List? Values { get; } = new();
+ }
+ """,
+ nullableEnabled: true);
const string expected = """
//
@@ -77,7 +81,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests
await GeneratorTest.RunAsync(
source,
- ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
+ ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
}
[Test]
@@ -131,47 +135,29 @@ public class AutoRegisterExportedCollectionsGeneratorTests
.WithLocation(0)
.WithArguments("Values"));
- await test.RunAsync();
+ await test.RunAsync().ConfigureAwait(false);
}
[Test]
public async Task Generates_Batch_Registration_Method_When_Register_Method_Uses_Array_Parameter()
{
- const string source = """
- #nullable enable
- using System;
- using System.Collections.Generic;
- using GFramework.Godot.SourceGenerators.Abstractions.UI;
+ string source = CreateSource(
+ """
+ public sealed class ArrayRegistry
+ {
+ public void Register(int[] value) { }
+ }
- namespace GFramework.Godot.SourceGenerators.Abstractions.UI
- {
- [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
- public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
+ [AutoRegisterExportedCollections]
+ public partial class Bootstrapper
+ {
+ private readonly ArrayRegistry _registry = new();
- [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
- public sealed class RegisterExportedCollectionAttribute : Attribute
- {
- public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
- }
- }
-
- namespace TestApp
- {
- public sealed class ArrayRegistry
- {
- public void Register(int[] value) { }
- }
-
- [AutoRegisterExportedCollections]
- public partial class Bootstrapper
- {
- private readonly ArrayRegistry _registry = new();
-
- [RegisterExportedCollection(nameof(_registry), nameof(ArrayRegistry.Register))]
- public List Values { get; } = new();
- }
- }
- """;
+ [RegisterExportedCollection(nameof(_registry), nameof(ArrayRegistry.Register))]
+ public List Values { get; } = new();
+ }
+ """,
+ nullableEnabled: true);
const string expected = """
//
@@ -197,59 +183,41 @@ public class AutoRegisterExportedCollectionsGeneratorTests
await GeneratorTest.RunAsync(
source,
- ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
+ ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
}
[Test]
public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Inherited_Interface()
{
- const string source = """
- #nullable enable
- using System;
- using System.Collections.Generic;
- using GFramework.Godot.SourceGenerators.Abstractions.UI;
+ string source = CreateSource(
+ """
+ public interface IKeyValue
+ {
+ }
- namespace GFramework.Godot.SourceGenerators.Abstractions.UI
- {
- [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
- public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
+ public interface IRegistry
+ {
+ void Registry(IKeyValue mapping);
+ }
- [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
- public sealed class RegisterExportedCollectionAttribute : Attribute
- {
- public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
- }
- }
+ public interface IAssetRegistry : IRegistry
+ {
+ }
- namespace TestApp
- {
- public interface IKeyValue
- {
- }
+ public sealed class IntConfig : IKeyValue
+ {
+ }
- public interface IRegistry
- {
- void Registry(IKeyValue mapping);
- }
+ [AutoRegisterExportedCollections]
+ public partial class Bootstrapper
+ {
+ private readonly IAssetRegistry? _registry = null;
- public interface IAssetRegistry : IRegistry
- {
- }
-
- public sealed class IntConfig : IKeyValue
- {
- }
-
- [AutoRegisterExportedCollections]
- public partial class Bootstrapper
- {
- private readonly IAssetRegistry? _registry = null;
-
- [RegisterExportedCollection(nameof(_registry), "Registry")]
- public List? Values { get; } = new();
- }
- }
- """;
+ [RegisterExportedCollection(nameof(_registry), "Registry")]
+ public List? Values { get; } = new();
+ }
+ """,
+ nullableEnabled: true);
const string expected = """
//
@@ -275,7 +243,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests
await GeneratorTest.RunAsync(
source,
- ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
+ ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
}
[Test]
@@ -334,51 +302,33 @@ public class AutoRegisterExportedCollectionsGeneratorTests
.WithLocation(0)
.WithArguments("Register", "_registry", "Values"));
- await test.RunAsync();
+ await test.RunAsync().ConfigureAwait(false);
}
[Test]
public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Base_Class()
{
- const string source = """
- #nullable enable
- using System;
- using System.Collections.Generic;
- using GFramework.Godot.SourceGenerators.Abstractions.UI;
+ string source = CreateSource(
+ """
+ public class BaseRegistry
+ {
+ public void Register(int value) { }
+ }
- namespace GFramework.Godot.SourceGenerators.Abstractions.UI
- {
- [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
- public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
+ public sealed class DerivedRegistry : BaseRegistry
+ {
+ }
- [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
- public sealed class RegisterExportedCollectionAttribute : Attribute
- {
- public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
- }
- }
+ [AutoRegisterExportedCollections]
+ public partial class Bootstrapper
+ {
+ private readonly DerivedRegistry? _registry = new();
- namespace TestApp
- {
- public class BaseRegistry
- {
- public void Register(int value) { }
- }
-
- public sealed class DerivedRegistry : BaseRegistry
- {
- }
-
- [AutoRegisterExportedCollections]
- public partial class Bootstrapper
- {
- private readonly DerivedRegistry? _registry = new();
-
- [RegisterExportedCollection(nameof(_registry), nameof(BaseRegistry.Register))]
- public List? Values { get; } = new();
- }
- }
- """;
+ [RegisterExportedCollection(nameof(_registry), nameof(BaseRegistry.Register))]
+ public List? Values { get; } = new();
+ }
+ """,
+ nullableEnabled: true);
const string expected = """
//
@@ -404,50 +354,32 @@ public class AutoRegisterExportedCollectionsGeneratorTests
await GeneratorTest.RunAsync(
source,
- ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
+ ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
}
[Test]
public async Task Generates_Batch_Registration_Method_When_Registry_Member_Comes_From_Base_Class()
{
- const string source = """
- #nullable enable
- using System;
- using System.Collections.Generic;
- using GFramework.Godot.SourceGenerators.Abstractions.UI;
+ string source = CreateSource(
+ """
+ public sealed class IntRegistry
+ {
+ public void Register(int value) { }
+ }
- namespace GFramework.Godot.SourceGenerators.Abstractions.UI
- {
- [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
- public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
+ public abstract class BootstrapperBase
+ {
+ protected readonly IntRegistry? _registry = new();
+ }
- [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
- public sealed class RegisterExportedCollectionAttribute : Attribute
- {
- public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
- }
- }
-
- namespace TestApp
- {
- public sealed class IntRegistry
- {
- public void Register(int value) { }
- }
-
- public abstract class BootstrapperBase
- {
- protected readonly IntRegistry? _registry = new();
- }
-
- [AutoRegisterExportedCollections]
- public partial class Bootstrapper : BootstrapperBase
- {
- [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
- public List? Values { get; } = new();
- }
- }
- """;
+ [AutoRegisterExportedCollections]
+ public partial class Bootstrapper : BootstrapperBase
+ {
+ [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
+ public List? Values { get; } = new();
+ }
+ """,
+ nullableEnabled: true);
const string expected = """
//
@@ -473,74 +405,47 @@ public class AutoRegisterExportedCollectionsGeneratorTests
await GeneratorTest.RunAsync(
source,
- ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
+ ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
}
[Test]
public async Task Reports_Diagnostic_When_Collection_Member_Is_Not_Instance_Readable()
{
- const string source = """
- using System;
- using System.Collections.Generic;
- using GFramework.Godot.SourceGenerators.Abstractions.UI;
+ string source = CreateSource(
+ """
+ public sealed class IntRegistry
+ {
+ public void Register(int value) { }
+ }
- namespace GFramework.Godot.SourceGenerators.Abstractions.UI
- {
- [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
- public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
+ [AutoRegisterExportedCollections]
+ public partial class Bootstrapper
+ {
+ private readonly IntRegistry _registry = new();
- [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
- public sealed class RegisterExportedCollectionAttribute : Attribute
- {
- public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
- }
- }
+ [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
+ public static List {|#0:StaticValues|} = new();
- namespace TestApp
- {
- public sealed class IntRegistry
- {
- public void Register(int value) { }
- }
+ [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
+ public static List {|#1:StaticPropertyValues|} { get; } = new();
- [AutoRegisterExportedCollections]
- public partial class Bootstrapper
- {
- private readonly IntRegistry _registry = new();
+ [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
+ public List {|#2:WriteOnlyValues|} { set { } }
+ }
+ """);
- [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
- public static List {|#0:StaticValues|} = new();
-
- [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
- public static List {|#1:StaticPropertyValues|} { get; } = new();
-
- [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
- public List {|#2:WriteOnlyValues|} { set { } }
- }
- }
- """;
-
- var test = new CSharpSourceGeneratorTest
- {
- TestState =
- {
- Sources = { source }
- },
- DisabledDiagnostics = { "GF_Common_Trace_001" },
- TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
- };
-
- test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
- .WithLocation(0)
- .WithArguments("StaticValues"));
- test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
- .WithLocation(1)
- .WithArguments("StaticPropertyValues"));
- test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
- .WithLocation(2)
- .WithArguments("WriteOnlyValues"));
-
- await test.RunAsync();
+ await VerifyDiagnosticsAsync(
+ source,
+ skipGeneratedSourcesCheck: true,
+ new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
+ .WithLocation(0)
+ .WithArguments("StaticValues"),
+ new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
+ .WithLocation(1)
+ .WithArguments("StaticPropertyValues"),
+ new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
+ .WithLocation(2)
+ .WithArguments("WriteOnlyValues")).ConfigureAwait(false);
}
[Test]
@@ -595,7 +500,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests
.WithLocation(0)
.WithArguments("_registry", "Values"));
- await test.RunAsync();
+ await test.RunAsync().ConfigureAwait(false);
}
[Test]
@@ -650,7 +555,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests
.WithLocation(0)
.WithArguments("Register", "_registry", "Values"));
- await test.RunAsync();
+ await test.RunAsync().ConfigureAwait(false);
}
[Test]
@@ -705,51 +610,34 @@ public class AutoRegisterExportedCollectionsGeneratorTests
.WithLocation(0)
.WithArguments("Values"));
- await test.RunAsync();
+ await test.RunAsync().ConfigureAwait(false);
}
[Test]
public async Task Generates_Only_One_Source_When_Multiple_Partial_Declarations_Are_Annotated()
{
- const string source = """
- #nullable enable
- using System;
- using System.Collections.Generic;
- using GFramework.Godot.SourceGenerators.Abstractions.UI;
+ string source = CreateSource(
+ """
+ public sealed class IntRegistry
+ {
+ public void Register(int value) { }
+ }
- namespace GFramework.Godot.SourceGenerators.Abstractions.UI
- {
- [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
- public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
+ [AutoRegisterExportedCollections]
+ public partial class Bootstrapper
+ {
+ private readonly IntRegistry? _registry = new();
+ }
- [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
- public sealed class RegisterExportedCollectionAttribute : Attribute
- {
- public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
- }
- }
-
- namespace TestApp
- {
- public sealed class IntRegistry
- {
- public void Register(int value) { }
- }
-
- [AutoRegisterExportedCollections]
- public partial class Bootstrapper
- {
- private readonly IntRegistry? _registry = new();
- }
-
- [AutoRegisterExportedCollections]
- public partial class Bootstrapper
- {
- [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
- public List? Values { get; } = new();
- }
- }
- """;
+ [AutoRegisterExportedCollections]
+ public partial class Bootstrapper
+ {
+ [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
+ public List? Values { get; } = new();
+ }
+ """,
+ nullableEnabled: true,
+ allowMultipleDeclarations: true);
const string expected = """
//
@@ -775,6 +663,61 @@ public class AutoRegisterExportedCollectionsGeneratorTests
await GeneratorTest.RunAsync(
source,
- ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
+ ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
+ }
+
+ private static string CreateSource(
+ string applicationSource,
+ bool nullableEnabled = false,
+ bool allowMultipleDeclarations = false)
+ {
+ string nullableDirective = nullableEnabled ? "#nullable enable\n" : string.Empty;
+ string attributeDeclarations = allowMultipleDeclarations
+ ? MultiDeclarationAttributeDeclarations
+ : StandardAttributeDeclarations;
+
+ return $$"""
+ {{nullableDirective}}using System;
+ using System.Collections;
+ using System.Collections.Generic;
+ using GFramework.Godot.SourceGenerators.Abstractions.UI;
+
+ namespace GFramework.Godot.SourceGenerators.Abstractions.UI
+ {
+ {{attributeDeclarations}}
+ }
+
+ namespace TestApp
+ {
+ {{applicationSource}}
+ }
+ """;
+ }
+
+ private static async Task VerifyDiagnosticsAsync(
+ string source,
+ bool skipGeneratedSourcesCheck = false,
+ params DiagnosticResult[] expectedDiagnostics)
+ {
+ var test = new CSharpSourceGeneratorTest
+ {
+ TestState =
+ {
+ Sources = { source }
+ },
+ DisabledDiagnostics = { "GF_Common_Trace_001" }
+ };
+
+ if (skipGeneratedSourcesCheck)
+ {
+ test.TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck;
+ }
+
+ foreach (DiagnosticResult expectedDiagnostic in expectedDiagnostics)
+ {
+ test.ExpectedDiagnostics.Add(expectedDiagnostic);
+ }
+
+ await test.RunAsync().ConfigureAwait(false);
}
}
diff --git a/GFramework.Godot.SourceGenerators/BindNodeSignalGenerator.cs b/GFramework.Godot.SourceGenerators/BindNodeSignalGenerator.cs
index 610662a2..e0e4e3a1 100644
--- a/GFramework.Godot.SourceGenerators/BindNodeSignalGenerator.cs
+++ b/GFramework.Godot.SourceGenerators/BindNodeSignalGenerator.cs
@@ -72,19 +72,8 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
if (bindNodeSignalAttribute is null || godotNodeSymbol is null)
return;
- // 缓存每个方法上已解析的特性,避免在筛选和生成阶段重复做语义查询。
- var methodAttributes = candidates
- .Where(static candidate => candidate is not null)
- .Select(static candidate => candidate!)
- .ToDictionary(
- static candidate => candidate,
- candidate => ResolveAttributes(candidate.MethodSymbol, bindNodeSignalAttribute),
- ReferenceEqualityComparer.Instance);
-
- var methodCandidates = methodAttributes
- .Where(static pair => pair.Value.Count > 0)
- .Select(static pair => pair.Key)
- .ToList();
+ var methodAttributes = BuildMethodAttributeMap(candidates, bindNodeSignalAttribute);
+ var methodCandidates = CollectMethodCandidates(methodAttributes);
foreach (var group in GroupByContainingType(methodCandidates))
{
@@ -99,19 +88,7 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
UnbindMethodName))
continue;
- var bindings = new List();
-
- foreach (var candidate in group.Methods)
- {
- foreach (var attribute in methodAttributes[candidate])
- {
- if (!TryCreateBinding(context, candidate, attribute, godotNodeSymbol, out var binding))
- continue;
-
- bindings.Add(binding);
- }
- }
-
+ var bindings = CollectBindings(context, group, methodAttributes, godotNodeSymbol);
if (bindings.Count == 0)
continue;
@@ -171,99 +148,22 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
if (candidate.MethodSymbol.IsStatic)
{
- ReportMethodDiagnostic(
- context,
- BindNodeSignalDiagnostics.StaticMethodNotSupported,
- candidate,
- attribute,
- candidate.MethodSymbol.Name);
+ ReportStaticMethodDiagnostic(context, candidate, attribute);
return false;
}
- if (!TryResolveCtorString(attribute, 0, out var nodeFieldName))
- {
- ReportMethodDiagnostic(
- context,
- BindNodeSignalDiagnostics.InvalidConstructorArgument,
- candidate,
- attribute,
- candidate.MethodSymbol.Name,
- "nodeFieldName");
+ if (!TryResolveBindingTargetNames(context, candidate, attribute, out var nodeFieldName, out var signalName))
return false;
- }
- if (!TryResolveCtorString(attribute, 1, out var signalName))
- {
- ReportMethodDiagnostic(
- context,
- BindNodeSignalDiagnostics.InvalidConstructorArgument,
- candidate,
- attribute,
- candidate.MethodSymbol.Name,
- "signalName");
+ if (!TryFindCompatibleField(context, candidate, attribute, godotNodeSymbol, nodeFieldName, out var fieldSymbol))
return false;
- }
- var fieldSymbol = FindField(candidate.MethodSymbol.ContainingType, nodeFieldName);
- if (fieldSymbol is null)
- {
- ReportMethodDiagnostic(
- context,
- BindNodeSignalDiagnostics.NodeFieldNotFound,
- candidate,
- attribute,
- candidate.MethodSymbol.Name,
- nodeFieldName,
- candidate.MethodSymbol.ContainingType.Name);
+ if (!TryFindCompatibleEvent(context, candidate, attribute, fieldSymbol, signalName, out var eventSymbol))
return false;
- }
-
- if (fieldSymbol.IsStatic)
- {
- ReportMethodDiagnostic(
- context,
- BindNodeSignalDiagnostics.NodeFieldMustBeInstanceField,
- candidate,
- attribute,
- candidate.MethodSymbol.Name,
- fieldSymbol.Name);
- return false;
- }
-
- if (!fieldSymbol.Type.IsAssignableTo(godotNodeSymbol))
- {
- ReportMethodDiagnostic(
- context,
- BindNodeSignalDiagnostics.FieldTypeMustDeriveFromNode,
- candidate,
- attribute,
- fieldSymbol.Name);
- return false;
- }
-
- var eventSymbol = FindEvent(fieldSymbol.Type, signalName);
- if (eventSymbol is null)
- {
- ReportMethodDiagnostic(
- context,
- BindNodeSignalDiagnostics.SignalNotFound,
- candidate,
- attribute,
- fieldSymbol.Name,
- signalName);
- return false;
- }
if (!IsMethodCompatibleWithEvent(candidate.MethodSymbol, eventSymbol))
{
- ReportMethodDiagnostic(
- context,
- BindNodeSignalDiagnostics.MethodSignatureNotCompatible,
- candidate,
- attribute,
- candidate.MethodSymbol.Name,
- eventSymbol.Name,
- fieldSymbol.Name);
+ ReportIncompatibleSignatureDiagnostic(context, candidate, attribute, eventSymbol, fieldSymbol);
return false;
}
@@ -271,6 +171,235 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
return true;
}
+ private static Dictionary> BuildMethodAttributeMap(
+ ImmutableArray candidates,
+ INamedTypeSymbol bindNodeSignalAttribute)
+ {
+ return candidates
+ .Where(static candidate => candidate is not null)
+ .Select(static candidate => candidate!)
+ .ToDictionary(
+ static candidate => candidate,
+ candidate => ResolveAttributes(candidate.MethodSymbol, bindNodeSignalAttribute),
+ ReferenceEqualityComparer.Instance);
+ }
+
+ private static List CollectMethodCandidates(
+ IReadOnlyDictionary> methodAttributes)
+ {
+ return methodAttributes
+ .Where(static pair => pair.Value.Count > 0)
+ .Select(static pair => pair.Key)
+ .ToList();
+ }
+
+ private static List CollectBindings(
+ SourceProductionContext context,
+ TypeGroup group,
+ IReadOnlyDictionary> methodAttributes,
+ INamedTypeSymbol godotNodeSymbol)
+ {
+ var bindings = new List();
+
+ foreach (var candidate in group.Methods)
+ {
+ foreach (var attribute in methodAttributes[candidate])
+ {
+ if (!TryCreateBinding(context, candidate, attribute, godotNodeSymbol, out var binding))
+ continue;
+
+ bindings.Add(binding);
+ }
+ }
+
+ return bindings;
+ }
+
+ private static void ReportStaticMethodDiagnostic(
+ SourceProductionContext context,
+ MethodCandidate candidate,
+ AttributeData attribute)
+ {
+ ReportMethodDiagnostic(
+ context,
+ BindNodeSignalDiagnostics.StaticMethodNotSupported,
+ candidate,
+ attribute,
+ candidate.MethodSymbol.Name);
+ }
+
+ private static bool TryResolveBindingTargetNames(
+ SourceProductionContext context,
+ MethodCandidate candidate,
+ AttributeData attribute,
+ out string nodeFieldName,
+ out string signalName)
+ {
+ nodeFieldName = string.Empty;
+ signalName = string.Empty;
+
+ if (!TryResolveCtorString(attribute, 0, out nodeFieldName))
+ {
+ ReportInvalidConstructorArgumentDiagnostic(context, candidate, attribute, "nodeFieldName");
+ return false;
+ }
+
+ if (!TryResolveCtorString(attribute, 1, out signalName))
+ {
+ ReportInvalidConstructorArgumentDiagnostic(context, candidate, attribute, "signalName");
+ return false;
+ }
+
+ return true;
+ }
+
+ private static void ReportInvalidConstructorArgumentDiagnostic(
+ SourceProductionContext context,
+ MethodCandidate candidate,
+ AttributeData attribute,
+ string argumentName)
+ {
+ ReportMethodDiagnostic(
+ context,
+ BindNodeSignalDiagnostics.InvalidConstructorArgument,
+ candidate,
+ attribute,
+ candidate.MethodSymbol.Name,
+ argumentName);
+ }
+
+ private static bool TryFindCompatibleField(
+ SourceProductionContext context,
+ MethodCandidate candidate,
+ AttributeData attribute,
+ INamedTypeSymbol godotNodeSymbol,
+ string nodeFieldName,
+ out IFieldSymbol fieldSymbol)
+ {
+ fieldSymbol = null!;
+
+ var resolvedField = FindField(candidate.MethodSymbol.ContainingType, nodeFieldName);
+ if (resolvedField is null)
+ {
+ ReportNodeFieldNotFoundDiagnostic(context, candidate, attribute, nodeFieldName);
+ return false;
+ }
+
+ if (resolvedField.IsStatic)
+ {
+ ReportNodeFieldMustBeInstanceDiagnostic(context, candidate, attribute, resolvedField);
+ return false;
+ }
+
+ if (!resolvedField.Type.IsAssignableTo(godotNodeSymbol))
+ {
+ ReportFieldTypeMustDeriveFromNodeDiagnostic(context, candidate, attribute, resolvedField);
+ return false;
+ }
+
+ fieldSymbol = resolvedField;
+ return true;
+ }
+
+ private static void ReportNodeFieldNotFoundDiagnostic(
+ SourceProductionContext context,
+ MethodCandidate candidate,
+ AttributeData attribute,
+ string nodeFieldName)
+ {
+ ReportMethodDiagnostic(
+ context,
+ BindNodeSignalDiagnostics.NodeFieldNotFound,
+ candidate,
+ attribute,
+ candidate.MethodSymbol.Name,
+ nodeFieldName,
+ candidate.MethodSymbol.ContainingType.Name);
+ }
+
+ private static void ReportNodeFieldMustBeInstanceDiagnostic(
+ SourceProductionContext context,
+ MethodCandidate candidate,
+ AttributeData attribute,
+ IFieldSymbol fieldSymbol)
+ {
+ ReportMethodDiagnostic(
+ context,
+ BindNodeSignalDiagnostics.NodeFieldMustBeInstanceField,
+ candidate,
+ attribute,
+ candidate.MethodSymbol.Name,
+ fieldSymbol.Name);
+ }
+
+ private static void ReportFieldTypeMustDeriveFromNodeDiagnostic(
+ SourceProductionContext context,
+ MethodCandidate candidate,
+ AttributeData attribute,
+ IFieldSymbol fieldSymbol)
+ {
+ ReportMethodDiagnostic(
+ context,
+ BindNodeSignalDiagnostics.FieldTypeMustDeriveFromNode,
+ candidate,
+ attribute,
+ fieldSymbol.Name);
+ }
+
+ private static bool TryFindCompatibleEvent(
+ SourceProductionContext context,
+ MethodCandidate candidate,
+ AttributeData attribute,
+ IFieldSymbol fieldSymbol,
+ string signalName,
+ out IEventSymbol eventSymbol)
+ {
+ eventSymbol = null!;
+
+ var resolvedEvent = FindEvent(fieldSymbol.Type, signalName);
+ if (resolvedEvent is null)
+ {
+ ReportSignalNotFoundDiagnostic(context, candidate, attribute, fieldSymbol, signalName);
+ return false;
+ }
+
+ eventSymbol = resolvedEvent;
+ return true;
+ }
+
+ private static void ReportSignalNotFoundDiagnostic(
+ SourceProductionContext context,
+ MethodCandidate candidate,
+ AttributeData attribute,
+ IFieldSymbol fieldSymbol,
+ string signalName)
+ {
+ ReportMethodDiagnostic(
+ context,
+ BindNodeSignalDiagnostics.SignalNotFound,
+ candidate,
+ attribute,
+ fieldSymbol.Name,
+ signalName);
+ }
+
+ private static void ReportIncompatibleSignatureDiagnostic(
+ SourceProductionContext context,
+ MethodCandidate candidate,
+ AttributeData attribute,
+ IEventSymbol eventSymbol,
+ IFieldSymbol fieldSymbol)
+ {
+ ReportMethodDiagnostic(
+ context,
+ BindNodeSignalDiagnostics.MethodSignatureNotCompatible,
+ candidate,
+ attribute,
+ candidate.MethodSymbol.Name,
+ eventSymbol.Name,
+ fieldSymbol.Name);
+ }
+
private static void ReportMethodDiagnostic(
SourceProductionContext context,
DiagnosticDescriptor descriptor,
@@ -404,11 +533,7 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
{
return typeSymbol.GetMembers()
.OfType()
- .FirstOrDefault(method =>
- method.Name == methodName &&
- !method.IsStatic &&
- method.Parameters.Length == 0 &&
- method.MethodKind == MethodKind.Ordinary);
+ .FirstOrDefault(method => IsParameterlessInstanceMethod(method, methodName));
}
private static bool CallsGeneratedMethod(
@@ -447,6 +572,14 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
};
}
+ private static bool IsParameterlessInstanceMethod(IMethodSymbol method, string methodName)
+ {
+ return string.Equals(method.Name, methodName, StringComparison.Ordinal) &&
+ !method.IsStatic &&
+ method.Parameters.Length == 0 &&
+ method.MethodKind == MethodKind.Ordinary;
+ }
+
private static bool IsBindNodeSignalAttributeName(NameSyntax attributeName)
{
var simpleName = GetAttributeSimpleName(attributeName);
@@ -608,4 +741,4 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
return RuntimeHelpers.GetHashCode(obj);
}
}
-}
\ No newline at end of file
+}
diff --git a/GFramework.Godot.SourceGenerators/GetNodeGenerator.cs b/GFramework.Godot.SourceGenerators/GetNodeGenerator.cs
index 6aa3ac81..7122faab 100644
--- a/GFramework.Godot.SourceGenerators/GetNodeGenerator.cs
+++ b/GFramework.Godot.SourceGenerators/GetNodeGenerator.cs
@@ -259,11 +259,7 @@ public sealed class GetNodeGenerator : IIncrementalGenerator
{
return typeSymbol.GetMembers()
.OfType()
- .FirstOrDefault(static method =>
- method.Name == "_Ready" &&
- !method.IsStatic &&
- method.Parameters.Length == 0 &&
- method.MethodKind == MethodKind.Ordinary);
+ .FirstOrDefault(static method => IsParameterlessInstanceMethod(method, "_Ready"));
}
private static bool CallsGeneratedInjection(IMethodSymbol readyMethod)
@@ -306,6 +302,14 @@ public sealed class GetNodeGenerator : IIncrementalGenerator
return attribute.GetNamedArgument("Required", true);
}
+ private static bool IsParameterlessInstanceMethod(IMethodSymbol method, string methodName)
+ {
+ return string.Equals(method.Name, methodName, StringComparison.Ordinal) &&
+ !method.IsStatic &&
+ method.Parameters.Length == 0 &&
+ method.MethodKind == MethodKind.Ordinary;
+ }
+
private static bool TryResolvePath(
IFieldSymbol fieldSymbol,
AttributeData attribute,
@@ -373,7 +377,10 @@ public sealed class GetNodeGenerator : IIncrementalGenerator
if (!string.Equals(namedArgument.Key, "Lookup", StringComparison.Ordinal))
continue;
- if (namedArgument.Value.Type?.ToDisplayString() != GetNodeLookupModeMetadataName)
+ if (!string.Equals(
+ namedArgument.Value.Type?.ToDisplayString(),
+ GetNodeLookupModeMetadataName,
+ StringComparison.Ordinal))
continue;
if (namedArgument.Value.Value is int value)
@@ -568,4 +575,4 @@ public sealed class GetNodeGenerator : IIncrementalGenerator
public List Fields { get; } = new();
}
-}
\ No newline at end of file
+}
diff --git a/GFramework.Godot.SourceGenerators/GodotProjectMetadataGenerator.cs b/GFramework.Godot.SourceGenerators/GodotProjectMetadataGenerator.cs
index c080cc71..400c2f31 100644
--- a/GFramework.Godot.SourceGenerators/GodotProjectMetadataGenerator.cs
+++ b/GFramework.Godot.SourceGenerators/GodotProjectMetadataGenerator.cs
@@ -126,7 +126,27 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator
var explicitMappings = new Dictionary>(StringComparer.Ordinal);
var implicitCandidates = new Dictionary>(StringComparer.Ordinal);
+ CollectMappingCandidates(
+ context,
+ typeCandidates,
+ autoLoadAttributeSymbol,
+ godotNodeSymbol,
+ projectAutoLoadNames,
+ explicitMappings,
+ implicitCandidates);
+ return ResolveTypedMappings(context, projectAutoLoadNames, explicitMappings, implicitCandidates);
+ }
+
+ private static void CollectMappingCandidates(
+ SourceProductionContext context,
+ IReadOnlyList typeCandidates,
+ INamedTypeSymbol? autoLoadAttributeSymbol,
+ INamedTypeSymbol godotNodeSymbol,
+ ISet projectAutoLoadNames,
+ IDictionary> explicitMappings,
+ IDictionary> implicitCandidates)
+ {
foreach (var candidate in typeCandidates)
{
var typeSymbol = candidate.TypeSymbol;
@@ -176,7 +196,14 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator
explicitList.Add(typeSymbol);
}
+ }
+ private static Dictionary ResolveTypedMappings(
+ SourceProductionContext context,
+ IEnumerable projectAutoLoadNames,
+ IReadOnlyDictionary> explicitMappings,
+ IReadOnlyDictionary> implicitCandidates)
+ {
var resolvedMappings = new Dictionary(StringComparer.Ordinal);
foreach (var projectAutoLoadName in projectAutoLoadNames.OrderBy(static name => name, StringComparer.Ordinal))
@@ -408,24 +435,40 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator
foreach (var member in members)
{
- builder.AppendLine(" /// ");
- builder.AppendLine($" /// 获取 AutoLoad {member.AutoLoadName}。");
- builder.AppendLine(" /// ");
- builder.AppendLine(
- $" public static {member.TypeName} {member.Identifier} => GetRequiredNode<{member.TypeName}>({SymbolDisplay.FormatLiteral(member.AutoLoadName, true)});");
- builder.AppendLine();
- builder.AppendLine(" /// ");
- builder.AppendLine($" /// 尝试获取 AutoLoad {member.AutoLoadName}。");
- builder.AppendLine(" /// ");
- builder.AppendLine(
- $" public static bool TryGet{member.Identifier}(out {member.TypeName}? value)");
- builder.AppendLine(" {");
- builder.AppendLine(
- $" return TryGetNode({SymbolDisplay.FormatLiteral(member.AutoLoadName, true)}, out value);");
- builder.AppendLine(" }");
- builder.AppendLine();
+ AppendAutoLoadMemberSource(builder, member);
}
+ AppendGetRequiredNodeSource(builder);
+ AppendTryGetNodeSource(builder);
+ builder.AppendLine("}");
+
+ return builder.ToString();
+ }
+
+ private static void AppendAutoLoadMemberSource(
+ StringBuilder builder,
+ GeneratedAutoLoadMember member)
+ {
+ builder.AppendLine(" /// ");
+ builder.AppendLine($" /// 获取 AutoLoad {member.AutoLoadName}。");
+ builder.AppendLine(" /// ");
+ builder.AppendLine(
+ $" public static {member.TypeName} {member.Identifier} => GetRequiredNode<{member.TypeName}>({SymbolDisplay.FormatLiteral(member.AutoLoadName, true)});");
+ builder.AppendLine();
+ builder.AppendLine(" /// ");
+ builder.AppendLine($" /// 尝试获取 AutoLoad {member.AutoLoadName}。");
+ builder.AppendLine(" /// ");
+ builder.AppendLine(
+ $" public static bool TryGet{member.Identifier}(out {member.TypeName}? value)");
+ builder.AppendLine(" {");
+ builder.AppendLine(
+ $" return TryGetNode({SymbolDisplay.FormatLiteral(member.AutoLoadName, true)}, out value);");
+ builder.AppendLine(" }");
+ builder.AppendLine();
+ }
+
+ private static void AppendGetRequiredNodeSource(StringBuilder builder)
+ {
builder.AppendLine(" /// ");
builder.AppendLine(" /// 获取一个必填的 AutoLoad 节点;缺失时抛出异常。");
builder.AppendLine(" /// ");
@@ -444,6 +487,10 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator
" throw new global::System.InvalidOperationException($\"AutoLoad '{autoLoadName}' is not available on the active SceneTree root.\");");
builder.AppendLine(" }");
builder.AppendLine();
+ }
+
+ private static void AppendTryGetNodeSource(StringBuilder builder)
+ {
builder.AppendLine(" /// ");
builder.AppendLine(" /// 尝试从当前 SceneTree 根节点解析 AutoLoad。");
builder.AppendLine(" /// ");
@@ -470,9 +517,6 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator
builder.AppendLine(" value = root.GetNodeOrNull($\"/root/{autoLoadName}\");");
builder.AppendLine(" return value is not null;");
builder.AppendLine(" }");
- builder.AppendLine("}");
-
- return builder.ToString();
}
private static string GenerateInputActionsSource(IReadOnlyList members)
@@ -530,45 +574,16 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator
if (string.IsNullOrWhiteSpace(content) || content.StartsWith(";", StringComparison.Ordinal))
continue;
- if (content.StartsWith("[", StringComparison.Ordinal) && content.EndsWith("]", StringComparison.Ordinal))
- {
- currentSection = content.Substring(1, content.Length - 2).Trim();
+ if (TryUpdateSection(content, ref currentSection))
continue;
- }
if (!TryParseAssignment(content, out var key, out var value))
continue;
- if (string.Equals(currentSection, "autoload", StringComparison.OrdinalIgnoreCase))
- {
- if (!seenAutoLoads.Add(key))
- {
- diagnostics.Add(Diagnostic.Create(
- GodotProjectDiagnostics.DuplicateAutoLoadEntry,
- CreateFileLocation(file.Path),
- key));
- continue;
- }
-
- autoLoads.Add(new ProjectAutoLoadEntry(
- key,
- NormalizeProjectPath(value)));
+ if (TryCollectAutoLoadEntry(file, currentSection, key, value, seenAutoLoads, autoLoads, diagnostics))
continue;
- }
- if (string.Equals(currentSection, "input", StringComparison.OrdinalIgnoreCase))
- {
- if (!seenInputActions.Add(key))
- {
- diagnostics.Add(Diagnostic.Create(
- GodotProjectDiagnostics.DuplicateInputActionEntry,
- CreateFileLocation(file.Path),
- key));
- continue;
- }
-
- inputActions.Add(key);
- }
+ TryCollectInputAction(currentSection, key, seenInputActions, inputActions, diagnostics, file.Path);
}
return new ProjectMetadataParseResult(
@@ -578,6 +593,68 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator
diagnostics.ToImmutableArray());
}
+ private static bool TryUpdateSection(string content, ref string currentSection)
+ {
+ if (!content.StartsWith("[", StringComparison.Ordinal) ||
+ !content.EndsWith("]", StringComparison.Ordinal))
+ {
+ return false;
+ }
+
+ currentSection = content.Substring(1, content.Length - 2).Trim();
+ return true;
+ }
+
+ private static bool TryCollectAutoLoadEntry(
+ AdditionalText file,
+ string currentSection,
+ string key,
+ string value,
+ ISet seenAutoLoads,
+ ICollection autoLoads,
+ ICollection diagnostics)
+ {
+ if (!string.Equals(currentSection, "autoload", StringComparison.OrdinalIgnoreCase))
+ return false;
+
+ if (!seenAutoLoads.Add(key))
+ {
+ diagnostics.Add(Diagnostic.Create(
+ GodotProjectDiagnostics.DuplicateAutoLoadEntry,
+ CreateFileLocation(file.Path),
+ key));
+ return true;
+ }
+
+ autoLoads.Add(new ProjectAutoLoadEntry(
+ key,
+ NormalizeProjectPath(value)));
+ return true;
+ }
+
+ private static void TryCollectInputAction(
+ string currentSection,
+ string key,
+ ISet seenInputActions,
+ ICollection inputActions,
+ ICollection diagnostics,
+ string filePath)
+ {
+ if (!string.Equals(currentSection, "input", StringComparison.OrdinalIgnoreCase))
+ return;
+
+ if (!seenInputActions.Add(key))
+ {
+ diagnostics.Add(Diagnostic.Create(
+ GodotProjectDiagnostics.DuplicateInputActionEntry,
+ CreateFileLocation(filePath),
+ key));
+ return;
+ }
+
+ inputActions.Add(key);
+ }
+
private static string NormalizeProjectPath(string rawValue)
{
var trimmed = rawValue.Trim();
diff --git a/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs b/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs
index 93219dcb..23dc1109 100644
--- a/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs
+++ b/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs
@@ -190,6 +190,48 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
{
registration = null!;
+ if (!TryResolveCollectionType(context, collectionMember, enumerableType, out var collectionType))
+ return false;
+
+ if (!TryResolveRegistryTarget(
+ context,
+ compilation,
+ ownerType,
+ collectionMember,
+ attribute,
+ out var registryMemberName,
+ out var registerMethodName,
+ out var registryType))
+ {
+ return false;
+ }
+
+ if (!TryResolveElementType(context, collectionMember, collectionType, out var elementType))
+ return false;
+
+ if (!HasCompatibleRegisterMethod(compilation, ownerType, registryType, registerMethodName, elementType))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ AutoRegisterExportedCollectionsDiagnostics.RegisterMethodNotFound,
+ collectionMember.Locations.FirstOrDefault() ?? Location.None,
+ registerMethodName,
+ registryMemberName,
+ collectionMember.Name));
+ return false;
+ }
+
+ registration = new RegistrationSpec(collectionMember.Name, registryMemberName, registerMethodName);
+ return true;
+ }
+
+ private static bool TryResolveCollectionType(
+ SourceProductionContext context,
+ ISymbol collectionMember,
+ INamedTypeSymbol enumerableType,
+ out ITypeSymbol collectionType)
+ {
+ collectionType = null!;
+
if (!IsInstanceReadableMember(collectionMember))
{
context.ReportDiagnostic(Diagnostic.Create(
@@ -199,17 +241,11 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
return false;
}
- var collectionType = collectionMember switch
- {
- IFieldSymbol field => field.Type,
- IPropertySymbol property => property.Type,
- _ => null
- };
-
- if (collectionType is null)
+ var resolvedType = GetMemberType(collectionMember);
+ if (resolvedType is null)
return false;
- if (!collectionType.IsAssignableTo(enumerableType))
+ if (!resolvedType.IsAssignableTo(enumerableType))
{
context.ReportDiagnostic(Diagnostic.Create(
AutoRegisterExportedCollectionsDiagnostics.CollectionTypeMustBeEnumerable,
@@ -218,12 +254,35 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
return false;
}
- if (!TryGetRegistrationAttributeArguments(context, collectionMember, attribute, out var registryMemberName,
- out var registerMethodName))
+ collectionType = resolvedType;
+ return true;
+ }
+
+ private static bool TryResolveRegistryTarget(
+ SourceProductionContext context,
+ Compilation compilation,
+ INamedTypeSymbol ownerType,
+ ISymbol collectionMember,
+ AttributeData attribute,
+ out string registryMemberName,
+ out string registerMethodName,
+ out INamedTypeSymbol registryType)
+ {
+ registryMemberName = string.Empty;
+ registerMethodName = string.Empty;
+ registryType = null!;
+
+ if (!TryGetRegistrationAttributeArguments(
+ context,
+ collectionMember,
+ attribute,
+ out registryMemberName,
+ out registerMethodName))
+ {
return false;
+ }
var registryMember = FindRegistryMember(ownerType, registryMemberName);
-
if (registryMember is null)
{
context.ReportDiagnostic(Diagnostic.Create(
@@ -246,18 +305,24 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
return false;
}
- var registryType = registryMember switch
- {
- IFieldSymbol field => field.Type as INamedTypeSymbol,
- IPropertySymbol property => property.Type as INamedTypeSymbol,
- _ => null
- };
-
- if (registryType is null)
+ var resolvedRegistryType = GetMemberType(registryMember) as INamedTypeSymbol;
+ if (resolvedRegistryType is null)
return false;
- var elementType = TryGetElementType(collectionType);
- if (elementType is null)
+ registryType = resolvedRegistryType;
+ return true;
+ }
+
+ private static bool TryResolveElementType(
+ SourceProductionContext context,
+ ISymbol collectionMember,
+ ITypeSymbol collectionType,
+ out ITypeSymbol elementType)
+ {
+ elementType = null!;
+
+ var resolvedElementType = TryGetElementType(collectionType);
+ if (resolvedElementType is null)
{
// Non-generic IEnumerable exposes elements as object at compile time, which is not safe
// for validating or generating a strongly typed registry call.
@@ -268,26 +333,33 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
return false;
}
- var hasCompatibleMethod = EnumerateCandidateMethods(registryType, registerMethodName)
+ elementType = resolvedElementType;
+ return true;
+ }
+
+ private static bool HasCompatibleRegisterMethod(
+ Compilation compilation,
+ INamedTypeSymbol ownerType,
+ INamedTypeSymbol registryType,
+ string registerMethodName,
+ ITypeSymbol elementType)
+ {
+ return EnumerateCandidateMethods(registryType, registerMethodName)
.Any(method =>
!method.IsStatic &&
method.Parameters.Length == 1 &&
compilation.IsSymbolAccessibleWithin(method, ownerType) &&
CanAcceptElementType(compilation, elementType, method.Parameters[0].Type));
+ }
- if (!hasCompatibleMethod)
+ private static ITypeSymbol? GetMemberType(ISymbol member)
+ {
+ return member switch
{
- context.ReportDiagnostic(Diagnostic.Create(
- AutoRegisterExportedCollectionsDiagnostics.RegisterMethodNotFound,
- collectionMember.Locations.FirstOrDefault() ?? Location.None,
- registerMethodName,
- registryMemberName,
- collectionMember.Name));
- return false;
- }
-
- registration = new RegistrationSpec(collectionMember.Name, registryMemberName, registerMethodName);
- return true;
+ IFieldSymbol field => field.Type,
+ IPropertySymbol property => property.Type,
+ _ => null
+ };
}
private static bool IsInstanceReadableMember(ISymbol member)
diff --git a/GFramework.Godot.Tests/GFramework.Godot.Tests.csproj b/GFramework.Godot.Tests/GFramework.Godot.Tests.csproj
index fea6d616..8712c964 100644
--- a/GFramework.Godot.Tests/GFramework.Godot.Tests.csproj
+++ b/GFramework.Godot.Tests/GFramework.Godot.Tests.csproj
@@ -7,7 +7,6 @@
enable
false
true
- 0
diff --git a/GFramework.Godot/Setting/Data/LocalizationMap.cs b/GFramework.Godot/Setting/Data/LocalizationMap.cs
index fffcd0f7..9d5f33bf 100644
--- a/GFramework.Godot/Setting/Data/LocalizationMap.cs
+++ b/GFramework.Godot/Setting/Data/LocalizationMap.cs
@@ -11,6 +11,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+using System;
+using System.Collections.Generic;
+
namespace GFramework.Godot.Setting.Data;
///
@@ -20,24 +23,47 @@ public class LocalizationMap
{
private const string DefaultFrameworkLanguage = "eng";
private const string DefaultGodotLocale = "en";
+ private readonly Dictionary _frameworkLanguageMap;
+ private readonly Dictionary _languageMap;
///
- /// 用户语言 -> Godot locale 映射表。
+ /// 使用默认的 Godot locale 与框架语言码映射初始化本地化设置。
///
- public Dictionary LanguageMap { get; set; } = new(StringComparer.Ordinal)
+ public LocalizationMap()
+ : this(CreateDefaultLanguageMap(), CreateDefaultFrameworkLanguageMap())
{
- { "简体中文", "zh_CN" },
- { "English", "en" }
- };
+ }
///
- /// 用户语言 -> GFramework 本地化语言码映射表。
+ /// 使用外部提供的映射初始化本地化设置。
+ /// 构造函数会复制输入字典,避免调用方在实例创建后继续修改内部状态。
///
- public Dictionary FrameworkLanguageMap { get; set; } = new(StringComparer.Ordinal)
+ /// 用户语言到 Godot locale 的映射。
+ /// 用户语言到 GFramework 本地化语言码的映射。
+ ///
+ /// 当 或 为 时抛出。
+ ///
+ public LocalizationMap(
+ IReadOnlyDictionary languageMap,
+ IReadOnlyDictionary frameworkLanguageMap)
{
- { "简体中文", "zhs" },
- { "English", "eng" }
- };
+ ArgumentNullException.ThrowIfNull(languageMap);
+ ArgumentNullException.ThrowIfNull(frameworkLanguageMap);
+
+ // 复制外部输入,避免公共属性把可变集合直接暴露给调用方。
+ _languageMap = new Dictionary(languageMap, StringComparer.Ordinal);
+ _frameworkLanguageMap = new Dictionary(frameworkLanguageMap, StringComparer.Ordinal);
+ }
+
+ ///
+ /// 获取用户语言到 Godot locale 的只读映射表。
+ ///
+ public IReadOnlyDictionary LanguageMap => _languageMap;
+
+ ///
+ /// 获取用户语言到 GFramework 本地化语言码的只读映射表。
+ ///
+ public IReadOnlyDictionary FrameworkLanguageMap => _frameworkLanguageMap;
///
/// 解析用户保存的语言值对应的 Godot locale。
@@ -68,4 +94,22 @@ public class LocalizationMap
return FrameworkLanguageMap.GetValueOrDefault(storedLanguage, DefaultFrameworkLanguage);
}
+
+ private static Dictionary CreateDefaultLanguageMap()
+ {
+ return new Dictionary(StringComparer.Ordinal)
+ {
+ { "简体中文", "zh_CN" },
+ { "English", "en" }
+ };
+ }
+
+ private static Dictionary CreateDefaultFrameworkLanguageMap()
+ {
+ return new Dictionary(StringComparer.Ordinal)
+ {
+ { "简体中文", "zhs" },
+ { "English", "eng" }
+ };
+ }
}
diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs
index 3930aef9..458eaa18 100644
--- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs
+++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs
@@ -684,6 +684,685 @@ public class CqrsHandlerRegistryGeneratorTests
""";
+ // Keep large source fixtures at class scope so MA0051 reduction stays behavior-neutral for generator tests.
+ private const string HiddenPointerResponseCompilationErrorSource = """
+ using System;
+
+ namespace Microsoft.Extensions.DependencyInjection
+ {
+ public interface IServiceCollection { }
+
+ public static class ServiceCollectionServiceExtensions
+ {
+ public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { }
+ }
+ }
+
+ namespace GFramework.Core.Abstractions.Logging
+ {
+ public interface ILogger
+ {
+ void Debug(string msg);
+ }
+ }
+
+ namespace GFramework.Cqrs.Abstractions.Cqrs
+ {
+ public interface IRequest { }
+ public interface INotification { }
+ public interface IStreamRequest { }
+
+ public interface IRequestHandler where TRequest : IRequest { }
+ public interface INotificationHandler where TNotification : INotification { }
+ public interface IStreamRequestHandler where TRequest : IStreamRequest { }
+ }
+
+ namespace GFramework.Cqrs
+ {
+ public interface ICqrsHandlerRegistry
+ {
+ void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger);
+ }
+
+ [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
+ public sealed class CqrsHandlerRegistryAttribute : Attribute
+ {
+ public CqrsHandlerRegistryAttribute(Type registryType) { }
+ }
+ }
+
+ namespace TestApp
+ {
+ using GFramework.Cqrs.Abstractions.Cqrs;
+
+ public sealed class Container
+ {
+ private unsafe struct HiddenResponse
+ {
+ }
+
+ private unsafe sealed record HiddenRequest() : IRequest;
+
+ public unsafe sealed class HiddenHandler : IRequestHandler
+ {
+ }
+ }
+ }
+ """;
+
+ private const string MixedDirectAndPreciseRegistrationsSource = """
+ using System;
+
+ namespace Microsoft.Extensions.DependencyInjection
+ {
+ public interface IServiceCollection { }
+
+ public static class ServiceCollectionServiceExtensions
+ {
+ public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { }
+ }
+ }
+
+ namespace GFramework.Core.Abstractions.Logging
+ {
+ public interface ILogger
+ {
+ void Debug(string msg);
+ }
+ }
+
+ namespace GFramework.Cqrs.Abstractions.Cqrs
+ {
+ public interface IRequest { }
+ public interface INotification { }
+ public interface IStreamRequest { }
+
+ public interface IRequestHandler where TRequest : IRequest { }
+ public interface INotificationHandler where TNotification : INotification { }
+ public interface IStreamRequestHandler where TRequest : IStreamRequest { }
+ }
+
+ namespace GFramework.Cqrs
+ {
+ public interface ICqrsHandlerRegistry
+ {
+ void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger);
+ }
+
+ [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
+ public sealed class CqrsHandlerRegistryAttribute : Attribute
+ {
+ public CqrsHandlerRegistryAttribute(Type registryType) { }
+ }
+ }
+
+ namespace TestApp
+ {
+ using GFramework.Cqrs.Abstractions.Cqrs;
+
+ public sealed record VisibleRequest() : IRequest;
+
+ public sealed class Container
+ {
+ private sealed record HiddenResponse();
+
+ private sealed record HiddenRequest() : IRequest;
+
+ public sealed class MixedHandler :
+ IRequestHandler,
+ IRequestHandler
+ {
+ }
+ }
+ }
+ """;
+
+ private const string MixedReflectedImplementationAndPreciseRegistrationsSource = """
+ using System;
+
+ namespace Microsoft.Extensions.DependencyInjection
+ {
+ public interface IServiceCollection { }
+
+ public static class ServiceCollectionServiceExtensions
+ {
+ public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { }
+ }
+ }
+
+ namespace GFramework.Core.Abstractions.Logging
+ {
+ public interface ILogger
+ {
+ void Debug(string msg);
+ }
+ }
+
+ namespace GFramework.Cqrs.Abstractions.Cqrs
+ {
+ public interface IRequest { }
+ public interface INotification { }
+ public interface IStreamRequest { }
+
+ public interface IRequestHandler where TRequest : IRequest { }
+ public interface INotificationHandler where TNotification : INotification { }
+ public interface IStreamRequestHandler where TRequest : IStreamRequest { }
+ }
+
+ namespace GFramework.Cqrs
+ {
+ public interface ICqrsHandlerRegistry
+ {
+ void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger);
+ }
+
+ [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
+ public sealed class CqrsHandlerRegistryAttribute : Attribute
+ {
+ public CqrsHandlerRegistryAttribute(Type registryType) { }
+ }
+ }
+
+ namespace TestApp
+ {
+ using GFramework.Cqrs.Abstractions.Cqrs;
+
+ public sealed record VisibleRequest() : IRequest;
+
+ public sealed class Container
+ {
+ private sealed record HiddenResponse();
+
+ private sealed record HiddenRequest() : IRequest;
+
+ private sealed class HiddenMixedHandler :
+ IRequestHandler,
+ IRequestHandler
+ {
+ }
+ }
+ }
+ """;
+
+ private const string ExternalProtectedTypeContractsSource = """
+ namespace GFramework.Cqrs.Abstractions.Cqrs
+ {
+ public interface IRequest { }
+ public interface INotification { }
+ public interface IStreamRequest { }
+
+ public interface IRequestHandler where TRequest : IRequest { }
+ public interface INotificationHandler where TNotification : INotification { }
+ public interface IStreamRequestHandler where TRequest : IStreamRequest { }
+ }
+ """;
+
+ private const string ExternalProtectedTypeDependencySource = """
+ using GFramework.Cqrs.Abstractions.Cqrs;
+
+ namespace Dep;
+
+ public sealed record VisibleRequest() : IRequest;
+
+ public abstract class VisibilityScope
+ {
+ protected internal sealed record ProtectedResponse();
+
+ protected internal sealed record ProtectedRequest() : IRequest;
+ }
+
+ public abstract class HandlerBase :
+ VisibilityScope,
+ IRequestHandler,
+ IRequestHandler
+ {
+ }
+ """;
+
+ private const string ExternalProtectedTypeLookupSource = """
+ using System;
+ using Dep;
+
+ namespace Microsoft.Extensions.DependencyInjection
+ {
+ public interface IServiceCollection { }
+
+ public static class ServiceCollectionServiceExtensions
+ {
+ public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { }
+ }
+ }
+
+ namespace GFramework.Core.Abstractions.Logging
+ {
+ public interface ILogger
+ {
+ void Debug(string msg);
+ }
+ }
+
+ namespace GFramework.Cqrs
+ {
+ public interface ICqrsHandlerRegistry
+ {
+ void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger);
+ }
+
+ [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
+ public sealed class CqrsHandlerRegistryAttribute : Attribute
+ {
+ public CqrsHandlerRegistryAttribute(Type registryType) { }
+ }
+ }
+
+ namespace TestApp
+ {
+ public sealed class DerivedHandler : HandlerBase
+ {
+ }
+ }
+ """;
+
+ private const string LegacyFallbackMarkerHiddenHandlerSource = """
+ using System;
+
+ namespace Microsoft.Extensions.DependencyInjection
+ {
+ public interface IServiceCollection { }
+
+ public static class ServiceCollectionServiceExtensions
+ {
+ public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { }
+ }
+ }
+
+ namespace GFramework.Core.Abstractions.Logging
+ {
+ public interface ILogger
+ {
+ void Debug(string msg);
+ }
+ }
+
+ namespace GFramework.Cqrs.Abstractions.Cqrs
+ {
+ public interface IRequest { }
+ public interface INotification { }
+ public interface IStreamRequest { }
+
+ public interface IRequestHandler where TRequest : IRequest { }
+ public interface INotificationHandler where TNotification : INotification { }
+ public interface IStreamRequestHandler where TRequest : IStreamRequest { }
+ }
+
+ namespace GFramework.Cqrs
+ {
+ public interface ICqrsHandlerRegistry
+ {
+ void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger);
+ }
+
+ [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
+ public sealed class CqrsHandlerRegistryAttribute : Attribute
+ {
+ public CqrsHandlerRegistryAttribute(Type registryType) { }
+ }
+
+ [AttributeUsage(AttributeTargets.Assembly)]
+ public sealed class CqrsReflectionFallbackAttribute : Attribute
+ {
+ public CqrsReflectionFallbackAttribute() { }
+ }
+ }
+
+ namespace TestApp
+ {
+ using GFramework.Cqrs.Abstractions.Cqrs;
+
+ public sealed record VisibleRequest() : IRequest;
+
+ public sealed class Container
+ {
+ private sealed record HiddenRequest() : IRequest;
+
+ private sealed class HiddenHandler : IRequestHandler { }
+ }
+
+ public sealed class VisibleHandler : IRequestHandler { }
+ }
+ """;
+
+ private const string FallbackMarkerUnavailableHiddenHandlerSource = """
+ using System;
+
+ namespace Microsoft.Extensions.DependencyInjection
+ {
+ public interface IServiceCollection { }
+
+ public static class ServiceCollectionServiceExtensions
+ {
+ public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { }
+ }
+ }
+
+ namespace GFramework.Core.Abstractions.Logging
+ {
+ public interface ILogger
+ {
+ void Debug(string msg);
+ }
+ }
+
+ namespace GFramework.Cqrs.Abstractions.Cqrs
+ {
+ public interface IRequest { }
+ public interface INotification { }
+ public interface IStreamRequest { }
+
+ public interface IRequestHandler where TRequest : IRequest { }
+ public interface INotificationHandler where TNotification : INotification { }
+ public interface IStreamRequestHandler where TRequest : IStreamRequest { }
+ }
+
+ namespace GFramework.Cqrs
+ {
+ public interface ICqrsHandlerRegistry
+ {
+ void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger);
+ }
+
+ [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
+ public sealed class CqrsHandlerRegistryAttribute : Attribute
+ {
+ public CqrsHandlerRegistryAttribute(Type registryType) { }
+ }
+ }
+
+ namespace TestApp
+ {
+ using GFramework.Cqrs.Abstractions.Cqrs;
+
+ public sealed record VisibleRequest() : IRequest;
+
+ public sealed class Container
+ {
+ private sealed record HiddenRequest() : IRequest;
+
+ private sealed class HiddenHandler : IRequestHandler { }
+ }
+
+ public sealed class VisibleHandler : IRequestHandler { }
+ }
+ """;
+
+ private const string MissingFallbackAttributeDiagnosticSource = """
+ using System;
+
+ namespace Microsoft.Extensions.DependencyInjection
+ {
+ public interface IServiceCollection { }
+
+ public static class ServiceCollectionServiceExtensions
+ {
+ public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { }
+ }
+ }
+
+ namespace GFramework.Core.Abstractions.Logging
+ {
+ public interface ILogger
+ {
+ void Debug(string msg);
+ }
+ }
+
+ namespace GFramework.Cqrs.Abstractions.Cqrs
+ {
+ public interface IRequest { }
+ public interface INotification { }
+ public interface IStreamRequest { }
+
+ public interface IRequestHandler where TRequest : IRequest { }
+ public interface INotificationHandler where TNotification : INotification { }
+ public interface IStreamRequestHandler where TRequest : IStreamRequest { }
+ }
+
+ namespace GFramework.Cqrs
+ {
+ public interface ICqrsHandlerRegistry
+ {
+ void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger);
+ }
+
+ [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
+ public sealed class CqrsHandlerRegistryAttribute : Attribute
+ {
+ public CqrsHandlerRegistryAttribute(Type registryType) { }
+ }
+ }
+
+ namespace TestApp
+ {
+ using GFramework.Cqrs.Abstractions.Cqrs;
+
+ public sealed class Container
+ {
+ private unsafe struct HiddenResponse
+ {
+ }
+
+ private unsafe sealed record HiddenRequest() : IRequest>;
+
+ public unsafe sealed class HiddenHandler : IRequestHandler>
+ {
+ }
+ }
+ }
+ """;
+
+ private const string UnresolvedErrorTypeRuntimeLookupSource = """
+ using System;
+
+ namespace Microsoft.Extensions.DependencyInjection
+ {
+ public interface IServiceCollection { }
+
+ public static class ServiceCollectionServiceExtensions
+ {
+ public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { }
+ }
+ }
+
+ namespace GFramework.Core.Abstractions.Logging
+ {
+ public interface ILogger
+ {
+ void Debug(string msg);
+ }
+ }
+
+ namespace GFramework.Cqrs.Abstractions.Cqrs
+ {
+ public interface IRequest { }
+ public interface INotification { }
+ public interface IStreamRequest { }
+
+ public interface IRequestHandler where TRequest : IRequest { }
+ public interface INotificationHandler where TNotification : INotification { }
+ public interface IStreamRequestHandler where TRequest : IStreamRequest { }
+ }
+
+ namespace GFramework.Cqrs
+ {
+ public interface ICqrsHandlerRegistry
+ {
+ void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger);
+ }
+
+ [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
+ public sealed class CqrsHandlerRegistryAttribute : Attribute
+ {
+ public CqrsHandlerRegistryAttribute(Type registryType) { }
+ }
+
+ [AttributeUsage(AttributeTargets.Assembly)]
+ public sealed class CqrsReflectionFallbackAttribute : Attribute
+ {
+ public CqrsReflectionFallbackAttribute(params string[] fallbackHandlerTypeNames) { }
+ }
+ }
+
+ namespace TestApp
+ {
+ using GFramework.Cqrs.Abstractions.Cqrs;
+
+ public sealed record BrokenRequest() : IRequest;
+
+ public sealed class BrokenHandler : IRequestHandler
+ {
+ }
+ }
+ """;
+
+ private const string DynamicResponseNormalizationSource = """
+ using System;
+
+ namespace Microsoft.Extensions.DependencyInjection
+ {
+ public interface IServiceCollection { }
+
+ public static class ServiceCollectionServiceExtensions
+ {
+ public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { }
+ }
+ }
+
+ namespace GFramework.Core.Abstractions.Logging
+ {
+ public interface ILogger
+ {
+ void Debug(string msg);
+ }
+ }
+
+ namespace GFramework.Cqrs.Abstractions.Cqrs
+ {
+ public interface IRequest { }
+ public interface INotification { }
+ public interface IStreamRequest { }
+
+ public interface IRequestHandler where TRequest : IRequest { }
+ public interface INotificationHandler where TNotification : INotification { }
+ public interface IStreamRequestHandler where TRequest : IStreamRequest { }
+ }
+
+ namespace GFramework.Cqrs
+ {
+ public interface ICqrsHandlerRegistry
+ {
+ void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger);
+ }
+
+ [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
+ public sealed class CqrsHandlerRegistryAttribute : Attribute
+ {
+ public CqrsHandlerRegistryAttribute(Type registryType) { }
+ }
+ }
+
+ namespace TestApp
+ {
+ using GFramework.Cqrs.Abstractions.Cqrs;
+
+ public sealed record DynamicRequest() : IRequest;
+
+ public sealed class DynamicHandler : IRequestHandler
+ {
+ }
+ }
+ """;
+
+ private const string AssemblyLevelFallbackMetadataSource = """
+ using System;
+
+ namespace Microsoft.Extensions.DependencyInjection
+ {
+ public interface IServiceCollection { }
+
+ public static class ServiceCollectionServiceExtensions
+ {
+ public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { }
+ }
+ }
+
+ namespace GFramework.Core.Abstractions.Logging
+ {
+ public interface ILogger
+ {
+ void Debug(string msg);
+ }
+ }
+
+ namespace GFramework.Cqrs.Abstractions.Cqrs
+ {
+ public interface IRequest { }
+ public interface INotification { }
+ public interface IStreamRequest { }
+
+ public interface IRequestHandler where TRequest : IRequest { }
+ public interface INotificationHandler where TNotification : INotification { }
+ public interface IStreamRequestHandler where TRequest : IStreamRequest { }
+ }
+
+ namespace GFramework.Cqrs
+ {
+ public interface ICqrsHandlerRegistry
+ {
+ void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger);
+ }
+
+ [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
+ public sealed class CqrsHandlerRegistryAttribute : Attribute
+ {
+ public CqrsHandlerRegistryAttribute(Type registryType) { }
+ }
+
+ [AttributeUsage(AttributeTargets.Assembly)]
+ public sealed class CqrsReflectionFallbackAttribute : Attribute
+ {
+ public CqrsReflectionFallbackAttribute(params string[] fallbackHandlerTypeNames) { }
+ }
+ }
+
+ namespace TestApp
+ {
+ using GFramework.Cqrs.Abstractions.Cqrs;
+
+ public sealed class Container
+ {
+ private unsafe struct AlphaResponse
+ {
+ }
+
+ private unsafe struct BetaResponse
+ {
+ }
+
+ private unsafe sealed record AlphaRequest() : IRequest>;
+
+ private unsafe sealed record BetaRequest() : IRequest