Compare commits

...

23 Commits

Author SHA1 Message Date
gewuyou
7e45197698 test(godot-source-generators): 清理源生成器测试项目警告
- 重构 GFramework.Godot.SourceGenerators.Tests 的测试模板与诊断辅助,清除项目内全部 analyzer warning
- 更新 GeneratorTest 异步等待与 analyzer-warning-reduction 跟踪文档,记录批次验证结果与恢复点
2026-04-24 14:06:41 +08:00
gewuyou
a439fb8f4e refactor(godot-source-generators): 清理生成器告警与构建基线
- 重构 Godot source generator 的长方法与字符串比较逻辑,清理 GFramework.Godot.SourceGenerators 的 MA0051 和 MA0006 告警

- 更新 AutoRegisterExportedCollectionsGenerator 的注册解析阶段拆分,消除剩余的长方法告警

- 更新 AGENTS 与 analyzer-warning-reduction 跟踪文档,明确 warning 检查必须先 clean 再 build
2026-04-24 13:25:42 +08:00
GeWuYou
25d33d0bf9 chore(build): 更新项目配置以启用代码分析并修改许可证
- 移除测试项目的警告级别设置
- 将包许可证从 MIT 更改为 Apache-2.0
- 为 GFramework 项目启用 .NET 代码分析器
- 保持目标框架 net8.0、net9.0 和 net10.0 的支持
2026-04-24 13:08:11 +08:00
gewuyou
b710f31b86 docs(workflow): 更新构建告警检查约定
- 更新 AGENTS.md,明确使用 plain dotnet build 作为默认构建告警检查入口
- 归档 analyzer warning reduction 在 RP-042 至 RP-048 的晚期 active 文档细节
- 压缩 active todo 与 trace,只保留当前分支目标所需的恢复真值
2026-04-24 13:05:10 +08:00
gewuyou
a98d1cb8d0 docs(ai-plan): 更新告警批处理恢复点
- 更新 analyzer warning reduction 的 active todo 为 RP-048 当前真值
- 补充 plain dotnet build 成功与最新 origin/main baseline
- 记录当前批处理已到自然停点并收敛下一步建议
2026-04-24 12:59:03 +08:00
gewuyou
77e332fd44 fix(analyzer): 收口当前批次警告切片
- 修复 UnifiedSettingsFile 与 LocalizationMap 的集合暴露形状,减少可变集合泄漏风险
- 优化 CqrsHandlerRegistryGeneratorTests 的大型 fixture 组织方式,降低 MA0051 噪音
- 更新 analyzer warning reduction 的 active todo 与 trace,回写 0 warning solution 基线
2026-04-24 12:37:47 +08:00
gewuyou
091b872c86 docs(ai-plan): 更新告警基线追踪
- 更新 analyzer warning reduction 的 active tracking 到 RP-046 并记录 solution 级 891 条 warning 基线
- 补充前台构建与日志采集形态不一致的环境风险和后续恢复建议
2026-04-24 11:57:49 +08:00
gewuyou
a0ce04b185 fix(godot): 收紧本地化映射集合暴露
- 修复 LocalizationMap 对可变 Dictionary 的直接公共暴露,降低 MA0016 集合暴露风险

- 新增复制输入映射的构造函数,并保留默认映射初始化行为以维持现有消费者兼容性

- 更新 XML 注释,明确只读访问语义和内部状态隔离原因
2026-04-24 10:46:17 +08:00
gewuyou
e692ed3e43
Merge pull request #280 from GeWuYou/fix/analyzer-warning-reduction-batch
Fix/analyzer warning reduction batch
2026-04-24 09:36:17 +08:00
gewuyou
136b139312 fix(ai-plan): 修复PR评审涉及的归档跟踪文档问题
- 修复 analyzer warning reduction 归档 todo 中指向 RP-001 的相对链接

- 清理 rp002-rp041 trace 中误混入的 RP-001 历史段落

- 更新 active tracking 与 trace 的恢复点描述和验证结论
2026-04-24 08:41:31 +08:00
gewuyou
833a95f7f3 fix(analyzer-warning-reduction): 收口PR280评审并压缩恢复入口
- 修复 SchemaConfigGeneratorTests 中冗余的 global:: 返回类型声明
- 归档 analyzer-warning-reduction 主题的 RP-002 至 RP-041 详细 tracking 与 trace 历史
- 更新 RP-042 的 active 跟踪与验证记录以反映 PR #280 follow-up 结论
2026-04-24 07:42:19 +08:00
gewuyou
2de57f5fde
Merge pull request #281 from GeWuYou/docs/sdk-update-documentation 2026-04-23 23:03:40 +08:00
GeWuYou
a3501c9b91 docs(skills): 更新 README 中关于文件变更停止条件的说明
- 移除了关于分支相对基线的旧说明
- 明确单个数字表示当前分支全部提交相对远程 origin/main 的文件变更数
- 详细说明两个数字表示文件数或变更行数的固定顺序
- 添加关于避免使用 | 符号的建议并说明其 OR 语义
2026-04-23 23:02:02 +08:00
dependabot[bot]
fdccfc4448 Bump Microsoft.Extensions.DependencyInjection.Abstractions from 10.0.6 to 10.0.7
---
updated-dependencies:
- dependency-name: Microsoft.Extensions.DependencyInjection.Abstractions
  dependency-version: 10.0.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-23 22:55:51 +08:00
