fix(sourcegenerators-tests): 清理低风险 warning 基线

- 修复 GFramework.SourceGenerators.Tests 中低风险的 MA0004 与 MA0048,统一改为直接返回 Task 或在文件 I/O 上显式使用 ConfigureAwait(false)

- 更新 AnalyzerTestDriver 文件名与类型名对齐,避免测试基础设施继续产生文件命名 warning

- 更新 analyzer-warning-reduction 的 active tracking 与 trace,记录 RP-030 的 61 到 49 warnings-only 基线变化和验证结果
This commit is contained in:
gewuyou 2026-04-23 11:22:59 +08:00
parent dc8c5766dc
commit d0e86933cf
8 changed files with 115 additions and 41 deletions

View File

@ -13,7 +13,7 @@ public class SchemaConfigGeneratorEnumTests
/// 验证对象 <c>enum</c> 文档输出与快照保持一致。
/// </summary>
[Test]
public async Task Snapshot_Should_Preserve_Object_Enum_Documentation()
public Task Snapshot_Should_Preserve_Object_Enum_Documentation()
{
const string source = """
namespace TestApp
@ -51,14 +51,14 @@ public class SchemaConfigGeneratorEnumTests
("monster.schema.json", schema));
Assert.That(result.Results.Single().Diagnostics, Is.Empty);
await AssertSnapshotAsync(result, "MonsterConfig.ObjectEnum.g.txt");
return AssertSnapshotAsync(result, "MonsterConfig.ObjectEnum.g.txt");
}
/// <summary>
/// 验证数组项 <c>enum</c> 文档回退输出与快照保持一致。
/// </summary>
[Test]
public async Task Snapshot_Should_Preserve_Array_Item_Enum_Documentation_Fallback()
public Task Snapshot_Should_Preserve_Array_Item_Enum_Documentation_Fallback()
{
const string source = """
namespace TestApp
@ -88,14 +88,14 @@ public class SchemaConfigGeneratorEnumTests
("monster.schema.json", schema));
Assert.That(result.Results.Single().Diagnostics, Is.Empty);
await AssertSnapshotAsync(result, "MonsterConfig.ArrayItemEnum.g.txt");
return AssertSnapshotAsync(result, "MonsterConfig.ArrayItemEnum.g.txt");
}
/// <summary>
/// 验证对象数组项 <c>enum</c> 文档回退输出与快照保持一致。
/// </summary>
[Test]
public async Task Snapshot_Should_Preserve_Array_Object_Item_Enum_Documentation_Fallback()
public Task Snapshot_Should_Preserve_Array_Object_Item_Enum_Documentation_Fallback()
{
const string source = """
namespace TestApp
@ -136,7 +136,7 @@ public class SchemaConfigGeneratorEnumTests
("monster.schema.json", schema));
Assert.That(result.Results.Single().Diagnostics, Is.Empty);
await AssertSnapshotAsync(result, "MonsterConfig.ArrayObjectItemEnum.g.txt");
return AssertSnapshotAsync(result, "MonsterConfig.ArrayObjectItemEnum.g.txt");
}
/// <summary>
@ -176,11 +176,11 @@ public class SchemaConfigGeneratorEnumTests
if (!File.Exists(path))
{
Directory.CreateDirectory(snapshotFolder);
await File.WriteAllTextAsync(path, actual);
await File.WriteAllTextAsync(path, actual).ConfigureAwait(false);
Assert.Fail($"Snapshot not found. Generated new snapshot at:\n{path}");
}
var expected = await File.ReadAllTextAsync(path);
var expected = await File.ReadAllTextAsync(path).ConfigureAwait(false);
Assert.That(
Normalize(expected),
Is.EqualTo(Normalize(actual)),

View File

@ -12,7 +12,7 @@ public class SchemaConfigGeneratorSnapshotTests
/// 验证一个最小 monster schema 能生成配置类型、表包装和注册辅助。
/// </summary>
[Test]
public async Task Snapshot_SchemaConfigGenerator()
public Task Snapshot_SchemaConfigGenerator()
{
const string source = """
using System;
@ -183,12 +183,7 @@ public class SchemaConfigGeneratorSnapshotTests
"SchemaConfigGenerator");
snapshotFolder = Path.GetFullPath(snapshotFolder);
await AssertSnapshotAsync(generatedSources, snapshotFolder, "MonsterConfig.g.cs", "MonsterConfig.g.txt");
await AssertSnapshotAsync(generatedSources, snapshotFolder, "MonsterTable.g.cs", "MonsterTable.g.txt");
await AssertSnapshotAsync(generatedSources, snapshotFolder, "MonsterConfigBindings.g.cs",
"MonsterConfigBindings.g.txt");
await AssertSnapshotAsync(generatedSources, snapshotFolder, "GeneratedConfigCatalog.g.cs",
"GeneratedConfigCatalog.g.txt");
return AssertAllSnapshotsAsync(generatedSources, snapshotFolder);
}
/// <summary>
@ -213,17 +208,45 @@ public class SchemaConfigGeneratorSnapshotTests
if (!File.Exists(path))
{
Directory.CreateDirectory(snapshotFolder);
await File.WriteAllTextAsync(path, actual);
await File.WriteAllTextAsync(path, actual).ConfigureAwait(false);
Assert.Fail($"Snapshot not found. Generated new snapshot at:\n{path}");
}
var expected = await File.ReadAllTextAsync(path);
var expected = await File.ReadAllTextAsync(path).ConfigureAwait(false);
Assert.That(
Normalize(expected),
Is.EqualTo(Normalize(actual)),
$"Snapshot mismatch: {generatedFileName}");
}
/// <summary>
/// 依次验证 schema 生成器产出的全部核心快照文件。
/// </summary>
/// <param name="generatedSources">生成结果字典。</param>
/// <param name="snapshotFolder">快照目录。</param>
/// <returns>全部快照断言完成后的异步任务。</returns>
private static async Task AssertAllSnapshotsAsync(
IReadOnlyDictionary<string, string> generatedSources,
string snapshotFolder)
{
await AssertSnapshotAsync(generatedSources, snapshotFolder, "MonsterConfig.g.cs", "MonsterConfig.g.txt")
.ConfigureAwait(false);
await AssertSnapshotAsync(generatedSources, snapshotFolder, "MonsterTable.g.cs", "MonsterTable.g.txt")
.ConfigureAwait(false);
await AssertSnapshotAsync(
generatedSources,
snapshotFolder,
"MonsterConfigBindings.g.cs",
"MonsterConfigBindings.g.txt")
.ConfigureAwait(false);
await AssertSnapshotAsync(
generatedSources,
snapshotFolder,
"GeneratedConfigCatalog.g.cs",
"GeneratedConfigCatalog.g.txt")
.ConfigureAwait(false);
}
/// <summary>
/// 标准化快照文本以避免平台换行差异。
/// </summary>

