Compare commits

..

No commits in common. "7e45197698705e184e4c3348d4986d5129de6aaf" and "1a9e8f64bd4031d6487364e4ae6e7e04e8f8a1b5" have entirely different histories.

54 changed files with 4052 additions and 4258 deletions

View File

@ -46,22 +46,9 @@
/gframework-batch-boot <task-or-stop-condition>
```
批处理阈值速记:
```bash
/gframework-batch-boot 75
/gframework-batch-boot 75 2000
```
- 单个数字默认表示“当前分支全部提交相对远程 `origin/main` 接近多少个文件变更时停止”
- 两个数字默认表示“当前分支全部提交相对远程 `origin/main``文件数 OR 变更行数`”,顺序固定为 `<files> <lines>`
- 不推荐写 `/gframework-batch-boot 75 | 2000`,因为 `|` 很像 shell pipe若用户这样写也应按 OR 语义理解并在后续说明中归一化成无 `|` 版本
示例:
```bash
/gframework-batch-boot 75
/gframework-batch-boot 75 2000
/gframework-batch-boot continue analyzer warning reduction until branch diff vs origin/main approaches 75 files
/gframework-batch-boot keep refactoring repetitive source-generator tests in bounded batches
```

View File

@ -50,19 +50,6 @@ For changed-file limits, measure branch-wide scope against the chosen baseline,
- use `git diff --name-only <baseline>...HEAD`
- do not confuse branch diff size with `git status --short`
For changed-line limits, also measure branch-wide scope against the chosen baseline:
- prefer `git diff --numstat <baseline>...HEAD`
- treat "changed lines" as `added + deleted` summed across the branch diff
- do not use working-tree-only line counts as a substitute for branch-wide scope
For shorthand numeric thresholds, use a fixed default baseline:
- compare the current branch's cumulative diff against remote `origin/main`
- include all commits reachable from `HEAD` that are not already in `origin/main`
- do not reinterpret shorthand thresholds as "this batch only" or "current unstaged changes only"
- only use another baseline when the user explicitly names it in the prompt
## Stop Conditions
Choose one primary stop condition before the first batch and restate it to the user.
@ -76,32 +63,6 @@ Common stop conditions:
If multiple stop conditions exist, rank them and treat one as primary.
## Shorthand Stop-Condition Syntax
`gframework-batch-boot` may be invoked with shorthand numeric thresholds when the user clearly wants a branch-size stop
condition instead of a long natural-language prompt.
Interpret shorthand as follows:
- `$gframework-batch-boot 75`
- means: stop when the current branch's cumulative diff vs remote `origin/main` approaches `75` changed files
- `$gframework-batch-boot 75 2000`
- means: stop when the current branch's cumulative diff vs remote `origin/main` approaches `75` changed files OR
`2000` changed lines
- default positional meaning is `<files> <lines>`
- `$gframework-batch-boot 75 | 2000`
- may be interpreted as the same OR shorthand in plain-language chat
- when restating, planning, or documenting the command, normalize it to `$gframework-batch-boot 75 2000`
- prefer the no-pipe form because `|` is easy to confuse with a shell pipeline
When shorthand is used:
- report the resolved thresholds explicitly before the first batch
- report that the baseline is remote `origin/main`, unless the user explicitly overrides it
- if two numeric thresholds are present, treat file count as the default primary metric for status reporting unless the
user says otherwise
- stop when either threshold is reached or exceeded, even if the other threshold still has headroom
## Batch Loop
1. Inspect the current state before the first batch:
@ -173,8 +134,6 @@ When stopping, report:
## Example Triggers
- `Use $gframework-batch-boot 75 to keep reducing analyzer warnings until the branch diff vs baseline approaches 75 files.`
- `Use $gframework-batch-boot 75 2000 to keep reducing warnings until the branch diff approaches 75 files or 2000 changed lines.`
- `Use $gframework-batch-boot and keep reducing analyzer warnings until the branch diff vs origin/main approaches 75 files.`
- `Use $gframework-batch-boot to continue this repetitive test refactor in bounded batches until the warning count drops below 10.`
- `Use $gframework-batch-boot and refresh module docs in waves without asking me to trigger every round.`

View File

@ -63,7 +63,7 @@ jobs:
# 使用 TruffleHog 工具扫描代码库中的敏感信息泄露如API密钥、密码等
# 该步骤会比较基础分支和当前提交之间的差异,检测新增内容中是否包含敏感数据
- name: TruffleHog OSS
uses: trufflesecurity/trufflehog@v3.95.2
uses: trufflesecurity/trufflehog@v3.94.3
with:
# 扫描路径,. 表示扫描整个仓库
path: .

View File

@ -29,10 +29,6 @@ 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
@ -237,10 +233,6 @@ 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

View File

@ -1,11 +1,11 @@
<Project>
<!-- Keep repository-wide analyzer behavior consistent while allowing only selected projects to opt into polyfills. -->
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="3.0.52">
<PackageReference Include="Meziantou.Analyzer" Version="3.0.48">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Update="Meziantou.Polyfill" Version="1.0.116">
<PackageReference Update="Meziantou.Polyfill" Version="1.0.110">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>

View File

@ -21,6 +21,6 @@
<ProjectReference Include="..\GFramework.Cqrs.Abstractions\GFramework.Cqrs.Abstractions.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.7"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.6"/>
</ItemGroup>
</Project>

View File

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

View File

@ -14,7 +14,7 @@
<ProjectReference Include="..\$(AssemblyName).Abstractions\$(AssemblyName).Abstractions.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.7"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.6"/>
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0"/>
</ItemGroup>
</Project>

View File

@ -283,21 +283,15 @@ 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
// 否则退回到按当前内容复制,避免因为 API 抽象化而改变持久化前后的键比较语义。
var sections = source.Sections is Dictionary<string, string> dictionary
? new Dictionary<string, string>(dictionary, dictionary.Comparer)
: new Dictionary<string, string>(source.Sections);
return new UnifiedSettingsFile
{
Version = source.Version,
Sections = sections
Sections = new Dictionary<string, string>(source.Sections, source.Sections.Comparer)
};
}

View File

@ -11,7 +11,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using System.Collections.Generic;
using GFramework.Core.Abstractions.Versioning;
namespace GFramework.Game.Data;
@ -23,16 +22,13 @@ namespace GFramework.Game.Data;
internal sealed class UnifiedSettingsFile : IVersioned
{
/// <summary>
/// 配置节映射,存储不同类型的配置数据。
/// 配置节集合,存储不同类型的配置数据
/// 键为配置节名称,值为配置对象
/// </summary>
/// <remarks>
/// 这里公开为 <see cref="IDictionary{TKey,TValue}" /> 而不是具体的 <see cref="Dictionary{TKey,TValue}" />
/// 以避免暴露可替换的具体集合实现,同时继续兼容 Newtonsoft.Json 对字典对象的序列化与反序列化。
/// </remarks>
public IDictionary<string, string> Sections { get; set; } = new Dictionary<string, string>();
public Dictionary<string, string> Sections { get; set; } = new();
/// <summary>
/// 配置文件版本号,用于版本控制和兼容性检查
/// </summary>
public int Version { get; set; }
}
}

View File

@ -6,61 +6,57 @@ 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()
{
string source = CreateAutoSceneSource(
AutoSceneAttributeWithKeyDeclaration,
"""
[AutoScene("Gameplay")]
public partial class GameplayRoot : Node2D
{
}
""",
includeBehaviorInfrastructure: true);
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
{
}
}
""";
const string expected = """
// <auto-generated />
@ -84,20 +80,40 @@ public class AutoSceneGeneratorTests
await GeneratorTest<AutoSceneGenerator>.RunAsync(
source,
("TestApp_GameplayRoot.AutoScene.g.cs", expected)).ConfigureAwait(false);
("TestApp_GameplayRoot.AutoScene.g.cs", expected));
}
[Test]
public async Task Reports_Diagnostic_When_AutoScene_Arguments_Are_Invalid()
{
string source = CreateAutoSceneSource(
AutoSceneAttributeWithoutKeyDeclaration,
"""
[{|#0:AutoScene|}]
public partial class GameplayRoot : Node2D
{
}
""");
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
{
}
}
""";
var test = new CSharpSourceGeneratorTest<AutoSceneGenerator, DefaultVerifier>
{
@ -112,26 +128,65 @@ public class AutoSceneGeneratorTests
.WithLocation(0)
.WithArguments("AutoSceneAttribute", "GameplayRoot", "a single string scene key argument"));
await test.RunAsync().ConfigureAwait(false);
await test.RunAsync();
}
[Test]
public async Task Generates_Type_Constraints_For_Nullable_Reference_NotNull_And_Unmanaged_Parameters()
{
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 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
{
}
}
""";
const string expected = """
// <auto-generated />
@ -159,7 +214,7 @@ public class AutoSceneGeneratorTests
await GeneratorTest<AutoSceneGenerator>.RunAsync(
source,
("TestApp_GameplayRoot.AutoScene.g.cs", expected)).ConfigureAwait(false);
("TestApp_GameplayRoot.AutoScene.g.cs", expected));
}
/// <summary>
@ -212,7 +267,7 @@ public class AutoSceneGeneratorTests
.WithLocation(0)
.WithArguments("GameplayRoot", "SceneKeyStr"));
await test.RunAsync().ConfigureAwait(false);
await test.RunAsync();
}
/// <summary>
@ -271,39 +326,6 @@ public class AutoSceneGeneratorTests
.WithLocation(0)
.WithArguments("GameplayRoot", "__autoSceneBehavior_Generated"));
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}}
}
""";
await test.RunAsync();
}
}

View File

@ -6,85 +6,69 @@ 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()
{
string source = CreateAutoUiPageSource(
AutoUiPageAttributeWithLayerDeclaration,
UiLayerFullEnum,
"""
[AutoUiPage("MainMenu", "Page")]
public partial class MainMenu : Control
{
}
""");
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
{
}
}
""";
const string expected = """
// <auto-generated />
@ -108,21 +92,70 @@ public class AutoUiPageGeneratorTests
await GeneratorTest<AutoUiPageGenerator>.RunAsync(
source,
("TestApp_MainMenu.AutoUiPage.g.cs", expected)).ConfigureAwait(false);
("TestApp_MainMenu.AutoUiPage.g.cs", expected));
}
[Test]
public async Task Reports_Diagnostic_When_AutoUiPage_Attribute_Arguments_Are_Invalid()
{
string source = CreateAutoUiPageSource(
AutoUiPageAttributeWithoutLayerDeclaration,
UiLayerPageOnlyEnum,
"""
[{|#0:AutoUiPage("MainMenu")|}]
public partial class MainMenu : Control
{
}
""");
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
{
}
}
""";
var test = new CSharpSourceGeneratorTest<AutoUiPageGenerator, DefaultVerifier>
{
@ -141,25 +174,74 @@ public class AutoUiPageGeneratorTests
"MainMenu",
"a string key argument and a string UiLayer name argument"));
await test.RunAsync().ConfigureAwait(false);
await test.RunAsync();
}
[Test]
public async Task Generates_Type_Constraints_For_ClassNullable_NotNull_And_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 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
{
}
}
""";
const string expected = """
// <auto-generated />
@ -186,40 +268,6 @@ public class AutoUiPageGeneratorTests
await GeneratorTest<AutoUiPageGenerator>.RunAsync(
source,
("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}}
}
""";
("TestApp_MainMenu.AutoUiPage.g.cs", expected));
}
}

View File

@ -8,103 +8,93 @@ 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()
{
string source = CreateHudSource(
CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
"""
private Button _startButton = null!;
private SpinBox _startOreSpinBox = null!;
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
private void OnStartButtonPressed()
{
}
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(_startOreSpinBox), nameof(SpinBox.ValueChanged))]
private void OnStartOreValueChanged(double value)
{
}
public string NodeFieldName { get; }
public override void _Ready()
{
__BindNodeSignals_Generated();
}
public string SignalName { get; }
}
}
public override void _ExitTree()
{
__UnbindNodeSignals_Generated();
}
""",
LifecycleNodeType,
ButtonType,
SpinBoxType);
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();
}
}
}
""";
const string expected = """
// <auto-generated />
@ -131,7 +121,7 @@ public class BindNodeSignalGeneratorTests
await GeneratorTest<BindNodeSignalGenerator>.RunAsync(
source,
("TestApp_Hud.BindNodeSignal.g.cs", expected)).ConfigureAwait(false);
("TestApp_Hud.BindNodeSignal.g.cs", expected));
}
/// <summary>
@ -140,23 +130,70 @@ public class BindNodeSignalGeneratorTests
[Test]
public async Task Generates_Multiple_Subscriptions_For_The_Same_Handler_And_Coexists_With_GetNode()
{
string source = CreateHudSource(
CreateAbstractionsSource(BindNodeSignalAttributeDeclaration, GetNodeAttributeDeclaration),
"""
[GetNode]
private Button _startButton = null!;
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
[GetNode]
private Button _cancelButton = 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))]
[BindNodeSignal(nameof(_cancelButton), nameof(Button.Pressed))]
private void OnAnyButtonPressed()
{
}
""",
LifecycleNodeType,
ButtonType);
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()
{
}
}
}
""";
const string expected = """
// <auto-generated />
@ -183,7 +220,7 @@ public class BindNodeSignalGeneratorTests
await GeneratorTest<BindNodeSignalGenerator>.RunAsync(
source,
("TestApp_Hud.BindNodeSignal.g.cs", expected)).ConfigureAwait(false);
("TestApp_Hud.BindNodeSignal.g.cs", expected));
}
/// <summary>
@ -192,195 +229,58 @@ public class BindNodeSignalGeneratorTests
[Test]
public async Task Reports_Diagnostic_When_Signal_Does_Not_Exist()
{
string source = CreateHudSource(
CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
"""
private Button _startButton = null!;
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
[{|#0:BindNodeSignal(nameof(_startButton), "Released")|}]
private void OnStartButtonPressed()
{
}
""",
EmptyNodeType,
ButtonType);
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;
}
await VerifyDiagnosticsAsync(
source,
new DiagnosticResult("GF_Godot_BindNodeSignal_006", DiagnosticSeverity.Error)
.WithLocation(0)
.WithArguments("_startButton", "Released")).ConfigureAwait(false);
}
public string NodeFieldName { get; }
/// <summary>
/// 验证方法签名与事件委托不匹配时会报告错误。
/// </summary>
[Test]
public async Task Reports_Diagnostic_When_Method_Signature_Does_Not_Match_Event()
{
string source = CreateHudSource(
CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
"""
private SpinBox _startOreSpinBox = null!;
public string SignalName { get; }
}
}
[{|#0:BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))|}]
private void OnStartOreValueChanged()
{
}
""",
EmptyNodeType,
SpinBoxType);
namespace Godot
{
public class Node
{
}
await VerifyDiagnosticsAsync(
source,
new DiagnosticResult("GF_Godot_BindNodeSignal_007", DiagnosticSeverity.Error)
.WithLocation(0)
.WithArguments("OnStartOreValueChanged", "ValueChanged", "_startOreSpinBox")).ConfigureAwait(false);
}
public class Button : Node
{
public event Action? Pressed
{
add {}
remove {}
}
}
}
/// <summary>
/// 验证特性构造参数为空时会报告明确的参数无效诊断。
/// </summary>
[Test]
public async Task Reports_Diagnostic_When_Constructor_Argument_Is_Empty()
{
string source = CreateHudSource(
CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
"""
private Button _startButton = null!;
namespace TestApp
{
public partial class Hud : Node
{
private Button _startButton = null!;
[{|#0:BindNodeSignal(nameof(_startButton), "")|}]
private void OnStartButtonPressed()
{
}
""",
EmptyNodeType,
ButtonType);
[{|#0:BindNodeSignal(nameof(_startButton), "Released")|}]
private void OnStartButtonPressed()
{
}
}
}
""";
await VerifyDiagnosticsAsync(
source,
new DiagnosticResult("GF_Godot_BindNodeSignal_010", DiagnosticSeverity.Error)
.WithLocation(0)
.WithArguments("OnStartButtonPressed", "signalName")).ConfigureAwait(false);
}
/// <summary>
/// 验证当用户自定义了与生成方法同名的成员时,会报告冲突而不是生成重复成员。
/// </summary>
[Test]
public async Task Reports_Diagnostic_When_Generated_Method_Names_Already_Exist()
{
string source = CreateHudSource(
CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
"""
private Button _startButton = null!;
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
private void OnStartButtonPressed()
{
}
private void {|#0:__BindNodeSignals_Generated|}()
{
}
private void {|#1:__UnbindNodeSignals_Generated|}()
{
}
""",
EmptyNodeType,
ButtonType);
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>
/// 验证已有生命周期方法但未调用生成方法时会报告对称的警告。
/// </summary>
[Test]
public async Task Reports_Warnings_When_Lifecycle_Methods_Do_Not_Call_Generated_Methods()
{
string source = CreateHudSource(
CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
"""
private Button _startButton = null!;
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
private void OnStartButtonPressed()
{
}
public override void {|#0:_Ready|}()
{
}
public override void {|#1:_ExitTree|}()
{
}
""",
LifecycleNodeType,
ButtonType);
await VerifyDiagnosticsAsync(
source,
new DiagnosticResult("GF_Godot_BindNodeSignal_008", DiagnosticSeverity.Warning)
.WithLocation(0)
.WithArguments("Hud"),
new DiagnosticResult("GF_Godot_BindNodeSignal_009", DiagnosticSeverity.Warning)
.WithLocation(1)
.WithArguments("Hud")).ConfigureAwait(false);
}
private static string CreateAbstractionsSource(params string[] attributeDeclarations)
{
string declarations = string.Join($"{Environment.NewLine}{Environment.NewLine}", attributeDeclarations);
return $$"""
namespace GFramework.Godot.SourceGenerators.Abstractions
{
{{declarations}}
}
""";
}
private static string CreateHudSource(
string abstractionsSource,
string hudMembers,
params string[] godotTypes)
{
string godotSource = string.Join($"{Environment.NewLine}{Environment.NewLine}", godotTypes);
return $$"""
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
{{abstractionsSource}}
namespace Godot
{
{{godotSource}}
}
namespace TestApp
{
public partial class Hud : Node
{
{{hudMembers}}
}
}
""";
}
private static Task VerifyDiagnosticsAsync(string source, params DiagnosticResult[] expectedDiagnostics)
{
var test = new CSharpSourceGeneratorTest<BindNodeSignalGenerator, DefaultVerifier>
{
TestState =
@ -391,11 +291,339 @@ public class BindNodeSignalGeneratorTests
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
};
foreach (DiagnosticResult expectedDiagnostic in expectedDiagnostics)
{
test.ExpectedDiagnostics.Add(expectedDiagnostic);
}
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_006", DiagnosticSeverity.Error)
.WithLocation(0)
.WithArguments("_startButton", "Released"));
return test.RunAsync();
await test.RunAsync();
}
}
/// <summary>
/// 验证方法签名与事件委托不匹配时会报告错误。
/// </summary>
[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;
namespace GFramework.Godot.SourceGenerators.Abstractions
{
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public sealed class BindNodeSignalAttribute : Attribute
{
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
{
NodeFieldName = nodeFieldName;
SignalName = signalName;
}
public string NodeFieldName { get; }
public string SignalName { get; }
}
}
namespace Godot
{
public class Node
{
}
public class SpinBox : Node
{
public delegate void ValueChangedEventHandler(double value);
public event ValueChangedEventHandler? ValueChanged
{
add {}
remove {}
}
}
}
namespace TestApp
{
public partial class Hud : Node
{
private SpinBox _startOreSpinBox = null!;
[{|#0:BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))|}]
private void OnStartOreValueChanged()
{
}
}
}
""";
var test = new CSharpSourceGeneratorTest<BindNodeSignalGenerator, DefaultVerifier>
{
TestState =
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" },
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
};
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_007", DiagnosticSeverity.Error)
.WithLocation(0)
.WithArguments("OnStartOreValueChanged", "ValueChanged", "_startOreSpinBox"));
await test.RunAsync();
}
/// <summary>
/// 验证特性构造参数为空时会报告明确的参数无效诊断。
/// </summary>
[Test]
public async Task Reports_Diagnostic_When_Constructor_Argument_Is_Empty()
{
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions
{
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public sealed class BindNodeSignalAttribute : Attribute
{
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
{
NodeFieldName = nodeFieldName;
SignalName = signalName;
}
public string NodeFieldName { get; }
public string SignalName { get; }
}
}
namespace Godot
{
public class Node
{
}
public class Button : Node
{
public event Action? Pressed
{
add {}
remove {}
}
}
}
namespace TestApp
{
public partial class Hud : Node
{
private Button _startButton = null!;
[{|#0:BindNodeSignal(nameof(_startButton), "")|}]
private void OnStartButtonPressed()
{
}
}
}
""";
var test = new CSharpSourceGeneratorTest<BindNodeSignalGenerator, DefaultVerifier>
{
TestState =
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" },
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
};
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_010", DiagnosticSeverity.Error)
.WithLocation(0)
.WithArguments("OnStartButtonPressed", "signalName"));
await test.RunAsync();
}
/// <summary>
/// 验证当用户自定义了与生成方法同名的成员时,会报告冲突而不是生成重复成员。
/// </summary>
[Test]
public async Task Reports_Diagnostic_When_Generated_Method_Names_Already_Exist()
{
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions
{
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public sealed class BindNodeSignalAttribute : Attribute
{
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
{
NodeFieldName = nodeFieldName;
SignalName = signalName;
}
public string NodeFieldName { get; }
public string SignalName { get; }
}
}
namespace Godot
{
public class Node
{
}
public class Button : Node
{
public event Action? Pressed
{
add {}
remove {}
}
}
}
namespace TestApp
{
public partial class Hud : Node
{
private Button _startButton = null!;
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
private void OnStartButtonPressed()
{
}
private void {|#0:__BindNodeSignals_Generated|}()
{
}
private void {|#1:__UnbindNodeSignals_Generated|}()
{
}
}
}
""";
var test = new CSharpSourceGeneratorTest<BindNodeSignalGenerator, DefaultVerifier>
{
TestState =
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" },
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
};
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error)
.WithLocation(0)
.WithArguments("Hud", "__BindNodeSignals_Generated"));
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error)
.WithLocation(1)
.WithArguments("Hud", "__UnbindNodeSignals_Generated"));
await test.RunAsync();
}
/// <summary>
/// 验证已有生命周期方法但未调用生成方法时会报告对称的警告。
/// </summary>
[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;
namespace GFramework.Godot.SourceGenerators.Abstractions
{
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public sealed class BindNodeSignalAttribute : Attribute
{
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
{
NodeFieldName = nodeFieldName;
SignalName = signalName;
}
public string NodeFieldName { get; }
public string SignalName { get; }
}
}
namespace Godot
{
public class Node
{
public virtual void _Ready() {}
public virtual void _ExitTree() {}
}
public class Button : Node
{
public event Action? Pressed
{
add {}
remove {}
}
}
}
namespace TestApp
{
public partial class Hud : Node
{
private Button _startButton = null!;
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
private void OnStartButtonPressed()
{
}
public override void {|#0:_Ready|}()
{
}
public override void {|#1:_ExitTree|}()
{
}
}
}
""";
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_008", DiagnosticSeverity.Warning)
.WithLocation(0)
.WithArguments("Hud"));
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_009", DiagnosticSeverity.Warning)
.WithLocation(1)
.WithArguments("Hud"));
await test.RunAsync();
}
}

View File

@ -29,7 +29,7 @@ public static class GeneratorTest<TGenerator>
test.TestState.GeneratedSources.Add(
(typeof(TGenerator), filename, NormalizeLineEndings(content)));
await test.RunAsync().ConfigureAwait(false);
await test.RunAsync();
}
/// <summary>
@ -44,4 +44,4 @@ public static class GeneratorTest<TGenerator>
.Replace("\r", "\n", StringComparison.Ordinal)
.Replace("\n", Environment.NewLine, StringComparison.Ordinal);
}
}
}

View File

@ -5,88 +5,61 @@ 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()
{
string source = CreateGetNodeSource(
FullGetNodeAttributeDeclaration,
"""
public partial class TopBar : HBoxContainer
{
[GetNode]
private HBoxContainer _leftContainer = null!;
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
[GetNode]
private HBoxContainer m_rightContainer = null!;
}
""",
HBoxContainerType);
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!;
}
}
""";
const string expected = """
// <auto-generated />
@ -115,30 +88,69 @@ public class GetNodeGeneratorTests
await GeneratorTest<GetNodeGenerator>.RunAsync(
source,
("TestApp_TopBar.GetNode.g.cs", expected)).ConfigureAwait(false);
("TestApp_TopBar.GetNode.g.cs", expected));
}
[Test]
public async Task Generates_ManualInjectionOnly_WhenReadyAlreadyExists()
{
string source = CreateGetNodeSource(
FullGetNodeAttributeDeclaration,
"""
public partial class TopBar : HBoxContainer
{
[GetNode("%LeftContainer")]
private HBoxContainer _leftContainer = null!;
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
[GetNode(Required = false, Lookup = NodeLookupMode.RelativePath)]
private HBoxContainer? _rightContainer;
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 override void _Ready()
{
__InjectGetNodes_Generated();
}
}
""",
HBoxContainerType);
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();
}
}
}
""";
const string expected = """
// <auto-generated />
@ -159,7 +171,7 @@ public class GetNodeGeneratorTests
await GeneratorTest<GetNodeGenerator>.RunAsync(
source,
("TestApp_TopBar.GetNode.g.cs", expected)).ConfigureAwait(false);
("TestApp_TopBar.GetNode.g.cs", expected));
}
[Test]
@ -222,26 +234,58 @@ public class GetNodeGeneratorTests
.WithSpan(39, 24, 39, 38)
.WithArguments("_leftContainer"));
await test.RunAsync().ConfigureAwait(false);
await test.RunAsync();
}
[Test]
public async Task Reports_Diagnostic_When_Generated_Injection_Method_Name_Already_Exists()
{
string source = CreateGetNodeSource(
MinimalGetNodeAttributeDeclaration,
"""
public partial class TopBar : HBoxContainer
{
[GetNode]
private HBoxContainer _leftContainer = null!;
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
private void {|#0:__InjectGetNodes_Generated|}()
{
}
}
""",
HBoxContainerType);
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|}()
{
}
}
}
""";
var test = new CSharpSourceGeneratorTest<GetNodeGenerator, DefaultVerifier>
{
@ -257,39 +301,6 @@ public class GetNodeGeneratorTests
.WithLocation(0)
.WithArguments("TopBar", "__InjectGetNodes_Generated"));
await test.RunAsync().ConfigureAwait(false);
await test.RunAsync();
}
private static string CreateGetNodeSource(
string attributeDeclaration,
string testAppSource,
params string[] godotTypes)
{
string[] allGodotTypes = new string[godotTypes.Length + 1];
allGodotTypes[0] = NodeWithReadyAndLookupMethods;
Array.Copy(godotTypes, 0, allGodotTypes, 1, godotTypes.Length);
string godotSource = string.Join($"{Environment.NewLine}{Environment.NewLine}", allGodotTypes);
return $$"""
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions
{
{{attributeDeclaration}}
}
namespace Godot
{
{{godotSource}}
}
namespace TestApp
{
{{testAppSource}}
}
""";
}
}
}

View File

@ -8,131 +8,6 @@ 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>
@ -154,19 +29,142 @@ 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", $"{AutoLoadProjectFile}\n\n{InputActionsProjectFile}"));
("project.godot", projectFile));
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>

View File

@ -6,52 +6,48 @@ 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()
{
string source = CreateSource(
"""
public sealed class IntRegistry
{
public void Register(int value) { }
}
const string source = """
#nullable enable
using System;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
[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();
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public List<int>? Values { get; } = new();
}
""",
nullableEnabled: true);
[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();
}
}
""";
const string expected = """
// <auto-generated />
@ -81,7 +77,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
}
[Test]
@ -141,23 +137,41 @@ public class AutoRegisterExportedCollectionsGeneratorTests
[Test]
public async Task Generates_Batch_Registration_Method_When_Register_Method_Uses_Array_Parameter()
{
string source = CreateSource(
"""
public sealed class ArrayRegistry
{
public void Register(int[] value) { }
}
const string source = """
#nullable enable
using System;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
private readonly ArrayRegistry _registry = new();
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[RegisterExportedCollection(nameof(_registry), nameof(ArrayRegistry.Register))]
public List<int[]> Values { get; } = new();
}
""",
nullableEnabled: true);
[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();
}
}
""";
const string expected = """
// <auto-generated />
@ -183,41 +197,59 @@ public class AutoRegisterExportedCollectionsGeneratorTests
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
}
[Test]
public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Inherited_Interface()
{
string source = CreateSource(
"""
public interface IKeyValue<TKey, TValue>
{
}
const string source = """
#nullable enable
using System;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
public interface IRegistry<TKey, TValue>
{
void Registry(IKeyValue<TKey, TValue> mapping);
}
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
public interface IAssetRegistry<TValue> : IRegistry<string, TValue>
{
}
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
}
public sealed class IntConfig : IKeyValue<string, int>
{
}
namespace TestApp
{
public interface IKeyValue<TKey, TValue>
{
}
[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
private readonly IAssetRegistry<int>? _registry = null;
public interface IRegistry<TKey, TValue>
{
void Registry(IKeyValue<TKey, TValue> mapping);
}
[RegisterExportedCollection(nameof(_registry), "Registry")]
public List<IntConfig>? Values { get; } = new();
}
""",
nullableEnabled: true);
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();
}
}
""";
const string expected = """
// <auto-generated />
@ -243,7 +275,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
}
[Test]
@ -308,27 +340,45 @@ public class AutoRegisterExportedCollectionsGeneratorTests
[Test]
public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Base_Class()
{
string source = CreateSource(
"""
public class BaseRegistry
{
public void Register(int value) { }
}
const string source = """
#nullable enable
using System;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
public sealed class DerivedRegistry : BaseRegistry
{
}
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 DerivedRegistry? _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(BaseRegistry.Register))]
public List<int>? Values { get; } = new();
}
""",
nullableEnabled: true);
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();
}
}
""";
const string expected = """
// <auto-generated />
@ -354,32 +404,50 @@ public class AutoRegisterExportedCollectionsGeneratorTests
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
}
[Test]
public async Task Generates_Batch_Registration_Method_When_Registry_Member_Comes_From_Base_Class()
{
string source = CreateSource(
"""
public sealed class IntRegistry
{
public void Register(int value) { }
}
const string source = """
#nullable enable
using System;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
public abstract class BootstrapperBase
{
protected readonly IntRegistry? _registry = new();
}
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[AutoRegisterExportedCollections]
public partial class Bootstrapper : BootstrapperBase
{
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public List<int>? Values { get; } = new();
}
""",
nullableEnabled: true);
[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();
}
}
""";
const string expected = """
// <auto-generated />
@ -405,47 +473,74 @@ public class AutoRegisterExportedCollectionsGeneratorTests
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
}
[Test]
public async Task Reports_Diagnostic_When_Collection_Member_Is_Not_Instance_Readable()
{
string source = CreateSource(
"""
public sealed class IntRegistry
{
public void Register(int value) { }
}
const string source = """
using System;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
private readonly IntRegistry _registry = new();
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public static List<int> {|#0:StaticValues|} = 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> {|#1:StaticPropertyValues|} { get; } = new();
namespace TestApp
{
public sealed class IntRegistry
{
public void Register(int value) { }
}
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public List<int> {|#2:WriteOnlyValues|} { set { } }
}
""");
[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
private readonly IntRegistry _registry = new();
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);
[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();
}
[Test]
@ -616,28 +711,45 @@ public class AutoRegisterExportedCollectionsGeneratorTests
[Test]
public async Task Generates_Only_One_Source_When_Multiple_Partial_Declarations_Are_Annotated()
{
string source = CreateSource(
"""
public sealed class IntRegistry
{
public void Register(int value) { }
}
const string source = """
#nullable enable
using System;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
private readonly IntRegistry? _registry = new();
}
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public List<int>? Values { get; } = new();
}
""",
nullableEnabled: true,
allowMultipleDeclarations: true);
[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();
}
}
""";
const string expected = """
// <auto-generated />
@ -663,61 +775,6 @@ public class AutoRegisterExportedCollectionsGeneratorTests
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
}
private static string CreateSource(
string applicationSource,
bool nullableEnabled = false,
bool allowMultipleDeclarations = false)
{
string nullableDirective = nullableEnabled ? "#nullable enable\n" : string.Empty;
string attributeDeclarations = allowMultipleDeclarations
? MultiDeclarationAttributeDeclarations
: StandardAttributeDeclarations;
return $$"""
{{nullableDirective}}using System;
using System.Collections;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
{{attributeDeclarations}}
}
namespace TestApp
{
{{applicationSource}}
}
""";
}
private static Task VerifyDiagnosticsAsync(
string source,
bool skipGeneratedSourcesCheck = false,
params DiagnosticResult[] expectedDiagnostics)
{
var test = new CSharpSourceGeneratorTest<AutoRegisterExportedCollectionsGenerator, DefaultVerifier>
{
TestState =
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" }
};
if (skipGeneratedSourcesCheck)
{
test.TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck;
}
foreach (DiagnosticResult expectedDiagnostic in expectedDiagnostics)
{
test.ExpectedDiagnostics.Add(expectedDiagnostic);
}
return test.RunAsync();
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
}
}

View File

@ -72,8 +72,19 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
if (bindNodeSignalAttribute is null || godotNodeSymbol is null)
return;
var methodAttributes = BuildMethodAttributeMap(candidates, bindNodeSignalAttribute);
var methodCandidates = CollectMethodCandidates(methodAttributes);
// 缓存每个方法上已解析的特性,避免在筛选和生成阶段重复做语义查询。
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();
foreach (var group in GroupByContainingType(methodCandidates))
{
@ -88,7 +99,19 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
UnbindMethodName))
continue;
var bindings = CollectBindings(context, group, methodAttributes, 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);
}
}
if (bindings.Count == 0)
continue;
@ -148,22 +171,99 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
if (candidate.MethodSymbol.IsStatic)
{
ReportStaticMethodDiagnostic(context, candidate, attribute);
ReportMethodDiagnostic(
context,
BindNodeSignalDiagnostics.StaticMethodNotSupported,
candidate,
attribute,
candidate.MethodSymbol.Name);
return false;
}
if (!TryResolveBindingTargetNames(context, candidate, attribute, out var nodeFieldName, out var signalName))
if (!TryResolveCtorString(attribute, 0, out var nodeFieldName))
{
ReportMethodDiagnostic(
context,
BindNodeSignalDiagnostics.InvalidConstructorArgument,
candidate,
attribute,
candidate.MethodSymbol.Name,
"nodeFieldName");
return false;
}
if (!TryFindCompatibleField(context, candidate, attribute, godotNodeSymbol, nodeFieldName, out var fieldSymbol))
if (!TryResolveCtorString(attribute, 1, out var signalName))
{
ReportMethodDiagnostic(
context,
BindNodeSignalDiagnostics.InvalidConstructorArgument,
candidate,
attribute,
candidate.MethodSymbol.Name,
"signalName");
return false;
}
if (!TryFindCompatibleEvent(context, candidate, attribute, fieldSymbol, signalName, out var eventSymbol))
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);
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))
{
ReportIncompatibleSignatureDiagnostic(context, candidate, attribute, eventSymbol, fieldSymbol);
ReportMethodDiagnostic(
context,
BindNodeSignalDiagnostics.MethodSignatureNotCompatible,
candidate,
attribute,
candidate.MethodSymbol.Name,
eventSymbol.Name,
fieldSymbol.Name);
return false;
}
@ -171,235 +271,6 @@ 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,
@ -533,7 +404,11 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
{
return typeSymbol.GetMembers()
.OfType<IMethodSymbol>()
.FirstOrDefault(method => IsParameterlessInstanceMethod(method, methodName));
.FirstOrDefault(method =>
method.Name == methodName &&
!method.IsStatic &&
method.Parameters.Length == 0 &&
method.MethodKind == MethodKind.Ordinary);
}
private static bool CallsGeneratedMethod(
@ -572,14 +447,6 @@ 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);
@ -741,4 +608,4 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
return RuntimeHelpers.GetHashCode(obj);
}
}
}
}

View File

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

View File

@ -126,27 +126,7 @@ 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;
@ -196,14 +176,7 @@ 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))
@ -435,40 +408,24 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator
foreach (var member in members)
{
AppendAutoLoadMemberSource(builder, 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();
}
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>");
@ -487,10 +444,6 @@ 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>");
@ -517,6 +470,9 @@ 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)
@ -574,16 +530,45 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator
if (string.IsNullOrWhiteSpace(content) || content.StartsWith(";", StringComparison.Ordinal))
continue;
if (TryUpdateSection(content, ref currentSection))
if (content.StartsWith("[", StringComparison.Ordinal) && content.EndsWith("]", StringComparison.Ordinal))
{
currentSection = content.Substring(1, content.Length - 2).Trim();
continue;
}
if (!TryParseAssignment(content, out var key, out var value))
continue;
if (TryCollectAutoLoadEntry(file, currentSection, key, value, seenAutoLoads, autoLoads, diagnostics))
continue;
if (string.Equals(currentSection, "autoload", StringComparison.OrdinalIgnoreCase))
{
if (!seenAutoLoads.Add(key))
{
diagnostics.Add(Diagnostic.Create(
GodotProjectDiagnostics.DuplicateAutoLoadEntry,
CreateFileLocation(file.Path),
key));
continue;
}
TryCollectInputAction(currentSection, key, seenInputActions, inputActions, diagnostics, file.Path);
autoLoads.Add(new ProjectAutoLoadEntry(
key,
NormalizeProjectPath(value)));
continue;
}
if (string.Equals(currentSection, "input", StringComparison.OrdinalIgnoreCase))
{
if (!seenInputActions.Add(key))
{
diagnostics.Add(Diagnostic.Create(
GodotProjectDiagnostics.DuplicateInputActionEntry,
CreateFileLocation(file.Path),
key));
continue;
}
inputActions.Add(key);
}
}
return new ProjectMetadataParseResult(
@ -593,68 +578,6 @@ 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();

View File

@ -190,48 +190,6 @@ 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(
@ -241,11 +199,17 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
return false;
}
var resolvedType = GetMemberType(collectionMember);
if (resolvedType is null)
var collectionType = collectionMember switch
{
IFieldSymbol field => field.Type,
IPropertySymbol property => property.Type,
_ => null
};
if (collectionType is null)
return false;
if (!resolvedType.IsAssignableTo(enumerableType))
if (!collectionType.IsAssignableTo(enumerableType))
{
context.ReportDiagnostic(Diagnostic.Create(
AutoRegisterExportedCollectionsDiagnostics.CollectionTypeMustBeEnumerable,
@ -254,35 +218,12 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
return false;
}
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))
{
if (!TryGetRegistrationAttributeArguments(context, collectionMember, attribute, out var registryMemberName,
out var registerMethodName))
return false;
}
var registryMember = FindRegistryMember(ownerType, registryMemberName);
if (registryMember is null)
{
context.ReportDiagnostic(Diagnostic.Create(
@ -305,24 +246,18 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
return false;
}
var resolvedRegistryType = GetMemberType(registryMember) as INamedTypeSymbol;
if (resolvedRegistryType is null)
var registryType = registryMember switch
{
IFieldSymbol field => field.Type as INamedTypeSymbol,
IPropertySymbol property => property.Type as INamedTypeSymbol,
_ => null
};
if (registryType is null)
return false;
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)
var elementType = TryGetElementType(collectionType);
if (elementType 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.
@ -333,33 +268,26 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
return false;
}
elementType = resolvedElementType;
return true;
}
private static bool HasCompatibleRegisterMethod(
Compilation compilation,
INamedTypeSymbol ownerType,
INamedTypeSymbol registryType,
string registerMethodName,
ITypeSymbol elementType)
{
return EnumerateCandidateMethods(registryType, registerMethodName)
var hasCompatibleMethod = EnumerateCandidateMethods(registryType, registerMethodName)
.Any(method =>
!method.IsStatic &&
method.Parameters.Length == 1 &&
compilation.IsSymbolAccessibleWithin(method, ownerType) &&
CanAcceptElementType(compilation, elementType, method.Parameters[0].Type));
}
private static ITypeSymbol? GetMemberType(ISymbol member)
{
return member switch
if (!hasCompatibleMethod)
{
IFieldSymbol field => field.Type,
IPropertySymbol property => property.Type,
_ => null
};
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 IsInstanceReadableMember(ISymbol member)

View File

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

View File

@ -11,9 +11,6 @@
// 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>
@ -23,47 +20,24 @@ 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 LocalizationMap()
: this(CreateDefaultLanguageMap(), CreateDefaultFrameworkLanguageMap())
public Dictionary<string, string> LanguageMap { get; set; } = new(StringComparer.Ordinal)
{
}
{ "简体中文", "zh_CN" },
{ "English", "en" }
};
/// <summary>
/// 使用外部提供的映射初始化本地化设置。
/// 构造函数会复制输入字典,避免调用方在实例创建后继续修改内部状态。
/// 用户语言 -> GFramework 本地化语言码映射表。
/// </summary>
/// <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)
public Dictionary<string, string> FrameworkLanguageMap { get; set; } = new(StringComparer.Ordinal)
{
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;
{ "简体中文", "zhs" },
{ "English", "eng" }
};
/// <summary>
/// 解析用户保存的语言值对应的 Godot locale。
@ -94,22 +68,4 @@ 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" }
};
}
}

View File

@ -2553,7 +2553,7 @@ public class SchemaConfigGeneratorTests
/// <param name="source">测试输入源码。</param>
/// <param name="additionalFiles">参与本次生成的 schema 文件集合。</param>
/// <returns>按 HintName 索引的生成源码字典。</returns>
private static IReadOnlyDictionary<string, string> RunAndCollectGeneratedSources(
private static global::System.Collections.Generic.IReadOnlyDictionary<string, string> RunAndCollectGeneratedSources(
string source,
params (string path, string content)[] additionalFiles)
{

View File

@ -8,7 +8,7 @@
<Copyright>Copyright © 2025</Copyright>
<RepositoryUrl>https://github.com/GeWuYou/GFramework</RepositoryUrl>
<PackageProjectUrl>https://github.com/GeWuYou/GFramework</PackageProjectUrl>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageTags>game;framework</PackageTags>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@ -16,7 +16,6 @@
<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>

View File

@ -1,517 +0,0 @@
# Analyzer Warning Reduction 跟踪
## 目标
继续以“优先低风险、保持行为兼容”为原则收敛当前仓库的 Meziantou analyzer warnings并在首轮大规模清理完成后
判断剩余结构性 warning 是否值得在下一轮继续推进。
## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-041`
- 当前阶段:`Phase 41`
- 当前焦点:
- 已通过第五个有效 subagent 切片完成
`CqrsHandlerRegistryGeneratorTests.cs`
`Generates_Precise_Service_Type_For_Hidden_Generic_Type_Definitions()``MA0051` 收口:
将内联 `source` 文本提取为类级常量,保持既有 expected 常量和断言语义不变
- 当前 `GFramework.SourceGenerators.Tests` Release warnings-only 基线已从 `11` 条降到 `10` 条;
行号 `680` 已从 `MA0051` 输出中消失,剩余热点继续集中在
`CqrsHandlerRegistryGeneratorTests.cs`
- 已通过第四个有效 subagent 切片完成
`CqrsHandlerRegistryGeneratorTests.cs`
`Generates_Precise_Service_Type_For_Hidden_Array_Type_Arguments()``MA0051` 收口:
将内联 `source` 文本提取为类级常量,保持既有 expected 常量和断言语义不变
- 当前 `GFramework.SourceGenerators.Tests` Release warnings-only 基线已从 `12` 条降到 `11` 条;
行号 `607` 已从 `MA0051` 输出中消失,剩余热点继续集中在
`CqrsHandlerRegistryGeneratorTests.cs`
- 已通过第三个有效 subagent 切片完成
`CqrsHandlerRegistryGeneratorTests.cs`
`Generates_Direct_Interface_Registrations_For_Hidden_Implementation_When_Handler_Interface_Is_Public()`
`MA0051` 收口:将内联 `source` 文本提取为类级常量,保持既有 expected 常量和断言语义不变
- 当前 `GFramework.SourceGenerators.Tests` Release warnings-only 基线已从 `13` 条降到 `12` 条;
行号 `536` 已从 `MA0051` 输出中消失,剩余热点继续集中在
`CqrsHandlerRegistryGeneratorTests.cs`
- 已通过第二个有效 subagent 切片完成
`CqrsHandlerRegistryGeneratorTests.cs`
`Generates_Visible_Handlers_And_Self_Registers_Private_Nested_Handler_When_Assembly_Contains_Hidden_Handler()`
`MA0051` 收口:将内联 `source` 文本提取为类级常量,保持既有 expected 常量和断言语义不变
- 当前 `GFramework.SourceGenerators.Tests` Release warnings-only 基线已从 `14` 条降到 `13` 条;
行号 `454` 已从 `MA0051` 输出中消失,剩余热点继续集中在
`CqrsHandlerRegistryGeneratorTests.cs`
- 已通过 subagent 循环的首个可交付切片完成
`GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs`
`Generates_Assembly_Level_Cqrs_Handler_Registry()``MA0051` 收口:
将内联 `source` / `expected` 文本提取为类级常量,保持生成文本、断言语义和文件名不变
- 当前 `GFramework.SourceGenerators.Tests` Release warnings-only 基线已从 `15` 条降到 `14` 条;
行号 `337` 已从 `MA0051` 输出中消失,剩余热点仍全部集中在
`CqrsHandlerRegistryGeneratorTests.cs`
- 已确认当前分支相对 `origin/main` 的唯一变更文件数仍只有 `3`;按该统计口径距离用户要求的
“接近 `75` 个文件变更”仍很远,需要继续多轮切片
- 已完成 `GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs``MA0051` 收口:
将共享 consumer runtime fixture 提取到类级常量,并把生成结果收集与 catalog 契约断言拆成小 helper
保持 schema 文本、断言语义与生成输出契约不变
- 当前 `GFramework.SourceGenerators.Tests` Release warnings-only 基线已从 `22` 条降到 `15` 条;
`SchemaConfigGeneratorTests.cs` 已不再出现在 `MA0051` 列表中,剩余热点全部集中在
`CqrsHandlerRegistryGeneratorTests.cs`
- 已完成 `SchemaConfigGeneratorTests` 定向验证:串行重跑 `50` 个用例全部通过;并确认先前并行 build/test
触发的 `MSB3030` / `CS0006` 属于共享输出竞争噪音,不是代码回归
- 已按 `gframework-boot` 重新恢复当前 worktree确认分支 `fix/analyzer-warning-reduction-batch` 仍映射到
`analyzer-warning-reduction`,且当前不存在 `ai-plan/private/` 私有恢复上下文
- 已重新抓取当前分支关联的 PR #273 review 状态PR 已处于 `CLOSED`latest-head review 仍显示 `2`
CodeRabbit open thread但本地复核后 `GeneratorSnapshotTest` 的 snapshot 路径空值防御已显式改为
`InvalidOperationException` 防御,`SchemaConfigGenerator``dependentSchemas` / `allOf` / conditional helper
也已补齐 XML 文档,当前更像历史线程未随已关闭 PR 一起收敛
- 已重新以 `GFramework.SourceGenerators.Tests` Release warnings-only build 复核当前 `MA0051` 热点:
基线现为 `22` 条,且已不再落在 `GeneratorSnapshotTest``ContextRegistrationAnalyzerTests`
`ContextGetGeneratorTests`,而是集中在 `CqrsHandlerRegistryGeneratorTests.cs``15` 条)与
`SchemaConfigGeneratorTests.cs``7` 条)
- 已完成 `GFramework.Core` 当前 `MA0016` / `MA0002` / `MA0015` / `MA0077` 低风险收口批次
- 已复核 `net10.0` 下的 `MA0158` 基线:`GFramework.Core` / `GFramework.Cqrs` 当前共有 `16` 个 object lock
建议点,属于跨 target 兼容性风险,不在本轮直接批量替换
- 已完成 `GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs` 的剩余 `MA0051` 结构拆分,生成输出保持不变
- 已完成 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs``MA0051` 结构拆分,生成输出保持不变
- 已完成 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs``MA0006` 低风险收口schema 关键字比较显式使用
`StringComparison.Ordinal`
- 已完成 `SchemaConfigGenerator.cs` 的第一批 `MA0051` 结构拆分schema 入口解析、属性解析、schema 遍历、数组属性解析、
约束文档生成与若干生成代码发射 helper 已拆出语义阶段
- 已完成当前 PR #269 review follow-up`CqrsHandlerRegistryGenerator` 按职责拆分为 partial 生成器文件,
`ContextAwareGenerator` 已补上字段名去冲突与锁内读取修正,`Option<T>` 补齐 `<remarks>` 契约说明
- 已完成当前 PR #269 第二轮 follow-up恢复 `EasyEvents``CollectionExtensions``LoggingConfiguration`
`FilterConfiguration` 的公共 API 兼容形状,并将 analyzer 兼容性处理收敛到局部 pragma
- 已完成当前 PR #269 第三轮 follow-up继续收口 `SchemaConfigGenerator` 的根类型标识符校验与 XML 文档转义,
并补齐 `LoggingConfigurationTests``CollectionExtensionsTests``Cqrs` helper 抽取与 `ai-plan` 命令文本修正
- 已完成当前 PR #269 第四轮 follow-up`CqrsHandlerRegistryGenerator` 的 Roslyn error type 直接引用改为
运行时精确查找路径,并为 `SchemaConfigGenerator` 补上根 `type` 非字符串时的防御与回归测试
- 已完成当前 PR #269 第五轮 follow-up`SchemaConfigGenerator` 补上归一化后属性名冲突诊断并新增
`GF_ConfigSchema_014``CqrsHandlerRegistryGenerator``dynamic` 归一化为 `global::System.Object`
同时收紧相关 generator regression tests
- 已完成当前 PR #269 failed-test follow-up修正 `SchemaConfigGeneratorTests`
`Run_Should_Assign_Globally_Unique_Reference_Metadata_Member_Names` 的测试输入,使其继续覆盖
reference metadata 成员名全局去冲突,但不再依赖现已被 `GF_ConfigSchema_014` 拦截的非法同层 schema key 冲突
- 已完成当前 PR #269 Greptile follow-up`ContextAwareGenerator` 现在会把基类链显式成员名也纳入
`_gFrameworkContextAware*` 字段分配冲突检测,并新增 inherited-field collision 快照回归测试
- 已完成当前分支与 `main``CqrsHandlerRegistryGenerator.cs` 文件级冲突收口:确认 `main` 侧新增的是
`OrderedRegistrationKind` / `RuntimeTypeReferenceSpec` 的 XML 文档,现已按当前 partial 拆分结构迁移到
`CqrsHandlerRegistryGenerator.Models.cs`,不回退已完成的生成器拆分
- 已完成 `SchemaConfigGenerator.cs` 剩余 `MA0051` 收口:将 `dependentRequired` / `allOf` / conditional schema 校验
拆成更小的验证阶段,并将 `GenerateTableClass``GenerateBindingsClass``AppendGeneratedConfigCatalogType`
拆成稳定的代码发射 helper保持生成输出与快照一致
- 已更新 `AGENTS.md`:变更模块必须运行对应 `dotnet build -c Release`,并处理或显式报告模块构建 warning
不再默认留给长期 warning 清理分支
- `CoroutineScheduler` 的 tag/group 字典已显式使用 `StringComparer.Ordinal`,保持既有区分大小写语义
- `EasyEvents.AddEvent<T>()` 的重复注册路径已恢复为 `ArgumentException`,以保持既有异常契约
- `Option<T>` 已声明 `IEquatable<Option<T>>`,与已有强类型 `Equals(Option<T>)` 契约对齐
- 当前 `GFramework.Core` `net8.0` warnings-only 基线已降到 `0`
- 当前 `GFramework.Core.SourceGenerators` warnings-only 基线已降到 `0`
- 当前 `GFramework.Cqrs.SourceGenerators` warnings-only 基线已降到 `0`
- 当前 `GFramework.Game.SourceGenerators` warnings-only 基线已从 `46` 条降到 `0`
- 已完成 `GFramework.SourceGenerators.Tests` 低风险 `MA0004` / `MA0048` 收口:测试辅助器改为直接返回 `Task`
文件 I/O 显式补齐 `ConfigureAwait(false)``AnalyzerTestDriver` 文件名与类型名重新对齐
- 当前 `GFramework.SourceGenerators.Tests` warnings-only 基线已从 `61` 条降到 `49` 条,剩余 warning 均为 `MA0051`
- 已完成 `GFramework.SourceGenerators.Tests/Logging/LoggerGeneratorSnapshotTests.cs``MA0051` 收口:同构 snapshot
场景已收敛为模板化 helper保留原有快照目录与生成器输入语义不变
- 当前 `GFramework.SourceGenerators.Tests` Release build 基线已从 `49` 条降到 `43` 条;`LoggerGeneratorSnapshotTests.cs`
已不再出现在 `MA0051` 列表中
- 已完成 `GFramework.SourceGenerators.Tests/Architectures/AutoRegisterModuleGeneratorTests.cs``MA0051` 收口:
将内联测试源码与期望快照抽到类级常量、补齐测试类 XML 文档,并将仅作转发的异步测试改为直接返回 `Task`
- 当前 `GFramework.SourceGenerators.Tests` Release build 基线已从 `43` 条降到 `40` 条;
`AutoRegisterModuleGeneratorTests.cs` 已不再出现在 `MA0051` 列表中
- 已完成 `GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorSnapshotTests.cs``MA0051` 收口:
将 monster 场景的运行时契约与 schema 输入提取为类级常量,并把生成结果与快照目录解析拆成小 helper保持
生成文件名、快照目录和断言语义不变
- 当前 `GFramework.SourceGenerators.Tests` Release build 基线已从 `40` 条降到 `39` 条;
`SchemaConfigGeneratorSnapshotTests.cs` 已不再出现在 `MA0051` 列表中
- 已完成当前 PR #273 review follow-up 首轮核对:确认本地仍成立的问题集中在
`SchemaConfigGenerator` helper XML 文档、`GeneratorSnapshotTest``StringComparison.Ordinal` /
snapshot 路径空值防御、`AutoRegisterModuleGeneratorTests` 的 XML 文档位置,以及
`SchemaConfigGeneratorSnapshotTests` 的 monster 快照覆盖缺口
- 已将 monster 快照场景扩展到 `dependentRequired``dependentSchemas``allOf` 与 object-focused
`if/then/else`,以便把新增 schema 约束文档纳入 snapshot 验证
- 已完成本轮定向验证:
`GFramework.Game.SourceGenerators` Release build 通过;
`GFramework.SourceGenerators.Tests``-m:1 --no-restore` 下 Release build 通过;
`SchemaConfigGeneratorSnapshotTests``AutoRegisterModuleGeneratorTests` 定向测试共 `4` 项全部通过
- 当前验证仍受环境/基线约束:
`GFramework.SourceGenerators.Tests` Release build 保留既有 `MA0051` warning 基线;
NuGet vulnerability audit 在离线环境下产生 `NU1900`
- `GFramework.Godot``Timing.cs` 已同步适配新事件签名,但当前 worktree 的 Godot restore 资产仍受 Windows fallback package folder 干扰,独立 build 需在修复资产后补跑
- 后续继续按 warning 类型和数量批处理,而不是回退到按单文件切片推进
- 下一轮默认重新抓取 PR #273 最新 review 线程,并确认本轮 snapshot 更新后是否还存在剩余 open thread 或
`dotnet-format` 细项
- 单次 `boot` 的工作树改动上限控制在约 `100` 个文件以内,避免 recovery context 与 review 面同时失控
- 若任务边界互不冲突,允许使用不同模型的 subagent 并行处理不同 warning 类型或不同目录,但必须遵守显式 ownership
## 当前状态摘要
- 已完成 `GFramework.Core``GFramework.Cqrs``GFramework.Godot` 与部分 source generator 的低风险 warning 清理
- 已完成多轮 CodeRabbit follow-up 修复,并用定向测试与项目/解决方案构建验证了关键回归风险
- 已完成当前 PR #265 review follow-up修复 `CoroutineScheduler` 的零容量扩容边界,并补上 `Store` dispatch 作用域的异常安全回滚
- 已继续完成当前 PR #265 review follow-up修复 `Event<T>``Event<T, TK>` 监听器计数的 off-by-one并补充回归测试
- 已增强 `gframework-pr-review` 脚本与 skill 文档,降低超长 JSON 直出导致的 review 信号漏看风险
- 已完成 `GFramework.Core` 当前 `MA0046` 批次:将阶段、协程与异步日志事件统一迁移到 `EventHandler<TEventArgs>` 形状,
并同步更新 `GFramework.Godot` 订阅点、定向测试与 `docs/zh-CN` 示例
- 已完成当前 PR #267 review follow-up修复 `AsyncLogAppender``ILogAppender.Flush()` 双重完成通知,并补齐
`PhaseChanged` / `CoroutineExceptionEventArgs` XML 文档、`PhaseChanged` 迁移说明和 `ai-plan` 基线注释
- 已完成当前 PR #267 failed-test follow-up修复 `AsyncLogAppender.Flush()` 在队列已被后台线程提前清空时仍可能
等待满默认超时并返回 `false` 的竞态,并通过整包 `GFramework.Core.Tests` 重新验证
- 已完成当前 `GFramework.Core` `net8.0` 剩余低风险 analyzer warning 批次warnings-only 基线已降到 `0`
- 已完成 `GFramework.Core.SourceGenerators``ContextAwareGenerator` 的剩余 `MA0051` 收口warnings-only 基线已降到 `0`
- 已完成 `GFramework.Cqrs.SourceGenerators``CqrsHandlerRegistryGenerator` 的剩余 `MA0051` 收口warnings-only 基线已降到 `0`
- 已完成当前 PR #269 的 review follow-up收口 `ContextAwareGenerator` 的字段命名冲突 / 锁内读取契约、
`CqrsHandlerRegistryGenerator` 的运行时类型 null 防御与超大文件拆分、`SchemaConfigGenerator` 的取消语义,
并恢复 `EasyEvents` / `CollectionExtensions` / logging 配置模型的公共 API 兼容形状
- 已完成当前 PR #269 的第四轮 review follow-up确认 5 个 latest-head 未解决线程中仅剩 2 个本地仍成立,
已分别在 `CqrsHandlerRegistryGenerator``SchemaConfigGenerator` 中收口,并补齐定向 generator regression tests
- 已完成当前 PR #269 的第五轮 review follow-up收口 `SchemaConfigGenerator` 的归一化字段名冲突诊断、
`CqrsHandlerRegistryGenerator``dynamic` 类型引用风险,并同步更新 `AGENTS.md` 的模块 build / warning 治理规范
- 已完成当前 PR #269 的 failed-test follow-up将 reference metadata 成员名唯一性回归测试改为合法 schema 路径组合,
并重新通过定向 generator test
- 已完成当前 PR #269 的 Greptile follow-up修复 `ContextAwareGenerator` 未覆盖基类成员名冲突的问题,并补齐
inherited-collision 快照测试
- 已完成当前分支与 `main``CqrsHandlerRegistryGenerator.cs` 冲突化解:保留当前 partial 结构,并把
`main` 侧新增的模型文档合并到 `CqrsHandlerRegistryGenerator.Models.cs`
- 已完成 `GFramework.Game.SourceGenerators``SchemaConfigGenerator` 的剩余 `MA0051` 收口warnings-only 基线已降到 `0`
- 已完成 `GFramework.SourceGenerators.Tests` 的首轮低风险 warning 清理;当前项目已清空 `MA0004` / `MA0048`,剩余 warning
全部收敛为 `MA0051`
- 已完成 `LoggerGeneratorSnapshotTests` 的单文件 `MA0051` 收口;当前 `GFramework.SourceGenerators.Tests` Release build 基线已降到
`43` 条,并通过 focused snapshot tests 保持行为不变
- 已完成 `AutoRegisterModuleGeneratorTests` 的单文件 `MA0051` 收口;当前 `GFramework.SourceGenerators.Tests` Release build 基线已降到
`40` 条,并通过 focused generator tests 保持输出契约不变
- 已完成 `SchemaConfigGeneratorSnapshotTests` 的单文件 `MA0051` 收口;当前 `GFramework.SourceGenerators.Tests`
Release build 基线已降到 `39` 条,并通过 focused snapshot test 保持生成输出契约不变
- 已完成 `RP-035` 启动复核:确认 PR #273 已关闭、历史 open thread 暂无新的本地修复点,且
`GFramework.SourceGenerators.Tests` 当前剩余 `MA0051` 已重排为 `CqrsHandlerRegistryGeneratorTests` /
`SchemaConfigGeneratorTests` 两个测试写集
- 已完成 `RP-036`:清空 `SchemaConfigGeneratorTests.cs` 当前 `MA0051`,并将
`GFramework.SourceGenerators.Tests` Release warnings-only 基线进一步降到 `15`
- 已完成 `RP-037` 的首个 subagent 接收:`CqrsHandlerRegistryGeneratorTests.cs`
`Generates_Assembly_Level_Cqrs_Handler_Registry()` 已抽出类级 fixture当前测试项目基线进一步降到 `14`
- 已完成 `RP-038` 的第二个 subagent 接收:`HiddenNestedHandlerSelfRegistrationSource` 已提取到类级常量,
当前测试项目基线进一步降到 `13`
- 已完成 `RP-039` 的第三个 subagent 接收:`HiddenImplementationDirectInterfaceRegistrationSource`
已提取到类级常量,当前测试项目基线进一步降到 `12`
- 已完成 `RP-040` 的第四个 subagent 接收:`HiddenArrayResponseFallbackSource`
已提取到类级常量,当前测试项目基线进一步降到 `11`
- 已完成 `RP-041` 的第五个 subagent 接收:`HiddenGenericEnvelopeResponseSource`
已提取到类级常量,当前测试项目基线进一步降到 `10`
## 当前活跃事实
- 当前主题仍是 active topic因为剩余结构性 warning 是否继续推进尚未决策
- `RP-001` 的详细实现历史、测试记录和 warning 热点清单已归档到主题内 `archive/`
- `RP-002` 已在不改公共契约的前提下完成 `CqrsHandlerRegistrar` 结构拆分,并通过定向 build/test 验证
- `RP-003` 已在不改生命周期契约的前提下完成 `ArchitectureLifecycle` 初始化主流程拆分,并通过定向 build/test 验证
- `RP-004` 已完成当前 PR review follow-up修复 `TryCreateGeneratedRegistry` 的可空 `out` 契约并清理 trace 文档重复标题
- `RP-005` 已在不改公共 API 的前提下完成 `PauseStackManager` 两个 `MA0051` 的结构拆分,并补充销毁通知回归测试
- `RP-006` 已在不改公共 API 的前提下完成 `Store` 两个 `MA0051` 的结构拆分,并通过定向 build/test 验证 dispatch、
多态 reducer 匹配与历史语义未回归
- `RP-007` 已在不改公共 API 的前提下完成 `CoroutineScheduler` 两个 `MA0051` 的结构拆分,并通过定向 build/test 验证
调度、取消与完成状态语义未回归
- `RP-008` 将后续策略从“单文件 warning 切片”切换为“按类型批处理 + 文件数上限控制”,并允许在非冲突前提下使用
不同模型的 subagent 并行处理
- `RP-009` 在不改公共 API 的前提下,将同名泛型家族收拢到与类型名一致的单文件中,清空当前 `GFramework.Core`
`net8.0` 基线中的 `MA0048`,并通过定向 build/test 验证 `Command``Query``Event` 路径未回归
- `RP-010` 使用 `gframework-pr-review` 复核当前分支 PR #265 后,修复了仍在本地成立的两个 follow-up 风险:
`CoroutineScheduler``initialCapacity: 0` 扩容越界,以及 `Store` 在 dispatch 快照阶段抛异常时可能残留
`_isDispatching = true` 的锁死问题
- `RP-011` 根据补充复核继续收口 PR #265 的 outside-diff comment修复 `Event<T>` / `Event<T, TK>` 默认 no-op
委托导致的 `GetListenerCount()` off-by-one并以定向事件测试验证注册、注销和计数语义
- `RP-012``gframework-pr-review` 增加 `--json-output``--section``--path` 与文本截断能力,并更新 skill 推荐用法,
让“先落盘、再定向抽取”成为默认可操作路径
- `RP-013` 已完成 `GFramework.Core` 当前 `MA0046` 批次,并以新的事件参数类型替换阶段、协程和异步日志事件的
非标准签名;`GFramework.Core` `net8.0` warnings-only 基线由 `15` 降至 `9`
- `RP-014` 使用 `gframework-pr-review` 复核当前分支 PR #267 的 latest head review threads、outside-diff comment 与
nitpick comment 后,确认 8 条高信号项中仍成立的是 1 个行为 bug 与 7 个文档/测试/跟踪缺口,并按最小改动收口
- `RP-015` 使用 `$gframework-pr-review` 复核 PR #267 的 CTRF 失败测试评论后,确认 `AsyncLogAppender` 仍存在
“队列已空但 Flush 仍超时失败”的竞态;该问题在本地整包 `GFramework.Core.Tests` 中可复现,现已修复并补上稳定回归测试
- `RP-016``GFramework.Core` 当前剩余 `MA0016` / `MA0002` / `MA0015` / `MA0077` 低风险批次清零,并用
warnings-only build 与 focused tests 验证配置反序列化、集合扩展、事件重复注册、`Option<T>` 相等性和协程 tag/group 语义
- `RP-017` 复核 `MA0158` 当前仍是跨 target 锁类型迁移问题,因此先收口单点 `ContextAwareGenerator` `MA0051`
并通过 source generator 项目 build 与 `ContextAwareGeneratorSnapshotTests` 验证生成输出未回归
- `RP-018` 暂缓跨 target `MA0158`,转入 `GFramework.Cqrs.SourceGenerators` 的单文件结构性 warning
通过拆分 handler 分析、运行时类型引用构造、注册器源码发射与精确反射注册输出阶段,清空该项目当前 `MA0051`
- `RP-019` 转入 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs`,先完成低风险 `MA0006` 批次;
通过 schema 类型比较 helper 与显式 `StringComparison.Ordinal` 清空当前项目的 `MA0006`
- `RP-020` 继续拆分 `SchemaConfigGenerator.cs``MA0051` 热点,将当前项目 warnings-only 基线从 `19` 条降到 `9` 条,
并用 focused schema generator tests 验证 50 个用例通过
- `RP-032` 已完成 `AutoRegisterModuleGeneratorTests` 的 3 个 `MA0051` 收口:通过提取类级常量承载测试源码与快照,保持
生成文件名、断言路径与源生成输出不变;`GFramework.SourceGenerators.Tests` warnings-only 基线由 `43` 降至 `40`
- `RP-033` 已完成 `SchemaConfigGeneratorSnapshotTests``MA0051` 收口monster schema 运行时契约与 schema 输入已提取为
类级常量,生成结果映射与快照目录解析已拆为小 helper`GFramework.SourceGenerators.Tests` warnings-only 基线由 `40` 降至 `39`
- `RP-035` 已完成启动级恢复核对:当前分支对应的 GitHub PR #273 已关闭,因此 remaining open thread 仅作为历史信号参考;
下一轮应以本地 `warnings-only` build 的实时热点为主,而不是继续按已过时的 `GeneratorSnapshotTest` /
`ContextRegistrationAnalyzerTests` 建议恢复
- `RP-036` 已完成 `SchemaConfigGeneratorTests` 的单文件 `MA0051` 收口:共享 runtime fixture、
generated-source 收集与 catalog 契约断言均已拆出 helper当前测试项目剩余 `MA0051` 已全部收敛到
`CqrsHandlerRegistryGeneratorTests`
- `RP-037` 已验证 subagent 循环开始产生稳定吞吐,但当前一轮只消掉 `1` 个 warning 位点;
若继续按“唯一变更文件数接近 `75`”推进,需要接受很多轮单文件、单方法级切片
- `RP-038` 继续验证了“单方法 + 主线程记录恢复点”的 subagent 节奏可稳定复用,但按当前速度离
“接近 `75` 个唯一变更文件”仍然非常远
- `RP-039` 进一步确认:只要 subagent 继续在同一热点文件内逐点消除 warning唯一变更文件数会基本停留在 `4`
左右,不会因为重复修改同一文件快速逼近 `75`
- `RP-040` 延续了这一趋势:当前吞吐稳定,但按“唯一变更文件数接近 `75`”作为停止条件并不匹配当前单文件收口节奏
- `RP-041` 再次确认当前分支相对 `origin/main` 的唯一变更文件数仍是 `4`;若继续只收口同一文件的 warning
该计数基本不会上涨
- `RP-021` 使用 `$gframework-pr-review` 复核当前分支 PR #269 后,修复仍在本地成立的 4 个项:将
`CqrsHandlerRegistryGenerator` 拆分为职责清晰的 partial 文件、为 `ContextAwareGenerator` 生成字段增加稳定前缀并补上
`SetContextProvider` 的运行时 null 校验、为 `Option<T>` 补齐 `<remarks>`,并新增字段重名场景的生成器快照测试
- `RP-022` 继续复核 PR #269 的 latest-head review threads 与 nitpick确认仍成立的项包括公共 API 兼容回退、
`ContextAwareGenerator` 字段名真正去冲突与锁内读取、`SchemaConfigGenerator` 取消传播、`Cqrs` 运行时类型 null 防御;
已补齐对应回归测试与 focused build/test 验证
- `RP-023` 继续复核 PR #269 剩余 nitpick/outside-diff 项,确认仍成立的项集中在 `SchemaConfigGenerator` 根类型名校验、
aggregate registration comparer XML 文档转义、logging / collection 反射测试补强,以及跟踪文档中的
`RestoreFallbackFolders=""` 可复制性问题
- `RP-024` 使用 `$gframework-pr-review` 继续复核 PR #269 latest-head unresolved threads确认 `EasyEvents` 异常契约、
`SchemaConfigGenerator` 取消传播与 `ContextAwareGenerator` 快照冲突线程均已在本地收口,仅剩 `Cqrs` error type
直接引用与根 schema `type` 非字符串防御仍成立;现已补齐实现与回归测试
- `RP-025` 继续复核 PR #269 剩余 outside-diff / nitpick 信号后,确认本地仍成立的是 `SchemaConfigGenerator`
的归一化字段名冲突与 `Cqrs``dynamic` 的直接类型引用;已分别补上诊断、运行时类型归一化与回归测试,
并把“变更模块必须运行对应 build 且处理 warning”的治理规则写回 `AGENTS.md`
- `RP-029` 已完成 `SchemaConfigGenerator` 剩余 `MA0051` 收口:`GFramework.Game.SourceGenerators` 独立 Release
warnings-only build 已清零,并通过 `SchemaConfigGenerator` focused generator tests 锁定生成输出未回退
- `RP-030` 已完成 `GFramework.SourceGenerators.Tests` 低风险 `MA0004` / `MA0048` 收口:`AnalyzerTestDriver` 文件名已与
类型名一致,测试辅助器与 schema snapshot 断言路径已改为直接返回 `Task` 或显式使用 `ConfigureAwait(false)`
当前测试项目 warnings-only 基线从 `61` 条降到 `49` 条,剩余均为 `MA0051`
- `RP-031` 已完成 `LoggerGeneratorSnapshotTests``MA0051` 收口:重复场景源码改为模板化 helper 生成,
当前 `GFramework.SourceGenerators.Tests` Release build 基线从 `49` 条降到 `43` 条,并通过 focused test 验证 6 个用例全部通过
- 当前工作树分支 `fix/analyzer-warning-reduction-batch` 已在 `ai-plan/public/README.md` 建立 topic 映射
## 当前风险
- analyzer 收口回退风险:后续若继续压 `MA0015` / `MA0016`,容易再次把公共 API 收窄成与既有契约不兼容的形状
- 缓解措施:优先保留既有公共 API并将兼容性例外收敛到局部 pragma继续用反射断言覆盖返回类型、属性类型与异常类型
- 测试宿主稳定性风险:部分 Godot 失败路径在当前 .NET 测试宿主下仍不稳定
- 缓解措施:继续优先使用稳定的 targeted test、项目构建和相邻 smoke test 组合验证
- 多目标框架 warning 解释风险:同一源位置会在多个 target framework 下重复计数
- 缓解措施:继续以唯一源位置和 warning 家族为主要决策依据,而不是只看原始 warning 总数
- net10 专属 warning 风险:`MA0158` 建议使用 `System.Threading.Lock`,但项目多 target 时需要确认兼容边界
- 缓解措施:下一轮先按 target framework 与 API 可用性评估,不直接批量替换共享源码中的 `object` lock
- source generator warning 外溢风险:运行 `GFramework.SourceGenerators.Tests` 会构建相邻 generator/test 项目,并在输出中混入
测试项目自身的结构性 warning 基线
- 缓解措施:继续以被修改项目的独立 warnings-only build 作为主验收,并用 focused generator test 验证行为
- source generator test warning 治理风险:`GFramework.SourceGenerators.Tests` 当前仍有 `43` 条既有 `MA0051` warning
一旦继续进入该写集,就必须把测试项目 warning 一并纳入本轮完成条件
- 缓解措施:已先清空低风险 `MA0004` / `MA0048`,后续继续保持“单 warning family、单测试域”的节奏推进 `MA0051`
- ContextAware 基类命名隐藏风险:若生成器只看当前类型声明成员,派生规则会重新占用基类已声明的
`_gFrameworkContextAware*` 字段名,导致生成成员隐藏继承状态并让快照无法锁定后缀分配行为
- 缓解措施:本轮已改为遍历完整 base-type 链收集保留名,并用 inherited collision 快照用例锁定该行为
- Godot 资产文件环境风险:当前 worktree 的 `GFramework.Godot` restore/build 仍会命中 Windows fallback package folder
- 缓解措施:后续若继续触达 Godot 模块,先用 Linux 侧 restore 资产或 Windows-hosted 构建链刷新该项目,再补跑定向 build
- 并行实现风险:批量收敛时若 subagent 写入边界不清晰,容易引入命名冲突或重复重构
- 缓解措施:只在 warning 类型或目录边界清晰时并行;每个 subagent 必须有独占文件 ownership主代理负责合并验证
## 活跃文档
- 历史跟踪归档:[analyzer-warning-reduction-history-rp001.md](analyzer-warning-reduction-history-rp001.md)
- 历史 trace 归档:[analyzer-warning-reduction-history-rp001.md](../traces/analyzer-warning-reduction-history-rp001.md)
## 验证说明
- `RP-001` 的详细 warning 清理、回归修复与定向验证命令均已迁入主题内历史归档
- `RP-002` 的定向验证结果:
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:UseSharedCompilation=false -p:RestoreFallbackFolders=`
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter FullyQualifiedName~CqrsHandlerRegistrarTests -p:RestoreFallbackFolders=`
- `RP-003` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders= -nologo -clp:Summary;WarningsOnly`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~ArchitectureLifecycleBehaviorTests -p:RestoreFallbackFolders=`
- `RP-004` 的定向验证结果:
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:UseSharedCompilation=false -p:RestoreFallbackFolders=`
- 结果:`0 Warning(s)``0 Error(s)`
- `RP-005` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
- 结果:`27 Warning(s)``0 Error(s)``PauseStackManager.cs` 已不再出现在 `MA0051` 列表中
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~PauseStackManagerTests -p:RestoreFallbackFolders=`
- 结果:`25 Passed``0 Failed`
- `RP-006` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo -clp:"Summary;WarningsOnly"`
- 结果:`25 Warning(s)``0 Error(s)``Store.cs` 已不再出现在 `MA0051` 列表中
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~StoreTests -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo`
- 结果:`30 Passed``0 Failed`
- `RP-007` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo -clp:"Summary;WarningsOnly"`
- 结果:`23 Warning(s)``0 Error(s)``CoroutineScheduler.cs` 已不再出现在 `MA0051` 列表中
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~CoroutineScheduler -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo`
- 结果:`34 Passed``0 Failed`
- `RP-008` 的策略基线:
- 当前 `GFramework.Core` 剩余 warning 分布:`MA0048=8``MA0046=6``MA0016=5``MA0002=2``MA0015=1``MA0077=1`
- 后续批处理规则:优先按类型推进;若当轮主类型数量不足,可顺手吸收其他低冲突类型,不限定于 `MA0015``MA0077`
- `RP-009` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
- 结果:`15 Warning(s)``0 Error(s)`;当前 `GFramework.Core` `net8.0` warnings-only 输出中已不再出现 `MA0048`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~AbstractAsyncCommandTests|FullyQualifiedName~QueryExecutorTests|FullyQualifiedName~AbstractAsyncQueryTests|FullyQualifiedName~EventTests" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`83 Passed``0 Failed`
- `RP-010` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo`
- 结果:`15 Warning(s)``0 Error(s)`;新增修复未引入新的 `GFramework.Core` `net8.0` 构建错误
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~CoroutineSchedulerTests.Run_Should_Grow_From_Zero_Initial_Capacity|FullyQualifiedName~StoreTests.Dispatch_Should_Reset_Dispatching_Flag_When_Snapshot_Creation_Throws" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`2 Passed``0 Failed`
- `RP-011` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo`
- 结果:`15 Warning(s)``0 Error(s)``Event.cs` 的 listener count 修复未引入新的 `GFramework.Core` `net8.0` 构建错误
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~EventTests.EventT_GetListenerCount_Should_Exclude_Placeholder_Handler|FullyQualifiedName~EventTests.EventTTK_GetListenerCount_Should_Exclude_Placeholder_Handler" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`2 Passed``0 Failed`
- `RP-012` 的定向验证结果:
- `python3 -m py_compile .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py`
- 结果:通过;使用 `PYTHONPYCACHEPREFIX=/tmp/codex-pycache` 规避技能目录只读导致的 `__pycache__` 写入限制
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --help`
- 结果:通过;`--json-output``--section``--path``--max-description-length` 已出现在 CLI 帮助中
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo`
- 结果:`0 Warning(s)``0 Error(s)`
- `RP-013` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
- 结果:`9 Warning(s)``0 Error(s)`;相对 `RP-009` / `RP-011` 的 warnings-only 基线 `15 Warning(s)` 已降到 `9 Warning(s)`
当前 `GFramework.Core` `net8.0` 输出中已不再出现 `MA0046`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureLifecycleBehaviorTests|FullyQualifiedName~CoroutineSchedulerTests|FullyQualifiedName~AsyncLogAppenderTests" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`50 Passed``0 Failed`
- `dotnet build GFramework.Godot/GFramework.Godot.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -nologo`
- 结果:失败;当前 worktree 的 Godot restore 资产仍引用 Windows fallback package folder尚未完成独立项目编译验证
- `RP-014` 的定向验证结果:
- `dotnet restore GFramework.Core.Tests/GFramework.Core.Tests.csproj -p:RestoreFallbackFolders="" -nologo`
- 结果通过host Windows `dotnet` 首次验证前补齐了缺失的 `Meziantou.Analyzer 3.0.48`
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo`
- 结果:`9 Warning(s)``0 Error(s)``AsyncLogAppender` 行为修复与 XML / 文档补充未引入新的 `GFramework.Core` `net8.0` 构建错误
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~CoroutineSchedulerTests.Scheduler_Should_Raise_OnCoroutineException_With_EventArgs|FullyQualifiedName~AsyncLogAppenderTests.Flush_Should_Raise_OnFlushCompleted_With_Sender_And_Result|FullyQualifiedName~AsyncLogAppenderTests.ILogAppender_Flush_Should_Raise_OnFlushCompleted_Only_Once|FullyQualifiedName~ArchitectureLifecycleBehaviorTests.InitializeAsync_Should_Raise_PhaseChanged_With_Sender_And_EventArgs" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`4 Passed``0 Failed`
- `RP-015` 的验证结果:
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --disable-build-servers --filter "FullyQualifiedName~AsyncLogAppenderTests"`
- 结果:`15 Passed``0 Failed`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --disable-build-servers`
- 结果:`1607 Passed``0 Failed`
- `RP-016` 的验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -nologo -clp:"Summary;WarningsOnly"`
- 结果:`0 Warning(s)``0 Error(s)`;当前 `GFramework.Core` `net8.0` analyzer baseline 已清零
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~LoggingConfigurationTests|FullyQualifiedName~ConfigurableLoggerFactoryTests|FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~EasyEventsTests|FullyQualifiedName~OptionTests|FullyQualifiedName~CoroutineGroupTests|FullyQualifiedName~CoroutineSchedulerTests" -m:1 -nologo`
- 结果:`112 Passed``0 Failed`;测试构建仍会显示既有 `net10.0` `MA0158` 与 source generator `MA0051` warning
- `RP-017` 的验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net10.0 -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
- 结果:`16 Warning(s)``0 Error(s)`;当前 `MA0158``GFramework.Core` / `GFramework.Cqrs`,本轮只记录基线不批量改锁
- `dotnet build GFramework.Core.SourceGenerators/GFramework.Core.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
- 结果:`0 Warning(s)``0 Error(s)``ContextAwareGenerator.cs` 已不再出现 `MA0051`
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~ContextAwareGeneratorSnapshotTests" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`1 Passed``0 Failed`;测试构建仍显示相邻 source generator 和测试项目的既有 analyzer warning
- `RP-018` 的验证结果:
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
- 结果:`0 Warning(s)``0 Error(s)``CqrsHandlerRegistryGenerator.cs` 当前 `MA0051` 已清零
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~CqrsHandlerRegistryGeneratorTests -m:1 -p:RestoreFallbackFolders= -nologo`
- 结果:`14 Passed``0 Failed`
- 说明:该 test project 构建仍显示 `GFramework.Game.SourceGenerators` 与测试项目中的既有 analyzer warning本轮关注的
`GFramework.Cqrs.SourceGenerators` 独立 build 已清零
- `RP-019` 的验证结果:
- `dotnet restore GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -p:RestoreFallbackFolders= -nologo`
- 结果:通过;刷新 Linux 侧资产以清除 stale Windows fallback package folder
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
- 结果:`19 Warning(s)``0 Error(s)`;当前项目输出已不再出现 `MA0006`,剩余均为 `SchemaConfigGenerator.cs`
`MA0051`
- `dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders= -nologo`
- 结果:通过;刷新 test project 资产以清除 stale Windows fallback package folder
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~SchemaConfigGenerator -m:1 -p:RestoreFallbackFolders= -nologo`
- 结果:`50 Passed``0 Failed`
- 说明:测试项目构建仍显示既有 source generator test analyzer warning不属于本轮写集
- `RP-020` 的验证结果:
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
- 结果:`9 Warning(s)``0 Error(s)`;当前项目剩余 warning 均为 `SchemaConfigGenerator.cs``MA0051`
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~SchemaConfigGenerator -m:1 -p:RestoreFallbackFolders= -nologo`
- 结果:`50 Passed``0 Failed`
- 说明:测试项目构建仍显示既有 source generator test analyzer warning不属于本轮写集
- `RP-021` 的验证结果:
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
- 结果:`0 Warning(s)``0 Error(s)`;拆分后最大单文件已降到 `851` 行,满足仓库 800-1000 行上限
- `dotnet build GFramework.Core.SourceGenerators/GFramework.Core.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
- 结果:`0 Warning(s)``0 Error(s)``ContextAwareGenerator` 的字段命名与 provider 契约修复未引入新的 generator warning
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~ContextAwareGeneratorSnapshotTests|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests" -m:1 -p:RestoreFallbackFolders= -nologo`
- 结果:先并行运行两条 `dotnet test` 时触发共享输出文件锁冲突;改为串行重跑后 `ContextAwareGeneratorSnapshotTests=2 Passed`
`CqrsHandlerRegistryGeneratorTests=14 Passed`
- 说明:失败来自测试宿主并行写入同一 build 输出,不是代码回归;串行重跑后快照新增的字段重名场景和 CQRS 快照均通过
- `RP-022` 的验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:通过;`EasyEvents``CollectionExtensions` 与 logging 配置模型的兼容性回退未引入新的 `net8.0` 构建错误
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -v minimal`
- 结果:通过;`ContainingAssembly` null 防御与发射 helper 精简未引入新的构建错误
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -v minimal`
- 结果:通过;仍保留既有 `9``SchemaConfigGenerator.cs` `MA0051` warning非本轮新增
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~SchemaConfigGeneratorTests|FullyQualifiedName~ContextAwareGeneratorSnapshotTests|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
- 结果:`63 Passed``0 Failed`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~EasyEventsTests|FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~LoggingConfigurationTests|FullyQualifiedName~ConfigurableLoggerFactoryTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
- 结果:`38 Passed``0 Failed`
- `RP-023` 的验证结果:
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -v minimal`
- 结果:通过;仍保留既有 `9``SchemaConfigGenerator.cs` `MA0051` warning未新增新的 generator warning
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -v minimal`
- 结果:通过;并行构建时 `GFramework.SourceGenerators.Common.dll` 复制阶段出现一次 `MSB3026` 重试,随后成功完成
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~SchemaConfigGeneratorTests|FullyQualifiedName~SchemaConfigGeneratorSnapshotTests|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
- 结果:`63 Passed``0 Failed`
- 说明:测试项目构建仍会显示既有 `GFramework.SourceGenerators.Tests` analyzer warning不属于本轮写集
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~LoggingConfigurationTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
- 结果:`27 Passed``0 Failed`
- `RP-029` 的验证结果:
- `dotnet restore GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -p:RestoreFallbackFolders="" -nologo`
- 结果:通过;刷新 Linux 侧 restore 资产以移除 Windows fallback package folder 干扰
- `dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders="" -nologo`
- 结果通过focused test 所属测试项目已同步刷新 Linux 侧 restore 资产
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
- 结果:`0 Warning(s)``0 Error(s)``SchemaConfigGenerator.cs` 剩余 `MA0051` 已清零
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~SchemaConfigGenerator -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`54 Passed``0 Failed`
- 说明:测试项目构建仍显示既有 `GFramework.SourceGenerators.Tests` `MA0048` / `MA0051` / `MA0004` warning不属于本轮
`GFramework.Game.SourceGenerators` 写集
- `RP-030` 的验证结果:
- `dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders="" -nologo`
- 结果:通过;刷新 Linux 侧 restore 资产以移除 Windows fallback package folder 干扰
- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
- 结果:`49 Warning(s)``0 Error(s)`;当前项目已不再出现 `MA0004` / `MA0048`,剩余 warning 全部为 `MA0051`
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~GeneratorSnapshotTestSecurityTests|FullyQualifiedName~SchemaConfigGeneratorSnapshotTests|FullyQualifiedName~SchemaConfigGeneratorEnumTests" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`6 Passed``0 Failed`
- `RP-031` 的验证结果:
- `dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders="" -nologo`
- 结果:通过;刷新 Linux 侧 restore 资产以支持后续串行 build/test 验证
- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --disable-build-servers -m:1 -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:Summary`
- 结果:`43 Warning(s)``0 Error(s)``LoggerGeneratorSnapshotTests.cs` 已不再出现在 `MA0051` 列表中
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-build --disable-build-servers --filter FullyQualifiedName~LoggerGeneratorSnapshotTests -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`6 Passed``0 Failed`
- `RP-033` 的验证结果:
- `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"`
- 结果:`39 Warning(s)``0 Error(s)``SchemaConfigGeneratorSnapshotTests.cs` 已不再出现在 `MA0051` 列表中
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-build --disable-build-servers --filter FullyQualifiedName~SchemaConfigGeneratorSnapshotTests -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`1 Passed``0 Failed`
- `RP-035` 的验证结果:
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json`
- 结果:成功定位当前分支关联的 `PR #273`;状态为 `CLOSED`latest-head review threads 仍显示 `2` 条 open thread
test report 均为通过MegaLinter 仅保留 docstring coverage / `dotnet-format` 历史信号
- `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"`
- 结果:`22 Warning(s)``0 Error(s)`;剩余 `MA0051` 全部集中在 `CqrsHandlerRegistryGeneratorTests.cs`
`SchemaConfigGeneratorTests.cs`
- `RP-036` 的验证结果:
- `dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders="" -nologo`
- 结果:通过;刷新测试依赖输出,规避 `--no-build` 场景下的缺包噪音
- `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"`
- 结果:`15 Warning(s)``0 Error(s)``SchemaConfigGeneratorTests.cs` 已不再出现在 `MA0051` 列表中
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-build --disable-build-servers --filter FullyQualifiedName~SchemaConfigGeneratorTests -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`50 Passed``0 Failed`
- `RP-037` 的验证结果:
- `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"`
- 结果:`14 Warning(s)``0 Error(s)``CqrsHandlerRegistryGeneratorTests.cs` 的行号 `337` 已不再出现在 `MA0051` 列表中
- `RP-038` 的验证结果:
- `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"`
- 结果:`13 Warning(s)``0 Error(s)``CqrsHandlerRegistryGeneratorTests.cs` 的行号 `454` 已不再出现在 `MA0051` 列表中
- `RP-039` 的验证结果:
- `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"`
- 结果:`12 Warning(s)``0 Error(s)``CqrsHandlerRegistryGeneratorTests.cs` 的行号 `536` 已不再出现在 `MA0051` 列表中
- `RP-040` 的验证结果:
- `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"`
- 结果:`11 Warning(s)``0 Error(s)``CqrsHandlerRegistryGeneratorTests.cs` 的行号 `607` 已不再出现在 `MA0051` 列表中
- `RP-041` 的验证结果:
- `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)``CqrsHandlerRegistryGeneratorTests.cs` 的行号 `680` 已不再出现在 `MA0051` 列表中
- active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史
## 下一步
1. 若要继续该主题,先读 active tracking再按需展开历史归档中的 warning 热点与验证记录
2. 下一轮优先继续 `GFramework.SourceGenerators.Tests``MA0051` 收口,并直接进入唯一剩余热点
`CqrsHandlerRegistryGeneratorTests.cs`;但若用户真正关心“唯一变更文件数接近 `75`”,下一轮应改为选择新的文件写集,
而不是继续停留在当前同一文件
3. 若改回推进 `MA0158`,先设计 `net8.0` / `net9.0` / `net10.0` 多 target 条件编译方案,不直接批量替换共享源码中的
`object` lock
4. 若后续继续改动 `GFramework.Godot`,先修复该项目的 Linux 侧 restore 资产,再补跑独立 build
5. 若本主题确认暂缓,可保持当前归档状态,不需要再恢复 `local-plan/`