dependabot[bot]
fa2488c108 Bump Microsoft.Extensions.DependencyInjection from 10.0.6 to 10.0.7
---
updated-dependencies:
- dependency-name: Microsoft.Extensions.DependencyInjection
  dependency-version: 10.0.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-23 22:55:34 +08:00
dependabot[bot]
06f8db2efd Bump Meziantou.Polyfill from 1.0.110 to 1.0.116
---
updated-dependencies:
- dependency-name: Meziantou.Polyfill
  dependency-version: 1.0.116
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-23 22:54:52 +08:00
dependabot[bot]
a7bd213044 Bump Meziantou.Analyzer from 3.0.48 to 3.0.52
---
updated-dependencies:
- dependency-name: Meziantou.Analyzer
  dependency-version: 3.0.52
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-23 22:54:36 +08:00
dependabot[bot]
c578910b40 build(deps): bump trufflesecurity/trufflehog from 3.94.3 to 3.95.2
Bumps [trufflesecurity/trufflehog](https://github.com/trufflesecurity/trufflehog) from 3.94.3 to 3.95.2.
- [Release notes](https://github.com/trufflesecurity/trufflehog/releases)
- [Commits](https://github.com/trufflesecurity/trufflehog/compare/v3.94.3...v3.95.2)

---
updated-dependencies:
- dependency-name: trufflesecurity/trufflehog
  dependency-version: 3.95.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-23 22:54:07 +08:00
gewuyou
45b25f429f docs(documentation): 更新批处理恢复点指标
- 更新 documentation-full-coverage-governance 跟踪中的当前分支阈值状态
- 补充 trace 对 24 个文件与 264 行变更的 stop-condition 记录
- 说明剩余热点已转为正文语义性提及,适合后续逐页复核
2026-04-23 21:04:02 +08:00
gewuyou
8a117201d4 docs(documentation): 收口专题页README导航入口
- 更新专题页推荐阅读中的 README 入口为可点击 GitHub 链接
- 修复 source-generators 与 Game/ECS/CQRS 页面中的裸路径导航
- 更新 documentation-full-coverage-governance 跟踪与轨迹以记录第二批治理结果
2026-04-23 21:01:28 +08:00
gewuyou
a4bb041b0d docs(documentation): 补充文档站仓库入口链接
- 更新 docs/zh-CN landing page 与 API 导航页中的 README 入口为可点击 GitHub 链接
- 修复 VitePress 对 docs 外相对链接的 dead link 构建失败
- 更新 documentation-full-coverage-governance 跟踪与轨迹以记录本轮批处理结论
2026-04-23 20:53:41 +08:00
gewuyou
148cfe14b0 docs(skills): 明确批处理阈值速记语义
- 更新 gframework-batch-boot skill,明确纯数字速记默认按当前分支相对远程 origin/main 的累计 diff 计算
- 补充文件数与代码行数双阈值的 OR 语义,并将无管道的 75 2000 作为推荐写法
- 同步更新 skills README 与 documentation governance tracking/trace,记录本轮规则收口与恢复点
2026-04-23 19:22:45 +08:00
gewuyou
59fe63bba6 fix(docs): 修正文档中的泛型内联代码渲染
- 修复 Core 与函数式教程页面中的泛型 inline code 写法,避免 VitePress 将 HTML entity 按字面量展示
- 更新 documentation-full-coverage-governance 的 tracking 与 trace,记录本轮批处理基线、验证和恢复点
2026-04-23 18:06:52 +08:00
54 changed files with 4212 additions and 4006 deletions

View File

@ -46,9 +46,22 @@
/gframework-batch-boot <task-or-stop-condition> /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 ```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 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 /gframework-batch-boot keep refactoring repetitive source-generator tests in bounded batches
``` ```

View File

@ -50,6 +50,19 @@ For changed-file limits, measure branch-wide scope against the chosen baseline,
- use `git diff --name-only <baseline>...HEAD` - use `git diff --name-only <baseline>...HEAD`
- do not confuse branch diff size with `git status --short` - 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 ## Stop Conditions
Choose one primary stop condition before the first batch and restate it to the user. Choose one primary stop condition before the first batch and restate it to the user.
@ -63,6 +76,32 @@ Common stop conditions:
If multiple stop conditions exist, rank them and treat one as primary. 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 ## Batch Loop
1. Inspect the current state before the first batch: 1. Inspect the current state before the first batch:
@ -134,6 +173,8 @@ When stopping, report:
## Example Triggers ## 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 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 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.` - `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密钥、密码等 # 使用 TruffleHog 工具扫描代码库中的敏感信息泄露如API密钥、密码等
# 该步骤会比较基础分支和当前提交之间的差异,检测新增内容中是否包含敏感数据 # 该步骤会比较基础分支和当前提交之间的差异,检测新增内容中是否包含敏感数据
- name: TruffleHog OSS - name: TruffleHog OSS
uses: trufflesecurity/trufflehog@v3.94.3 uses: trufflesecurity/trufflehog@v3.95.2
with: with:
# 扫描路径,. 表示扫描整个仓库 # 扫描路径,. 表示扫描整个仓库
path: . path: .

View File

@ -29,6 +29,10 @@ All AI agents and contributors must follow these rules when writing, reviewing,
## Git Workflow Rules ## Git Workflow Rules
- Every completed task MUST pass at least one build validation before it is considered done. - Every completed task MUST pass at least one build validation before it is considered done.
- When the goal is to inspect or reduce warnings printed during project build, contributors MUST establish the warning
baseline from a non-incremental repository-root build by running `dotnet clean` and then `dotnet build`.
- Contributors MUST NOT treat a repeated incremental `dotnet build` result as authoritative for warning inspection when
a clean baseline has not been captured in the same round.
- If the task changes multiple projects or shared abstractions, prefer a solution-level or affected-project - If the task changes multiple projects or shared abstractions, prefer a solution-level or affected-project
`dotnet build ... -c Release`; otherwise use the smallest build command that still proves the result compiles. `dotnet build ... -c Release`; otherwise use the smallest build command that still proves the result compiles.
- When a task adds a feature or modifies code, contributors MUST run a Release build for every directly affected - When a task adds a feature or modifies code, contributors MUST run a Release build for every directly affected
@ -233,6 +237,10 @@ All generated or modified code MUST include clear and meaningful comments where
Use the smallest command set that proves the change, then expand if the change is cross-cutting. Use the smallest command set that proves the change, then expand if the change is cross-cutting.
```bash ```bash
# Check warnings from the default repository build entrypoint
dotnet clean
dotnet build
# Build the full solution # Build the full solution
dotnet build GFramework.sln -c Release dotnet build GFramework.sln -c Release

View File

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

View File

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

View File

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

View File

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

View File

@ -283,15 +283,21 @@ public class UnifiedSettingsDataRepository(
/// 复制当前统一文件快照,确保未提交修改不会污染内存中的已提交状态。 /// 复制当前统一文件快照,确保未提交修改不会污染内存中的已提交状态。
/// </summary> /// </summary>
/// <param name="source">要复制的统一文件快照。</param> /// <param name="source">要复制的统一文件快照。</param>
/// <returns>包含独立 section 字典的新快照。</returns> /// <returns>包含独立 section 映射副本的新快照。</returns>
private static UnifiedSettingsFile CloneFile(UnifiedSettingsFile source) private static UnifiedSettingsFile CloneFile(UnifiedSettingsFile source)
{ {
ArgumentNullException.ThrowIfNull(source); ArgumentNullException.ThrowIfNull(source);
// 反序列化后的运行时类型可能只是 IDictionary 实现;若底层仍是 Dictionary则保留其 comparer
// 否则退回到按当前内容复制,避免因为 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 return new UnifiedSettingsFile
{ {
Version = source.Version, Version = source.Version,
Sections = new Dictionary<string, string>(source.Sections, source.Sections.Comparer) Sections = sections
}; };
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,131 @@ namespace GFramework.Godot.SourceGenerators.Tests.Project;
[TestFixture] [TestFixture]
public class GodotProjectMetadataGeneratorTests public class GodotProjectMetadataGeneratorTests
{ {
private const string AutoLoadProjectFile = """
[autoload]
GameServices="*res://autoload/game_services.tscn"
AudioBus="*res://autoload/audio_bus.gd"
""";
private const string InputActionsProjectFile = """
[input]
move_up={
"deadzone": 0.5
}
ui_cancel={
"deadzone": 0.5
}
""";
private const string ExpectedAutoLoads = """
// <auto-generated />
#nullable enable
namespace GFramework.Godot.Generated;
/// <summary>
/// 提供 project.godot 中 AutoLoad 单例的强类型访问入口。
/// </summary>
public static partial class AutoLoads
{
/// <summary>
/// 获取 AutoLoad <c>GameServices</c>。
/// </summary>
public static global::TestApp.GameServices GameServices => GetRequiredNode<global::TestApp.GameServices>("GameServices");
/// <summary>
/// 尝试获取 AutoLoad <c>GameServices</c>。
/// </summary>
public static bool TryGetGameServices(out global::TestApp.GameServices? value)
{
return TryGetNode("GameServices", out value);
}
/// <summary>
/// 获取 AutoLoad <c>AudioBus</c>。
/// </summary>
public static global::Godot.Node AudioBus => GetRequiredNode<global::Godot.Node>("AudioBus");
/// <summary>
/// 尝试获取 AutoLoad <c>AudioBus</c>。
/// </summary>
public static bool TryGetAudioBus(out global::Godot.Node? value)
{
return TryGetNode("AudioBus", out value);
}
/// <summary>
/// 获取一个必填的 AutoLoad 节点;缺失时抛出异常。
/// </summary>
/// <typeparam name="TNode">节点类型。</typeparam>
/// <param name="autoLoadName">AutoLoad 名称。</param>
/// <returns>已解析的 AutoLoad 节点。</returns>
private static TNode GetRequiredNode<TNode>(string autoLoadName)
where TNode : global::Godot.Node
{
if (TryGetNode(autoLoadName, out TNode? value))
{
return value!;
}
throw new global::System.InvalidOperationException($"AutoLoad '{autoLoadName}' is not available on the active SceneTree root.");
}
/// <summary>
/// 尝试从当前 SceneTree 根节点解析 AutoLoad。
/// </summary>
/// <typeparam name="TNode">节点类型。</typeparam>
/// <param name="autoLoadName">AutoLoad 名称。</param>
/// <param name="value">解析到的节点实例。</param>
/// <returns>若当前进程存在 SceneTree 且根节点中能解析到该 AutoLoad则返回 <c>true</c>。</returns>
private static bool TryGetNode<TNode>(string autoLoadName, out TNode? value)
where TNode : global::Godot.Node
{
value = default;
if (global::Godot.Engine.GetMainLoop() is not global::Godot.SceneTree sceneTree)
{
return false;
}
var root = sceneTree.Root;
if (root is null)
{
return false;
}
value = root.GetNodeOrNull<TNode>($"/root/{autoLoadName}");
return value is not null;
}
}
""";
private const string ExpectedInputActions = """
// <auto-generated />
#nullable enable
namespace GFramework.Godot.Generated;
/// <summary>
/// 提供 project.godot 中 Input Action 名称的强类型常量。
/// </summary>
public static partial class InputActions
{
/// <summary>
/// Input Action <c>move_up</c> 的稳定名称。
/// </summary>
public const string MoveUp = "move_up";
/// <summary>
/// Input Action <c>ui_cancel</c> 的稳定名称。
/// </summary>
public const string UiCancel = "ui_cancel";
}
""";
/// <summary> /// <summary>
/// 验证会根据 AutoLoad 与 Input Action 生成稳定的强类型入口。 /// 验证会根据 AutoLoad 与 Input Action 生成稳定的强类型入口。
/// </summary> /// </summary>
@ -29,142 +154,19 @@ public class GodotProjectMetadataGeneratorTests
""", """,
includeAutoLoadAttribute: true); includeAutoLoadAttribute: true);
const string projectFile = """
[autoload]
GameServices="*res://autoload/game_services.tscn"
AudioBus="*res://autoload/audio_bus.gd"
[input]
move_up={
"deadzone": 0.5
}
ui_cancel={
"deadzone": 0.5
}
""";
const string expectedAutoLoads = """
// <auto-generated />
#nullable enable
namespace GFramework.Godot.Generated;
/// <summary>
/// 提供 project.godot 中 AutoLoad 单例的强类型访问入口。
/// </summary>
public static partial class AutoLoads
{
/// <summary>
/// 获取 AutoLoad <c>GameServices</c>。
/// </summary>
public static global::TestApp.GameServices GameServices => GetRequiredNode<global::TestApp.GameServices>("GameServices");
/// <summary>
/// 尝试获取 AutoLoad <c>GameServices</c>。
/// </summary>
public static bool TryGetGameServices(out global::TestApp.GameServices? value)
{
return TryGetNode("GameServices", out value);
}
/// <summary>
/// 获取 AutoLoad <c>AudioBus</c>。
/// </summary>
public static global::Godot.Node AudioBus => GetRequiredNode<global::Godot.Node>("AudioBus");
/// <summary>
/// 尝试获取 AutoLoad <c>AudioBus</c>。
/// </summary>
public static bool TryGetAudioBus(out global::Godot.Node? value)
{
return TryGetNode("AudioBus", out value);
}
/// <summary>
/// 获取一个必填的 AutoLoad 节点;缺失时抛出异常。
/// </summary>
/// <typeparam name="TNode">节点类型。</typeparam>
/// <param name="autoLoadName">AutoLoad 名称。</param>
/// <returns>已解析的 AutoLoad 节点。</returns>
private static TNode GetRequiredNode<TNode>(string autoLoadName)
where TNode : global::Godot.Node
{
if (TryGetNode(autoLoadName, out TNode? value))
{
return value!;
}
throw new global::System.InvalidOperationException($"AutoLoad '{autoLoadName}' is not available on the active SceneTree root.");
}
/// <summary>
/// 尝试从当前 SceneTree 根节点解析 AutoLoad。
/// </summary>
/// <typeparam name="TNode">节点类型。</typeparam>
/// <param name="autoLoadName">AutoLoad 名称。</param>
/// <param name="value">解析到的节点实例。</param>
/// <returns>若当前进程存在 SceneTree 且根节点中能解析到该 AutoLoad则返回 <c>true</c>。</returns>
private static bool TryGetNode<TNode>(string autoLoadName, out TNode? value)
where TNode : global::Godot.Node
{
value = default;
if (global::Godot.Engine.GetMainLoop() is not global::Godot.SceneTree sceneTree)
{
return false;
}
var root = sceneTree.Root;
if (root is null)
{
return false;
}
value = root.GetNodeOrNull<TNode>($"/root/{autoLoadName}");
return value is not null;
}
}
""";
const string expectedInputActions = """
// <auto-generated />
#nullable enable
namespace GFramework.Godot.Generated;
/// <summary>
/// 提供 project.godot 中 Input Action 名称的强类型常量。
/// </summary>
public static partial class InputActions
{
/// <summary>
/// Input Action <c>move_up</c> 的稳定名称。
/// </summary>
public const string MoveUp = "move_up";
/// <summary>
/// Input Action <c>ui_cancel</c> 的稳定名称。
/// </summary>
public const string UiCancel = "ui_cancel";
}
""";
var result = AdditionalTextGeneratorTestDriver.Run<GodotProjectMetadataGenerator>( var result = AdditionalTextGeneratorTestDriver.Run<GodotProjectMetadataGenerator>(
source, source,
("project.godot", projectFile)); ("project.godot", $"{AutoLoadProjectFile}\n\n{InputActionsProjectFile}"));
var generatedSources = AdditionalTextGeneratorTestDriver.ToGeneratedSourceMap(result); var generatedSources = AdditionalTextGeneratorTestDriver.ToGeneratedSourceMap(result);
Assert.That(result.Results.Single().Diagnostics, Is.Empty); Assert.That(result.Results.Single().Diagnostics, Is.Empty);
Assert.That( Assert.That(
generatedSources["GFramework_Godot_Generated_AutoLoads.g.cs"], generatedSources["GFramework_Godot_Generated_AutoLoads.g.cs"],
Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(expectedAutoLoads))); Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(ExpectedAutoLoads)));
Assert.That( Assert.That(
generatedSources["GFramework_Godot_Generated_InputActions.g.cs"], generatedSources["GFramework_Godot_Generated_InputActions.g.cs"],
Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(expectedInputActions))); Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(ExpectedInputActions)));
} }
/// <summary> /// <summary>

View File

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

View File

@ -72,19 +72,8 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
if (bindNodeSignalAttribute is null || godotNodeSymbol is null) if (bindNodeSignalAttribute is null || godotNodeSymbol is null)
return; return;
// 缓存每个方法上已解析的特性,避免在筛选和生成阶段重复做语义查询。 var methodAttributes = BuildMethodAttributeMap(candidates, bindNodeSignalAttribute);
var methodAttributes = candidates var methodCandidates = CollectMethodCandidates(methodAttributes);
.Where(static candidate => candidate is not null)
.Select(static candidate => candidate!)
.ToDictionary(
static candidate => candidate,
candidate => ResolveAttributes(candidate.MethodSymbol, bindNodeSignalAttribute),
ReferenceEqualityComparer.Instance);
var methodCandidates = methodAttributes
.Where(static pair => pair.Value.Count > 0)
.Select(static pair => pair.Key)
.ToList();
foreach (var group in GroupByContainingType(methodCandidates)) foreach (var group in GroupByContainingType(methodCandidates))
{ {
@ -99,19 +88,7 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
UnbindMethodName)) UnbindMethodName))
continue; continue;
var bindings = new List<SignalBindingInfo>(); var bindings = CollectBindings(context, group, methodAttributes, godotNodeSymbol);
foreach (var candidate in group.Methods)
{
foreach (var attribute in methodAttributes[candidate])
{
if (!TryCreateBinding(context, candidate, attribute, godotNodeSymbol, out var binding))
continue;
bindings.Add(binding);
}
}
if (bindings.Count == 0) if (bindings.Count == 0)
continue; continue;
@ -171,99 +148,22 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
if (candidate.MethodSymbol.IsStatic) if (candidate.MethodSymbol.IsStatic)
{ {
ReportMethodDiagnostic( ReportStaticMethodDiagnostic(context, candidate, attribute);
context,
BindNodeSignalDiagnostics.StaticMethodNotSupported,
candidate,
attribute,
candidate.MethodSymbol.Name);
return false; return false;
} }
if (!TryResolveCtorString(attribute, 0, out var nodeFieldName)) if (!TryResolveBindingTargetNames(context, candidate, attribute, out var nodeFieldName, out var signalName))
{
ReportMethodDiagnostic(
context,
BindNodeSignalDiagnostics.InvalidConstructorArgument,
candidate,
attribute,
candidate.MethodSymbol.Name,
"nodeFieldName");
return false; return false;
}
if (!TryResolveCtorString(attribute, 1, out var signalName)) if (!TryFindCompatibleField(context, candidate, attribute, godotNodeSymbol, nodeFieldName, out var fieldSymbol))
{
ReportMethodDiagnostic(
context,
BindNodeSignalDiagnostics.InvalidConstructorArgument,
candidate,
attribute,
candidate.MethodSymbol.Name,
"signalName");
return false; return false;
}
var fieldSymbol = FindField(candidate.MethodSymbol.ContainingType, nodeFieldName); if (!TryFindCompatibleEvent(context, candidate, attribute, fieldSymbol, signalName, out var eventSymbol))
if (fieldSymbol is null)
{
ReportMethodDiagnostic(
context,
BindNodeSignalDiagnostics.NodeFieldNotFound,
candidate,
attribute,
candidate.MethodSymbol.Name,
nodeFieldName,
candidate.MethodSymbol.ContainingType.Name);
return false; 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)) if (!IsMethodCompatibleWithEvent(candidate.MethodSymbol, eventSymbol))
{ {
ReportMethodDiagnostic( ReportIncompatibleSignatureDiagnostic(context, candidate, attribute, eventSymbol, fieldSymbol);
context,
BindNodeSignalDiagnostics.MethodSignatureNotCompatible,
candidate,
attribute,
candidate.MethodSymbol.Name,
eventSymbol.Name,
fieldSymbol.Name);
return false; return false;
} }
@ -271,6 +171,235 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
return true; 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( private static void ReportMethodDiagnostic(
SourceProductionContext context, SourceProductionContext context,
DiagnosticDescriptor descriptor, DiagnosticDescriptor descriptor,
@ -404,11 +533,7 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
{ {
return typeSymbol.GetMembers() return typeSymbol.GetMembers()
.OfType<IMethodSymbol>() .OfType<IMethodSymbol>()
.FirstOrDefault(method => .FirstOrDefault(method => IsParameterlessInstanceMethod(method, methodName));
method.Name == methodName &&
!method.IsStatic &&
method.Parameters.Length == 0 &&
method.MethodKind == MethodKind.Ordinary);
} }
private static bool CallsGeneratedMethod( private static bool CallsGeneratedMethod(
@ -447,6 +572,14 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
}; };
} }
private static bool IsParameterlessInstanceMethod(IMethodSymbol method, string methodName)
{
return string.Equals(method.Name, methodName, StringComparison.Ordinal) &&
!method.IsStatic &&
method.Parameters.Length == 0 &&
method.MethodKind == MethodKind.Ordinary;
}
private static bool IsBindNodeSignalAttributeName(NameSyntax attributeName) private static bool IsBindNodeSignalAttributeName(NameSyntax attributeName)
{ {
var simpleName = GetAttributeSimpleName(attributeName); var simpleName = GetAttributeSimpleName(attributeName);
@ -608,4 +741,4 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
return RuntimeHelpers.GetHashCode(obj); return RuntimeHelpers.GetHashCode(obj);
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,517 @@
# 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

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

View File

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

View File

@ -2,516 +2,64 @@
## 目标 ## 目标
继续以“优先低风险、保持行为兼容”为原则收敛当前仓库的 Meziantou analyzer warnings并在首轮大规模清理完成后 继续以“直接看构建输出、直接修构建 warning”为原则推进当前分支并保持 active recovery 文档只保留当前真值。
判断剩余结构性 warning 是否值得在下一轮继续推进。
## 当前恢复点 ## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-041` - 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-051`
- 当前阶段:`Phase 41` - 当前阶段:`Phase 51`
- 当前焦点: - 当前焦点:
- 已通过第五个有效 subagent 切片完成 - `2026-04-24` 本轮已完成 `GFramework.Godot.SourceGenerators.Tests` warning 清理
`CqrsHandlerRegistryGeneratorTests.cs` - 当前主线程切片从生成器实现转到对应测试项目,并已把 `GFramework.Godot.SourceGenerators.Tests``24` 个 warning 降到 `0`
`Generates_Precise_Service_Type_For_Hidden_Generic_Type_Definitions()``MA0051` 收口: - 当前批次按 `origin/main` merge-base 计算的累计分支 diff 预计为 `23` 个文件,仍低于 `$gframework-batch-boot 75` 的主阈值
将内联 `source` 文本提取为类级常量,保持既有 expected 常量和断言语义不变 - 当前工作树除未跟踪的 `.codex` 目录外,还存在与本批次无关的既有文档 / 跟踪文件修改;提交当前批次时必须只包含本 topic 相关文件
- 当前 `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 是否继续推进尚未决策 - 之前记录的 plain `dotnet build` `0 Warning(s)` 属于增量构建假阴性,不能再作为 warning 检查真值
- `RP-001` 的详细实现历史、测试记录和 warning 热点清单已归档到主题内 `archive/` - 本轮已完成 `GFramework.Godot.SourceGenerators` warning 清理clean `Release` build 从 9 个 warning 降至 0 个 warning
- `RP-002` 已在不改公共契约的前提下完成 `CqrsHandlerRegistrar` 结构拆分,并通过定向 build/test 验证 - 当前已确认解决的文件包括 `BindNodeSignalGenerator.cs``GetNodeGenerator.cs``GodotProjectMetadataGenerator.cs``Registration/AutoRegisterExportedCollectionsGenerator.cs`
- `RP-003` 已在不改生命周期契约的前提下完成 `ArchitectureLifecycle` 初始化主流程拆分,并通过定向 build/test 验证 - 本轮直接执行仓库根目录 `dotnet clean` 仍在 `ValidateSolutionConfiguration` 阶段失败,输出未提供具体 error 文本
- `RP-004` 已完成当前 PR review follow-up修复 `TryCreateGeneratedRegistry` 的可空 `out` 契约并清理 trace 文档重复标题 - 本轮直接执行仓库根目录 `dotnet build` 成功,并给出 `1184 warning(s)` 的真实输出
- `RP-005` 已在不改公共 API 的前提下完成 `PauseStackManager` 两个 `MA0051` 的结构拆分,并补充销毁通知回归测试 - `GFramework.Godot.SourceGenerators.Tests` 已通过测试辅助模板抽取与 `ConfigureAwait(false)` 修正,当前 `Debug` / `Release` 构建均为 `0 Warning(s)`
- `RP-006` 已在不改公共 API 的前提下完成 `Store` 两个 `MA0051` 的结构拆分,并通过定向 build/test 验证 dispatch、 - 本轮已验证 `dotnet test GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release --no-build`,结果为 `Passed: 48`
多态 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 收窄成与既有契约不兼容的形状 - 如果后续继续依赖增量 `dotnet build`,容易再次把 warning 数量误判为 0
- 缓解措施:优先保留既有公共 API并将兼容性例外收敛到局部 pragma继续用反射断言覆盖返回类型、属性类型与异常类型 - 缓解措施:每轮 warning 检查前先执行 `dotnet clean`,再执行目标 `dotnet build`
- 测试宿主稳定性风险:部分 Godot 失败路径在当前 .NET 测试宿主下仍不稳定 - 仓库根目录 `dotnet clean` 目前仍然无法给出新的 clean 基线
- 缓解措施:继续优先使用稳定的 targeted test、项目构建和相邻 smoke test 组合验证 - 缓解措施:若下一轮继续做整仓 warning reduction先定位 `dotnet clean` 的 solution-level 失败原因,或明确继续沿用用户确认的 `1193 warning(s)` clean 基线与本轮 `1184 warning(s)` direct build 观测值
- 多目标框架 warning 解释风险:同一源位置会在多个 target framework 下重复计数 - 当前 worktree 已存在与本批次无关的未提交改动
- 缓解措施:继续以唯一源位置和 warning 家族为主要决策依据,而不是只看原始 warning 总数 - 缓解措施:提交当前批次时只暂存 `GFramework.Godot.SourceGenerators.Tests` 与对应 `ai-plan` 文件,避免混入其他 topic 变更
- 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](../archive/todos/analyzer-warning-reduction-history-rp001.md) - 当前轮次归档:
- 历史 trace 归档:[analyzer-warning-reduction-history-rp001.md](../archive/traces/analyzer-warning-reduction-history-rp001.md) - [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)
## 验证说明 ## 验证说明
- `RP-001` 的详细 warning 清理、回归修复与定向验证命令均已迁入主题内历史归档 - `dotnet clean`
- `RP-002` 的定向验证结果: - 结果:失败;停在 solution `ValidateSolutionConfiguration``0 Warning(s)``0 Error(s)`,未输出更具体的 error 文本
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:UseSharedCompilation=false -p:RestoreFallbackFolders=` - `dotnet build`
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter FullyQualifiedName~CqrsHandlerRegistrarTests -p:RestoreFallbackFolders=` - 结果:成功;`1184 Warning(s)``0 Error(s)`
- `RP-003` 的定向验证结果: - `dotnet build GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj`
- `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` - 初始结果:成功;`24 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~ArchitectureLifecycleBehaviorTests -p:RestoreFallbackFolders=` - 本轮收尾结果:成功;`0 Warning(s)``0 Error(s)`
- `RP-004` 的定向验证结果: - `dotnet build GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release`
- `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)`
- 结果:`0 Warning(s)``0 Error(s)` - `dotnet test GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release --no-build`
- `RP-005` 的定向验证结果: - 结果:成功;`Passed: 48``Failed: 0`
- `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 热点与验证记录 1. 提交当前 `GFramework.Godot.SourceGenerators.Tests` 清理批次,并确认提交只包含本 topic 相关文件
2. 下一轮优先继续 `GFramework.SourceGenerators.Tests``MA0051` 收口,并直接进入唯一剩余热点 2. 如果继续 warning reduction优先重新评估仓库根目录 `dotnet clean` 的 solution-level 失败,再决定是继续从整仓 `dotnet build` 输出挑热点,还是先修复 clean 基线采集问题
`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,9 +12,11 @@
## 当前恢复点 ## 当前恢复点
- 恢复点编号:`DOCUMENTATION-FULL-COVERAGE-GOV-RP-019` - 恢复点编号:`DOCUMENTATION-FULL-COVERAGE-GOV-RP-023`
- 当前阶段:`Phase 5 - Governance Maintenance` - 当前阶段:`Phase 5 - Governance Maintenance`
- 当前焦点: - 当前焦点:
- 保持 landing page / API 导航页中的仓库 README 入口可点击,避免读者在 docs 站点里遇到裸路径文本
- 继续按 `origin/main` 分支 diff 阈值做小批量文档治理,优先处理低风险导航 / 渲染热点
- 保持 `Game` persistence docs surface 与当前 `README`、源码、`PersistenceTests` 使用同一套 owner / adoption path 叙述 - 保持 `Game` persistence docs surface 与当前 `README`、源码、`PersistenceTests` 使用同一套 owner / adoption path 叙述
- 保持 `GFramework.Godot.SourceGenerators/README.md``docs/zh-CN/tutorials/godot-integration.md` 在生命周期接法上的一致性 - 保持 `GFramework.Godot.SourceGenerators/README.md``docs/zh-CN/tutorials/godot-integration.md` 在生命周期接法上的一致性
- 保持 active tracking / trace 只承载当前恢复入口,把阶段细节留在 `archive/` - 保持 active tracking / trace 只承载当前恢复入口,把阶段细节留在 `archive/`
@ -36,6 +38,26 @@
`SettingsModel&lt;ISettingsDataRepository&gt;` `SettingsModel&lt;ISettingsDataRepository&gt;`
- 结合当前 PR 已改动的 `docs/zh-CN/godot/storage.md` 做同类巡检后,确认 `SaveRepository&lt;TSaveData&gt;` - 结合当前 PR 已改动的 `docs/zh-CN/godot/storage.md` 做同类巡检后,确认 `SaveRepository&lt;TSaveData&gt;`
也会在 VitePress code span 中按字面量渲染;两处现已在本地统一改为真实泛型写法。 也会在 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 元数据,不是本地 - 当前剩余的托管侧信号是 GitHub `Title check` 对 PR 标题过泛的 inconclusive 提示;这属于 PR 元数据,不是本地
文件缺陷。 文件缺陷。
@ -62,14 +84,24 @@
`docs/zh-CN/godot/setting.md:75` 的 inline code HTML entity 渲染问题。 `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` - `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` 两处同类写法,均已修正。 - 结果:命中 `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/` - `2026-04-23` `bun run build`(工作目录:`docs/`
- 结果:通过;仅保留既有 VitePress 大 chunk warning无构建失败。 - 结果:通过;仓库 README 外链改为 GitHub `main` blob 后,不再触发 VitePress dead link仅保留既有大 chunk warning
## 下一步 ## 下一步
1. 提交并推送本地对 `docs/zh-CN/godot/setting.md``docs/zh-CN/godot/storage.md` 的 Markdown 泛型写法修正, 1. 若继续执行文档治理批处理,优先改做标题锚点、站内链接和少量非导航型裸路径引用的逐页复核,而不是继续按统一模板机械替换。
然后重新抓取 PR `#272` 确认 Greptile open thread 是否已在新 head commit 上消失。 2. 若后续继续扩展批处理 skill可考虑再补充显式单位写法例如 `75 files 2000 lines`,但当前默认速记已足够覆盖
2. 如果 PR `#272``Title check` 仍需要消除,到 GitHub 上把标题改成更具体的文档治理描述。 常见分支阈值场景
3. 若后续分支继续调整 `Game` persistence runtime、README 或公共 API优先复核 `docs/zh-CN/game/data.md` 3. 若后续分支继续调整 `Game` persistence runtime、README 或公共 API优先复核 `docs/zh-CN/game/data.md`
`storage.md``serialization.md``setting.md` 与 landing page 是否仍保持同一套职责边界。 `storage.md``serialization.md``setting.md` 与 landing page 是否仍保持同一套职责边界。
4. 若后续分支继续调整 `Godot` generator 接法,优先复核 `GFramework.Godot.SourceGenerators/README.md` 4. 若后续分支继续调整 `Godot` generator 接法,优先复核 `GFramework.Godot.SourceGenerators/README.md`

View File

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

View File

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

View File

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

View File

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

View File

@ -21,7 +21,7 @@ description: GFramework 的 API 阅读入口,按模块映射 README、专题
先读模块 README再读对应 landing page 先读模块 README再读对应 landing page
- 入门入口:[`../getting-started/index.md`](../getting-started/index.md) - 入门入口:[`../getting-started/index.md`](../getting-started/index.md)
- 根模块地图:仓库根 `README.md` - 根模块地图:仓库根 [`README.md`](https://github.com/GeWuYou/GFramework/blob/main/README.md)
### 想确认“这个功能属于哪个模块” ### 想确认“这个功能属于哪个模块”
@ -29,11 +29,11 @@ description: GFramework 的 API 阅读入口,按模块映射 README、专题
| 模块族 | 模块 README | 站内入口 | XML 文档关注点 | | 模块族 | 模块 README | 站内入口 | XML 文档关注点 |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `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) | 架构入口、生命周期、命令 / 查询 / 事件 / 状态 / 资源 / 日志 / 配置 / 并发契约 | | `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``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 | | `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``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、存储、序列化契约 | | `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``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 生成器入口 | | `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``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 模块契约、系统适配、配置对象和运行时装配边界 | | `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 模块契约、系统适配、配置对象和运行时装配边界 |
## 先看 XML还是先看教程 ## 先看 XML还是先看教程

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -195,4 +195,4 @@ finally
1. [context-get-generator.md](./context-get-generator.md) 1. [context-get-generator.md](./context-get-generator.md)
2. [logging-generator.md](./logging-generator.md) 2. [logging-generator.md](./logging-generator.md)
3. [../core/index.md](../core/index.md) 3. [../core/index.md](../core/index.md)
4. `GFramework.Core.SourceGenerators/README.md` 4. [`GFramework.Core.SourceGenerators/README.md`](https://github.com/GeWuYou/GFramework/blob/main/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) 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) 2. [/zh-CN/source-generators/godot-project-generator](./godot-project-generator.md)
3. [/zh-CN/godot/ui](../godot/ui.md) 3. [/zh-CN/godot/ui](../godot/ui.md)
4. `GFramework.Godot.SourceGenerators/README.md` 4. [`GFramework.Godot.SourceGenerators/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot.SourceGenerators/README.md)

View File

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

View File

@ -101,7 +101,7 @@ GFramework 当前发布的生成器包是:
## 对应模块入口 ## 对应模块入口
- `GFramework.Core.SourceGenerators/README.md` - [`GFramework.Core.SourceGenerators/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core.SourceGenerators/README.md)
- `GFramework.Game.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` - [`GFramework.Cqrs.SourceGenerators/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs.SourceGenerators/README.md)
- `GFramework.Godot.SourceGenerators/README.md` - [`GFramework.Godot.SourceGenerators/README.md`](https://github.com/GeWuYou/GFramework/blob/main/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) 1. [context-aware-generator.md](./context-aware-generator.md)
2. [context-get-generator.md](./context-get-generator.md) 2. [context-get-generator.md](./context-get-generator.md)
3. [../core/index.md](../core/index.md) 3. [../core/index.md](../core/index.md)
4. `GFramework.Core.SourceGenerators/README.md` 4. [`GFramework.Core.SourceGenerators/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core.SourceGenerators/README.md)

View File

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