View File

@ -15,7 +15,7 @@ public static class AnalyzerTestDriver<TAnalyzer>
/// <param name="source">测试输入源码。</param>
/// <param name="diagnostics">期望诊断集合。</param>
/// <returns>异步测试任务。</returns>
public static async Task RunAsync(
public static Task RunAsync(
string source,
params DiagnosticResult[] diagnostics)
{
@ -29,6 +29,6 @@ public static class AnalyzerTestDriver<TAnalyzer>
};
test.ExpectedDiagnostics.AddRange(diagnostics);
await test.RunAsync();
return test.RunAsync();
}
}

View File

@ -83,13 +83,13 @@ public static class GeneratorSnapshotTest<TGenerator>
{
// 第一次运行:生成 snapshot
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
await File.WriteAllTextAsync(path, content.ToString());
await File.WriteAllTextAsync(path, content.ToString()).ConfigureAwait(false);
Assert.Fail(
$"未找到快照文件,已在以下路径生成新快照:\n{path}");
}
var expected = await File.ReadAllTextAsync(path);
var expected = await File.ReadAllTextAsync(path).ConfigureAwait(false);
Assert.That(
Normalize(expected),

View File

@ -20,8 +20,8 @@ public class GeneratorSnapshotTestSecurityTests
var snapshotRoot = CreateSnapshotRoot();
var source = BuildSource();
Assert.ThrowsAsync<InvalidOperationException>(async () =>
await GeneratorSnapshotTest<EnumExtensionsGenerator>.RunAsync(
Assert.ThrowsAsync<InvalidOperationException>(() =>
GeneratorSnapshotTest<EnumExtensionsGenerator>.RunAsync(
source,
snapshotRoot,
_ => Path.Combine(snapshotRoot, "Status.EnumExtensions.g.cs")));
@ -36,8 +36,8 @@ public class GeneratorSnapshotTestSecurityTests
var snapshotRoot = CreateSnapshotRoot();
var source = BuildSource();
Assert.ThrowsAsync<InvalidOperationException>(async () =>
await GeneratorSnapshotTest<EnumExtensionsGenerator>.RunAsync(
Assert.ThrowsAsync<InvalidOperationException>(() =>
GeneratorSnapshotTest<EnumExtensionsGenerator>.RunAsync(
source,
snapshotRoot,
_ => Path.Combine("..", "escaped", "Status.EnumExtensions.g.cs")));

View File

@ -13,11 +13,11 @@ public static class GeneratorTest<TGenerator>
/// <param name="source">输入的源代码</param>
/// <param name="generatedSources">期望生成的源文件集合,包含文件名和内容的元组</param>
/// <returns>异步操作任务</returns>
public static async Task RunAsync(
public static Task RunAsync(
string source,
params (string filename, string content)[] generatedSources)
{
await RunAsync(
return RunAsync(
source,
additionalReferences: [],
generatedSources);
@ -30,7 +30,7 @@ public static class GeneratorTest<TGenerator>
/// <param name="additionalReferences">附加元数据引用,用于构造多程序集场景。</param>
/// <param name="generatedSources">期望生成的源文件集合,包含文件名和内容的元组。</param>
/// <returns>异步操作任务。</returns>
public static async Task RunAsync(
public static Task RunAsync(
string source,
IEnumerable<MetadataReference> additionalReferences,
params (string filename, string content)[] generatedSources)
@ -52,7 +52,7 @@ public static class GeneratorTest<TGenerator>
foreach (var additionalReference in additionalReferences)
test.TestState.AdditionalReferences.Add(additionalReference);
await test.RunAsync();
return test.RunAsync();
}
/// <summary>

View File

@ -7,8 +7,8 @@
## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-029`
- 当前阶段:`Phase 29`
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-030`
- 当前阶段:`Phase 30`
- 当前焦点:
- 已完成 `GFramework.Core` 当前 `MA0016` / `MA0002` / `MA0015` / `MA0077` 低风险收口批次
- 已复核 `net10.0` 下的 `MA0158` 基线:`GFramework.Core` / `GFramework.Cqrs` 当前共有 `16` 个 object lock
@ -50,9 +50,12 @@
- 当前 `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.Godot``Timing.cs` 已同步适配新事件签名,但当前 worktree 的 Godot restore 资产仍受 Windows fallback package folder 干扰,独立 build 需在修复资产后补跑
- 后续继续按 warning 类型和数量批处理,而不是回退到按单文件切片推进
- 下一轮默认继续拆分 `GFramework.Game.SourceGenerators``MA0051` 热点,或评估跨 target 的 `MA0158`
- 下一轮默认继续拆分 `GFramework.SourceGenerators.Tests` 的 `MA0051` 热点,或评估跨 target 的 `MA0158`
锁替换风险
- 单次 `boot` 的工作树改动上限控制在约 `100` 个文件以内,避免 recovery context 与 review 面同时失控
- 若任务边界互不冲突,允许使用不同模型的 subagent 并行处理不同 warning 类型或不同目录,但必须遵守显式 ownership
@ -87,6 +90,8 @@
- 已完成当前分支与 `main``CqrsHandlerRegistryGenerator.cs` 冲突化解:保留当前 partial 结构,并把
`main` 侧新增的模型文档合并到 `CqrsHandlerRegistryGenerator.Models.cs`
- 已完成 `GFramework.Game.SourceGenerators``SchemaConfigGenerator` 的剩余 `MA0051` 收口warnings-only 基线已降到 `0`
- 已完成 `GFramework.SourceGenerators.Tests` 的首轮低风险 warning 清理;当前项目已清空 `MA0004` / `MA0048`,剩余 warning
全部收敛为 `MA0051`
## 当前活跃事实
@ -144,6 +149,9 @@
并把“变更模块必须运行对应 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`
- 当前工作树分支 `fix/analyzer-warning-reduction-batch` 已在 `ai-plan/public/README.md` 建立 topic 映射
## 当前风险
@ -156,13 +164,12 @@
- 缓解措施:继续以唯一源位置和 warning 家族为主要决策依据,而不是只看原始 warning 总数
- net10 专属 warning 风险:`MA0158` 建议使用 `System.Threading.Lock`,但项目多 target 时需要确认兼容边界
- 缓解措施:下一轮先按 target framework 与 API 可用性评估,不直接批量替换共享源码中的 `object` lock
- source generator warning 外溢风险:运行 `GFramework.SourceGenerators.Tests` 会构建相邻 generator/test 项目并显示既有
`GFramework.Game.SourceGenerators` 与测试项目 warning
- 缓解措施:继续以被修改 generator 项目的独立 warnings-only build 作为主验收,并用 focused generator test 验证行为
- source generator test warning 治理风险:`GFramework.SourceGenerators.Tests` 当前仍有既有 `MA0051` / `MA0004` / `MA0048`
warning本轮 focused test 已通过,但测试项目整包 warning 尚未进入本轮写集
- 缓解措施:本轮已在 failed-test follow-up 的定向 `dotnet test` 中再次确认这些 warning 仍为既有基线;后续若继续修改该测试项目,
应按新增 `AGENTS.md` 规则先明确 warning 收口范围,再决定是否进入专门清理切片
- source generator warning 外溢风险:运行 `GFramework.SourceGenerators.Tests` 会构建相邻 generator/test 项目,并在输出中混入
测试项目自身的结构性 warning 基线
- 缓解措施:继续以被修改项目的独立 warnings-only build 作为主验收,并用 focused generator test 验证行为
- source generator test warning 治理风险:`GFramework.SourceGenerators.Tests` 当前仍有 `49` 条既有 `MA0051` warning
一旦继续进入该写集,就必须把测试项目 warning 一并纳入本轮完成条件
- 缓解措施:已先清空低风险 `MA0004` / `MA0048`,后续继续保持“单 warning family、单测试域”的节奏推进 `MA0051`
- ContextAware 基类命名隐藏风险:若生成器只看当前类型声明成员,派生规则会重新占用基类已声明的
`_gFrameworkContextAware*` 字段名,导致生成成员隐藏继承状态并让快照无法锁定后缀分配行为
- 缓解措施:本轮已改为遍历完整 base-type 链收集保留名,并用 inherited collision 快照用例锁定该行为
@ -325,13 +332,20 @@
- 结果:`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`
- active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史
## 下一步
1. 若要继续该主题,先读 active tracking再按需展开历史归档中的 warning 热点与验证记录
2. 下一轮优先评估是否将 `GFramework.SourceGenerators.Tests` 的既有 `MA0051` / `MA0004` / `MA0048` 作为独立 warning
清理切片;若进入该写集,需要先明确测试项目 warning 也属于本轮必须处理的模块范围
2. 下一轮优先继续 `GFramework.SourceGenerators.Tests``MA0051` 收口,先在单一测试域内选择一个文件或一组同构 snapshot
用例拆分,不再把已清零的 `MA0004` / `MA0048` 混回写集
3. 若改回推进 `MA0158`,先设计 `net8.0` / `net9.0` / `net10.0` 多 target 条件编译方案,不直接批量替换共享源码中的
`object` lock
4. 若后续继续改动 `GFramework.Godot`,先修复该项目的 Linux 侧 restore 资产,再补跑独立 build

View File

@ -1,5 +1,42 @@
# Analyzer Warning Reduction 追踪
## 2026-04-23 — RP-030
### 阶段:`GFramework.SourceGenerators.Tests` 低风险 `MA0004` / `MA0048` 收口RP-030
- 启动复核:
- 按 `gframework-boot` 流程恢复当前 worktree 后,读取 `AGENTS.md``.ai/environment/tools.ai.yaml`
`ai-plan/public/README.md` 与 active topic 跟踪文件,确认当前分支 `fix/analyzer-warning-reduction-batch`
仍映射到 `analyzer-warning-reduction`
- 先对 `GFramework.SourceGenerators.Tests` 执行
`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"`
复核当前基线,确认该测试项目共有 `61` 条 warning其中低风险切片集中在 `MA0004` 与单个 `MA0048`
- 决策:
- 不直接进入大型 snapshot/test 方法的 `MA0051`,先收口纯 test-infrastructure 层的 `MA0004` / `MA0048`
- 对“只是转发异步调用”的 helper 直接返回 `Task`,只在真实文件 I/O 上显式补 `ConfigureAwait(false)`,避免无意义的
`async/await` 包装
- 将 `AnalyzerTestDriver<TAnalyzer>` 所在文件改名为与类型一致,单独清理 `MA0048`,不改类型名与调用方契约
- 实施调整:
- 将 `AnalyzerTestDriver.RunAsync(...)``GeneratorTest.RunAsync(...)` 改为直接返回下游 `Task`
- 为 `GeneratorSnapshotTest``SchemaConfigGeneratorSnapshotTests``SchemaConfigGeneratorEnumTests` 中的异步文件读写
显式补齐 `ConfigureAwait(false)`,并把仅作转发的测试方法改为直接返回 `Task`
- 将 `GeneratorSnapshotTestSecurityTests``Assert.ThrowsAsync(...)` 改为直接返回目标 `Task`,移除无收益的
`async` 包装
- 将 `GFramework.SourceGenerators.Tests/Core/AnalyzerTest.cs` 重命名为
`GFramework.SourceGenerators.Tests/Core/AnalyzerTestDriver.cs`
- 验证结果:
- `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`
- 下一步建议:
- 若继续 analyzer warning reduction继续把 `GFramework.SourceGenerators.Tests` 作为独立写集,只处理 `MA0051`
- 下一轮优先选择单一测试域的同构长方法,例如 `LoggerGeneratorSnapshotTests``AutoRegisterModuleGeneratorTests`
或共享 helper `GeneratorSnapshotTest`
## 2026-04-23 — RP-029
### 阶段:`SchemaConfigGenerator.cs` 剩余 `MA0051` 收口RP-029