View File

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

View File

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

View File

@ -2,64 +2,516 @@
## 目标
继续以“直接看构建输出、直接修构建 warning”为原则推进当前分支并保持 active recovery 文档只保留当前真值。
继续以“优先低风险、保持行为兼容”为原则收敛当前仓库的 Meziantou analyzer warnings并在首轮大规模清理完成后
判断剩余结构性 warning 是否值得在下一轮继续推进。
## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-051`
- 当前阶段:`Phase 51`
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-041`
- 当前阶段:`Phase 41`
- 当前焦点:
- `2026-04-24` 本轮已完成 `GFramework.Godot.SourceGenerators.Tests` warning 清理
- 当前主线程切片从生成器实现转到对应测试项目,并已把 `GFramework.Godot.SourceGenerators.Tests``24` 个 warning 降到 `0`
- 当前批次按 `origin/main` merge-base 计算的累计分支 diff 预计为 `23` 个文件,仍低于 `$gframework-batch-boot 75` 的主阈值
- 当前工作树除未跟踪的 `.codex` 目录外,还存在与本批次无关的既有文档 / 跟踪文件修改;提交当前批次时必须只包含本 topic 相关文件
- 已通过第五个有效 subagent 切片完成
`CqrsHandlerRegistryGeneratorTests.cs`
`Generates_Precise_Service_Type_For_Hidden_Generic_Type_Definitions()``MA0051` 收口:
将内联 `source` 文本提取为类级常量,保持既有 expected 常量和断言语义不变
- 当前 `GFramework.SourceGenerators.Tests` Release warnings-only 基线已从 `11` 条降到 `10` 条;
行号 `680` 已从 `MA0051` 输出中消失,剩余热点继续集中在
`CqrsHandlerRegistryGeneratorTests.cs`
- 已通过第四个有效 subagent 切片完成
`CqrsHandlerRegistryGeneratorTests.cs`
`Generates_Precise_Service_Type_For_Hidden_Array_Type_Arguments()``MA0051` 收口:
将内联 `source` 文本提取为类级常量,保持既有 expected 常量和断言语义不变
- 当前 `GFramework.SourceGenerators.Tests` Release warnings-only 基线已从 `12` 条降到 `11` 条;
行号 `607` 已从 `MA0051` 输出中消失,剩余热点继续集中在
`CqrsHandlerRegistryGeneratorTests.cs`
- 已通过第三个有效 subagent 切片完成
`CqrsHandlerRegistryGeneratorTests.cs`
`Generates_Direct_Interface_Registrations_For_Hidden_Implementation_When_Handler_Interface_Is_Public()`
`MA0051` 收口:将内联 `source` 文本提取为类级常量,保持既有 expected 常量和断言语义不变
- 当前 `GFramework.SourceGenerators.Tests` Release warnings-only 基线已从 `13` 条降到 `12` 条;
行号 `536` 已从 `MA0051` 输出中消失,剩余热点继续集中在
`CqrsHandlerRegistryGeneratorTests.cs`
- 已通过第二个有效 subagent 切片完成
`CqrsHandlerRegistryGeneratorTests.cs`
`Generates_Visible_Handlers_And_Self_Registers_Private_Nested_Handler_When_Assembly_Contains_Hidden_Handler()`
`MA0051` 收口:将内联 `source` 文本提取为类级常量,保持既有 expected 常量和断言语义不变
- 当前 `GFramework.SourceGenerators.Tests` Release warnings-only 基线已从 `14` 条降到 `13` 条;
行号 `454` 已从 `MA0051` 输出中消失,剩余热点继续集中在
`CqrsHandlerRegistryGeneratorTests.cs`
- 已通过 subagent 循环的首个可交付切片完成
`GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs`
`Generates_Assembly_Level_Cqrs_Handler_Registry()``MA0051` 收口:
将内联 `source` / `expected` 文本提取为类级常量,保持生成文本、断言语义和文件名不变
- 当前 `GFramework.SourceGenerators.Tests` Release warnings-only 基线已从 `15` 条降到 `14` 条;
行号 `337` 已从 `MA0051` 输出中消失,剩余热点仍全部集中在
`CqrsHandlerRegistryGeneratorTests.cs`
- 已确认当前分支相对 `origin/main` 的唯一变更文件数仍只有 `3`;按该统计口径距离用户要求的
“接近 `75` 个文件变更”仍很远,需要继续多轮切片
- 已完成 `GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs``MA0051` 收口:
将共享 consumer runtime fixture 提取到类级常量,并把生成结果收集与 catalog 契约断言拆成小 helper
保持 schema 文本、断言语义与生成输出契约不变
- 当前 `GFramework.SourceGenerators.Tests` Release warnings-only 基线已从 `22` 条降到 `15` 条;
`SchemaConfigGeneratorTests.cs` 已不再出现在 `MA0051` 列表中,剩余热点全部集中在
`CqrsHandlerRegistryGeneratorTests.cs`
- 已完成 `SchemaConfigGeneratorTests` 定向验证:串行重跑 `50` 个用例全部通过;并确认先前并行 build/test
触发的 `MSB3030` / `CS0006` 属于共享输出竞争噪音,不是代码回归
- 已按 `gframework-boot` 重新恢复当前 worktree确认分支 `fix/analyzer-warning-reduction-batch` 仍映射到
`analyzer-warning-reduction`,且当前不存在 `ai-plan/private/` 私有恢复上下文
- 已重新抓取当前分支关联的 PR #273 review 状态PR 已处于 `CLOSED`latest-head review 仍显示 `2`
CodeRabbit open thread但本地复核后 `GeneratorSnapshotTest` 的 snapshot 路径空值防御已显式改为
`InvalidOperationException` 防御,`SchemaConfigGenerator``dependentSchemas` / `allOf` / conditional helper
也已补齐 XML 文档,当前更像历史线程未随已关闭 PR 一起收敛
- 已重新以 `GFramework.SourceGenerators.Tests` Release warnings-only build 复核当前 `MA0051` 热点:
基线现为 `22` 条,且已不再落在 `GeneratorSnapshotTest``ContextRegistrationAnalyzerTests`
`ContextGetGeneratorTests`,而是集中在 `CqrsHandlerRegistryGeneratorTests.cs``15` 条)与
`SchemaConfigGeneratorTests.cs``7` 条)
- 已完成 `GFramework.Core` 当前 `MA0016` / `MA0002` / `MA0015` / `MA0077` 低风险收口批次
- 已复核 `net10.0` 下的 `MA0158` 基线:`GFramework.Core` / `GFramework.Cqrs` 当前共有 `16` 个 object lock
建议点,属于跨 target 兼容性风险,不在本轮直接批量替换
- 已完成 `GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs` 的剩余 `MA0051` 结构拆分,生成输出保持不变
- 已完成 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs``MA0051` 结构拆分,生成输出保持不变
- 已完成 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs``MA0006` 低风险收口schema 关键字比较显式使用
`StringComparison.Ordinal`
- 已完成 `SchemaConfigGenerator.cs` 的第一批 `MA0051` 结构拆分schema 入口解析、属性解析、schema 遍历、数组属性解析、
约束文档生成与若干生成代码发射 helper 已拆出语义阶段
- 已完成当前 PR #269 review follow-up`CqrsHandlerRegistryGenerator` 按职责拆分为 partial 生成器文件,
`ContextAwareGenerator` 已补上字段名去冲突与锁内读取修正,`Option<T>` 补齐 `<remarks>` 契约说明
- 已完成当前 PR #269 第二轮 follow-up恢复 `EasyEvents``CollectionExtensions``LoggingConfiguration`
`FilterConfiguration` 的公共 API 兼容形状,并将 analyzer 兼容性处理收敛到局部 pragma
- 已完成当前 PR #269 第三轮 follow-up继续收口 `SchemaConfigGenerator` 的根类型标识符校验与 XML 文档转义,
并补齐 `LoggingConfigurationTests``CollectionExtensionsTests``Cqrs` helper 抽取与 `ai-plan` 命令文本修正
- 已完成当前 PR #269 第四轮 follow-up`CqrsHandlerRegistryGenerator` 的 Roslyn error type 直接引用改为
运行时精确查找路径,并为 `SchemaConfigGenerator` 补上根 `type` 非字符串时的防御与回归测试
- 已完成当前 PR #269 第五轮 follow-up`SchemaConfigGenerator` 补上归一化后属性名冲突诊断并新增
`GF_ConfigSchema_014``CqrsHandlerRegistryGenerator``dynamic` 归一化为 `global::System.Object`
同时收紧相关 generator regression tests
- 已完成当前 PR #269 failed-test follow-up修正 `SchemaConfigGeneratorTests`
`Run_Should_Assign_Globally_Unique_Reference_Metadata_Member_Names` 的测试输入,使其继续覆盖
reference metadata 成员名全局去冲突,但不再依赖现已被 `GF_ConfigSchema_014` 拦截的非法同层 schema key 冲突
- 已完成当前 PR #269 Greptile follow-up`ContextAwareGenerator` 现在会把基类链显式成员名也纳入
`_gFrameworkContextAware*` 字段分配冲突检测,并新增 inherited-field collision 快照回归测试
- 已完成当前分支与 `main``CqrsHandlerRegistryGenerator.cs` 文件级冲突收口:确认 `main` 侧新增的是
`OrderedRegistrationKind` / `RuntimeTypeReferenceSpec` 的 XML 文档,现已按当前 partial 拆分结构迁移到
`CqrsHandlerRegistryGenerator.Models.cs`,不回退已完成的生成器拆分
- 已完成 `SchemaConfigGenerator.cs` 剩余 `MA0051` 收口:将 `dependentRequired` / `allOf` / conditional schema 校验
拆成更小的验证阶段,并将 `GenerateTableClass``GenerateBindingsClass``AppendGeneratedConfigCatalogType`
拆成稳定的代码发射 helper保持生成输出与快照一致
- 已更新 `AGENTS.md`:变更模块必须运行对应 `dotnet build -c Release`,并处理或显式报告模块构建 warning
不再默认留给长期 warning 清理分支
- `CoroutineScheduler` 的 tag/group 字典已显式使用 `StringComparer.Ordinal`,保持既有区分大小写语义
- `EasyEvents.AddEvent<T>()` 的重复注册路径已恢复为 `ArgumentException`,以保持既有异常契约
- `Option<T>` 已声明 `IEquatable<Option<T>>`,与已有强类型 `Equals(Option<T>)` 契约对齐
- 当前 `GFramework.Core` `net8.0` warnings-only 基线已降到 `0`
- 当前 `GFramework.Core.SourceGenerators` warnings-only 基线已降到 `0`
- 当前 `GFramework.Cqrs.SourceGenerators` warnings-only 基线已降到 `0`
- 当前 `GFramework.Game.SourceGenerators` warnings-only 基线已从 `46` 条降到 `0`
- 已完成 `GFramework.SourceGenerators.Tests` 低风险 `MA0004` / `MA0048` 收口:测试辅助器改为直接返回 `Task`
文件 I/O 显式补齐 `ConfigureAwait(false)``AnalyzerTestDriver` 文件名与类型名重新对齐
- 当前 `GFramework.SourceGenerators.Tests` warnings-only 基线已从 `61` 条降到 `49` 条,剩余 warning 均为 `MA0051`
- 已完成 `GFramework.SourceGenerators.Tests/Logging/LoggerGeneratorSnapshotTests.cs``MA0051` 收口:同构 snapshot
场景已收敛为模板化 helper保留原有快照目录与生成器输入语义不变
- 当前 `GFramework.SourceGenerators.Tests` Release build 基线已从 `49` 条降到 `43` 条;`LoggerGeneratorSnapshotTests.cs`
已不再出现在 `MA0051` 列表中
- 已完成 `GFramework.SourceGenerators.Tests/Architectures/AutoRegisterModuleGeneratorTests.cs``MA0051` 收口:
将内联测试源码与期望快照抽到类级常量、补齐测试类 XML 文档,并将仅作转发的异步测试改为直接返回 `Task`
- 当前 `GFramework.SourceGenerators.Tests` Release build 基线已从 `43` 条降到 `40` 条;
`AutoRegisterModuleGeneratorTests.cs` 已不再出现在 `MA0051` 列表中
- 已完成 `GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorSnapshotTests.cs``MA0051` 收口:
将 monster 场景的运行时契约与 schema 输入提取为类级常量,并把生成结果与快照目录解析拆成小 helper保持
生成文件名、快照目录和断言语义不变
- 当前 `GFramework.SourceGenerators.Tests` Release build 基线已从 `40` 条降到 `39` 条;
`SchemaConfigGeneratorSnapshotTests.cs` 已不再出现在 `MA0051` 列表中
- 已完成当前 PR #273 review follow-up 首轮核对:确认本地仍成立的问题集中在
`SchemaConfigGenerator` helper XML 文档、`GeneratorSnapshotTest``StringComparison.Ordinal` /
snapshot 路径空值防御、`AutoRegisterModuleGeneratorTests` 的 XML 文档位置,以及
`SchemaConfigGeneratorSnapshotTests` 的 monster 快照覆盖缺口
- 已将 monster 快照场景扩展到 `dependentRequired``dependentSchemas``allOf` 与 object-focused
`if/then/else`,以便把新增 schema 约束文档纳入 snapshot 验证
- 已完成本轮定向验证:
`GFramework.Game.SourceGenerators` Release build 通过;
`GFramework.SourceGenerators.Tests``-m:1 --no-restore` 下 Release build 通过;
`SchemaConfigGeneratorSnapshotTests``AutoRegisterModuleGeneratorTests` 定向测试共 `4` 项全部通过
- 当前验证仍受环境/基线约束:
`GFramework.SourceGenerators.Tests` Release build 保留既有 `MA0051` warning 基线;
NuGet vulnerability audit 在离线环境下产生 `NU1900`
- `GFramework.Godot``Timing.cs` 已同步适配新事件签名,但当前 worktree 的 Godot restore 资产仍受 Windows fallback package folder 干扰,独立 build 需在修复资产后补跑
- 后续继续按 warning 类型和数量批处理,而不是回退到按单文件切片推进
- 下一轮默认重新抓取 PR #273 最新 review 线程,并确认本轮 snapshot 更新后是否还存在剩余 open thread 或
`dotnet-format` 细项
- 单次 `boot` 的工作树改动上限控制在约 `100` 个文件以内,避免 recovery context 与 review 面同时失控
- 若任务边界互不冲突,允许使用不同模型的 subagent 并行处理不同 warning 类型或不同目录,但必须遵守显式 ownership
## 当前状态摘要
- 已完成 `GFramework.Core``GFramework.Cqrs``GFramework.Godot` 与部分 source generator 的低风险 warning 清理
- 已完成多轮 CodeRabbit follow-up 修复,并用定向测试与项目/解决方案构建验证了关键回归风险
- 已完成当前 PR #265 review follow-up修复 `CoroutineScheduler` 的零容量扩容边界,并补上 `Store` dispatch 作用域的异常安全回滚
- 已继续完成当前 PR #265 review follow-up修复 `Event<T>``Event<T, TK>` 监听器计数的 off-by-one并补充回归测试
- 已增强 `gframework-pr-review` 脚本与 skill 文档,降低超长 JSON 直出导致的 review 信号漏看风险
- 已完成 `GFramework.Core` 当前 `MA0046` 批次:将阶段、协程与异步日志事件统一迁移到 `EventHandler<TEventArgs>` 形状,
并同步更新 `GFramework.Godot` 订阅点、定向测试与 `docs/zh-CN` 示例
- 已完成当前 PR #267 review follow-up修复 `AsyncLogAppender``ILogAppender.Flush()` 双重完成通知,并补齐
`PhaseChanged` / `CoroutineExceptionEventArgs` XML 文档、`PhaseChanged` 迁移说明和 `ai-plan` 基线注释
- 已完成当前 PR #267 failed-test follow-up修复 `AsyncLogAppender.Flush()` 在队列已被后台线程提前清空时仍可能
等待满默认超时并返回 `false` 的竞态,并通过整包 `GFramework.Core.Tests` 重新验证
- 已完成当前 `GFramework.Core` `net8.0` 剩余低风险 analyzer warning 批次warnings-only 基线已降到 `0`
- 已完成 `GFramework.Core.SourceGenerators``ContextAwareGenerator` 的剩余 `MA0051` 收口warnings-only 基线已降到 `0`
- 已完成 `GFramework.Cqrs.SourceGenerators``CqrsHandlerRegistryGenerator` 的剩余 `MA0051` 收口warnings-only 基线已降到 `0`
- 已完成当前 PR #269 的 review follow-up收口 `ContextAwareGenerator` 的字段命名冲突 / 锁内读取契约、
`CqrsHandlerRegistryGenerator` 的运行时类型 null 防御与超大文件拆分、`SchemaConfigGenerator` 的取消语义,
并恢复 `EasyEvents` / `CollectionExtensions` / logging 配置模型的公共 API 兼容形状
- 已完成当前 PR #269 的第四轮 review follow-up确认 5 个 latest-head 未解决线程中仅剩 2 个本地仍成立,
已分别在 `CqrsHandlerRegistryGenerator``SchemaConfigGenerator` 中收口,并补齐定向 generator regression tests
- 已完成当前 PR #269 的第五轮 review follow-up收口 `SchemaConfigGenerator` 的归一化字段名冲突诊断、
`CqrsHandlerRegistryGenerator``dynamic` 类型引用风险,并同步更新 `AGENTS.md` 的模块 build / warning 治理规范
- 已完成当前 PR #269 的 failed-test follow-up将 reference metadata 成员名唯一性回归测试改为合法 schema 路径组合,
并重新通过定向 generator test
- 已完成当前 PR #269 的 Greptile follow-up修复 `ContextAwareGenerator` 未覆盖基类成员名冲突的问题,并补齐
inherited-collision 快照测试
- 已完成当前分支与 `main``CqrsHandlerRegistryGenerator.cs` 冲突化解:保留当前 partial 结构,并把
`main` 侧新增的模型文档合并到 `CqrsHandlerRegistryGenerator.Models.cs`
- 已完成 `GFramework.Game.SourceGenerators``SchemaConfigGenerator` 的剩余 `MA0051` 收口warnings-only 基线已降到 `0`
- 已完成 `GFramework.SourceGenerators.Tests` 的首轮低风险 warning 清理;当前项目已清空 `MA0004` / `MA0048`,剩余 warning
全部收敛为 `MA0051`
- 已完成 `LoggerGeneratorSnapshotTests` 的单文件 `MA0051` 收口;当前 `GFramework.SourceGenerators.Tests` Release build 基线已降到
`43` 条,并通过 focused snapshot tests 保持行为不变
- 已完成 `AutoRegisterModuleGeneratorTests` 的单文件 `MA0051` 收口;当前 `GFramework.SourceGenerators.Tests` Release build 基线已降到
`40` 条,并通过 focused generator tests 保持输出契约不变
- 已完成 `SchemaConfigGeneratorSnapshotTests` 的单文件 `MA0051` 收口;当前 `GFramework.SourceGenerators.Tests`
Release build 基线已降到 `39` 条,并通过 focused snapshot test 保持生成输出契约不变
- 已完成 `RP-035` 启动复核:确认 PR #273 已关闭、历史 open thread 暂无新的本地修复点,且
`GFramework.SourceGenerators.Tests` 当前剩余 `MA0051` 已重排为 `CqrsHandlerRegistryGeneratorTests` /
`SchemaConfigGeneratorTests` 两个测试写集
- 已完成 `RP-036`:清空 `SchemaConfigGeneratorTests.cs` 当前 `MA0051`,并将
`GFramework.SourceGenerators.Tests` Release warnings-only 基线进一步降到 `15`
- 已完成 `RP-037` 的首个 subagent 接收:`CqrsHandlerRegistryGeneratorTests.cs`
`Generates_Assembly_Level_Cqrs_Handler_Registry()` 已抽出类级 fixture当前测试项目基线进一步降到 `14`
- 已完成 `RP-038` 的第二个 subagent 接收:`HiddenNestedHandlerSelfRegistrationSource` 已提取到类级常量,
当前测试项目基线进一步降到 `13`
- 已完成 `RP-039` 的第三个 subagent 接收:`HiddenImplementationDirectInterfaceRegistrationSource`
已提取到类级常量,当前测试项目基线进一步降到 `12`
- 已完成 `RP-040` 的第四个 subagent 接收:`HiddenArrayResponseFallbackSource`
已提取到类级常量,当前测试项目基线进一步降到 `11`
- 已完成 `RP-041` 的第五个 subagent 接收:`HiddenGenericEnvelopeResponseSource`
已提取到类级常量,当前测试项目基线进一步降到 `10`
## 当前活跃事实
- 之前记录的 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`
- 当前主题仍是 active topic因为剩余结构性 warning 是否继续推进尚未决策
- `RP-001` 的详细实现历史、测试记录和 warning 热点清单已归档到主题内 `archive/`
- `RP-002` 已在不改公共契约的前提下完成 `CqrsHandlerRegistrar` 结构拆分,并通过定向 build/test 验证
- `RP-003` 已在不改生命周期契约的前提下完成 `ArchitectureLifecycle` 初始化主流程拆分,并通过定向 build/test 验证
- `RP-004` 已完成当前 PR review follow-up修复 `TryCreateGeneratedRegistry` 的可空 `out` 契约并清理 trace 文档重复标题
- `RP-005` 已在不改公共 API 的前提下完成 `PauseStackManager` 两个 `MA0051` 的结构拆分,并补充销毁通知回归测试
- `RP-006` 已在不改公共 API 的前提下完成 `Store` 两个 `MA0051` 的结构拆分,并通过定向 build/test 验证 dispatch、
多态 reducer 匹配与历史语义未回归
- `RP-007` 已在不改公共 API 的前提下完成 `CoroutineScheduler` 两个 `MA0051` 的结构拆分,并通过定向 build/test 验证
调度、取消与完成状态语义未回归
- `RP-008` 将后续策略从“单文件 warning 切片”切换为“按类型批处理 + 文件数上限控制”,并允许在非冲突前提下使用
不同模型的 subagent 并行处理
- `RP-009` 在不改公共 API 的前提下,将同名泛型家族收拢到与类型名一致的单文件中,清空当前 `GFramework.Core`
`net8.0` 基线中的 `MA0048`,并通过定向 build/test 验证 `Command``Query``Event` 路径未回归
- `RP-010` 使用 `gframework-pr-review` 复核当前分支 PR #265 后,修复了仍在本地成立的两个 follow-up 风险:
`CoroutineScheduler``initialCapacity: 0` 扩容越界,以及 `Store` 在 dispatch 快照阶段抛异常时可能残留
`_isDispatching = true` 的锁死问题
- `RP-011` 根据补充复核继续收口 PR #265 的 outside-diff comment修复 `Event<T>` / `Event<T, TK>` 默认 no-op
委托导致的 `GetListenerCount()` off-by-one并以定向事件测试验证注册、注销和计数语义
- `RP-012``gframework-pr-review` 增加 `--json-output``--section``--path` 与文本截断能力,并更新 skill 推荐用法,
让“先落盘、再定向抽取”成为默认可操作路径
- `RP-013` 已完成 `GFramework.Core` 当前 `MA0046` 批次,并以新的事件参数类型替换阶段、协程和异步日志事件的
非标准签名;`GFramework.Core` `net8.0` warnings-only 基线由 `15` 降至 `9`
- `RP-014` 使用 `gframework-pr-review` 复核当前分支 PR #267 的 latest head review threads、outside-diff comment 与
nitpick comment 后,确认 8 条高信号项中仍成立的是 1 个行为 bug 与 7 个文档/测试/跟踪缺口,并按最小改动收口
- `RP-015` 使用 `$gframework-pr-review` 复核 PR #267 的 CTRF 失败测试评论后,确认 `AsyncLogAppender` 仍存在
“队列已空但 Flush 仍超时失败”的竞态;该问题在本地整包 `GFramework.Core.Tests` 中可复现,现已修复并补上稳定回归测试
- `RP-016``GFramework.Core` 当前剩余 `MA0016` / `MA0002` / `MA0015` / `MA0077` 低风险批次清零,并用
warnings-only build 与 focused tests 验证配置反序列化、集合扩展、事件重复注册、`Option<T>` 相等性和协程 tag/group 语义
- `RP-017` 复核 `MA0158` 当前仍是跨 target 锁类型迁移问题,因此先收口单点 `ContextAwareGenerator` `MA0051`
并通过 source generator 项目 build 与 `ContextAwareGeneratorSnapshotTests` 验证生成输出未回归
- `RP-018` 暂缓跨 target `MA0158`,转入 `GFramework.Cqrs.SourceGenerators` 的单文件结构性 warning
通过拆分 handler 分析、运行时类型引用构造、注册器源码发射与精确反射注册输出阶段,清空该项目当前 `MA0051`
- `RP-019` 转入 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs`,先完成低风险 `MA0006` 批次;
通过 schema 类型比较 helper 与显式 `StringComparison.Ordinal` 清空当前项目的 `MA0006`
- `RP-020` 继续拆分 `SchemaConfigGenerator.cs``MA0051` 热点,将当前项目 warnings-only 基线从 `19` 条降到 `9` 条,
并用 focused schema generator tests 验证 50 个用例通过
- `RP-032` 已完成 `AutoRegisterModuleGeneratorTests` 的 3 个 `MA0051` 收口:通过提取类级常量承载测试源码与快照,保持
生成文件名、断言路径与源生成输出不变;`GFramework.SourceGenerators.Tests` warnings-only 基线由 `43` 降至 `40`
- `RP-033` 已完成 `SchemaConfigGeneratorSnapshotTests``MA0051` 收口monster schema 运行时契约与 schema 输入已提取为
类级常量,生成结果映射与快照目录解析已拆为小 helper`GFramework.SourceGenerators.Tests` warnings-only 基线由 `40` 降至 `39`
- `RP-035` 已完成启动级恢复核对:当前分支对应的 GitHub PR #273 已关闭,因此 remaining open thread 仅作为历史信号参考;
下一轮应以本地 `warnings-only` build 的实时热点为主,而不是继续按已过时的 `GeneratorSnapshotTest` /
`ContextRegistrationAnalyzerTests` 建议恢复
- `RP-036` 已完成 `SchemaConfigGeneratorTests` 的单文件 `MA0051` 收口:共享 runtime fixture、
generated-source 收集与 catalog 契约断言均已拆出 helper当前测试项目剩余 `MA0051` 已全部收敛到
`CqrsHandlerRegistryGeneratorTests`
- `RP-037` 已验证 subagent 循环开始产生稳定吞吐,但当前一轮只消掉 `1` 个 warning 位点;
若继续按“唯一变更文件数接近 `75`”推进,需要接受很多轮单文件、单方法级切片
- `RP-038` 继续验证了“单方法 + 主线程记录恢复点”的 subagent 节奏可稳定复用,但按当前速度离
“接近 `75` 个唯一变更文件”仍然非常远
- `RP-039` 进一步确认:只要 subagent 继续在同一热点文件内逐点消除 warning唯一变更文件数会基本停留在 `4`
左右,不会因为重复修改同一文件快速逼近 `75`
- `RP-040` 延续了这一趋势:当前吞吐稳定,但按“唯一变更文件数接近 `75`”作为停止条件并不匹配当前单文件收口节奏
- `RP-041` 再次确认当前分支相对 `origin/main` 的唯一变更文件数仍是 `4`;若继续只收口同一文件的 warning
该计数基本不会上涨
- `RP-021` 使用 `$gframework-pr-review` 复核当前分支 PR #269 后,修复仍在本地成立的 4 个项:将
`CqrsHandlerRegistryGenerator` 拆分为职责清晰的 partial 文件、为 `ContextAwareGenerator` 生成字段增加稳定前缀并补上
`SetContextProvider` 的运行时 null 校验、为 `Option<T>` 补齐 `<remarks>`,并新增字段重名场景的生成器快照测试
- `RP-022` 继续复核 PR #269 的 latest-head review threads 与 nitpick确认仍成立的项包括公共 API 兼容回退、
`ContextAwareGenerator` 字段名真正去冲突与锁内读取、`SchemaConfigGenerator` 取消传播、`Cqrs` 运行时类型 null 防御;
已补齐对应回归测试与 focused build/test 验证
- `RP-023` 继续复核 PR #269 剩余 nitpick/outside-diff 项,确认仍成立的项集中在 `SchemaConfigGenerator` 根类型名校验、
aggregate registration comparer XML 文档转义、logging / collection 反射测试补强,以及跟踪文档中的
`RestoreFallbackFolders=""` 可复制性问题
- `RP-024` 使用 `$gframework-pr-review` 继续复核 PR #269 latest-head unresolved threads确认 `EasyEvents` 异常契约、
`SchemaConfigGenerator` 取消传播与 `ContextAwareGenerator` 快照冲突线程均已在本地收口,仅剩 `Cqrs` error type
直接引用与根 schema `type` 非字符串防御仍成立;现已补齐实现与回归测试
- `RP-025` 继续复核 PR #269 剩余 outside-diff / nitpick 信号后,确认本地仍成立的是 `SchemaConfigGenerator`
的归一化字段名冲突与 `Cqrs``dynamic` 的直接类型引用;已分别补上诊断、运行时类型归一化与回归测试,
并把“变更模块必须运行对应 build 且处理 warning”的治理规则写回 `AGENTS.md`
- `RP-029` 已完成 `SchemaConfigGenerator` 剩余 `MA0051` 收口:`GFramework.Game.SourceGenerators` 独立 Release
warnings-only build 已清零,并通过 `SchemaConfigGenerator` focused generator tests 锁定生成输出未回退
- `RP-030` 已完成 `GFramework.SourceGenerators.Tests` 低风险 `MA0004` / `MA0048` 收口:`AnalyzerTestDriver` 文件名已与
类型名一致,测试辅助器与 schema snapshot 断言路径已改为直接返回 `Task` 或显式使用 `ConfigureAwait(false)`
当前测试项目 warnings-only 基线从 `61` 条降到 `49` 条,剩余均为 `MA0051`
- `RP-031` 已完成 `LoggerGeneratorSnapshotTests``MA0051` 收口:重复场景源码改为模板化 helper 生成,
当前 `GFramework.SourceGenerators.Tests` Release build 基线从 `49` 条降到 `43` 条,并通过 focused test 验证 6 个用例全部通过
- 当前工作树分支 `fix/analyzer-warning-reduction-batch` 已在 `ai-plan/public/README.md` 建立 topic 映射
## 当前风险
- 如果后续继续依赖增量 `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 变更
- analyzer 收口回退风险:后续若继续压 `MA0015` / `MA0016`,容易再次把公共 API 收窄成与既有契约不兼容的形状
- 缓解措施:优先保留既有公共 API并将兼容性例外收敛到局部 pragma继续用反射断言覆盖返回类型、属性类型与异常类型
- 测试宿主稳定性风险:部分 Godot 失败路径在当前 .NET 测试宿主下仍不稳定
- 缓解措施:继续优先使用稳定的 targeted test、项目构建和相邻 smoke test 组合验证
- 多目标框架 warning 解释风险:同一源位置会在多个 target framework 下重复计数
- 缓解措施:继续以唯一源位置和 warning 家族为主要决策依据,而不是只看原始 warning 总数
- net10 专属 warning 风险:`MA0158` 建议使用 `System.Threading.Lock`,但项目多 target 时需要确认兼容边界
- 缓解措施:下一轮先按 target framework 与 API 可用性评估,不直接批量替换共享源码中的 `object` lock
- source generator warning 外溢风险:运行 `GFramework.SourceGenerators.Tests` 会构建相邻 generator/test 项目,并在输出中混入
测试项目自身的结构性 warning 基线
- 缓解措施:继续以被修改项目的独立 warnings-only build 作为主验收,并用 focused generator test 验证行为
- source generator test warning 治理风险:`GFramework.SourceGenerators.Tests` 当前仍有 `43` 条既有 `MA0051` warning
一旦继续进入该写集,就必须把测试项目 warning 一并纳入本轮完成条件
- 缓解措施:已先清空低风险 `MA0004` / `MA0048`,后续继续保持“单 warning family、单测试域”的节奏推进 `MA0051`
- ContextAware 基类命名隐藏风险:若生成器只看当前类型声明成员,派生规则会重新占用基类已声明的
`_gFrameworkContextAware*` 字段名,导致生成成员隐藏继承状态并让快照无法锁定后缀分配行为
- 缓解措施:本轮已改为遍历完整 base-type 链收集保留名,并用 inherited collision 快照用例锁定该行为
- Godot 资产文件环境风险:当前 worktree 的 `GFramework.Godot` restore/build 仍会命中 Windows fallback package folder
- 缓解措施:后续若继续触达 Godot 模块,先用 Linux 侧 restore 资产或 Windows-hosted 构建链刷新该项目,再补跑定向 build
- 并行实现风险:批量收敛时若 subagent 写入边界不清晰,容易引入命名冲突或重复重构
- 缓解措施:只在 warning 类型或目录边界清晰时并行;每个 subagent 必须有独占文件 ownership主代理负责合并验证
## 活跃文档
- 当前轮次归档:
- [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)
- 历史跟踪归档:[analyzer-warning-reduction-history-rp001.md](../archive/todos/analyzer-warning-reduction-history-rp001.md)
- 历史 trace 归档:[analyzer-warning-reduction-history-rp001.md](../archive/traces/analyzer-warning-reduction-history-rp001.md)
## 验证说明
- `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`
- `RP-001` 的详细 warning 清理、回归修复与定向验证命令均已迁入主题内历史归档
- `RP-002` 的定向验证结果:
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:UseSharedCompilation=false -p:RestoreFallbackFolders=`
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter FullyQualifiedName~CqrsHandlerRegistrarTests -p:RestoreFallbackFolders=`
- `RP-003` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders= -nologo -clp:Summary;WarningsOnly`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~ArchitectureLifecycleBehaviorTests -p:RestoreFallbackFolders=`
- `RP-004` 的定向验证结果:
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:UseSharedCompilation=false -p:RestoreFallbackFolders=`
- 结果:`0 Warning(s)``0 Error(s)`
- `RP-005` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
- 结果:`27 Warning(s)``0 Error(s)``PauseStackManager.cs` 已不再出现在 `MA0051` 列表中
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~PauseStackManagerTests -p:RestoreFallbackFolders=`
- 结果:`25 Passed``0 Failed`
- `RP-006` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo -clp:"Summary;WarningsOnly"`
- 结果:`25 Warning(s)``0 Error(s)``Store.cs` 已不再出现在 `MA0051` 列表中
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~StoreTests -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo`
- 结果:`30 Passed``0 Failed`
- `RP-007` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo -clp:"Summary;WarningsOnly"`
- 结果:`23 Warning(s)``0 Error(s)``CoroutineScheduler.cs` 已不再出现在 `MA0051` 列表中
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~CoroutineScheduler -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo`
- 结果:`34 Passed``0 Failed`
- `RP-008` 的策略基线:
- 当前 `GFramework.Core` 剩余 warning 分布:`MA0048=8``MA0046=6``MA0016=5``MA0002=2``MA0015=1``MA0077=1`
- 后续批处理规则:优先按类型推进;若当轮主类型数量不足,可顺手吸收其他低冲突类型,不限定于 `MA0015``MA0077`
- `RP-009` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
- 结果:`15 Warning(s)``0 Error(s)`;当前 `GFramework.Core` `net8.0` warnings-only 输出中已不再出现 `MA0048`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~AbstractAsyncCommandTests|FullyQualifiedName~QueryExecutorTests|FullyQualifiedName~AbstractAsyncQueryTests|FullyQualifiedName~EventTests" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`83 Passed``0 Failed`
- `RP-010` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo`
- 结果:`15 Warning(s)``0 Error(s)`;新增修复未引入新的 `GFramework.Core` `net8.0` 构建错误
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~CoroutineSchedulerTests.Run_Should_Grow_From_Zero_Initial_Capacity|FullyQualifiedName~StoreTests.Dispatch_Should_Reset_Dispatching_Flag_When_Snapshot_Creation_Throws" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`2 Passed``0 Failed`
- `RP-011` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo`
- 结果:`15 Warning(s)``0 Error(s)``Event.cs` 的 listener count 修复未引入新的 `GFramework.Core` `net8.0` 构建错误
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~EventTests.EventT_GetListenerCount_Should_Exclude_Placeholder_Handler|FullyQualifiedName~EventTests.EventTTK_GetListenerCount_Should_Exclude_Placeholder_Handler" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`2 Passed``0 Failed`
- `RP-012` 的定向验证结果:
- `python3 -m py_compile .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py`
- 结果:通过;使用 `PYTHONPYCACHEPREFIX=/tmp/codex-pycache` 规避技能目录只读导致的 `__pycache__` 写入限制
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --help`
- 结果:通过;`--json-output``--section``--path``--max-description-length` 已出现在 CLI 帮助中
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo`
- 结果:`0 Warning(s)``0 Error(s)`
- `RP-013` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
- 结果:`9 Warning(s)``0 Error(s)`;相对 `RP-009` / `RP-011` 的 warnings-only 基线 `15 Warning(s)` 已降到 `9 Warning(s)`
当前 `GFramework.Core` `net8.0` 输出中已不再出现 `MA0046`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureLifecycleBehaviorTests|FullyQualifiedName~CoroutineSchedulerTests|FullyQualifiedName~AsyncLogAppenderTests" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`50 Passed``0 Failed`
- `dotnet build GFramework.Godot/GFramework.Godot.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -nologo`
- 结果:失败;当前 worktree 的 Godot restore 资产仍引用 Windows fallback package folder尚未完成独立项目编译验证
- `RP-014` 的定向验证结果:
- `dotnet restore GFramework.Core.Tests/GFramework.Core.Tests.csproj -p:RestoreFallbackFolders="" -nologo`
- 结果通过host Windows `dotnet` 首次验证前补齐了缺失的 `Meziantou.Analyzer 3.0.48`
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo`
- 结果:`9 Warning(s)``0 Error(s)``AsyncLogAppender` 行为修复与 XML / 文档补充未引入新的 `GFramework.Core` `net8.0` 构建错误
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~CoroutineSchedulerTests.Scheduler_Should_Raise_OnCoroutineException_With_EventArgs|FullyQualifiedName~AsyncLogAppenderTests.Flush_Should_Raise_OnFlushCompleted_With_Sender_And_Result|FullyQualifiedName~AsyncLogAppenderTests.ILogAppender_Flush_Should_Raise_OnFlushCompleted_Only_Once|FullyQualifiedName~ArchitectureLifecycleBehaviorTests.InitializeAsync_Should_Raise_PhaseChanged_With_Sender_And_EventArgs" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`4 Passed``0 Failed`
- `RP-015` 的验证结果:
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --disable-build-servers --filter "FullyQualifiedName~AsyncLogAppenderTests"`
- 结果:`15 Passed``0 Failed`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --disable-build-servers`
- 结果:`1607 Passed``0 Failed`
- `RP-016` 的验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -nologo -clp:"Summary;WarningsOnly"`
- 结果:`0 Warning(s)``0 Error(s)`;当前 `GFramework.Core` `net8.0` analyzer baseline 已清零
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~LoggingConfigurationTests|FullyQualifiedName~ConfigurableLoggerFactoryTests|FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~EasyEventsTests|FullyQualifiedName~OptionTests|FullyQualifiedName~CoroutineGroupTests|FullyQualifiedName~CoroutineSchedulerTests" -m:1 -nologo`
- 结果:`112 Passed``0 Failed`;测试构建仍会显示既有 `net10.0` `MA0158` 与 source generator `MA0051` warning
- `RP-017` 的验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net10.0 -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
- 结果:`16 Warning(s)``0 Error(s)`;当前 `MA0158``GFramework.Core` / `GFramework.Cqrs`,本轮只记录基线不批量改锁
- `dotnet build GFramework.Core.SourceGenerators/GFramework.Core.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
- 结果:`0 Warning(s)``0 Error(s)``ContextAwareGenerator.cs` 已不再出现 `MA0051`
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~ContextAwareGeneratorSnapshotTests" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`1 Passed``0 Failed`;测试构建仍显示相邻 source generator 和测试项目的既有 analyzer warning
- `RP-018` 的验证结果:
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
- 结果:`0 Warning(s)``0 Error(s)``CqrsHandlerRegistryGenerator.cs` 当前 `MA0051` 已清零
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~CqrsHandlerRegistryGeneratorTests -m:1 -p:RestoreFallbackFolders= -nologo`
- 结果:`14 Passed``0 Failed`
- 说明:该 test project 构建仍显示 `GFramework.Game.SourceGenerators` 与测试项目中的既有 analyzer warning本轮关注的
`GFramework.Cqrs.SourceGenerators` 独立 build 已清零
- `RP-019` 的验证结果:
- `dotnet restore GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -p:RestoreFallbackFolders= -nologo`
- 结果:通过;刷新 Linux 侧资产以清除 stale Windows fallback package folder
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
- 结果:`19 Warning(s)``0 Error(s)`;当前项目输出已不再出现 `MA0006`,剩余均为 `SchemaConfigGenerator.cs`
`MA0051`
- `dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders= -nologo`
- 结果:通过;刷新 test project 资产以清除 stale Windows fallback package folder
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~SchemaConfigGenerator -m:1 -p:RestoreFallbackFolders= -nologo`
- 结果:`50 Passed``0 Failed`
- 说明:测试项目构建仍显示既有 source generator test analyzer warning不属于本轮写集
- `RP-020` 的验证结果:
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
- 结果:`9 Warning(s)``0 Error(s)`;当前项目剩余 warning 均为 `SchemaConfigGenerator.cs``MA0051`
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~SchemaConfigGenerator -m:1 -p:RestoreFallbackFolders= -nologo`
- 结果:`50 Passed``0 Failed`
- 说明:测试项目构建仍显示既有 source generator test analyzer warning不属于本轮写集
- `RP-021` 的验证结果:
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
- 结果:`0 Warning(s)``0 Error(s)`;拆分后最大单文件已降到 `851` 行,满足仓库 800-1000 行上限
- `dotnet build GFramework.Core.SourceGenerators/GFramework.Core.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
- 结果:`0 Warning(s)``0 Error(s)``ContextAwareGenerator` 的字段命名与 provider 契约修复未引入新的 generator warning
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~ContextAwareGeneratorSnapshotTests|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests" -m:1 -p:RestoreFallbackFolders= -nologo`
- 结果:先并行运行两条 `dotnet test` 时触发共享输出文件锁冲突;改为串行重跑后 `ContextAwareGeneratorSnapshotTests=2 Passed`
`CqrsHandlerRegistryGeneratorTests=14 Passed`
- 说明:失败来自测试宿主并行写入同一 build 输出,不是代码回归;串行重跑后快照新增的字段重名场景和 CQRS 快照均通过
- `RP-022` 的验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:通过;`EasyEvents``CollectionExtensions` 与 logging 配置模型的兼容性回退未引入新的 `net8.0` 构建错误
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -v minimal`
- 结果:通过;`ContainingAssembly` null 防御与发射 helper 精简未引入新的构建错误
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -v minimal`
- 结果:通过;仍保留既有 `9``SchemaConfigGenerator.cs` `MA0051` warning非本轮新增
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~SchemaConfigGeneratorTests|FullyQualifiedName~ContextAwareGeneratorSnapshotTests|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
- 结果:`63 Passed``0 Failed`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~EasyEventsTests|FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~LoggingConfigurationTests|FullyQualifiedName~ConfigurableLoggerFactoryTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
- 结果:`38 Passed``0 Failed`
- `RP-023` 的验证结果:
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -v minimal`
- 结果:通过;仍保留既有 `9``SchemaConfigGenerator.cs` `MA0051` warning未新增新的 generator warning
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -v minimal`
- 结果:通过;并行构建时 `GFramework.SourceGenerators.Common.dll` 复制阶段出现一次 `MSB3026` 重试,随后成功完成
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~SchemaConfigGeneratorTests|FullyQualifiedName~SchemaConfigGeneratorSnapshotTests|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
- 结果:`63 Passed``0 Failed`
- 说明:测试项目构建仍会显示既有 `GFramework.SourceGenerators.Tests` analyzer warning不属于本轮写集
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~LoggingConfigurationTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
- 结果:`27 Passed``0 Failed`
- `RP-029` 的验证结果:
- `dotnet restore GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -p:RestoreFallbackFolders="" -nologo`
- 结果:通过;刷新 Linux 侧 restore 资产以移除 Windows fallback package folder 干扰
- `dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders="" -nologo`
- 结果通过focused test 所属测试项目已同步刷新 Linux 侧 restore 资产
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
- 结果:`0 Warning(s)``0 Error(s)``SchemaConfigGenerator.cs` 剩余 `MA0051` 已清零
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~SchemaConfigGenerator -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`54 Passed``0 Failed`
- 说明:测试项目构建仍显示既有 `GFramework.SourceGenerators.Tests` `MA0048` / `MA0051` / `MA0004` warning不属于本轮
`GFramework.Game.SourceGenerators` 写集
- `RP-030` 的验证结果:
- `dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders="" -nologo`
- 结果:通过;刷新 Linux 侧 restore 资产以移除 Windows fallback package folder 干扰
- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
- 结果:`49 Warning(s)``0 Error(s)`;当前项目已不再出现 `MA0004` / `MA0048`,剩余 warning 全部为 `MA0051`
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~GeneratorSnapshotTestSecurityTests|FullyQualifiedName~SchemaConfigGeneratorSnapshotTests|FullyQualifiedName~SchemaConfigGeneratorEnumTests" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`6 Passed``0 Failed`
- `RP-031` 的验证结果:
- `dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders="" -nologo`
- 结果:通过;刷新 Linux 侧 restore 资产以支持后续串行 build/test 验证
- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --disable-build-servers -m:1 -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:Summary`
- 结果:`43 Warning(s)``0 Error(s)``LoggerGeneratorSnapshotTests.cs` 已不再出现在 `MA0051` 列表中
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-build --disable-build-servers --filter FullyQualifiedName~LoggerGeneratorSnapshotTests -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`6 Passed``0 Failed`
- `RP-033` 的验证结果:
- `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"`
- 结果:`39 Warning(s)``0 Error(s)``SchemaConfigGeneratorSnapshotTests.cs` 已不再出现在 `MA0051` 列表中
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-build --disable-build-servers --filter FullyQualifiedName~SchemaConfigGeneratorSnapshotTests -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`1 Passed``0 Failed`
- `RP-035` 的验证结果:
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json`
- 结果:成功定位当前分支关联的 `PR #273`;状态为 `CLOSED`latest-head review threads 仍显示 `2` 条 open thread
test report 均为通过MegaLinter 仅保留 docstring coverage / `dotnet-format` 历史信号
- `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"`
- 结果:`22 Warning(s)``0 Error(s)`;剩余 `MA0051` 全部集中在 `CqrsHandlerRegistryGeneratorTests.cs`
`SchemaConfigGeneratorTests.cs`
- `RP-036` 的验证结果:
- `dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders="" -nologo`
- 结果:通过;刷新测试依赖输出,规避 `--no-build` 场景下的缺包噪音
- `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"`
- 结果:`15 Warning(s)``0 Error(s)``SchemaConfigGeneratorTests.cs` 已不再出现在 `MA0051` 列表中
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-build --disable-build-servers --filter FullyQualifiedName~SchemaConfigGeneratorTests -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`50 Passed``0 Failed`
- `RP-037` 的验证结果:
- `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"`
- 结果:`14 Warning(s)``0 Error(s)``CqrsHandlerRegistryGeneratorTests.cs` 的行号 `337` 已不再出现在 `MA0051` 列表中
- `RP-038` 的验证结果:
- `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"`
- 结果:`13 Warning(s)``0 Error(s)``CqrsHandlerRegistryGeneratorTests.cs` 的行号 `454` 已不再出现在 `MA0051` 列表中
- `RP-039` 的验证结果:
- `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"`
- 结果:`12 Warning(s)``0 Error(s)``CqrsHandlerRegistryGeneratorTests.cs` 的行号 `536` 已不再出现在 `MA0051` 列表中
- `RP-040` 的验证结果:
- `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"`
- 结果:`11 Warning(s)``0 Error(s)``CqrsHandlerRegistryGeneratorTests.cs` 的行号 `607` 已不再出现在 `MA0051` 列表中
- `RP-041` 的验证结果:
- `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)``CqrsHandlerRegistryGeneratorTests.cs` 的行号 `680` 已不再出现在 `MA0051` 列表中
- active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史
## 下一步建议
## 下一步
1. 提交当前 `GFramework.Godot.SourceGenerators.Tests` 清理批次,并确认提交只包含本 topic 相关文件
2. 如果继续 warning reduction优先重新评估仓库根目录 `dotnet clean` 的 solution-level 失败,再决定是继续从整仓 `dotnet build` 输出挑热点,还是先修复 clean 基线采集问题
1. 若要继续该主题,先读 active tracking再按需展开历史归档中的 warning 热点与验证记录
2. 下一轮优先继续 `GFramework.SourceGenerators.Tests``MA0051` 收口,并直接进入唯一剩余热点
`CqrsHandlerRegistryGeneratorTests.cs`;但若用户真正关心“唯一变更文件数接近 `75`”,下一轮应改为选择新的文件写集,
而不是继续停留在当前同一文件
3. 若改回推进 `MA0158`,先设计 `net8.0` / `net9.0` / `net10.0` 多 target 条件编译方案,不直接批量替换共享源码中的
`object` lock
4. 若后续继续改动 `GFramework.Godot`,先修复该项目的 Linux 侧 restore 资产,再补跑独立 build
5. 若本主题确认暂缓,可保持当前归档状态,不需要再恢复 `local-plan/`

