Merge pull request #283 from GeWuYou/fix/analyzer-warning-reduction-batch

Fix/analyzer warning reduction batch
This commit is contained in:
gewuyou 2026-04-24 16:51:03 +08:00 committed by GitHub
commit 63f563cd49
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 2429 additions and 2331 deletions

View File

@ -29,6 +29,10 @@ All AI agents and contributors must follow these rules when writing, reviewing,
## Git Workflow Rules ## Git Workflow Rules
- Every completed task MUST pass at least one build validation before it is considered done. - 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 - 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. `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 - 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. Use the smallest command set that proves the change, then expand if the change is cross-cutting.
```bash ```bash
# Check warnings from the default repository build entrypoint
dotnet clean
dotnet build
# Build the full solution # Build the full solution
dotnet build GFramework.sln -c Release dotnet build GFramework.sln -c Release

View File

@ -7,7 +7,6 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject> <IsTestProject>true</IsTestProject>
<WarningLevel>0</WarningLevel>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.4.0"/> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.4.0"/>

View File

@ -283,15 +283,21 @@ public class UnifiedSettingsDataRepository(
/// 复制当前统一文件快照,确保未提交修改不会污染内存中的已提交状态。 /// 复制当前统一文件快照,确保未提交修改不会污染内存中的已提交状态。
/// </summary> /// </summary>
/// <param name="source">要复制的统一文件快照。</param> /// <param name="source">要复制的统一文件快照。</param>
/// <returns>包含独立 section 字典的新快照。</returns> /// <returns>包含独立 section 映射副本的新快照。</returns>
private static UnifiedSettingsFile CloneFile(UnifiedSettingsFile source) private static UnifiedSettingsFile CloneFile(UnifiedSettingsFile source)
{ {
ArgumentNullException.ThrowIfNull(source); ArgumentNullException.ThrowIfNull(source);
// 反序列化后的运行时类型可能只是 IDictionary 实现;若底层仍是 Dictionary则保留其 comparer。
// 若 comparer 已因接口抽象而不可恢复,则显式回退到 Ordinal避免让默认 comparer 语义继续隐式存在。
var sections = source.Sections is Dictionary<string, string> dictionary
? new Dictionary<string, string>(dictionary, dictionary.Comparer)
: new Dictionary<string, string>(source.Sections, StringComparer.Ordinal);
return new UnifiedSettingsFile return new UnifiedSettingsFile
{ {
Version = source.Version, Version = source.Version,
Sections = new Dictionary<string, string>(source.Sections, source.Sections.Comparer) Sections = sections
}; };
} }

View File

@ -11,6 +11,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
using System;
using System.Collections.Generic;
using GFramework.Core.Abstractions.Versioning; using GFramework.Core.Abstractions.Versioning;
namespace GFramework.Game.Data; namespace GFramework.Game.Data;
@ -22,10 +24,16 @@ namespace GFramework.Game.Data;
internal sealed class UnifiedSettingsFile : IVersioned internal sealed class UnifiedSettingsFile : IVersioned
{ {
/// <summary> /// <summary>
/// 配置节集合,存储不同类型的配置数据 /// 配置节映射,存储不同类型的配置数据。
/// 键为配置节名称,值为配置对象
/// </summary> /// </summary>
public Dictionary<string, string> Sections { get; set; } = new(); /// <remarks>
/// 这里公开为 <see cref="IDictionary{TKey,TValue}" /> 而不是具体的 <see cref="Dictionary{TKey,TValue}" />
/// 以避免暴露可替换的具体集合实现,同时继续兼容 Newtonsoft.Json 对字典对象的序列化与反序列化。
/// 默认实例使用 <see cref="StringComparer.Ordinal" />;若调用方提供其他实现,仓库在可以识别底层
/// <see cref="Dictionary{TKey,TValue}" /> comparer 时会保留原语义,否则克隆快照时会显式回退到
/// <see cref="StringComparer.Ordinal" />。
/// </remarks>
public IDictionary<string, string> Sections { get; set; } = new Dictionary<string, string>(StringComparer.Ordinal);
/// <summary> /// <summary>
/// 配置文件版本号,用于版本控制和兼容性检查 /// 配置文件版本号,用于版本控制和兼容性检查

View File

@ -6,29 +6,28 @@ namespace GFramework.Godot.SourceGenerators.Tests.Behavior;
[TestFixture] [TestFixture]
public class AutoSceneGeneratorTests public class AutoSceneGeneratorTests
{ {
[Test] private const string AutoSceneAttributeWithKeyDeclaration = """
public async Task Generates_Scene_Behavior_Boilerplate()
{
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoSceneAttribute : Attribute public sealed class AutoSceneAttribute : Attribute
{ {
public AutoSceneAttribute(string key) { } public AutoSceneAttribute(string key) { }
} }
} """;
namespace Godot private const string AutoSceneAttributeWithoutKeyDeclaration = """
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoSceneAttribute : Attribute
{ {
public AutoSceneAttribute() { }
}
""";
private const string NodeTypes = """
public class Node { } public class Node { }
public class Node2D : Node { } public class Node2D : Node { }
} """;
private const string SceneBehaviorInfrastructure = """
namespace GFramework.Game.Abstractions.Scene namespace GFramework.Game.Abstractions.Scene
{ {
public interface ISceneBehavior { } public interface ISceneBehavior { }
@ -48,15 +47,20 @@ public class AutoSceneGeneratorTests
} }
} }
} }
""";
namespace TestApp [Test]
public async Task Generates_Scene_Behavior_Boilerplate()
{ {
string source = CreateAutoSceneSource(
AutoSceneAttributeWithKeyDeclaration,
"""
[AutoScene("Gameplay")] [AutoScene("Gameplay")]
public partial class GameplayRoot : Node2D public partial class GameplayRoot : Node2D
{ {
} }
} """,
"""; includeBehaviorInfrastructure: true);
const string expected = """ const string expected = """
// <auto-generated /> // <auto-generated />
@ -80,40 +84,20 @@ public class AutoSceneGeneratorTests
await GeneratorTest<AutoSceneGenerator>.RunAsync( await GeneratorTest<AutoSceneGenerator>.RunAsync(
source, source,
("TestApp_GameplayRoot.AutoScene.g.cs", expected)); ("TestApp_GameplayRoot.AutoScene.g.cs", expected)).ConfigureAwait(false);
} }
[Test] [Test]
public async Task Reports_Diagnostic_When_AutoScene_Arguments_Are_Invalid() public async Task Reports_Diagnostic_When_AutoScene_Arguments_Are_Invalid()
{ {
const string source = """ string source = CreateAutoSceneSource(
using System; AutoSceneAttributeWithoutKeyDeclaration,
using GFramework.Godot.SourceGenerators.Abstractions.UI; """
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoSceneAttribute : Attribute
{
public AutoSceneAttribute() { }
}
}
namespace Godot
{
public class Node { }
public class Node2D : Node { }
}
namespace TestApp
{
[{|#0:AutoScene|}] [{|#0:AutoScene|}]
public partial class GameplayRoot : Node2D public partial class GameplayRoot : Node2D
{ {
} }
} """);
""";
var test = new CSharpSourceGeneratorTest<AutoSceneGenerator, DefaultVerifier> var test = new CSharpSourceGeneratorTest<AutoSceneGenerator, DefaultVerifier>
{ {
@ -128,55 +112,15 @@ public class AutoSceneGeneratorTests
.WithLocation(0) .WithLocation(0)
.WithArguments("AutoSceneAttribute", "GameplayRoot", "a single string scene key argument")); .WithArguments("AutoSceneAttribute", "GameplayRoot", "a single string scene key argument"));
await test.RunAsync(); await test.RunAsync().ConfigureAwait(false);
} }
[Test] [Test]
public async Task Generates_Type_Constraints_For_Nullable_Reference_NotNull_And_Unmanaged_Parameters() public async Task Generates_Type_Constraints_For_Nullable_Reference_NotNull_And_Unmanaged_Parameters()
{ {
const string source = """ string source = CreateAutoSceneSource(
#nullable enable AutoSceneAttributeWithKeyDeclaration,
using System; """
using GFramework.Godot.SourceGenerators.Abstractions.UI;
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoSceneAttribute : Attribute
{
public AutoSceneAttribute(string key) { }
}
}
namespace Godot
{
public class Node { }
public class Node2D : Node { }
}
namespace GFramework.Game.Abstractions.Scene
{
public interface ISceneBehavior { }
}
namespace GFramework.Godot.Scene
{
using GFramework.Game.Abstractions.Scene;
using Godot;
public static class SceneBehaviorFactory
{
public static ISceneBehavior Create<T>(T owner, string key)
where T : Node
{
return null!;
}
}
}
namespace TestApp
{
[AutoScene("Gameplay")] [AutoScene("Gameplay")]
public partial class GameplayRoot<TReference, TNotNull, TValue, TUnmanaged> : Node2D public partial class GameplayRoot<TReference, TNotNull, TValue, TUnmanaged> : Node2D
where TReference : class? where TReference : class?
@ -185,8 +129,9 @@ public class AutoSceneGeneratorTests
where TUnmanaged : unmanaged where TUnmanaged : unmanaged
{ {
} }
} """,
"""; includeBehaviorInfrastructure: true,
nullableEnabled: true);
const string expected = """ const string expected = """
// <auto-generated /> // <auto-generated />
@ -214,7 +159,7 @@ public class AutoSceneGeneratorTests
await GeneratorTest<AutoSceneGenerator>.RunAsync( await GeneratorTest<AutoSceneGenerator>.RunAsync(
source, source,
("TestApp_GameplayRoot.AutoScene.g.cs", expected)); ("TestApp_GameplayRoot.AutoScene.g.cs", expected)).ConfigureAwait(false);
} }
/// <summary> /// <summary>
@ -267,7 +212,7 @@ public class AutoSceneGeneratorTests
.WithLocation(0) .WithLocation(0)
.WithArguments("GameplayRoot", "SceneKeyStr")); .WithArguments("GameplayRoot", "SceneKeyStr"));
await test.RunAsync(); await test.RunAsync().ConfigureAwait(false);
} }
/// <summary> /// <summary>
@ -326,6 +271,39 @@ public class AutoSceneGeneratorTests
.WithLocation(0) .WithLocation(0)
.WithArguments("GameplayRoot", "__autoSceneBehavior_Generated")); .WithArguments("GameplayRoot", "__autoSceneBehavior_Generated"));
await test.RunAsync(); await test.RunAsync().ConfigureAwait(false);
}
private static string CreateAutoSceneSource(
string attributeDeclaration,
string testAppSource,
bool includeBehaviorInfrastructure = false,
bool nullableEnabled = false)
{
string nullableDirective = nullableEnabled ? "#nullable enable\n" : string.Empty;
string infrastructure = includeBehaviorInfrastructure
? $"{Environment.NewLine}{Environment.NewLine}{SceneBehaviorInfrastructure}"
: string.Empty;
return $$"""
{{nullableDirective}}using System;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
{{attributeDeclaration}}
}
namespace Godot
{
{{NodeTypes}}
}{{infrastructure}}
namespace TestApp
{
{{testAppSource}}
}
""";
} }
} }

View File

@ -6,30 +6,29 @@ namespace GFramework.Godot.SourceGenerators.Tests.Behavior;
[TestFixture] [TestFixture]
public class AutoUiPageGeneratorTests public class AutoUiPageGeneratorTests
{ {
[Test] private const string AutoUiPageAttributeWithLayerDeclaration = """
public async Task Generates_Ui_Page_Behavior_Boilerplate()
{
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoUiPageAttribute : Attribute public sealed class AutoUiPageAttribute : Attribute
{ {
public AutoUiPageAttribute(string key, string layerName) { } public AutoUiPageAttribute(string key, string layerName) { }
} }
} """;
namespace Godot private const string AutoUiPageAttributeWithoutLayerDeclaration = """
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoUiPageAttribute : Attribute
{ {
public AutoUiPageAttribute(string key) { }
}
""";
private const string CanvasNodeTypes = """
public class Node { } public class Node { }
public class CanvasItem : Node { } public class CanvasItem : Node { }
public class Control : CanvasItem { } public class Control : CanvasItem { }
} """;
private const string UiLayerFullEnum = """
namespace GFramework.Game.Abstractions.Enums namespace GFramework.Game.Abstractions.Enums
{ {
public enum UiLayer public enum UiLayer
@ -39,7 +38,19 @@ public class AutoUiPageGeneratorTests
Modal Modal
} }
} }
""";
private const string UiLayerPageOnlyEnum = """
namespace GFramework.Game.Abstractions.Enums
{
public enum UiLayer
{
Page
}
}
""";
private const string UiBehaviorInfrastructure = """
namespace GFramework.Game.Abstractions.UI namespace GFramework.Game.Abstractions.UI
{ {
public interface IUiPageBehavior { } public interface IUiPageBehavior { }
@ -60,15 +71,20 @@ public class AutoUiPageGeneratorTests
} }
} }
} }
""";
namespace TestApp [Test]
public async Task Generates_Ui_Page_Behavior_Boilerplate()
{ {
string source = CreateAutoUiPageSource(
AutoUiPageAttributeWithLayerDeclaration,
UiLayerFullEnum,
"""
[AutoUiPage("MainMenu", "Page")] [AutoUiPage("MainMenu", "Page")]
public partial class MainMenu : Control public partial class MainMenu : Control
{ {
} }
} """);
""";
const string expected = """ const string expected = """
// <auto-generated /> // <auto-generated />
@ -92,70 +108,21 @@ public class AutoUiPageGeneratorTests
await GeneratorTest<AutoUiPageGenerator>.RunAsync( await GeneratorTest<AutoUiPageGenerator>.RunAsync(
source, source,
("TestApp_MainMenu.AutoUiPage.g.cs", expected)); ("TestApp_MainMenu.AutoUiPage.g.cs", expected)).ConfigureAwait(false);
} }
[Test] [Test]
public async Task Reports_Diagnostic_When_AutoUiPage_Attribute_Arguments_Are_Invalid() public async Task Reports_Diagnostic_When_AutoUiPage_Attribute_Arguments_Are_Invalid()
{ {
const string source = """ string source = CreateAutoUiPageSource(
using System; AutoUiPageAttributeWithoutLayerDeclaration,
using GFramework.Godot.SourceGenerators.Abstractions.UI; UiLayerPageOnlyEnum,
using Godot; """
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoUiPageAttribute : Attribute
{
public AutoUiPageAttribute(string key) { }
}
}
namespace Godot
{
public class Node { }
public class CanvasItem : Node { }
public class Control : CanvasItem { }
}
namespace GFramework.Game.Abstractions.Enums
{
public enum UiLayer
{
Page
}
}
namespace GFramework.Game.Abstractions.UI
{
public interface IUiPageBehavior { }
}
namespace GFramework.Godot.UI
{
using GFramework.Game.Abstractions.Enums;
using GFramework.Game.Abstractions.UI;
using Godot;
public static class UiPageBehaviorFactory
{
public static IUiPageBehavior Create<T>(T owner, string key, UiLayer layer)
where T : CanvasItem
{
return null!;
}
}
}
namespace TestApp
{
[{|#0:AutoUiPage("MainMenu")|}] [{|#0:AutoUiPage("MainMenu")|}]
public partial class MainMenu : Control public partial class MainMenu : Control
{ {
} }
} """);
""";
var test = new CSharpSourceGeneratorTest<AutoUiPageGenerator, DefaultVerifier> var test = new CSharpSourceGeneratorTest<AutoUiPageGenerator, DefaultVerifier>
{ {
@ -174,65 +141,16 @@ public class AutoUiPageGeneratorTests
"MainMenu", "MainMenu",
"a string key argument and a string UiLayer name argument")); "a string key argument and a string UiLayer name argument"));
await test.RunAsync(); await test.RunAsync().ConfigureAwait(false);
} }
[Test] [Test]
public async Task Generates_Type_Constraints_For_ClassNullable_NotNull_And_Unmanaged() public async Task Generates_Type_Constraints_For_ClassNullable_NotNull_And_Unmanaged()
{ {
const string source = """ string source = CreateAutoUiPageSource(
#nullable enable AutoUiPageAttributeWithLayerDeclaration,
using System; UiLayerPageOnlyEnum,
using GFramework.Godot.SourceGenerators.Abstractions.UI; """
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoUiPageAttribute : Attribute
{
public AutoUiPageAttribute(string key, string layerName) { }
}
}
namespace Godot
{
public class Node { }
public class CanvasItem : Node { }
public class Control : CanvasItem { }
}
namespace GFramework.Game.Abstractions.Enums
{
public enum UiLayer
{
Page
}
}
namespace GFramework.Game.Abstractions.UI
{
public interface IUiPageBehavior { }
}
namespace GFramework.Godot.UI
{
using GFramework.Game.Abstractions.Enums;
using GFramework.Game.Abstractions.UI;
using Godot;
public static class UiPageBehaviorFactory
{
public static IUiPageBehavior Create<T>(T owner, string key, UiLayer layer)
where T : CanvasItem
{
return null!;
}
}
}
namespace TestApp
{
[AutoUiPage("MainMenu", "Page")] [AutoUiPage("MainMenu", "Page")]
public partial class MainMenu<TReference, TNotNull, TUnmanaged> : Control public partial class MainMenu<TReference, TNotNull, TUnmanaged> : Control
where TReference : class? where TReference : class?
@ -240,8 +158,8 @@ public class AutoUiPageGeneratorTests
where TUnmanaged : unmanaged where TUnmanaged : unmanaged
{ {
} }
} """,
"""; nullableEnabled: true);
const string expected = """ const string expected = """
// <auto-generated /> // <auto-generated />
@ -268,6 +186,40 @@ public class AutoUiPageGeneratorTests
await GeneratorTest<AutoUiPageGenerator>.RunAsync( await GeneratorTest<AutoUiPageGenerator>.RunAsync(
source, source,
("TestApp_MainMenu.AutoUiPage.g.cs", expected)); ("TestApp_MainMenu.AutoUiPage.g.cs", expected)).ConfigureAwait(false);
}
private static string CreateAutoUiPageSource(
string attributeDeclaration,
string uiLayerDeclaration,
string testAppSource,
bool nullableEnabled = false)
{
string nullableDirective = nullableEnabled ? "#nullable enable\n" : string.Empty;
return $$"""
{{nullableDirective}}using System;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
{{attributeDeclaration}}
}
namespace Godot
{
{{CanvasNodeTypes}}
}
{{uiLayerDeclaration}}
{{UiBehaviorInfrastructure}}
namespace TestApp
{
{{testAppSource}}
}
""";
} }
} }

View File

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

View File

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

View File

@ -5,16 +5,7 @@ namespace GFramework.Godot.SourceGenerators.Tests.GetNode;
[TestFixture] [TestFixture]
public class GetNodeGeneratorTests public class GetNodeGeneratorTests
{ {
[Test] private const string FullGetNodeAttributeDeclaration = """
public async Task Generates_InferredUniqueNameBindings_And_ReadyHook_WhenReadyIsMissing()
{
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions
{
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class GetNodeAttribute : Attribute public sealed class GetNodeAttribute : Attribute
{ {
@ -32,24 +23,60 @@ public class GetNodeGeneratorTests
RelativePath = 2, RelativePath = 2,
AbsolutePath = 3 AbsolutePath = 3
} }
""";
private const string MinimalGetNodeAttributeDeclaration = """
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class GetNodeAttribute : Attribute
{
public GetNodeAttribute() {}
} }
namespace Godot public enum NodeLookupMode
{ {
Auto = 0
}
""";
private const string PropertyOnlyGetNodeAttributeDeclaration = """
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class GetNodeAttribute : Attribute
{
public string? Path { get; set; }
public bool Required { get; set; } = true;
public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto;
}
public enum NodeLookupMode
{
Auto = 0,
UniqueName = 1,
RelativePath = 2,
AbsolutePath = 3
}
""";
private const string NodeWithReadyAndLookupMethods = """
public class Node public class Node
{ {
public virtual void _Ready() {} public virtual void _Ready() {}
public T GetNode<T>(string path) where T : Node => throw new InvalidOperationException(path); public T GetNode<T>(string path) where T : Node => throw new InvalidOperationException(path);
public T? GetNodeOrNull<T>(string path) where T : Node => default; public T? GetNodeOrNull<T>(string path) where T : Node => default;
} }
""";
private const string HBoxContainerType = """
public class HBoxContainer : Node public class HBoxContainer : Node
{ {
} }
} """;
namespace TestApp [Test]
public async Task Generates_InferredUniqueNameBindings_And_ReadyHook_WhenReadyIsMissing()
{ {
string source = CreateGetNodeSource(
FullGetNodeAttributeDeclaration,
"""
public partial class TopBar : HBoxContainer public partial class TopBar : HBoxContainer
{ {
[GetNode] [GetNode]
@ -58,8 +85,8 @@ public class GetNodeGeneratorTests
[GetNode] [GetNode]
private HBoxContainer m_rightContainer = null!; private HBoxContainer m_rightContainer = null!;
} }
} """,
"""; HBoxContainerType);
const string expected = """ const string expected = """
// <auto-generated /> // <auto-generated />
@ -88,54 +115,15 @@ public class GetNodeGeneratorTests
await GeneratorTest<GetNodeGenerator>.RunAsync( await GeneratorTest<GetNodeGenerator>.RunAsync(
source, source,
("TestApp_TopBar.GetNode.g.cs", expected)); ("TestApp_TopBar.GetNode.g.cs", expected)).ConfigureAwait(false);
} }
[Test] [Test]
public async Task Generates_ManualInjectionOnly_WhenReadyAlreadyExists() public async Task Generates_ManualInjectionOnly_WhenReadyAlreadyExists()
{ {
const string source = """ string source = CreateGetNodeSource(
using System; FullGetNodeAttributeDeclaration,
using GFramework.Godot.SourceGenerators.Abstractions; """
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions
{
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class GetNodeAttribute : Attribute
{
public GetNodeAttribute() {}
public GetNodeAttribute(string path) { Path = path; }
public string? Path { get; set; }
public bool Required { get; set; } = true;
public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto;
}
public enum NodeLookupMode
{
Auto = 0,
UniqueName = 1,
RelativePath = 2,
AbsolutePath = 3
}
}
namespace Godot
{
public class Node
{
public virtual void _Ready() {}
public T GetNode<T>(string path) where T : Node => throw new InvalidOperationException(path);
public T? GetNodeOrNull<T>(string path) where T : Node => default;
}
public class HBoxContainer : Node
{
}
}
namespace TestApp
{
public partial class TopBar : HBoxContainer public partial class TopBar : HBoxContainer
{ {
[GetNode("%LeftContainer")] [GetNode("%LeftContainer")]
@ -149,8 +137,8 @@ public class GetNodeGeneratorTests
__InjectGetNodes_Generated(); __InjectGetNodes_Generated();
} }
} }
} """,
"""; HBoxContainerType);
const string expected = """ const string expected = """
// <auto-generated /> // <auto-generated />
@ -171,7 +159,7 @@ public class GetNodeGeneratorTests
await GeneratorTest<GetNodeGenerator>.RunAsync( await GeneratorTest<GetNodeGenerator>.RunAsync(
source, source,
("TestApp_TopBar.GetNode.g.cs", expected)); ("TestApp_TopBar.GetNode.g.cs", expected)).ConfigureAwait(false);
} }
[Test] [Test]
@ -234,47 +222,15 @@ public class GetNodeGeneratorTests
.WithSpan(39, 24, 39, 38) .WithSpan(39, 24, 39, 38)
.WithArguments("_leftContainer")); .WithArguments("_leftContainer"));
await test.RunAsync(); await test.RunAsync().ConfigureAwait(false);
} }
[Test] [Test]
public async Task Reports_Diagnostic_When_Generated_Injection_Method_Name_Already_Exists() public async Task Reports_Diagnostic_When_Generated_Injection_Method_Name_Already_Exists()
{ {
const string source = """ string source = CreateGetNodeSource(
using System; MinimalGetNodeAttributeDeclaration,
using GFramework.Godot.SourceGenerators.Abstractions; """
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions
{
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class GetNodeAttribute : Attribute
{
public GetNodeAttribute() {}
}
public enum NodeLookupMode
{
Auto = 0
}
}
namespace Godot
{
public class Node
{
public virtual void _Ready() {}
public T GetNode<T>(string path) where T : Node => throw new InvalidOperationException(path);
public T? GetNodeOrNull<T>(string path) where T : Node => default;
}
public class HBoxContainer : Node
{
}
}
namespace TestApp
{
public partial class TopBar : HBoxContainer public partial class TopBar : HBoxContainer
{ {
[GetNode] [GetNode]
@ -284,8 +240,8 @@ public class GetNodeGeneratorTests
{ {
} }
} }
} """,
"""; HBoxContainerType);
var test = new CSharpSourceGeneratorTest<GetNodeGenerator, DefaultVerifier> var test = new CSharpSourceGeneratorTest<GetNodeGenerator, DefaultVerifier>
{ {
@ -301,6 +257,39 @@ public class GetNodeGeneratorTests
.WithLocation(0) .WithLocation(0)
.WithArguments("TopBar", "__InjectGetNodes_Generated")); .WithArguments("TopBar", "__InjectGetNodes_Generated"));
await test.RunAsync(); await test.RunAsync().ConfigureAwait(false);
}
private static string CreateGetNodeSource(
string attributeDeclaration,
string testAppSource,
params string[] godotTypes)
{
string[] allGodotTypes = new string[godotTypes.Length + 1];
allGodotTypes[0] = NodeWithReadyAndLookupMethods;
Array.Copy(godotTypes, 0, allGodotTypes, 1, godotTypes.Length);
string godotSource = string.Join($"{Environment.NewLine}{Environment.NewLine}", allGodotTypes);
return $$"""
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions
{
{{attributeDeclaration}}
}
namespace Godot
{
{{godotSource}}
}
namespace TestApp
{
{{testAppSource}}
}
""";
} }
} }

View File

@ -8,32 +8,13 @@ namespace GFramework.Godot.SourceGenerators.Tests.Project;
[TestFixture] [TestFixture]
public class GodotProjectMetadataGeneratorTests public class GodotProjectMetadataGeneratorTests
{ {
/// <summary> private const string AutoLoadProjectFile = """
/// 验证会根据 AutoLoad 与 Input Action 生成稳定的强类型入口。
/// </summary>
[Test]
public void Run_Should_Generate_AutoLoads_And_InputActions()
{
var source = CreateSource(
"""
namespace TestApp
{
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
[AutoLoad("GameServices")]
public partial class GameServices : Node
{
}
}
""",
includeAutoLoadAttribute: true);
const string projectFile = """
[autoload] [autoload]
GameServices="*res://autoload/game_services.tscn" GameServices="*res://autoload/game_services.tscn"
AudioBus="*res://autoload/audio_bus.gd" AudioBus="*res://autoload/audio_bus.gd"
""";
private const string InputActionsProjectFile = """
[input] [input]
move_up={ move_up={
"deadzone": 0.5 "deadzone": 0.5
@ -43,7 +24,7 @@ public class GodotProjectMetadataGeneratorTests
} }
"""; """;
const string expectedAutoLoads = """ private const string ExpectedAutoLoads = """
// <auto-generated /> // <auto-generated />
#nullable enable #nullable enable
@ -127,7 +108,7 @@ public class GodotProjectMetadataGeneratorTests
"""; """;
const string expectedInputActions = """ private const string ExpectedInputActions = """
// <auto-generated /> // <auto-generated />
#nullable enable #nullable enable
@ -152,19 +133,40 @@ public class GodotProjectMetadataGeneratorTests
"""; """;
/// <summary>
/// 验证会根据 AutoLoad 与 Input Action 生成稳定的强类型入口。
/// </summary>
[Test]
public void Run_Should_Generate_AutoLoads_And_InputActions()
{
var source = CreateSource(
"""
namespace TestApp
{
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
[AutoLoad("GameServices")]
public partial class GameServices : Node
{
}
}
""",
includeAutoLoadAttribute: true);
var result = AdditionalTextGeneratorTestDriver.Run<GodotProjectMetadataGenerator>( var result = AdditionalTextGeneratorTestDriver.Run<GodotProjectMetadataGenerator>(
source, source,
("project.godot", projectFile)); ("project.godot", $"{AutoLoadProjectFile}\n\n{InputActionsProjectFile}"));
var generatedSources = AdditionalTextGeneratorTestDriver.ToGeneratedSourceMap(result); var generatedSources = AdditionalTextGeneratorTestDriver.ToGeneratedSourceMap(result);
Assert.That(result.Results.Single().Diagnostics, Is.Empty); Assert.That(result.Results.Single().Diagnostics, Is.Empty);
Assert.That( Assert.That(
generatedSources["GFramework_Godot_Generated_AutoLoads.g.cs"], generatedSources["GFramework_Godot_Generated_AutoLoads.g.cs"],
Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(expectedAutoLoads))); Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(ExpectedAutoLoads)));
Assert.That( Assert.That(
generatedSources["GFramework_Godot_Generated_InputActions.g.cs"], generatedSources["GFramework_Godot_Generated_InputActions.g.cs"],
Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(expectedInputActions))); Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(ExpectedInputActions)));
} }
/// <summary> /// <summary>

View File

@ -6,17 +6,7 @@ namespace GFramework.Godot.SourceGenerators.Tests.Registration;
[TestFixture] [TestFixture]
public class AutoRegisterExportedCollectionsGeneratorTests public class AutoRegisterExportedCollectionsGeneratorTests
{ {
[Test] private const string StandardAttributeDeclarations = """
public async Task Generates_Batch_Registration_Method_For_Annotated_Collections()
{
const string source = """
#nullable enable
using System;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
@ -25,10 +15,24 @@ public class AutoRegisterExportedCollectionsGeneratorTests
{ {
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { } public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
} }
} """;
namespace TestApp private const string MultiDeclarationAttributeDeclarations = """
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{ {
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
""";
[Test]
public async Task Generates_Batch_Registration_Method_For_Annotated_Collections()
{
string source = CreateSource(
"""
public sealed class IntRegistry public sealed class IntRegistry
{ {
public void Register(int value) { } public void Register(int value) { }
@ -46,8 +50,8 @@ public class AutoRegisterExportedCollectionsGeneratorTests
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public List<int>? Values { get; } = new(); public List<int>? Values { get; } = new();
} }
} """,
"""; nullableEnabled: true);
const string expected = """ const string expected = """
// <auto-generated /> // <auto-generated />
@ -77,7 +81,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync( await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source, source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)); ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
} }
[Test] [Test]
@ -131,32 +135,14 @@ public class AutoRegisterExportedCollectionsGeneratorTests
.WithLocation(0) .WithLocation(0)
.WithArguments("Values")); .WithArguments("Values"));
await test.RunAsync(); await test.RunAsync().ConfigureAwait(false);
} }
[Test] [Test]
public async Task Generates_Batch_Registration_Method_When_Register_Method_Uses_Array_Parameter() public async Task Generates_Batch_Registration_Method_When_Register_Method_Uses_Array_Parameter()
{ {
const string source = """ string source = CreateSource(
#nullable enable """
using System;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
}
namespace TestApp
{
public sealed class ArrayRegistry public sealed class ArrayRegistry
{ {
public void Register(int[] value) { } public void Register(int[] value) { }
@ -170,8 +156,8 @@ public class AutoRegisterExportedCollectionsGeneratorTests
[RegisterExportedCollection(nameof(_registry), nameof(ArrayRegistry.Register))] [RegisterExportedCollection(nameof(_registry), nameof(ArrayRegistry.Register))]
public List<int[]> Values { get; } = new(); public List<int[]> Values { get; } = new();
} }
} """,
"""; nullableEnabled: true);
const string expected = """ const string expected = """
// <auto-generated /> // <auto-generated />
@ -197,32 +183,14 @@ public class AutoRegisterExportedCollectionsGeneratorTests
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync( await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source, source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)); ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
} }
[Test] [Test]
public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Inherited_Interface() public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Inherited_Interface()
{ {
const string source = """ string source = CreateSource(
#nullable enable """
using System;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
}
namespace TestApp
{
public interface IKeyValue<TKey, TValue> public interface IKeyValue<TKey, TValue>
{ {
} }
@ -248,8 +216,8 @@ public class AutoRegisterExportedCollectionsGeneratorTests
[RegisterExportedCollection(nameof(_registry), "Registry")] [RegisterExportedCollection(nameof(_registry), "Registry")]
public List<IntConfig>? Values { get; } = new(); public List<IntConfig>? Values { get; } = new();
} }
} """,
"""; nullableEnabled: true);
const string expected = """ const string expected = """
// <auto-generated /> // <auto-generated />
@ -275,7 +243,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync( await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source, source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)); ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
} }
[Test] [Test]
@ -334,32 +302,14 @@ public class AutoRegisterExportedCollectionsGeneratorTests
.WithLocation(0) .WithLocation(0)
.WithArguments("Register", "_registry", "Values")); .WithArguments("Register", "_registry", "Values"));
await test.RunAsync(); await test.RunAsync().ConfigureAwait(false);
} }
[Test] [Test]
public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Base_Class() public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Base_Class()
{ {
const string source = """ string source = CreateSource(
#nullable enable """
using System;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
}
namespace TestApp
{
public class BaseRegistry public class BaseRegistry
{ {
public void Register(int value) { } public void Register(int value) { }
@ -377,8 +327,8 @@ public class AutoRegisterExportedCollectionsGeneratorTests
[RegisterExportedCollection(nameof(_registry), nameof(BaseRegistry.Register))] [RegisterExportedCollection(nameof(_registry), nameof(BaseRegistry.Register))]
public List<int>? Values { get; } = new(); public List<int>? Values { get; } = new();
} }
} """,
"""; nullableEnabled: true);
const string expected = """ const string expected = """
// <auto-generated /> // <auto-generated />
@ -404,32 +354,14 @@ public class AutoRegisterExportedCollectionsGeneratorTests
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync( await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source, source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)); ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
} }
[Test] [Test]
public async Task Generates_Batch_Registration_Method_When_Registry_Member_Comes_From_Base_Class() public async Task Generates_Batch_Registration_Method_When_Registry_Member_Comes_From_Base_Class()
{ {
const string source = """ string source = CreateSource(
#nullable enable """
using System;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
}
namespace TestApp
{
public sealed class IntRegistry public sealed class IntRegistry
{ {
public void Register(int value) { } public void Register(int value) { }
@ -446,8 +378,8 @@ public class AutoRegisterExportedCollectionsGeneratorTests
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public List<int>? Values { get; } = new(); public List<int>? Values { get; } = new();
} }
} """,
"""; nullableEnabled: true);
const string expected = """ const string expected = """
// <auto-generated /> // <auto-generated />
@ -473,31 +405,14 @@ public class AutoRegisterExportedCollectionsGeneratorTests
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync( await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source, source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)); ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
} }
[Test] [Test]
public async Task Reports_Diagnostic_When_Collection_Member_Is_Not_Instance_Readable() public async Task Reports_Diagnostic_When_Collection_Member_Is_Not_Instance_Readable()
{ {
const string source = """ string source = CreateSource(
using System; """
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
}
namespace TestApp
{
public sealed class IntRegistry public sealed class IntRegistry
{ {
public void Register(int value) { } public void Register(int value) { }
@ -517,30 +432,20 @@ public class AutoRegisterExportedCollectionsGeneratorTests
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public List<int> {|#2:WriteOnlyValues|} { set { } } public List<int> {|#2:WriteOnlyValues|} { set { } }
} }
} """);
""";
var test = new CSharpSourceGeneratorTest<AutoRegisterExportedCollectionsGenerator, DefaultVerifier> await VerifyDiagnosticsAsync(
{ source,
TestState = skipGeneratedSourcesCheck: true,
{ new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" },
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
};
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
.WithLocation(0) .WithLocation(0)
.WithArguments("StaticValues")); .WithArguments("StaticValues"),
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error) new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
.WithLocation(1) .WithLocation(1)
.WithArguments("StaticPropertyValues")); .WithArguments("StaticPropertyValues"),
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error) new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
.WithLocation(2) .WithLocation(2)
.WithArguments("WriteOnlyValues")); .WithArguments("WriteOnlyValues")).ConfigureAwait(false);
await test.RunAsync();
} }
[Test] [Test]
@ -595,7 +500,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests
.WithLocation(0) .WithLocation(0)
.WithArguments("_registry", "Values")); .WithArguments("_registry", "Values"));
await test.RunAsync(); await test.RunAsync().ConfigureAwait(false);
} }
[Test] [Test]
@ -650,7 +555,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests
.WithLocation(0) .WithLocation(0)
.WithArguments("Register", "_registry", "Values")); .WithArguments("Register", "_registry", "Values"));
await test.RunAsync(); await test.RunAsync().ConfigureAwait(false);
} }
[Test] [Test]
@ -705,32 +610,14 @@ public class AutoRegisterExportedCollectionsGeneratorTests
.WithLocation(0) .WithLocation(0)
.WithArguments("Values")); .WithArguments("Values"));
await test.RunAsync(); await test.RunAsync().ConfigureAwait(false);
} }
[Test] [Test]
public async Task Generates_Only_One_Source_When_Multiple_Partial_Declarations_Are_Annotated() public async Task Generates_Only_One_Source_When_Multiple_Partial_Declarations_Are_Annotated()
{ {
const string source = """ string source = CreateSource(
#nullable enable """
using System;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
}
namespace TestApp
{
public sealed class IntRegistry public sealed class IntRegistry
{ {
public void Register(int value) { } public void Register(int value) { }
@ -748,8 +635,9 @@ public class AutoRegisterExportedCollectionsGeneratorTests
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public List<int>? Values { get; } = new(); public List<int>? Values { get; } = new();
} }
} """,
"""; nullableEnabled: true,
allowMultipleDeclarations: true);
const string expected = """ const string expected = """
// <auto-generated /> // <auto-generated />
@ -775,6 +663,61 @@ public class AutoRegisterExportedCollectionsGeneratorTests
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync( await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source, source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)); ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
}
private static string CreateSource(
string applicationSource,
bool nullableEnabled = false,
bool allowMultipleDeclarations = false)
{
string nullableDirective = nullableEnabled ? "#nullable enable\n" : string.Empty;
string attributeDeclarations = allowMultipleDeclarations
? MultiDeclarationAttributeDeclarations
: StandardAttributeDeclarations;
return $$"""
{{nullableDirective}}using System;
using System.Collections;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
{{attributeDeclarations}}
}
namespace TestApp
{
{{applicationSource}}
}
""";
}
private static async Task VerifyDiagnosticsAsync(
string source,
bool skipGeneratedSourcesCheck = false,
params DiagnosticResult[] expectedDiagnostics)
{
var test = new CSharpSourceGeneratorTest<AutoRegisterExportedCollectionsGenerator, DefaultVerifier>
{
TestState =
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" }
};
if (skipGeneratedSourcesCheck)
{
test.TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck;
}
foreach (DiagnosticResult expectedDiagnostic in expectedDiagnostics)
{
test.ExpectedDiagnostics.Add(expectedDiagnostic);
}
await test.RunAsync().ConfigureAwait(false);
} }
} }

View File

@ -72,19 +72,8 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
if (bindNodeSignalAttribute is null || godotNodeSymbol is null) if (bindNodeSignalAttribute is null || godotNodeSymbol is null)
return; return;
// 缓存每个方法上已解析的特性,避免在筛选和生成阶段重复做语义查询。 var methodAttributes = BuildMethodAttributeMap(candidates, bindNodeSignalAttribute);
var methodAttributes = candidates var methodCandidates = CollectMethodCandidates(methodAttributes);
.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();
foreach (var group in GroupByContainingType(methodCandidates)) foreach (var group in GroupByContainingType(methodCandidates))
{ {
@ -99,19 +88,7 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
UnbindMethodName)) UnbindMethodName))
continue; continue;
var bindings = new List<SignalBindingInfo>(); var bindings = CollectBindings(context, group, methodAttributes, godotNodeSymbol);
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);
}
}
if (bindings.Count == 0) if (bindings.Count == 0)
continue; continue;
@ -170,6 +147,78 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
binding = default!; binding = default!;
if (candidate.MethodSymbol.IsStatic) if (candidate.MethodSymbol.IsStatic)
{
ReportStaticMethodDiagnostic(context, candidate, attribute);
return false;
}
if (!TryResolveBindingTargetNames(context, candidate, attribute, out var nodeFieldName, out var signalName))
return false;
if (!TryFindCompatibleField(context, candidate, attribute, godotNodeSymbol, nodeFieldName, out var fieldSymbol))
return false;
if (!TryFindCompatibleEvent(context, candidate, attribute, fieldSymbol, signalName, out var eventSymbol))
return false;
if (!IsMethodCompatibleWithEvent(candidate.MethodSymbol, eventSymbol))
{
ReportIncompatibleSignatureDiagnostic(context, candidate, attribute, eventSymbol, fieldSymbol);
return false;
}
binding = new SignalBindingInfo(fieldSymbol, eventSymbol, candidate.MethodSymbol);
return true;
}
private static Dictionary<MethodCandidate, IReadOnlyList<AttributeData>> BuildMethodAttributeMap(
ImmutableArray<MethodCandidate?> 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<MethodCandidate> CollectMethodCandidates(
IReadOnlyDictionary<MethodCandidate, IReadOnlyList<AttributeData>> methodAttributes)
{
return methodAttributes
.Where(static pair => pair.Value.Count > 0)
.Select(static pair => pair.Key)
.ToList();
}
private static List<SignalBindingInfo> CollectBindings(
SourceProductionContext context,
TypeGroup group,
IReadOnlyDictionary<MethodCandidate, IReadOnlyList<AttributeData>> methodAttributes,
INamedTypeSymbol godotNodeSymbol)
{
var bindings = new List<SignalBindingInfo>();
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( ReportMethodDiagnostic(
context, context,
@ -177,10 +226,38 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
candidate, candidate,
attribute, attribute,
candidate.MethodSymbol.Name); 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; return false;
} }
if (!TryResolveCtorString(attribute, 0, out var nodeFieldName)) 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( ReportMethodDiagnostic(
context, context,
@ -188,24 +265,47 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
candidate, candidate,
attribute, attribute,
candidate.MethodSymbol.Name, candidate.MethodSymbol.Name,
"nodeFieldName"); argumentName);
return false;
} }
if (!TryResolveCtorString(attribute, 1, out var signalName)) private static bool TryFindCompatibleField(
SourceProductionContext context,
MethodCandidate candidate,
AttributeData attribute,
INamedTypeSymbol godotNodeSymbol,
string nodeFieldName,
out IFieldSymbol fieldSymbol)
{ {
ReportMethodDiagnostic( fieldSymbol = null!;
context,
BindNodeSignalDiagnostics.InvalidConstructorArgument, var resolvedField = FindField(candidate.MethodSymbol.ContainingType, nodeFieldName);
candidate, if (resolvedField is null)
attribute, {
candidate.MethodSymbol.Name, ReportNodeFieldNotFoundDiagnostic(context, candidate, attribute, nodeFieldName);
"signalName");
return false; return false;
} }
var fieldSymbol = FindField(candidate.MethodSymbol.ContainingType, nodeFieldName); if (resolvedField.IsStatic)
if (fieldSymbol is null) {
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( ReportMethodDiagnostic(
context, context,
@ -215,10 +315,13 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
candidate.MethodSymbol.Name, candidate.MethodSymbol.Name,
nodeFieldName, nodeFieldName,
candidate.MethodSymbol.ContainingType.Name); candidate.MethodSymbol.ContainingType.Name);
return false;
} }
if (fieldSymbol.IsStatic) private static void ReportNodeFieldMustBeInstanceDiagnostic(
SourceProductionContext context,
MethodCandidate candidate,
AttributeData attribute,
IFieldSymbol fieldSymbol)
{ {
ReportMethodDiagnostic( ReportMethodDiagnostic(
context, context,
@ -227,10 +330,13 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
attribute, attribute,
candidate.MethodSymbol.Name, candidate.MethodSymbol.Name,
fieldSymbol.Name); fieldSymbol.Name);
return false;
} }
if (!fieldSymbol.Type.IsAssignableTo(godotNodeSymbol)) private static void ReportFieldTypeMustDeriveFromNodeDiagnostic(
SourceProductionContext context,
MethodCandidate candidate,
AttributeData attribute,
IFieldSymbol fieldSymbol)
{ {
ReportMethodDiagnostic( ReportMethodDiagnostic(
context, context,
@ -238,11 +344,35 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
candidate, candidate,
attribute, attribute,
fieldSymbol.Name); 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; return false;
} }
var eventSymbol = FindEvent(fieldSymbol.Type, signalName); eventSymbol = resolvedEvent;
if (eventSymbol is null) return true;
}
private static void ReportSignalNotFoundDiagnostic(
SourceProductionContext context,
MethodCandidate candidate,
AttributeData attribute,
IFieldSymbol fieldSymbol,
string signalName)
{ {
ReportMethodDiagnostic( ReportMethodDiagnostic(
context, context,
@ -251,10 +381,14 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
attribute, attribute,
fieldSymbol.Name, fieldSymbol.Name,
signalName); signalName);
return false;
} }
if (!IsMethodCompatibleWithEvent(candidate.MethodSymbol, eventSymbol)) private static void ReportIncompatibleSignatureDiagnostic(
SourceProductionContext context,
MethodCandidate candidate,
AttributeData attribute,
IEventSymbol eventSymbol,
IFieldSymbol fieldSymbol)
{ {
ReportMethodDiagnostic( ReportMethodDiagnostic(
context, context,
@ -264,11 +398,6 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
candidate.MethodSymbol.Name, candidate.MethodSymbol.Name,
eventSymbol.Name, eventSymbol.Name,
fieldSymbol.Name); fieldSymbol.Name);
return false;
}
binding = new SignalBindingInfo(fieldSymbol, eventSymbol, candidate.MethodSymbol);
return true;
} }
private static void ReportMethodDiagnostic( private static void ReportMethodDiagnostic(
@ -404,11 +533,7 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
{ {
return typeSymbol.GetMembers() return typeSymbol.GetMembers()
.OfType<IMethodSymbol>() .OfType<IMethodSymbol>()
.FirstOrDefault(method => .FirstOrDefault(method => IsParameterlessInstanceMethod(method, methodName));
method.Name == methodName &&
!method.IsStatic &&
method.Parameters.Length == 0 &&
method.MethodKind == MethodKind.Ordinary);
} }
private static bool CallsGeneratedMethod( 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) private static bool IsBindNodeSignalAttributeName(NameSyntax attributeName)
{ {
var simpleName = GetAttributeSimpleName(attributeName); var simpleName = GetAttributeSimpleName(attributeName);

View File

@ -259,11 +259,7 @@ public sealed class GetNodeGenerator : IIncrementalGenerator
{ {
return typeSymbol.GetMembers() return typeSymbol.GetMembers()
.OfType<IMethodSymbol>() .OfType<IMethodSymbol>()
.FirstOrDefault(static method => .FirstOrDefault(static method => IsParameterlessInstanceMethod(method, "_Ready"));
method.Name == "_Ready" &&
!method.IsStatic &&
method.Parameters.Length == 0 &&
method.MethodKind == MethodKind.Ordinary);
} }
private static bool CallsGeneratedInjection(IMethodSymbol readyMethod) private static bool CallsGeneratedInjection(IMethodSymbol readyMethod)
@ -306,6 +302,14 @@ public sealed class GetNodeGenerator : IIncrementalGenerator
return attribute.GetNamedArgument("Required", true); 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( private static bool TryResolvePath(
IFieldSymbol fieldSymbol, IFieldSymbol fieldSymbol,
AttributeData attribute, AttributeData attribute,
@ -373,7 +377,10 @@ public sealed class GetNodeGenerator : IIncrementalGenerator
if (!string.Equals(namedArgument.Key, "Lookup", StringComparison.Ordinal)) if (!string.Equals(namedArgument.Key, "Lookup", StringComparison.Ordinal))
continue; continue;
if (namedArgument.Value.Type?.ToDisplayString() != GetNodeLookupModeMetadataName) if (!string.Equals(
namedArgument.Value.Type?.ToDisplayString(),
GetNodeLookupModeMetadataName,
StringComparison.Ordinal))
continue; continue;
if (namedArgument.Value.Value is int value) if (namedArgument.Value.Value is int value)

View File

@ -126,7 +126,27 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator
var explicitMappings = new Dictionary<string, List<INamedTypeSymbol>>(StringComparer.Ordinal); var explicitMappings = new Dictionary<string, List<INamedTypeSymbol>>(StringComparer.Ordinal);
var implicitCandidates = new Dictionary<string, List<INamedTypeSymbol>>(StringComparer.Ordinal); var implicitCandidates = new Dictionary<string, List<INamedTypeSymbol>>(StringComparer.Ordinal);
CollectMappingCandidates(
context,
typeCandidates,
autoLoadAttributeSymbol,
godotNodeSymbol,
projectAutoLoadNames,
explicitMappings,
implicitCandidates);
return ResolveTypedMappings(context, projectAutoLoadNames, explicitMappings, implicitCandidates);
}
private static void CollectMappingCandidates(
SourceProductionContext context,
IReadOnlyList<GodotTypeCandidate> typeCandidates,
INamedTypeSymbol? autoLoadAttributeSymbol,
INamedTypeSymbol godotNodeSymbol,
ISet<string> projectAutoLoadNames,
IDictionary<string, List<INamedTypeSymbol>> explicitMappings,
IDictionary<string, List<INamedTypeSymbol>> implicitCandidates)
{
foreach (var candidate in typeCandidates) foreach (var candidate in typeCandidates)
{ {
var typeSymbol = candidate.TypeSymbol; var typeSymbol = candidate.TypeSymbol;
@ -176,7 +196,14 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator
explicitList.Add(typeSymbol); explicitList.Add(typeSymbol);
} }
}
private static Dictionary<string, INamedTypeSymbol> ResolveTypedMappings(
SourceProductionContext context,
IEnumerable<string> projectAutoLoadNames,
IReadOnlyDictionary<string, List<INamedTypeSymbol>> explicitMappings,
IReadOnlyDictionary<string, List<INamedTypeSymbol>> implicitCandidates)
{
var resolvedMappings = new Dictionary<string, INamedTypeSymbol>(StringComparer.Ordinal); var resolvedMappings = new Dictionary<string, INamedTypeSymbol>(StringComparer.Ordinal);
foreach (var projectAutoLoadName in projectAutoLoadNames.OrderBy(static name => name, StringComparer.Ordinal)) foreach (var projectAutoLoadName in projectAutoLoadNames.OrderBy(static name => name, StringComparer.Ordinal))
@ -407,6 +434,20 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator
builder.AppendLine("{"); builder.AppendLine("{");
foreach (var member in members) foreach (var member in members)
{
AppendAutoLoadMemberSource(builder, member);
}
AppendGetRequiredNodeSource(builder);
AppendTryGetNodeSource(builder);
builder.AppendLine("}");
return builder.ToString();
}
private static void AppendAutoLoadMemberSource(
StringBuilder builder,
GeneratedAutoLoadMember member)
{ {
builder.AppendLine(" /// <summary>"); builder.AppendLine(" /// <summary>");
builder.AppendLine($" /// 获取 AutoLoad <c>{member.AutoLoadName}</c>。"); builder.AppendLine($" /// 获取 AutoLoad <c>{member.AutoLoadName}</c>。");
@ -426,6 +467,8 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator
builder.AppendLine(); builder.AppendLine();
} }
private static void AppendGetRequiredNodeSource(StringBuilder builder)
{
builder.AppendLine(" /// <summary>"); builder.AppendLine(" /// <summary>");
builder.AppendLine(" /// 获取一个必填的 AutoLoad 节点;缺失时抛出异常。"); builder.AppendLine(" /// 获取一个必填的 AutoLoad 节点;缺失时抛出异常。");
builder.AppendLine(" /// </summary>"); builder.AppendLine(" /// </summary>");
@ -444,6 +487,10 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator
" throw new global::System.InvalidOperationException($\"AutoLoad '{autoLoadName}' is not available on the active SceneTree root.\");"); " throw new global::System.InvalidOperationException($\"AutoLoad '{autoLoadName}' is not available on the active SceneTree root.\");");
builder.AppendLine(" }"); builder.AppendLine(" }");
builder.AppendLine(); builder.AppendLine();
}
private static void AppendTryGetNodeSource(StringBuilder builder)
{
builder.AppendLine(" /// <summary>"); builder.AppendLine(" /// <summary>");
builder.AppendLine(" /// 尝试从当前 SceneTree 根节点解析 AutoLoad。"); builder.AppendLine(" /// 尝试从当前 SceneTree 根节点解析 AutoLoad。");
builder.AppendLine(" /// </summary>"); builder.AppendLine(" /// </summary>");
@ -470,9 +517,6 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator
builder.AppendLine(" value = root.GetNodeOrNull<TNode>($\"/root/{autoLoadName}\");"); builder.AppendLine(" value = root.GetNodeOrNull<TNode>($\"/root/{autoLoadName}\");");
builder.AppendLine(" return value is not null;"); builder.AppendLine(" return value is not null;");
builder.AppendLine(" }"); builder.AppendLine(" }");
builder.AppendLine("}");
return builder.ToString();
} }
private static string GenerateInputActionsSource(IReadOnlyList<GeneratedInputActionMember> members) private static string GenerateInputActionsSource(IReadOnlyList<GeneratedInputActionMember> members)
@ -530,45 +574,16 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator
if (string.IsNullOrWhiteSpace(content) || content.StartsWith(";", StringComparison.Ordinal)) if (string.IsNullOrWhiteSpace(content) || content.StartsWith(";", StringComparison.Ordinal))
continue; continue;
if (content.StartsWith("[", StringComparison.Ordinal) && content.EndsWith("]", StringComparison.Ordinal)) if (TryUpdateSection(content, ref currentSection))
{
currentSection = content.Substring(1, content.Length - 2).Trim();
continue; continue;
}
if (!TryParseAssignment(content, out var key, out var value)) if (!TryParseAssignment(content, out var key, out var value))
continue; continue;
if (string.Equals(currentSection, "autoload", StringComparison.OrdinalIgnoreCase)) if (TryCollectAutoLoadEntry(file, currentSection, key, value, seenAutoLoads, autoLoads, diagnostics))
{
if (!seenAutoLoads.Add(key))
{
diagnostics.Add(Diagnostic.Create(
GodotProjectDiagnostics.DuplicateAutoLoadEntry,
CreateFileLocation(file.Path),
key));
continue; continue;
}
autoLoads.Add(new ProjectAutoLoadEntry( TryCollectInputAction(currentSection, key, seenInputActions, inputActions, diagnostics, file.Path);
key,
NormalizeProjectPath(value)));
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);
}
} }
return new ProjectMetadataParseResult( return new ProjectMetadataParseResult(
@ -578,6 +593,68 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator
diagnostics.ToImmutableArray()); 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<string> seenAutoLoads,
ICollection<ProjectAutoLoadEntry> autoLoads,
ICollection<Diagnostic> 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<string> seenInputActions,
ICollection<string> inputActions,
ICollection<Diagnostic> 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) private static string NormalizeProjectPath(string rawValue)
{ {
var trimmed = rawValue.Trim(); var trimmed = rawValue.Trim();

View File

@ -190,6 +190,48 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
{ {
registration = null!; 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)) if (!IsInstanceReadableMember(collectionMember))
{ {
context.ReportDiagnostic(Diagnostic.Create( context.ReportDiagnostic(Diagnostic.Create(
@ -199,17 +241,11 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
return false; return false;
} }
var collectionType = collectionMember switch var resolvedType = GetMemberType(collectionMember);
{ if (resolvedType is null)
IFieldSymbol field => field.Type,
IPropertySymbol property => property.Type,
_ => null
};
if (collectionType is null)
return false; return false;
if (!collectionType.IsAssignableTo(enumerableType)) if (!resolvedType.IsAssignableTo(enumerableType))
{ {
context.ReportDiagnostic(Diagnostic.Create( context.ReportDiagnostic(Diagnostic.Create(
AutoRegisterExportedCollectionsDiagnostics.CollectionTypeMustBeEnumerable, AutoRegisterExportedCollectionsDiagnostics.CollectionTypeMustBeEnumerable,
@ -218,12 +254,35 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
return false; return false;
} }
if (!TryGetRegistrationAttributeArguments(context, collectionMember, attribute, out var registryMemberName, collectionType = resolvedType;
out var registerMethodName)) 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; return false;
}
var registryMember = FindRegistryMember(ownerType, registryMemberName); var registryMember = FindRegistryMember(ownerType, registryMemberName);
if (registryMember is null) if (registryMember is null)
{ {
context.ReportDiagnostic(Diagnostic.Create( context.ReportDiagnostic(Diagnostic.Create(
@ -246,18 +305,24 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
return false; return false;
} }
var registryType = registryMember switch var resolvedRegistryType = GetMemberType(registryMember) as INamedTypeSymbol;
{ if (resolvedRegistryType is null)
IFieldSymbol field => field.Type as INamedTypeSymbol,
IPropertySymbol property => property.Type as INamedTypeSymbol,
_ => null
};
if (registryType is null)
return false; return false;
var elementType = TryGetElementType(collectionType); registryType = resolvedRegistryType;
if (elementType is null) 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 // Non-generic IEnumerable exposes elements as object at compile time, which is not safe
// for validating or generating a strongly typed registry call. // for validating or generating a strongly typed registry call.
@ -268,26 +333,33 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
return false; 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 => .Any(method =>
!method.IsStatic && !method.IsStatic &&
method.Parameters.Length == 1 && method.Parameters.Length == 1 &&
compilation.IsSymbolAccessibleWithin(method, ownerType) && compilation.IsSymbolAccessibleWithin(method, ownerType) &&
CanAcceptElementType(compilation, elementType, method.Parameters[0].Type)); CanAcceptElementType(compilation, elementType, method.Parameters[0].Type));
if (!hasCompatibleMethod)
{
context.ReportDiagnostic(Diagnostic.Create(
AutoRegisterExportedCollectionsDiagnostics.RegisterMethodNotFound,
collectionMember.Locations.FirstOrDefault() ?? Location.None,
registerMethodName,
registryMemberName,
collectionMember.Name));
return false;
} }
registration = new RegistrationSpec(collectionMember.Name, registryMemberName, registerMethodName); private static ITypeSymbol? GetMemberType(ISymbol member)
return true; {
return member switch
{
IFieldSymbol field => field.Type,
IPropertySymbol property => property.Type,
_ => null
};
} }
private static bool IsInstanceReadableMember(ISymbol member) private static bool IsInstanceReadableMember(ISymbol member)

View File

@ -7,7 +7,6 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject> <IsTestProject>true</IsTestProject>
<WarningLevel>0</WarningLevel>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -11,6 +11,9 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
using System;
using System.Collections.Generic;
namespace GFramework.Godot.Setting.Data; namespace GFramework.Godot.Setting.Data;
/// <summary> /// <summary>
@ -20,24 +23,47 @@ public class LocalizationMap
{ {
private const string DefaultFrameworkLanguage = "eng"; private const string DefaultFrameworkLanguage = "eng";
private const string DefaultGodotLocale = "en"; private const string DefaultGodotLocale = "en";
private readonly Dictionary<string, string> _frameworkLanguageMap;
private readonly Dictionary<string, string> _languageMap;
/// <summary> /// <summary>
/// 用户语言 -> Godot locale 映射表 /// 使用默认的 Godot locale 与框架语言码映射初始化本地化设置
/// </summary> /// </summary>
public Dictionary<string, string> LanguageMap { get; set; } = new(StringComparer.Ordinal) public LocalizationMap()
: this(CreateDefaultLanguageMap(), CreateDefaultFrameworkLanguageMap())
{ {
{ "简体中文", "zh_CN" }, }
{ "English", "en" }
};
/// <summary> /// <summary>
/// 用户语言 -> GFramework 本地化语言码映射表。 /// 使用外部提供的映射初始化本地化设置。
/// 构造函数会复制输入字典,避免调用方在实例创建后继续修改内部状态。
/// </summary> /// </summary>
public Dictionary<string, string> FrameworkLanguageMap { get; set; } = new(StringComparer.Ordinal) /// <param name="languageMap">用户语言到 Godot locale 的映射。</param>
/// <param name="frameworkLanguageMap">用户语言到 GFramework 本地化语言码的映射。</param>
/// <exception cref="ArgumentNullException">
/// 当 <paramref name="languageMap" /> 或 <paramref name="frameworkLanguageMap" /> 为 <see langword="null" /> 时抛出。
/// </exception>
public LocalizationMap(
IReadOnlyDictionary<string, string> languageMap,
IReadOnlyDictionary<string, string> frameworkLanguageMap)
{ {
{ "简体中文", "zhs" }, ArgumentNullException.ThrowIfNull(languageMap);
{ "English", "eng" } ArgumentNullException.ThrowIfNull(frameworkLanguageMap);
};
// 复制外部输入,避免公共属性把可变集合直接暴露给调用方。
_languageMap = new Dictionary<string, string>(languageMap, StringComparer.Ordinal);
_frameworkLanguageMap = new Dictionary<string, string>(frameworkLanguageMap, StringComparer.Ordinal);
}
/// <summary>
/// 获取用户语言到 Godot locale 的只读映射表。
/// </summary>
public IReadOnlyDictionary<string, string> LanguageMap => _languageMap;
/// <summary>
/// 获取用户语言到 GFramework 本地化语言码的只读映射表。
/// </summary>
public IReadOnlyDictionary<string, string> FrameworkLanguageMap => _frameworkLanguageMap;
/// <summary> /// <summary>
/// 解析用户保存的语言值对应的 Godot locale。 /// 解析用户保存的语言值对应的 Godot locale。
@ -68,4 +94,22 @@ public class LocalizationMap
return FrameworkLanguageMap.GetValueOrDefault(storedLanguage, DefaultFrameworkLanguage); return FrameworkLanguageMap.GetValueOrDefault(storedLanguage, DefaultFrameworkLanguage);
} }
private static Dictionary<string, string> CreateDefaultLanguageMap()
{
return new Dictionary<string, string>(StringComparer.Ordinal)
{
{ "简体中文", "zh_CN" },
{ "English", "en" }
};
}
private static Dictionary<string, string> CreateDefaultFrameworkLanguageMap()
{
return new Dictionary<string, string>(StringComparer.Ordinal)
{
{ "简体中文", "zhs" },
{ "English", "eng" }
};
}
} }

View File

@ -684,75 +684,8 @@ public class CqrsHandlerRegistryGeneratorTests
"""; """;
/// <summary> // Keep large source fixtures at class scope so MA0051 reduction stays behavior-neutral for generator tests.
/// 验证生成器会为当前程序集中的 request、notification 和 stream 处理器生成稳定顺序的注册器。 private const string HiddenPointerResponseCompilationErrorSource = """
/// </summary>
[Test]
public async Task Generates_Assembly_Level_Cqrs_Handler_Registry()
{
await GeneratorTest<CqrsHandlerRegistryGenerator>.RunAsync(
AssemblyLevelCqrsHandlerRegistrySource,
("CqrsHandlerRegistry.g.cs", AssemblyLevelCqrsHandlerRegistryExpected));
}
/// <summary>
/// 验证当程序集包含生成代码无法合法引用的私有嵌套处理器时,生成器会在生成注册器内部执行定向反射注册,
/// 不再依赖程序集级 fallback marker。
/// </summary>
[Test]
public async Task
Generates_Visible_Handlers_And_Self_Registers_Private_Nested_Handler_When_Assembly_Contains_Hidden_Handler()
{
await GeneratorTest<CqrsHandlerRegistryGenerator>.RunAsync(
HiddenNestedHandlerSelfRegistrationSource,
("CqrsHandlerRegistry.g.cs", HiddenNestedHandlerSelfRegistrationExpected));
}
/// <summary>
/// 验证当隐藏实现类型的 handler 接口仍可被生成代码直接引用时,
/// 生成器只会定向反射实现类型,而不会再生成基于 <c>GetInterfaces()</c> 的接口发现辅助逻辑。
/// </summary>
[Test]
public async Task
Generates_Direct_Interface_Registrations_For_Hidden_Implementation_When_Handler_Interface_Is_Public()
{
await GeneratorTest<CqrsHandlerRegistryGenerator>.RunAsync(
HiddenImplementationDirectInterfaceRegistrationSource,
("CqrsHandlerRegistry.g.cs", HiddenImplementationDirectInterfaceRegistrationExpected));
}
/// <summary>
/// 验证精确重建路径会递归覆盖隐藏元素类型数组,
/// 使这类 handler interface 也能直接生成 closed service type而不再退回 <c>GetInterfaces()</c>。
/// </summary>
[Test]
public async Task Generates_Precise_Service_Type_For_Hidden_Array_Type_Arguments()
{
await GeneratorTest<CqrsHandlerRegistryGenerator>.RunAsync(
HiddenArrayResponseFallbackSource,
("CqrsHandlerRegistry.g.cs", HiddenArrayResponseFallbackExpected));
}
/// <summary>
/// 验证精确重建路径会递归覆盖隐藏泛型定义,
/// 使“隐藏泛型定义 + 可见/常量型实参”的闭包类型也能直接生成 closed service type。
/// </summary>
[Test]
public async Task Generates_Precise_Service_Type_For_Hidden_Generic_Type_Definitions()
{
await GeneratorTest<CqrsHandlerRegistryGenerator>.RunAsync(
HiddenGenericEnvelopeResponseSource,
("CqrsHandlerRegistry.g.cs", HiddenGenericEnvelopeResponseExpected));
}
/// <summary>
/// 验证当 handler 合同把 pointer 响应类型放进 CQRS 泛型参数时,
/// 生成器会保守回退而不是继续发射不可构造的精确注册代码。
/// </summary>
[Test]
public void Reports_Compilation_Error_And_Skips_Precise_Registration_For_Hidden_Pointer_Response()
{
const string source = """
using System; using System;
namespace Microsoft.Extensions.DependencyInjection namespace Microsoft.Extensions.DependencyInjection
@ -817,45 +750,7 @@ public class CqrsHandlerRegistryGeneratorTests
} }
"""; """;
var execution = ExecuteGenerator( private const string MixedDirectAndPreciseRegistrationsSource = """
source,
allowUnsafe: true);
var inputCompilationErrors = execution.InputCompilationDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
var generatedCompilationErrors = execution.GeneratedCompilationDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
var generatorErrors = execution.GeneratorDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
var missingContractDiagnostic =
generatorErrors.SingleOrDefault(static diagnostic =>
string.Equals(diagnostic.Id, "GF_Cqrs_001", StringComparison.Ordinal));
Assert.Multiple(() =>
{
Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS0306"));
Assert.That(generatedCompilationErrors, Is.Empty);
Assert.That(execution.GeneratedSources, Is.Empty);
Assert.That(missingContractDiagnostic, Is.Not.Null);
Assert.That(
missingContractDiagnostic!.GetMessage(),
Does.Contain("TestApp.Container+HiddenHandler"));
Assert.That(
missingContractDiagnostic.GetMessage(),
Does.Contain("GFramework.Cqrs.CqrsReflectionFallbackAttribute"));
});
}
/// <summary>
/// 验证同一个 implementation 同时包含可直接注册接口与需精确重建接口时,
/// 生成器会保留两类注册,并继续按 handler interface 名称稳定排序。
/// </summary>
[Test]
public async Task Generates_Mixed_Direct_And_Precise_Registrations_For_Same_Implementation()
{
const string source = """
using System; using System;
namespace Microsoft.Extensions.DependencyInjection namespace Microsoft.Extensions.DependencyInjection
@ -922,19 +817,7 @@ public class CqrsHandlerRegistryGeneratorTests
} }
"""; """;
await GeneratorTest<CqrsHandlerRegistryGenerator>.RunAsync( private const string MixedReflectedImplementationAndPreciseRegistrationsSource = """
source,
("CqrsHandlerRegistry.g.cs", MixedDirectAndPreciseRegistrationsExpected));
}
/// <summary>
/// 验证隐藏 implementation 同时包含可见 handler interface 与需精确重建接口时,
/// 生成器会保留两类注册,而不会让可见接口被整实现回退吞掉。
/// </summary>
[Test]
public async Task Generates_Mixed_Reflected_Implementation_And_Precise_Registrations_For_Same_Implementation()
{
const string source = """
using System; using System;
namespace Microsoft.Extensions.DependencyInjection namespace Microsoft.Extensions.DependencyInjection
@ -1001,19 +884,7 @@ public class CqrsHandlerRegistryGeneratorTests
} }
"""; """;
await GeneratorTest<CqrsHandlerRegistryGenerator>.RunAsync( private const string ExternalProtectedTypeContractsSource = """
source,
("CqrsHandlerRegistry.g.cs", MixedReflectedImplementationAndPreciseRegistrationsExpected));
}
/// <summary>
/// 验证当外部基类暴露的 handler interface 含有生成注册器顶层上下文不可直接引用的 protected 类型时,
/// 生成器会输出定向程序集查找,而不是继续退回 implementation 级接口发现。
/// </summary>
[Test]
public void Generates_Precise_Assembly_Type_Lookups_For_Inaccessible_External_Protected_Types()
{
const string contractsSource = """
namespace GFramework.Cqrs.Abstractions.Cqrs namespace GFramework.Cqrs.Abstractions.Cqrs
{ {
public interface IRequest<TResponse> { } public interface IRequest<TResponse> { }
@ -1026,7 +897,7 @@ public class CqrsHandlerRegistryGeneratorTests
} }
"""; """;
const string dependencySource = """ private const string ExternalProtectedTypeDependencySource = """
using GFramework.Cqrs.Abstractions.Cqrs; using GFramework.Cqrs.Abstractions.Cqrs;
namespace Dep; namespace Dep;
@ -1048,7 +919,7 @@ public class CqrsHandlerRegistryGeneratorTests
} }
"""; """;
const string source = """ private const string ExternalProtectedTypeLookupSource = """
using System; using System;
using Dep; using Dep;
@ -1092,37 +963,7 @@ public class CqrsHandlerRegistryGeneratorTests
} }
"""; """;
var contractsReference = MetadataReferenceTestBuilder.CreateFromSource( private const string LegacyFallbackMarkerHiddenHandlerSource = """
"Contracts",
contractsSource);
var dependencyReference = MetadataReferenceTestBuilder.CreateFromSource(
"Dependency",
dependencySource,
contractsReference);
var generatedSource = RunGenerator(
source,
contractsReference,
dependencyReference);
Assert.That(
generatedSource,
Does.Not.Contain("RegisterRemainingReflectedHandlerInterfaces("));
Assert.That(
generatedSource,
Does.Not.Contain("Remaining runtime interface discovery target:"));
Assert.That(
generatedSource,
Is.EqualTo(ExternalAssemblyPreciseLookupExpected));
}
/// <summary>
/// 验证即使 runtime 仍暴露旧版无参 fallback marker生成器也会优先在生成注册器内部处理隐藏 handler
/// 不再输出 fallback marker。
/// </summary>
[Test]
public async Task Does_Not_Emit_Legacy_Fallback_Marker_When_Generated_Registry_Can_Self_Register_Hidden_Handler()
{
const string source = """
using System; using System;
namespace Microsoft.Extensions.DependencyInjection namespace Microsoft.Extensions.DependencyInjection
@ -1191,19 +1032,7 @@ public class CqrsHandlerRegistryGeneratorTests
} }
"""; """;
await GeneratorTest<CqrsHandlerRegistryGenerator>.RunAsync( private const string FallbackMarkerUnavailableHiddenHandlerSource = """
source,
("CqrsHandlerRegistry.g.cs", HiddenNestedHandlerSelfRegistrationExpected));
}
/// <summary>
/// 验证即使 runtime 合同中完全不存在 reflection fallback 标记特性,
/// 生成器仍能通过生成注册器内部的定向反射逻辑覆盖隐藏 handler。
/// </summary>
[Test]
public async Task Generates_Registry_For_Hidden_Handler_When_Fallback_Marker_Is_Unavailable()
{
const string source = """
using System; using System;
namespace Microsoft.Extensions.DependencyInjection namespace Microsoft.Extensions.DependencyInjection
@ -1266,20 +1095,7 @@ public class CqrsHandlerRegistryGeneratorTests
} }
"""; """;
await GeneratorTest<CqrsHandlerRegistryGenerator>.RunAsync( private const string MissingFallbackAttributeDiagnosticSource = """
source,
("CqrsHandlerRegistry.g.cs", HiddenNestedHandlerSelfRegistrationExpected));
}
/// <summary>
/// 验证当某轮生成仍然需要程序集级 reflection fallback 元数据,且 runtime 合同缺少承载该元数据的特性时,
/// 生成器会给出明确诊断并停止输出注册器。
/// </summary>
[Test]
public void
Reports_Diagnostic_And_Skips_Registry_When_Fallback_Metadata_Is_Required_But_Runtime_Contract_Lacks_Fallback_Attribute()
{
const string source = """
using System; using System;
namespace Microsoft.Extensions.DependencyInjection namespace Microsoft.Extensions.DependencyInjection
@ -1344,45 +1160,7 @@ public class CqrsHandlerRegistryGeneratorTests
} }
"""; """;
var execution = ExecuteGenerator( private const string UnresolvedErrorTypeRuntimeLookupSource = """
source,
allowUnsafe: true);
var inputCompilationErrors = execution.InputCompilationDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
var generatedCompilationErrors = execution.GeneratedCompilationDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
var generatorErrors = execution.GeneratorDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
var missingContractDiagnostic =
generatorErrors.SingleOrDefault(static diagnostic =>
string.Equals(diagnostic.Id, "GF_Cqrs_001", StringComparison.Ordinal));
Assert.Multiple(() =>
{
Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS0306"));
Assert.That(generatedCompilationErrors, Is.Empty);
Assert.That(execution.GeneratedSources, Is.Empty);
Assert.That(missingContractDiagnostic, Is.Not.Null);
Assert.That(
missingContractDiagnostic!.GetMessage(),
Does.Contain("TestApp.Container+HiddenHandler"));
Assert.That(
missingContractDiagnostic.GetMessage(),
Does.Contain("GFramework.Cqrs.CqrsReflectionFallbackAttribute"));
});
}
/// <summary>
/// 验证 handler 合同里出现未解析错误类型时,生成器会改为运行时精确查找该类型,
/// 而不会把无效类型名直接写进生成代码中的 <c>typeof(...)</c>。
/// </summary>
[Test]
public void Emits_Runtime_Type_Lookup_When_Handler_Contract_Contains_Unresolved_Error_Types()
{
const string source = """
using System; using System;
namespace Microsoft.Extensions.DependencyInjection namespace Microsoft.Extensions.DependencyInjection
@ -1446,44 +1224,7 @@ public class CqrsHandlerRegistryGeneratorTests
} }
"""; """;
var execution = ExecuteGenerator(source); private const string DynamicResponseNormalizationSource = """
var inputCompilationErrors = execution.InputCompilationDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
var generatedCompilationErrors = execution.GeneratedCompilationDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
var generatorErrors = execution.GeneratorDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
Assert.Multiple(() =>
{
Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS0246"));
Assert.That(generatedCompilationErrors, Is.Empty);
Assert.That(generatorErrors, Is.Empty);
Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1));
Assert.That(execution.GeneratedSources[0].filename, Is.EqualTo("CqrsHandlerRegistry.g.cs"));
var generatedSource = execution.GeneratedSources[0].content;
Assert.That(
generatedSource,
Does.Contain("registryAssembly.GetType(\"MissingResponse\", throwOnError: false, ignoreCase: false);"));
Assert.That(
generatedSource,
Does.Contain("internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry"));
Assert.That(generatedSource, Does.Not.Contain("typeof(MissingResponse)"));
Assert.That(generatedSource, Does.Not.Contain("CqrsReflectionFallbackAttribute("));
});
}
/// <summary>
/// 验证 <see langword="dynamic" /> 响应类型会在生成阶段归一化为 <see cref="System.Object" />
/// 避免注册器发射非法的 <c>typeof(dynamic)</c>。
/// </summary>
[Test]
public void Emits_Object_Type_Reference_When_Handler_Response_Uses_Dynamic()
{
const string source = """
using System; using System;
namespace Microsoft.Extensions.DependencyInjection namespace Microsoft.Extensions.DependencyInjection
@ -1541,40 +1282,7 @@ public class CqrsHandlerRegistryGeneratorTests
} }
"""; """;
var execution = ExecuteGenerator(source); private const string AssemblyLevelFallbackMetadataSource = """
var inputCompilationErrors = execution.InputCompilationDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
var generatedCompilationErrors = execution.GeneratedCompilationDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
var generatorErrors = execution.GeneratorDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
Assert.Multiple(() =>
{
Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS1966"));
Assert.That(generatedCompilationErrors, Is.Empty);
Assert.That(generatorErrors, Is.Empty);
Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1));
Assert.That(execution.GeneratedSources[0].filename, Is.EqualTo("CqrsHandlerRegistry.g.cs"));
var generatedSource = execution.GeneratedSources[0].content;
Assert.That(generatedSource, Does.Contain("typeof(global::System.Object)"));
Assert.That(generatedSource, Does.Not.Contain("typeof(dynamic)"));
Assert.That(generatedSource, Does.Contain("internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry"));
});
}
/// <summary>
/// 验证当 fallback metadata 仍然必需且 runtime 提供了承载契约时,
/// 生成器会继续产出注册器并发射程序集级 <c>CqrsReflectionFallbackAttribute</c>。
/// </summary>
[Test]
public void
Emits_Assembly_Level_Fallback_Metadata_When_Fallback_Is_Required_And_Runtime_Contract_Is_Available()
{
const string source = """
using System; using System;
namespace Microsoft.Extensions.DependencyInjection namespace Microsoft.Extensions.DependencyInjection
@ -1655,8 +1363,301 @@ public class CqrsHandlerRegistryGeneratorTests
} }
"""; """;
/// <summary>
/// 验证生成器会为当前程序集中的 request、notification 和 stream 处理器生成稳定顺序的注册器。
/// </summary>
[Test]
public async Task Generates_Assembly_Level_Cqrs_Handler_Registry()
{
await GeneratorTest<CqrsHandlerRegistryGenerator>.RunAsync(
AssemblyLevelCqrsHandlerRegistrySource,
("CqrsHandlerRegistry.g.cs", AssemblyLevelCqrsHandlerRegistryExpected));
}
/// <summary>
/// 验证当程序集包含生成代码无法合法引用的私有嵌套处理器时,生成器会在生成注册器内部执行定向反射注册,
/// 不再依赖程序集级 fallback marker。
/// </summary>
[Test]
public async Task
Generates_Visible_Handlers_And_Self_Registers_Private_Nested_Handler_When_Assembly_Contains_Hidden_Handler()
{
await GeneratorTest<CqrsHandlerRegistryGenerator>.RunAsync(
HiddenNestedHandlerSelfRegistrationSource,
("CqrsHandlerRegistry.g.cs", HiddenNestedHandlerSelfRegistrationExpected));
}
/// <summary>
/// 验证当隐藏实现类型的 handler 接口仍可被生成代码直接引用时,
/// 生成器只会定向反射实现类型,而不会再生成基于 <c>GetInterfaces()</c> 的接口发现辅助逻辑。
/// </summary>
[Test]
public async Task
Generates_Direct_Interface_Registrations_For_Hidden_Implementation_When_Handler_Interface_Is_Public()
{
await GeneratorTest<CqrsHandlerRegistryGenerator>.RunAsync(
HiddenImplementationDirectInterfaceRegistrationSource,
("CqrsHandlerRegistry.g.cs", HiddenImplementationDirectInterfaceRegistrationExpected));
}
/// <summary>
/// 验证精确重建路径会递归覆盖隐藏元素类型数组,
/// 使这类 handler interface 也能直接生成 closed service type而不再退回 <c>GetInterfaces()</c>。
/// </summary>
[Test]
public async Task Generates_Precise_Service_Type_For_Hidden_Array_Type_Arguments()
{
await GeneratorTest<CqrsHandlerRegistryGenerator>.RunAsync(
HiddenArrayResponseFallbackSource,
("CqrsHandlerRegistry.g.cs", HiddenArrayResponseFallbackExpected));
}
/// <summary>
/// 验证精确重建路径会递归覆盖隐藏泛型定义,
/// 使“隐藏泛型定义 + 可见/常量型实参”的闭包类型也能直接生成 closed service type。
/// </summary>
[Test]
public async Task Generates_Precise_Service_Type_For_Hidden_Generic_Type_Definitions()
{
await GeneratorTest<CqrsHandlerRegistryGenerator>.RunAsync(
HiddenGenericEnvelopeResponseSource,
("CqrsHandlerRegistry.g.cs", HiddenGenericEnvelopeResponseExpected));
}
/// <summary>
/// 验证当 handler 合同把 pointer 响应类型放进 CQRS 泛型参数时,
/// 生成器会保守回退而不是继续发射不可构造的精确注册代码。
/// </summary>
[Test]
public void Reports_Compilation_Error_And_Skips_Precise_Registration_For_Hidden_Pointer_Response()
{
var execution = ExecuteGenerator( var execution = ExecuteGenerator(
source, HiddenPointerResponseCompilationErrorSource,
allowUnsafe: true);
var inputCompilationErrors = execution.InputCompilationDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
var generatedCompilationErrors = execution.GeneratedCompilationDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
var generatorErrors = execution.GeneratorDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
var missingContractDiagnostic =
generatorErrors.SingleOrDefault(static diagnostic =>
string.Equals(diagnostic.Id, "GF_Cqrs_001", StringComparison.Ordinal));
Assert.Multiple(() =>
{
Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS0306"));
Assert.That(generatedCompilationErrors, Is.Empty);
Assert.That(execution.GeneratedSources, Is.Empty);
Assert.That(missingContractDiagnostic, Is.Not.Null);
Assert.That(
missingContractDiagnostic!.GetMessage(),
Does.Contain("TestApp.Container+HiddenHandler"));
Assert.That(
missingContractDiagnostic.GetMessage(),
Does.Contain("GFramework.Cqrs.CqrsReflectionFallbackAttribute"));
});
}
/// <summary>
/// 验证同一个 implementation 同时包含可直接注册接口与需精确重建接口时,
/// 生成器会保留两类注册,并继续按 handler interface 名称稳定排序。
/// </summary>
[Test]
public async Task Generates_Mixed_Direct_And_Precise_Registrations_For_Same_Implementation()
{
await GeneratorTest<CqrsHandlerRegistryGenerator>.RunAsync(
MixedDirectAndPreciseRegistrationsSource,
("CqrsHandlerRegistry.g.cs", MixedDirectAndPreciseRegistrationsExpected));
}
/// <summary>
/// 验证隐藏 implementation 同时包含可见 handler interface 与需精确重建接口时,
/// 生成器会保留两类注册,而不会让可见接口被整实现回退吞掉。
/// </summary>
[Test]
public async Task Generates_Mixed_Reflected_Implementation_And_Precise_Registrations_For_Same_Implementation()
{
await GeneratorTest<CqrsHandlerRegistryGenerator>.RunAsync(
MixedReflectedImplementationAndPreciseRegistrationsSource,
("CqrsHandlerRegistry.g.cs", MixedReflectedImplementationAndPreciseRegistrationsExpected));
}
/// <summary>
/// 验证当外部基类暴露的 handler interface 含有生成注册器顶层上下文不可直接引用的 protected 类型时,
/// 生成器会输出定向程序集查找,而不是继续退回 implementation 级接口发现。
/// </summary>
[Test]
public void Generates_Precise_Assembly_Type_Lookups_For_Inaccessible_External_Protected_Types()
{
var contractsReference = MetadataReferenceTestBuilder.CreateFromSource(
"Contracts",
ExternalProtectedTypeContractsSource);
var dependencyReference = MetadataReferenceTestBuilder.CreateFromSource(
"Dependency",
ExternalProtectedTypeDependencySource,
contractsReference);
var generatedSource = RunGenerator(
ExternalProtectedTypeLookupSource,
contractsReference,
dependencyReference);
Assert.That(
generatedSource,
Does.Not.Contain("RegisterRemainingReflectedHandlerInterfaces("));
Assert.That(
generatedSource,
Does.Not.Contain("Remaining runtime interface discovery target:"));
Assert.That(
generatedSource,
Is.EqualTo(ExternalAssemblyPreciseLookupExpected));
}
/// <summary>
/// 验证即使 runtime 仍暴露旧版无参 fallback marker生成器也会优先在生成注册器内部处理隐藏 handler
/// 不再输出 fallback marker。
/// </summary>
[Test]
public async Task Does_Not_Emit_Legacy_Fallback_Marker_When_Generated_Registry_Can_Self_Register_Hidden_Handler()
{
await GeneratorTest<CqrsHandlerRegistryGenerator>.RunAsync(
LegacyFallbackMarkerHiddenHandlerSource,
("CqrsHandlerRegistry.g.cs", HiddenNestedHandlerSelfRegistrationExpected));
}
/// <summary>
/// 验证即使 runtime 合同中完全不存在 reflection fallback 标记特性,
/// 生成器仍能通过生成注册器内部的定向反射逻辑覆盖隐藏 handler。
/// </summary>
[Test]
public async Task Generates_Registry_For_Hidden_Handler_When_Fallback_Marker_Is_Unavailable()
{
await GeneratorTest<CqrsHandlerRegistryGenerator>.RunAsync(
FallbackMarkerUnavailableHiddenHandlerSource,
("CqrsHandlerRegistry.g.cs", HiddenNestedHandlerSelfRegistrationExpected));
}
/// <summary>
/// 验证当某轮生成仍然需要程序集级 reflection fallback 元数据,且 runtime 合同缺少承载该元数据的特性时,
/// 生成器会给出明确诊断并停止输出注册器。
/// </summary>
[Test]
public void
Reports_Diagnostic_And_Skips_Registry_When_Fallback_Metadata_Is_Required_But_Runtime_Contract_Lacks_Fallback_Attribute()
{
var execution = ExecuteGenerator(
MissingFallbackAttributeDiagnosticSource,
allowUnsafe: true);
var inputCompilationErrors = execution.InputCompilationDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
var generatedCompilationErrors = execution.GeneratedCompilationDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
var generatorErrors = execution.GeneratorDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
var missingContractDiagnostic =
generatorErrors.SingleOrDefault(static diagnostic =>
string.Equals(diagnostic.Id, "GF_Cqrs_001", StringComparison.Ordinal));
Assert.Multiple(() =>
{
Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS0306"));
Assert.That(generatedCompilationErrors, Is.Empty);
Assert.That(execution.GeneratedSources, Is.Empty);
Assert.That(missingContractDiagnostic, Is.Not.Null);
Assert.That(
missingContractDiagnostic!.GetMessage(),
Does.Contain("TestApp.Container+HiddenHandler"));
Assert.That(
missingContractDiagnostic.GetMessage(),
Does.Contain("GFramework.Cqrs.CqrsReflectionFallbackAttribute"));
});
}
/// <summary>
/// 验证 handler 合同里出现未解析错误类型时,生成器会改为运行时精确查找该类型,
/// 而不会把无效类型名直接写进生成代码中的 <c>typeof(...)</c>。
/// </summary>
[Test]
public void Emits_Runtime_Type_Lookup_When_Handler_Contract_Contains_Unresolved_Error_Types()
{
var execution = ExecuteGenerator(UnresolvedErrorTypeRuntimeLookupSource);
var inputCompilationErrors = execution.InputCompilationDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
var generatedCompilationErrors = execution.GeneratedCompilationDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
var generatorErrors = execution.GeneratorDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
Assert.Multiple(() =>
{
Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS0246"));
Assert.That(generatedCompilationErrors, Is.Empty);
Assert.That(generatorErrors, Is.Empty);
Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1));
Assert.That(execution.GeneratedSources[0].filename, Is.EqualTo("CqrsHandlerRegistry.g.cs"));
var generatedSource = execution.GeneratedSources[0].content;
Assert.That(
generatedSource,
Does.Contain("registryAssembly.GetType(\"MissingResponse\", throwOnError: false, ignoreCase: false);"));
Assert.That(
generatedSource,
Does.Contain("internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry"));
Assert.That(generatedSource, Does.Not.Contain("typeof(MissingResponse)"));
Assert.That(generatedSource, Does.Not.Contain("CqrsReflectionFallbackAttribute("));
});
}
/// <summary>
/// 验证 <see langword="dynamic" /> 响应类型会在生成阶段归一化为 <see cref="System.Object" />
/// 避免注册器发射非法的 <c>typeof(dynamic)</c>。
/// </summary>
[Test]
public void Emits_Object_Type_Reference_When_Handler_Response_Uses_Dynamic()
{
var execution = ExecuteGenerator(DynamicResponseNormalizationSource);
var inputCompilationErrors = execution.InputCompilationDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
var generatedCompilationErrors = execution.GeneratedCompilationDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
var generatorErrors = execution.GeneratorDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
Assert.Multiple(() =>
{
Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS1966"));
Assert.That(generatedCompilationErrors, Is.Empty);
Assert.That(generatorErrors, Is.Empty);
Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1));
Assert.That(execution.GeneratedSources[0].filename, Is.EqualTo("CqrsHandlerRegistry.g.cs"));
var generatedSource = execution.GeneratedSources[0].content;
Assert.That(generatedSource, Does.Contain("typeof(global::System.Object)"));
Assert.That(generatedSource, Does.Not.Contain("typeof(dynamic)"));
Assert.That(generatedSource, Does.Contain("internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry"));
});
}
/// <summary>
/// 验证当 fallback metadata 仍然必需且 runtime 提供了承载契约时,
/// 生成器会继续产出注册器并发射程序集级 <c>CqrsReflectionFallbackAttribute</c>。
/// </summary>
[Test]
public void
Emits_Assembly_Level_Fallback_Metadata_When_Fallback_Is_Required_And_Runtime_Contract_Is_Available()
{
var execution = ExecuteGenerator(
AssemblyLevelFallbackMetadataSource,
allowUnsafe: true); allowUnsafe: true);
var inputCompilationErrors = execution.InputCompilationDiagnostics var inputCompilationErrors = execution.InputCompilationDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)

View File

@ -8,7 +8,7 @@
<Copyright>Copyright © 2025</Copyright> <Copyright>Copyright © 2025</Copyright>
<RepositoryUrl>https://github.com/GeWuYou/GFramework</RepositoryUrl> <RepositoryUrl>https://github.com/GeWuYou/GFramework</RepositoryUrl>
<PackageProjectUrl>https://github.com/GeWuYou/GFramework</PackageProjectUrl> <PackageProjectUrl>https://github.com/GeWuYou/GFramework</PackageProjectUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<PackageTags>game;framework</PackageTags> <PackageTags>game;framework</PackageTags>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PublishRepositoryUrl>true</PublishRepositoryUrl> <PublishRepositoryUrl>true</PublishRepositoryUrl>
@ -16,6 +16,7 @@
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks> <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<IncludeBuildOutput>false</IncludeBuildOutput> <IncludeBuildOutput>false</IncludeBuildOutput>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<!-- This package is a pure meta-package that only aggregates dependencies. --> <!-- This package is a pure meta-package that only aggregates dependencies. -->
<NoPackageAnalysis>false</NoPackageAnalysis> <NoPackageAnalysis>false</NoPackageAnalysis>
</PropertyGroup> </PropertyGroup>

View File

@ -0,0 +1,16 @@
# Analyzer Warning Reduction 跟踪历史RP-042 至 RP-048
## 范围说明
本归档承接 `RP-042``RP-048` 的晚期 active todo 内容,保留当时围绕 warning-reduction batch、baseline 与构建入口讨论的阶段性结论。
## 归档摘要
- 曾记录 `origin/main` baseline、branch diff 文件数与行数,用于 `$gframework-batch-boot 75` 的批处理停点判断
- 曾记录 `UnifiedSettingsFile``UnifiedSettingsDataRepository``LocalizationMap``CqrsHandlerRegistryGeneratorTests` 的 warning-reduction 切片已提交到当前分支
- 曾记录 RP-048 时在仓库根目录执行 plain `dotnet build` 成功,结果为 `0 Warning(s)` / `0 Error(s)`
- 这些内容在 RP-049 之后不再保留在 active todo 中因为当前恢复入口应只聚焦“plain `dotnet build` 是否打印 warning”这个真值
## superseded by
- [analyzer-warning-reduction-tracking.md](../../todos/analyzer-warning-reduction-tracking.md)

View File

@ -0,0 +1,16 @@
# Analyzer Warning Reduction 追踪历史RP-042 至 RP-048
## 范围说明
本归档承接 `RP-042``RP-048` 的 late-stage trace保留 active trace 在被 RP-049 压缩前的关键执行背景。
## 归档摘要
- 记录了 warning-reduction batch 在 `origin/main` 基线上的 diff 指标与“接近 75 个文件时停止”的批处理语境
- 记录了对 plain `dotnet build` 与带参数构建命令的比较,以及当时对 warning 检查入口的整理过程
- 记录了 RP-048 已确认默认 `dotnet build` 成功且当前工作树无活动代码修改
- RP-049 之后,这些内容不再作为默认恢复入口,而改为保存在 archive 供历史追溯
## superseded by
- [analyzer-warning-reduction-trace.md](../../traces/analyzer-warning-reduction-trace.md)

View File

@ -2,59 +2,74 @@
## 目标 ## 目标
继续以“优先低风险、保持行为兼容”为原则收敛当前仓库的 Meziantou analyzer warnings并确保 active recovery 入口保持精简、可恢复 继续以“直接看构建输出、直接修构建 warning”为原则推进当前分支并保持 active recovery 文档只保留当前真值
## 当前恢复点 ## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-042` - 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-052`
- 当前阶段:`Phase 42` - 当前阶段:`Phase 52`
- 当前焦点: - 当前焦点:
- 已于 `2026-04-24` 使用 `gframework-pr-review` 复核当前分支 PR #280latest-head review 仍有 `3` 条 open threads - `2026-04-24` 本轮从当前 PR review 的未解决线程回切到 `GFramework.Game` / `GFramework.Godot.SourceGenerators.Tests`
- 本地确认这 `3` 条 open threads 均指向 `ai-plan` 文档:错误归档链接、`rp002-rp041` trace 混入 `RP-001` 段落,以及 active trace 的恢复信息失真 - `UnifiedSettingsFile.Sections``CloneFile` fallback 已对齐为“可保留原 comparer 时保留,否则显式回退到 `StringComparer.Ordinal`”的文档与实现契约
- 本轮按最小写集直接修正文档恢复入口,不再扩大 `GFramework.SourceGenerators.Tests` 的代码写集 - `AutoRegisterExportedCollectionsGeneratorTests` 中剩余的 `await test.RunAsync();` 已统一补齐 `.ConfigureAwait(false)`,并同步让 `VerifyDiagnosticsAsync` 内部消费异步等待
- `RP-041` 验证完成时,分支相对 `origin/main` 的唯一变更文件数为 `4`;这说明继续只处理同一热点文件时,该指标增长会很慢 - 当前批次仍需避免混入与 analyzer-warning-reduction 无关的既有工作树改动
- `GFramework.SourceGenerators.Tests``RP-042``net10.0` Release build 中仍为 `10``MA0051` warning、`0` error剩余热点继续集中在 `CqrsHandlerRegistryGeneratorTests.cs`
## 当前状态摘要
- 已修正 `archive/todos/analyzer-warning-reduction-history-rp002-rp041.md` 中指向 `RP-001` 归档的相对链接,恢复历史入口可点击性
- 已从 `archive/traces/analyzer-warning-reduction-history-rp002-rp041.md` 中移除误混入的 `RP-001` 段落,确保文件名与内容范围一致
- 已刷新 active tracking / trace 的恢复点描述,使其反映当前仍待远端收敛的是文档类 review threads而不是已处理过的代码项
- PR #280 的 MegaLinter 仍显示 `dotnet-format` warning但测试报告为 `2156 passed / 0 failed`;该 warning 目前更像 CI 环境 restore / SDK 噪音,而不是本地代码行为回归
## 当前活跃事实 ## 当前活跃事实
- 当前主题仍保持 active因为 `GFramework.SourceGenerators.Tests` 尚有剩余 `MA0051` warning 需要决定是否继续推进 - 之前记录的 plain `dotnet build` `0 Warning(s)` 属于增量构建假阴性,不能再作为 warning 检查真值
- 继续按“单文件单方法”节奏处理 `CqrsHandlerRegistryGeneratorTests.cs` 可以稳定消除 warning但不利于快速提高唯一变更文件数 - 本轮已完成 `GFramework.Godot.SourceGenerators` warning 清理clean `Release` build 从 9 个 warning 降至 0 个 warning
- 当前 PR review 已没有新的 failed-test 信号;当前优先级是提交这轮 `ai-plan` 修正并等待远端 PR threads 收敛 - 当前已确认解决的文件包括 `BindNodeSignalGenerator.cs``GetNodeGenerator.cs``GodotProjectMetadataGenerator.cs``Registration/AutoRegisterExportedCollectionsGenerator.cs`
- 本轮直接执行仓库根目录 `dotnet clean` 仍在 `ValidateSolutionConfiguration` 阶段失败,输出未提供具体 error 文本
- 本轮直接执行仓库根目录 `dotnet build` 成功,并给出 `1184 warning(s)` 的真实输出
- `GFramework.Godot.SourceGenerators.Tests` 已通过测试辅助模板抽取与 `ConfigureAwait(false)` 修正,当前 `Debug` / `Release` 构建均为 `0 Warning(s)`
- 本轮已验证 `dotnet test GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release --no-build`,结果为 `Passed: 48`
- 本轮已把 PR #283 中仍打开的 `UnifiedSettingsDataRepository.cs` comparer 契约线程落到代码与 XML 注释,避免 fallback 语义继续依赖隐式默认 comparer
- 本轮已确认 `AutoRegisterExportedCollectionsGeneratorTests` 的 5 处裸 `await test.RunAsync();` 不是当前 Release build 告警来源,但仍作为 PR review 一致性项一并修正
## 当前风险 ## 当前风险
- warning 治理策略风险:如果用户仍以“唯一变更文件数接近 `75`”作为目标,继续深挖同一测试文件会让目标推进缓慢 - 如果后续继续依赖增量 `dotnet build`,容易再次把 warning 数量误判为 0
- 缓解措施:下一轮先确认是继续压低 `MA0051` 基线,还是切换到新的文件写集 - 缓解措施:每轮 warning 检查前先执行 `dotnet clean`,再执行目标 `dotnet build`
- WSL 构建环境风险:当前 worktree 的 .NET 定向验证仍需显式附带 `-p:RestoreFallbackFolders=`,并在沙箱外运行以规避命名管道 / socket 限制 - 仓库根目录 `dotnet clean` 目前仍然无法给出新的 clean 基线
- 缓解措施:后续所有 affected-project Release build 继续复用该参数组合 - 缓解措施:若下一轮继续做整仓 warning reduction先定位 `dotnet clean` 的 solution-level 失败原因,或明确继续沿用用户确认的 `1193 warning(s)` clean 基线与本轮 `1184 warning(s)` direct build 观测值
- source generator test warning 范围风险:一旦继续触达 `GFramework.SourceGenerators.Tests`,剩余 warning 会继续成为本轮完成条件的一部分 - 当前 worktree 已存在与本批次无关的未提交改动
- 缓解措施:继续用最小写集和 warnings-only build 锁定范围 - 缓解措施:提交当前批次时只暂存 `GFramework.Godot.SourceGenerators.Tests` 与对应 `ai-plan` 文件,避免混入其他 topic 变更
- `GFramework.Game` 当前 `Release` build 仍带有既有 analyzer warning 基线
- 缓解措施:本轮仅验证改动未新增 `UnifiedSettingsDataRepository` / `UnifiedSettingsFile` 相关 warning若继续在该模块做 warning reduction需要另开切片处理现存基线
## 活跃文档 ## 活跃文档
- 当前轮次归档:
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/todos/analyzer-warning-reduction-history-rp042-rp048.md)
- 历史跟踪归档: - 历史跟踪归档:
- [analyzer-warning-reduction-history-rp001.md](../archive/todos/analyzer-warning-reduction-history-rp001.md) - [analyzer-warning-reduction-history-rp001.md](../archive/todos/analyzer-warning-reduction-history-rp001.md)
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/todos/analyzer-warning-reduction-history-rp002-rp041.md) - [analyzer-warning-reduction-history-rp002-rp041.md](../archive/todos/analyzer-warning-reduction-history-rp002-rp041.md)
- 历史 trace 归档: - 历史 trace 归档:
- [analyzer-warning-reduction-history-rp001.md](../archive/traces/analyzer-warning-reduction-history-rp001.md) - [analyzer-warning-reduction-history-rp001.md](../archive/traces/analyzer-warning-reduction-history-rp001.md)
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/traces/analyzer-warning-reduction-history-rp002-rp041.md) - [analyzer-warning-reduction-history-rp002-rp041.md](../archive/traces/analyzer-warning-reduction-history-rp002-rp041.md)
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/traces/analyzer-warning-reduction-history-rp042-rp048.md)
## 验证说明 ## 验证说明
- `DOTNET_CLI_HOME=/tmp/dotnet-home dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders=""` - `dotnet clean`
- 结果:通过;重写了受 Windows fallback package folder 影响的测试项目资产文件 - 结果:失败;停在 solution `ValidateSolutionConfiguration``0 Warning(s)``0 Error(s)`,未输出更具体的 error 文本
- `DOTNET_CLI_HOME=/tmp/dotnet-home dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release -t:Rebuild --no-restore --disable-build-servers -m:1 -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"` - `dotnet build`
- 结果:`10 Warning(s)``0 Error(s)`warning 仍全部来自 `CqrsHandlerRegistryGeneratorTests.cs` 的既有 `MA0051` 基线 - 结果:成功;`1184 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj`
- 初始结果:成功;`24 Warning(s)``0 Error(s)`
- 本轮收尾结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release --no-build`
- 结果:成功;`Passed: 48``Failed: 0`
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
- 结果:成功;`533 Warning(s)``0 Error(s)`;模块仍存在既有 warning 基线,本轮 follow-up 仅处理 PR review 指向的 comparer 契约与测试异步等待一致性
- `dotnet build GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release --no-build`
- 结果:成功;`Passed: 48``Failed: 0`
## 下一步建议 ## 下一步建议
1. 提交 `RP-042` 后重新抓取 PR #280 review确认这 `3` 条 latest-head open threads 是否随新提交收敛 1. 提交当前 comparer 契约与 `ConfigureAwait(false)` PR follow-up并确认只纳入本 topic 相关文件
2. 若 PR threads 收敛,再决定下一轮是继续清理 `CqrsHandlerRegistryGeneratorTests.cs` 的剩余 `MA0051`,还是切换到新的文件写集 2. 视 PR review 反馈决定是否继续收敛 `GFramework.Game` 现有 warning 基线,或返回下一轮整仓 warning 热点筛选
3. 如果仍要继续沿用“唯一变更文件数接近 `75`”的目标,应优先切到新的 warning 写集,而不是继续深挖同一测试文件

View File

@ -1,31 +1,91 @@
# Analyzer Warning Reduction 追踪 # Analyzer Warning Reduction 追踪
## 2026-04-24 — RP-042 # Analyzer Warning Reduction 追踪
### 阶段PR #280 review follow-up 与 ai-plan 恢复入口修正 ## 2026-04-24 — RP-052
- 启动复核: ### 阶段PR review follow-upcomparer 契约 + `ConfigureAwait(false)` 收尾)
- 使用 `gframework-pr-review` 抓取当前分支 PR #280 的 latest-head review threads、MegaLinter 摘要与测试报告
- 本地核对后确认 `3` 条 open threads 均仍成立,但全部集中在 `ai-plan` 文档恢复入口,而不是新的代码行为问题 - 触发背景:
- 决策: - 当前分支 PR #283 的最新 review 中,`greptile-apps[bot]` 仍有一个未解决线程,指出 `UnifiedSettingsDataRepository.CloneFile` fallback 会静默丢失原 comparer
- 不再继续扩大 `GFramework.SourceGenerators.Tests` 的写集,先把远端 latest-head review 中仍成立的文档问题全部收口 - CodeRabbit 另指出 `AutoRegisterExportedCollectionsGeneratorTests.cs` 中还残留 5 处 `await test.RunAsync();`,与同项目其他测试文件的 `.ConfigureAwait(false)` 风格不一致
- 保持 `RP-042` 作为 active recovery point仅刷新其事实描述、归档链接和 trace 范围边界 - 主线程实施:
- 实施调整: - 复核 PR review JSON、`UnifiedSettingsDataRepository.cs``UnifiedSettingsFile.cs``AutoRegisterExportedCollectionsGeneratorTests.cs` 的当前代码,确认只有 comparer 契约线程仍属最新 head 上的实质问题
- 修正 `archive/todos/analyzer-warning-reduction-history-rp002-rp041.md` 中两条指向 `RP-001` 归档的相对链接 - 将 `UnifiedSettingsFile.Sections` 的 XML 注释补充为显式 comparer 契约,并把默认字典初始化改为 `StringComparer.Ordinal`
- 从 `archive/traces/analyzer-warning-reduction-history-rp002-rp041.md` 中移除误混入的 `RP-001` 段落,使文件只保留 `RP-002``RP-041` - 将 `CloneFile` fallback 从隐式默认 comparer 改为显式 `StringComparer.Ordinal`,并同步修正文档注释,避免继续暗含“保留原语义”的错误表述
- 刷新 active tracking / trace 的恢复点描述,明确当前 open threads 已收敛为文档问题,并记录本轮 follow-up 的事实与下一步 - 把 `AutoRegisterExportedCollectionsGeneratorTests` 中剩余的 5 处 `await test.RunAsync();` 统一为 `.ConfigureAwait(false)`,同时让 `VerifyDiagnosticsAsync` 内部也消费 `ConfigureAwait(false)`
- 验证结果: - 验证里程碑:
- `DOTNET_CLI_HOME=/tmp/dotnet-home dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release -t:Rebuild --no-restore --disable-build-servers -m:1 -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"` - `dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
- 结果:`10 Warning(s)``0 Error(s)`warning 仍全部来自 `CqrsHandlerRegistryGeneratorTests.cs` 的既有 `MA0051` 基线 - 结果:成功;`533 Warning(s)``0 Error(s)``GFramework.Game` 仍有既有 warning 基线,本轮 follow-up 仅处理 PR review 指向的 comparer 契约与测试异步等待一致性
- `dotnet build GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release --no-build`
- 首次并行复验:失败;`FileNotFoundException`,原因是 `--no-build` 测试在 Release DLL 落盘前启动
- 串行复验:成功;`Passed: 48``Failed: 0`
- 当前结论: - 当前结论:
- PR #280 当前没有 failed-test 回归信号latest-head review 剩余项可以全部在 `ai-plan` 范围内处理 - PR #283 当前仍打开的 comparer review thread 已在本地代码与 XML 注释层面得到对应修复
- active 恢复入口与历史归档范围已重新对齐,后续 `boot` 不会再从 `rp002-rp041` 误读 `RP-001` - `AutoRegisterExportedCollectionsGeneratorTests` 的异步等待风格已与同项目其他测试保持一致
- 下一步建议: - 当前改动已通过直接受影响测试项目的 Release build 与串行 Release test 复验,可进入提交阶段
- 提交后重新抓取 PR #280 review确认 open threads 是否收敛
- 若 threads 收敛,则回到 `CqrsHandlerRegistryGeneratorTests.cs` 剩余 `MA0051`,或根据目标改切新的 warning 写集 ## 2026-04-24 — RP-051
### 阶段:`GFramework.Godot.SourceGenerators.Tests` warning 清零
- 触发背景:
- 用户要求直接运行 `dotnet clean`,不再添加额外 shell 包装solution-level `dotnet clean` 仍然在 `ValidateSolutionConfiguration` 阶段失败
- 直接执行仓库根目录 `dotnet build` 成功,并输出 `1184 warning(s)`,说明当前真实热点已从 `GFramework.Godot.SourceGenerators` 转移到对应测试项目
- 主线程实施:
- 以 `GFramework.Godot.SourceGenerators.Tests` 为独立批次,先确认该项目本地基线为 `24 warning(s)`
- 在 `BindNodeSignalGeneratorTests.cs``AutoSceneGeneratorTests.cs``AutoUiPageGeneratorTests.cs``GetNodeGeneratorTests.cs``AutoRegisterExportedCollectionsGeneratorTests.cs``GodotProjectMetadataGeneratorTests.cs` 中抽取共享 source / diagnostic helper压缩重复长方法
- 在 `Core/GeneratorTest.cs` 中补充 `ConfigureAwait(false)`,清除项目内唯一 `MA0004`
- 把 `GFramework.Godot.SourceGenerators.Tests` 项目 warning 从 `24` 降到 `0`
- 验证里程碑:
- `dotnet build`
- 结果:成功;`1184 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj`
- 初始结果:成功;`24 Warning(s)``0 Error(s)`
- 第一批(`BindNodeSignal` + `GeneratorTest`)后:`16 Warning(s)`
- 第二批(`AutoScene` / `AutoUiPage` / `GetNode`)后:`8 Warning(s)`
- 第三批(`Registration` / `Project`)后:`1 Warning(s)`
- 收尾修复后:成功;`0 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release --no-build`
- 结果:成功;`Passed: 48``Failed: 0`
- 当前结论:
- `GFramework.Godot.SourceGenerators.Tests` 已在 `Debug` / `Release` 构建下达到 `0 warning(s)`
- 按 `origin/main` merge-base 计算并只纳入当前暂存批次时,累计分支 diff 为 `23` 个文件,低于 `$gframework-batch-boot 75` 的主停止阈值
- 仓库根目录 `dotnet clean` 仍无法稳定产出新的 clean 基线,需要在下一轮单独排查
- 当前 worktree 已有与本批次无关的既有改动;提交时必须只暂存 analyzer warning reduction 相关文件
## 2026-04-24 — RP-050
### 阶段clean-build 基线修正与 `GFramework.Godot.SourceGenerators` 切片清零
- 触发背景:
- 用户确认之前的 `0 Warning(s)` 来自增量构建假阴性;只有先 `dotnet clean``dotnet build`warning 才会重新出现
- 用户给出 clean solution build 的真实结果:`Build succeeded with 1193 warning(s)`
- 主线程实施:
- 纠正当前 topic 的 active todo / trace把 clean build 作为新的 warning 检查真值
- 在 `BindNodeSignalGenerator.cs``GetNodeGenerator.cs``GodotProjectMetadataGenerator.cs` 中完成分阶段方法抽取与字符串比较修正
- 在 `Registration/AutoRegisterExportedCollectionsGenerator.cs` 中拆分 `TryCreateRegistration`,清除最后一个 `MA0051`
- 更新 `AGENTS.md`,明确 warning 检查必须先 `dotnet clean``dotnet build`
- 验证里程碑:
- `dotnet clean GFramework.Godot.SourceGenerators/GFramework.Godot.SourceGenerators.csproj -c Release`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Godot.SourceGenerators/GFramework.Godot.SourceGenerators.csproj -c Release`
- 首次验证:成功;`1 Warning(s)`,剩余 `Registration/AutoRegisterExportedCollectionsGenerator.cs(182,25)` `MA0051`
- 修复后复验:成功;`0 Warning(s)``0 Error(s)`
- 当前结论:
- `GFramework.Godot.SourceGenerators` 已在 clean `Release` build 下从 9 个 warning 降到 0 个 warning
- 整仓库 warning 基线仍以用户确认的 clean solution build `1193 warning(s)` 为准
- 下一轮应继续从 clean solution build 输出中选择新的低风险热点
## Archive Context ## Archive Context
- 当前轮次归档:
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/todos/analyzer-warning-reduction-history-rp042-rp048.md)
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/traces/analyzer-warning-reduction-history-rp042-rp048.md)
- 历史跟踪归档: - 历史跟踪归档:
- [analyzer-warning-reduction-history-rp001.md](../archive/todos/analyzer-warning-reduction-history-rp001.md) - [analyzer-warning-reduction-history-rp001.md](../archive/todos/analyzer-warning-reduction-history-rp001.md)
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/todos/analyzer-warning-reduction-history-rp002-rp041.md) - [analyzer-warning-reduction-history-rp002-rp041.md](../archive/todos/analyzer-warning-reduction-history-rp002-rp041.md)