mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
Merge pull request #283 from GeWuYou/fix/analyzer-warning-reduction-batch
Fix/analyzer warning reduction batch
This commit is contained in:
commit
63f563cd49
@ -29,6 +29,10 @@ All AI agents and contributors must follow these rules when writing, reviewing,
|
||||
## Git Workflow Rules
|
||||
|
||||
- Every completed task MUST pass at least one build validation before it is considered done.
|
||||
- When the goal is to inspect or reduce warnings printed during project build, contributors MUST establish the warning
|
||||
baseline from a non-incremental repository-root build by running `dotnet clean` and then `dotnet build`.
|
||||
- Contributors MUST NOT treat a repeated incremental `dotnet build` result as authoritative for warning inspection when
|
||||
a clean baseline has not been captured in the same round.
|
||||
- If the task changes multiple projects or shared abstractions, prefer a solution-level or affected-project
|
||||
`dotnet build ... -c Release`; otherwise use the smallest build command that still proves the result compiles.
|
||||
- When a task adds a feature or modifies code, contributors MUST run a Release build for every directly affected
|
||||
@ -233,6 +237,10 @@ All generated or modified code MUST include clear and meaningful comments where
|
||||
Use the smallest command set that proves the change, then expand if the change is cross-cutting.
|
||||
|
||||
```bash
|
||||
# Check warnings from the default repository build entrypoint
|
||||
dotnet clean
|
||||
dotnet build
|
||||
|
||||
# Build the full solution
|
||||
dotnet build GFramework.sln -c Release
|
||||
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<WarningLevel>0</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.4.0"/>
|
||||
|
||||
@ -283,15 +283,21 @@ public class UnifiedSettingsDataRepository(
|
||||
/// 复制当前统一文件快照,确保未提交修改不会污染内存中的已提交状态。
|
||||
/// </summary>
|
||||
/// <param name="source">要复制的统一文件快照。</param>
|
||||
/// <returns>包含独立 section 字典的新快照。</returns>
|
||||
/// <returns>包含独立 section 映射副本的新快照。</returns>
|
||||
private static UnifiedSettingsFile CloneFile(UnifiedSettingsFile 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
|
||||
{
|
||||
Version = source.Version,
|
||||
Sections = new Dictionary<string, string>(source.Sections, source.Sections.Comparer)
|
||||
Sections = sections
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -11,6 +11,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GFramework.Core.Abstractions.Versioning;
|
||||
|
||||
namespace GFramework.Game.Data;
|
||||
@ -22,13 +24,19 @@ namespace GFramework.Game.Data;
|
||||
internal sealed class UnifiedSettingsFile : IVersioned
|
||||
{
|
||||
/// <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>
|
||||
public int Version { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,57 +6,61 @@ namespace GFramework.Godot.SourceGenerators.Tests.Behavior;
|
||||
[TestFixture]
|
||||
public class AutoSceneGeneratorTests
|
||||
{
|
||||
private const string AutoSceneAttributeWithKeyDeclaration = """
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class AutoSceneAttribute : Attribute
|
||||
{
|
||||
public AutoSceneAttribute(string key) { }
|
||||
}
|
||||
""";
|
||||
|
||||
private const string AutoSceneAttributeWithoutKeyDeclaration = """
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class AutoSceneAttribute : Attribute
|
||||
{
|
||||
public AutoSceneAttribute() { }
|
||||
}
|
||||
""";
|
||||
|
||||
private const string NodeTypes = """
|
||||
public class Node { }
|
||||
public class Node2D : Node { }
|
||||
""";
|
||||
|
||||
private const string SceneBehaviorInfrastructure = """
|
||||
namespace GFramework.Game.Abstractions.Scene
|
||||
{
|
||||
public interface ISceneBehavior { }
|
||||
}
|
||||
|
||||
namespace GFramework.Godot.Scene
|
||||
{
|
||||
using GFramework.Game.Abstractions.Scene;
|
||||
using Godot;
|
||||
|
||||
public static class SceneBehaviorFactory
|
||||
{
|
||||
public static ISceneBehavior Create<T>(T owner, string key)
|
||||
where T : Node
|
||||
{
|
||||
return null!;
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
[Test]
|
||||
public async Task Generates_Scene_Behavior_Boilerplate()
|
||||
{
|
||||
const string source = """
|
||||
using System;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
||||
using Godot;
|
||||
|
||||
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class AutoSceneAttribute : Attribute
|
||||
{
|
||||
public AutoSceneAttribute(string key) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace Godot
|
||||
{
|
||||
public class Node { }
|
||||
public class Node2D : Node { }
|
||||
}
|
||||
|
||||
namespace GFramework.Game.Abstractions.Scene
|
||||
{
|
||||
public interface ISceneBehavior { }
|
||||
}
|
||||
|
||||
namespace GFramework.Godot.Scene
|
||||
{
|
||||
using GFramework.Game.Abstractions.Scene;
|
||||
using Godot;
|
||||
|
||||
public static class SceneBehaviorFactory
|
||||
{
|
||||
public static ISceneBehavior Create<T>(T owner, string key)
|
||||
where T : Node
|
||||
{
|
||||
return null!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
[AutoScene("Gameplay")]
|
||||
public partial class GameplayRoot : Node2D
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
string source = CreateAutoSceneSource(
|
||||
AutoSceneAttributeWithKeyDeclaration,
|
||||
"""
|
||||
[AutoScene("Gameplay")]
|
||||
public partial class GameplayRoot : Node2D
|
||||
{
|
||||
}
|
||||
""",
|
||||
includeBehaviorInfrastructure: true);
|
||||
|
||||
const string expected = """
|
||||
// <auto-generated />
|
||||
@ -80,40 +84,20 @@ public class AutoSceneGeneratorTests
|
||||
|
||||
await GeneratorTest<AutoSceneGenerator>.RunAsync(
|
||||
source,
|
||||
("TestApp_GameplayRoot.AutoScene.g.cs", expected));
|
||||
("TestApp_GameplayRoot.AutoScene.g.cs", expected)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Reports_Diagnostic_When_AutoScene_Arguments_Are_Invalid()
|
||||
{
|
||||
const string source = """
|
||||
using System;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
||||
using Godot;
|
||||
|
||||
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class AutoSceneAttribute : Attribute
|
||||
{
|
||||
public AutoSceneAttribute() { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace Godot
|
||||
{
|
||||
public class Node { }
|
||||
public class Node2D : Node { }
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
[{|#0:AutoScene|}]
|
||||
public partial class GameplayRoot : Node2D
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
string source = CreateAutoSceneSource(
|
||||
AutoSceneAttributeWithoutKeyDeclaration,
|
||||
"""
|
||||
[{|#0:AutoScene|}]
|
||||
public partial class GameplayRoot : Node2D
|
||||
{
|
||||
}
|
||||
""");
|
||||
|
||||
var test = new CSharpSourceGeneratorTest<AutoSceneGenerator, DefaultVerifier>
|
||||
{
|
||||
@ -128,65 +112,26 @@ public class AutoSceneGeneratorTests
|
||||
.WithLocation(0)
|
||||
.WithArguments("AutoSceneAttribute", "GameplayRoot", "a single string scene key argument"));
|
||||
|
||||
await test.RunAsync();
|
||||
await test.RunAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Generates_Type_Constraints_For_Nullable_Reference_NotNull_And_Unmanaged_Parameters()
|
||||
{
|
||||
const string source = """
|
||||
#nullable enable
|
||||
using System;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
||||
using Godot;
|
||||
|
||||
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class AutoSceneAttribute : Attribute
|
||||
{
|
||||
public AutoSceneAttribute(string key) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace Godot
|
||||
{
|
||||
public class Node { }
|
||||
public class Node2D : Node { }
|
||||
}
|
||||
|
||||
namespace GFramework.Game.Abstractions.Scene
|
||||
{
|
||||
public interface ISceneBehavior { }
|
||||
}
|
||||
|
||||
namespace GFramework.Godot.Scene
|
||||
{
|
||||
using GFramework.Game.Abstractions.Scene;
|
||||
using Godot;
|
||||
|
||||
public static class SceneBehaviorFactory
|
||||
{
|
||||
public static ISceneBehavior Create<T>(T owner, string key)
|
||||
where T : Node
|
||||
{
|
||||
return null!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
[AutoScene("Gameplay")]
|
||||
public partial class GameplayRoot<TReference, TNotNull, TValue, TUnmanaged> : Node2D
|
||||
where TReference : class?
|
||||
where TNotNull : notnull
|
||||
where TValue : struct
|
||||
where TUnmanaged : unmanaged
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
string source = CreateAutoSceneSource(
|
||||
AutoSceneAttributeWithKeyDeclaration,
|
||||
"""
|
||||
[AutoScene("Gameplay")]
|
||||
public partial class GameplayRoot<TReference, TNotNull, TValue, TUnmanaged> : Node2D
|
||||
where TReference : class?
|
||||
where TNotNull : notnull
|
||||
where TValue : struct
|
||||
where TUnmanaged : unmanaged
|
||||
{
|
||||
}
|
||||
""",
|
||||
includeBehaviorInfrastructure: true,
|
||||
nullableEnabled: true);
|
||||
|
||||
const string expected = """
|
||||
// <auto-generated />
|
||||
@ -214,7 +159,7 @@ public class AutoSceneGeneratorTests
|
||||
|
||||
await GeneratorTest<AutoSceneGenerator>.RunAsync(
|
||||
source,
|
||||
("TestApp_GameplayRoot.AutoScene.g.cs", expected));
|
||||
("TestApp_GameplayRoot.AutoScene.g.cs", expected)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -267,7 +212,7 @@ public class AutoSceneGeneratorTests
|
||||
.WithLocation(0)
|
||||
.WithArguments("GameplayRoot", "SceneKeyStr"));
|
||||
|
||||
await test.RunAsync();
|
||||
await test.RunAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -326,6 +271,39 @@ public class AutoSceneGeneratorTests
|
||||
.WithLocation(0)
|
||||
.WithArguments("GameplayRoot", "__autoSceneBehavior_Generated"));
|
||||
|
||||
await test.RunAsync();
|
||||
await test.RunAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static string CreateAutoSceneSource(
|
||||
string attributeDeclaration,
|
||||
string testAppSource,
|
||||
bool includeBehaviorInfrastructure = false,
|
||||
bool nullableEnabled = false)
|
||||
{
|
||||
string nullableDirective = nullableEnabled ? "#nullable enable\n" : string.Empty;
|
||||
string infrastructure = includeBehaviorInfrastructure
|
||||
? $"{Environment.NewLine}{Environment.NewLine}{SceneBehaviorInfrastructure}"
|
||||
: string.Empty;
|
||||
|
||||
return $$"""
|
||||
{{nullableDirective}}using System;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
||||
using Godot;
|
||||
|
||||
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
|
||||
{
|
||||
{{attributeDeclaration}}
|
||||
}
|
||||
|
||||
namespace Godot
|
||||
{
|
||||
{{NodeTypes}}
|
||||
}{{infrastructure}}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
{{testAppSource}}
|
||||
}
|
||||
""";
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,69 +6,85 @@ namespace GFramework.Godot.SourceGenerators.Tests.Behavior;
|
||||
[TestFixture]
|
||||
public class AutoUiPageGeneratorTests
|
||||
{
|
||||
private const string AutoUiPageAttributeWithLayerDeclaration = """
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class AutoUiPageAttribute : Attribute
|
||||
{
|
||||
public AutoUiPageAttribute(string key, string layerName) { }
|
||||
}
|
||||
""";
|
||||
|
||||
private const string AutoUiPageAttributeWithoutLayerDeclaration = """
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class AutoUiPageAttribute : Attribute
|
||||
{
|
||||
public AutoUiPageAttribute(string key) { }
|
||||
}
|
||||
""";
|
||||
|
||||
private const string CanvasNodeTypes = """
|
||||
public class Node { }
|
||||
public class CanvasItem : Node { }
|
||||
public class Control : CanvasItem { }
|
||||
""";
|
||||
|
||||
private const string UiLayerFullEnum = """
|
||||
namespace GFramework.Game.Abstractions.Enums
|
||||
{
|
||||
public enum UiLayer
|
||||
{
|
||||
Page,
|
||||
Overlay,
|
||||
Modal
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private const string UiLayerPageOnlyEnum = """
|
||||
namespace GFramework.Game.Abstractions.Enums
|
||||
{
|
||||
public enum UiLayer
|
||||
{
|
||||
Page
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private const string UiBehaviorInfrastructure = """
|
||||
namespace GFramework.Game.Abstractions.UI
|
||||
{
|
||||
public interface IUiPageBehavior { }
|
||||
}
|
||||
|
||||
namespace GFramework.Godot.UI
|
||||
{
|
||||
using GFramework.Game.Abstractions.Enums;
|
||||
using GFramework.Game.Abstractions.UI;
|
||||
using Godot;
|
||||
|
||||
public static class UiPageBehaviorFactory
|
||||
{
|
||||
public static IUiPageBehavior Create<T>(T owner, string key, UiLayer layer)
|
||||
where T : CanvasItem
|
||||
{
|
||||
return null!;
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
[Test]
|
||||
public async Task Generates_Ui_Page_Behavior_Boilerplate()
|
||||
{
|
||||
const string source = """
|
||||
using System;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
||||
using Godot;
|
||||
|
||||
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class AutoUiPageAttribute : Attribute
|
||||
{
|
||||
public AutoUiPageAttribute(string key, string layerName) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace Godot
|
||||
{
|
||||
public class Node { }
|
||||
public class CanvasItem : Node { }
|
||||
public class Control : CanvasItem { }
|
||||
}
|
||||
|
||||
namespace GFramework.Game.Abstractions.Enums
|
||||
{
|
||||
public enum UiLayer
|
||||
{
|
||||
Page,
|
||||
Overlay,
|
||||
Modal
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Game.Abstractions.UI
|
||||
{
|
||||
public interface IUiPageBehavior { }
|
||||
}
|
||||
|
||||
namespace GFramework.Godot.UI
|
||||
{
|
||||
using GFramework.Game.Abstractions.Enums;
|
||||
using GFramework.Game.Abstractions.UI;
|
||||
using Godot;
|
||||
|
||||
public static class UiPageBehaviorFactory
|
||||
{
|
||||
public static IUiPageBehavior Create<T>(T owner, string key, UiLayer layer)
|
||||
where T : CanvasItem
|
||||
{
|
||||
return null!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
[AutoUiPage("MainMenu", "Page")]
|
||||
public partial class MainMenu : Control
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
string source = CreateAutoUiPageSource(
|
||||
AutoUiPageAttributeWithLayerDeclaration,
|
||||
UiLayerFullEnum,
|
||||
"""
|
||||
[AutoUiPage("MainMenu", "Page")]
|
||||
public partial class MainMenu : Control
|
||||
{
|
||||
}
|
||||
""");
|
||||
|
||||
const string expected = """
|
||||
// <auto-generated />
|
||||
@ -92,70 +108,21 @@ public class AutoUiPageGeneratorTests
|
||||
|
||||
await GeneratorTest<AutoUiPageGenerator>.RunAsync(
|
||||
source,
|
||||
("TestApp_MainMenu.AutoUiPage.g.cs", expected));
|
||||
("TestApp_MainMenu.AutoUiPage.g.cs", expected)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Reports_Diagnostic_When_AutoUiPage_Attribute_Arguments_Are_Invalid()
|
||||
{
|
||||
const string source = """
|
||||
using System;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
||||
using Godot;
|
||||
|
||||
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class AutoUiPageAttribute : Attribute
|
||||
{
|
||||
public AutoUiPageAttribute(string key) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace Godot
|
||||
{
|
||||
public class Node { }
|
||||
public class CanvasItem : Node { }
|
||||
public class Control : CanvasItem { }
|
||||
}
|
||||
|
||||
namespace GFramework.Game.Abstractions.Enums
|
||||
{
|
||||
public enum UiLayer
|
||||
{
|
||||
Page
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Game.Abstractions.UI
|
||||
{
|
||||
public interface IUiPageBehavior { }
|
||||
}
|
||||
|
||||
namespace GFramework.Godot.UI
|
||||
{
|
||||
using GFramework.Game.Abstractions.Enums;
|
||||
using GFramework.Game.Abstractions.UI;
|
||||
using Godot;
|
||||
|
||||
public static class UiPageBehaviorFactory
|
||||
{
|
||||
public static IUiPageBehavior Create<T>(T owner, string key, UiLayer layer)
|
||||
where T : CanvasItem
|
||||
{
|
||||
return null!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
[{|#0:AutoUiPage("MainMenu")|}]
|
||||
public partial class MainMenu : Control
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
string source = CreateAutoUiPageSource(
|
||||
AutoUiPageAttributeWithoutLayerDeclaration,
|
||||
UiLayerPageOnlyEnum,
|
||||
"""
|
||||
[{|#0:AutoUiPage("MainMenu")|}]
|
||||
public partial class MainMenu : Control
|
||||
{
|
||||
}
|
||||
""");
|
||||
|
||||
var test = new CSharpSourceGeneratorTest<AutoUiPageGenerator, DefaultVerifier>
|
||||
{
|
||||
@ -174,74 +141,25 @@ public class AutoUiPageGeneratorTests
|
||||
"MainMenu",
|
||||
"a string key argument and a string UiLayer name argument"));
|
||||
|
||||
await test.RunAsync();
|
||||
await test.RunAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Generates_Type_Constraints_For_ClassNullable_NotNull_And_Unmanaged()
|
||||
{
|
||||
const string source = """
|
||||
#nullable enable
|
||||
using System;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
||||
using Godot;
|
||||
|
||||
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class AutoUiPageAttribute : Attribute
|
||||
{
|
||||
public AutoUiPageAttribute(string key, string layerName) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace Godot
|
||||
{
|
||||
public class Node { }
|
||||
public class CanvasItem : Node { }
|
||||
public class Control : CanvasItem { }
|
||||
}
|
||||
|
||||
namespace GFramework.Game.Abstractions.Enums
|
||||
{
|
||||
public enum UiLayer
|
||||
{
|
||||
Page
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Game.Abstractions.UI
|
||||
{
|
||||
public interface IUiPageBehavior { }
|
||||
}
|
||||
|
||||
namespace GFramework.Godot.UI
|
||||
{
|
||||
using GFramework.Game.Abstractions.Enums;
|
||||
using GFramework.Game.Abstractions.UI;
|
||||
using Godot;
|
||||
|
||||
public static class UiPageBehaviorFactory
|
||||
{
|
||||
public static IUiPageBehavior Create<T>(T owner, string key, UiLayer layer)
|
||||
where T : CanvasItem
|
||||
{
|
||||
return null!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
[AutoUiPage("MainMenu", "Page")]
|
||||
public partial class MainMenu<TReference, TNotNull, TUnmanaged> : Control
|
||||
where TReference : class?
|
||||
where TNotNull : notnull
|
||||
where TUnmanaged : unmanaged
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
string source = CreateAutoUiPageSource(
|
||||
AutoUiPageAttributeWithLayerDeclaration,
|
||||
UiLayerPageOnlyEnum,
|
||||
"""
|
||||
[AutoUiPage("MainMenu", "Page")]
|
||||
public partial class MainMenu<TReference, TNotNull, TUnmanaged> : Control
|
||||
where TReference : class?
|
||||
where TNotNull : notnull
|
||||
where TUnmanaged : unmanaged
|
||||
{
|
||||
}
|
||||
""",
|
||||
nullableEnabled: true);
|
||||
|
||||
const string expected = """
|
||||
// <auto-generated />
|
||||
@ -268,6 +186,40 @@ public class AutoUiPageGeneratorTests
|
||||
|
||||
await GeneratorTest<AutoUiPageGenerator>.RunAsync(
|
||||
source,
|
||||
("TestApp_MainMenu.AutoUiPage.g.cs", expected));
|
||||
("TestApp_MainMenu.AutoUiPage.g.cs", expected)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static string CreateAutoUiPageSource(
|
||||
string attributeDeclaration,
|
||||
string uiLayerDeclaration,
|
||||
string testAppSource,
|
||||
bool nullableEnabled = false)
|
||||
{
|
||||
string nullableDirective = nullableEnabled ? "#nullable enable\n" : string.Empty;
|
||||
|
||||
return $$"""
|
||||
{{nullableDirective}}using System;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
||||
using Godot;
|
||||
|
||||
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
|
||||
{
|
||||
{{attributeDeclaration}}
|
||||
}
|
||||
|
||||
namespace Godot
|
||||
{
|
||||
{{CanvasNodeTypes}}
|
||||
}
|
||||
|
||||
{{uiLayerDeclaration}}
|
||||
|
||||
{{UiBehaviorInfrastructure}}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
{{testAppSource}}
|
||||
}
|
||||
""";
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,93 +8,103 @@ namespace GFramework.Godot.SourceGenerators.Tests.BindNodeSignal;
|
||||
[TestFixture]
|
||||
public class BindNodeSignalGeneratorTests
|
||||
{
|
||||
private const string BindNodeSignalAttributeDeclaration = """
|
||||
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
|
||||
public sealed class BindNodeSignalAttribute : Attribute
|
||||
{
|
||||
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
|
||||
{
|
||||
NodeFieldName = nodeFieldName;
|
||||
SignalName = signalName;
|
||||
}
|
||||
|
||||
public string NodeFieldName { get; }
|
||||
|
||||
public string SignalName { get; }
|
||||
}
|
||||
""";
|
||||
|
||||
private const string GetNodeAttributeDeclaration = """
|
||||
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class GetNodeAttribute : Attribute
|
||||
{
|
||||
}
|
||||
""";
|
||||
|
||||
private const string EmptyNodeType = """
|
||||
public class Node
|
||||
{
|
||||
}
|
||||
""";
|
||||
|
||||
private const string LifecycleNodeType = """
|
||||
public class Node
|
||||
{
|
||||
public virtual void _Ready() {}
|
||||
|
||||
public virtual void _ExitTree() {}
|
||||
}
|
||||
""";
|
||||
|
||||
private const string ButtonType = """
|
||||
public class Button : Node
|
||||
{
|
||||
public event Action? Pressed
|
||||
{
|
||||
add {}
|
||||
remove {}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private const string SpinBoxType = """
|
||||
public class SpinBox : Node
|
||||
{
|
||||
public delegate void ValueChangedEventHandler(double value);
|
||||
|
||||
public event ValueChangedEventHandler? ValueChanged
|
||||
{
|
||||
add {}
|
||||
remove {}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
/// <summary>
|
||||
/// 验证生成器会为已有生命周期调用生成成对的绑定与解绑方法。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Generates_Bind_And_Unbind_Methods_For_Existing_Lifecycle_Hooks()
|
||||
{
|
||||
const string source = """
|
||||
using System;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||
using Godot;
|
||||
string source = CreateHudSource(
|
||||
CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
|
||||
"""
|
||||
private Button _startButton = null!;
|
||||
private SpinBox _startOreSpinBox = null!;
|
||||
|
||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
|
||||
public sealed class BindNodeSignalAttribute : Attribute
|
||||
{
|
||||
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
|
||||
{
|
||||
NodeFieldName = nodeFieldName;
|
||||
SignalName = signalName;
|
||||
}
|
||||
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
|
||||
private void OnStartButtonPressed()
|
||||
{
|
||||
}
|
||||
|
||||
public string NodeFieldName { get; }
|
||||
[BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))]
|
||||
private void OnStartOreValueChanged(double value)
|
||||
{
|
||||
}
|
||||
|
||||
public string SignalName { get; }
|
||||
}
|
||||
}
|
||||
public override void _Ready()
|
||||
{
|
||||
__BindNodeSignals_Generated();
|
||||
}
|
||||
|
||||
namespace Godot
|
||||
{
|
||||
public class Node
|
||||
{
|
||||
public virtual void _Ready() {}
|
||||
|
||||
public virtual void _ExitTree() {}
|
||||
}
|
||||
|
||||
public class Button : Node
|
||||
{
|
||||
public event Action? Pressed
|
||||
{
|
||||
add {}
|
||||
remove {}
|
||||
}
|
||||
}
|
||||
|
||||
public class SpinBox : Node
|
||||
{
|
||||
public delegate void ValueChangedEventHandler(double value);
|
||||
|
||||
public event ValueChangedEventHandler? ValueChanged
|
||||
{
|
||||
add {}
|
||||
remove {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
public partial class Hud : Node
|
||||
{
|
||||
private Button _startButton = null!;
|
||||
private SpinBox _startOreSpinBox = null!;
|
||||
|
||||
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
|
||||
private void OnStartButtonPressed()
|
||||
{
|
||||
}
|
||||
|
||||
[BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))]
|
||||
private void OnStartOreValueChanged(double value)
|
||||
{
|
||||
}
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
__BindNodeSignals_Generated();
|
||||
}
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
__UnbindNodeSignals_Generated();
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
public override void _ExitTree()
|
||||
{
|
||||
__UnbindNodeSignals_Generated();
|
||||
}
|
||||
""",
|
||||
LifecycleNodeType,
|
||||
ButtonType,
|
||||
SpinBoxType);
|
||||
|
||||
const string expected = """
|
||||
// <auto-generated />
|
||||
@ -121,7 +131,7 @@ public class BindNodeSignalGeneratorTests
|
||||
|
||||
await GeneratorTest<BindNodeSignalGenerator>.RunAsync(
|
||||
source,
|
||||
("TestApp_Hud.BindNodeSignal.g.cs", expected));
|
||||
("TestApp_Hud.BindNodeSignal.g.cs", expected)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -130,70 +140,23 @@ public class BindNodeSignalGeneratorTests
|
||||
[Test]
|
||||
public async Task Generates_Multiple_Subscriptions_For_The_Same_Handler_And_Coexists_With_GetNode()
|
||||
{
|
||||
const string source = """
|
||||
using System;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||
using Godot;
|
||||
string source = CreateHudSource(
|
||||
CreateAbstractionsSource(BindNodeSignalAttributeDeclaration, GetNodeAttributeDeclaration),
|
||||
"""
|
||||
[GetNode]
|
||||
private Button _startButton = null!;
|
||||
|
||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
|
||||
public sealed class BindNodeSignalAttribute : Attribute
|
||||
{
|
||||
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
|
||||
{
|
||||
NodeFieldName = nodeFieldName;
|
||||
SignalName = signalName;
|
||||
}
|
||||
[GetNode]
|
||||
private Button _cancelButton = null!;
|
||||
|
||||
public string NodeFieldName { get; }
|
||||
|
||||
public string SignalName { get; }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class GetNodeAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
namespace Godot
|
||||
{
|
||||
public class Node
|
||||
{
|
||||
public virtual void _Ready() {}
|
||||
|
||||
public virtual void _ExitTree() {}
|
||||
}
|
||||
|
||||
public class Button : Node
|
||||
{
|
||||
public event Action? Pressed
|
||||
{
|
||||
add {}
|
||||
remove {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
public partial class Hud : Node
|
||||
{
|
||||
[GetNode]
|
||||
private Button _startButton = null!;
|
||||
|
||||
[GetNode]
|
||||
private Button _cancelButton = null!;
|
||||
|
||||
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
|
||||
[BindNodeSignal(nameof(_cancelButton), nameof(Button.Pressed))]
|
||||
private void OnAnyButtonPressed()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
|
||||
[BindNodeSignal(nameof(_cancelButton), nameof(Button.Pressed))]
|
||||
private void OnAnyButtonPressed()
|
||||
{
|
||||
}
|
||||
""",
|
||||
LifecycleNodeType,
|
||||
ButtonType);
|
||||
|
||||
const string expected = """
|
||||
// <auto-generated />
|
||||
@ -220,7 +183,7 @@ public class BindNodeSignalGeneratorTests
|
||||
|
||||
await GeneratorTest<BindNodeSignalGenerator>.RunAsync(
|
||||
source,
|
||||
("TestApp_Hud.BindNodeSignal.g.cs", expected));
|
||||
("TestApp_Hud.BindNodeSignal.g.cs", expected)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -229,73 +192,24 @@ public class BindNodeSignalGeneratorTests
|
||||
[Test]
|
||||
public async Task Reports_Diagnostic_When_Signal_Does_Not_Exist()
|
||||
{
|
||||
const string source = """
|
||||
using System;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||
using Godot;
|
||||
string source = CreateHudSource(
|
||||
CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
|
||||
"""
|
||||
private Button _startButton = null!;
|
||||
|
||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
|
||||
public sealed class BindNodeSignalAttribute : Attribute
|
||||
{
|
||||
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
|
||||
{
|
||||
NodeFieldName = nodeFieldName;
|
||||
SignalName = signalName;
|
||||
}
|
||||
[{|#0:BindNodeSignal(nameof(_startButton), "Released")|}]
|
||||
private void OnStartButtonPressed()
|
||||
{
|
||||
}
|
||||
""",
|
||||
EmptyNodeType,
|
||||
ButtonType);
|
||||
|
||||
public string NodeFieldName { get; }
|
||||
|
||||
public string SignalName { get; }
|
||||
}
|
||||
}
|
||||
|
||||
namespace Godot
|
||||
{
|
||||
public class Node
|
||||
{
|
||||
}
|
||||
|
||||
public class Button : Node
|
||||
{
|
||||
public event Action? Pressed
|
||||
{
|
||||
add {}
|
||||
remove {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
public partial class Hud : Node
|
||||
{
|
||||
private Button _startButton = null!;
|
||||
|
||||
[{|#0:BindNodeSignal(nameof(_startButton), "Released")|}]
|
||||
private void OnStartButtonPressed()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var test = new CSharpSourceGeneratorTest<BindNodeSignalGenerator, DefaultVerifier>
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
Sources = { source }
|
||||
},
|
||||
DisabledDiagnostics = { "GF_Common_Trace_001" },
|
||||
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
|
||||
};
|
||||
|
||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_006", DiagnosticSeverity.Error)
|
||||
.WithLocation(0)
|
||||
.WithArguments("_startButton", "Released"));
|
||||
|
||||
await test.RunAsync();
|
||||
await VerifyDiagnosticsAsync(
|
||||
source,
|
||||
new DiagnosticResult("GF_Godot_BindNodeSignal_006", DiagnosticSeverity.Error)
|
||||
.WithLocation(0)
|
||||
.WithArguments("_startButton", "Released")).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -304,75 +218,24 @@ public class BindNodeSignalGeneratorTests
|
||||
[Test]
|
||||
public async Task Reports_Diagnostic_When_Method_Signature_Does_Not_Match_Event()
|
||||
{
|
||||
const string source = """
|
||||
using System;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||
using Godot;
|
||||
string source = CreateHudSource(
|
||||
CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
|
||||
"""
|
||||
private SpinBox _startOreSpinBox = null!;
|
||||
|
||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
|
||||
public sealed class BindNodeSignalAttribute : Attribute
|
||||
{
|
||||
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
|
||||
{
|
||||
NodeFieldName = nodeFieldName;
|
||||
SignalName = signalName;
|
||||
}
|
||||
[{|#0:BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))|}]
|
||||
private void OnStartOreValueChanged()
|
||||
{
|
||||
}
|
||||
""",
|
||||
EmptyNodeType,
|
||||
SpinBoxType);
|
||||
|
||||
public string NodeFieldName { get; }
|
||||
|
||||
public string SignalName { get; }
|
||||
}
|
||||
}
|
||||
|
||||
namespace Godot
|
||||
{
|
||||
public class Node
|
||||
{
|
||||
}
|
||||
|
||||
public class SpinBox : Node
|
||||
{
|
||||
public delegate void ValueChangedEventHandler(double value);
|
||||
|
||||
public event ValueChangedEventHandler? ValueChanged
|
||||
{
|
||||
add {}
|
||||
remove {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
public partial class Hud : Node
|
||||
{
|
||||
private SpinBox _startOreSpinBox = null!;
|
||||
|
||||
[{|#0:BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))|}]
|
||||
private void OnStartOreValueChanged()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var test = new CSharpSourceGeneratorTest<BindNodeSignalGenerator, DefaultVerifier>
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
Sources = { source }
|
||||
},
|
||||
DisabledDiagnostics = { "GF_Common_Trace_001" },
|
||||
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
|
||||
};
|
||||
|
||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_007", DiagnosticSeverity.Error)
|
||||
.WithLocation(0)
|
||||
.WithArguments("OnStartOreValueChanged", "ValueChanged", "_startOreSpinBox"));
|
||||
|
||||
await test.RunAsync();
|
||||
await VerifyDiagnosticsAsync(
|
||||
source,
|
||||
new DiagnosticResult("GF_Godot_BindNodeSignal_007", DiagnosticSeverity.Error)
|
||||
.WithLocation(0)
|
||||
.WithArguments("OnStartOreValueChanged", "ValueChanged", "_startOreSpinBox")).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -381,73 +244,24 @@ public class BindNodeSignalGeneratorTests
|
||||
[Test]
|
||||
public async Task Reports_Diagnostic_When_Constructor_Argument_Is_Empty()
|
||||
{
|
||||
const string source = """
|
||||
using System;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||
using Godot;
|
||||
string source = CreateHudSource(
|
||||
CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
|
||||
"""
|
||||
private Button _startButton = null!;
|
||||
|
||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
|
||||
public sealed class BindNodeSignalAttribute : Attribute
|
||||
{
|
||||
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
|
||||
{
|
||||
NodeFieldName = nodeFieldName;
|
||||
SignalName = signalName;
|
||||
}
|
||||
[{|#0:BindNodeSignal(nameof(_startButton), "")|}]
|
||||
private void OnStartButtonPressed()
|
||||
{
|
||||
}
|
||||
""",
|
||||
EmptyNodeType,
|
||||
ButtonType);
|
||||
|
||||
public string NodeFieldName { get; }
|
||||
|
||||
public string SignalName { get; }
|
||||
}
|
||||
}
|
||||
|
||||
namespace Godot
|
||||
{
|
||||
public class Node
|
||||
{
|
||||
}
|
||||
|
||||
public class Button : Node
|
||||
{
|
||||
public event Action? Pressed
|
||||
{
|
||||
add {}
|
||||
remove {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
public partial class Hud : Node
|
||||
{
|
||||
private Button _startButton = null!;
|
||||
|
||||
[{|#0:BindNodeSignal(nameof(_startButton), "")|}]
|
||||
private void OnStartButtonPressed()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var test = new CSharpSourceGeneratorTest<BindNodeSignalGenerator, DefaultVerifier>
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
Sources = { source }
|
||||
},
|
||||
DisabledDiagnostics = { "GF_Common_Trace_001" },
|
||||
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
|
||||
};
|
||||
|
||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_010", DiagnosticSeverity.Error)
|
||||
.WithLocation(0)
|
||||
.WithArguments("OnStartButtonPressed", "signalName"));
|
||||
|
||||
await test.RunAsync();
|
||||
await VerifyDiagnosticsAsync(
|
||||
source,
|
||||
new DiagnosticResult("GF_Godot_BindNodeSignal_010", DiagnosticSeverity.Error)
|
||||
.WithLocation(0)
|
||||
.WithArguments("OnStartButtonPressed", "signalName")).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -456,85 +270,35 @@ public class BindNodeSignalGeneratorTests
|
||||
[Test]
|
||||
public async Task Reports_Diagnostic_When_Generated_Method_Names_Already_Exist()
|
||||
{
|
||||
const string source = """
|
||||
using System;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||
using Godot;
|
||||
string source = CreateHudSource(
|
||||
CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
|
||||
"""
|
||||
private Button _startButton = null!;
|
||||
|
||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
|
||||
public sealed class BindNodeSignalAttribute : Attribute
|
||||
{
|
||||
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
|
||||
{
|
||||
NodeFieldName = nodeFieldName;
|
||||
SignalName = signalName;
|
||||
}
|
||||
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
|
||||
private void OnStartButtonPressed()
|
||||
{
|
||||
}
|
||||
|
||||
public string NodeFieldName { get; }
|
||||
private void {|#0:__BindNodeSignals_Generated|}()
|
||||
{
|
||||
}
|
||||
|
||||
public string SignalName { get; }
|
||||
}
|
||||
}
|
||||
private void {|#1:__UnbindNodeSignals_Generated|}()
|
||||
{
|
||||
}
|
||||
""",
|
||||
EmptyNodeType,
|
||||
ButtonType);
|
||||
|
||||
namespace Godot
|
||||
{
|
||||
public class Node
|
||||
{
|
||||
}
|
||||
|
||||
public class Button : Node
|
||||
{
|
||||
public event Action? Pressed
|
||||
{
|
||||
add {}
|
||||
remove {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
public partial class Hud : Node
|
||||
{
|
||||
private Button _startButton = null!;
|
||||
|
||||
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
|
||||
private void OnStartButtonPressed()
|
||||
{
|
||||
}
|
||||
|
||||
private void {|#0:__BindNodeSignals_Generated|}()
|
||||
{
|
||||
}
|
||||
|
||||
private void {|#1:__UnbindNodeSignals_Generated|}()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var test = new CSharpSourceGeneratorTest<BindNodeSignalGenerator, DefaultVerifier>
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
Sources = { source }
|
||||
},
|
||||
DisabledDiagnostics = { "GF_Common_Trace_001" },
|
||||
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
|
||||
};
|
||||
|
||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error)
|
||||
.WithLocation(0)
|
||||
.WithArguments("Hud", "__BindNodeSignals_Generated"));
|
||||
|
||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error)
|
||||
.WithLocation(1)
|
||||
.WithArguments("Hud", "__UnbindNodeSignals_Generated"));
|
||||
|
||||
await test.RunAsync();
|
||||
await VerifyDiagnosticsAsync(
|
||||
source,
|
||||
new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error)
|
||||
.WithLocation(0)
|
||||
.WithArguments("Hud", "__BindNodeSignals_Generated"),
|
||||
new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error)
|
||||
.WithLocation(1)
|
||||
.WithArguments("Hud", "__UnbindNodeSignals_Generated")).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -543,69 +307,80 @@ public class BindNodeSignalGeneratorTests
|
||||
[Test]
|
||||
public async Task Reports_Warnings_When_Lifecycle_Methods_Do_Not_Call_Generated_Methods()
|
||||
{
|
||||
const string source = """
|
||||
using System;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||
using Godot;
|
||||
string source = CreateHudSource(
|
||||
CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
|
||||
"""
|
||||
private Button _startButton = null!;
|
||||
|
||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
|
||||
public sealed class BindNodeSignalAttribute : Attribute
|
||||
{
|
||||
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
|
||||
{
|
||||
NodeFieldName = nodeFieldName;
|
||||
SignalName = signalName;
|
||||
}
|
||||
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
|
||||
private void OnStartButtonPressed()
|
||||
{
|
||||
}
|
||||
|
||||
public string NodeFieldName { get; }
|
||||
public override void {|#0:_Ready|}()
|
||||
{
|
||||
}
|
||||
|
||||
public string SignalName { get; }
|
||||
}
|
||||
}
|
||||
public override void {|#1:_ExitTree|}()
|
||||
{
|
||||
}
|
||||
""",
|
||||
LifecycleNodeType,
|
||||
ButtonType);
|
||||
|
||||
namespace Godot
|
||||
{
|
||||
public class Node
|
||||
{
|
||||
public virtual void _Ready() {}
|
||||
await VerifyDiagnosticsAsync(
|
||||
source,
|
||||
new DiagnosticResult("GF_Godot_BindNodeSignal_008", DiagnosticSeverity.Warning)
|
||||
.WithLocation(0)
|
||||
.WithArguments("Hud"),
|
||||
new DiagnosticResult("GF_Godot_BindNodeSignal_009", DiagnosticSeverity.Warning)
|
||||
.WithLocation(1)
|
||||
.WithArguments("Hud")).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public virtual void _ExitTree() {}
|
||||
}
|
||||
private static string CreateAbstractionsSource(params string[] attributeDeclarations)
|
||||
{
|
||||
string declarations = string.Join($"{Environment.NewLine}{Environment.NewLine}", attributeDeclarations);
|
||||
|
||||
public class Button : Node
|
||||
{
|
||||
public event Action? Pressed
|
||||
{
|
||||
add {}
|
||||
remove {}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $$"""
|
||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
||||
{
|
||||
{{declarations}}
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
public partial class Hud : Node
|
||||
{
|
||||
private Button _startButton = null!;
|
||||
private static string CreateHudSource(
|
||||
string abstractionsSource,
|
||||
string hudMembers,
|
||||
params string[] godotTypes)
|
||||
{
|
||||
string godotSource = string.Join($"{Environment.NewLine}{Environment.NewLine}", godotTypes);
|
||||
|
||||
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
|
||||
private void OnStartButtonPressed()
|
||||
{
|
||||
}
|
||||
return $$"""
|
||||
using System;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||
using Godot;
|
||||
|
||||
public override void {|#0:_Ready|}()
|
||||
{
|
||||
}
|
||||
{{abstractionsSource}}
|
||||
|
||||
public override void {|#1:_ExitTree|}()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
namespace Godot
|
||||
{
|
||||
{{godotSource}}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
public partial class Hud : Node
|
||||
{
|
||||
{{hudMembers}}
|
||||
}
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
private static Task VerifyDiagnosticsAsync(string source, params DiagnosticResult[] expectedDiagnostics)
|
||||
{
|
||||
var test = new CSharpSourceGeneratorTest<BindNodeSignalGenerator, DefaultVerifier>
|
||||
{
|
||||
TestState =
|
||||
@ -616,14 +391,11 @@ public class BindNodeSignalGeneratorTests
|
||||
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
|
||||
};
|
||||
|
||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_008", DiagnosticSeverity.Warning)
|
||||
.WithLocation(0)
|
||||
.WithArguments("Hud"));
|
||||
foreach (DiagnosticResult expectedDiagnostic in expectedDiagnostics)
|
||||
{
|
||||
test.ExpectedDiagnostics.Add(expectedDiagnostic);
|
||||
}
|
||||
|
||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_009", DiagnosticSeverity.Warning)
|
||||
.WithLocation(1)
|
||||
.WithArguments("Hud"));
|
||||
|
||||
await test.RunAsync();
|
||||
return test.RunAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ public static class GeneratorTest<TGenerator>
|
||||
test.TestState.GeneratedSources.Add(
|
||||
(typeof(TGenerator), filename, NormalizeLineEndings(content)));
|
||||
|
||||
await test.RunAsync();
|
||||
await test.RunAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -44,4 +44,4 @@ public static class GeneratorTest<TGenerator>
|
||||
.Replace("\r", "\n", StringComparison.Ordinal)
|
||||
.Replace("\n", Environment.NewLine, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,61 +5,88 @@ namespace GFramework.Godot.SourceGenerators.Tests.GetNode;
|
||||
[TestFixture]
|
||||
public class GetNodeGeneratorTests
|
||||
{
|
||||
private const string FullGetNodeAttributeDeclaration = """
|
||||
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class GetNodeAttribute : Attribute
|
||||
{
|
||||
public GetNodeAttribute() {}
|
||||
public GetNodeAttribute(string path) { Path = path; }
|
||||
public string? Path { get; set; }
|
||||
public bool Required { get; set; } = true;
|
||||
public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto;
|
||||
}
|
||||
|
||||
public enum NodeLookupMode
|
||||
{
|
||||
Auto = 0,
|
||||
UniqueName = 1,
|
||||
RelativePath = 2,
|
||||
AbsolutePath = 3
|
||||
}
|
||||
""";
|
||||
|
||||
private const string MinimalGetNodeAttributeDeclaration = """
|
||||
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class GetNodeAttribute : Attribute
|
||||
{
|
||||
public GetNodeAttribute() {}
|
||||
}
|
||||
|
||||
public enum NodeLookupMode
|
||||
{
|
||||
Auto = 0
|
||||
}
|
||||
""";
|
||||
|
||||
private const string PropertyOnlyGetNodeAttributeDeclaration = """
|
||||
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class GetNodeAttribute : Attribute
|
||||
{
|
||||
public string? Path { get; set; }
|
||||
public bool Required { get; set; } = true;
|
||||
public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto;
|
||||
}
|
||||
|
||||
public enum NodeLookupMode
|
||||
{
|
||||
Auto = 0,
|
||||
UniqueName = 1,
|
||||
RelativePath = 2,
|
||||
AbsolutePath = 3
|
||||
}
|
||||
""";
|
||||
|
||||
private const string NodeWithReadyAndLookupMethods = """
|
||||
public class Node
|
||||
{
|
||||
public virtual void _Ready() {}
|
||||
public T GetNode<T>(string path) where T : Node => throw new InvalidOperationException(path);
|
||||
public T? GetNodeOrNull<T>(string path) where T : Node => default;
|
||||
}
|
||||
""";
|
||||
|
||||
private const string HBoxContainerType = """
|
||||
public class HBoxContainer : Node
|
||||
{
|
||||
}
|
||||
""";
|
||||
|
||||
[Test]
|
||||
public async Task Generates_InferredUniqueNameBindings_And_ReadyHook_WhenReadyIsMissing()
|
||||
{
|
||||
const string source = """
|
||||
using System;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||
using Godot;
|
||||
string source = CreateGetNodeSource(
|
||||
FullGetNodeAttributeDeclaration,
|
||||
"""
|
||||
public partial class TopBar : HBoxContainer
|
||||
{
|
||||
[GetNode]
|
||||
private HBoxContainer _leftContainer = null!;
|
||||
|
||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class GetNodeAttribute : Attribute
|
||||
{
|
||||
public GetNodeAttribute() {}
|
||||
public GetNodeAttribute(string path) { Path = path; }
|
||||
public string? Path { get; set; }
|
||||
public bool Required { get; set; } = true;
|
||||
public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto;
|
||||
}
|
||||
|
||||
public enum NodeLookupMode
|
||||
{
|
||||
Auto = 0,
|
||||
UniqueName = 1,
|
||||
RelativePath = 2,
|
||||
AbsolutePath = 3
|
||||
}
|
||||
}
|
||||
|
||||
namespace Godot
|
||||
{
|
||||
public class Node
|
||||
{
|
||||
public virtual void _Ready() {}
|
||||
public T GetNode<T>(string path) where T : Node => throw new InvalidOperationException(path);
|
||||
public T? GetNodeOrNull<T>(string path) where T : Node => default;
|
||||
}
|
||||
|
||||
public class HBoxContainer : Node
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
public partial class TopBar : HBoxContainer
|
||||
{
|
||||
[GetNode]
|
||||
private HBoxContainer _leftContainer = null!;
|
||||
|
||||
[GetNode]
|
||||
private HBoxContainer m_rightContainer = null!;
|
||||
}
|
||||
}
|
||||
""";
|
||||
[GetNode]
|
||||
private HBoxContainer m_rightContainer = null!;
|
||||
}
|
||||
""",
|
||||
HBoxContainerType);
|
||||
|
||||
const string expected = """
|
||||
// <auto-generated />
|
||||
@ -88,69 +115,30 @@ public class GetNodeGeneratorTests
|
||||
|
||||
await GeneratorTest<GetNodeGenerator>.RunAsync(
|
||||
source,
|
||||
("TestApp_TopBar.GetNode.g.cs", expected));
|
||||
("TestApp_TopBar.GetNode.g.cs", expected)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Generates_ManualInjectionOnly_WhenReadyAlreadyExists()
|
||||
{
|
||||
const string source = """
|
||||
using System;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||
using Godot;
|
||||
string source = CreateGetNodeSource(
|
||||
FullGetNodeAttributeDeclaration,
|
||||
"""
|
||||
public partial class TopBar : HBoxContainer
|
||||
{
|
||||
[GetNode("%LeftContainer")]
|
||||
private HBoxContainer _leftContainer = null!;
|
||||
|
||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class GetNodeAttribute : Attribute
|
||||
{
|
||||
public GetNodeAttribute() {}
|
||||
public GetNodeAttribute(string path) { Path = path; }
|
||||
public string? Path { get; set; }
|
||||
public bool Required { get; set; } = true;
|
||||
public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto;
|
||||
}
|
||||
[GetNode(Required = false, Lookup = NodeLookupMode.RelativePath)]
|
||||
private HBoxContainer? _rightContainer;
|
||||
|
||||
public enum NodeLookupMode
|
||||
{
|
||||
Auto = 0,
|
||||
UniqueName = 1,
|
||||
RelativePath = 2,
|
||||
AbsolutePath = 3
|
||||
}
|
||||
}
|
||||
|
||||
namespace Godot
|
||||
{
|
||||
public class Node
|
||||
{
|
||||
public virtual void _Ready() {}
|
||||
public T GetNode<T>(string path) where T : Node => throw new InvalidOperationException(path);
|
||||
public T? GetNodeOrNull<T>(string path) where T : Node => default;
|
||||
}
|
||||
|
||||
public class HBoxContainer : Node
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
public partial class TopBar : HBoxContainer
|
||||
{
|
||||
[GetNode("%LeftContainer")]
|
||||
private HBoxContainer _leftContainer = null!;
|
||||
|
||||
[GetNode(Required = false, Lookup = NodeLookupMode.RelativePath)]
|
||||
private HBoxContainer? _rightContainer;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
__InjectGetNodes_Generated();
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
public override void _Ready()
|
||||
{
|
||||
__InjectGetNodes_Generated();
|
||||
}
|
||||
}
|
||||
""",
|
||||
HBoxContainerType);
|
||||
|
||||
const string expected = """
|
||||
// <auto-generated />
|
||||
@ -171,7 +159,7 @@ public class GetNodeGeneratorTests
|
||||
|
||||
await GeneratorTest<GetNodeGenerator>.RunAsync(
|
||||
source,
|
||||
("TestApp_TopBar.GetNode.g.cs", expected));
|
||||
("TestApp_TopBar.GetNode.g.cs", expected)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -234,58 +222,26 @@ public class GetNodeGeneratorTests
|
||||
.WithSpan(39, 24, 39, 38)
|
||||
.WithArguments("_leftContainer"));
|
||||
|
||||
await test.RunAsync();
|
||||
await test.RunAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Reports_Diagnostic_When_Generated_Injection_Method_Name_Already_Exists()
|
||||
{
|
||||
const string source = """
|
||||
using System;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||
using Godot;
|
||||
string source = CreateGetNodeSource(
|
||||
MinimalGetNodeAttributeDeclaration,
|
||||
"""
|
||||
public partial class TopBar : HBoxContainer
|
||||
{
|
||||
[GetNode]
|
||||
private HBoxContainer _leftContainer = null!;
|
||||
|
||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class GetNodeAttribute : Attribute
|
||||
{
|
||||
public GetNodeAttribute() {}
|
||||
}
|
||||
|
||||
public enum NodeLookupMode
|
||||
{
|
||||
Auto = 0
|
||||
}
|
||||
}
|
||||
|
||||
namespace Godot
|
||||
{
|
||||
public class Node
|
||||
{
|
||||
public virtual void _Ready() {}
|
||||
public T GetNode<T>(string path) where T : Node => throw new InvalidOperationException(path);
|
||||
public T? GetNodeOrNull<T>(string path) where T : Node => default;
|
||||
}
|
||||
|
||||
public class HBoxContainer : Node
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
public partial class TopBar : HBoxContainer
|
||||
{
|
||||
[GetNode]
|
||||
private HBoxContainer _leftContainer = null!;
|
||||
|
||||
private void {|#0:__InjectGetNodes_Generated|}()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
private void {|#0:__InjectGetNodes_Generated|}()
|
||||
{
|
||||
}
|
||||
}
|
||||
""",
|
||||
HBoxContainerType);
|
||||
|
||||
var test = new CSharpSourceGeneratorTest<GetNodeGenerator, DefaultVerifier>
|
||||
{
|
||||
@ -301,6 +257,39 @@ public class GetNodeGeneratorTests
|
||||
.WithLocation(0)
|
||||
.WithArguments("TopBar", "__InjectGetNodes_Generated"));
|
||||
|
||||
await test.RunAsync();
|
||||
await test.RunAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static string CreateGetNodeSource(
|
||||
string attributeDeclaration,
|
||||
string testAppSource,
|
||||
params string[] godotTypes)
|
||||
{
|
||||
string[] allGodotTypes = new string[godotTypes.Length + 1];
|
||||
allGodotTypes[0] = NodeWithReadyAndLookupMethods;
|
||||
Array.Copy(godotTypes, 0, allGodotTypes, 1, godotTypes.Length);
|
||||
|
||||
string godotSource = string.Join($"{Environment.NewLine}{Environment.NewLine}", allGodotTypes);
|
||||
|
||||
return $$"""
|
||||
using System;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||
using Godot;
|
||||
|
||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
||||
{
|
||||
{{attributeDeclaration}}
|
||||
}
|
||||
|
||||
namespace Godot
|
||||
{
|
||||
{{godotSource}}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
{{testAppSource}}
|
||||
}
|
||||
""";
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,131 @@ namespace GFramework.Godot.SourceGenerators.Tests.Project;
|
||||
[TestFixture]
|
||||
public class GodotProjectMetadataGeneratorTests
|
||||
{
|
||||
private const string AutoLoadProjectFile = """
|
||||
[autoload]
|
||||
GameServices="*res://autoload/game_services.tscn"
|
||||
AudioBus="*res://autoload/audio_bus.gd"
|
||||
""";
|
||||
|
||||
private const string InputActionsProjectFile = """
|
||||
[input]
|
||||
move_up={
|
||||
"deadzone": 0.5
|
||||
}
|
||||
ui_cancel={
|
||||
"deadzone": 0.5
|
||||
}
|
||||
""";
|
||||
|
||||
private const string ExpectedAutoLoads = """
|
||||
// <auto-generated />
|
||||
#nullable enable
|
||||
|
||||
namespace GFramework.Godot.Generated;
|
||||
|
||||
/// <summary>
|
||||
/// 提供 project.godot 中 AutoLoad 单例的强类型访问入口。
|
||||
/// </summary>
|
||||
public static partial class AutoLoads
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取 AutoLoad <c>GameServices</c>。
|
||||
/// </summary>
|
||||
public static global::TestApp.GameServices GameServices => GetRequiredNode<global::TestApp.GameServices>("GameServices");
|
||||
|
||||
/// <summary>
|
||||
/// 尝试获取 AutoLoad <c>GameServices</c>。
|
||||
/// </summary>
|
||||
public static bool TryGetGameServices(out global::TestApp.GameServices? value)
|
||||
{
|
||||
return TryGetNode("GameServices", out value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 AutoLoad <c>AudioBus</c>。
|
||||
/// </summary>
|
||||
public static global::Godot.Node AudioBus => GetRequiredNode<global::Godot.Node>("AudioBus");
|
||||
|
||||
/// <summary>
|
||||
/// 尝试获取 AutoLoad <c>AudioBus</c>。
|
||||
/// </summary>
|
||||
public static bool TryGetAudioBus(out global::Godot.Node? value)
|
||||
{
|
||||
return TryGetNode("AudioBus", out value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取一个必填的 AutoLoad 节点;缺失时抛出异常。
|
||||
/// </summary>
|
||||
/// <typeparam name="TNode">节点类型。</typeparam>
|
||||
/// <param name="autoLoadName">AutoLoad 名称。</param>
|
||||
/// <returns>已解析的 AutoLoad 节点。</returns>
|
||||
private static TNode GetRequiredNode<TNode>(string autoLoadName)
|
||||
where TNode : global::Godot.Node
|
||||
{
|
||||
if (TryGetNode(autoLoadName, out TNode? value))
|
||||
{
|
||||
return value!;
|
||||
}
|
||||
|
||||
throw new global::System.InvalidOperationException($"AutoLoad '{autoLoadName}' is not available on the active SceneTree root.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试从当前 SceneTree 根节点解析 AutoLoad。
|
||||
/// </summary>
|
||||
/// <typeparam name="TNode">节点类型。</typeparam>
|
||||
/// <param name="autoLoadName">AutoLoad 名称。</param>
|
||||
/// <param name="value">解析到的节点实例。</param>
|
||||
/// <returns>若当前进程存在 SceneTree 且根节点中能解析到该 AutoLoad,则返回 <c>true</c>。</returns>
|
||||
private static bool TryGetNode<TNode>(string autoLoadName, out TNode? value)
|
||||
where TNode : global::Godot.Node
|
||||
{
|
||||
value = default;
|
||||
|
||||
if (global::Godot.Engine.GetMainLoop() is not global::Godot.SceneTree sceneTree)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var root = sceneTree.Root;
|
||||
if (root is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value = root.GetNodeOrNull<TNode>($"/root/{autoLoadName}");
|
||||
return value is not null;
|
||||
}
|
||||
}
|
||||
|
||||
""";
|
||||
|
||||
private const string ExpectedInputActions = """
|
||||
// <auto-generated />
|
||||
#nullable enable
|
||||
|
||||
namespace GFramework.Godot.Generated;
|
||||
|
||||
/// <summary>
|
||||
/// 提供 project.godot 中 Input Action 名称的强类型常量。
|
||||
/// </summary>
|
||||
public static partial class InputActions
|
||||
{
|
||||
/// <summary>
|
||||
/// Input Action <c>move_up</c> 的稳定名称。
|
||||
/// </summary>
|
||||
public const string MoveUp = "move_up";
|
||||
|
||||
/// <summary>
|
||||
/// Input Action <c>ui_cancel</c> 的稳定名称。
|
||||
/// </summary>
|
||||
public const string UiCancel = "ui_cancel";
|
||||
|
||||
}
|
||||
|
||||
""";
|
||||
|
||||
/// <summary>
|
||||
/// 验证会根据 AutoLoad 与 Input Action 生成稳定的强类型入口。
|
||||
/// </summary>
|
||||
@ -29,142 +154,19 @@ public class GodotProjectMetadataGeneratorTests
|
||||
""",
|
||||
includeAutoLoadAttribute: true);
|
||||
|
||||
const string projectFile = """
|
||||
[autoload]
|
||||
GameServices="*res://autoload/game_services.tscn"
|
||||
AudioBus="*res://autoload/audio_bus.gd"
|
||||
|
||||
[input]
|
||||
move_up={
|
||||
"deadzone": 0.5
|
||||
}
|
||||
ui_cancel={
|
||||
"deadzone": 0.5
|
||||
}
|
||||
""";
|
||||
|
||||
const string expectedAutoLoads = """
|
||||
// <auto-generated />
|
||||
#nullable enable
|
||||
|
||||
namespace GFramework.Godot.Generated;
|
||||
|
||||
/// <summary>
|
||||
/// 提供 project.godot 中 AutoLoad 单例的强类型访问入口。
|
||||
/// </summary>
|
||||
public static partial class AutoLoads
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取 AutoLoad <c>GameServices</c>。
|
||||
/// </summary>
|
||||
public static global::TestApp.GameServices GameServices => GetRequiredNode<global::TestApp.GameServices>("GameServices");
|
||||
|
||||
/// <summary>
|
||||
/// 尝试获取 AutoLoad <c>GameServices</c>。
|
||||
/// </summary>
|
||||
public static bool TryGetGameServices(out global::TestApp.GameServices? value)
|
||||
{
|
||||
return TryGetNode("GameServices", out value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 AutoLoad <c>AudioBus</c>。
|
||||
/// </summary>
|
||||
public static global::Godot.Node AudioBus => GetRequiredNode<global::Godot.Node>("AudioBus");
|
||||
|
||||
/// <summary>
|
||||
/// 尝试获取 AutoLoad <c>AudioBus</c>。
|
||||
/// </summary>
|
||||
public static bool TryGetAudioBus(out global::Godot.Node? value)
|
||||
{
|
||||
return TryGetNode("AudioBus", out value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取一个必填的 AutoLoad 节点;缺失时抛出异常。
|
||||
/// </summary>
|
||||
/// <typeparam name="TNode">节点类型。</typeparam>
|
||||
/// <param name="autoLoadName">AutoLoad 名称。</param>
|
||||
/// <returns>已解析的 AutoLoad 节点。</returns>
|
||||
private static TNode GetRequiredNode<TNode>(string autoLoadName)
|
||||
where TNode : global::Godot.Node
|
||||
{
|
||||
if (TryGetNode(autoLoadName, out TNode? value))
|
||||
{
|
||||
return value!;
|
||||
}
|
||||
|
||||
throw new global::System.InvalidOperationException($"AutoLoad '{autoLoadName}' is not available on the active SceneTree root.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试从当前 SceneTree 根节点解析 AutoLoad。
|
||||
/// </summary>
|
||||
/// <typeparam name="TNode">节点类型。</typeparam>
|
||||
/// <param name="autoLoadName">AutoLoad 名称。</param>
|
||||
/// <param name="value">解析到的节点实例。</param>
|
||||
/// <returns>若当前进程存在 SceneTree 且根节点中能解析到该 AutoLoad,则返回 <c>true</c>。</returns>
|
||||
private static bool TryGetNode<TNode>(string autoLoadName, out TNode? value)
|
||||
where TNode : global::Godot.Node
|
||||
{
|
||||
value = default;
|
||||
|
||||
if (global::Godot.Engine.GetMainLoop() is not global::Godot.SceneTree sceneTree)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var root = sceneTree.Root;
|
||||
if (root is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value = root.GetNodeOrNull<TNode>($"/root/{autoLoadName}");
|
||||
return value is not null;
|
||||
}
|
||||
}
|
||||
|
||||
""";
|
||||
|
||||
const string expectedInputActions = """
|
||||
// <auto-generated />
|
||||
#nullable enable
|
||||
|
||||
namespace GFramework.Godot.Generated;
|
||||
|
||||
/// <summary>
|
||||
/// 提供 project.godot 中 Input Action 名称的强类型常量。
|
||||
/// </summary>
|
||||
public static partial class InputActions
|
||||
{
|
||||
/// <summary>
|
||||
/// Input Action <c>move_up</c> 的稳定名称。
|
||||
/// </summary>
|
||||
public const string MoveUp = "move_up";
|
||||
|
||||
/// <summary>
|
||||
/// Input Action <c>ui_cancel</c> 的稳定名称。
|
||||
/// </summary>
|
||||
public const string UiCancel = "ui_cancel";
|
||||
|
||||
}
|
||||
|
||||
""";
|
||||
|
||||
var result = AdditionalTextGeneratorTestDriver.Run<GodotProjectMetadataGenerator>(
|
||||
source,
|
||||
("project.godot", projectFile));
|
||||
("project.godot", $"{AutoLoadProjectFile}\n\n{InputActionsProjectFile}"));
|
||||
|
||||
var generatedSources = AdditionalTextGeneratorTestDriver.ToGeneratedSourceMap(result);
|
||||
|
||||
Assert.That(result.Results.Single().Diagnostics, Is.Empty);
|
||||
Assert.That(
|
||||
generatedSources["GFramework_Godot_Generated_AutoLoads.g.cs"],
|
||||
Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(expectedAutoLoads)));
|
||||
Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(ExpectedAutoLoads)));
|
||||
Assert.That(
|
||||
generatedSources["GFramework_Godot_Generated_InputActions.g.cs"],
|
||||
Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(expectedInputActions)));
|
||||
Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(ExpectedInputActions)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -6,48 +6,52 @@ namespace GFramework.Godot.SourceGenerators.Tests.Registration;
|
||||
[TestFixture]
|
||||
public class AutoRegisterExportedCollectionsGeneratorTests
|
||||
{
|
||||
private const string StandardAttributeDeclarations = """
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
|
||||
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class RegisterExportedCollectionAttribute : Attribute
|
||||
{
|
||||
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
|
||||
}
|
||||
""";
|
||||
|
||||
private const string MultiDeclarationAttributeDeclarations = """
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
||||
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
|
||||
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class RegisterExportedCollectionAttribute : Attribute
|
||||
{
|
||||
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
|
||||
}
|
||||
""";
|
||||
|
||||
[Test]
|
||||
public async Task Generates_Batch_Registration_Method_For_Annotated_Collections()
|
||||
{
|
||||
const string source = """
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
||||
string source = CreateSource(
|
||||
"""
|
||||
public sealed class IntRegistry
|
||||
{
|
||||
public void Register(int value) { }
|
||||
}
|
||||
|
||||
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
|
||||
[AutoRegisterExportedCollections]
|
||||
public partial class Bootstrapper<TReference, TNotNull, TValue, TUnmanaged>
|
||||
where TReference : class?
|
||||
where TNotNull : notnull
|
||||
where TValue : struct
|
||||
where TUnmanaged : unmanaged
|
||||
{
|
||||
private readonly IntRegistry? _registry = new();
|
||||
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class RegisterExportedCollectionAttribute : Attribute
|
||||
{
|
||||
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
public sealed class IntRegistry
|
||||
{
|
||||
public void Register(int value) { }
|
||||
}
|
||||
|
||||
[AutoRegisterExportedCollections]
|
||||
public partial class Bootstrapper<TReference, TNotNull, TValue, TUnmanaged>
|
||||
where TReference : class?
|
||||
where TNotNull : notnull
|
||||
where TValue : struct
|
||||
where TUnmanaged : unmanaged
|
||||
{
|
||||
private readonly IntRegistry? _registry = new();
|
||||
|
||||
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
||||
public List<int>? Values { get; } = new();
|
||||
}
|
||||
}
|
||||
""";
|
||||
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
||||
public List<int>? Values { get; } = new();
|
||||
}
|
||||
""",
|
||||
nullableEnabled: true);
|
||||
|
||||
const string expected = """
|
||||
// <auto-generated />
|
||||
@ -77,7 +81,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests
|
||||
|
||||
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
|
||||
source,
|
||||
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
|
||||
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -131,47 +135,29 @@ public class AutoRegisterExportedCollectionsGeneratorTests
|
||||
.WithLocation(0)
|
||||
.WithArguments("Values"));
|
||||
|
||||
await test.RunAsync();
|
||||
await test.RunAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Generates_Batch_Registration_Method_When_Register_Method_Uses_Array_Parameter()
|
||||
{
|
||||
const string source = """
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
||||
string source = CreateSource(
|
||||
"""
|
||||
public sealed class ArrayRegistry
|
||||
{
|
||||
public void Register(int[] value) { }
|
||||
}
|
||||
|
||||
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
|
||||
[AutoRegisterExportedCollections]
|
||||
public partial class Bootstrapper
|
||||
{
|
||||
private readonly ArrayRegistry _registry = new();
|
||||
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class RegisterExportedCollectionAttribute : Attribute
|
||||
{
|
||||
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
public sealed class ArrayRegistry
|
||||
{
|
||||
public void Register(int[] value) { }
|
||||
}
|
||||
|
||||
[AutoRegisterExportedCollections]
|
||||
public partial class Bootstrapper
|
||||
{
|
||||
private readonly ArrayRegistry _registry = new();
|
||||
|
||||
[RegisterExportedCollection(nameof(_registry), nameof(ArrayRegistry.Register))]
|
||||
public List<int[]> Values { get; } = new();
|
||||
}
|
||||
}
|
||||
""";
|
||||
[RegisterExportedCollection(nameof(_registry), nameof(ArrayRegistry.Register))]
|
||||
public List<int[]> Values { get; } = new();
|
||||
}
|
||||
""",
|
||||
nullableEnabled: true);
|
||||
|
||||
const string expected = """
|
||||
// <auto-generated />
|
||||
@ -197,59 +183,41 @@ public class AutoRegisterExportedCollectionsGeneratorTests
|
||||
|
||||
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
|
||||
source,
|
||||
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
|
||||
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Inherited_Interface()
|
||||
{
|
||||
const string source = """
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
||||
string source = CreateSource(
|
||||
"""
|
||||
public interface IKeyValue<TKey, TValue>
|
||||
{
|
||||
}
|
||||
|
||||
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
|
||||
public interface IRegistry<TKey, TValue>
|
||||
{
|
||||
void Registry(IKeyValue<TKey, TValue> mapping);
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class RegisterExportedCollectionAttribute : Attribute
|
||||
{
|
||||
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
|
||||
}
|
||||
}
|
||||
public interface IAssetRegistry<TValue> : IRegistry<string, TValue>
|
||||
{
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
public interface IKeyValue<TKey, TValue>
|
||||
{
|
||||
}
|
||||
public sealed class IntConfig : IKeyValue<string, int>
|
||||
{
|
||||
}
|
||||
|
||||
public interface IRegistry<TKey, TValue>
|
||||
{
|
||||
void Registry(IKeyValue<TKey, TValue> mapping);
|
||||
}
|
||||
[AutoRegisterExportedCollections]
|
||||
public partial class Bootstrapper
|
||||
{
|
||||
private readonly IAssetRegistry<int>? _registry = null;
|
||||
|
||||
public interface IAssetRegistry<TValue> : IRegistry<string, TValue>
|
||||
{
|
||||
}
|
||||
|
||||
public sealed class IntConfig : IKeyValue<string, int>
|
||||
{
|
||||
}
|
||||
|
||||
[AutoRegisterExportedCollections]
|
||||
public partial class Bootstrapper
|
||||
{
|
||||
private readonly IAssetRegistry<int>? _registry = null;
|
||||
|
||||
[RegisterExportedCollection(nameof(_registry), "Registry")]
|
||||
public List<IntConfig>? Values { get; } = new();
|
||||
}
|
||||
}
|
||||
""";
|
||||
[RegisterExportedCollection(nameof(_registry), "Registry")]
|
||||
public List<IntConfig>? Values { get; } = new();
|
||||
}
|
||||
""",
|
||||
nullableEnabled: true);
|
||||
|
||||
const string expected = """
|
||||
// <auto-generated />
|
||||
@ -275,7 +243,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests
|
||||
|
||||
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
|
||||
source,
|
||||
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
|
||||
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -334,51 +302,33 @@ public class AutoRegisterExportedCollectionsGeneratorTests
|
||||
.WithLocation(0)
|
||||
.WithArguments("Register", "_registry", "Values"));
|
||||
|
||||
await test.RunAsync();
|
||||
await test.RunAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Base_Class()
|
||||
{
|
||||
const string source = """
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
||||
string source = CreateSource(
|
||||
"""
|
||||
public class BaseRegistry
|
||||
{
|
||||
public void Register(int value) { }
|
||||
}
|
||||
|
||||
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
|
||||
public sealed class DerivedRegistry : BaseRegistry
|
||||
{
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class RegisterExportedCollectionAttribute : Attribute
|
||||
{
|
||||
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
|
||||
}
|
||||
}
|
||||
[AutoRegisterExportedCollections]
|
||||
public partial class Bootstrapper
|
||||
{
|
||||
private readonly DerivedRegistry? _registry = new();
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
public class BaseRegistry
|
||||
{
|
||||
public void Register(int value) { }
|
||||
}
|
||||
|
||||
public sealed class DerivedRegistry : BaseRegistry
|
||||
{
|
||||
}
|
||||
|
||||
[AutoRegisterExportedCollections]
|
||||
public partial class Bootstrapper
|
||||
{
|
||||
private readonly DerivedRegistry? _registry = new();
|
||||
|
||||
[RegisterExportedCollection(nameof(_registry), nameof(BaseRegistry.Register))]
|
||||
public List<int>? Values { get; } = new();
|
||||
}
|
||||
}
|
||||
""";
|
||||
[RegisterExportedCollection(nameof(_registry), nameof(BaseRegistry.Register))]
|
||||
public List<int>? Values { get; } = new();
|
||||
}
|
||||
""",
|
||||
nullableEnabled: true);
|
||||
|
||||
const string expected = """
|
||||
// <auto-generated />
|
||||
@ -404,50 +354,32 @@ public class AutoRegisterExportedCollectionsGeneratorTests
|
||||
|
||||
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
|
||||
source,
|
||||
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
|
||||
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Generates_Batch_Registration_Method_When_Registry_Member_Comes_From_Base_Class()
|
||||
{
|
||||
const string source = """
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
||||
string source = CreateSource(
|
||||
"""
|
||||
public sealed class IntRegistry
|
||||
{
|
||||
public void Register(int value) { }
|
||||
}
|
||||
|
||||
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
|
||||
public abstract class BootstrapperBase
|
||||
{
|
||||
protected readonly IntRegistry? _registry = new();
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class RegisterExportedCollectionAttribute : Attribute
|
||||
{
|
||||
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
public sealed class IntRegistry
|
||||
{
|
||||
public void Register(int value) { }
|
||||
}
|
||||
|
||||
public abstract class BootstrapperBase
|
||||
{
|
||||
protected readonly IntRegistry? _registry = new();
|
||||
}
|
||||
|
||||
[AutoRegisterExportedCollections]
|
||||
public partial class Bootstrapper : BootstrapperBase
|
||||
{
|
||||
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
||||
public List<int>? Values { get; } = new();
|
||||
}
|
||||
}
|
||||
""";
|
||||
[AutoRegisterExportedCollections]
|
||||
public partial class Bootstrapper : BootstrapperBase
|
||||
{
|
||||
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
||||
public List<int>? Values { get; } = new();
|
||||
}
|
||||
""",
|
||||
nullableEnabled: true);
|
||||
|
||||
const string expected = """
|
||||
// <auto-generated />
|
||||
@ -473,74 +405,47 @@ public class AutoRegisterExportedCollectionsGeneratorTests
|
||||
|
||||
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
|
||||
source,
|
||||
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
|
||||
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Reports_Diagnostic_When_Collection_Member_Is_Not_Instance_Readable()
|
||||
{
|
||||
const string source = """
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
||||
string source = CreateSource(
|
||||
"""
|
||||
public sealed class IntRegistry
|
||||
{
|
||||
public void Register(int value) { }
|
||||
}
|
||||
|
||||
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
|
||||
[AutoRegisterExportedCollections]
|
||||
public partial class Bootstrapper
|
||||
{
|
||||
private readonly IntRegistry _registry = new();
|
||||
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class RegisterExportedCollectionAttribute : Attribute
|
||||
{
|
||||
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
|
||||
}
|
||||
}
|
||||
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
||||
public static List<int> {|#0:StaticValues|} = new();
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
public sealed class IntRegistry
|
||||
{
|
||||
public void Register(int value) { }
|
||||
}
|
||||
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
||||
public static List<int> {|#1:StaticPropertyValues|} { get; } = new();
|
||||
|
||||
[AutoRegisterExportedCollections]
|
||||
public partial class Bootstrapper
|
||||
{
|
||||
private readonly IntRegistry _registry = new();
|
||||
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
||||
public List<int> {|#2:WriteOnlyValues|} { set { } }
|
||||
}
|
||||
""");
|
||||
|
||||
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
||||
public static List<int> {|#0:StaticValues|} = new();
|
||||
|
||||
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
||||
public static List<int> {|#1:StaticPropertyValues|} { get; } = new();
|
||||
|
||||
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
||||
public List<int> {|#2:WriteOnlyValues|} { set { } }
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var test = new CSharpSourceGeneratorTest<AutoRegisterExportedCollectionsGenerator, DefaultVerifier>
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
Sources = { source }
|
||||
},
|
||||
DisabledDiagnostics = { "GF_Common_Trace_001" },
|
||||
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
|
||||
};
|
||||
|
||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
|
||||
.WithLocation(0)
|
||||
.WithArguments("StaticValues"));
|
||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
|
||||
.WithLocation(1)
|
||||
.WithArguments("StaticPropertyValues"));
|
||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
|
||||
.WithLocation(2)
|
||||
.WithArguments("WriteOnlyValues"));
|
||||
|
||||
await test.RunAsync();
|
||||
await VerifyDiagnosticsAsync(
|
||||
source,
|
||||
skipGeneratedSourcesCheck: true,
|
||||
new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
|
||||
.WithLocation(0)
|
||||
.WithArguments("StaticValues"),
|
||||
new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
|
||||
.WithLocation(1)
|
||||
.WithArguments("StaticPropertyValues"),
|
||||
new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
|
||||
.WithLocation(2)
|
||||
.WithArguments("WriteOnlyValues")).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -595,7 +500,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests
|
||||
.WithLocation(0)
|
||||
.WithArguments("_registry", "Values"));
|
||||
|
||||
await test.RunAsync();
|
||||
await test.RunAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -650,7 +555,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests
|
||||
.WithLocation(0)
|
||||
.WithArguments("Register", "_registry", "Values"));
|
||||
|
||||
await test.RunAsync();
|
||||
await test.RunAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -705,51 +610,34 @@ public class AutoRegisterExportedCollectionsGeneratorTests
|
||||
.WithLocation(0)
|
||||
.WithArguments("Values"));
|
||||
|
||||
await test.RunAsync();
|
||||
await test.RunAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Generates_Only_One_Source_When_Multiple_Partial_Declarations_Are_Annotated()
|
||||
{
|
||||
const string source = """
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
||||
string source = CreateSource(
|
||||
"""
|
||||
public sealed class IntRegistry
|
||||
{
|
||||
public void Register(int value) { }
|
||||
}
|
||||
|
||||
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
||||
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
|
||||
[AutoRegisterExportedCollections]
|
||||
public partial class Bootstrapper
|
||||
{
|
||||
private readonly IntRegistry? _registry = new();
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class RegisterExportedCollectionAttribute : Attribute
|
||||
{
|
||||
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
public sealed class IntRegistry
|
||||
{
|
||||
public void Register(int value) { }
|
||||
}
|
||||
|
||||
[AutoRegisterExportedCollections]
|
||||
public partial class Bootstrapper
|
||||
{
|
||||
private readonly IntRegistry? _registry = new();
|
||||
}
|
||||
|
||||
[AutoRegisterExportedCollections]
|
||||
public partial class Bootstrapper
|
||||
{
|
||||
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
||||
public List<int>? Values { get; } = new();
|
||||
}
|
||||
}
|
||||
""";
|
||||
[AutoRegisterExportedCollections]
|
||||
public partial class Bootstrapper
|
||||
{
|
||||
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
|
||||
public List<int>? Values { get; } = new();
|
||||
}
|
||||
""",
|
||||
nullableEnabled: true,
|
||||
allowMultipleDeclarations: true);
|
||||
|
||||
const string expected = """
|
||||
// <auto-generated />
|
||||
@ -775,6 +663,61 @@ public class AutoRegisterExportedCollectionsGeneratorTests
|
||||
|
||||
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
|
||||
source,
|
||||
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
|
||||
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static string CreateSource(
|
||||
string applicationSource,
|
||||
bool nullableEnabled = false,
|
||||
bool allowMultipleDeclarations = false)
|
||||
{
|
||||
string nullableDirective = nullableEnabled ? "#nullable enable\n" : string.Empty;
|
||||
string attributeDeclarations = allowMultipleDeclarations
|
||||
? MultiDeclarationAttributeDeclarations
|
||||
: StandardAttributeDeclarations;
|
||||
|
||||
return $$"""
|
||||
{{nullableDirective}}using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions.UI;
|
||||
|
||||
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
|
||||
{
|
||||
{{attributeDeclarations}}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
{{applicationSource}}
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
private static 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,19 +72,8 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
|
||||
if (bindNodeSignalAttribute is null || godotNodeSymbol is null)
|
||||
return;
|
||||
|
||||
// 缓存每个方法上已解析的特性,避免在筛选和生成阶段重复做语义查询。
|
||||
var methodAttributes = candidates
|
||||
.Where(static candidate => candidate is not null)
|
||||
.Select(static candidate => candidate!)
|
||||
.ToDictionary(
|
||||
static candidate => candidate,
|
||||
candidate => ResolveAttributes(candidate.MethodSymbol, bindNodeSignalAttribute),
|
||||
ReferenceEqualityComparer.Instance);
|
||||
|
||||
var methodCandidates = methodAttributes
|
||||
.Where(static pair => pair.Value.Count > 0)
|
||||
.Select(static pair => pair.Key)
|
||||
.ToList();
|
||||
var methodAttributes = BuildMethodAttributeMap(candidates, bindNodeSignalAttribute);
|
||||
var methodCandidates = CollectMethodCandidates(methodAttributes);
|
||||
|
||||
foreach (var group in GroupByContainingType(methodCandidates))
|
||||
{
|
||||
@ -99,19 +88,7 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
|
||||
UnbindMethodName))
|
||||
continue;
|
||||
|
||||
var bindings = new List<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);
|
||||
}
|
||||
}
|
||||
|
||||
var bindings = CollectBindings(context, group, methodAttributes, godotNodeSymbol);
|
||||
if (bindings.Count == 0)
|
||||
continue;
|
||||
|
||||
@ -171,99 +148,22 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
|
||||
|
||||
if (candidate.MethodSymbol.IsStatic)
|
||||
{
|
||||
ReportMethodDiagnostic(
|
||||
context,
|
||||
BindNodeSignalDiagnostics.StaticMethodNotSupported,
|
||||
candidate,
|
||||
attribute,
|
||||
candidate.MethodSymbol.Name);
|
||||
ReportStaticMethodDiagnostic(context, candidate, attribute);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryResolveCtorString(attribute, 0, out var nodeFieldName))
|
||||
{
|
||||
ReportMethodDiagnostic(
|
||||
context,
|
||||
BindNodeSignalDiagnostics.InvalidConstructorArgument,
|
||||
candidate,
|
||||
attribute,
|
||||
candidate.MethodSymbol.Name,
|
||||
"nodeFieldName");
|
||||
if (!TryResolveBindingTargetNames(context, candidate, attribute, out var nodeFieldName, out var signalName))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryResolveCtorString(attribute, 1, out var signalName))
|
||||
{
|
||||
ReportMethodDiagnostic(
|
||||
context,
|
||||
BindNodeSignalDiagnostics.InvalidConstructorArgument,
|
||||
candidate,
|
||||
attribute,
|
||||
candidate.MethodSymbol.Name,
|
||||
"signalName");
|
||||
if (!TryFindCompatibleField(context, candidate, attribute, godotNodeSymbol, nodeFieldName, out var fieldSymbol))
|
||||
return false;
|
||||
}
|
||||
|
||||
var fieldSymbol = FindField(candidate.MethodSymbol.ContainingType, nodeFieldName);
|
||||
if (fieldSymbol is null)
|
||||
{
|
||||
ReportMethodDiagnostic(
|
||||
context,
|
||||
BindNodeSignalDiagnostics.NodeFieldNotFound,
|
||||
candidate,
|
||||
attribute,
|
||||
candidate.MethodSymbol.Name,
|
||||
nodeFieldName,
|
||||
candidate.MethodSymbol.ContainingType.Name);
|
||||
if (!TryFindCompatibleEvent(context, candidate, attribute, fieldSymbol, signalName, out var eventSymbol))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fieldSymbol.IsStatic)
|
||||
{
|
||||
ReportMethodDiagnostic(
|
||||
context,
|
||||
BindNodeSignalDiagnostics.NodeFieldMustBeInstanceField,
|
||||
candidate,
|
||||
attribute,
|
||||
candidate.MethodSymbol.Name,
|
||||
fieldSymbol.Name);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!fieldSymbol.Type.IsAssignableTo(godotNodeSymbol))
|
||||
{
|
||||
ReportMethodDiagnostic(
|
||||
context,
|
||||
BindNodeSignalDiagnostics.FieldTypeMustDeriveFromNode,
|
||||
candidate,
|
||||
attribute,
|
||||
fieldSymbol.Name);
|
||||
return false;
|
||||
}
|
||||
|
||||
var eventSymbol = FindEvent(fieldSymbol.Type, signalName);
|
||||
if (eventSymbol is null)
|
||||
{
|
||||
ReportMethodDiagnostic(
|
||||
context,
|
||||
BindNodeSignalDiagnostics.SignalNotFound,
|
||||
candidate,
|
||||
attribute,
|
||||
fieldSymbol.Name,
|
||||
signalName);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsMethodCompatibleWithEvent(candidate.MethodSymbol, eventSymbol))
|
||||
{
|
||||
ReportMethodDiagnostic(
|
||||
context,
|
||||
BindNodeSignalDiagnostics.MethodSignatureNotCompatible,
|
||||
candidate,
|
||||
attribute,
|
||||
candidate.MethodSymbol.Name,
|
||||
eventSymbol.Name,
|
||||
fieldSymbol.Name);
|
||||
ReportIncompatibleSignatureDiagnostic(context, candidate, attribute, eventSymbol, fieldSymbol);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -271,6 +171,235 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Dictionary<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(
|
||||
context,
|
||||
BindNodeSignalDiagnostics.StaticMethodNotSupported,
|
||||
candidate,
|
||||
attribute,
|
||||
candidate.MethodSymbol.Name);
|
||||
}
|
||||
|
||||
private static bool TryResolveBindingTargetNames(
|
||||
SourceProductionContext context,
|
||||
MethodCandidate candidate,
|
||||
AttributeData attribute,
|
||||
out string nodeFieldName,
|
||||
out string signalName)
|
||||
{
|
||||
nodeFieldName = string.Empty;
|
||||
signalName = string.Empty;
|
||||
|
||||
if (!TryResolveCtorString(attribute, 0, out nodeFieldName))
|
||||
{
|
||||
ReportInvalidConstructorArgumentDiagnostic(context, candidate, attribute, "nodeFieldName");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryResolveCtorString(attribute, 1, out signalName))
|
||||
{
|
||||
ReportInvalidConstructorArgumentDiagnostic(context, candidate, attribute, "signalName");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void ReportInvalidConstructorArgumentDiagnostic(
|
||||
SourceProductionContext context,
|
||||
MethodCandidate candidate,
|
||||
AttributeData attribute,
|
||||
string argumentName)
|
||||
{
|
||||
ReportMethodDiagnostic(
|
||||
context,
|
||||
BindNodeSignalDiagnostics.InvalidConstructorArgument,
|
||||
candidate,
|
||||
attribute,
|
||||
candidate.MethodSymbol.Name,
|
||||
argumentName);
|
||||
}
|
||||
|
||||
private static bool TryFindCompatibleField(
|
||||
SourceProductionContext context,
|
||||
MethodCandidate candidate,
|
||||
AttributeData attribute,
|
||||
INamedTypeSymbol godotNodeSymbol,
|
||||
string nodeFieldName,
|
||||
out IFieldSymbol fieldSymbol)
|
||||
{
|
||||
fieldSymbol = null!;
|
||||
|
||||
var resolvedField = FindField(candidate.MethodSymbol.ContainingType, nodeFieldName);
|
||||
if (resolvedField is null)
|
||||
{
|
||||
ReportNodeFieldNotFoundDiagnostic(context, candidate, attribute, nodeFieldName);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (resolvedField.IsStatic)
|
||||
{
|
||||
ReportNodeFieldMustBeInstanceDiagnostic(context, candidate, attribute, resolvedField);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!resolvedField.Type.IsAssignableTo(godotNodeSymbol))
|
||||
{
|
||||
ReportFieldTypeMustDeriveFromNodeDiagnostic(context, candidate, attribute, resolvedField);
|
||||
return false;
|
||||
}
|
||||
|
||||
fieldSymbol = resolvedField;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void ReportNodeFieldNotFoundDiagnostic(
|
||||
SourceProductionContext context,
|
||||
MethodCandidate candidate,
|
||||
AttributeData attribute,
|
||||
string nodeFieldName)
|
||||
{
|
||||
ReportMethodDiagnostic(
|
||||
context,
|
||||
BindNodeSignalDiagnostics.NodeFieldNotFound,
|
||||
candidate,
|
||||
attribute,
|
||||
candidate.MethodSymbol.Name,
|
||||
nodeFieldName,
|
||||
candidate.MethodSymbol.ContainingType.Name);
|
||||
}
|
||||
|
||||
private static void ReportNodeFieldMustBeInstanceDiagnostic(
|
||||
SourceProductionContext context,
|
||||
MethodCandidate candidate,
|
||||
AttributeData attribute,
|
||||
IFieldSymbol fieldSymbol)
|
||||
{
|
||||
ReportMethodDiagnostic(
|
||||
context,
|
||||
BindNodeSignalDiagnostics.NodeFieldMustBeInstanceField,
|
||||
candidate,
|
||||
attribute,
|
||||
candidate.MethodSymbol.Name,
|
||||
fieldSymbol.Name);
|
||||
}
|
||||
|
||||
private static void ReportFieldTypeMustDeriveFromNodeDiagnostic(
|
||||
SourceProductionContext context,
|
||||
MethodCandidate candidate,
|
||||
AttributeData attribute,
|
||||
IFieldSymbol fieldSymbol)
|
||||
{
|
||||
ReportMethodDiagnostic(
|
||||
context,
|
||||
BindNodeSignalDiagnostics.FieldTypeMustDeriveFromNode,
|
||||
candidate,
|
||||
attribute,
|
||||
fieldSymbol.Name);
|
||||
}
|
||||
|
||||
private static bool TryFindCompatibleEvent(
|
||||
SourceProductionContext context,
|
||||
MethodCandidate candidate,
|
||||
AttributeData attribute,
|
||||
IFieldSymbol fieldSymbol,
|
||||
string signalName,
|
||||
out IEventSymbol eventSymbol)
|
||||
{
|
||||
eventSymbol = null!;
|
||||
|
||||
var resolvedEvent = FindEvent(fieldSymbol.Type, signalName);
|
||||
if (resolvedEvent is null)
|
||||
{
|
||||
ReportSignalNotFoundDiagnostic(context, candidate, attribute, fieldSymbol, signalName);
|
||||
return false;
|
||||
}
|
||||
|
||||
eventSymbol = resolvedEvent;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void ReportSignalNotFoundDiagnostic(
|
||||
SourceProductionContext context,
|
||||
MethodCandidate candidate,
|
||||
AttributeData attribute,
|
||||
IFieldSymbol fieldSymbol,
|
||||
string signalName)
|
||||
{
|
||||
ReportMethodDiagnostic(
|
||||
context,
|
||||
BindNodeSignalDiagnostics.SignalNotFound,
|
||||
candidate,
|
||||
attribute,
|
||||
fieldSymbol.Name,
|
||||
signalName);
|
||||
}
|
||||
|
||||
private static void ReportIncompatibleSignatureDiagnostic(
|
||||
SourceProductionContext context,
|
||||
MethodCandidate candidate,
|
||||
AttributeData attribute,
|
||||
IEventSymbol eventSymbol,
|
||||
IFieldSymbol fieldSymbol)
|
||||
{
|
||||
ReportMethodDiagnostic(
|
||||
context,
|
||||
BindNodeSignalDiagnostics.MethodSignatureNotCompatible,
|
||||
candidate,
|
||||
attribute,
|
||||
candidate.MethodSymbol.Name,
|
||||
eventSymbol.Name,
|
||||
fieldSymbol.Name);
|
||||
}
|
||||
|
||||
private static void ReportMethodDiagnostic(
|
||||
SourceProductionContext context,
|
||||
DiagnosticDescriptor descriptor,
|
||||
@ -404,11 +533,7 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
|
||||
{
|
||||
return typeSymbol.GetMembers()
|
||||
.OfType<IMethodSymbol>()
|
||||
.FirstOrDefault(method =>
|
||||
method.Name == methodName &&
|
||||
!method.IsStatic &&
|
||||
method.Parameters.Length == 0 &&
|
||||
method.MethodKind == MethodKind.Ordinary);
|
||||
.FirstOrDefault(method => IsParameterlessInstanceMethod(method, methodName));
|
||||
}
|
||||
|
||||
private static bool CallsGeneratedMethod(
|
||||
@ -447,6 +572,14 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsParameterlessInstanceMethod(IMethodSymbol method, string methodName)
|
||||
{
|
||||
return string.Equals(method.Name, methodName, StringComparison.Ordinal) &&
|
||||
!method.IsStatic &&
|
||||
method.Parameters.Length == 0 &&
|
||||
method.MethodKind == MethodKind.Ordinary;
|
||||
}
|
||||
|
||||
private static bool IsBindNodeSignalAttributeName(NameSyntax attributeName)
|
||||
{
|
||||
var simpleName = GetAttributeSimpleName(attributeName);
|
||||
@ -608,4 +741,4 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
|
||||
return RuntimeHelpers.GetHashCode(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -259,11 +259,7 @@ public sealed class GetNodeGenerator : IIncrementalGenerator
|
||||
{
|
||||
return typeSymbol.GetMembers()
|
||||
.OfType<IMethodSymbol>()
|
||||
.FirstOrDefault(static method =>
|
||||
method.Name == "_Ready" &&
|
||||
!method.IsStatic &&
|
||||
method.Parameters.Length == 0 &&
|
||||
method.MethodKind == MethodKind.Ordinary);
|
||||
.FirstOrDefault(static method => IsParameterlessInstanceMethod(method, "_Ready"));
|
||||
}
|
||||
|
||||
private static bool CallsGeneratedInjection(IMethodSymbol readyMethod)
|
||||
@ -306,6 +302,14 @@ public sealed class GetNodeGenerator : IIncrementalGenerator
|
||||
return attribute.GetNamedArgument("Required", true);
|
||||
}
|
||||
|
||||
private static bool IsParameterlessInstanceMethod(IMethodSymbol method, string methodName)
|
||||
{
|
||||
return string.Equals(method.Name, methodName, StringComparison.Ordinal) &&
|
||||
!method.IsStatic &&
|
||||
method.Parameters.Length == 0 &&
|
||||
method.MethodKind == MethodKind.Ordinary;
|
||||
}
|
||||
|
||||
private static bool TryResolvePath(
|
||||
IFieldSymbol fieldSymbol,
|
||||
AttributeData attribute,
|
||||
@ -373,7 +377,10 @@ public sealed class GetNodeGenerator : IIncrementalGenerator
|
||||
if (!string.Equals(namedArgument.Key, "Lookup", StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
if (namedArgument.Value.Type?.ToDisplayString() != GetNodeLookupModeMetadataName)
|
||||
if (!string.Equals(
|
||||
namedArgument.Value.Type?.ToDisplayString(),
|
||||
GetNodeLookupModeMetadataName,
|
||||
StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
if (namedArgument.Value.Value is int value)
|
||||
@ -568,4 +575,4 @@ public sealed class GetNodeGenerator : IIncrementalGenerator
|
||||
|
||||
public List<FieldCandidate> Fields { get; } = new();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -126,7 +126,27 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator
|
||||
|
||||
var explicitMappings = 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)
|
||||
{
|
||||
var typeSymbol = candidate.TypeSymbol;
|
||||
@ -176,7 +196,14 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator
|
||||
|
||||
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);
|
||||
|
||||
foreach (var projectAutoLoadName in projectAutoLoadNames.OrderBy(static name => name, StringComparer.Ordinal))
|
||||
@ -408,24 +435,40 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator
|
||||
|
||||
foreach (var member in members)
|
||||
{
|
||||
builder.AppendLine(" /// <summary>");
|
||||
builder.AppendLine($" /// 获取 AutoLoad <c>{member.AutoLoadName}</c>。");
|
||||
builder.AppendLine(" /// </summary>");
|
||||
builder.AppendLine(
|
||||
$" public static {member.TypeName} {member.Identifier} => GetRequiredNode<{member.TypeName}>({SymbolDisplay.FormatLiteral(member.AutoLoadName, true)});");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine(" /// <summary>");
|
||||
builder.AppendLine($" /// 尝试获取 AutoLoad <c>{member.AutoLoadName}</c>。");
|
||||
builder.AppendLine(" /// </summary>");
|
||||
builder.AppendLine(
|
||||
$" public static bool TryGet{member.Identifier}(out {member.TypeName}? value)");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine(
|
||||
$" return TryGetNode({SymbolDisplay.FormatLiteral(member.AutoLoadName, true)}, out value);");
|
||||
builder.AppendLine(" }");
|
||||
builder.AppendLine();
|
||||
AppendAutoLoadMemberSource(builder, member);
|
||||
}
|
||||
|
||||
AppendGetRequiredNodeSource(builder);
|
||||
AppendTryGetNodeSource(builder);
|
||||
builder.AppendLine("}");
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static void AppendAutoLoadMemberSource(
|
||||
StringBuilder builder,
|
||||
GeneratedAutoLoadMember member)
|
||||
{
|
||||
builder.AppendLine(" /// <summary>");
|
||||
builder.AppendLine($" /// 获取 AutoLoad <c>{member.AutoLoadName}</c>。");
|
||||
builder.AppendLine(" /// </summary>");
|
||||
builder.AppendLine(
|
||||
$" public static {member.TypeName} {member.Identifier} => GetRequiredNode<{member.TypeName}>({SymbolDisplay.FormatLiteral(member.AutoLoadName, true)});");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine(" /// <summary>");
|
||||
builder.AppendLine($" /// 尝试获取 AutoLoad <c>{member.AutoLoadName}</c>。");
|
||||
builder.AppendLine(" /// </summary>");
|
||||
builder.AppendLine(
|
||||
$" public static bool TryGet{member.Identifier}(out {member.TypeName}? value)");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine(
|
||||
$" return TryGetNode({SymbolDisplay.FormatLiteral(member.AutoLoadName, true)}, out value);");
|
||||
builder.AppendLine(" }");
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
private static void AppendGetRequiredNodeSource(StringBuilder builder)
|
||||
{
|
||||
builder.AppendLine(" /// <summary>");
|
||||
builder.AppendLine(" /// 获取一个必填的 AutoLoad 节点;缺失时抛出异常。");
|
||||
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.\");");
|
||||
builder.AppendLine(" }");
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
private static void AppendTryGetNodeSource(StringBuilder builder)
|
||||
{
|
||||
builder.AppendLine(" /// <summary>");
|
||||
builder.AppendLine(" /// 尝试从当前 SceneTree 根节点解析 AutoLoad。");
|
||||
builder.AppendLine(" /// </summary>");
|
||||
@ -470,9 +517,6 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator
|
||||
builder.AppendLine(" value = root.GetNodeOrNull<TNode>($\"/root/{autoLoadName}\");");
|
||||
builder.AppendLine(" return value is not null;");
|
||||
builder.AppendLine(" }");
|
||||
builder.AppendLine("}");
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string GenerateInputActionsSource(IReadOnlyList<GeneratedInputActionMember> members)
|
||||
@ -530,45 +574,16 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator
|
||||
if (string.IsNullOrWhiteSpace(content) || content.StartsWith(";", StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
if (content.StartsWith("[", StringComparison.Ordinal) && content.EndsWith("]", StringComparison.Ordinal))
|
||||
{
|
||||
currentSection = content.Substring(1, content.Length - 2).Trim();
|
||||
if (TryUpdateSection(content, ref currentSection))
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryParseAssignment(content, out var key, out var value))
|
||||
continue;
|
||||
|
||||
if (string.Equals(currentSection, "autoload", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!seenAutoLoads.Add(key))
|
||||
{
|
||||
diagnostics.Add(Diagnostic.Create(
|
||||
GodotProjectDiagnostics.DuplicateAutoLoadEntry,
|
||||
CreateFileLocation(file.Path),
|
||||
key));
|
||||
continue;
|
||||
}
|
||||
|
||||
autoLoads.Add(new ProjectAutoLoadEntry(
|
||||
key,
|
||||
NormalizeProjectPath(value)));
|
||||
if (TryCollectAutoLoadEntry(file, currentSection, key, value, seenAutoLoads, autoLoads, diagnostics))
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.Equals(currentSection, "input", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!seenInputActions.Add(key))
|
||||
{
|
||||
diagnostics.Add(Diagnostic.Create(
|
||||
GodotProjectDiagnostics.DuplicateInputActionEntry,
|
||||
CreateFileLocation(file.Path),
|
||||
key));
|
||||
continue;
|
||||
}
|
||||
|
||||
inputActions.Add(key);
|
||||
}
|
||||
TryCollectInputAction(currentSection, key, seenInputActions, inputActions, diagnostics, file.Path);
|
||||
}
|
||||
|
||||
return new ProjectMetadataParseResult(
|
||||
@ -578,6 +593,68 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator
|
||||
diagnostics.ToImmutableArray());
|
||||
}
|
||||
|
||||
private static bool TryUpdateSection(string content, ref string currentSection)
|
||||
{
|
||||
if (!content.StartsWith("[", StringComparison.Ordinal) ||
|
||||
!content.EndsWith("]", StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
currentSection = content.Substring(1, content.Length - 2).Trim();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryCollectAutoLoadEntry(
|
||||
AdditionalText file,
|
||||
string currentSection,
|
||||
string key,
|
||||
string value,
|
||||
ISet<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)
|
||||
{
|
||||
var trimmed = rawValue.Trim();
|
||||
|
||||
@ -190,6 +190,48 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
|
||||
{
|
||||
registration = null!;
|
||||
|
||||
if (!TryResolveCollectionType(context, collectionMember, enumerableType, out var collectionType))
|
||||
return false;
|
||||
|
||||
if (!TryResolveRegistryTarget(
|
||||
context,
|
||||
compilation,
|
||||
ownerType,
|
||||
collectionMember,
|
||||
attribute,
|
||||
out var registryMemberName,
|
||||
out var registerMethodName,
|
||||
out var registryType))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryResolveElementType(context, collectionMember, collectionType, out var elementType))
|
||||
return false;
|
||||
|
||||
if (!HasCompatibleRegisterMethod(compilation, ownerType, registryType, registerMethodName, elementType))
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(
|
||||
AutoRegisterExportedCollectionsDiagnostics.RegisterMethodNotFound,
|
||||
collectionMember.Locations.FirstOrDefault() ?? Location.None,
|
||||
registerMethodName,
|
||||
registryMemberName,
|
||||
collectionMember.Name));
|
||||
return false;
|
||||
}
|
||||
|
||||
registration = new RegistrationSpec(collectionMember.Name, registryMemberName, registerMethodName);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryResolveCollectionType(
|
||||
SourceProductionContext context,
|
||||
ISymbol collectionMember,
|
||||
INamedTypeSymbol enumerableType,
|
||||
out ITypeSymbol collectionType)
|
||||
{
|
||||
collectionType = null!;
|
||||
|
||||
if (!IsInstanceReadableMember(collectionMember))
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(
|
||||
@ -199,17 +241,11 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
|
||||
return false;
|
||||
}
|
||||
|
||||
var collectionType = collectionMember switch
|
||||
{
|
||||
IFieldSymbol field => field.Type,
|
||||
IPropertySymbol property => property.Type,
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (collectionType is null)
|
||||
var resolvedType = GetMemberType(collectionMember);
|
||||
if (resolvedType is null)
|
||||
return false;
|
||||
|
||||
if (!collectionType.IsAssignableTo(enumerableType))
|
||||
if (!resolvedType.IsAssignableTo(enumerableType))
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(
|
||||
AutoRegisterExportedCollectionsDiagnostics.CollectionTypeMustBeEnumerable,
|
||||
@ -218,12 +254,35 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryGetRegistrationAttributeArguments(context, collectionMember, attribute, out var registryMemberName,
|
||||
out var registerMethodName))
|
||||
collectionType = resolvedType;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryResolveRegistryTarget(
|
||||
SourceProductionContext context,
|
||||
Compilation compilation,
|
||||
INamedTypeSymbol ownerType,
|
||||
ISymbol collectionMember,
|
||||
AttributeData attribute,
|
||||
out string registryMemberName,
|
||||
out string registerMethodName,
|
||||
out INamedTypeSymbol registryType)
|
||||
{
|
||||
registryMemberName = string.Empty;
|
||||
registerMethodName = string.Empty;
|
||||
registryType = null!;
|
||||
|
||||
if (!TryGetRegistrationAttributeArguments(
|
||||
context,
|
||||
collectionMember,
|
||||
attribute,
|
||||
out registryMemberName,
|
||||
out registerMethodName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var registryMember = FindRegistryMember(ownerType, registryMemberName);
|
||||
|
||||
if (registryMember is null)
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(
|
||||
@ -246,18 +305,24 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
|
||||
return false;
|
||||
}
|
||||
|
||||
var registryType = registryMember switch
|
||||
{
|
||||
IFieldSymbol field => field.Type as INamedTypeSymbol,
|
||||
IPropertySymbol property => property.Type as INamedTypeSymbol,
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (registryType is null)
|
||||
var resolvedRegistryType = GetMemberType(registryMember) as INamedTypeSymbol;
|
||||
if (resolvedRegistryType is null)
|
||||
return false;
|
||||
|
||||
var elementType = TryGetElementType(collectionType);
|
||||
if (elementType is null)
|
||||
registryType = resolvedRegistryType;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryResolveElementType(
|
||||
SourceProductionContext context,
|
||||
ISymbol collectionMember,
|
||||
ITypeSymbol collectionType,
|
||||
out ITypeSymbol elementType)
|
||||
{
|
||||
elementType = null!;
|
||||
|
||||
var resolvedElementType = TryGetElementType(collectionType);
|
||||
if (resolvedElementType is null)
|
||||
{
|
||||
// Non-generic IEnumerable exposes elements as object at compile time, which is not safe
|
||||
// for validating or generating a strongly typed registry call.
|
||||
@ -268,26 +333,33 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
|
||||
return false;
|
||||
}
|
||||
|
||||
var hasCompatibleMethod = EnumerateCandidateMethods(registryType, registerMethodName)
|
||||
elementType = resolvedElementType;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool HasCompatibleRegisterMethod(
|
||||
Compilation compilation,
|
||||
INamedTypeSymbol ownerType,
|
||||
INamedTypeSymbol registryType,
|
||||
string registerMethodName,
|
||||
ITypeSymbol elementType)
|
||||
{
|
||||
return EnumerateCandidateMethods(registryType, registerMethodName)
|
||||
.Any(method =>
|
||||
!method.IsStatic &&
|
||||
method.Parameters.Length == 1 &&
|
||||
compilation.IsSymbolAccessibleWithin(method, ownerType) &&
|
||||
CanAcceptElementType(compilation, elementType, method.Parameters[0].Type));
|
||||
}
|
||||
|
||||
if (!hasCompatibleMethod)
|
||||
private static ITypeSymbol? GetMemberType(ISymbol member)
|
||||
{
|
||||
return member switch
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(
|
||||
AutoRegisterExportedCollectionsDiagnostics.RegisterMethodNotFound,
|
||||
collectionMember.Locations.FirstOrDefault() ?? Location.None,
|
||||
registerMethodName,
|
||||
registryMemberName,
|
||||
collectionMember.Name));
|
||||
return false;
|
||||
}
|
||||
|
||||
registration = new RegistrationSpec(collectionMember.Name, registryMemberName, registerMethodName);
|
||||
return true;
|
||||
IFieldSymbol field => field.Type,
|
||||
IPropertySymbol property => property.Type,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsInstanceReadableMember(ISymbol member)
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<WarningLevel>0</WarningLevel>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -11,6 +11,9 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace GFramework.Godot.Setting.Data;
|
||||
|
||||
/// <summary>
|
||||
@ -20,24 +23,47 @@ public class LocalizationMap
|
||||
{
|
||||
private const string DefaultFrameworkLanguage = "eng";
|
||||
private const string DefaultGodotLocale = "en";
|
||||
private readonly Dictionary<string, string> _frameworkLanguageMap;
|
||||
private readonly Dictionary<string, string> _languageMap;
|
||||
|
||||
/// <summary>
|
||||
/// 用户语言 -> Godot locale 映射表。
|
||||
/// 使用默认的 Godot locale 与框架语言码映射初始化本地化设置。
|
||||
/// </summary>
|
||||
public Dictionary<string, string> LanguageMap { get; set; } = new(StringComparer.Ordinal)
|
||||
public LocalizationMap()
|
||||
: this(CreateDefaultLanguageMap(), CreateDefaultFrameworkLanguageMap())
|
||||
{
|
||||
{ "简体中文", "zh_CN" },
|
||||
{ "English", "en" }
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户语言 -> GFramework 本地化语言码映射表。
|
||||
/// 使用外部提供的映射初始化本地化设置。
|
||||
/// 构造函数会复制输入字典,避免调用方在实例创建后继续修改内部状态。
|
||||
/// </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" },
|
||||
{ "English", "eng" }
|
||||
};
|
||||
ArgumentNullException.ThrowIfNull(languageMap);
|
||||
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>
|
||||
/// 解析用户保存的语言值对应的 Godot locale。
|
||||
@ -68,4 +94,22 @@ public class LocalizationMap
|
||||
|
||||
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" }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -8,7 +8,7 @@
|
||||
<Copyright>Copyright © 2025</Copyright>
|
||||
<RepositoryUrl>https://github.com/GeWuYou/GFramework</RepositoryUrl>
|
||||
<PackageProjectUrl>https://github.com/GeWuYou/GFramework</PackageProjectUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
|
||||
<PackageTags>game;framework</PackageTags>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
@ -16,6 +16,7 @@
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
|
||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
||||
<!-- This package is a pure meta-package that only aggregates dependencies. -->
|
||||
<NoPackageAnalysis>false</NoPackageAnalysis>
|
||||
</PropertyGroup>
|
||||
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -2,59 +2,74 @@
|
||||
|
||||
## 目标
|
||||
|
||||
继续以“优先低风险、保持行为兼容”为原则收敛当前仓库的 Meziantou analyzer warnings,并确保 active recovery 入口保持精简、可恢复。
|
||||
继续以“直接看构建输出、直接修构建 warning”为原则推进当前分支,并保持 active recovery 文档只保留当前真值。
|
||||
|
||||
## 当前恢复点
|
||||
|
||||
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-042`
|
||||
- 当前阶段:`Phase 42`
|
||||
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-052`
|
||||
- 当前阶段:`Phase 52`
|
||||
- 当前焦点:
|
||||
- 已于 `2026-04-24` 使用 `gframework-pr-review` 复核当前分支 PR #280,latest-head review 仍有 `3` 条 open threads
|
||||
- 本地确认这 `3` 条 open threads 均指向 `ai-plan` 文档:错误归档链接、`rp002-rp041` trace 混入 `RP-001` 段落,以及 active trace 的恢复信息失真
|
||||
- 本轮按最小写集直接修正文档恢复入口,不再扩大 `GFramework.SourceGenerators.Tests` 的代码写集
|
||||
- `RP-041` 验证完成时,分支相对 `origin/main` 的唯一变更文件数为 `4`;这说明继续只处理同一热点文件时,该指标增长会很慢
|
||||
- `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 噪音,而不是本地代码行为回归
|
||||
- `2026-04-24` 本轮从当前 PR review 的未解决线程回切到 `GFramework.Game` / `GFramework.Godot.SourceGenerators.Tests`
|
||||
- `UnifiedSettingsFile.Sections` 与 `CloneFile` fallback 已对齐为“可保留原 comparer 时保留,否则显式回退到 `StringComparer.Ordinal`”的文档与实现契约
|
||||
- `AutoRegisterExportedCollectionsGeneratorTests` 中剩余的 `await test.RunAsync();` 已统一补齐 `.ConfigureAwait(false)`,并同步让 `VerifyDiagnosticsAsync` 内部消费异步等待
|
||||
- 当前批次仍需避免混入与 analyzer-warning-reduction 无关的既有工作树改动
|
||||
|
||||
## 当前活跃事实
|
||||
|
||||
- 当前主题仍保持 active,因为 `GFramework.SourceGenerators.Tests` 尚有剩余 `MA0051` warning 需要决定是否继续推进
|
||||
- 继续按“单文件单方法”节奏处理 `CqrsHandlerRegistryGeneratorTests.cs` 可以稳定消除 warning,但不利于快速提高唯一变更文件数
|
||||
- 当前 PR review 已没有新的 failed-test 信号;当前优先级是提交这轮 `ai-plan` 修正并等待远端 PR threads 收敛
|
||||
- 之前记录的 plain `dotnet build` `0 Warning(s)` 属于增量构建假阴性,不能再作为 warning 检查真值
|
||||
- 本轮已完成 `GFramework.Godot.SourceGenerators` warning 清理:clean `Release` build 从 9 个 warning 降至 0 个 warning
|
||||
- 当前已确认解决的文件包括 `BindNodeSignalGenerator.cs`、`GetNodeGenerator.cs`、`GodotProjectMetadataGenerator.cs`、`Registration/AutoRegisterExportedCollectionsGenerator.cs`
|
||||
- 本轮直接执行仓库根目录 `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`”作为目标,继续深挖同一测试文件会让目标推进缓慢
|
||||
- 缓解措施:下一轮先确认是继续压低 `MA0051` 基线,还是切换到新的文件写集
|
||||
- WSL 构建环境风险:当前 worktree 的 .NET 定向验证仍需显式附带 `-p:RestoreFallbackFolders=`,并在沙箱外运行以规避命名管道 / socket 限制
|
||||
- 缓解措施:后续所有 affected-project Release build 继续复用该参数组合
|
||||
- source generator test warning 范围风险:一旦继续触达 `GFramework.SourceGenerators.Tests`,剩余 warning 会继续成为本轮完成条件的一部分
|
||||
- 缓解措施:继续用最小写集和 warnings-only build 锁定范围
|
||||
- 如果后续继续依赖增量 `dotnet build`,容易再次把 warning 数量误判为 0
|
||||
- 缓解措施:每轮 warning 检查前先执行 `dotnet clean`,再执行目标 `dotnet build`
|
||||
- 仓库根目录 `dotnet clean` 目前仍然无法给出新的 clean 基线
|
||||
- 缓解措施:若下一轮继续做整仓 warning reduction,先定位 `dotnet clean` 的 solution-level 失败原因,或明确继续沿用用户确认的 `1193 warning(s)` clean 基线与本轮 `1184 warning(s)` direct build 观测值
|
||||
- 当前 worktree 已存在与本批次无关的未提交改动
|
||||
- 缓解措施:提交当前批次时只暂存 `GFramework.Godot.SourceGenerators.Tests` 与对应 `ai-plan` 文件,避免混入其他 topic 变更
|
||||
- `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-rp002-rp041.md](../archive/todos/analyzer-warning-reduction-history-rp002-rp041.md)
|
||||
- 历史 trace 归档:
|
||||
- [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-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=""`
|
||||
- 结果:通过;重写了受 Windows fallback package folder 影响的测试项目资产文件
|
||||
- `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"`
|
||||
- 结果:`10 Warning(s)`,`0 Error(s)`;warning 仍全部来自 `CqrsHandlerRegistryGeneratorTests.cs` 的既有 `MA0051` 基线
|
||||
- `dotnet clean`
|
||||
- 结果:失败;停在 solution `ValidateSolutionConfiguration`,`0 Warning(s)`、`0 Error(s)`,未输出更具体的 error 文本
|
||||
- `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)`
|
||||
- 本轮收尾结果:成功;`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 是否随新提交收敛
|
||||
2. 若 PR threads 收敛,再决定下一轮是继续清理 `CqrsHandlerRegistryGeneratorTests.cs` 的剩余 `MA0051`,还是切换到新的文件写集
|
||||
3. 如果仍要继续沿用“唯一变更文件数接近 `75`”的目标,应优先切到新的 warning 写集,而不是继续深挖同一测试文件
|
||||
1. 提交当前 comparer 契约与 `ConfigureAwait(false)` PR follow-up,并确认只纳入本 topic 相关文件
|
||||
2. 视 PR review 反馈决定是否继续收敛 `GFramework.Game` 现有 warning 基线,或返回下一轮整仓 warning 热点筛选
|
||||
|
||||
@ -1,31 +1,91 @@
|
||||
# Analyzer Warning Reduction 追踪
|
||||
|
||||
## 2026-04-24 — RP-042
|
||||
# Analyzer Warning Reduction 追踪
|
||||
|
||||
### 阶段:PR #280 review follow-up 与 ai-plan 恢复入口修正
|
||||
## 2026-04-24 — RP-052
|
||||
|
||||
- 启动复核:
|
||||
- 使用 `gframework-pr-review` 抓取当前分支 PR #280 的 latest-head review threads、MegaLinter 摘要与测试报告
|
||||
- 本地核对后确认 `3` 条 open threads 均仍成立,但全部集中在 `ai-plan` 文档恢复入口,而不是新的代码行为问题
|
||||
- 决策:
|
||||
- 不再继续扩大 `GFramework.SourceGenerators.Tests` 的写集,先把远端 latest-head review 中仍成立的文档问题全部收口
|
||||
- 保持 `RP-042` 作为 active recovery point,仅刷新其事实描述、归档链接和 trace 范围边界
|
||||
- 实施调整:
|
||||
- 修正 `archive/todos/analyzer-warning-reduction-history-rp002-rp041.md` 中两条指向 `RP-001` 归档的相对链接
|
||||
- 从 `archive/traces/analyzer-warning-reduction-history-rp002-rp041.md` 中移除误混入的 `RP-001` 段落,使文件只保留 `RP-002` 到 `RP-041`
|
||||
- 刷新 active tracking / trace 的恢复点描述,明确当前 open threads 已收敛为文档问题,并记录本轮 follow-up 的事实与下一步
|
||||
- 验证结果:
|
||||
- `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"`
|
||||
- 结果:`10 Warning(s)`,`0 Error(s)`;warning 仍全部来自 `CqrsHandlerRegistryGeneratorTests.cs` 的既有 `MA0051` 基线
|
||||
### 阶段:PR review follow-up(comparer 契约 + `ConfigureAwait(false)` 收尾)
|
||||
|
||||
- 触发背景:
|
||||
- 当前分支 PR #283 的最新 review 中,`greptile-apps[bot]` 仍有一个未解决线程,指出 `UnifiedSettingsDataRepository.CloneFile` fallback 会静默丢失原 comparer
|
||||
- CodeRabbit 另指出 `AutoRegisterExportedCollectionsGeneratorTests.cs` 中还残留 5 处 `await test.RunAsync();`,与同项目其他测试文件的 `.ConfigureAwait(false)` 风格不一致
|
||||
- 主线程实施:
|
||||
- 复核 PR review JSON、`UnifiedSettingsDataRepository.cs`、`UnifiedSettingsFile.cs` 与 `AutoRegisterExportedCollectionsGeneratorTests.cs` 的当前代码,确认只有 comparer 契约线程仍属最新 head 上的实质问题
|
||||
- 将 `UnifiedSettingsFile.Sections` 的 XML 注释补充为显式 comparer 契约,并把默认字典初始化改为 `StringComparer.Ordinal`
|
||||
- 将 `CloneFile` fallback 从隐式默认 comparer 改为显式 `StringComparer.Ordinal`,并同步修正文档注释,避免继续暗含“保留原语义”的错误表述
|
||||
- 把 `AutoRegisterExportedCollectionsGeneratorTests` 中剩余的 5 处 `await test.RunAsync();` 统一为 `.ConfigureAwait(false)`,同时让 `VerifyDiagnosticsAsync` 内部也消费 `ConfigureAwait(false)`
|
||||
- 验证里程碑:
|
||||
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
|
||||
- 结果:成功;`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` 范围内处理
|
||||
- active 恢复入口与历史归档范围已重新对齐,后续 `boot` 不会再从 `rp002-rp041` 误读 `RP-001`
|
||||
- 下一步建议:
|
||||
- 提交后重新抓取 PR #280 review,确认 open threads 是否收敛
|
||||
- 若 threads 收敛,则回到 `CqrsHandlerRegistryGeneratorTests.cs` 剩余 `MA0051`,或根据目标改切新的 warning 写集
|
||||
- PR #283 当前仍打开的 comparer review thread 已在本地代码与 XML 注释层面得到对应修复
|
||||
- `AutoRegisterExportedCollectionsGeneratorTests` 的异步等待风格已与同项目其他测试保持一致
|
||||
- 当前改动已通过直接受影响测试项目的 Release build 与串行 Release test 复验,可进入提交阶段
|
||||
|
||||
## 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
|
||||
|
||||
- 当前轮次归档:
|
||||
- [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-rp002-rp041.md](../archive/todos/analyzer-warning-reduction-history-rp002-rp041.md)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user