View File

@ -12,11 +12,9 @@
## 当前恢复点
- 恢复点编号:`DOCUMENTATION-FULL-COVERAGE-GOV-RP-023`
- 恢复点编号:`DOCUMENTATION-FULL-COVERAGE-GOV-RP-019`
- 当前阶段:`Phase 5 - Governance Maintenance`
- 当前焦点:
- 保持 landing page / API 导航页中的仓库 README 入口可点击,避免读者在 docs 站点里遇到裸路径文本
- 继续按 `origin/main` 分支 diff 阈值做小批量文档治理,优先处理低风险导航 / 渲染热点
- 保持 `Game` persistence docs surface 与当前 `README`、源码、`PersistenceTests` 使用同一套 owner / adoption path 叙述
- 保持 `GFramework.Godot.SourceGenerators/README.md``docs/zh-CN/tutorials/godot-integration.md` 在生命周期接法上的一致性
- 保持 active tracking / trace 只承载当前恢复入口,把阶段细节留在 `archive/`
@ -38,26 +36,6 @@
`SettingsModel&lt;ISettingsDataRepository&gt;`
- 结合当前 PR 已改动的 `docs/zh-CN/godot/storage.md` 做同类巡检后,确认 `SaveRepository&lt;TSaveData&gt;`
也会在 VitePress code span 中按字面量渲染;两处现已在本地统一改为真实泛型写法。
- `2026-04-23``origin/main``aa879d2``2026-04-23T17:51:41+08:00`)为批处理基线,对
`README.md``GFramework.*``docs/zh-CN/**` 执行同类模式巡检,确认剩余热点仅位于
`docs/zh-CN/core/functional.md``docs/zh-CN/tutorials/functional-programming.md` 共 8 处。
- 上述 8 处 inline code 中的 `Option&lt;T&gt;``Result&lt;T&gt;``Nullable&lt;T&gt;` 已统一改为真实
泛型写法,避免在 VitePress 中显示字面量 HTML entity。
- `2026-04-23` 根据本轮使用反馈,已为 `.agents/skills/gframework-batch-boot/SKILL.md`
`.agents/skills/README.md` 补充数字速记阈值语义:
- `$gframework-batch-boot 75` 默认表示“当前分支全部提交相对远程 `origin/main` 接近 75 个分支 diff 文件时停止”
- `$gframework-batch-boot 75 2000` 默认表示“当前分支全部提交相对远程 `origin/main` 接近 75 个文件或 2000 行变更时停止”
- `75 | 2000` 仅作为可理解的 OR 写法保留,不再作为推荐写法,以避免与 shell pipe 混淆
- `2026-04-23``origin/main``aa879d2``2026-04-23T17:51:41+08:00`)为批处理基线,对
`docs/zh-CN/getting-started/index.md``core/index.md``game/index.md``source-generators/index.md`
`api-reference/index.md``abstractions/core-abstractions.md``abstractions/game-abstractions.md`
做导航可达性修复,把仓库 README / 根 README 裸路径统一改为指向 GitHub `main` 分支的可点击链接。
- 该批次不改变文档语义,只收口 docs 站点中的入口可达性;适合继续作为小步快跑的低风险治理模式。
- `2026-04-23` 在同一基线下继续收口第二批专题页导航热点,已将 `core/cqrs.md``ecs/arch.md`
`abstractions/ecs-arch-abstractions.md``game/scene.md``game/ui.md` 和 6 个
`source-generators/*.md` 专题页里的 README 裸路径统一改为 GitHub `main` blob 外链。
- 截至提交 `8a11720``2026-04-23T21:01:28+08:00`),当前分支相对 `origin/main``aa879d2`)的累计 diff
`24` 个文件、`264` 行,仍低于 `$gframework-batch-boot 75` 的停止阈值;但剩余命中已主要是正文语义性提及,不再适合作为同类批处理。
- 当前剩余的托管侧信号是 GitHub `Title check` 对 PR 标题过泛的 inconclusive 提示;这属于 PR 元数据,不是本地
文件缺陷。
@ -84,24 +62,14 @@
`docs/zh-CN/godot/setting.md:75` 的 inline code HTML entity 渲染问题。
- `2026-04-23` `rg -n '`[^`]*&lt;[^`]*`|`[^`]*&gt;[^`]*`' GFramework.Godot.SourceGenerators/README.md GFramework.Godot/README.md README.md docs/zh-CN/api-reference/index.md docs/zh-CN/game/data.md docs/zh-CN/game/serialization.md docs/zh-CN/game/setting.md docs/zh-CN/game/storage.md docs/zh-CN/godot/setting.md docs/zh-CN/godot/storage.md docs/zh-CN/source-generators/index.md`
- 结果:命中 `docs/zh-CN/godot/setting.md:75``docs/zh-CN/godot/storage.md:102` 两处同类写法,均已修正。
- `2026-04-23` `rg -n '`[^`]*&lt;[^`]*`|`[^`]*&gt;[^`]*`' README.md GFramework.* docs/zh-CN -g '*.md'`
- 结果:命中 `docs/zh-CN/core/functional.md``docs/zh-CN/tutorials/functional-programming.md` 共 8 处,已全部修正。
- `2026-04-23` `sed -n '1,260p' .agents/skills/gframework-batch-boot/SKILL.md``sed -n '1,220p' .agents/skills/README.md`
- 结果:确认原文仅描述自然语言 stop condition没有定义数字速记或多阈值 OR 语义;现已补齐。
- `2026-04-23` `rg -n '`GFramework\\.[^`]+/README\\.md`|`docs/zh-CN/[^`]+\\.md`|仓库根 `README\\.md`' docs/zh-CN -g '*.md'`
- 结果:确认 landing / API 导航页仍有一批裸路径仓库入口;本轮已先修复 `getting-started``core``game`
`source-generators``api-reference` 与两个 abstractions 页面。
- `2026-04-23` `rg -n '`GFramework\\.[^`]+/README\\.md`|仓库根 `README\\.md`' docs/zh-CN -g '*.md'`
- 结果:定位第二批专题页导航热点,已修复 `core/cqrs.md``ecs/arch.md``abstractions/ecs-arch-abstractions.md`
`game/scene.md``game/ui.md` 以及 6 个 `source-generators/*.md` 页面。
- `2026-04-23` `bun run build`(工作目录:`docs/`
- 结果:通过;仓库 README 外链改为 GitHub `main` blob 后,不再触发 VitePress dead link仅保留既有大 chunk warning
- 结果:通过;仅保留既有 VitePress 大 chunk warning无构建失败。
## 下一步
1. 若继续执行文档治理批处理,优先改做标题锚点、站内链接和少量非导航型裸路径引用的逐页复核,而不是继续按统一模板机械替换。
2. 若后续继续扩展批处理 skill可考虑再补充显式单位写法例如 `75 files 2000 lines`,但当前默认速记已足够覆盖
常见分支阈值场景
1. 提交并推送本地对 `docs/zh-CN/godot/setting.md``docs/zh-CN/godot/storage.md` 的 Markdown 泛型写法修正,
然后重新抓取 PR `#272` 确认 Greptile open thread 是否已在新 head commit 上消失。
2. 如果 PR `#272``Title check` 仍需要消除,到 GitHub 上把标题改成更具体的文档治理描述。
3. 若后续分支继续调整 `Game` persistence runtime、README 或公共 API优先复核 `docs/zh-CN/game/data.md`
`storage.md``serialization.md``setting.md` 与 landing page 是否仍保持同一套职责边界。
4. 若后续分支继续调整 `Godot` generator 接法,优先复核 `GFramework.Godot.SourceGenerators/README.md`

View File

@ -2,48 +2,49 @@
## 2026-04-23
### 当前恢复点RP-023
### 当前恢复点RP-019
- 按当前使用反馈继续执行 `documentation-full-coverage-governance` 下的 skill 文档治理。
- 本轮目标定义为“继续沿用上一批的 GitHub 外链策略,收口专题页里的裸路径 README 入口”。
- 使用 `$gframework-pr-review` 重新复核当前分支 PR `#272`
- GitHub latest-head review 当前暴露 1 条新的 Greptile open thread
`docs/zh-CN/godot/setting.md:75` 在 inline code 中写成
`SettingsModel&lt;ISettingsDataRepository&gt;`
- 本地核对当前文档渲染语义后,确认 CommonMark / VitePress 不会在 code span 内解码 HTML entity
该评论成立。
- 对当前 PR 已变更的 Godot 文档做同类扫描后,又在 `docs/zh-CN/godot/storage.md:102` 发现
`SaveRepository&lt;TSaveData&gt;` 的同型问题。
- 本轮执行的修复:
- 将 `docs/zh-CN/core/cqrs.md``ecs/arch.md` 的仓库 README 入口改为 GitHub `main` blob 外链
- 将 `docs/zh-CN/abstractions/ecs-arch-abstractions.md``game/scene.md``game/ui.md` 的回跳 README 入口改为可点击链接
- 将 `docs/zh-CN/source-generators/priority-generator.md``context-aware-generator.md`
`bind-node-signal-generator.md``godot-project-generator.md``get-node-generator.md`
`auto-register-exported-collections-generator.md` 的推荐阅读 README 入口改为可点击链接
- 同步更新 active tracking / trace记录第二批导航治理与新的恢复点
- 将 `docs/zh-CN/godot/setting.md``SettingsModel&lt;ISettingsDataRepository&gt;` 改为
`SettingsModel<ISettingsDataRepository>`
- 将 `docs/zh-CN/godot/storage.md``SaveRepository&lt;TSaveData&gt;` 改为
`SaveRepository<TSaveData>`
- 同步更新 active tracking / trace记录该 PR review follow-up 与新的恢复点
### 当前决策RP-023
### 当前决策RP-019
- 继续使用 `origin/main` 作为 `$gframework-batch-boot 75` 的固定基线,并以“分支累计 diff 文件数”作为主状态指标
- 对文档治理类批次,优先选择“导航可达性 / 渲染一致性”这类不改变产品语义的低风险切片。
- 在 docs 页面里出现仓库内 README 路径时,优先使用可点击的相对链接,而不是裸路径代码片段
- 当 docs 页需要跳转到 `docs/` 外部的 README 时,使用 GitHub `main` 分支 blob 外链,而不是跨出 `docs/` 根目录的相对路径
- 第二批继续沿用同一外链策略,避免在同一 docs surface 中混用“裸路径 / 相对死链 / GitHub 外链”三套入口风格
- PR review 结果以 GitHub latest-head open threads 为准;即便 active tracking 曾记录“无 open thread”也必须按新抓取结果回写
- 对 Markdown inline code 中的 C# 泛型示例,必须直接写真实的 `<T>` 语法,不能在反引号内部再写
`&lt;` / `&gt;`,否则 VitePress 会把 entity 当作字面量展示
- 当 latest-head review 命中某个文档表述问题时,应顺手扫描同一批 PR 已改动文档中的同类模式,避免只消掉单条 thread 却把相同渲染缺陷留在相邻页面
- 当前本地修复完成后,下一次 GitHub 侧复核需要基于新提交/新 head commit而不是旧的 PR review 快照
### 当前验证RP-023
### 当前验证RP-019
- 导航热点巡检
- `rg -n '`GFramework\\.[^`]+/README\\.md`|`docs/zh-CN/[^`]+\\.md`|仓库根 `README\\.md`' docs/zh-CN -g '*.md'`
- 结果:命中 landing / API 导航页中的裸路径仓库入口,已按本轮批次收口 7 个页面。
- 第二批专题页巡检:
- `rg -n '`GFramework\\.[^`]+/README\\.md`|仓库根 `README\\.md`' docs/zh-CN -g '*.md'`
- 结果:命中 `core/cqrs.md``ecs/arch.md``abstractions/ecs-arch-abstractions.md``game/scene.md`
`game/ui.md` 与 6 个 `source-generators/*.md` 专题页,均已修复
- PR review 抓取
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output /tmp/current-pr-review.json`
- 结果:通过PR `#272` 处于 `OPEN`latest head commit 存在 1 条 Greptile open thread定位到
`docs/zh-CN/godot/setting.md:75` 的 inline code HTML entity 渲染问题。
- 同类模式巡检:
- `rg -n '`[^`]*&lt;[^`]*`|`[^`]*&gt;[^`]*`' GFramework.Godot.SourceGenerators/README.md GFramework.Godot/README.md README.md docs/zh-CN/api-reference/index.md docs/zh-CN/game/data.md docs/zh-CN/game/serialization.md docs/zh-CN/game/setting.md docs/zh-CN/game/storage.md docs/zh-CN/godot/setting.md docs/zh-CN/godot/storage.md docs/zh-CN/source-generators/index.md`
- 结果:命中 `docs/zh-CN/godot/setting.md:75``docs/zh-CN/godot/storage.md:102` 两处同类写法,均已修正
- 构建校验:
- `bun run build`(工作目录:`docs/`
- 结果:通过;将仓库 README 跳转改为 GitHub `main` blob 外链后,不再触发 VitePress dead link仅保留既有大 chunk warning。
- 当前阈值状态:
- `git diff --name-only origin/main...HEAD | wc -l` => `24`
- `git diff --numstat origin/main...HEAD` 汇总 => `264` changed lines
- 结论:尚未接近 `75` 文件阈值,但剩余命中主要是正文语义性提及,当前批次在低风险模板化导航治理上可先收口。
- 结果:通过;仅保留既有 VitePress 大 chunk warning无构建失败。
### 归档摘要RP-022
### 归档摘要RP-018
- `.agents/skills/gframework-batch-boot/SKILL.md``.agents/skills/README.md` 补齐数字速记 stop condition 语义
- 明确 `$gframework-batch-boot 75` / `75 2000` 默认绑定 `origin/main` 累计 diff 口径
- 完成第一批 landing / API 导航页 README 外链治理,并通过 `docs/` 站点构建
- 使用 `$gframework-pr-review` 重新复核当前分支 PR `#272`
- latest-head review 命中 `GFramework.Godot.SourceGenerators/README.md:135` 的错误命名空间引用,并已在本地修正
- README 校验与 `docs/` 站点构建通过,待新提交推送后回 GitHub 侧确认 open thread 消失
### 归档指针
@ -54,4 +55,4 @@
### 下一步
1. 提交并推送本地修正后,再次抓取 PR `#272`,确认 Greptile open thread 是否已在新 head commit 上消失。
2. 若继续执行文档治理批处理,优先排查剩余的非导航型裸路径引用、标题锚点与站内链接热点,而不是扩成跨模块大波次
2. 如果 PR `#272``Title check` 仍需要处理,到 GitHub 上把标题改成更具体的文档治理描述

View File

@ -99,6 +99,6 @@ public sealed class DiagnosticsFeature
1. 先读本页,确认你是否真的只需要契约层
2. 再看 [`../core/index.md`](../core/index.md) 了解默认运行时怎么组织这些契约
3. 回到模块 README
- [`GFramework.Core.Abstractions/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core.Abstractions/README.md)
- [`GFramework.Core/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core/README.md)
- `GFramework.Core.Abstractions/README.md`
- `GFramework.Core/README.md`
4. 需要统一导航时,再看 [`../api-reference/index.md`](../api-reference/index.md)

View File

@ -90,5 +90,5 @@ var options = new ArchOptions
1. 先读本页,确认你是否真的只需要契约层
2. 如果需要默认实现,再看 [`../ecs/arch.md`](../ecs/arch.md)
3. 回到对应模块 README
- [`GFramework.Ecs.Arch.Abstractions/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch.Abstractions/README.md)
- [`GFramework.Ecs.Arch/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch/README.md)
- `GFramework.Ecs.Arch.Abstractions/README.md`
- `GFramework.Ecs.Arch/README.md`

View File

@ -118,5 +118,5 @@ public sealed class ContinueGameCommandHandler
- [`../game/scene.md`](../game/scene.md)
- [`../game/ui.md`](../game/ui.md)
4. 需要仓库侧入口时,回到:
- [`GFramework.Game.Abstractions/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.Abstractions/README.md)
- [`GFramework.Game/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)
- `GFramework.Game.Abstractions/README.md`
- `GFramework.Game/README.md`

View File

@ -21,7 +21,7 @@ description: GFramework 的 API 阅读入口,按模块映射 README、专题
先读模块 README再读对应 landing page
- 入门入口:[`../getting-started/index.md`](../getting-started/index.md)
- 根模块地图:仓库根 [`README.md`](https://github.com/GeWuYou/GFramework/blob/main/README.md)
- 根模块地图:仓库根 `README.md`
### 想确认“这个功能属于哪个模块”
@ -29,11 +29,11 @@ description: GFramework 的 API 阅读入口,按模块映射 README、专题
| 模块族 | 模块 README | 站内入口 | XML 文档关注点 |
| --- | --- | --- | --- |
| `Core` / `Core.Abstractions` | [`GFramework.Core/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core/README.md)、[`GFramework.Core.Abstractions/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core.Abstractions/README.md) | [`../core/index.md`](../core/index.md)、[`../abstractions/core-abstractions.md`](../abstractions/core-abstractions.md) | 架构入口、生命周期、命令 / 查询 / 事件 / 状态 / 资源 / 日志 / 配置 / 并发契约 |
| `Cqrs` / `Cqrs.Abstractions` / `Cqrs.SourceGenerators` | [`GFramework.Cqrs/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs/README.md)、[`GFramework.Cqrs.Abstractions/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs.Abstractions/README.md)、[`GFramework.Cqrs.SourceGenerators/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs.SourceGenerators/README.md) | [`../core/cqrs.md`](../core/cqrs.md)、[`../source-generators/cqrs-handler-registry-generator.md`](../source-generators/cqrs-handler-registry-generator.md) | request / notification / handler / pipeline / registry / fallback contract |
| `Game` / `Game.Abstractions` / `Game.SourceGenerators` | [`GFramework.Game/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)、[`GFramework.Game.Abstractions/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.Abstractions/README.md)、[`GFramework.Game.SourceGenerators/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.SourceGenerators/README.md) | [`../game/index.md`](../game/index.md)、[`../abstractions/game-abstractions.md`](../abstractions/game-abstractions.md) | 配置、数据、设置、场景、UI、存储、序列化契约 |
| `Godot` / `Godot.SourceGenerators` | [`GFramework.Godot/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot/README.md)、[`GFramework.Godot.SourceGenerators/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot.SourceGenerators/README.md) | [`../godot/index.md`](../godot/index.md)、[`../source-generators/godot-project-generator.md`](../source-generators/godot-project-generator.md)、[`../source-generators/get-node-generator.md`](../source-generators/get-node-generator.md)、[`../source-generators/bind-node-signal-generator.md`](../source-generators/bind-node-signal-generator.md) | 节点扩展、场景 / UI 适配、配置 / 存储 / 设置接线、Godot 生成器入口 |
| `Ecs.Arch` / `Ecs.Arch.Abstractions` | [`GFramework.Ecs.Arch/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch/README.md)、[`GFramework.Ecs.Arch.Abstractions/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch.Abstractions/README.md) | [`../ecs/index.md`](../ecs/index.md)、[`../ecs/arch.md`](../ecs/arch.md)、[`../abstractions/ecs-arch-abstractions.md`](../abstractions/ecs-arch-abstractions.md) | ECS 模块契约、系统适配、配置对象和运行时装配边界 |
| `Core` / `Core.Abstractions` | `GFramework.Core/README.md``GFramework.Core.Abstractions/README.md` | [`../core/index.md`](../core/index.md)、[`../abstractions/core-abstractions.md`](../abstractions/core-abstractions.md) | 架构入口、生命周期、命令 / 查询 / 事件 / 状态 / 资源 / 日志 / 配置 / 并发契约 |
| `Cqrs` / `Cqrs.Abstractions` / `Cqrs.SourceGenerators` | `GFramework.Cqrs/README.md``GFramework.Cqrs.Abstractions/README.md``GFramework.Cqrs.SourceGenerators/README.md` | [`../core/cqrs.md`](../core/cqrs.md)、[`../source-generators/cqrs-handler-registry-generator.md`](../source-generators/cqrs-handler-registry-generator.md) | request / notification / handler / pipeline / registry / fallback contract |
| `Game` / `Game.Abstractions` / `Game.SourceGenerators` | `GFramework.Game/README.md``GFramework.Game.Abstractions/README.md``GFramework.Game.SourceGenerators/README.md` | [`../game/index.md`](../game/index.md)、[`../abstractions/game-abstractions.md`](../abstractions/game-abstractions.md) | 配置、数据、设置、场景、UI、存储、序列化契约 |
| `Godot` / `Godot.SourceGenerators` | `GFramework.Godot/README.md``GFramework.Godot.SourceGenerators/README.md` | [`../godot/index.md`](../godot/index.md)、[`../source-generators/godot-project-generator.md`](../source-generators/godot-project-generator.md)、[`../source-generators/get-node-generator.md`](../source-generators/get-node-generator.md)、[`../source-generators/bind-node-signal-generator.md`](../source-generators/bind-node-signal-generator.md) | 节点扩展、场景 / UI 适配、配置 / 存储 / 设置接线、Godot 生成器入口 |
| `Ecs.Arch` / `Ecs.Arch.Abstractions` | `GFramework.Ecs.Arch/README.md``GFramework.Ecs.Arch.Abstractions/README.md` | [`../ecs/index.md`](../ecs/index.md)、[`../ecs/arch.md`](../ecs/arch.md)、[`../abstractions/ecs-arch-abstractions.md`](../abstractions/ecs-arch-abstractions.md) | ECS 模块契约、系统适配、配置对象和运行时装配边界 |
## 先看 XML还是先看教程

View File

@ -186,4 +186,4 @@ RegisterCqrsPipelineBehavior<LoggingBehavior<,>>();
- 架构入口:[architecture](./architecture.md)
- 上下文入口:[context](./context.md)
- 生成器专题:[../source-generators/cqrs-handler-registry-generator.md](../source-generators/cqrs-handler-registry-generator.md)
- 模块 README[`GFramework.Cqrs/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs/README.md)
- 模块 README`GFramework.Cqrs/README.md`

View File

@ -17,7 +17,7 @@ GFramework.Core 提供了一套完整的函数式编程工具,帮助开发者
### Option 类型
`Option<T>` 表示可能存在或不存在的值,用于替代 null 引用。它有两种状态:
`Option&lt;T&gt;` 表示可能存在或不存在的值,用于替代 null 引用。它有两种状态:
- **Some**:包含一个值
- **None**:不包含值
@ -26,7 +26,7 @@ GFramework.Core 提供了一套完整的函数式编程工具,帮助开发者
### Result 类型
`Result<T>` 表示操作的结果,可能是成功值或失败异常。它有三种状态:
`Result&lt;T&gt;` 表示操作的结果,可能是成功值或失败异常。它有三种状态:
- **Success**:操作成功,包含返回值
- **Faulted**:操作失败,包含异常信息
@ -586,14 +586,14 @@ public async Task<Result<Response>> ProcessRequestAsync(Request request)
### Option vs Nullable
**Q: Option 和 `Nullable<T>` 有什么区别?**
**Q: Option 和 Nullable&lt;T&gt; 有什么区别?**
A:
- `Nullable<T>` 只能用于值类型,`Option<T>` 可用于任何类型
- `Option<T>` 提供丰富的函数式操作Map、Bind、Filter 等)
- `Option<T>` 强制显式处理"无值"情况,更安全
- `Option<T>` 可以与 Result 等其他函数式类型组合
- `Nullable&lt;T&gt;` 只能用于值类型,`Option&lt;T&gt;` 可用于任何类型
- `Option&lt;T&gt;` 提供丰富的函数式操作Map、Bind、Filter 等)
- `Option&lt;T&gt;` 强制显式处理"无值"情况,更安全
- `Option&lt;T&gt;` 可以与 Result 等其他函数式类型组合
### Result vs Exception

View File

@ -71,7 +71,7 @@ dotnet add package GeWuYou.GFramework.Core.Abstractions
如果你已经知道模块归属,但想确认公开类型的契约边界,建议按下面顺序阅读:
1. 先看模块 README [`GFramework.Core/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core/README.md),确认包关系和目录边界
1. 先看模块 README `GFramework.Core/README.md`,确认包关系和目录边界
2. 再看本栏目对应专题页,确认采用顺序、生命周期与推荐接线方式
3. 最后回到源码中的 XML 文档,重点核对这些类型族:
- `Architecture` / `IArchitectureContext`
@ -149,7 +149,7 @@ public sealed class CounterArchitecture : Architecture
## 对应模块入口
- [`GFramework.Core/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core/README.md)
- [`GFramework.Core.Abstractions/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core.Abstractions/README.md)
- [`docs/zh-CN/api-reference/index.md`](../api-reference/index.md)
- 仓库根 [`README.md`](https://github.com/GeWuYou/GFramework/blob/main/README.md)
- `GFramework.Core/README.md`
- `GFramework.Core.Abstractions/README.md`
- `docs/zh-CN/api-reference/index.md`
- 仓库根 `README.md`

View File

@ -140,5 +140,5 @@ ecsModule.Update(deltaTime);
- ECS 模块总览:[`index.md`](./index.md)
- 抽象契约页:[`../abstractions/ecs-arch-abstractions.md`](../abstractions/ecs-arch-abstractions.md)
- 仓库模块 README[`GFramework.Ecs.Arch/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch/README.md)
- 仓库模块 README`GFramework.Ecs.Arch/README.md`
- 统一 API / XML 导航:[`../api-reference/index.md`](../api-reference/index.md)

View File

@ -127,6 +127,6 @@ IStorage storage = new FileStorage("GameData", serializer);
## 对应模块入口
- [`GFramework.Game/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)
- [`GFramework.Game.Abstractions/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.Abstractions/README.md)
- 仓库根 [`README.md`](https://github.com/GeWuYou/GFramework/blob/main/README.md)
- `GFramework.Game/README.md`
- `GFramework.Game.Abstractions/README.md`
- 仓库根 `README.md`

View File

@ -258,5 +258,5 @@ await sceneRouter.PopAsync();
1. [game/index.md](./index.md)
2. [ui.md](./ui.md)
3. [`GFramework.Game/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)
4. [`GFramework.Game.Abstractions/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.Abstractions/README.md)
3. `GFramework.Game/README.md`
4. `GFramework.Game.Abstractions/README.md`

View File

@ -329,5 +329,5 @@ uiRouter.Hide(modalHandle, UiLayer.Modal);
1. [game/index.md](./index.md)
2. [scene.md](./scene.md)
3. [../source-generators/auto-ui-page-generator.md](../source-generators/auto-ui-page-generator.md)
4. [`GFramework.Game/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)
5. [`GFramework.Game.Abstractions/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.Abstractions/README.md)
4. `GFramework.Game/README.md`
5. `GFramework.Game.Abstractions/README.md`

View File

@ -46,7 +46,7 @@
对应文档:
- [`../core/cqrs.md`](../core/cqrs.md)
- 仓库内模块入口:[`GFramework.Cqrs/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs/README.md)
- 仓库内模块入口:`GFramework.Cqrs/README.md`
### 想做游戏运行时
@ -65,7 +65,7 @@
对应文档:
- [`../game/index.md`](../game/index.md)
- 仓库内模块入口:[`GFramework.Game/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)
- 仓库内模块入口:`GFramework.Game/README.md`
### 想接入 Godot
@ -76,7 +76,7 @@
对应文档:
- [`../godot/index.md`](../godot/index.md)
- 仓库内模块入口:[`GFramework.Godot/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot/README.md)
- 仓库内模块入口:`GFramework.Godot/README.md`
## Source Generators 什么时候装

View File

@ -220,4 +220,4 @@ public List<IntConfig>? Values { get; } = new();
1. [/zh-CN/source-generators/index](./index.md)
2. [/zh-CN/game/config-system](../game/config-system.md)
3. [/zh-CN/source-generators/godot-project-generator](./godot-project-generator.md)
4. [`GFramework.Godot.SourceGenerators/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot.SourceGenerators/README.md)
4. `GFramework.Godot.SourceGenerators/README.md`

View File

@ -189,4 +189,4 @@ private void OnAnyButtonPressed()
1. [/zh-CN/source-generators/get-node-generator](./get-node-generator.md)
2. [/zh-CN/source-generators/godot-project-generator](./godot-project-generator.md)
3. [/zh-CN/godot/ui](../godot/ui.md)
4. [`GFramework.Godot.SourceGenerators/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot.SourceGenerators/README.md)
4. `GFramework.Godot.SourceGenerators/README.md`

View File

@ -195,4 +195,4 @@ finally
1. [context-get-generator.md](./context-get-generator.md)
2. [logging-generator.md](./logging-generator.md)
3. [../core/index.md](../core/index.md)
4. [`GFramework.Core.SourceGenerators/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core.SourceGenerators/README.md)
4. `GFramework.Core.SourceGenerators/README.md`

View File

@ -195,4 +195,4 @@ public override void _Ready()
1. [/zh-CN/source-generators/bind-node-signal-generator](./bind-node-signal-generator.md)
2. [/zh-CN/source-generators/godot-project-generator](./godot-project-generator.md)
3. [/zh-CN/godot/ui](../godot/ui.md)
4. [`GFramework.Godot.SourceGenerators/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot.SourceGenerators/README.md)
4. `GFramework.Godot.SourceGenerators/README.md`

View File

@ -217,4 +217,4 @@ AutoLoad 名称也遵循同样的冲突处理策略。
1. [/zh-CN/source-generators/get-node-generator](./get-node-generator.md)
2. [/zh-CN/source-generators/bind-node-signal-generator](./bind-node-signal-generator.md)
3. [/zh-CN/tutorials/godot-integration](../tutorials/godot-integration.md)
4. [`GFramework.Godot.SourceGenerators/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot.SourceGenerators/README.md)
4. `GFramework.Godot.SourceGenerators/README.md`

View File

@ -101,7 +101,7 @@ GFramework 当前发布的生成器包是:
## 对应模块入口
- [`GFramework.Core.SourceGenerators/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core.SourceGenerators/README.md)
- [`GFramework.Game.SourceGenerators/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.SourceGenerators/README.md)
- [`GFramework.Cqrs.SourceGenerators/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs.SourceGenerators/README.md)
- [`GFramework.Godot.SourceGenerators/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot.SourceGenerators/README.md)
- `GFramework.Core.SourceGenerators/README.md`
- `GFramework.Game.SourceGenerators/README.md`
- `GFramework.Cqrs.SourceGenerators/README.md`
- `GFramework.Godot.SourceGenerators/README.md`

View File

@ -213,4 +213,4 @@ public sealed class DynamicPrioritySystem : IPrioritized
1. [context-aware-generator.md](./context-aware-generator.md)
2. [context-get-generator.md](./context-get-generator.md)
3. [../core/index.md](../core/index.md)
4. [`GFramework.Core.SourceGenerators/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core.SourceGenerators/README.md)
4. `GFramework.Core.SourceGenerators/README.md`

View File

@ -121,7 +121,7 @@ namespace MyGame.Services
**代码说明**
- `Option<T>` 明确表示值可能不存在,避免 NullReferenceException
- `Option&lt;T&gt;` 明确表示值可能不存在,避免 NullReferenceException
- `Match` 强制处理两种情况,不会遗漏 null 检查
- `Map``Bind` 实现链式转换,代码更简洁
- `Filter` 可以安全地过滤值
@ -250,7 +250,7 @@ namespace MyGame.Services
**代码说明**
- `Result<T>` 将错误作为值返回,而不是抛出异常
- `Result&lt;T&gt;` 将错误作为值返回,而不是抛出异常
- `Result.Try` 自动捕获异常并转换为 Result
- `Bind` 可以链接多个可能失败的操作
- `Match` 强制处理成功和失败两种情况