From a0ce04b185d11041ade6ca74ff0f03ac1d4e9228 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Fri, 24 Apr 2026 10:46:17 +0800 Subject: [PATCH 1/9] =?UTF-8?q?fix(godot):=20=E6=94=B6=E7=B4=A7=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0=E5=8C=96=E6=98=A0=E5=B0=84=E9=9B=86=E5=90=88=E6=9A=B4?= =?UTF-8?q?=E9=9C=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 LocalizationMap 对可变 Dictionary 的直接公共暴露,降低 MA0016 集合暴露风险 - 新增复制输入映射的构造函数,并保留默认映射初始化行为以维持现有消费者兼容性 - 更新 XML 注释,明确只读访问语义和内部状态隔离原因 --- .../Setting/Data/LocalizationMap.cs | 61 ++++++++++++++++--- 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/GFramework.Godot/Setting/Data/LocalizationMap.cs b/GFramework.Godot/Setting/Data/LocalizationMap.cs index fffcd0f7..369d0fc3 100644 --- a/GFramework.Godot/Setting/Data/LocalizationMap.cs +++ b/GFramework.Godot/Setting/Data/LocalizationMap.cs @@ -20,24 +20,47 @@ public class LocalizationMap { private const string DefaultFrameworkLanguage = "eng"; private const string DefaultGodotLocale = "en"; + private readonly Dictionary _frameworkLanguageMap; + private readonly Dictionary _languageMap; /// - /// 用户语言 -> Godot locale 映射表。 + /// 使用默认的 Godot locale 与框架语言码映射初始化本地化设置。 /// - public Dictionary LanguageMap { get; set; } = new(StringComparer.Ordinal) + public LocalizationMap() + : this(CreateDefaultLanguageMap(), CreateDefaultFrameworkLanguageMap()) { - { "简体中文", "zh_CN" }, - { "English", "en" } - }; + } /// - /// 用户语言 -> GFramework 本地化语言码映射表。 + /// 使用外部提供的映射初始化本地化设置。 + /// 构造函数会复制输入字典,避免调用方在实例创建后继续修改内部状态。 /// - public Dictionary FrameworkLanguageMap { get; set; } = new(StringComparer.Ordinal) + /// 用户语言到 Godot locale 的映射。 + /// 用户语言到 GFramework 本地化语言码的映射。 + /// + /// 当 时抛出。 + /// + public LocalizationMap( + IReadOnlyDictionary languageMap, + IReadOnlyDictionary frameworkLanguageMap) { - { "简体中文", "zhs" }, - { "English", "eng" } - }; + ArgumentNullException.ThrowIfNull(languageMap); + ArgumentNullException.ThrowIfNull(frameworkLanguageMap); + + // 复制外部输入,避免公共属性把可变集合直接暴露给调用方。 + _languageMap = new Dictionary(languageMap, StringComparer.Ordinal); + _frameworkLanguageMap = new Dictionary(frameworkLanguageMap, StringComparer.Ordinal); + } + + /// + /// 获取用户语言到 Godot locale 的只读映射表。 + /// + public IReadOnlyDictionary LanguageMap => _languageMap; + + /// + /// 获取用户语言到 GFramework 本地化语言码的只读映射表。 + /// + public IReadOnlyDictionary FrameworkLanguageMap => _frameworkLanguageMap; /// /// 解析用户保存的语言值对应的 Godot locale。 @@ -68,4 +91,22 @@ public class LocalizationMap return FrameworkLanguageMap.GetValueOrDefault(storedLanguage, DefaultFrameworkLanguage); } + + private static Dictionary CreateDefaultLanguageMap() + { + return new Dictionary(StringComparer.Ordinal) + { + { "简体中文", "zh_CN" }, + { "English", "en" } + }; + } + + private static Dictionary CreateDefaultFrameworkLanguageMap() + { + return new Dictionary(StringComparer.Ordinal) + { + { "简体中文", "zhs" }, + { "English", "eng" } + }; + } } From 091b872c860e9fd6cc6fcd6727a323a9574c0ae4 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Fri, 24 Apr 2026 11:57:49 +0800 Subject: [PATCH 2/9] =?UTF-8?q?docs(ai-plan):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E5=91=8A=E8=AD=A6=E5=9F=BA=E7=BA=BF=E8=BF=BD=E8=B8=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 analyzer warning reduction 的 active tracking 到 RP-046 并记录 solution 级 891 条 warning 基线 - 补充前台构建与日志采集形态不一致的环境风险和后续恢复建议 --- .../analyzer-warning-reduction-tracking.md | 86 +++++++++++++------ .../analyzer-warning-reduction-trace.md | 83 +++++++++++++----- 2 files changed, 122 insertions(+), 47 deletions(-) diff --git a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md index 0cba238d..707b2f78 100644 --- a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md +++ b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md @@ -6,36 +6,59 @@ ## 当前恢复点 -- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-042` -- 当前阶段:`Phase 42` +- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-046` +- 当前阶段:`Phase 46` - 当前焦点: - - 已于 `2026-04-24` 使用 `gframework-pr-review` 复核当前分支 PR #280,latest-head review 仍有 `3` 条 open threads - - 本地确认这 `3` 条 open threads 均指向 `ai-plan` 文档:错误归档链接、`rp002-rp041` trace 混入 `RP-001` 段落,以及 active trace 的恢复信息失真 - - 本轮按最小写集直接修正文档恢复入口,不再扩大 `GFramework.SourceGenerators.Tests` 的代码写集 - - `RP-041` 验证完成时,分支相对 `origin/main` 的唯一变更文件数为 `4`;这说明继续只处理同一热点文件时,该指标增长会很慢 - - `GFramework.SourceGenerators.Tests` 在 `RP-042` 的 `net10.0` Release build 中仍为 `10` 条 `MA0051` warning、`0` error;剩余热点继续集中在 `CqrsHandlerRegistryGeneratorTests.cs` + - 已按用户更正后的要求执行前台 `dotnet build GFramework.sln -c Release`,收集当前工作树的 solution 级 warning 基线,并将结果回写 active plan + - 当前前台 solution Release build 已能稳定完成,结果为 `891 Warning(s)` / `0 Error(s)` / `Time Elapsed 00:00:18.57` + - 当前 baseline 仍为 `origin/main` (`e692ed3`, `2026-04-24 09:36:17 +0800`);分支仍映射到 `fix/analyzer-warning-reduction-batch` / `GFramework-analyzer` + - 当前直接观察到的热点 warning 规则集中在 `MA0051`、`MA0158`、`MA0004`,并伴随一部分 `MA0006`、`MA0002`、`MA0009` + - 当前直接观察到的热点模块集中在 `GFramework.Godot.SourceGenerators`、`GFramework.Godot.SourceGenerators.Tests`、`GFramework.Core`、`GFramework.Game`、`GFramework.Cqrs`、`GFramework.Godot` + - 非前台形态的同命令当前不稳定:重定向到文件、`script` 分配 TTY、以及若干 logger 组合都曾快速返回 `Build FAILED / 0 Warning(s) / 0 Error(s)` 或 `Restore failed` ## 当前状态摘要 -- 已修正 `archive/todos/analyzer-warning-reduction-history-rp002-rp041.md` 中指向 `RP-001` 归档的相对链接,恢复历史入口可点击性 -- 已从 `archive/traces/analyzer-warning-reduction-history-rp002-rp041.md` 中移除误混入的 `RP-001` 段落,确保文件名与内容范围一致 -- 已刷新 active tracking / trace 的恢复点描述,使其反映当前仍待远端收敛的是文档类 review threads,而不是已处理过的代码项 -- PR #280 的 MegaLinter 仍显示 `dotnet-format` warning,但测试报告为 `2156 passed / 0 failed`;该 warning 目前更像 CI 环境 restore / SDK 噪音,而不是本地代码行为回归 +- 已通过 explorer `Rawls` 盘点出本轮最适合并行推进的低风险切片: + - `GFramework.Game/Data/UnifiedSettingsFile.cs` 的 `MA0016` + - `GFramework.Godot/Setting/Data/LocalizationMap.cs` 的 `MA0016` + - `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 的 `MA0051` +- 已通过 explorer `Arendt` 收敛过旧构建根因:当前离线缓存只有 `Meziantou.Polyfill 1.0.110`,缺少项目请求的 `1.0.116`,导致 restore 失败并使 source-generator 相关 `netstandard2.0` 项目缺失有效引用图 +- worker `Aquinas` 已独立完成 `LocalizationMap` 收口并生成提交 `a0ce04b`(`fix(godot): 收紧本地化映射集合暴露`) +- worker `Boyle` 已完成 `UnifiedSettingsFile` / `UnifiedSettingsDataRepository` 的最小 API 形状修正,并同步更新 active todo / trace 草稿 +- 主线程本轮重新执行前台 `dotnet build GFramework.sln -c Release`,构建成功并暴露出完整 warning 面,说明当前工作树至少在交互式前台构建路径上已经恢复到可收集 analyzer baseline 的状态 +- 从实时输出可直接确认的热点包括: + - `GFramework.Godot.SourceGenerators/GodotProjectMetadataGenerator.cs`、`BindNodeSignalGenerator.cs`、`GetNodeGenerator.cs`、`Registration/AutoRegisterExportedCollectionsGenerator.cs` + - `GFramework.Godot.SourceGenerators.Tests/**` 多个 generator 测试文件 + - `GFramework.Cqrs/Internal/WeakKeyCache.cs` + - `GFramework.Core/**` 多个锁相关组件,如 `SamplingFilter.cs`、`BindableProperty.cs`、`FileAppender.cs`、`ResourceHandle.cs` + - `GFramework.Game/Config/YamlConfigSchemaValidator*.cs`、`GFramework.Game/UI/UiRouterBase.cs`、`GFramework.Game/Storage/FileStorage.cs` +- 同一轮中,多次尝试把相同命令切换到“重定向日志”“TTY 包裹”或“附加 logger 输出到文件”的形态时,构建又会退化成 `Build FAILED / 0 Warning(s) / 0 Error(s)` 或 `Restore failed` +- 这说明当前环境不只是“能否构建”的问题,还存在“前台构建”和“非前台采集”结果不一致的执行形态漂移 ## 当前活跃事实 -- 当前主题仍保持 active,因为 `GFramework.SourceGenerators.Tests` 尚有剩余 `MA0051` warning 需要决定是否继续推进 -- 继续按“单文件单方法”节奏处理 `CqrsHandlerRegistryGeneratorTests.cs` 可以稳定消除 warning,但不利于快速提高唯一变更文件数 -- 当前 PR review 已没有新的 failed-test 信号;当前优先级是提交这轮 `ai-plan` 修正并等待远端 PR threads 收敛 +- 当前主题仍保持 active,因为 analyzer warning reduction 主任务尚未结束,而且本轮新增了可直接消费的 solution warning baseline +- `CqrsHandlerRegistryGeneratorTests.cs` 当前已不再保留方法内 `const string source = """..."""` 型大型 fixture;后续只需回到真实 warning 热点继续收敛 +- `UnifiedSettingsFile.Sections` 改为 `IDictionary` 后,`CloneFile` 仍会在底层是 `Dictionary` 时保留 comparer,从而避免改变现有 key 比较行为 +- `LocalizationMap` 现在通过私有只读字典与 `IReadOnlyDictionary` 暴露映射,消费者 `GodotLocalizationSettings` 仍只按只读方式使用这些映射 +- 当前 worktree 的推荐构建入口仍是 `bash scripts/dotnet-wsl.sh ...` +- 当前前台 `dotnet build GFramework.sln -c Release` 的结果是 `891` 条 warning、`0` 条 error,且已确认不是“0 warning 的假成功” +- 当前 warning 规则的已观察集合包括 `MA0051`、`MA0158`、`MA0004`、`MA0006`、`MA0002`、`MA0009` +- 当前“非前台采集”路径仍不可信:把输出重定向到文件、追加 file logger 或经 `script` 分配 TTY 时,都未稳定复现前台结果 +- 先前已识别的 `--no-restore` 资产文件漂移、`Meziantou.Polyfill 1.0.116` 缺失、以及 `NU1301` 风险仍保留,但本轮用户要求的 warning 基线以成功的前台 `dotnet build` 结果为准 ## 当前风险 -- warning 治理策略风险:如果用户仍以“唯一变更文件数接近 `75`”作为目标,继续深挖同一测试文件会让目标推进缓慢 - - 缓解措施:下一轮先确认是继续压低 `MA0051` 基线,还是切换到新的文件写集 -- WSL 构建环境风险:当前 worktree 的 .NET 定向验证仍需显式附带 `-p:RestoreFallbackFolders=`,并在沙箱外运行以规避命名管道 / socket 限制 - - 缓解措施:后续所有 affected-project Release build 继续复用该参数组合 -- source generator test warning 范围风险:一旦继续触达 `GFramework.SourceGenerators.Tests`,剩余 warning 会继续成为本轮完成条件的一部分 - - 缓解措施:继续用最小写集和 warnings-only build 锁定范围 +- warning 采集形态漂移风险:同一个 `dotnet build GFramework.sln -c Release` 在前台可成功输出 `891` warnings,但一旦切到日志重定向、TTY 包裹或特定 logger 组合,就可能在约 `1` 秒内快速失败 + - 缓解措施:短期内把“前台普通构建”作为 warning 基线真值来源;若需要自动化统计,再单独排查 stdout/TTY/logger 相关环境差异 +- SDK / workload resolver 环境漂移风险:历史诊断样本里 Linux .NET SDK `10.0.106` 在 solution restore 图阶段报告过 `Microsoft.NET.SDK.WorkloadAutoImportPropsLocator` 缺失 + - 缓解措施:后续若要恢复脚本化或 `--no-restore` 验证,再重新检查当前 WSL `dotnet --info`、workload 解析器状态与仓库推荐构建入口是否一致 +- 资产文件环境漂移风险:历史样本中的根项目 `project.assets.json` 明显来自 Windows restore,上游记录的 fallback package folder 在当前 WSL 会话不存在 + - 缓解措施:需要重新启用 `--no-restore` 验证时,先用与 WSL 兼容的 NuGet / restore 配置重建根项目资产文件 +- 构建环境可达性风险:`Meziantou.Polyfill 1.0.116` 缺失与 `NU1301` 在旧样本中仍是有效线索 + - 缓解措施:后续若再次命中 restore 阻塞,优先核查本地缓存版本与 NuGet 可达性 +- reviewability 风险:当前分支只提交了 `LocalizationMap` 一个 warning 切片,而工作树仍保留 `ai-plan` 等未提交变更 + - 缓解措施:后续 warning reduction 继续按模块或规则切片提交,不把多个热点混成单个大提交 ## 活跃文档 @@ -48,13 +71,22 @@ ## 验证说明 -- `DOTNET_CLI_HOME=/tmp/dotnet-home dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders=""` - - 结果:通过;重写了受 Windows fallback package folder 影响的测试项目资产文件 -- `DOTNET_CLI_HOME=/tmp/dotnet-home dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release -t:Rebuild --no-restore --disable-build-servers -m:1 -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"` - - 结果:`10 Warning(s)`,`0 Error(s)`;warning 仍全部来自 `CqrsHandlerRegistryGeneratorTests.cs` 的既有 `MA0051` 基线 +- `dotnet build GFramework.sln -c Release` + - 结果:成功;`891 Warning(s)`、`0 Error(s)`、`Time Elapsed 00:00:18.57` + - 补充:当前 warning 流可直接从前台交互式构建观察到;热点规则以 `MA0051`、`MA0158`、`MA0004` 为主 +- `dotnet build GFramework.sln -c Release > /tmp/gframework-build-warnings.log 2>&1` + - 结果:失败;约 `0.78s` 结束,摘要仅显示 `Build FAILED / 0 Warning(s) / 0 Error(s)` +- `dotnet build GFramework.sln -c Release '/flp:logfile=/tmp/gframework-build-warnings.log;verbosity=normal'` + - 结果:成功;但日志文件只保留构建摘要,没有留下可消费的 warning 行 +- `dotnet build GFramework.sln -c Release '/flp1:logfile=/tmp/gframework-build-warnings-only.log;warningsonly'` + - 结果:成功;但 warning-only 日志文件为空 +- `script -q -c "dotnet build GFramework.sln -c Release" /tmp/gframework-build-full-typescript.log` + - 结果:失败;TTY 形态下 restore 于约 `0.8s` 退出 +- `git --git-dir=/mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework/.git/worktrees/GFramework-analyzer --work-tree=/mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework-WorkTree/GFramework-analyzer branch --show-current` + - 结果:成功;当前分支为 `fix/analyzer-warning-reduction-batch` ## 下一步建议 -1. 提交 `RP-042` 后重新抓取 PR #280 review,确认这 `3` 条 latest-head open threads 是否随新提交收敛 -2. 若 PR threads 收敛,再决定下一轮是继续清理 `CqrsHandlerRegistryGeneratorTests.cs` 的剩余 `MA0051`,还是切换到新的文件写集 -3. 如果仍要继续沿用“唯一变更文件数接近 `75`”的目标,应优先切到新的 warning 写集,而不是继续深挖同一测试文件 +1. 先以本轮前台 `dotnet build GFramework.sln -c Release` 的 `891` warning baseline 为起点,优先从 `MA0051` / `MA0158` 最密集的模块切分下一批低风险整改 +2. 并行保留环境核查:检查为什么相同命令在重定向日志、TTY 包裹或 logger 组合下会快速失败,避免后续 warning 统计自动化再次失真 +3. 在需要重新启用 `--no-restore` 或脚本化验证时,再回到根 `GFramework.csproj` 资产文件漂移、SDK workload resolver、以及 `Meziantou.Polyfill 1.0.116` / `NU1301` 这几条环境线索 diff --git a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md index 76acd1af..eb3a3153 100644 --- a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md +++ b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md @@ -1,28 +1,71 @@ # Analyzer Warning Reduction 追踪 -## 2026-04-24 — RP-042 +## 2026-04-24 — RP-046 -### 阶段:PR #280 review follow-up 与 ai-plan 恢复入口修正 +### 阶段:solution warning baseline 采样与 active plan 回写 -- 启动复核: - - 使用 `gframework-pr-review` 抓取当前分支 PR #280 的 latest-head review threads、MegaLinter 摘要与测试报告 - - 本地核对后确认 `3` 条 open threads 均仍成立,但全部集中在 `ai-plan` 文档恢复入口,而不是新的代码行为问题 -- 决策: - - 不再继续扩大 `GFramework.SourceGenerators.Tests` 的写集,先把远端 latest-head review 中仍成立的文档问题全部收口 - - 保持 `RP-042` 作为 active recovery point,仅刷新其事实描述、归档链接和 trace 范围边界 -- 实施调整: - - 修正 `archive/todos/analyzer-warning-reduction-history-rp002-rp041.md` 中两条指向 `RP-001` 归档的相对链接 - - 从 `archive/traces/analyzer-warning-reduction-history-rp002-rp041.md` 中移除误混入的 `RP-001` 段落,使文件只保留 `RP-002` 到 `RP-041` - - 刷新 active tracking / trace 的恢复点描述,明确当前 open threads 已收敛为文档问题,并记录本轮 follow-up 的事实与下一步 -- 验证结果: - - `DOTNET_CLI_HOME=/tmp/dotnet-home dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release -t:Rebuild --no-restore --disable-build-servers -m:1 -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"` - - 结果:`10 Warning(s)`,`0 Error(s)`;warning 仍全部来自 `CqrsHandlerRegistryGeneratorTests.cs` 的既有 `MA0051` 基线 +- 触发背景: + - 用户纠正本轮目标为“执行 `dotnet build` 收集当前项目 warning,并更新当前工作树激活计划” + - active topic 仍为 `analyzer-warning-reduction`,因此本轮需要先确认 solution 级 warning baseline 是否可直接从当前工作树获取 +- 主线程实施: + - 直接执行前台 `dotnet build GFramework.sln -c Release` + - 构建成功,得到 `891 Warning(s)` / `0 Error(s)` / `Time Elapsed 00:00:18.57` + - 从实时输出中确认 warning 热点主要集中在 `GFramework.Godot.SourceGenerators`、`GFramework.Godot.SourceGenerators.Tests`、`GFramework.Core`、`GFramework.Game`、`GFramework.Cqrs`、`GFramework.Godot` + - 从实时输出中确认规则热点以 `MA0051`、`MA0158`、`MA0004` 为主,并伴随 `MA0006`、`MA0002`、`MA0009` + - 追加尝试把同一命令改为“重定向到日志文件”“附加 file logger”“`script` 分配 TTY”几种采集方式;这些路径都未稳定复现前台结果,而是出现 `Build FAILED / 0 Warning(s) / 0 Error(s)` 或 `Restore failed` + - 基于上述差异,本轮把“前台普通构建”视为 warning baseline 真值来源,并把采集形态漂移记录为环境风险 +- 本轮验证结果: + - `dotnet build GFramework.sln -c Release` + - 结果:成功;`891 Warning(s)`、`0 Error(s)` + - `dotnet build GFramework.sln -c Release > /tmp/gframework-build-warnings.log 2>&1` + - 结果:失败;约 `0.78s` 结束,摘要仅显示 `Build FAILED / 0 Warning(s) / 0 Error(s)` + - `dotnet build GFramework.sln -c Release '/flp:logfile=/tmp/gframework-build-warnings.log;verbosity=normal'` + - 结果:成功;但日志文件只保留了构建摘要,没有留下 warning 行 + - `dotnet build GFramework.sln -c Release '/flp1:logfile=/tmp/gframework-build-warnings-only.log;warningsonly'` + - 结果:成功;但 warning-only 日志文件为空 + - `script -q -c "dotnet build GFramework.sln -c Release" /tmp/gframework-build-full-typescript.log` + - 结果:失败;TTY 形态下 restore 于约 `0.8s` 退出 - 当前结论: - - PR #280 当前没有 failed-test 回归信号;latest-head review 剩余项可以全部在 `ai-plan` 范围内处理 - - active 恢复入口与历史归档范围已重新对齐,后续 `boot` 不会再从 `rp002-rp041` 误读 `RP-001` -- 下一步建议: - - 提交后重新抓取 PR #280 review,确认 open threads 是否收敛 - - 若 threads 收敛,则回到 `CqrsHandlerRegistryGeneratorTests.cs` 剩余 `MA0051`,或根据目标改切新的 warning 写集 + - 当前工作树的 solution 级 warning baseline 可以通过普通前台 `dotnet build` 获取,且样本值是 `891` 条 warning、`0` 条 error + - 当前环境对 stdout/TTY/logger 形态敏感,不能把“把输出落到文件”的结果直接当作同等可信的构建事实 + - 下一轮 warning reduction 应以本轮前台 baseline 为准,而不是继续围绕空日志或快速失败结果做误判 + +## 2026-04-24 — RP-045 + +### 阶段:solution no-restore 阻塞面采样与 active plan 回写 + +- 触发背景: + - 用户要求显式执行 `dotnet build GFramework.sln -c Release --no-restore`,收集当前项目报错并同步更新当前工作树激活计划 + - active topic 仍为 `analyzer-warning-reduction`,因此本轮的核心工作是把 solution 级失败面与先前的 restore / warning 线索重新归并到同一个恢复点 +- 主线程实施: + - 先执行 `dotnet build GFramework.sln -c Release --no-restore`,发现命令约 `1` 秒即失败,标准摘要只有 `Build FAILED / 0 Warning(s) / 0 Error(s)` + - 补跑 `dotnet build GFramework.sln -c Release --no-restore -v:diag`,确认 solution 在根 `GFramework.csproj` 的 inner-build dispatch 阶段退出,没有进入各子项目编译 + - 继续把根项目拆成 `net8.0`、`net9.0`、`net10.0` 三个 `--no-restore` 构建,全部稳定复现同一条 `MSB4018` + - 读取根项目 `obj/project.assets.json`,确认当前资产文件记录了 Windows restore 元数据与不存在的 fallback package folder + - 按用户追加要求执行默认 `dotnet build` 与 `dotnet build -v:diag`,确认它不是落在相同失败层,而是更早停在 solution restore 图生成阶段 +- 本轮验证结果: + - `dotnet build GFramework.sln -c Release --no-restore` + - 结果:失败;仅有失败摘要,没有暴露真实阻塞点 + - `dotnet build GFramework.sln -c Release --no-restore -v:diag` + - 结果:失败;失败位置收敛到根 `GFramework.csproj` + - `dotnet build` + - 结果:失败;同样约 `1` 秒退出,摘要仍只有 `0 Warning(s) / 0 Error(s)` + - `dotnet build -v:diag` + - 结果:失败;停在 `GFramework.sln` 的 `Restore` 路径 `_FilterRestoreGraphProjectInputItems` + - 补充:具体落点是根 `GFramework.csproj` 的 `_IsProjectRestoreSupported`,日志记录 `MSB4276`,默认 SDK resolver 找不到 `Microsoft.NET.SDK.WorkloadAutoImportPropsLocator` + - `dotnet build GFramework.csproj -c Release -f net8.0 --no-restore` + - 结果:失败;`MSB4018`,`ResolvePackageAssets` 因缺失 `D:\Tool\Development Tools\Microsoft Visual Studio\Shared\NuGetPackages` 退出 + - `dotnet build GFramework.csproj -c Release -f net9.0 --no-restore` + - 结果:失败;与 `net8.0` 相同 + - `dotnet build GFramework.csproj -c Release -f net10.0 --no-restore` + - 结果:失败;与 `net8.0` 相同 +- 当前结论: + - 默认 `dotnet build` 与 `dotnet build GFramework.sln -c Release --no-restore` 失败,但二者不是同一层错误;前者先死在 restore 图阶段,后者死在资产解析阶段 + - 当前 solution 级 `--no-restore` 阻塞不是代码编译错误,而是根项目资产文件引用了当前 WSL 不存在的 Windows fallback package folder + - 当前 restore 路径还额外暴露出 SDK / workload resolver 环境问题,因此仅仅重建资产文件还不足以恢复默认 `dotnet build` + - 这一层阻塞比先前记录的 `NU1301` 更靠前,因为它会让 `--no-restore` 构建在读取资产阶段直接退出 + - `Meziantou.Polyfill 1.0.116` 缺失 / `NU1301` 仍然是 restore 路径的独立风险;修复资产文件后仍需继续处理 + - active tracking 已升级到 `RP-045`,下一轮恢复应先重建与当前环境一致的根项目资产文件,再回测 solution `--no-restore` ## Archive Context From 77e332fd44bfedd73287fee4f8ee111c8d1deae4 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:37:47 +0800 Subject: [PATCH 3/9] =?UTF-8?q?fix(analyzer):=20=E6=94=B6=E5=8F=A3?= =?UTF-8?q?=E5=BD=93=E5=89=8D=E6=89=B9=E6=AC=A1=E8=AD=A6=E5=91=8A=E5=88=87?= =?UTF-8?q?=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 UnifiedSettingsFile 与 LocalizationMap 的集合暴露形状,减少可变集合泄漏风险 - 优化 CqrsHandlerRegistryGeneratorTests 的大型 fixture 组织方式,降低 MA0051 噪音 - 更新 analyzer warning reduction 的 active todo 与 trace,回写 0 warning solution 基线 --- .../Data/UnifiedSettingsDataRepository.cs | 10 +- GFramework.Game/Data/UnifiedSettingsFile.cs | 12 +- .../Setting/Data/LocalizationMap.cs | 3 + .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 1381 +++++++++-------- .../analyzer-warning-reduction-tracking.md | 96 +- .../analyzer-warning-reduction-trace.md | 74 +- 6 files changed, 756 insertions(+), 820 deletions(-) diff --git a/GFramework.Game/Data/UnifiedSettingsDataRepository.cs b/GFramework.Game/Data/UnifiedSettingsDataRepository.cs index 1cd13eb1..7412c948 100644 --- a/GFramework.Game/Data/UnifiedSettingsDataRepository.cs +++ b/GFramework.Game/Data/UnifiedSettingsDataRepository.cs @@ -283,15 +283,21 @@ public class UnifiedSettingsDataRepository( /// 复制当前统一文件快照,确保未提交修改不会污染内存中的已提交状态。 /// /// 要复制的统一文件快照。 - /// 包含独立 section 字典的新快照。 + /// 包含独立 section 映射副本的新快照。 private static UnifiedSettingsFile CloneFile(UnifiedSettingsFile source) { ArgumentNullException.ThrowIfNull(source); + // 反序列化后的运行时类型可能只是 IDictionary 实现;若底层仍是 Dictionary,则保留其 comparer, + // 否则退回到按当前内容复制,避免因为 API 抽象化而改变持久化前后的键比较语义。 + var sections = source.Sections is Dictionary dictionary + ? new Dictionary(dictionary, dictionary.Comparer) + : new Dictionary(source.Sections); + return new UnifiedSettingsFile { Version = source.Version, - Sections = new Dictionary(source.Sections, source.Sections.Comparer) + Sections = sections }; } diff --git a/GFramework.Game/Data/UnifiedSettingsFile.cs b/GFramework.Game/Data/UnifiedSettingsFile.cs index 694dcc28..89e1425b 100644 --- a/GFramework.Game/Data/UnifiedSettingsFile.cs +++ b/GFramework.Game/Data/UnifiedSettingsFile.cs @@ -11,6 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Collections.Generic; using GFramework.Core.Abstractions.Versioning; namespace GFramework.Game.Data; @@ -22,13 +23,16 @@ namespace GFramework.Game.Data; internal sealed class UnifiedSettingsFile : IVersioned { /// - /// 配置节集合,存储不同类型的配置数据 - /// 键为配置节名称,值为配置对象 + /// 配置节映射,存储不同类型的配置数据。 /// - public Dictionary Sections { get; set; } = new(); + /// + /// 这里公开为 而不是具体的 , + /// 以避免暴露可替换的具体集合实现,同时继续兼容 Newtonsoft.Json 对字典对象的序列化与反序列化。 + /// + public IDictionary Sections { get; set; } = new Dictionary(); /// /// 配置文件版本号,用于版本控制和兼容性检查 /// public int Version { get; set; } -} \ No newline at end of file +} diff --git a/GFramework.Godot/Setting/Data/LocalizationMap.cs b/GFramework.Godot/Setting/Data/LocalizationMap.cs index 369d0fc3..9d5f33bf 100644 --- a/GFramework.Godot/Setting/Data/LocalizationMap.cs +++ b/GFramework.Godot/Setting/Data/LocalizationMap.cs @@ -11,6 +11,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System; +using System.Collections.Generic; + namespace GFramework.Godot.Setting.Data; /// diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index 3930aef9..458eaa18 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -684,6 +684,685 @@ public class CqrsHandlerRegistryGeneratorTests """; + // Keep large source fixtures at class scope so MA0051 reduction stays behavior-neutral for generator tests. + private const string HiddenPointerResponseCompilationErrorSource = """ + using System; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed class Container + { + private unsafe struct HiddenResponse + { + } + + private unsafe sealed record HiddenRequest() : IRequest; + + public unsafe sealed class HiddenHandler : IRequestHandler + { + } + } + } + """; + + private const string MixedDirectAndPreciseRegistrationsSource = """ + using System; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed record VisibleRequest() : IRequest; + + public sealed class Container + { + private sealed record HiddenResponse(); + + private sealed record HiddenRequest() : IRequest; + + public sealed class MixedHandler : + IRequestHandler, + IRequestHandler + { + } + } + } + """; + + private const string MixedReflectedImplementationAndPreciseRegistrationsSource = """ + using System; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed record VisibleRequest() : IRequest; + + public sealed class Container + { + private sealed record HiddenResponse(); + + private sealed record HiddenRequest() : IRequest; + + private sealed class HiddenMixedHandler : + IRequestHandler, + IRequestHandler + { + } + } + } + """; + + private const string ExternalProtectedTypeContractsSource = """ + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + """; + + private const string ExternalProtectedTypeDependencySource = """ + using GFramework.Cqrs.Abstractions.Cqrs; + + namespace Dep; + + public sealed record VisibleRequest() : IRequest; + + public abstract class VisibilityScope + { + protected internal sealed record ProtectedResponse(); + + protected internal sealed record ProtectedRequest() : IRequest; + } + + public abstract class HandlerBase : + VisibilityScope, + IRequestHandler, + IRequestHandler + { + } + """; + + private const string ExternalProtectedTypeLookupSource = """ + using System; + using Dep; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + } + + namespace TestApp + { + public sealed class DerivedHandler : HandlerBase + { + } + } + """; + + private const string LegacyFallbackMarkerHiddenHandlerSource = """ + using System; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + + [AttributeUsage(AttributeTargets.Assembly)] + public sealed class CqrsReflectionFallbackAttribute : Attribute + { + public CqrsReflectionFallbackAttribute() { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed record VisibleRequest() : IRequest; + + public sealed class Container + { + private sealed record HiddenRequest() : IRequest; + + private sealed class HiddenHandler : IRequestHandler { } + } + + public sealed class VisibleHandler : IRequestHandler { } + } + """; + + private const string FallbackMarkerUnavailableHiddenHandlerSource = """ + using System; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed record VisibleRequest() : IRequest; + + public sealed class Container + { + private sealed record HiddenRequest() : IRequest; + + private sealed class HiddenHandler : IRequestHandler { } + } + + public sealed class VisibleHandler : IRequestHandler { } + } + """; + + private const string MissingFallbackAttributeDiagnosticSource = """ + using System; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed class Container + { + private unsafe struct HiddenResponse + { + } + + private unsafe sealed record HiddenRequest() : IRequest>; + + public unsafe sealed class HiddenHandler : IRequestHandler> + { + } + } + } + """; + + private const string UnresolvedErrorTypeRuntimeLookupSource = """ + using System; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + + [AttributeUsage(AttributeTargets.Assembly)] + public sealed class CqrsReflectionFallbackAttribute : Attribute + { + public CqrsReflectionFallbackAttribute(params string[] fallbackHandlerTypeNames) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed record BrokenRequest() : IRequest; + + public sealed class BrokenHandler : IRequestHandler + { + } + } + """; + + private const string DynamicResponseNormalizationSource = """ + using System; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed record DynamicRequest() : IRequest; + + public sealed class DynamicHandler : IRequestHandler + { + } + } + """; + + private const string AssemblyLevelFallbackMetadataSource = """ + using System; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + + [AttributeUsage(AttributeTargets.Assembly)] + public sealed class CqrsReflectionFallbackAttribute : Attribute + { + public CqrsReflectionFallbackAttribute(params string[] fallbackHandlerTypeNames) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed class Container + { + private unsafe struct AlphaResponse + { + } + + private unsafe struct BetaResponse + { + } + + private unsafe sealed record AlphaRequest() : IRequest>; + + private unsafe sealed record BetaRequest() : IRequest>; + + public unsafe sealed class BetaHandler : IRequestHandler> + { + } + + public unsafe sealed class AlphaHandler : IRequestHandler> + { + } + } + } + """; + /// /// 验证生成器会为当前程序集中的 request、notification 和 stream 处理器生成稳定顺序的注册器。 /// @@ -752,73 +1431,8 @@ public class CqrsHandlerRegistryGeneratorTests [Test] public void Reports_Compilation_Error_And_Skips_Precise_Registration_For_Hidden_Pointer_Response() { - const string source = """ - using System; - - namespace Microsoft.Extensions.DependencyInjection - { - public interface IServiceCollection { } - - public static class ServiceCollectionServiceExtensions - { - public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } - } - } - - namespace GFramework.Core.Abstractions.Logging - { - public interface ILogger - { - void Debug(string msg); - } - } - - namespace GFramework.Cqrs.Abstractions.Cqrs - { - public interface IRequest { } - public interface INotification { } - public interface IStreamRequest { } - - public interface IRequestHandler where TRequest : IRequest { } - public interface INotificationHandler where TNotification : INotification { } - public interface IStreamRequestHandler where TRequest : IStreamRequest { } - } - - namespace GFramework.Cqrs - { - public interface ICqrsHandlerRegistry - { - void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - public sealed class CqrsHandlerRegistryAttribute : Attribute - { - public CqrsHandlerRegistryAttribute(Type registryType) { } - } - } - - namespace TestApp - { - using GFramework.Cqrs.Abstractions.Cqrs; - - public sealed class Container - { - private unsafe struct HiddenResponse - { - } - - private unsafe sealed record HiddenRequest() : IRequest; - - public unsafe sealed class HiddenHandler : IRequestHandler - { - } - } - } - """; - var execution = ExecuteGenerator( - source, + HiddenPointerResponseCompilationErrorSource, allowUnsafe: true); var inputCompilationErrors = execution.InputCompilationDiagnostics .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) @@ -855,75 +1469,8 @@ public class CqrsHandlerRegistryGeneratorTests [Test] public async Task Generates_Mixed_Direct_And_Precise_Registrations_For_Same_Implementation() { - const string source = """ - using System; - - namespace Microsoft.Extensions.DependencyInjection - { - public interface IServiceCollection { } - - public static class ServiceCollectionServiceExtensions - { - public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } - } - } - - namespace GFramework.Core.Abstractions.Logging - { - public interface ILogger - { - void Debug(string msg); - } - } - - namespace GFramework.Cqrs.Abstractions.Cqrs - { - public interface IRequest { } - public interface INotification { } - public interface IStreamRequest { } - - public interface IRequestHandler where TRequest : IRequest { } - public interface INotificationHandler where TNotification : INotification { } - public interface IStreamRequestHandler where TRequest : IStreamRequest { } - } - - namespace GFramework.Cqrs - { - public interface ICqrsHandlerRegistry - { - void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - public sealed class CqrsHandlerRegistryAttribute : Attribute - { - public CqrsHandlerRegistryAttribute(Type registryType) { } - } - } - - namespace TestApp - { - using GFramework.Cqrs.Abstractions.Cqrs; - - public sealed record VisibleRequest() : IRequest; - - public sealed class Container - { - private sealed record HiddenResponse(); - - private sealed record HiddenRequest() : IRequest; - - public sealed class MixedHandler : - IRequestHandler, - IRequestHandler - { - } - } - } - """; - await GeneratorTest.RunAsync( - source, + MixedDirectAndPreciseRegistrationsSource, ("CqrsHandlerRegistry.g.cs", MixedDirectAndPreciseRegistrationsExpected)); } @@ -934,75 +1481,8 @@ public class CqrsHandlerRegistryGeneratorTests [Test] public async Task Generates_Mixed_Reflected_Implementation_And_Precise_Registrations_For_Same_Implementation() { - const string source = """ - using System; - - namespace Microsoft.Extensions.DependencyInjection - { - public interface IServiceCollection { } - - public static class ServiceCollectionServiceExtensions - { - public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } - } - } - - namespace GFramework.Core.Abstractions.Logging - { - public interface ILogger - { - void Debug(string msg); - } - } - - namespace GFramework.Cqrs.Abstractions.Cqrs - { - public interface IRequest { } - public interface INotification { } - public interface IStreamRequest { } - - public interface IRequestHandler where TRequest : IRequest { } - public interface INotificationHandler where TNotification : INotification { } - public interface IStreamRequestHandler where TRequest : IStreamRequest { } - } - - namespace GFramework.Cqrs - { - public interface ICqrsHandlerRegistry - { - void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - public sealed class CqrsHandlerRegistryAttribute : Attribute - { - public CqrsHandlerRegistryAttribute(Type registryType) { } - } - } - - namespace TestApp - { - using GFramework.Cqrs.Abstractions.Cqrs; - - public sealed record VisibleRequest() : IRequest; - - public sealed class Container - { - private sealed record HiddenResponse(); - - private sealed record HiddenRequest() : IRequest; - - private sealed class HiddenMixedHandler : - IRequestHandler, - IRequestHandler - { - } - } - } - """; - await GeneratorTest.RunAsync( - source, + MixedReflectedImplementationAndPreciseRegistrationsSource, ("CqrsHandlerRegistry.g.cs", MixedReflectedImplementationAndPreciseRegistrationsExpected)); } @@ -1013,94 +1493,15 @@ public class CqrsHandlerRegistryGeneratorTests [Test] public void Generates_Precise_Assembly_Type_Lookups_For_Inaccessible_External_Protected_Types() { - const string contractsSource = """ - namespace GFramework.Cqrs.Abstractions.Cqrs - { - public interface IRequest { } - public interface INotification { } - public interface IStreamRequest { } - - public interface IRequestHandler where TRequest : IRequest { } - public interface INotificationHandler where TNotification : INotification { } - public interface IStreamRequestHandler where TRequest : IStreamRequest { } - } - """; - - const string dependencySource = """ - using GFramework.Cqrs.Abstractions.Cqrs; - - namespace Dep; - - public sealed record VisibleRequest() : IRequest; - - public abstract class VisibilityScope - { - protected internal sealed record ProtectedResponse(); - - protected internal sealed record ProtectedRequest() : IRequest; - } - - public abstract class HandlerBase : - VisibilityScope, - IRequestHandler, - IRequestHandler - { - } - """; - - const string source = """ - using System; - using Dep; - - namespace Microsoft.Extensions.DependencyInjection - { - public interface IServiceCollection { } - - public static class ServiceCollectionServiceExtensions - { - public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } - } - } - - namespace GFramework.Core.Abstractions.Logging - { - public interface ILogger - { - void Debug(string msg); - } - } - - namespace GFramework.Cqrs - { - public interface ICqrsHandlerRegistry - { - void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - public sealed class CqrsHandlerRegistryAttribute : Attribute - { - public CqrsHandlerRegistryAttribute(Type registryType) { } - } - } - - namespace TestApp - { - public sealed class DerivedHandler : HandlerBase - { - } - } - """; - var contractsReference = MetadataReferenceTestBuilder.CreateFromSource( "Contracts", - contractsSource); + ExternalProtectedTypeContractsSource); var dependencyReference = MetadataReferenceTestBuilder.CreateFromSource( "Dependency", - dependencySource, + ExternalProtectedTypeDependencySource, contractsReference); var generatedSource = RunGenerator( - source, + ExternalProtectedTypeLookupSource, contractsReference, dependencyReference); @@ -1122,77 +1523,8 @@ public class CqrsHandlerRegistryGeneratorTests [Test] public async Task Does_Not_Emit_Legacy_Fallback_Marker_When_Generated_Registry_Can_Self_Register_Hidden_Handler() { - const string source = """ - using System; - - namespace Microsoft.Extensions.DependencyInjection - { - public interface IServiceCollection { } - - public static class ServiceCollectionServiceExtensions - { - public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } - } - } - - namespace GFramework.Core.Abstractions.Logging - { - public interface ILogger - { - void Debug(string msg); - } - } - - namespace GFramework.Cqrs.Abstractions.Cqrs - { - public interface IRequest { } - public interface INotification { } - public interface IStreamRequest { } - - public interface IRequestHandler where TRequest : IRequest { } - public interface INotificationHandler where TNotification : INotification { } - public interface IStreamRequestHandler where TRequest : IStreamRequest { } - } - - namespace GFramework.Cqrs - { - public interface ICqrsHandlerRegistry - { - void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - public sealed class CqrsHandlerRegistryAttribute : Attribute - { - public CqrsHandlerRegistryAttribute(Type registryType) { } - } - - [AttributeUsage(AttributeTargets.Assembly)] - public sealed class CqrsReflectionFallbackAttribute : Attribute - { - public CqrsReflectionFallbackAttribute() { } - } - } - - namespace TestApp - { - using GFramework.Cqrs.Abstractions.Cqrs; - - public sealed record VisibleRequest() : IRequest; - - public sealed class Container - { - private sealed record HiddenRequest() : IRequest; - - private sealed class HiddenHandler : IRequestHandler { } - } - - public sealed class VisibleHandler : IRequestHandler { } - } - """; - await GeneratorTest.RunAsync( - source, + LegacyFallbackMarkerHiddenHandlerSource, ("CqrsHandlerRegistry.g.cs", HiddenNestedHandlerSelfRegistrationExpected)); } @@ -1203,71 +1535,8 @@ public class CqrsHandlerRegistryGeneratorTests [Test] public async Task Generates_Registry_For_Hidden_Handler_When_Fallback_Marker_Is_Unavailable() { - const string source = """ - using System; - - namespace Microsoft.Extensions.DependencyInjection - { - public interface IServiceCollection { } - - public static class ServiceCollectionServiceExtensions - { - public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } - } - } - - namespace GFramework.Core.Abstractions.Logging - { - public interface ILogger - { - void Debug(string msg); - } - } - - namespace GFramework.Cqrs.Abstractions.Cqrs - { - public interface IRequest { } - public interface INotification { } - public interface IStreamRequest { } - - public interface IRequestHandler where TRequest : IRequest { } - public interface INotificationHandler where TNotification : INotification { } - public interface IStreamRequestHandler where TRequest : IStreamRequest { } - } - - namespace GFramework.Cqrs - { - public interface ICqrsHandlerRegistry - { - void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - public sealed class CqrsHandlerRegistryAttribute : Attribute - { - public CqrsHandlerRegistryAttribute(Type registryType) { } - } - } - - namespace TestApp - { - using GFramework.Cqrs.Abstractions.Cqrs; - - public sealed record VisibleRequest() : IRequest; - - public sealed class Container - { - private sealed record HiddenRequest() : IRequest; - - private sealed class HiddenHandler : IRequestHandler { } - } - - public sealed class VisibleHandler : IRequestHandler { } - } - """; - await GeneratorTest.RunAsync( - source, + FallbackMarkerUnavailableHiddenHandlerSource, ("CqrsHandlerRegistry.g.cs", HiddenNestedHandlerSelfRegistrationExpected)); } @@ -1279,73 +1548,8 @@ public class CqrsHandlerRegistryGeneratorTests public void Reports_Diagnostic_And_Skips_Registry_When_Fallback_Metadata_Is_Required_But_Runtime_Contract_Lacks_Fallback_Attribute() { - const string source = """ - using System; - - namespace Microsoft.Extensions.DependencyInjection - { - public interface IServiceCollection { } - - public static class ServiceCollectionServiceExtensions - { - public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } - } - } - - namespace GFramework.Core.Abstractions.Logging - { - public interface ILogger - { - void Debug(string msg); - } - } - - namespace GFramework.Cqrs.Abstractions.Cqrs - { - public interface IRequest { } - public interface INotification { } - public interface IStreamRequest { } - - public interface IRequestHandler where TRequest : IRequest { } - public interface INotificationHandler where TNotification : INotification { } - public interface IStreamRequestHandler where TRequest : IStreamRequest { } - } - - namespace GFramework.Cqrs - { - public interface ICqrsHandlerRegistry - { - void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - public sealed class CqrsHandlerRegistryAttribute : Attribute - { - public CqrsHandlerRegistryAttribute(Type registryType) { } - } - } - - namespace TestApp - { - using GFramework.Cqrs.Abstractions.Cqrs; - - public sealed class Container - { - private unsafe struct HiddenResponse - { - } - - private unsafe sealed record HiddenRequest() : IRequest>; - - public unsafe sealed class HiddenHandler : IRequestHandler> - { - } - } - } - """; - var execution = ExecuteGenerator( - source, + MissingFallbackAttributeDiagnosticSource, allowUnsafe: true); var inputCompilationErrors = execution.InputCompilationDiagnostics .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) @@ -1382,71 +1586,7 @@ public class CqrsHandlerRegistryGeneratorTests [Test] public void Emits_Runtime_Type_Lookup_When_Handler_Contract_Contains_Unresolved_Error_Types() { - const string source = """ - using System; - - namespace Microsoft.Extensions.DependencyInjection - { - public interface IServiceCollection { } - - public static class ServiceCollectionServiceExtensions - { - public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } - } - } - - namespace GFramework.Core.Abstractions.Logging - { - public interface ILogger - { - void Debug(string msg); - } - } - - namespace GFramework.Cqrs.Abstractions.Cqrs - { - public interface IRequest { } - public interface INotification { } - public interface IStreamRequest { } - - public interface IRequestHandler where TRequest : IRequest { } - public interface INotificationHandler where TNotification : INotification { } - public interface IStreamRequestHandler where TRequest : IStreamRequest { } - } - - namespace GFramework.Cqrs - { - public interface ICqrsHandlerRegistry - { - void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - public sealed class CqrsHandlerRegistryAttribute : Attribute - { - public CqrsHandlerRegistryAttribute(Type registryType) { } - } - - [AttributeUsage(AttributeTargets.Assembly)] - public sealed class CqrsReflectionFallbackAttribute : Attribute - { - public CqrsReflectionFallbackAttribute(params string[] fallbackHandlerTypeNames) { } - } - } - - namespace TestApp - { - using GFramework.Cqrs.Abstractions.Cqrs; - - public sealed record BrokenRequest() : IRequest; - - public sealed class BrokenHandler : IRequestHandler - { - } - } - """; - - var execution = ExecuteGenerator(source); + var execution = ExecuteGenerator(UnresolvedErrorTypeRuntimeLookupSource); var inputCompilationErrors = execution.InputCompilationDiagnostics .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .ToArray(); @@ -1483,65 +1623,7 @@ public class CqrsHandlerRegistryGeneratorTests [Test] public void Emits_Object_Type_Reference_When_Handler_Response_Uses_Dynamic() { - const string source = """ - using System; - - namespace Microsoft.Extensions.DependencyInjection - { - public interface IServiceCollection { } - - public static class ServiceCollectionServiceExtensions - { - public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } - } - } - - namespace GFramework.Core.Abstractions.Logging - { - public interface ILogger - { - void Debug(string msg); - } - } - - namespace GFramework.Cqrs.Abstractions.Cqrs - { - public interface IRequest { } - public interface INotification { } - public interface IStreamRequest { } - - public interface IRequestHandler where TRequest : IRequest { } - public interface INotificationHandler where TNotification : INotification { } - public interface IStreamRequestHandler where TRequest : IStreamRequest { } - } - - namespace GFramework.Cqrs - { - public interface ICqrsHandlerRegistry - { - void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - public sealed class CqrsHandlerRegistryAttribute : Attribute - { - public CqrsHandlerRegistryAttribute(Type registryType) { } - } - } - - namespace TestApp - { - using GFramework.Cqrs.Abstractions.Cqrs; - - public sealed record DynamicRequest() : IRequest; - - public sealed class DynamicHandler : IRequestHandler - { - } - } - """; - - var execution = ExecuteGenerator(source); + var execution = ExecuteGenerator(DynamicResponseNormalizationSource); var inputCompilationErrors = execution.InputCompilationDiagnostics .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .ToArray(); @@ -1574,89 +1656,8 @@ public class CqrsHandlerRegistryGeneratorTests public void Emits_Assembly_Level_Fallback_Metadata_When_Fallback_Is_Required_And_Runtime_Contract_Is_Available() { - const string source = """ - using System; - - namespace Microsoft.Extensions.DependencyInjection - { - public interface IServiceCollection { } - - public static class ServiceCollectionServiceExtensions - { - public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } - } - } - - namespace GFramework.Core.Abstractions.Logging - { - public interface ILogger - { - void Debug(string msg); - } - } - - namespace GFramework.Cqrs.Abstractions.Cqrs - { - public interface IRequest { } - public interface INotification { } - public interface IStreamRequest { } - - public interface IRequestHandler where TRequest : IRequest { } - public interface INotificationHandler where TNotification : INotification { } - public interface IStreamRequestHandler where TRequest : IStreamRequest { } - } - - namespace GFramework.Cqrs - { - public interface ICqrsHandlerRegistry - { - void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - public sealed class CqrsHandlerRegistryAttribute : Attribute - { - public CqrsHandlerRegistryAttribute(Type registryType) { } - } - - [AttributeUsage(AttributeTargets.Assembly)] - public sealed class CqrsReflectionFallbackAttribute : Attribute - { - public CqrsReflectionFallbackAttribute(params string[] fallbackHandlerTypeNames) { } - } - } - - namespace TestApp - { - using GFramework.Cqrs.Abstractions.Cqrs; - - public sealed class Container - { - private unsafe struct AlphaResponse - { - } - - private unsafe struct BetaResponse - { - } - - private unsafe sealed record AlphaRequest() : IRequest>; - - private unsafe sealed record BetaRequest() : IRequest>; - - public unsafe sealed class BetaHandler : IRequestHandler> - { - } - - public unsafe sealed class AlphaHandler : IRequestHandler> - { - } - } - } - """; - var execution = ExecuteGenerator( - source, + AssemblyLevelFallbackMetadataSource, allowUnsafe: true); var inputCompilationErrors = execution.InputCompilationDiagnostics .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) diff --git a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md index 707b2f78..5f02b9c4 100644 --- a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md +++ b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md @@ -2,63 +2,40 @@ ## 目标 -继续以“优先低风险、保持行为兼容”为原则收敛当前仓库的 Meziantou analyzer warnings,并确保 active recovery 入口保持精简、可恢复。 +继续以“低风险、可审查、可恢复”为原则收敛 analyzer warning,并保持 active recovery 文档只保留当前真值。 ## 当前恢复点 -- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-046` -- 当前阶段:`Phase 46` +- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-047` +- 当前阶段:`Phase 47` - 当前焦点: - - 已按用户更正后的要求执行前台 `dotnet build GFramework.sln -c Release`,收集当前工作树的 solution 级 warning 基线,并将结果回写 active plan - - 当前前台 solution Release build 已能稳定完成,结果为 `891 Warning(s)` / `0 Error(s)` / `Time Elapsed 00:00:18.57` - - 当前 baseline 仍为 `origin/main` (`e692ed3`, `2026-04-24 09:36:17 +0800`);分支仍映射到 `fix/analyzer-warning-reduction-batch` / `GFramework-analyzer` - - 当前直接观察到的热点 warning 规则集中在 `MA0051`、`MA0158`、`MA0004`,并伴随一部分 `MA0006`、`MA0002`、`MA0009` - - 当前直接观察到的热点模块集中在 `GFramework.Godot.SourceGenerators`、`GFramework.Godot.SourceGenerators.Tests`、`GFramework.Core`、`GFramework.Game`、`GFramework.Cqrs`、`GFramework.Godot` - - 非前台形态的同命令当前不稳定:重定向到文件、`script` 分配 TTY、以及若干 logger 组合都曾快速返回 `Build FAILED / 0 Warning(s) / 0 Error(s)` 或 `Restore failed` - -## 当前状态摘要 - -- 已通过 explorer `Rawls` 盘点出本轮最适合并行推进的低风险切片: - - `GFramework.Game/Data/UnifiedSettingsFile.cs` 的 `MA0016` - - `GFramework.Godot/Setting/Data/LocalizationMap.cs` 的 `MA0016` - - `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 的 `MA0051` -- 已通过 explorer `Arendt` 收敛过旧构建根因:当前离线缓存只有 `Meziantou.Polyfill 1.0.110`,缺少项目请求的 `1.0.116`,导致 restore 失败并使 source-generator 相关 `netstandard2.0` 项目缺失有效引用图 -- worker `Aquinas` 已独立完成 `LocalizationMap` 收口并生成提交 `a0ce04b`(`fix(godot): 收紧本地化映射集合暴露`) -- worker `Boyle` 已完成 `UnifiedSettingsFile` / `UnifiedSettingsDataRepository` 的最小 API 形状修正,并同步更新 active todo / trace 草稿 -- 主线程本轮重新执行前台 `dotnet build GFramework.sln -c Release`,构建成功并暴露出完整 warning 面,说明当前工作树至少在交互式前台构建路径上已经恢复到可收集 analyzer baseline 的状态 -- 从实时输出可直接确认的热点包括: - - `GFramework.Godot.SourceGenerators/GodotProjectMetadataGenerator.cs`、`BindNodeSignalGenerator.cs`、`GetNodeGenerator.cs`、`Registration/AutoRegisterExportedCollectionsGenerator.cs` - - `GFramework.Godot.SourceGenerators.Tests/**` 多个 generator 测试文件 - - `GFramework.Cqrs/Internal/WeakKeyCache.cs` - - `GFramework.Core/**` 多个锁相关组件,如 `SamplingFilter.cs`、`BindableProperty.cs`、`FileAppender.cs`、`ResourceHandle.cs` - - `GFramework.Game/Config/YamlConfigSchemaValidator*.cs`、`GFramework.Game/UI/UiRouterBase.cs`、`GFramework.Game/Storage/FileStorage.cs` -- 同一轮中,多次尝试把相同命令切换到“重定向日志”“TTY 包裹”或“附加 logger 输出到文件”的形态时,构建又会退化成 `Build FAILED / 0 Warning(s) / 0 Error(s)` 或 `Restore failed` -- 这说明当前环境不只是“能否构建”的问题,还存在“前台构建”和“非前台采集”结果不一致的执行形态漂移 + - 已重新用经典 logger 形态执行 `dotnet build GFramework.sln -c Release -tl:off -nologo`,当前 solution 基线是 `0 Warning(s)` / `0 Error(s)` + - 受影响项目 `GFramework.Game` 与 `GFramework.SourceGenerators.Tests` 的 Release build 也已通过,当前工作树中的 warning-reduction 切片至少在编译层面成立 + - 当前 baseline 仍为 `origin/main`(`e692ed3`, `2026-04-24 09:36:17 +0800`);batch stop condition 仍为 branch diff 接近 `75` 个文件 + - 当前已提交 branch diff 仍只有 `3` 个文件、`234` 行,距离 stop condition 很远;工作树另外保留一批未提交切片待整理与提交 ## 当前活跃事实 -- 当前主题仍保持 active,因为 analyzer warning reduction 主任务尚未结束,而且本轮新增了可直接消费的 solution warning baseline -- `CqrsHandlerRegistryGeneratorTests.cs` 当前已不再保留方法内 `const string source = """..."""` 型大型 fixture;后续只需回到真实 warning 热点继续收敛 -- `UnifiedSettingsFile.Sections` 改为 `IDictionary` 后,`CloneFile` 仍会在底层是 `Dictionary` 时保留 comparer,从而避免改变现有 key 比较行为 -- `LocalizationMap` 现在通过私有只读字典与 `IReadOnlyDictionary` 暴露映射,消费者 `GodotLocalizationSettings` 仍只按只读方式使用这些映射 -- 当前 worktree 的推荐构建入口仍是 `bash scripts/dotnet-wsl.sh ...` -- 当前前台 `dotnet build GFramework.sln -c Release` 的结果是 `891` 条 warning、`0` 条 error,且已确认不是“0 warning 的假成功” -- 当前 warning 规则的已观察集合包括 `MA0051`、`MA0158`、`MA0004`、`MA0006`、`MA0002`、`MA0009` -- 当前“非前台采集”路径仍不可信:把输出重定向到文件、追加 file logger 或经 `script` 分配 TTY 时,都未稳定复现前台结果 -- 先前已识别的 `--no-restore` 资产文件漂移、`Meziantou.Polyfill 1.0.116` 缺失、以及 `NU1301` 风险仍保留,但本轮用户要求的 warning 基线以成功的前台 `dotnet build` 结果为准 +- `UnifiedSettingsFile.Sections` 已抽象为 `IDictionary`;`UnifiedSettingsDataRepository.CloneFile` 会在底层仍是 `Dictionary` 时保留 comparer,避免改变既有键比较语义 +- `LocalizationMap` 通过私有 `Dictionary` 字段配合 `IReadOnlyDictionary` 暴露映射,继续避免把可变集合直接暴露给调用方 +- `CqrsHandlerRegistryGeneratorTests.cs` 已把一批大型 fixture 提升到类级常量,当前目标是以更低噪音方式消化 `MA0051` +- 当前工作树的 tracked 变更集中在: + - `GFramework.Game/Data/UnifiedSettingsFile.cs` + - `GFramework.Game/Data/UnifiedSettingsDataRepository.cs` + - `GFramework.Godot/Setting/Data/LocalizationMap.cs` + - `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` + - `docs/zh-CN/contributing.md` + - `docs/zh-CN/troubleshooting.md` +- 当前还存在未跟踪的 `scripts/dotnet-wsl.sh`,用于 WSL Windows-backed worktree 下统一 `dotnet` 环境参数 ## 当前风险 -- warning 采集形态漂移风险:同一个 `dotnet build GFramework.sln -c Release` 在前台可成功输出 `891` warnings,但一旦切到日志重定向、TTY 包裹或特定 logger 组合,就可能在约 `1` 秒内快速失败 - - 缓解措施:短期内把“前台普通构建”作为 warning 基线真值来源;若需要自动化统计,再单独排查 stdout/TTY/logger 相关环境差异 -- SDK / workload resolver 环境漂移风险:历史诊断样本里 Linux .NET SDK `10.0.106` 在 solution restore 图阶段报告过 `Microsoft.NET.SDK.WorkloadAutoImportPropsLocator` 缺失 - - 缓解措施:后续若要恢复脚本化或 `--no-restore` 验证,再重新检查当前 WSL `dotnet --info`、workload 解析器状态与仓库推荐构建入口是否一致 -- 资产文件环境漂移风险:历史样本中的根项目 `project.assets.json` 明显来自 Windows restore,上游记录的 fallback package folder 在当前 WSL 会话不存在 - - 缓解措施:需要重新启用 `--no-restore` 验证时,先用与 WSL 兼容的 NuGet / restore 配置重建根项目资产文件 -- 构建环境可达性风险:`Meziantou.Polyfill 1.0.116` 缺失与 `NU1301` 在旧样本中仍是有效线索 - - 缓解措施:后续若再次命中 restore 阻塞,优先核查本地缓存版本与 NuGet 可达性 -- reviewability 风险:当前分支只提交了 `LocalizationMap` 一个 warning 切片,而工作树仍保留 `ai-plan` 等未提交变更 - - 缓解措施:后续 warning reduction 继续按模块或规则切片提交,不把多个热点混成单个大提交 +- `dotnet build` 默认 terminal logger 输出会折叠成进度视图,不适合作为 warning 基线采样入口 + - 缓解措施:继续使用 `-tl:off` 收集 warning 计数 +- 当前工作树仍有多处未提交修改;如果直接继续扩展批次,会降低 reviewability + - 缓解措施:先整理并提交当前切片,再决定是否继续下一轮 warning cleanup +- `scripts/dotnet-wsl.sh` 与文档更新属于环境治理切片,是否与本轮 warning-reduction 一起提交需要显式判断 + - 缓解措施:提交前按主题拆分 staging,避免把环境文档与 warning 修正混成一个提交 ## 活跃文档 @@ -71,22 +48,15 @@ ## 验证说明 -- `dotnet build GFramework.sln -c Release` - - 结果:成功;`891 Warning(s)`、`0 Error(s)`、`Time Elapsed 00:00:18.57` - - 补充:当前 warning 流可直接从前台交互式构建观察到;热点规则以 `MA0051`、`MA0158`、`MA0004` 为主 -- `dotnet build GFramework.sln -c Release > /tmp/gframework-build-warnings.log 2>&1` - - 结果:失败;约 `0.78s` 结束,摘要仅显示 `Build FAILED / 0 Warning(s) / 0 Error(s)` -- `dotnet build GFramework.sln -c Release '/flp:logfile=/tmp/gframework-build-warnings.log;verbosity=normal'` - - 结果:成功;但日志文件只保留构建摘要,没有留下可消费的 warning 行 -- `dotnet build GFramework.sln -c Release '/flp1:logfile=/tmp/gframework-build-warnings-only.log;warningsonly'` - - 结果:成功;但 warning-only 日志文件为空 -- `script -q -c "dotnet build GFramework.sln -c Release" /tmp/gframework-build-full-typescript.log` - - 结果:失败;TTY 形态下 restore 于约 `0.8s` 退出 -- `git --git-dir=/mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework/.git/worktrees/GFramework-analyzer --work-tree=/mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework-WorkTree/GFramework-analyzer branch --show-current` - - 结果:成功;当前分支为 `fix/analyzer-warning-reduction-batch` +- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release` + - 结果:成功 +- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release` + - 结果:成功 +- `dotnet build GFramework.sln -c Release -tl:off -nologo` + - 结果:成功;`0 Warning(s)`、`0 Error(s)`、`Time Elapsed 00:00:12.72` ## 下一步建议 -1. 先以本轮前台 `dotnet build GFramework.sln -c Release` 的 `891` warning baseline 为起点,优先从 `MA0051` / `MA0158` 最密集的模块切分下一批低风险整改 -2. 并行保留环境核查:检查为什么相同命令在重定向日志、TTY 包裹或 logger 组合下会快速失败,避免后续 warning 统计自动化再次失真 -3. 在需要重新启用 `--no-restore` 或脚本化验证时,再回到根 `GFramework.csproj` 资产文件漂移、SDK workload resolver、以及 `Meziantou.Polyfill 1.0.116` / `NU1301` 这几条环境线索 +1. 先把当前工作树中的 warning-reduction 切片与环境文档切片拆分清楚,避免混合提交 +2. 若确认本轮目标只是收口 warning reduction,则优先提交 `Game` / `Godot` / `SourceGenerators.Tests` 相关修改 +3. 若 `scripts/dotnet-wsl.sh` 与中文文档属于独立环境治理工作,则单独跟踪或另起提交 diff --git a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md index eb3a3153..efaa088c 100644 --- a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md +++ b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md @@ -1,71 +1,23 @@ # Analyzer Warning Reduction 追踪 -## 2026-04-24 — RP-046 +## 2026-04-24 — RP-047 -### 阶段:solution warning baseline 采样与 active plan 回写 +### 阶段:solution warning 基线复核与 active plan 去噪 - 触发背景: - - 用户纠正本轮目标为“执行 `dotnet build` 收集当前项目 warning,并更新当前工作树激活计划” - - active topic 仍为 `analyzer-warning-reduction`,因此本轮需要先确认 solution 级 warning baseline 是否可直接从当前工作树获取 + - 用户要求继续按 `$gframework-batch-boot 75` 推进,并明确要求“通过 `dotnet build` 检查警告” + - 用户追加要求清理当前计划中的噪音内容,因此本轮除了复核 warning 基线,还要同步压缩 active todo / trace - 主线程实施: - - 直接执行前台 `dotnet build GFramework.sln -c Release` - - 构建成功,得到 `891 Warning(s)` / `0 Error(s)` / `Time Elapsed 00:00:18.57` - - 从实时输出中确认 warning 热点主要集中在 `GFramework.Godot.SourceGenerators`、`GFramework.Godot.SourceGenerators.Tests`、`GFramework.Core`、`GFramework.Game`、`GFramework.Cqrs`、`GFramework.Godot` - - 从实时输出中确认规则热点以 `MA0051`、`MA0158`、`MA0004` 为主,并伴随 `MA0006`、`MA0002`、`MA0009` - - 追加尝试把同一命令改为“重定向到日志文件”“附加 file logger”“`script` 分配 TTY”几种采集方式;这些路径都未稳定复现前台结果,而是出现 `Build FAILED / 0 Warning(s) / 0 Error(s)` 或 `Restore failed` - - 基于上述差异,本轮把“前台普通构建”视为 warning baseline 真值来源,并把采集形态漂移记录为环境风险 -- 本轮验证结果: - - `dotnet build GFramework.sln -c Release` - - 结果:成功;`891 Warning(s)`、`0 Error(s)` - - `dotnet build GFramework.sln -c Release > /tmp/gframework-build-warnings.log 2>&1` - - 结果:失败;约 `0.78s` 结束,摘要仅显示 `Build FAILED / 0 Warning(s) / 0 Error(s)` - - `dotnet build GFramework.sln -c Release '/flp:logfile=/tmp/gframework-build-warnings.log;verbosity=normal'` - - 结果:成功;但日志文件只保留了构建摘要,没有留下 warning 行 - - `dotnet build GFramework.sln -c Release '/flp1:logfile=/tmp/gframework-build-warnings-only.log;warningsonly'` - - 结果:成功;但 warning-only 日志文件为空 - - `script -q -c "dotnet build GFramework.sln -c Release" /tmp/gframework-build-full-typescript.log` - - 结果:失败;TTY 形态下 restore 于约 `0.8s` 退出 + - 先读取 active topic 文档、基线信息与 branch diff 指标,确认 baseline 仍是 `origin/main`(`e692ed3`) + - 复查当前工作树中的 warning-reduction 切片,确认主要未提交修改集中在 `GFramework.Game`、`GFramework.Godot`、`GFramework.SourceGenerators.Tests` + - 执行 `dotnet build GFramework.Game/GFramework.Game.csproj -c Release` 与 `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release`,二者均成功 + - 发现默认 terminal logger 输出不利于读取 warning 数,因此改用 `dotnet build GFramework.sln -c Release -tl:off -nologo` + - solution Release build 在经典 logger 形态下成功完成,结果为 `0 Warning(s)` / `0 Error(s)` / `Time Elapsed 00:00:12.72` + - 基于该真值,压缩 active todo / trace,移除已经过期的 `891 warnings` 旧基线和过多执行形态细节 - 当前结论: - - 当前工作树的 solution 级 warning baseline 可以通过普通前台 `dotnet build` 获取,且样本值是 `891` 条 warning、`0` 条 error - - 当前环境对 stdout/TTY/logger 形态敏感,不能把“把输出落到文件”的结果直接当作同等可信的构建事实 - - 下一轮 warning reduction 应以本轮前台 baseline 为准,而不是继续围绕空日志或快速失败结果做误判 - -## 2026-04-24 — RP-045 - -### 阶段:solution no-restore 阻塞面采样与 active plan 回写 - -- 触发背景: - - 用户要求显式执行 `dotnet build GFramework.sln -c Release --no-restore`,收集当前项目报错并同步更新当前工作树激活计划 - - active topic 仍为 `analyzer-warning-reduction`,因此本轮的核心工作是把 solution 级失败面与先前的 restore / warning 线索重新归并到同一个恢复点 -- 主线程实施: - - 先执行 `dotnet build GFramework.sln -c Release --no-restore`,发现命令约 `1` 秒即失败,标准摘要只有 `Build FAILED / 0 Warning(s) / 0 Error(s)` - - 补跑 `dotnet build GFramework.sln -c Release --no-restore -v:diag`,确认 solution 在根 `GFramework.csproj` 的 inner-build dispatch 阶段退出,没有进入各子项目编译 - - 继续把根项目拆成 `net8.0`、`net9.0`、`net10.0` 三个 `--no-restore` 构建,全部稳定复现同一条 `MSB4018` - - 读取根项目 `obj/project.assets.json`,确认当前资产文件记录了 Windows restore 元数据与不存在的 fallback package folder - - 按用户追加要求执行默认 `dotnet build` 与 `dotnet build -v:diag`,确认它不是落在相同失败层,而是更早停在 solution restore 图生成阶段 -- 本轮验证结果: - - `dotnet build GFramework.sln -c Release --no-restore` - - 结果:失败;仅有失败摘要,没有暴露真实阻塞点 - - `dotnet build GFramework.sln -c Release --no-restore -v:diag` - - 结果:失败;失败位置收敛到根 `GFramework.csproj` - - `dotnet build` - - 结果:失败;同样约 `1` 秒退出,摘要仍只有 `0 Warning(s) / 0 Error(s)` - - `dotnet build -v:diag` - - 结果:失败;停在 `GFramework.sln` 的 `Restore` 路径 `_FilterRestoreGraphProjectInputItems` - - 补充:具体落点是根 `GFramework.csproj` 的 `_IsProjectRestoreSupported`,日志记录 `MSB4276`,默认 SDK resolver 找不到 `Microsoft.NET.SDK.WorkloadAutoImportPropsLocator` - - `dotnet build GFramework.csproj -c Release -f net8.0 --no-restore` - - 结果:失败;`MSB4018`,`ResolvePackageAssets` 因缺失 `D:\Tool\Development Tools\Microsoft Visual Studio\Shared\NuGetPackages` 退出 - - `dotnet build GFramework.csproj -c Release -f net9.0 --no-restore` - - 结果:失败;与 `net8.0` 相同 - - `dotnet build GFramework.csproj -c Release -f net10.0 --no-restore` - - 结果:失败;与 `net8.0` 相同 -- 当前结论: - - 默认 `dotnet build` 与 `dotnet build GFramework.sln -c Release --no-restore` 失败,但二者不是同一层错误;前者先死在 restore 图阶段,后者死在资产解析阶段 - - 当前 solution 级 `--no-restore` 阻塞不是代码编译错误,而是根项目资产文件引用了当前 WSL 不存在的 Windows fallback package folder - - 当前 restore 路径还额外暴露出 SDK / workload resolver 环境问题,因此仅仅重建资产文件还不足以恢复默认 `dotnet build` - - 这一层阻塞比先前记录的 `NU1301` 更靠前,因为它会让 `--no-restore` 构建在读取资产阶段直接退出 - - `Meziantou.Polyfill 1.0.116` 缺失 / `NU1301` 仍然是 restore 路径的独立风险;修复资产文件后仍需继续处理 - - active tracking 已升级到 `RP-045`,下一轮恢复应先重建与当前环境一致的根项目资产文件,再回测 solution `--no-restore` + - 当前工作树的 solution warning 基线已经降到 `0 Warning(s)`;active plan 中旧的高噪音 warning 基线不再适合作为恢复入口 + - `-tl:off` 是当前最可靠的 warning 采样入口;默认 terminal logger 更适合看进度,不适合记录计数 + - 当前批次的主要剩余工作不再是继续找 warning,而是整理并提交现有切片,避免 reviewability 下降 ## Archive Context From a98d1cb8d07397f784ce4711799ab0ed764a9996 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:59:03 +0800 Subject: [PATCH 4/9] =?UTF-8?q?docs(ai-plan):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E5=91=8A=E8=AD=A6=E6=89=B9=E5=A4=84=E7=90=86=E6=81=A2=E5=A4=8D?= =?UTF-8?q?=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 analyzer warning reduction 的 active todo 为 RP-048 当前真值 - 补充 plain dotnet build 成功与最新 origin/main baseline - 记录当前批处理已到自然停点并收敛下一步建议 --- .../analyzer-warning-reduction-tracking.md | 48 +++++++------------ .../analyzer-warning-reduction-trace.md | 27 ++++++----- 2 files changed, 32 insertions(+), 43 deletions(-) diff --git a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md index 5f02b9c4..89c03660 100644 --- a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md +++ b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md @@ -6,36 +6,28 @@ ## 当前恢复点 -- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-047` -- 当前阶段:`Phase 47` +- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-048` +- 当前阶段:`Phase 48` - 当前焦点: - - 已重新用经典 logger 形态执行 `dotnet build GFramework.sln -c Release -tl:off -nologo`,当前 solution 基线是 `0 Warning(s)` / `0 Error(s)` - - 受影响项目 `GFramework.Game` 与 `GFramework.SourceGenerators.Tests` 的 Release build 也已通过,当前工作树中的 warning-reduction 切片至少在编译层面成立 - - 当前 baseline 仍为 `origin/main`(`e692ed3`, `2026-04-24 09:36:17 +0800`);batch stop condition 仍为 branch diff 接近 `75` 个文件 - - 当前已提交 branch diff 仍只有 `3` 个文件、`234` 行,距离 stop condition 很远;工作树另外保留一批未提交切片待整理与提交 + - 当前 baseline 为 `origin/main`(`a8447a6`, `2026-04-24 12:53:39 +0800`),batch stop condition 仍为 branch diff 接近 `75` 个文件 + - 当前 branch diff 为 `6` 个文件、`1566` 行(相对 `origin/main...HEAD`),距离 stop condition 仍有空间 + - 当前 warning-reduction 代码切片已经提交到 `77e332f`(`fix(analyzer): 收口当前批次警告切片`) + - 当前工作树除未跟踪的 `.codex` 目录外无活动代码修改 ## 当前活跃事实 - `UnifiedSettingsFile.Sections` 已抽象为 `IDictionary`;`UnifiedSettingsDataRepository.CloneFile` 会在底层仍是 `Dictionary` 时保留 comparer,避免改变既有键比较语义 - `LocalizationMap` 通过私有 `Dictionary` 字段配合 `IReadOnlyDictionary` 暴露映射,继续避免把可变集合直接暴露给调用方 -- `CqrsHandlerRegistryGeneratorTests.cs` 已把一批大型 fixture 提升到类级常量,当前目标是以更低噪音方式消化 `MA0051` -- 当前工作树的 tracked 变更集中在: - - `GFramework.Game/Data/UnifiedSettingsFile.cs` - - `GFramework.Game/Data/UnifiedSettingsDataRepository.cs` - - `GFramework.Godot/Setting/Data/LocalizationMap.cs` - - `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` - - `docs/zh-CN/contributing.md` - - `docs/zh-CN/troubleshooting.md` -- 当前还存在未跟踪的 `scripts/dotnet-wsl.sh`,用于 WSL Windows-backed worktree 下统一 `dotnet` 环境参数 +- `CqrsHandlerRegistryGeneratorTests.cs` 已把一批大型 fixture 提升到类级常量,以更低噪音方式消化 `MA0051` +- 通过仓库根目录直接执行 `dotnet build` 已再次确认当前 solution 默认构建成功 +- 本地当前没有新的低风险 warning hotspot;继续扩展 batch 会先增加 branch 体积,而不是继续降低 warning ## 当前风险 -- `dotnet build` 默认 terminal logger 输出会折叠成进度视图,不适合作为 warning 基线采样入口 - - 缓解措施:继续使用 `-tl:off` 收集 warning 计数 -- 当前工作树仍有多处未提交修改;如果直接继续扩展批次,会降低 reviewability - - 缓解措施:先整理并提交当前切片,再决定是否继续下一轮 warning cleanup -- `scripts/dotnet-wsl.sh` 与文档更新属于环境治理切片,是否与本轮 warning-reduction 一起提交需要显式判断 - - 缓解措施:提交前按主题拆分 staging,避免把环境文档与 warning 修正混成一个提交 +- active todo / trace 在 RP-047 之后一度滞后于仓库真值,曾错误保留“工作树仍有未提交切片”的描述 + - 缓解措施:已在 RP-048 回写提交状态、默认 build 验证结果与最新 baseline +- 当前 solution warning 已为 `0`,若继续按 warning-reduction 名义扩展分支,reviewability 收益会快速下降 + - 缓解措施:将本轮 batch 视为自然停点;仅在出现新的 warning hotspot 或明确新切片目标后再继续 ## 活跃文档 @@ -48,15 +40,11 @@ ## 验证说明 -- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release` - - 结果:成功 -- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release` - - 结果:成功 -- `dotnet build GFramework.sln -c Release -tl:off -nologo` - - 结果:成功;`0 Warning(s)`、`0 Error(s)`、`Time Elapsed 00:00:12.72` +- `dotnet build` + - 结果:成功;solution 默认构建通过,`Build succeeded in 16.2s` ## 下一步建议 -1. 先把当前工作树中的 warning-reduction 切片与环境文档切片拆分清楚,避免混合提交 -2. 若确认本轮目标只是收口 warning reduction,则优先提交 `Game` / `Godot` / `SourceGenerators.Tests` 相关修改 -3. 若 `scripts/dotnet-wsl.sh` 与中文文档属于独立环境治理工作,则单独跟踪或另起提交 +1. 将当前 warning-reduction batch 视为已到自然停点,不再为了“凑批次”继续扩大 branch diff +2. 若后续仍要继续 warning work,先重新定位新的 warning hotspot 或回归来源,再开启下一轮批处理 +3. 继续下一轮前优先保持与 `origin/main` 的基线同步,避免在已清零 warning 的前提下无收益扩分支 diff --git a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md index efaa088c..673a8f4a 100644 --- a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md +++ b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md @@ -1,23 +1,24 @@ # Analyzer Warning Reduction 追踪 -## 2026-04-24 — RP-047 +## 2026-04-24 — RP-048 -### 阶段:solution warning 基线复核与 active plan 去噪 +### 阶段:plain `dotnet build` 复核与 batch 停点确认 - 触发背景: - - 用户要求继续按 `$gframework-batch-boot 75` 推进,并明确要求“通过 `dotnet build` 检查警告” - - 用户追加要求清理当前计划中的噪音内容,因此本轮除了复核 warning 基线,还要同步压缩 active todo / trace + - 用户继续按 `$gframework-batch-boot 75` 恢复 analyzer warning reduction + - 启动后发现 active todo 仍描述“工作树有未提交 warning 切片”,需要先核对仓库真值 + - 用户随后明确要求“用 `dotnet build` 不用加其它参数试试” - 主线程实施: - - 先读取 active topic 文档、基线信息与 branch diff 指标,确认 baseline 仍是 `origin/main`(`e692ed3`) - - 复查当前工作树中的 warning-reduction 切片,确认主要未提交修改集中在 `GFramework.Game`、`GFramework.Godot`、`GFramework.SourceGenerators.Tests` - - 执行 `dotnet build GFramework.Game/GFramework.Game.csproj -c Release` 与 `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release`,二者均成功 - - 发现默认 terminal logger 输出不利于读取 warning 数,因此改用 `dotnet build GFramework.sln -c Release -tl:off -nologo` - - solution Release build 在经典 logger 形态下成功完成,结果为 `0 Warning(s)` / `0 Error(s)` / `Time Elapsed 00:00:12.72` - - 基于该真值,压缩 active todo / trace,移除已经过期的 `891 warnings` 旧基线和过多执行形态细节 + - 读取 `AGENTS.md`、`.ai/environment/tools.ai.yaml`、`ai-plan/public/README.md` 以及 `analyzer-warning-reduction` 的 active todo / trace + - 使用显式 `git --git-dir/--work-tree` 绑定确认当前分支为 `fix/analyzer-warning-reduction-batch` + - 重新选择 batch baseline 为 `origin/main`,并记录最新可用 ref:`a8447a6`(`2026-04-24 12:53:39 +0800`);不再使用落后的本地 `main`(`84b40a2`) + - 复核 `origin/main...HEAD` 指标,当前 branch diff 为 `6` 个文件、`1566` 行 + - 复核最近提交,确认 warning-reduction 代码切片已经在 `77e332f`(`fix(analyzer): 收口当前批次警告切片`)落地,工作树当前除 `.codex` 外无活动修改 + - 按用户要求在仓库根目录直接执行 `dotnet build`,默认选中 solution 并成功完成,结果为 `Build succeeded in 16.2s` - 当前结论: - - 当前工作树的 solution warning 基线已经降到 `0 Warning(s)`;active plan 中旧的高噪音 warning 基线不再适合作为恢复入口 - - `-tl:off` 是当前最可靠的 warning 采样入口;默认 terminal logger 更适合看进度,不适合记录计数 - - 当前批次的主要剩余工作不再是继续找 warning,而是整理并提交现有切片,避免 reviewability 下降 + - 当前 solution 在默认 `dotnet build` 路径下可正常通过,RP-047 中“需要额外构建参数才能稳定验证”的假设不应继续作为 active 真值 + - 当前 warning-reduction branch 已没有新的低风险 warning hotspot;继续推进 batch 只会增加 branch 体积,不会继续降低 warning + - 因此本轮批处理应在 `6 / 75` 文件阈值处主动停止,而不是机械地继续扩展 ## Archive Context From b710f31b8675ed79359d3cc9e0b2cfe0378c75f4 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Fri, 24 Apr 2026 13:05:10 +0800 Subject: [PATCH 5/9] =?UTF-8?q?docs(workflow):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=E5=91=8A=E8=AD=A6=E6=A3=80=E6=9F=A5=E7=BA=A6?= =?UTF-8?q?=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 AGENTS.md,明确使用 plain dotnet build 作为默认构建告警检查入口 - 归档 analyzer warning reduction 在 RP-042 至 RP-048 的晚期 active 文档细节 - 压缩 active todo 与 trace,只保留当前分支目标所需的恢复真值 --- AGENTS.md | 6 +++ ...r-warning-reduction-history-rp042-rp048.md | 16 ++++++++ ...r-warning-reduction-history-rp042-rp048.md | 16 ++++++++ .../analyzer-warning-reduction-tracking.md | 38 +++++++++---------- .../analyzer-warning-reduction-trace.md | 29 +++++++------- 5 files changed, 72 insertions(+), 33 deletions(-) create mode 100644 ai-plan/public/analyzer-warning-reduction/archive/todos/analyzer-warning-reduction-history-rp042-rp048.md create mode 100644 ai-plan/public/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp042-rp048.md diff --git a/AGENTS.md b/AGENTS.md index 625a569d..c3932978 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,6 +29,9 @@ All AI agents and contributors must follow these rules when writing, reviewing, ## Git Workflow Rules - Every completed task MUST pass at least one build validation before it is considered done. +- When the goal is to inspect or reduce warnings printed during project build, contributors MUST start from a plain + `dotnet build` at the repository root and treat that output as the default warning inspection entrypoint before + adding extra build parameters or switching to narrower commands. - If the task changes multiple projects or shared abstractions, prefer a solution-level or affected-project `dotnet build ... -c Release`; otherwise use the smallest build command that still proves the result compiles. - When a task adds a feature or modifies code, contributors MUST run a Release build for every directly affected @@ -233,6 +236,9 @@ All generated or modified code MUST include clear and meaningful comments where Use the smallest command set that proves the change, then expand if the change is cross-cutting. ```bash +# Check warnings from the default repository build entrypoint +dotnet build + # Build the full solution dotnet build GFramework.sln -c Release diff --git a/ai-plan/public/analyzer-warning-reduction/archive/todos/analyzer-warning-reduction-history-rp042-rp048.md b/ai-plan/public/analyzer-warning-reduction/archive/todos/analyzer-warning-reduction-history-rp042-rp048.md new file mode 100644 index 00000000..07bce08b --- /dev/null +++ b/ai-plan/public/analyzer-warning-reduction/archive/todos/analyzer-warning-reduction-history-rp042-rp048.md @@ -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) diff --git a/ai-plan/public/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp042-rp048.md b/ai-plan/public/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp042-rp048.md new file mode 100644 index 00000000..56b23480 --- /dev/null +++ b/ai-plan/public/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp042-rp048.md @@ -0,0 +1,16 @@ +# Analyzer Warning Reduction 追踪历史(RP-042 至 RP-048) + +## 范围说明 + +本归档承接 `RP-042` 至 `RP-048` 的 late-stage trace,保留 active trace 在被 RP-049 压缩前的关键执行背景。 + +## 归档摘要 + +- 记录了 warning-reduction batch 在 `origin/main` 基线上的 diff 指标与“接近 75 个文件时停止”的批处理语境 +- 记录了对 plain `dotnet build` 与带参数构建命令的比较,以及当时对 warning 检查入口的整理过程 +- 记录了 RP-048 已确认默认 `dotnet build` 成功且当前工作树无活动代码修改 +- RP-049 之后,这些内容不再作为默认恢复入口,而改为保存在 archive 供历史追溯 + +## superseded by + +- [analyzer-warning-reduction-trace.md](../../traces/analyzer-warning-reduction-trace.md) diff --git a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md index 89c03660..22fffe03 100644 --- a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md +++ b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md @@ -2,49 +2,49 @@ ## 目标 -继续以“低风险、可审查、可恢复”为原则收敛 analyzer warning,并保持 active recovery 文档只保留当前真值。 +继续以“直接看构建输出、直接修构建 warning”为原则推进当前分支,并保持 active recovery 文档只保留当前真值。 ## 当前恢复点 -- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-048` -- 当前阶段:`Phase 48` +- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-049` +- 当前阶段:`Phase 49` - 当前焦点: - - 当前 baseline 为 `origin/main`(`a8447a6`, `2026-04-24 12:53:39 +0800`),batch stop condition 仍为 branch diff 接近 `75` 个文件 - - 当前 branch diff 为 `6` 个文件、`1566` 行(相对 `origin/main...HEAD`),距离 stop condition 仍有空间 - - 当前 warning-reduction 代码切片已经提交到 `77e332f`(`fix(analyzer): 收口当前批次警告切片`) + - 默认 warning 检查入口已统一为仓库根目录直接执行 `dotnet build` + - `2026-04-24` 最新一次 plain `dotnet build` 结果为 `Build succeeded.`、`0 Warning(s)`、`0 Error(s)` + - 当前分支仍为 `fix/analyzer-warning-reduction-batch`,最近相关提交包括 `77e332f` 与 `a98d1cb` - 当前工作树除未跟踪的 `.codex` 目录外无活动代码修改 ## 当前活跃事实 -- `UnifiedSettingsFile.Sections` 已抽象为 `IDictionary`;`UnifiedSettingsDataRepository.CloneFile` 会在底层仍是 `Dictionary` 时保留 comparer,避免改变既有键比较语义 -- `LocalizationMap` 通过私有 `Dictionary` 字段配合 `IReadOnlyDictionary` 暴露映射,继续避免把可变集合直接暴露给调用方 -- `CqrsHandlerRegistryGeneratorTests.cs` 已把一批大型 fixture 提升到类级常量,以更低噪音方式消化 `MA0051` -- 通过仓库根目录直接执行 `dotnet build` 已再次确认当前 solution 默认构建成功 -- 本地当前没有新的低风险 warning hotspot;继续扩展 batch 会先增加 branch 体积,而不是继续降低 warning +- 需要修复的对象是 plain `dotnet build` 实际打印出来的 warning,而不是不同 logger / 参数组合下的命令行为差异 +- 截至当前恢复点,默认 solution 构建入口没有打印 warning,因此没有可立即切分的 warning-fix 代码切片 +- `UnifiedSettingsFile`、`UnifiedSettingsDataRepository`、`LocalizationMap` 与 `CqrsHandlerRegistryGeneratorTests` 的上一轮 warning-reduction 修改已经提交在当前分支历史中 ## 当前风险 -- active todo / trace 在 RP-047 之后一度滞后于仓库真值,曾错误保留“工作树仍有未提交切片”的描述 - - 缓解措施:已在 RP-048 回写提交状态、默认 build 验证结果与最新 baseline -- 当前 solution warning 已为 `0`,若继续按 warning-reduction 名义扩展分支,reviewability 收益会快速下降 - - 缓解措施:将本轮 batch 视为自然停点;仅在出现新的 warning hotspot 或明确新切片目标后再继续 +- active 文档此前过度记录了 batch 停点、构建参数与旧 baseline 细节,容易把恢复重点带偏到“如何检查 warning”而不是“修 warning 本身” + - 缓解措施:active 文档只保留 plain `dotnet build` 的最新结果与下一步动作,把被替换的细节移入 archive +- 如果后续代码修改重新引入 warning,但没有先从 plain `dotnet build` 输出确认,就容易再次偏离当前分支目标 + - 缓解措施:后续每一轮都先跑 plain `dotnet build`,再按实际打印的 warning 逐项处理 ## 活跃文档 +- 当前轮次归档: + - [analyzer-warning-reduction-history-rp042-rp048.md](../archive/todos/analyzer-warning-reduction-history-rp042-rp048.md) - 历史跟踪归档: - [analyzer-warning-reduction-history-rp001.md](../archive/todos/analyzer-warning-reduction-history-rp001.md) - [analyzer-warning-reduction-history-rp002-rp041.md](../archive/todos/analyzer-warning-reduction-history-rp002-rp041.md) - 历史 trace 归档: - [analyzer-warning-reduction-history-rp001.md](../archive/traces/analyzer-warning-reduction-history-rp001.md) - [analyzer-warning-reduction-history-rp002-rp041.md](../archive/traces/analyzer-warning-reduction-history-rp002-rp041.md) + - [analyzer-warning-reduction-history-rp042-rp048.md](../archive/traces/analyzer-warning-reduction-history-rp042-rp048.md) ## 验证说明 - `dotnet build` - - 结果:成功;solution 默认构建通过,`Build succeeded in 16.2s` + - 结果:成功;`0 Warning(s)`、`0 Error(s)`、`Time Elapsed 00:00:14.97` ## 下一步建议 -1. 将当前 warning-reduction batch 视为已到自然停点,不再为了“凑批次”继续扩大 branch diff -2. 若后续仍要继续 warning work,先重新定位新的 warning hotspot 或回归来源,再开启下一轮批处理 -3. 继续下一轮前优先保持与 `origin/main` 的基线同步,避免在已清零 warning 的前提下无收益扩分支 +1. 后续继续当前分支目标时,先跑 plain `dotnet build`,只处理它实际打印出来的 warning +2. 如果下一轮 plain `dotnet build` 仍然保持 `0 Warning(s)`,则当前分支的 build-warning 目标可视为已完成 diff --git a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md index 673a8f4a..ced6be30 100644 --- a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md +++ b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md @@ -1,27 +1,28 @@ # Analyzer Warning Reduction 追踪 -## 2026-04-24 — RP-048 +## 2026-04-24 — RP-049 -### 阶段:plain `dotnet build` 复核与 batch 停点确认 +### 阶段:plain `dotnet build` 入口固化与 active 文档归档压缩 - 触发背景: - - 用户继续按 `$gframework-batch-boot 75` 恢复 analyzer warning reduction - - 启动后发现 active todo 仍描述“工作树有未提交 warning 切片”,需要先核对仓库真值 - - 用户随后明确要求“用 `dotnet build` 不用加其它参数试试” + - 用户要求把“执行 `dotnet build` 来检查警告”写入 `AGENTS.md` + - 用户要求清理或归档 `analyzer-warning-reduction` 的 active todo / trace 内容 + - 用户明确要求继续当前分支的真实目标:修复项目构建时打印的 warning,而不是继续纠结 warning 检查命令本身 - 主线程实施: - - 读取 `AGENTS.md`、`.ai/environment/tools.ai.yaml`、`ai-plan/public/README.md` 以及 `analyzer-warning-reduction` 的 active todo / trace - - 使用显式 `git --git-dir/--work-tree` 绑定确认当前分支为 `fix/analyzer-warning-reduction-batch` - - 重新选择 batch baseline 为 `origin/main`,并记录最新可用 ref:`a8447a6`(`2026-04-24 12:53:39 +0800`);不再使用落后的本地 `main`(`84b40a2`) - - 复核 `origin/main...HEAD` 指标,当前 branch diff 为 `6` 个文件、`1566` 行 - - 复核最近提交,确认 warning-reduction 代码切片已经在 `77e332f`(`fix(analyzer): 收口当前批次警告切片`)落地,工作树当前除 `.codex` 外无活动修改 - - 按用户要求在仓库根目录直接执行 `dotnet build`,默认选中 solution 并成功完成,结果为 `Build succeeded in 16.2s` + - 直接在仓库根目录执行 plain `dotnet build` + - 构建结果为 `Build succeeded.`、`0 Warning(s)`、`0 Error(s)`、`Time Elapsed 00:00:14.97` + - 更新 `AGENTS.md`,明确 plain `dotnet build` 是当前仓库默认的 build-warning 检查入口 + - 将 RP-048 之前 active 文档中关于旧 baseline、batch 停点与构建参数形态的细节移入新的 archive 文件 + - 重写 active todo / trace,只保留当前恢复点需要的真值 - 当前结论: - - 当前 solution 在默认 `dotnet build` 路径下可正常通过,RP-047 中“需要额外构建参数才能稳定验证”的假设不应继续作为 active 真值 - - 当前 warning-reduction branch 已没有新的低风险 warning hotspot;继续推进 batch 只会增加 branch 体积,不会继续降低 warning - - 因此本轮批处理应在 `6 / 75` 文件阈值处主动停止,而不是机械地继续扩展 + - 当前分支在默认 solution 构建入口下没有打印 warning,因此此刻没有新的 warning-fix 代码切片可继续实施 + - 当前分支目标没有改变:后续只要 plain `dotnet build` 再次打印 warning,就以该输出为唯一切片来源继续修复 ## Archive Context +- 当前轮次归档: + - [analyzer-warning-reduction-history-rp042-rp048.md](../archive/todos/analyzer-warning-reduction-history-rp042-rp048.md) + - [analyzer-warning-reduction-history-rp042-rp048.md](../archive/traces/analyzer-warning-reduction-history-rp042-rp048.md) - 历史跟踪归档: - [analyzer-warning-reduction-history-rp001.md](../archive/todos/analyzer-warning-reduction-history-rp001.md) - [analyzer-warning-reduction-history-rp002-rp041.md](../archive/todos/analyzer-warning-reduction-history-rp002-rp041.md) From 25d33d0bf9541633ac0f8447b416c8722c681ef3 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Fri, 24 Apr 2026 13:08:11 +0800 Subject: [PATCH 6/9] =?UTF-8?q?chore(build):=20=E6=9B=B4=E6=96=B0=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E9=85=8D=E7=BD=AE=E4=BB=A5=E5=90=AF=E7=94=A8=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E5=88=86=E6=9E=90=E5=B9=B6=E4=BF=AE=E6=94=B9=E8=AE=B8?= =?UTF-8?q?=E5=8F=AF=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除测试项目的警告级别设置 - 将包许可证从 MIT 更改为 Apache-2.0 - 为 GFramework 项目启用 .NET 代码分析器 - 保持目标框架 net8.0、net9.0 和 net10.0 的支持 --- GFramework.Core.Tests/GFramework.Core.Tests.csproj | 1 - GFramework.Godot.Tests/GFramework.Godot.Tests.csproj | 1 - GFramework.csproj | 3 ++- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/GFramework.Core.Tests/GFramework.Core.Tests.csproj b/GFramework.Core.Tests/GFramework.Core.Tests.csproj index efea29cf..968fedd2 100644 --- a/GFramework.Core.Tests/GFramework.Core.Tests.csproj +++ b/GFramework.Core.Tests/GFramework.Core.Tests.csproj @@ -7,7 +7,6 @@ enable false true - 0 diff --git a/GFramework.Godot.Tests/GFramework.Godot.Tests.csproj b/GFramework.Godot.Tests/GFramework.Godot.Tests.csproj index fea6d616..8712c964 100644 --- a/GFramework.Godot.Tests/GFramework.Godot.Tests.csproj +++ b/GFramework.Godot.Tests/GFramework.Godot.Tests.csproj @@ -7,7 +7,6 @@ enable false true - 0 diff --git a/GFramework.csproj b/GFramework.csproj index e4af07c1..00e67766 100644 --- a/GFramework.csproj +++ b/GFramework.csproj @@ -8,7 +8,7 @@ Copyright © 2025 https://github.com/GeWuYou/GFramework https://github.com/GeWuYou/GFramework - MIT + Apache-2.0 game;framework true true @@ -16,6 +16,7 @@ README.md net8.0;net9.0;net10.0 false + true false From a439fb8f4e1337703d6d398a098e70a0646d0d5e Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Fri, 24 Apr 2026 13:25:42 +0800 Subject: [PATCH 7/9] =?UTF-8?q?refactor(godot-source-generators):=20?= =?UTF-8?q?=E6=B8=85=E7=90=86=E7=94=9F=E6=88=90=E5=99=A8=E5=91=8A=E8=AD=A6?= =?UTF-8?q?=E4=B8=8E=E6=9E=84=E5=BB=BA=E5=9F=BA=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构 Godot source generator 的长方法与字符串比较逻辑,清理 GFramework.Godot.SourceGenerators 的 MA0051 和 MA0006 告警 - 更新 AutoRegisterExportedCollectionsGenerator 的注册解析阶段拆分,消除剩余的长方法告警 - 更新 AGENTS 与 analyzer-warning-reduction 跟踪文档,明确 warning 检查必须先 clean 再 build --- AGENTS.md | 8 +- .../BindNodeSignalGenerator.cs | 361 ++++++++++++------ .../GetNodeGenerator.cs | 21 +- .../GodotProjectMetadataGenerator.cs | 179 ++++++--- ...utoRegisterExportedCollectionsGenerator.cs | 142 +++++-- .../analyzer-warning-reduction-tracking.md | 37 +- .../analyzer-warning-reduction-trace.md | 29 +- 7 files changed, 539 insertions(+), 238 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c3932978..ae03504e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,9 +29,10 @@ All AI agents and contributors must follow these rules when writing, reviewing, ## Git Workflow Rules - Every completed task MUST pass at least one build validation before it is considered done. -- When the goal is to inspect or reduce warnings printed during project build, contributors MUST start from a plain - `dotnet build` at the repository root and treat that output as the default warning inspection entrypoint before - adding extra build parameters or switching to narrower commands. +- When the goal is to inspect or reduce warnings printed during project build, contributors MUST establish the warning + baseline from a non-incremental repository-root build by running `dotnet clean` and then `dotnet build`. +- Contributors MUST NOT treat a repeated incremental `dotnet build` result as authoritative for warning inspection when + a clean baseline has not been captured in the same round. - If the task changes multiple projects or shared abstractions, prefer a solution-level or affected-project `dotnet build ... -c Release`; otherwise use the smallest build command that still proves the result compiles. - When a task adds a feature or modifies code, contributors MUST run a Release build for every directly affected @@ -237,6 +238,7 @@ Use the smallest command set that proves the change, then expand if the change i ```bash # Check warnings from the default repository build entrypoint +dotnet clean dotnet build # Build the full solution diff --git a/GFramework.Godot.SourceGenerators/BindNodeSignalGenerator.cs b/GFramework.Godot.SourceGenerators/BindNodeSignalGenerator.cs index 610662a2..e0e4e3a1 100644 --- a/GFramework.Godot.SourceGenerators/BindNodeSignalGenerator.cs +++ b/GFramework.Godot.SourceGenerators/BindNodeSignalGenerator.cs @@ -72,19 +72,8 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator if (bindNodeSignalAttribute is null || godotNodeSymbol is null) return; - // 缓存每个方法上已解析的特性,避免在筛选和生成阶段重复做语义查询。 - var methodAttributes = candidates - .Where(static candidate => candidate is not null) - .Select(static candidate => candidate!) - .ToDictionary( - static candidate => candidate, - candidate => ResolveAttributes(candidate.MethodSymbol, bindNodeSignalAttribute), - ReferenceEqualityComparer.Instance); - - var methodCandidates = methodAttributes - .Where(static pair => pair.Value.Count > 0) - .Select(static pair => pair.Key) - .ToList(); + var methodAttributes = BuildMethodAttributeMap(candidates, bindNodeSignalAttribute); + var methodCandidates = CollectMethodCandidates(methodAttributes); foreach (var group in GroupByContainingType(methodCandidates)) { @@ -99,19 +88,7 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator UnbindMethodName)) continue; - var bindings = new List(); - - foreach (var candidate in group.Methods) - { - foreach (var attribute in methodAttributes[candidate]) - { - if (!TryCreateBinding(context, candidate, attribute, godotNodeSymbol, out var binding)) - continue; - - bindings.Add(binding); - } - } - + var bindings = CollectBindings(context, group, methodAttributes, godotNodeSymbol); if (bindings.Count == 0) continue; @@ -171,99 +148,22 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator if (candidate.MethodSymbol.IsStatic) { - ReportMethodDiagnostic( - context, - BindNodeSignalDiagnostics.StaticMethodNotSupported, - candidate, - attribute, - candidate.MethodSymbol.Name); + ReportStaticMethodDiagnostic(context, candidate, attribute); return false; } - if (!TryResolveCtorString(attribute, 0, out var nodeFieldName)) - { - ReportMethodDiagnostic( - context, - BindNodeSignalDiagnostics.InvalidConstructorArgument, - candidate, - attribute, - candidate.MethodSymbol.Name, - "nodeFieldName"); + if (!TryResolveBindingTargetNames(context, candidate, attribute, out var nodeFieldName, out var signalName)) return false; - } - if (!TryResolveCtorString(attribute, 1, out var signalName)) - { - ReportMethodDiagnostic( - context, - BindNodeSignalDiagnostics.InvalidConstructorArgument, - candidate, - attribute, - candidate.MethodSymbol.Name, - "signalName"); + if (!TryFindCompatibleField(context, candidate, attribute, godotNodeSymbol, nodeFieldName, out var fieldSymbol)) return false; - } - var fieldSymbol = FindField(candidate.MethodSymbol.ContainingType, nodeFieldName); - if (fieldSymbol is null) - { - ReportMethodDiagnostic( - context, - BindNodeSignalDiagnostics.NodeFieldNotFound, - candidate, - attribute, - candidate.MethodSymbol.Name, - nodeFieldName, - candidate.MethodSymbol.ContainingType.Name); + if (!TryFindCompatibleEvent(context, candidate, attribute, fieldSymbol, signalName, out var eventSymbol)) return false; - } - - if (fieldSymbol.IsStatic) - { - ReportMethodDiagnostic( - context, - BindNodeSignalDiagnostics.NodeFieldMustBeInstanceField, - candidate, - attribute, - candidate.MethodSymbol.Name, - fieldSymbol.Name); - return false; - } - - if (!fieldSymbol.Type.IsAssignableTo(godotNodeSymbol)) - { - ReportMethodDiagnostic( - context, - BindNodeSignalDiagnostics.FieldTypeMustDeriveFromNode, - candidate, - attribute, - fieldSymbol.Name); - return false; - } - - var eventSymbol = FindEvent(fieldSymbol.Type, signalName); - if (eventSymbol is null) - { - ReportMethodDiagnostic( - context, - BindNodeSignalDiagnostics.SignalNotFound, - candidate, - attribute, - fieldSymbol.Name, - signalName); - return false; - } if (!IsMethodCompatibleWithEvent(candidate.MethodSymbol, eventSymbol)) { - ReportMethodDiagnostic( - context, - BindNodeSignalDiagnostics.MethodSignatureNotCompatible, - candidate, - attribute, - candidate.MethodSymbol.Name, - eventSymbol.Name, - fieldSymbol.Name); + ReportIncompatibleSignatureDiagnostic(context, candidate, attribute, eventSymbol, fieldSymbol); return false; } @@ -271,6 +171,235 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator return true; } + private static Dictionary> BuildMethodAttributeMap( + ImmutableArray 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 CollectMethodCandidates( + IReadOnlyDictionary> methodAttributes) + { + return methodAttributes + .Where(static pair => pair.Value.Count > 0) + .Select(static pair => pair.Key) + .ToList(); + } + + private static List CollectBindings( + SourceProductionContext context, + TypeGroup group, + IReadOnlyDictionary> methodAttributes, + INamedTypeSymbol godotNodeSymbol) + { + var bindings = new List(); + + foreach (var candidate in group.Methods) + { + foreach (var attribute in methodAttributes[candidate]) + { + if (!TryCreateBinding(context, candidate, attribute, godotNodeSymbol, out var binding)) + continue; + + bindings.Add(binding); + } + } + + return bindings; + } + + private static void ReportStaticMethodDiagnostic( + SourceProductionContext context, + MethodCandidate candidate, + AttributeData attribute) + { + ReportMethodDiagnostic( + context, + BindNodeSignalDiagnostics.StaticMethodNotSupported, + candidate, + attribute, + candidate.MethodSymbol.Name); + } + + private static bool TryResolveBindingTargetNames( + SourceProductionContext context, + MethodCandidate candidate, + AttributeData attribute, + out string nodeFieldName, + out string signalName) + { + nodeFieldName = string.Empty; + signalName = string.Empty; + + if (!TryResolveCtorString(attribute, 0, out nodeFieldName)) + { + ReportInvalidConstructorArgumentDiagnostic(context, candidate, attribute, "nodeFieldName"); + return false; + } + + if (!TryResolveCtorString(attribute, 1, out signalName)) + { + ReportInvalidConstructorArgumentDiagnostic(context, candidate, attribute, "signalName"); + return false; + } + + return true; + } + + private static void ReportInvalidConstructorArgumentDiagnostic( + SourceProductionContext context, + MethodCandidate candidate, + AttributeData attribute, + string argumentName) + { + ReportMethodDiagnostic( + context, + BindNodeSignalDiagnostics.InvalidConstructorArgument, + candidate, + attribute, + candidate.MethodSymbol.Name, + argumentName); + } + + private static bool TryFindCompatibleField( + SourceProductionContext context, + MethodCandidate candidate, + AttributeData attribute, + INamedTypeSymbol godotNodeSymbol, + string nodeFieldName, + out IFieldSymbol fieldSymbol) + { + fieldSymbol = null!; + + var resolvedField = FindField(candidate.MethodSymbol.ContainingType, nodeFieldName); + if (resolvedField is null) + { + ReportNodeFieldNotFoundDiagnostic(context, candidate, attribute, nodeFieldName); + return false; + } + + if (resolvedField.IsStatic) + { + ReportNodeFieldMustBeInstanceDiagnostic(context, candidate, attribute, resolvedField); + return false; + } + + if (!resolvedField.Type.IsAssignableTo(godotNodeSymbol)) + { + ReportFieldTypeMustDeriveFromNodeDiagnostic(context, candidate, attribute, resolvedField); + return false; + } + + fieldSymbol = resolvedField; + return true; + } + + private static void ReportNodeFieldNotFoundDiagnostic( + SourceProductionContext context, + MethodCandidate candidate, + AttributeData attribute, + string nodeFieldName) + { + ReportMethodDiagnostic( + context, + BindNodeSignalDiagnostics.NodeFieldNotFound, + candidate, + attribute, + candidate.MethodSymbol.Name, + nodeFieldName, + candidate.MethodSymbol.ContainingType.Name); + } + + private static void ReportNodeFieldMustBeInstanceDiagnostic( + SourceProductionContext context, + MethodCandidate candidate, + AttributeData attribute, + IFieldSymbol fieldSymbol) + { + ReportMethodDiagnostic( + context, + BindNodeSignalDiagnostics.NodeFieldMustBeInstanceField, + candidate, + attribute, + candidate.MethodSymbol.Name, + fieldSymbol.Name); + } + + private static void ReportFieldTypeMustDeriveFromNodeDiagnostic( + SourceProductionContext context, + MethodCandidate candidate, + AttributeData attribute, + IFieldSymbol fieldSymbol) + { + ReportMethodDiagnostic( + context, + BindNodeSignalDiagnostics.FieldTypeMustDeriveFromNode, + candidate, + attribute, + fieldSymbol.Name); + } + + private static bool TryFindCompatibleEvent( + SourceProductionContext context, + MethodCandidate candidate, + AttributeData attribute, + IFieldSymbol fieldSymbol, + string signalName, + out IEventSymbol eventSymbol) + { + eventSymbol = null!; + + var resolvedEvent = FindEvent(fieldSymbol.Type, signalName); + if (resolvedEvent is null) + { + ReportSignalNotFoundDiagnostic(context, candidate, attribute, fieldSymbol, signalName); + return false; + } + + eventSymbol = resolvedEvent; + return true; + } + + private static void ReportSignalNotFoundDiagnostic( + SourceProductionContext context, + MethodCandidate candidate, + AttributeData attribute, + IFieldSymbol fieldSymbol, + string signalName) + { + ReportMethodDiagnostic( + context, + BindNodeSignalDiagnostics.SignalNotFound, + candidate, + attribute, + fieldSymbol.Name, + signalName); + } + + private static void ReportIncompatibleSignatureDiagnostic( + SourceProductionContext context, + MethodCandidate candidate, + AttributeData attribute, + IEventSymbol eventSymbol, + IFieldSymbol fieldSymbol) + { + ReportMethodDiagnostic( + context, + BindNodeSignalDiagnostics.MethodSignatureNotCompatible, + candidate, + attribute, + candidate.MethodSymbol.Name, + eventSymbol.Name, + fieldSymbol.Name); + } + private static void ReportMethodDiagnostic( SourceProductionContext context, DiagnosticDescriptor descriptor, @@ -404,11 +533,7 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator { return typeSymbol.GetMembers() .OfType() - .FirstOrDefault(method => - method.Name == methodName && - !method.IsStatic && - method.Parameters.Length == 0 && - method.MethodKind == MethodKind.Ordinary); + .FirstOrDefault(method => IsParameterlessInstanceMethod(method, methodName)); } private static bool CallsGeneratedMethod( @@ -447,6 +572,14 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator }; } + private static bool IsParameterlessInstanceMethod(IMethodSymbol method, string methodName) + { + return string.Equals(method.Name, methodName, StringComparison.Ordinal) && + !method.IsStatic && + method.Parameters.Length == 0 && + method.MethodKind == MethodKind.Ordinary; + } + private static bool IsBindNodeSignalAttributeName(NameSyntax attributeName) { var simpleName = GetAttributeSimpleName(attributeName); @@ -608,4 +741,4 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator return RuntimeHelpers.GetHashCode(obj); } } -} \ No newline at end of file +} diff --git a/GFramework.Godot.SourceGenerators/GetNodeGenerator.cs b/GFramework.Godot.SourceGenerators/GetNodeGenerator.cs index 6aa3ac81..7122faab 100644 --- a/GFramework.Godot.SourceGenerators/GetNodeGenerator.cs +++ b/GFramework.Godot.SourceGenerators/GetNodeGenerator.cs @@ -259,11 +259,7 @@ public sealed class GetNodeGenerator : IIncrementalGenerator { return typeSymbol.GetMembers() .OfType() - .FirstOrDefault(static method => - method.Name == "_Ready" && - !method.IsStatic && - method.Parameters.Length == 0 && - method.MethodKind == MethodKind.Ordinary); + .FirstOrDefault(static method => IsParameterlessInstanceMethod(method, "_Ready")); } private static bool CallsGeneratedInjection(IMethodSymbol readyMethod) @@ -306,6 +302,14 @@ public sealed class GetNodeGenerator : IIncrementalGenerator return attribute.GetNamedArgument("Required", true); } + private static bool IsParameterlessInstanceMethod(IMethodSymbol method, string methodName) + { + return string.Equals(method.Name, methodName, StringComparison.Ordinal) && + !method.IsStatic && + method.Parameters.Length == 0 && + method.MethodKind == MethodKind.Ordinary; + } + private static bool TryResolvePath( IFieldSymbol fieldSymbol, AttributeData attribute, @@ -373,7 +377,10 @@ public sealed class GetNodeGenerator : IIncrementalGenerator if (!string.Equals(namedArgument.Key, "Lookup", StringComparison.Ordinal)) continue; - if (namedArgument.Value.Type?.ToDisplayString() != GetNodeLookupModeMetadataName) + if (!string.Equals( + namedArgument.Value.Type?.ToDisplayString(), + GetNodeLookupModeMetadataName, + StringComparison.Ordinal)) continue; if (namedArgument.Value.Value is int value) @@ -568,4 +575,4 @@ public sealed class GetNodeGenerator : IIncrementalGenerator public List Fields { get; } = new(); } -} \ No newline at end of file +} diff --git a/GFramework.Godot.SourceGenerators/GodotProjectMetadataGenerator.cs b/GFramework.Godot.SourceGenerators/GodotProjectMetadataGenerator.cs index c080cc71..400c2f31 100644 --- a/GFramework.Godot.SourceGenerators/GodotProjectMetadataGenerator.cs +++ b/GFramework.Godot.SourceGenerators/GodotProjectMetadataGenerator.cs @@ -126,7 +126,27 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator var explicitMappings = new Dictionary>(StringComparer.Ordinal); var implicitCandidates = new Dictionary>(StringComparer.Ordinal); + CollectMappingCandidates( + context, + typeCandidates, + autoLoadAttributeSymbol, + godotNodeSymbol, + projectAutoLoadNames, + explicitMappings, + implicitCandidates); + return ResolveTypedMappings(context, projectAutoLoadNames, explicitMappings, implicitCandidates); + } + + private static void CollectMappingCandidates( + SourceProductionContext context, + IReadOnlyList typeCandidates, + INamedTypeSymbol? autoLoadAttributeSymbol, + INamedTypeSymbol godotNodeSymbol, + ISet projectAutoLoadNames, + IDictionary> explicitMappings, + IDictionary> implicitCandidates) + { foreach (var candidate in typeCandidates) { var typeSymbol = candidate.TypeSymbol; @@ -176,7 +196,14 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator explicitList.Add(typeSymbol); } + } + private static Dictionary ResolveTypedMappings( + SourceProductionContext context, + IEnumerable projectAutoLoadNames, + IReadOnlyDictionary> explicitMappings, + IReadOnlyDictionary> implicitCandidates) + { var resolvedMappings = new Dictionary(StringComparer.Ordinal); foreach (var projectAutoLoadName in projectAutoLoadNames.OrderBy(static name => name, StringComparer.Ordinal)) @@ -408,24 +435,40 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator foreach (var member in members) { - builder.AppendLine(" /// "); - builder.AppendLine($" /// 获取 AutoLoad {member.AutoLoadName}。"); - builder.AppendLine(" /// "); - builder.AppendLine( - $" public static {member.TypeName} {member.Identifier} => GetRequiredNode<{member.TypeName}>({SymbolDisplay.FormatLiteral(member.AutoLoadName, true)});"); - builder.AppendLine(); - builder.AppendLine(" /// "); - builder.AppendLine($" /// 尝试获取 AutoLoad {member.AutoLoadName}。"); - builder.AppendLine(" /// "); - builder.AppendLine( - $" public static bool TryGet{member.Identifier}(out {member.TypeName}? value)"); - builder.AppendLine(" {"); - builder.AppendLine( - $" return TryGetNode({SymbolDisplay.FormatLiteral(member.AutoLoadName, true)}, out value);"); - builder.AppendLine(" }"); - builder.AppendLine(); + AppendAutoLoadMemberSource(builder, member); } + AppendGetRequiredNodeSource(builder); + AppendTryGetNodeSource(builder); + builder.AppendLine("}"); + + return builder.ToString(); + } + + private static void AppendAutoLoadMemberSource( + StringBuilder builder, + GeneratedAutoLoadMember member) + { + builder.AppendLine(" /// "); + builder.AppendLine($" /// 获取 AutoLoad {member.AutoLoadName}。"); + builder.AppendLine(" /// "); + builder.AppendLine( + $" public static {member.TypeName} {member.Identifier} => GetRequiredNode<{member.TypeName}>({SymbolDisplay.FormatLiteral(member.AutoLoadName, true)});"); + builder.AppendLine(); + builder.AppendLine(" /// "); + builder.AppendLine($" /// 尝试获取 AutoLoad {member.AutoLoadName}。"); + builder.AppendLine(" /// "); + 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(" /// "); builder.AppendLine(" /// 获取一个必填的 AutoLoad 节点;缺失时抛出异常。"); builder.AppendLine(" /// "); @@ -444,6 +487,10 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator " throw new global::System.InvalidOperationException($\"AutoLoad '{autoLoadName}' is not available on the active SceneTree root.\");"); builder.AppendLine(" }"); builder.AppendLine(); + } + + private static void AppendTryGetNodeSource(StringBuilder builder) + { builder.AppendLine(" /// "); builder.AppendLine(" /// 尝试从当前 SceneTree 根节点解析 AutoLoad。"); builder.AppendLine(" /// "); @@ -470,9 +517,6 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator builder.AppendLine(" value = root.GetNodeOrNull($\"/root/{autoLoadName}\");"); builder.AppendLine(" return value is not null;"); builder.AppendLine(" }"); - builder.AppendLine("}"); - - return builder.ToString(); } private static string GenerateInputActionsSource(IReadOnlyList members) @@ -530,45 +574,16 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator if (string.IsNullOrWhiteSpace(content) || content.StartsWith(";", StringComparison.Ordinal)) continue; - if (content.StartsWith("[", StringComparison.Ordinal) && content.EndsWith("]", StringComparison.Ordinal)) - { - currentSection = content.Substring(1, content.Length - 2).Trim(); + if (TryUpdateSection(content, ref currentSection)) continue; - } if (!TryParseAssignment(content, out var key, out var value)) continue; - if (string.Equals(currentSection, "autoload", StringComparison.OrdinalIgnoreCase)) - { - if (!seenAutoLoads.Add(key)) - { - diagnostics.Add(Diagnostic.Create( - GodotProjectDiagnostics.DuplicateAutoLoadEntry, - CreateFileLocation(file.Path), - key)); - continue; - } - - autoLoads.Add(new ProjectAutoLoadEntry( - key, - NormalizeProjectPath(value))); + if (TryCollectAutoLoadEntry(file, currentSection, key, value, seenAutoLoads, autoLoads, diagnostics)) continue; - } - if (string.Equals(currentSection, "input", StringComparison.OrdinalIgnoreCase)) - { - if (!seenInputActions.Add(key)) - { - diagnostics.Add(Diagnostic.Create( - GodotProjectDiagnostics.DuplicateInputActionEntry, - CreateFileLocation(file.Path), - key)); - continue; - } - - inputActions.Add(key); - } + TryCollectInputAction(currentSection, key, seenInputActions, inputActions, diagnostics, file.Path); } return new ProjectMetadataParseResult( @@ -578,6 +593,68 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator diagnostics.ToImmutableArray()); } + private static bool TryUpdateSection(string content, ref string currentSection) + { + if (!content.StartsWith("[", StringComparison.Ordinal) || + !content.EndsWith("]", StringComparison.Ordinal)) + { + return false; + } + + currentSection = content.Substring(1, content.Length - 2).Trim(); + return true; + } + + private static bool TryCollectAutoLoadEntry( + AdditionalText file, + string currentSection, + string key, + string value, + ISet seenAutoLoads, + ICollection autoLoads, + ICollection 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 seenInputActions, + ICollection inputActions, + ICollection diagnostics, + string filePath) + { + if (!string.Equals(currentSection, "input", StringComparison.OrdinalIgnoreCase)) + return; + + if (!seenInputActions.Add(key)) + { + diagnostics.Add(Diagnostic.Create( + GodotProjectDiagnostics.DuplicateInputActionEntry, + CreateFileLocation(filePath), + key)); + return; + } + + inputActions.Add(key); + } + private static string NormalizeProjectPath(string rawValue) { var trimmed = rawValue.Trim(); diff --git a/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs b/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs index 93219dcb..23dc1109 100644 --- a/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs +++ b/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs @@ -190,6 +190,48 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener { registration = null!; + if (!TryResolveCollectionType(context, collectionMember, enumerableType, out var collectionType)) + return false; + + if (!TryResolveRegistryTarget( + context, + compilation, + ownerType, + collectionMember, + attribute, + out var registryMemberName, + out var registerMethodName, + out var registryType)) + { + return false; + } + + if (!TryResolveElementType(context, collectionMember, collectionType, out var elementType)) + return false; + + if (!HasCompatibleRegisterMethod(compilation, ownerType, registryType, registerMethodName, elementType)) + { + context.ReportDiagnostic(Diagnostic.Create( + AutoRegisterExportedCollectionsDiagnostics.RegisterMethodNotFound, + collectionMember.Locations.FirstOrDefault() ?? Location.None, + registerMethodName, + registryMemberName, + collectionMember.Name)); + return false; + } + + registration = new RegistrationSpec(collectionMember.Name, registryMemberName, registerMethodName); + return true; + } + + private static bool TryResolveCollectionType( + SourceProductionContext context, + ISymbol collectionMember, + INamedTypeSymbol enumerableType, + out ITypeSymbol collectionType) + { + collectionType = null!; + if (!IsInstanceReadableMember(collectionMember)) { context.ReportDiagnostic(Diagnostic.Create( @@ -199,17 +241,11 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener return false; } - var collectionType = collectionMember switch - { - IFieldSymbol field => field.Type, - IPropertySymbol property => property.Type, - _ => null - }; - - if (collectionType is null) + var resolvedType = GetMemberType(collectionMember); + if (resolvedType is null) return false; - if (!collectionType.IsAssignableTo(enumerableType)) + if (!resolvedType.IsAssignableTo(enumerableType)) { context.ReportDiagnostic(Diagnostic.Create( AutoRegisterExportedCollectionsDiagnostics.CollectionTypeMustBeEnumerable, @@ -218,12 +254,35 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener return false; } - if (!TryGetRegistrationAttributeArguments(context, collectionMember, attribute, out var registryMemberName, - out var registerMethodName)) + collectionType = resolvedType; + return true; + } + + private static bool TryResolveRegistryTarget( + SourceProductionContext context, + Compilation compilation, + INamedTypeSymbol ownerType, + ISymbol collectionMember, + AttributeData attribute, + out string registryMemberName, + out string registerMethodName, + out INamedTypeSymbol registryType) + { + registryMemberName = string.Empty; + registerMethodName = string.Empty; + registryType = null!; + + if (!TryGetRegistrationAttributeArguments( + context, + collectionMember, + attribute, + out registryMemberName, + out registerMethodName)) + { return false; + } var registryMember = FindRegistryMember(ownerType, registryMemberName); - if (registryMember is null) { context.ReportDiagnostic(Diagnostic.Create( @@ -246,18 +305,24 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener return false; } - var registryType = registryMember switch - { - IFieldSymbol field => field.Type as INamedTypeSymbol, - IPropertySymbol property => property.Type as INamedTypeSymbol, - _ => null - }; - - if (registryType is null) + var resolvedRegistryType = GetMemberType(registryMember) as INamedTypeSymbol; + if (resolvedRegistryType is null) return false; - var elementType = TryGetElementType(collectionType); - if (elementType is null) + registryType = resolvedRegistryType; + return true; + } + + private static bool TryResolveElementType( + SourceProductionContext context, + ISymbol collectionMember, + ITypeSymbol collectionType, + out ITypeSymbol elementType) + { + elementType = null!; + + var resolvedElementType = TryGetElementType(collectionType); + if (resolvedElementType is null) { // Non-generic IEnumerable exposes elements as object at compile time, which is not safe // for validating or generating a strongly typed registry call. @@ -268,26 +333,33 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener return false; } - var hasCompatibleMethod = EnumerateCandidateMethods(registryType, registerMethodName) + elementType = resolvedElementType; + return true; + } + + private static bool HasCompatibleRegisterMethod( + Compilation compilation, + INamedTypeSymbol ownerType, + INamedTypeSymbol registryType, + string registerMethodName, + ITypeSymbol elementType) + { + return EnumerateCandidateMethods(registryType, registerMethodName) .Any(method => !method.IsStatic && method.Parameters.Length == 1 && compilation.IsSymbolAccessibleWithin(method, ownerType) && CanAcceptElementType(compilation, elementType, method.Parameters[0].Type)); + } - if (!hasCompatibleMethod) + private static ITypeSymbol? GetMemberType(ISymbol member) + { + return member switch { - context.ReportDiagnostic(Diagnostic.Create( - AutoRegisterExportedCollectionsDiagnostics.RegisterMethodNotFound, - collectionMember.Locations.FirstOrDefault() ?? Location.None, - registerMethodName, - registryMemberName, - collectionMember.Name)); - return false; - } - - registration = new RegistrationSpec(collectionMember.Name, registryMemberName, registerMethodName); - return true; + IFieldSymbol field => field.Type, + IPropertySymbol property => property.Type, + _ => null + }; } private static bool IsInstanceReadableMember(ISymbol member) diff --git a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md index 22fffe03..dbc281af 100644 --- a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md +++ b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md @@ -6,26 +6,27 @@ ## 当前恢复点 -- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-049` -- 当前阶段:`Phase 49` +- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-050` +- 当前阶段:`Phase 50` - 当前焦点: - - 默认 warning 检查入口已统一为仓库根目录直接执行 `dotnet build` - - `2026-04-24` 最新一次 plain `dotnet build` 结果为 `Build succeeded.`、`0 Warning(s)`、`0 Error(s)` - - 当前分支仍为 `fix/analyzer-warning-reduction-batch`,最近相关提交包括 `77e332f` 与 `a98d1cb` - - 当前工作树除未跟踪的 `.codex` 目录外无活动代码修改 + - warning 基线已修正为仓库根目录执行 `dotnet clean` 后再执行 `dotnet build` + - `2026-04-24` 用户确认的 clean solution build 结果为 `Build succeeded with 1193 warning(s)` + - 当前主线程切片为 `GFramework.Godot.SourceGenerators` + - 当前工作树除未跟踪的 `.codex` 目录外,存在待提交的 source generator / `AGENTS.md` / `ai-plan` 修改 ## 当前活跃事实 -- 需要修复的对象是 plain `dotnet build` 实际打印出来的 warning,而不是不同 logger / 参数组合下的命令行为差异 -- 截至当前恢复点,默认 solution 构建入口没有打印 warning,因此没有可立即切分的 warning-fix 代码切片 -- `UnifiedSettingsFile`、`UnifiedSettingsDataRepository`、`LocalizationMap` 与 `CqrsHandlerRegistryGeneratorTests` 的上一轮 warning-reduction 修改已经提交在当前分支历史中 +- 之前记录的 plain `dotnet build` `0 Warning(s)` 属于增量构建假阴性,不能再作为 warning 检查真值 +- 本轮已完成 `GFramework.Godot.SourceGenerators` warning 清理:clean `Release` build 从 9 个 warning 降至 0 个 warning +- 当前已确认解决的文件包括 `BindNodeSignalGenerator.cs`、`GetNodeGenerator.cs`、`GodotProjectMetadataGenerator.cs`、`Registration/AutoRegisterExportedCollectionsGenerator.cs` +- 后续 warning-reduction 仍应以 clean solution build 的真实输出为切片来源 ## 当前风险 -- active 文档此前过度记录了 batch 停点、构建参数与旧 baseline 细节,容易把恢复重点带偏到“如何检查 warning”而不是“修 warning 本身” - - 缓解措施:active 文档只保留 plain `dotnet build` 的最新结果与下一步动作,把被替换的细节移入 archive -- 如果后续代码修改重新引入 warning,但没有先从 plain `dotnet build` 输出确认,就容易再次偏离当前分支目标 - - 缓解措施:后续每一轮都先跑 plain `dotnet build`,再按实际打印的 warning 逐项处理 +- 如果后续继续依赖增量 `dotnet build`,容易再次把 warning 数量误判为 0 + - 缓解措施:每轮 warning 检查前先执行 `dotnet clean`,再执行目标 `dotnet build` +- 当前只验证了受影响项目 `GFramework.Godot.SourceGenerators`;整仓库 warning 总量仍应以用户确认的 clean solution build 为基线 + - 缓解措施:下一轮从 clean solution build 输出里选择新的低风险 warning 热点继续切片 ## 活跃文档 @@ -41,10 +42,14 @@ ## 验证说明 +- `dotnet clean GFramework.Godot.SourceGenerators/GFramework.Godot.SourceGenerators.csproj -c Release` + - 结果:成功;`0 Warning(s)`、`0 Error(s)` +- `dotnet build GFramework.Godot.SourceGenerators/GFramework.Godot.SourceGenerators.csproj -c Release` + - 结果:成功;`0 Warning(s)`、`0 Error(s)` - `dotnet build` - - 结果:成功;`0 Warning(s)`、`0 Error(s)`、`Time Elapsed 00:00:14.97` + - 结果:此前被误记为 `0 Warning(s)`;现已确认这是增量构建假阴性,不再作为有效基线 ## 下一步建议 -1. 后续继续当前分支目标时,先跑 plain `dotnet build`,只处理它实际打印出来的 warning -2. 如果下一轮 plain `dotnet build` 仍然保持 `0 Warning(s)`,则当前分支的 build-warning 目标可视为已完成 +1. 在仓库根目录先执行 `dotnet clean`、再执行 `dotnet build`,重新采集当前 solution 的真实 warning 列表 +2. 以 clean build 输出中的下一个低风险热点作为新切片,优先继续 source generator、测试或单模块可局部验证的问题 diff --git a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md index ced6be30..725b2f08 100644 --- a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md +++ b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md @@ -1,22 +1,27 @@ # Analyzer Warning Reduction 追踪 -## 2026-04-24 — RP-049 +## 2026-04-24 — RP-050 -### 阶段:plain `dotnet build` 入口固化与 active 文档归档压缩 +### 阶段:clean-build 基线修正与 `GFramework.Godot.SourceGenerators` 切片清零 - 触发背景: - - 用户要求把“执行 `dotnet build` 来检查警告”写入 `AGENTS.md` - - 用户要求清理或归档 `analyzer-warning-reduction` 的 active todo / trace 内容 - - 用户明确要求继续当前分支的真实目标:修复项目构建时打印的 warning,而不是继续纠结 warning 检查命令本身 + - 用户确认之前的 `0 Warning(s)` 来自增量构建假阴性;只有先 `dotnet clean` 再 `dotnet build`,warning 才会重新出现 + - 用户给出 clean solution build 的真实结果:`Build succeeded with 1193 warning(s)` - 主线程实施: - - 直接在仓库根目录执行 plain `dotnet build` - - 构建结果为 `Build succeeded.`、`0 Warning(s)`、`0 Error(s)`、`Time Elapsed 00:00:14.97` - - 更新 `AGENTS.md`,明确 plain `dotnet build` 是当前仓库默认的 build-warning 检查入口 - - 将 RP-048 之前 active 文档中关于旧 baseline、batch 停点与构建参数形态的细节移入新的 archive 文件 - - 重写 active todo / trace,只保留当前恢复点需要的真值 + - 纠正当前 topic 的 active todo / trace,把 clean build 作为新的 warning 检查真值 + - 在 `BindNodeSignalGenerator.cs`、`GetNodeGenerator.cs`、`GodotProjectMetadataGenerator.cs` 中完成分阶段方法抽取与字符串比较修正 + - 在 `Registration/AutoRegisterExportedCollectionsGenerator.cs` 中拆分 `TryCreateRegistration`,清除最后一个 `MA0051` + - 更新 `AGENTS.md`,明确 warning 检查必须先 `dotnet clean` 再 `dotnet build` +- 验证里程碑: + - `dotnet clean GFramework.Godot.SourceGenerators/GFramework.Godot.SourceGenerators.csproj -c Release` + - 结果:成功;`0 Warning(s)`、`0 Error(s)` + - `dotnet build GFramework.Godot.SourceGenerators/GFramework.Godot.SourceGenerators.csproj -c Release` + - 首次验证:成功;`1 Warning(s)`,剩余 `Registration/AutoRegisterExportedCollectionsGenerator.cs(182,25)` `MA0051` + - 修复后复验:成功;`0 Warning(s)`、`0 Error(s)` - 当前结论: - - 当前分支在默认 solution 构建入口下没有打印 warning,因此此刻没有新的 warning-fix 代码切片可继续实施 - - 当前分支目标没有改变:后续只要 plain `dotnet build` 再次打印 warning,就以该输出为唯一切片来源继续修复 + - `GFramework.Godot.SourceGenerators` 已在 clean `Release` build 下从 9 个 warning 降到 0 个 warning + - 整仓库 warning 基线仍以用户确认的 clean solution build `1193 warning(s)` 为准 + - 下一轮应继续从 clean solution build 输出中选择新的低风险热点 ## Archive Context From 7e45197698705e184e4c3348d4986d5129de6aaf Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Fri, 24 Apr 2026 14:06:41 +0800 Subject: [PATCH 8/9] =?UTF-8?q?test(godot-source-generators):=20=E6=B8=85?= =?UTF-8?q?=E7=90=86=E6=BA=90=E7=94=9F=E6=88=90=E5=99=A8=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E8=AD=A6=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构 GFramework.Godot.SourceGenerators.Tests 的测试模板与诊断辅助,清除项目内全部 analyzer warning - 更新 GeneratorTest 异步等待与 analyzer-warning-reduction 跟踪文档,记录批次验证结果与恢复点 --- .../Behavior/AutoSceneGeneratorTests.cs | 246 +++--- .../Behavior/AutoUiPageGeneratorTests.cs | 318 ++++---- .../BindNodeSignalGeneratorTests.cs | 720 ++++++------------ .../Core/GeneratorTest.cs | 4 +- .../GetNode/GetNodeGeneratorTests.cs | 301 ++++---- .../GodotProjectMetadataGeneratorTests.cs | 254 +++--- ...gisterExportedCollectionsGeneratorTests.cs | 511 ++++++------- .../analyzer-warning-reduction-tracking.md | 42 +- .../analyzer-warning-reduction-trace.md | 31 + 9 files changed, 1052 insertions(+), 1375 deletions(-) diff --git a/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs index 2c6dc972..a4bb59cf 100644 --- a/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoSceneGeneratorTests.cs @@ -6,57 +6,61 @@ namespace GFramework.Godot.SourceGenerators.Tests.Behavior; [TestFixture] public class AutoSceneGeneratorTests { + private const string AutoSceneAttributeWithKeyDeclaration = """ + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + public sealed class AutoSceneAttribute : Attribute + { + public AutoSceneAttribute(string key) { } + } + """; + + private const string AutoSceneAttributeWithoutKeyDeclaration = """ + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + public sealed class AutoSceneAttribute : Attribute + { + public AutoSceneAttribute() { } + } + """; + + private const string NodeTypes = """ + public class Node { } + public class Node2D : Node { } + """; + + private const string SceneBehaviorInfrastructure = """ + namespace GFramework.Game.Abstractions.Scene + { + public interface ISceneBehavior { } + } + + namespace GFramework.Godot.Scene + { + using GFramework.Game.Abstractions.Scene; + using Godot; + + public static class SceneBehaviorFactory + { + public static ISceneBehavior Create(T owner, string key) + where T : Node + { + return null!; + } + } + } + """; + [Test] public async Task Generates_Scene_Behavior_Boilerplate() { - const string source = """ - using System; - using GFramework.Godot.SourceGenerators.Abstractions.UI; - using Godot; - - namespace GFramework.Godot.SourceGenerators.Abstractions.UI - { - [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - public sealed class AutoSceneAttribute : Attribute - { - public AutoSceneAttribute(string key) { } - } - } - - namespace Godot - { - public class Node { } - public class Node2D : Node { } - } - - namespace GFramework.Game.Abstractions.Scene - { - public interface ISceneBehavior { } - } - - namespace GFramework.Godot.Scene - { - using GFramework.Game.Abstractions.Scene; - using Godot; - - public static class SceneBehaviorFactory - { - public static ISceneBehavior Create(T owner, string key) - where T : Node - { - return null!; - } - } - } - - namespace TestApp - { - [AutoScene("Gameplay")] - public partial class GameplayRoot : Node2D - { - } - } - """; + string source = CreateAutoSceneSource( + AutoSceneAttributeWithKeyDeclaration, + """ + [AutoScene("Gameplay")] + public partial class GameplayRoot : Node2D + { + } + """, + includeBehaviorInfrastructure: true); const string expected = """ // @@ -80,40 +84,20 @@ public class AutoSceneGeneratorTests await GeneratorTest.RunAsync( source, - ("TestApp_GameplayRoot.AutoScene.g.cs", expected)); + ("TestApp_GameplayRoot.AutoScene.g.cs", expected)).ConfigureAwait(false); } [Test] public async Task Reports_Diagnostic_When_AutoScene_Arguments_Are_Invalid() { - const string source = """ - using System; - using GFramework.Godot.SourceGenerators.Abstractions.UI; - using Godot; - - namespace GFramework.Godot.SourceGenerators.Abstractions.UI - { - [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - public sealed class AutoSceneAttribute : Attribute - { - public AutoSceneAttribute() { } - } - } - - namespace Godot - { - public class Node { } - public class Node2D : Node { } - } - - namespace TestApp - { - [{|#0:AutoScene|}] - public partial class GameplayRoot : Node2D - { - } - } - """; + string source = CreateAutoSceneSource( + AutoSceneAttributeWithoutKeyDeclaration, + """ + [{|#0:AutoScene|}] + public partial class GameplayRoot : Node2D + { + } + """); var test = new CSharpSourceGeneratorTest { @@ -128,65 +112,26 @@ public class AutoSceneGeneratorTests .WithLocation(0) .WithArguments("AutoSceneAttribute", "GameplayRoot", "a single string scene key argument")); - await test.RunAsync(); + await test.RunAsync().ConfigureAwait(false); } [Test] public async Task Generates_Type_Constraints_For_Nullable_Reference_NotNull_And_Unmanaged_Parameters() { - const string source = """ - #nullable enable - using System; - using GFramework.Godot.SourceGenerators.Abstractions.UI; - using Godot; - - namespace GFramework.Godot.SourceGenerators.Abstractions.UI - { - [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - public sealed class AutoSceneAttribute : Attribute - { - public AutoSceneAttribute(string key) { } - } - } - - namespace Godot - { - public class Node { } - public class Node2D : Node { } - } - - namespace GFramework.Game.Abstractions.Scene - { - public interface ISceneBehavior { } - } - - namespace GFramework.Godot.Scene - { - using GFramework.Game.Abstractions.Scene; - using Godot; - - public static class SceneBehaviorFactory - { - public static ISceneBehavior Create(T owner, string key) - where T : Node - { - return null!; - } - } - } - - namespace TestApp - { - [AutoScene("Gameplay")] - public partial class GameplayRoot : Node2D - where TReference : class? - where TNotNull : notnull - where TValue : struct - where TUnmanaged : unmanaged - { - } - } - """; + string source = CreateAutoSceneSource( + AutoSceneAttributeWithKeyDeclaration, + """ + [AutoScene("Gameplay")] + public partial class GameplayRoot : Node2D + where TReference : class? + where TNotNull : notnull + where TValue : struct + where TUnmanaged : unmanaged + { + } + """, + includeBehaviorInfrastructure: true, + nullableEnabled: true); const string expected = """ // @@ -214,7 +159,7 @@ public class AutoSceneGeneratorTests await GeneratorTest.RunAsync( source, - ("TestApp_GameplayRoot.AutoScene.g.cs", expected)); + ("TestApp_GameplayRoot.AutoScene.g.cs", expected)).ConfigureAwait(false); } /// @@ -267,7 +212,7 @@ public class AutoSceneGeneratorTests .WithLocation(0) .WithArguments("GameplayRoot", "SceneKeyStr")); - await test.RunAsync(); + await test.RunAsync().ConfigureAwait(false); } /// @@ -326,6 +271,39 @@ public class AutoSceneGeneratorTests .WithLocation(0) .WithArguments("GameplayRoot", "__autoSceneBehavior_Generated")); - await test.RunAsync(); + await test.RunAsync().ConfigureAwait(false); + } + + private static string CreateAutoSceneSource( + string attributeDeclaration, + string testAppSource, + bool includeBehaviorInfrastructure = false, + bool nullableEnabled = false) + { + string nullableDirective = nullableEnabled ? "#nullable enable\n" : string.Empty; + string infrastructure = includeBehaviorInfrastructure + ? $"{Environment.NewLine}{Environment.NewLine}{SceneBehaviorInfrastructure}" + : string.Empty; + + return $$""" + {{nullableDirective}}using System; + using GFramework.Godot.SourceGenerators.Abstractions.UI; + using Godot; + + namespace GFramework.Godot.SourceGenerators.Abstractions.UI + { + {{attributeDeclaration}} + } + + namespace Godot + { + {{NodeTypes}} + }{{infrastructure}} + + namespace TestApp + { + {{testAppSource}} + } + """; } } diff --git a/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoUiPageGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoUiPageGeneratorTests.cs index 2b8aca67..5d667309 100644 --- a/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoUiPageGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/Behavior/AutoUiPageGeneratorTests.cs @@ -6,69 +6,85 @@ namespace GFramework.Godot.SourceGenerators.Tests.Behavior; [TestFixture] public class AutoUiPageGeneratorTests { + private const string AutoUiPageAttributeWithLayerDeclaration = """ + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + public sealed class AutoUiPageAttribute : Attribute + { + public AutoUiPageAttribute(string key, string layerName) { } + } + """; + + private const string AutoUiPageAttributeWithoutLayerDeclaration = """ + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + public sealed class AutoUiPageAttribute : Attribute + { + public AutoUiPageAttribute(string key) { } + } + """; + + private const string CanvasNodeTypes = """ + public class Node { } + public class CanvasItem : Node { } + public class Control : CanvasItem { } + """; + + private const string UiLayerFullEnum = """ + namespace GFramework.Game.Abstractions.Enums + { + public enum UiLayer + { + Page, + Overlay, + Modal + } + } + """; + + private const string UiLayerPageOnlyEnum = """ + namespace GFramework.Game.Abstractions.Enums + { + public enum UiLayer + { + Page + } + } + """; + + private const string UiBehaviorInfrastructure = """ + namespace GFramework.Game.Abstractions.UI + { + public interface IUiPageBehavior { } + } + + namespace GFramework.Godot.UI + { + using GFramework.Game.Abstractions.Enums; + using GFramework.Game.Abstractions.UI; + using Godot; + + public static class UiPageBehaviorFactory + { + public static IUiPageBehavior Create(T owner, string key, UiLayer layer) + where T : CanvasItem + { + return null!; + } + } + } + """; + [Test] public async Task Generates_Ui_Page_Behavior_Boilerplate() { - const string source = """ - using System; - using GFramework.Godot.SourceGenerators.Abstractions.UI; - using Godot; - - namespace GFramework.Godot.SourceGenerators.Abstractions.UI - { - [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - public sealed class AutoUiPageAttribute : Attribute - { - public AutoUiPageAttribute(string key, string layerName) { } - } - } - - namespace Godot - { - public class Node { } - public class CanvasItem : Node { } - public class Control : CanvasItem { } - } - - namespace GFramework.Game.Abstractions.Enums - { - public enum UiLayer - { - Page, - Overlay, - Modal - } - } - - namespace GFramework.Game.Abstractions.UI - { - public interface IUiPageBehavior { } - } - - namespace GFramework.Godot.UI - { - using GFramework.Game.Abstractions.Enums; - using GFramework.Game.Abstractions.UI; - using Godot; - - public static class UiPageBehaviorFactory - { - public static IUiPageBehavior Create(T owner, string key, UiLayer layer) - where T : CanvasItem - { - return null!; - } - } - } - - namespace TestApp - { - [AutoUiPage("MainMenu", "Page")] - public partial class MainMenu : Control - { - } - } - """; + string source = CreateAutoUiPageSource( + AutoUiPageAttributeWithLayerDeclaration, + UiLayerFullEnum, + """ + [AutoUiPage("MainMenu", "Page")] + public partial class MainMenu : Control + { + } + """); const string expected = """ // @@ -92,70 +108,21 @@ public class AutoUiPageGeneratorTests await GeneratorTest.RunAsync( source, - ("TestApp_MainMenu.AutoUiPage.g.cs", expected)); + ("TestApp_MainMenu.AutoUiPage.g.cs", expected)).ConfigureAwait(false); } [Test] public async Task Reports_Diagnostic_When_AutoUiPage_Attribute_Arguments_Are_Invalid() { - const string source = """ - using System; - using GFramework.Godot.SourceGenerators.Abstractions.UI; - using Godot; - - namespace GFramework.Godot.SourceGenerators.Abstractions.UI - { - [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - public sealed class AutoUiPageAttribute : Attribute - { - public AutoUiPageAttribute(string key) { } - } - } - - namespace Godot - { - public class Node { } - public class CanvasItem : Node { } - public class Control : CanvasItem { } - } - - namespace GFramework.Game.Abstractions.Enums - { - public enum UiLayer - { - Page - } - } - - namespace GFramework.Game.Abstractions.UI - { - public interface IUiPageBehavior { } - } - - namespace GFramework.Godot.UI - { - using GFramework.Game.Abstractions.Enums; - using GFramework.Game.Abstractions.UI; - using Godot; - - public static class UiPageBehaviorFactory - { - public static IUiPageBehavior Create(T owner, string key, UiLayer layer) - where T : CanvasItem - { - return null!; - } - } - } - - namespace TestApp - { - [{|#0:AutoUiPage("MainMenu")|}] - public partial class MainMenu : Control - { - } - } - """; + string source = CreateAutoUiPageSource( + AutoUiPageAttributeWithoutLayerDeclaration, + UiLayerPageOnlyEnum, + """ + [{|#0:AutoUiPage("MainMenu")|}] + public partial class MainMenu : Control + { + } + """); var test = new CSharpSourceGeneratorTest { @@ -174,74 +141,25 @@ public class AutoUiPageGeneratorTests "MainMenu", "a string key argument and a string UiLayer name argument")); - await test.RunAsync(); + await test.RunAsync().ConfigureAwait(false); } [Test] public async Task Generates_Type_Constraints_For_ClassNullable_NotNull_And_Unmanaged() { - const string source = """ - #nullable enable - using System; - using GFramework.Godot.SourceGenerators.Abstractions.UI; - using Godot; - - namespace GFramework.Godot.SourceGenerators.Abstractions.UI - { - [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - public sealed class AutoUiPageAttribute : Attribute - { - public AutoUiPageAttribute(string key, string layerName) { } - } - } - - namespace Godot - { - public class Node { } - public class CanvasItem : Node { } - public class Control : CanvasItem { } - } - - namespace GFramework.Game.Abstractions.Enums - { - public enum UiLayer - { - Page - } - } - - namespace GFramework.Game.Abstractions.UI - { - public interface IUiPageBehavior { } - } - - namespace GFramework.Godot.UI - { - using GFramework.Game.Abstractions.Enums; - using GFramework.Game.Abstractions.UI; - using Godot; - - public static class UiPageBehaviorFactory - { - public static IUiPageBehavior Create(T owner, string key, UiLayer layer) - where T : CanvasItem - { - return null!; - } - } - } - - namespace TestApp - { - [AutoUiPage("MainMenu", "Page")] - public partial class MainMenu : Control - where TReference : class? - where TNotNull : notnull - where TUnmanaged : unmanaged - { - } - } - """; + string source = CreateAutoUiPageSource( + AutoUiPageAttributeWithLayerDeclaration, + UiLayerPageOnlyEnum, + """ + [AutoUiPage("MainMenu", "Page")] + public partial class MainMenu : Control + where TReference : class? + where TNotNull : notnull + where TUnmanaged : unmanaged + { + } + """, + nullableEnabled: true); const string expected = """ // @@ -268,6 +186,40 @@ public class AutoUiPageGeneratorTests await GeneratorTest.RunAsync( source, - ("TestApp_MainMenu.AutoUiPage.g.cs", expected)); + ("TestApp_MainMenu.AutoUiPage.g.cs", expected)).ConfigureAwait(false); + } + + private static string CreateAutoUiPageSource( + string attributeDeclaration, + string uiLayerDeclaration, + string testAppSource, + bool nullableEnabled = false) + { + string nullableDirective = nullableEnabled ? "#nullable enable\n" : string.Empty; + + return $$""" + {{nullableDirective}}using System; + using GFramework.Godot.SourceGenerators.Abstractions.UI; + using Godot; + + namespace GFramework.Godot.SourceGenerators.Abstractions.UI + { + {{attributeDeclaration}} + } + + namespace Godot + { + {{CanvasNodeTypes}} + } + + {{uiLayerDeclaration}} + + {{UiBehaviorInfrastructure}} + + namespace TestApp + { + {{testAppSource}} + } + """; } } diff --git a/GFramework.Godot.SourceGenerators.Tests/BindNodeSignal/BindNodeSignalGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/BindNodeSignal/BindNodeSignalGeneratorTests.cs index 7f71f210..661023f7 100644 --- a/GFramework.Godot.SourceGenerators.Tests/BindNodeSignal/BindNodeSignalGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/BindNodeSignal/BindNodeSignalGeneratorTests.cs @@ -8,93 +8,103 @@ namespace GFramework.Godot.SourceGenerators.Tests.BindNodeSignal; [TestFixture] public class BindNodeSignalGeneratorTests { + private const string BindNodeSignalAttributeDeclaration = """ + [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + public sealed class BindNodeSignalAttribute : Attribute + { + public BindNodeSignalAttribute(string nodeFieldName, string signalName) + { + NodeFieldName = nodeFieldName; + SignalName = signalName; + } + + public string NodeFieldName { get; } + + public string SignalName { get; } + } + """; + + private const string GetNodeAttributeDeclaration = """ + [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] + public sealed class GetNodeAttribute : Attribute + { + } + """; + + private const string EmptyNodeType = """ + public class Node + { + } + """; + + private const string LifecycleNodeType = """ + public class Node + { + public virtual void _Ready() {} + + public virtual void _ExitTree() {} + } + """; + + private const string ButtonType = """ + public class Button : Node + { + public event Action? Pressed + { + add {} + remove {} + } + } + """; + + private const string SpinBoxType = """ + public class SpinBox : Node + { + public delegate void ValueChangedEventHandler(double value); + + public event ValueChangedEventHandler? ValueChanged + { + add {} + remove {} + } + } + """; + /// /// 验证生成器会为已有生命周期调用生成成对的绑定与解绑方法。 /// [Test] public async Task Generates_Bind_And_Unbind_Methods_For_Existing_Lifecycle_Hooks() { - const string source = """ - using System; - using GFramework.Godot.SourceGenerators.Abstractions; - using Godot; + string source = CreateHudSource( + CreateAbstractionsSource(BindNodeSignalAttributeDeclaration), + """ + private Button _startButton = null!; + private SpinBox _startOreSpinBox = null!; - namespace GFramework.Godot.SourceGenerators.Abstractions - { - [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - public sealed class BindNodeSignalAttribute : Attribute - { - public BindNodeSignalAttribute(string nodeFieldName, string signalName) - { - NodeFieldName = nodeFieldName; - SignalName = signalName; - } + [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] + private void OnStartButtonPressed() + { + } - public string NodeFieldName { get; } + [BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))] + private void OnStartOreValueChanged(double value) + { + } - public string SignalName { get; } - } - } + public override void _Ready() + { + __BindNodeSignals_Generated(); + } - namespace Godot - { - public class Node - { - public virtual void _Ready() {} - - public virtual void _ExitTree() {} - } - - public class Button : Node - { - public event Action? Pressed - { - add {} - remove {} - } - } - - public class SpinBox : Node - { - public delegate void ValueChangedEventHandler(double value); - - public event ValueChangedEventHandler? ValueChanged - { - add {} - remove {} - } - } - } - - namespace TestApp - { - public partial class Hud : Node - { - private Button _startButton = null!; - private SpinBox _startOreSpinBox = null!; - - [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] - private void OnStartButtonPressed() - { - } - - [BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))] - private void OnStartOreValueChanged(double value) - { - } - - public override void _Ready() - { - __BindNodeSignals_Generated(); - } - - public override void _ExitTree() - { - __UnbindNodeSignals_Generated(); - } - } - } - """; + public override void _ExitTree() + { + __UnbindNodeSignals_Generated(); + } + """, + LifecycleNodeType, + ButtonType, + SpinBoxType); const string expected = """ // @@ -121,7 +131,7 @@ public class BindNodeSignalGeneratorTests await GeneratorTest.RunAsync( source, - ("TestApp_Hud.BindNodeSignal.g.cs", expected)); + ("TestApp_Hud.BindNodeSignal.g.cs", expected)).ConfigureAwait(false); } /// @@ -130,70 +140,23 @@ public class BindNodeSignalGeneratorTests [Test] public async Task Generates_Multiple_Subscriptions_For_The_Same_Handler_And_Coexists_With_GetNode() { - const string source = """ - using System; - using GFramework.Godot.SourceGenerators.Abstractions; - using Godot; + string source = CreateHudSource( + CreateAbstractionsSource(BindNodeSignalAttributeDeclaration, GetNodeAttributeDeclaration), + """ + [GetNode] + private Button _startButton = null!; - namespace GFramework.Godot.SourceGenerators.Abstractions - { - [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - public sealed class BindNodeSignalAttribute : Attribute - { - public BindNodeSignalAttribute(string nodeFieldName, string signalName) - { - NodeFieldName = nodeFieldName; - SignalName = signalName; - } + [GetNode] + private Button _cancelButton = null!; - public string NodeFieldName { get; } - - public string SignalName { get; } - } - - [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] - public sealed class GetNodeAttribute : Attribute - { - } - } - - namespace Godot - { - public class Node - { - public virtual void _Ready() {} - - public virtual void _ExitTree() {} - } - - public class Button : Node - { - public event Action? Pressed - { - add {} - remove {} - } - } - } - - namespace TestApp - { - public partial class Hud : Node - { - [GetNode] - private Button _startButton = null!; - - [GetNode] - private Button _cancelButton = null!; - - [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] - [BindNodeSignal(nameof(_cancelButton), nameof(Button.Pressed))] - private void OnAnyButtonPressed() - { - } - } - } - """; + [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] + [BindNodeSignal(nameof(_cancelButton), nameof(Button.Pressed))] + private void OnAnyButtonPressed() + { + } + """, + LifecycleNodeType, + ButtonType); const string expected = """ // @@ -220,7 +183,7 @@ public class BindNodeSignalGeneratorTests await GeneratorTest.RunAsync( source, - ("TestApp_Hud.BindNodeSignal.g.cs", expected)); + ("TestApp_Hud.BindNodeSignal.g.cs", expected)).ConfigureAwait(false); } /// @@ -229,73 +192,24 @@ public class BindNodeSignalGeneratorTests [Test] public async Task Reports_Diagnostic_When_Signal_Does_Not_Exist() { - const string source = """ - using System; - using GFramework.Godot.SourceGenerators.Abstractions; - using Godot; + string source = CreateHudSource( + CreateAbstractionsSource(BindNodeSignalAttributeDeclaration), + """ + private Button _startButton = null!; - namespace GFramework.Godot.SourceGenerators.Abstractions - { - [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - public sealed class BindNodeSignalAttribute : Attribute - { - public BindNodeSignalAttribute(string nodeFieldName, string signalName) - { - NodeFieldName = nodeFieldName; - SignalName = signalName; - } + [{|#0:BindNodeSignal(nameof(_startButton), "Released")|}] + private void OnStartButtonPressed() + { + } + """, + EmptyNodeType, + ButtonType); - public string NodeFieldName { get; } - - public string SignalName { get; } - } - } - - namespace Godot - { - public class Node - { - } - - public class Button : Node - { - public event Action? Pressed - { - add {} - remove {} - } - } - } - - namespace TestApp - { - public partial class Hud : Node - { - private Button _startButton = null!; - - [{|#0:BindNodeSignal(nameof(_startButton), "Released")|}] - private void OnStartButtonPressed() - { - } - } - } - """; - - var test = new CSharpSourceGeneratorTest - { - TestState = - { - Sources = { source } - }, - DisabledDiagnostics = { "GF_Common_Trace_001" }, - TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck - }; - - test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_006", DiagnosticSeverity.Error) - .WithLocation(0) - .WithArguments("_startButton", "Released")); - - await test.RunAsync(); + await VerifyDiagnosticsAsync( + source, + new DiagnosticResult("GF_Godot_BindNodeSignal_006", DiagnosticSeverity.Error) + .WithLocation(0) + .WithArguments("_startButton", "Released")).ConfigureAwait(false); } /// @@ -304,75 +218,24 @@ public class BindNodeSignalGeneratorTests [Test] public async Task Reports_Diagnostic_When_Method_Signature_Does_Not_Match_Event() { - const string source = """ - using System; - using GFramework.Godot.SourceGenerators.Abstractions; - using Godot; + string source = CreateHudSource( + CreateAbstractionsSource(BindNodeSignalAttributeDeclaration), + """ + private SpinBox _startOreSpinBox = null!; - namespace GFramework.Godot.SourceGenerators.Abstractions - { - [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - public sealed class BindNodeSignalAttribute : Attribute - { - public BindNodeSignalAttribute(string nodeFieldName, string signalName) - { - NodeFieldName = nodeFieldName; - SignalName = signalName; - } + [{|#0:BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))|}] + private void OnStartOreValueChanged() + { + } + """, + EmptyNodeType, + SpinBoxType); - public string NodeFieldName { get; } - - public string SignalName { get; } - } - } - - namespace Godot - { - public class Node - { - } - - public class SpinBox : Node - { - public delegate void ValueChangedEventHandler(double value); - - public event ValueChangedEventHandler? ValueChanged - { - add {} - remove {} - } - } - } - - namespace TestApp - { - public partial class Hud : Node - { - private SpinBox _startOreSpinBox = null!; - - [{|#0:BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))|}] - private void OnStartOreValueChanged() - { - } - } - } - """; - - var test = new CSharpSourceGeneratorTest - { - TestState = - { - Sources = { source } - }, - DisabledDiagnostics = { "GF_Common_Trace_001" }, - TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck - }; - - test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_007", DiagnosticSeverity.Error) - .WithLocation(0) - .WithArguments("OnStartOreValueChanged", "ValueChanged", "_startOreSpinBox")); - - await test.RunAsync(); + await VerifyDiagnosticsAsync( + source, + new DiagnosticResult("GF_Godot_BindNodeSignal_007", DiagnosticSeverity.Error) + .WithLocation(0) + .WithArguments("OnStartOreValueChanged", "ValueChanged", "_startOreSpinBox")).ConfigureAwait(false); } /// @@ -381,73 +244,24 @@ public class BindNodeSignalGeneratorTests [Test] public async Task Reports_Diagnostic_When_Constructor_Argument_Is_Empty() { - const string source = """ - using System; - using GFramework.Godot.SourceGenerators.Abstractions; - using Godot; + string source = CreateHudSource( + CreateAbstractionsSource(BindNodeSignalAttributeDeclaration), + """ + private Button _startButton = null!; - namespace GFramework.Godot.SourceGenerators.Abstractions - { - [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - public sealed class BindNodeSignalAttribute : Attribute - { - public BindNodeSignalAttribute(string nodeFieldName, string signalName) - { - NodeFieldName = nodeFieldName; - SignalName = signalName; - } + [{|#0:BindNodeSignal(nameof(_startButton), "")|}] + private void OnStartButtonPressed() + { + } + """, + EmptyNodeType, + ButtonType); - public string NodeFieldName { get; } - - public string SignalName { get; } - } - } - - namespace Godot - { - public class Node - { - } - - public class Button : Node - { - public event Action? Pressed - { - add {} - remove {} - } - } - } - - namespace TestApp - { - public partial class Hud : Node - { - private Button _startButton = null!; - - [{|#0:BindNodeSignal(nameof(_startButton), "")|}] - private void OnStartButtonPressed() - { - } - } - } - """; - - var test = new CSharpSourceGeneratorTest - { - TestState = - { - Sources = { source } - }, - DisabledDiagnostics = { "GF_Common_Trace_001" }, - TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck - }; - - test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_010", DiagnosticSeverity.Error) - .WithLocation(0) - .WithArguments("OnStartButtonPressed", "signalName")); - - await test.RunAsync(); + await VerifyDiagnosticsAsync( + source, + new DiagnosticResult("GF_Godot_BindNodeSignal_010", DiagnosticSeverity.Error) + .WithLocation(0) + .WithArguments("OnStartButtonPressed", "signalName")).ConfigureAwait(false); } /// @@ -456,85 +270,35 @@ public class BindNodeSignalGeneratorTests [Test] public async Task Reports_Diagnostic_When_Generated_Method_Names_Already_Exist() { - const string source = """ - using System; - using GFramework.Godot.SourceGenerators.Abstractions; - using Godot; + string source = CreateHudSource( + CreateAbstractionsSource(BindNodeSignalAttributeDeclaration), + """ + private Button _startButton = null!; - namespace GFramework.Godot.SourceGenerators.Abstractions - { - [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - public sealed class BindNodeSignalAttribute : Attribute - { - public BindNodeSignalAttribute(string nodeFieldName, string signalName) - { - NodeFieldName = nodeFieldName; - SignalName = signalName; - } + [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] + private void OnStartButtonPressed() + { + } - public string NodeFieldName { get; } + private void {|#0:__BindNodeSignals_Generated|}() + { + } - public string SignalName { get; } - } - } + private void {|#1:__UnbindNodeSignals_Generated|}() + { + } + """, + EmptyNodeType, + ButtonType); - namespace Godot - { - public class Node - { - } - - public class Button : Node - { - public event Action? Pressed - { - add {} - remove {} - } - } - } - - namespace TestApp - { - public partial class Hud : Node - { - private Button _startButton = null!; - - [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] - private void OnStartButtonPressed() - { - } - - private void {|#0:__BindNodeSignals_Generated|}() - { - } - - private void {|#1:__UnbindNodeSignals_Generated|}() - { - } - } - } - """; - - var test = new CSharpSourceGeneratorTest - { - TestState = - { - Sources = { source } - }, - DisabledDiagnostics = { "GF_Common_Trace_001" }, - TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck - }; - - test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error) - .WithLocation(0) - .WithArguments("Hud", "__BindNodeSignals_Generated")); - - test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error) - .WithLocation(1) - .WithArguments("Hud", "__UnbindNodeSignals_Generated")); - - await test.RunAsync(); + await VerifyDiagnosticsAsync( + source, + new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error) + .WithLocation(0) + .WithArguments("Hud", "__BindNodeSignals_Generated"), + new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error) + .WithLocation(1) + .WithArguments("Hud", "__UnbindNodeSignals_Generated")).ConfigureAwait(false); } /// @@ -543,69 +307,80 @@ public class BindNodeSignalGeneratorTests [Test] public async Task Reports_Warnings_When_Lifecycle_Methods_Do_Not_Call_Generated_Methods() { - const string source = """ - using System; - using GFramework.Godot.SourceGenerators.Abstractions; - using Godot; + string source = CreateHudSource( + CreateAbstractionsSource(BindNodeSignalAttributeDeclaration), + """ + private Button _startButton = null!; - namespace GFramework.Godot.SourceGenerators.Abstractions - { - [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - public sealed class BindNodeSignalAttribute : Attribute - { - public BindNodeSignalAttribute(string nodeFieldName, string signalName) - { - NodeFieldName = nodeFieldName; - SignalName = signalName; - } + [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] + private void OnStartButtonPressed() + { + } - public string NodeFieldName { get; } + public override void {|#0:_Ready|}() + { + } - public string SignalName { get; } - } - } + public override void {|#1:_ExitTree|}() + { + } + """, + LifecycleNodeType, + ButtonType); - namespace Godot - { - public class Node - { - public virtual void _Ready() {} + await VerifyDiagnosticsAsync( + source, + new DiagnosticResult("GF_Godot_BindNodeSignal_008", DiagnosticSeverity.Warning) + .WithLocation(0) + .WithArguments("Hud"), + new DiagnosticResult("GF_Godot_BindNodeSignal_009", DiagnosticSeverity.Warning) + .WithLocation(1) + .WithArguments("Hud")).ConfigureAwait(false); + } - public virtual void _ExitTree() {} - } + private static string CreateAbstractionsSource(params string[] attributeDeclarations) + { + string declarations = string.Join($"{Environment.NewLine}{Environment.NewLine}", attributeDeclarations); - public class Button : Node - { - public event Action? Pressed - { - add {} - remove {} - } - } - } + return $$""" + namespace GFramework.Godot.SourceGenerators.Abstractions + { + {{declarations}} + } + """; + } - namespace TestApp - { - public partial class Hud : Node - { - private Button _startButton = null!; + private static string CreateHudSource( + string abstractionsSource, + string hudMembers, + params string[] godotTypes) + { + string godotSource = string.Join($"{Environment.NewLine}{Environment.NewLine}", godotTypes); - [BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))] - private void OnStartButtonPressed() - { - } + return $$""" + using System; + using GFramework.Godot.SourceGenerators.Abstractions; + using Godot; - public override void {|#0:_Ready|}() - { - } + {{abstractionsSource}} - public override void {|#1:_ExitTree|}() - { - } - } - } - """; + namespace Godot + { + {{godotSource}} + } + namespace TestApp + { + public partial class Hud : Node + { + {{hudMembers}} + } + } + """; + } + + private static Task VerifyDiagnosticsAsync(string source, params DiagnosticResult[] expectedDiagnostics) + { var test = new CSharpSourceGeneratorTest { TestState = @@ -616,14 +391,11 @@ public class BindNodeSignalGeneratorTests TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck }; - test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_008", DiagnosticSeverity.Warning) - .WithLocation(0) - .WithArguments("Hud")); + foreach (DiagnosticResult expectedDiagnostic in expectedDiagnostics) + { + test.ExpectedDiagnostics.Add(expectedDiagnostic); + } - test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_009", DiagnosticSeverity.Warning) - .WithLocation(1) - .WithArguments("Hud")); - - await test.RunAsync(); + return test.RunAsync(); } -} \ No newline at end of file +} diff --git a/GFramework.Godot.SourceGenerators.Tests/Core/GeneratorTest.cs b/GFramework.Godot.SourceGenerators.Tests/Core/GeneratorTest.cs index cca85465..c5cb4608 100644 --- a/GFramework.Godot.SourceGenerators.Tests/Core/GeneratorTest.cs +++ b/GFramework.Godot.SourceGenerators.Tests/Core/GeneratorTest.cs @@ -29,7 +29,7 @@ public static class GeneratorTest test.TestState.GeneratedSources.Add( (typeof(TGenerator), filename, NormalizeLineEndings(content))); - await test.RunAsync(); + await test.RunAsync().ConfigureAwait(false); } /// @@ -44,4 +44,4 @@ public static class GeneratorTest .Replace("\r", "\n", StringComparison.Ordinal) .Replace("\n", Environment.NewLine, StringComparison.Ordinal); } -} \ No newline at end of file +} diff --git a/GFramework.Godot.SourceGenerators.Tests/GetNode/GetNodeGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/GetNode/GetNodeGeneratorTests.cs index 266ff983..d6e7cd5e 100644 --- a/GFramework.Godot.SourceGenerators.Tests/GetNode/GetNodeGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/GetNode/GetNodeGeneratorTests.cs @@ -5,61 +5,88 @@ namespace GFramework.Godot.SourceGenerators.Tests.GetNode; [TestFixture] public class GetNodeGeneratorTests { + private const string FullGetNodeAttributeDeclaration = """ + [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] + public sealed class GetNodeAttribute : Attribute + { + public GetNodeAttribute() {} + public GetNodeAttribute(string path) { Path = path; } + public string? Path { get; set; } + public bool Required { get; set; } = true; + public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto; + } + + public enum NodeLookupMode + { + Auto = 0, + UniqueName = 1, + RelativePath = 2, + AbsolutePath = 3 + } + """; + + private const string MinimalGetNodeAttributeDeclaration = """ + [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] + public sealed class GetNodeAttribute : Attribute + { + public GetNodeAttribute() {} + } + + public enum NodeLookupMode + { + Auto = 0 + } + """; + + private const string PropertyOnlyGetNodeAttributeDeclaration = """ + [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] + public sealed class GetNodeAttribute : Attribute + { + public string? Path { get; set; } + public bool Required { get; set; } = true; + public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto; + } + + public enum NodeLookupMode + { + Auto = 0, + UniqueName = 1, + RelativePath = 2, + AbsolutePath = 3 + } + """; + + private const string NodeWithReadyAndLookupMethods = """ + public class Node + { + public virtual void _Ready() {} + public T GetNode(string path) where T : Node => throw new InvalidOperationException(path); + public T? GetNodeOrNull(string path) where T : Node => default; + } + """; + + private const string HBoxContainerType = """ + public class HBoxContainer : Node + { + } + """; + [Test] public async Task Generates_InferredUniqueNameBindings_And_ReadyHook_WhenReadyIsMissing() { - const string source = """ - using System; - using GFramework.Godot.SourceGenerators.Abstractions; - using Godot; + string source = CreateGetNodeSource( + FullGetNodeAttributeDeclaration, + """ + public partial class TopBar : HBoxContainer + { + [GetNode] + private HBoxContainer _leftContainer = null!; - namespace GFramework.Godot.SourceGenerators.Abstractions - { - [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] - public sealed class GetNodeAttribute : Attribute - { - public GetNodeAttribute() {} - public GetNodeAttribute(string path) { Path = path; } - public string? Path { get; set; } - public bool Required { get; set; } = true; - public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto; - } - - public enum NodeLookupMode - { - Auto = 0, - UniqueName = 1, - RelativePath = 2, - AbsolutePath = 3 - } - } - - namespace Godot - { - public class Node - { - public virtual void _Ready() {} - public T GetNode(string path) where T : Node => throw new InvalidOperationException(path); - public T? GetNodeOrNull(string path) where T : Node => default; - } - - public class HBoxContainer : Node - { - } - } - - namespace TestApp - { - public partial class TopBar : HBoxContainer - { - [GetNode] - private HBoxContainer _leftContainer = null!; - - [GetNode] - private HBoxContainer m_rightContainer = null!; - } - } - """; + [GetNode] + private HBoxContainer m_rightContainer = null!; + } + """, + HBoxContainerType); const string expected = """ // @@ -88,69 +115,30 @@ public class GetNodeGeneratorTests await GeneratorTest.RunAsync( source, - ("TestApp_TopBar.GetNode.g.cs", expected)); + ("TestApp_TopBar.GetNode.g.cs", expected)).ConfigureAwait(false); } [Test] public async Task Generates_ManualInjectionOnly_WhenReadyAlreadyExists() { - const string source = """ - using System; - using GFramework.Godot.SourceGenerators.Abstractions; - using Godot; + string source = CreateGetNodeSource( + FullGetNodeAttributeDeclaration, + """ + public partial class TopBar : HBoxContainer + { + [GetNode("%LeftContainer")] + private HBoxContainer _leftContainer = null!; - namespace GFramework.Godot.SourceGenerators.Abstractions - { - [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] - public sealed class GetNodeAttribute : Attribute - { - public GetNodeAttribute() {} - public GetNodeAttribute(string path) { Path = path; } - public string? Path { get; set; } - public bool Required { get; set; } = true; - public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto; - } + [GetNode(Required = false, Lookup = NodeLookupMode.RelativePath)] + private HBoxContainer? _rightContainer; - public enum NodeLookupMode - { - Auto = 0, - UniqueName = 1, - RelativePath = 2, - AbsolutePath = 3 - } - } - - namespace Godot - { - public class Node - { - public virtual void _Ready() {} - public T GetNode(string path) where T : Node => throw new InvalidOperationException(path); - public T? GetNodeOrNull(string path) where T : Node => default; - } - - public class HBoxContainer : Node - { - } - } - - namespace TestApp - { - public partial class TopBar : HBoxContainer - { - [GetNode("%LeftContainer")] - private HBoxContainer _leftContainer = null!; - - [GetNode(Required = false, Lookup = NodeLookupMode.RelativePath)] - private HBoxContainer? _rightContainer; - - public override void _Ready() - { - __InjectGetNodes_Generated(); - } - } - } - """; + public override void _Ready() + { + __InjectGetNodes_Generated(); + } + } + """, + HBoxContainerType); const string expected = """ // @@ -171,7 +159,7 @@ public class GetNodeGeneratorTests await GeneratorTest.RunAsync( source, - ("TestApp_TopBar.GetNode.g.cs", expected)); + ("TestApp_TopBar.GetNode.g.cs", expected)).ConfigureAwait(false); } [Test] @@ -234,58 +222,26 @@ public class GetNodeGeneratorTests .WithSpan(39, 24, 39, 38) .WithArguments("_leftContainer")); - await test.RunAsync(); + await test.RunAsync().ConfigureAwait(false); } [Test] public async Task Reports_Diagnostic_When_Generated_Injection_Method_Name_Already_Exists() { - const string source = """ - using System; - using GFramework.Godot.SourceGenerators.Abstractions; - using Godot; + string source = CreateGetNodeSource( + MinimalGetNodeAttributeDeclaration, + """ + public partial class TopBar : HBoxContainer + { + [GetNode] + private HBoxContainer _leftContainer = null!; - namespace GFramework.Godot.SourceGenerators.Abstractions - { - [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] - public sealed class GetNodeAttribute : Attribute - { - public GetNodeAttribute() {} - } - - public enum NodeLookupMode - { - Auto = 0 - } - } - - namespace Godot - { - public class Node - { - public virtual void _Ready() {} - public T GetNode(string path) where T : Node => throw new InvalidOperationException(path); - public T? GetNodeOrNull(string path) where T : Node => default; - } - - public class HBoxContainer : Node - { - } - } - - namespace TestApp - { - public partial class TopBar : HBoxContainer - { - [GetNode] - private HBoxContainer _leftContainer = null!; - - private void {|#0:__InjectGetNodes_Generated|}() - { - } - } - } - """; + private void {|#0:__InjectGetNodes_Generated|}() + { + } + } + """, + HBoxContainerType); var test = new CSharpSourceGeneratorTest { @@ -301,6 +257,39 @@ public class GetNodeGeneratorTests .WithLocation(0) .WithArguments("TopBar", "__InjectGetNodes_Generated")); - await test.RunAsync(); + await test.RunAsync().ConfigureAwait(false); } -} \ No newline at end of file + + 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}} + } + """; + } +} diff --git a/GFramework.Godot.SourceGenerators.Tests/Project/GodotProjectMetadataGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Project/GodotProjectMetadataGeneratorTests.cs index b033a865..508273d7 100644 --- a/GFramework.Godot.SourceGenerators.Tests/Project/GodotProjectMetadataGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/Project/GodotProjectMetadataGeneratorTests.cs @@ -8,6 +8,131 @@ namespace GFramework.Godot.SourceGenerators.Tests.Project; [TestFixture] public class GodotProjectMetadataGeneratorTests { + private const string AutoLoadProjectFile = """ + [autoload] + GameServices="*res://autoload/game_services.tscn" + AudioBus="*res://autoload/audio_bus.gd" + """; + + private const string InputActionsProjectFile = """ + [input] + move_up={ + "deadzone": 0.5 + } + ui_cancel={ + "deadzone": 0.5 + } + """; + + private const string ExpectedAutoLoads = """ + // + #nullable enable + + namespace GFramework.Godot.Generated; + + /// + /// 提供 project.godot 中 AutoLoad 单例的强类型访问入口。 + /// + public static partial class AutoLoads + { + /// + /// 获取 AutoLoad GameServices。 + /// + public static global::TestApp.GameServices GameServices => GetRequiredNode("GameServices"); + + /// + /// 尝试获取 AutoLoad GameServices。 + /// + public static bool TryGetGameServices(out global::TestApp.GameServices? value) + { + return TryGetNode("GameServices", out value); + } + + /// + /// 获取 AutoLoad AudioBus。 + /// + public static global::Godot.Node AudioBus => GetRequiredNode("AudioBus"); + + /// + /// 尝试获取 AutoLoad AudioBus。 + /// + public static bool TryGetAudioBus(out global::Godot.Node? value) + { + return TryGetNode("AudioBus", out value); + } + + /// + /// 获取一个必填的 AutoLoad 节点;缺失时抛出异常。 + /// + /// 节点类型。 + /// AutoLoad 名称。 + /// 已解析的 AutoLoad 节点。 + private static TNode GetRequiredNode(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."); + } + + /// + /// 尝试从当前 SceneTree 根节点解析 AutoLoad。 + /// + /// 节点类型。 + /// AutoLoad 名称。 + /// 解析到的节点实例。 + /// 若当前进程存在 SceneTree 且根节点中能解析到该 AutoLoad,则返回 true + private static bool TryGetNode(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($"/root/{autoLoadName}"); + return value is not null; + } + } + + """; + + private const string ExpectedInputActions = """ + // + #nullable enable + + namespace GFramework.Godot.Generated; + + /// + /// 提供 project.godot 中 Input Action 名称的强类型常量。 + /// + public static partial class InputActions + { + /// + /// Input Action move_up 的稳定名称。 + /// + public const string MoveUp = "move_up"; + + /// + /// Input Action ui_cancel 的稳定名称。 + /// + public const string UiCancel = "ui_cancel"; + + } + + """; + /// /// 验证会根据 AutoLoad 与 Input Action 生成稳定的强类型入口。 /// @@ -29,142 +154,19 @@ public class GodotProjectMetadataGeneratorTests """, includeAutoLoadAttribute: true); - const string projectFile = """ - [autoload] - GameServices="*res://autoload/game_services.tscn" - AudioBus="*res://autoload/audio_bus.gd" - - [input] - move_up={ - "deadzone": 0.5 - } - ui_cancel={ - "deadzone": 0.5 - } - """; - - const string expectedAutoLoads = """ - // - #nullable enable - - namespace GFramework.Godot.Generated; - - /// - /// 提供 project.godot 中 AutoLoad 单例的强类型访问入口。 - /// - public static partial class AutoLoads - { - /// - /// 获取 AutoLoad GameServices。 - /// - public static global::TestApp.GameServices GameServices => GetRequiredNode("GameServices"); - - /// - /// 尝试获取 AutoLoad GameServices。 - /// - public static bool TryGetGameServices(out global::TestApp.GameServices? value) - { - return TryGetNode("GameServices", out value); - } - - /// - /// 获取 AutoLoad AudioBus。 - /// - public static global::Godot.Node AudioBus => GetRequiredNode("AudioBus"); - - /// - /// 尝试获取 AutoLoad AudioBus。 - /// - public static bool TryGetAudioBus(out global::Godot.Node? value) - { - return TryGetNode("AudioBus", out value); - } - - /// - /// 获取一个必填的 AutoLoad 节点;缺失时抛出异常。 - /// - /// 节点类型。 - /// AutoLoad 名称。 - /// 已解析的 AutoLoad 节点。 - private static TNode GetRequiredNode(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."); - } - - /// - /// 尝试从当前 SceneTree 根节点解析 AutoLoad。 - /// - /// 节点类型。 - /// AutoLoad 名称。 - /// 解析到的节点实例。 - /// 若当前进程存在 SceneTree 且根节点中能解析到该 AutoLoad,则返回 true - private static bool TryGetNode(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($"/root/{autoLoadName}"); - return value is not null; - } - } - - """; - - const string expectedInputActions = """ - // - #nullable enable - - namespace GFramework.Godot.Generated; - - /// - /// 提供 project.godot 中 Input Action 名称的强类型常量。 - /// - public static partial class InputActions - { - /// - /// Input Action move_up 的稳定名称。 - /// - public const string MoveUp = "move_up"; - - /// - /// Input Action ui_cancel 的稳定名称。 - /// - public const string UiCancel = "ui_cancel"; - - } - - """; - var result = AdditionalTextGeneratorTestDriver.Run( source, - ("project.godot", projectFile)); + ("project.godot", $"{AutoLoadProjectFile}\n\n{InputActionsProjectFile}")); var generatedSources = AdditionalTextGeneratorTestDriver.ToGeneratedSourceMap(result); Assert.That(result.Results.Single().Diagnostics, Is.Empty); Assert.That( generatedSources["GFramework_Godot_Generated_AutoLoads.g.cs"], - Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(expectedAutoLoads))); + Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(ExpectedAutoLoads))); Assert.That( generatedSources["GFramework_Godot_Generated_InputActions.g.cs"], - Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(expectedInputActions))); + Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(ExpectedInputActions))); } /// diff --git a/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs index befdcf59..d57a74c6 100644 --- a/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs @@ -6,48 +6,52 @@ namespace GFramework.Godot.SourceGenerators.Tests.Registration; [TestFixture] public class AutoRegisterExportedCollectionsGeneratorTests { + private const string StandardAttributeDeclarations = """ + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] + public sealed class RegisterExportedCollectionAttribute : Attribute + { + public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { } + } + """; + + private const string MultiDeclarationAttributeDeclarations = """ + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] + public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] + public sealed class RegisterExportedCollectionAttribute : Attribute + { + public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { } + } + """; + [Test] public async Task Generates_Batch_Registration_Method_For_Annotated_Collections() { - const string source = """ - #nullable enable - using System; - using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions.UI; + string source = CreateSource( + """ + public sealed class IntRegistry + { + public void Register(int value) { } + } - namespace GFramework.Godot.SourceGenerators.Abstractions.UI - { - [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } + [AutoRegisterExportedCollections] + public partial class Bootstrapper + where TReference : class? + where TNotNull : notnull + where TValue : struct + where TUnmanaged : unmanaged + { + private readonly IntRegistry? _registry = new(); - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] - public sealed class RegisterExportedCollectionAttribute : Attribute - { - public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { } - } - } - - namespace TestApp - { - public sealed class IntRegistry - { - public void Register(int value) { } - } - - [AutoRegisterExportedCollections] - public partial class Bootstrapper - 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? Values { get; } = new(); - } - } - """; + [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] + public List? Values { get; } = new(); + } + """, + nullableEnabled: true); const string expected = """ // @@ -77,7 +81,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests await GeneratorTest.RunAsync( source, - ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)); + ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false); } [Test] @@ -137,41 +141,23 @@ public class AutoRegisterExportedCollectionsGeneratorTests [Test] public async Task Generates_Batch_Registration_Method_When_Register_Method_Uses_Array_Parameter() { - const string source = """ - #nullable enable - using System; - using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions.UI; + string source = CreateSource( + """ + public sealed class ArrayRegistry + { + public void Register(int[] value) { } + } - namespace GFramework.Godot.SourceGenerators.Abstractions.UI - { - [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } + [AutoRegisterExportedCollections] + public partial class Bootstrapper + { + private readonly ArrayRegistry _registry = new(); - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] - public sealed class RegisterExportedCollectionAttribute : Attribute - { - public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { } - } - } - - namespace TestApp - { - public sealed class ArrayRegistry - { - public void Register(int[] value) { } - } - - [AutoRegisterExportedCollections] - public partial class Bootstrapper - { - private readonly ArrayRegistry _registry = new(); - - [RegisterExportedCollection(nameof(_registry), nameof(ArrayRegistry.Register))] - public List Values { get; } = new(); - } - } - """; + [RegisterExportedCollection(nameof(_registry), nameof(ArrayRegistry.Register))] + public List Values { get; } = new(); + } + """, + nullableEnabled: true); const string expected = """ // @@ -197,59 +183,41 @@ public class AutoRegisterExportedCollectionsGeneratorTests await GeneratorTest.RunAsync( source, - ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)); + ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false); } [Test] public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Inherited_Interface() { - const string source = """ - #nullable enable - using System; - using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions.UI; + string source = CreateSource( + """ + public interface IKeyValue + { + } - namespace GFramework.Godot.SourceGenerators.Abstractions.UI - { - [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } + public interface IRegistry + { + void Registry(IKeyValue mapping); + } - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] - public sealed class RegisterExportedCollectionAttribute : Attribute - { - public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { } - } - } + public interface IAssetRegistry : IRegistry + { + } - namespace TestApp - { - public interface IKeyValue - { - } + public sealed class IntConfig : IKeyValue + { + } - public interface IRegistry - { - void Registry(IKeyValue mapping); - } + [AutoRegisterExportedCollections] + public partial class Bootstrapper + { + private readonly IAssetRegistry? _registry = null; - public interface IAssetRegistry : IRegistry - { - } - - public sealed class IntConfig : IKeyValue - { - } - - [AutoRegisterExportedCollections] - public partial class Bootstrapper - { - private readonly IAssetRegistry? _registry = null; - - [RegisterExportedCollection(nameof(_registry), "Registry")] - public List? Values { get; } = new(); - } - } - """; + [RegisterExportedCollection(nameof(_registry), "Registry")] + public List? Values { get; } = new(); + } + """, + nullableEnabled: true); const string expected = """ // @@ -275,7 +243,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests await GeneratorTest.RunAsync( source, - ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)); + ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false); } [Test] @@ -340,45 +308,27 @@ public class AutoRegisterExportedCollectionsGeneratorTests [Test] public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Base_Class() { - const string source = """ - #nullable enable - using System; - using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions.UI; + string source = CreateSource( + """ + public class BaseRegistry + { + public void Register(int value) { } + } - namespace GFramework.Godot.SourceGenerators.Abstractions.UI - { - [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } + public sealed class DerivedRegistry : BaseRegistry + { + } - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] - public sealed class RegisterExportedCollectionAttribute : Attribute - { - public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { } - } - } + [AutoRegisterExportedCollections] + public partial class Bootstrapper + { + private readonly DerivedRegistry? _registry = new(); - namespace TestApp - { - public class BaseRegistry - { - public void Register(int value) { } - } - - public sealed class DerivedRegistry : BaseRegistry - { - } - - [AutoRegisterExportedCollections] - public partial class Bootstrapper - { - private readonly DerivedRegistry? _registry = new(); - - [RegisterExportedCollection(nameof(_registry), nameof(BaseRegistry.Register))] - public List? Values { get; } = new(); - } - } - """; + [RegisterExportedCollection(nameof(_registry), nameof(BaseRegistry.Register))] + public List? Values { get; } = new(); + } + """, + nullableEnabled: true); const string expected = """ // @@ -404,50 +354,32 @@ public class AutoRegisterExportedCollectionsGeneratorTests await GeneratorTest.RunAsync( source, - ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)); + ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false); } [Test] public async Task Generates_Batch_Registration_Method_When_Registry_Member_Comes_From_Base_Class() { - const string source = """ - #nullable enable - using System; - using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions.UI; + string source = CreateSource( + """ + public sealed class IntRegistry + { + public void Register(int value) { } + } - namespace GFramework.Godot.SourceGenerators.Abstractions.UI - { - [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } + public abstract class BootstrapperBase + { + protected readonly IntRegistry? _registry = new(); + } - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] - public sealed class RegisterExportedCollectionAttribute : Attribute - { - public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { } - } - } - - namespace TestApp - { - public sealed class IntRegistry - { - public void Register(int value) { } - } - - public abstract class BootstrapperBase - { - protected readonly IntRegistry? _registry = new(); - } - - [AutoRegisterExportedCollections] - public partial class Bootstrapper : BootstrapperBase - { - [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] - public List? Values { get; } = new(); - } - } - """; + [AutoRegisterExportedCollections] + public partial class Bootstrapper : BootstrapperBase + { + [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] + public List? Values { get; } = new(); + } + """, + nullableEnabled: true); const string expected = """ // @@ -473,74 +405,47 @@ public class AutoRegisterExportedCollectionsGeneratorTests await GeneratorTest.RunAsync( source, - ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)); + ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false); } [Test] public async Task Reports_Diagnostic_When_Collection_Member_Is_Not_Instance_Readable() { - const string source = """ - using System; - using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions.UI; + string source = CreateSource( + """ + public sealed class IntRegistry + { + public void Register(int value) { } + } - namespace GFramework.Godot.SourceGenerators.Abstractions.UI - { - [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } + [AutoRegisterExportedCollections] + public partial class Bootstrapper + { + private readonly IntRegistry _registry = new(); - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] - public sealed class RegisterExportedCollectionAttribute : Attribute - { - public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { } - } - } + [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] + public static List {|#0:StaticValues|} = new(); - namespace TestApp - { - public sealed class IntRegistry - { - public void Register(int value) { } - } + [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] + public static List {|#1:StaticPropertyValues|} { get; } = new(); - [AutoRegisterExportedCollections] - public partial class Bootstrapper - { - private readonly IntRegistry _registry = new(); + [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] + public List {|#2:WriteOnlyValues|} { set { } } + } + """); - [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] - public static List {|#0:StaticValues|} = new(); - - [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] - public static List {|#1:StaticPropertyValues|} { get; } = new(); - - [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] - public List {|#2:WriteOnlyValues|} { set { } } - } - } - """; - - var test = new CSharpSourceGeneratorTest - { - TestState = - { - Sources = { source } - }, - DisabledDiagnostics = { "GF_Common_Trace_001" }, - TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck - }; - - test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error) - .WithLocation(0) - .WithArguments("StaticValues")); - test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error) - .WithLocation(1) - .WithArguments("StaticPropertyValues")); - test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error) - .WithLocation(2) - .WithArguments("WriteOnlyValues")); - - await test.RunAsync(); + await VerifyDiagnosticsAsync( + source, + skipGeneratedSourcesCheck: true, + new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error) + .WithLocation(0) + .WithArguments("StaticValues"), + new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error) + .WithLocation(1) + .WithArguments("StaticPropertyValues"), + new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error) + .WithLocation(2) + .WithArguments("WriteOnlyValues")).ConfigureAwait(false); } [Test] @@ -711,45 +616,28 @@ public class AutoRegisterExportedCollectionsGeneratorTests [Test] public async Task Generates_Only_One_Source_When_Multiple_Partial_Declarations_Are_Annotated() { - const string source = """ - #nullable enable - using System; - using System.Collections.Generic; - using GFramework.Godot.SourceGenerators.Abstractions.UI; + string source = CreateSource( + """ + public sealed class IntRegistry + { + public void Register(int value) { } + } - namespace GFramework.Godot.SourceGenerators.Abstractions.UI - { - [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] - public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { } + [AutoRegisterExportedCollections] + public partial class Bootstrapper + { + private readonly IntRegistry? _registry = new(); + } - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] - public sealed class RegisterExportedCollectionAttribute : Attribute - { - public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { } - } - } - - namespace TestApp - { - public sealed class IntRegistry - { - public void Register(int value) { } - } - - [AutoRegisterExportedCollections] - public partial class Bootstrapper - { - private readonly IntRegistry? _registry = new(); - } - - [AutoRegisterExportedCollections] - public partial class Bootstrapper - { - [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] - public List? Values { get; } = new(); - } - } - """; + [AutoRegisterExportedCollections] + public partial class Bootstrapper + { + [RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))] + public List? Values { get; } = new(); + } + """, + nullableEnabled: true, + allowMultipleDeclarations: true); const string expected = """ // @@ -775,6 +663,61 @@ public class AutoRegisterExportedCollectionsGeneratorTests await GeneratorTest.RunAsync( source, - ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)); + ("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false); + } + + private static string CreateSource( + string applicationSource, + bool nullableEnabled = false, + bool allowMultipleDeclarations = false) + { + string nullableDirective = nullableEnabled ? "#nullable enable\n" : string.Empty; + string attributeDeclarations = allowMultipleDeclarations + ? MultiDeclarationAttributeDeclarations + : StandardAttributeDeclarations; + + return $$""" + {{nullableDirective}}using System; + using System.Collections; + using System.Collections.Generic; + using GFramework.Godot.SourceGenerators.Abstractions.UI; + + namespace GFramework.Godot.SourceGenerators.Abstractions.UI + { + {{attributeDeclarations}} + } + + namespace TestApp + { + {{applicationSource}} + } + """; + } + + private static Task VerifyDiagnosticsAsync( + string source, + bool skipGeneratedSourcesCheck = false, + params DiagnosticResult[] expectedDiagnostics) + { + var test = new CSharpSourceGeneratorTest + { + 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(); } } diff --git a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md index dbc281af..1ee133e9 100644 --- a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md +++ b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md @@ -6,27 +6,32 @@ ## 当前恢复点 -- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-050` -- 当前阶段:`Phase 50` +- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-051` +- 当前阶段:`Phase 51` - 当前焦点: - - warning 基线已修正为仓库根目录执行 `dotnet clean` 后再执行 `dotnet build` - - `2026-04-24` 用户确认的 clean solution build 结果为 `Build succeeded with 1193 warning(s)` - - 当前主线程切片为 `GFramework.Godot.SourceGenerators` - - 当前工作树除未跟踪的 `.codex` 目录外,存在待提交的 source generator / `AGENTS.md` / `ai-plan` 修改 + - `2026-04-24` 本轮已完成 `GFramework.Godot.SourceGenerators.Tests` warning 清理 + - 当前主线程切片从生成器实现转到对应测试项目,并已把 `GFramework.Godot.SourceGenerators.Tests` 从 `24` 个 warning 降到 `0` + - 当前批次按 `origin/main` merge-base 计算的累计分支 diff 预计为 `23` 个文件,仍低于 `$gframework-batch-boot 75` 的主阈值 + - 当前工作树除未跟踪的 `.codex` 目录外,还存在与本批次无关的既有文档 / 跟踪文件修改;提交当前批次时必须只包含本 topic 相关文件 ## 当前活跃事实 - 之前记录的 plain `dotnet build` `0 Warning(s)` 属于增量构建假阴性,不能再作为 warning 检查真值 - 本轮已完成 `GFramework.Godot.SourceGenerators` warning 清理:clean `Release` build 从 9 个 warning 降至 0 个 warning - 当前已确认解决的文件包括 `BindNodeSignalGenerator.cs`、`GetNodeGenerator.cs`、`GodotProjectMetadataGenerator.cs`、`Registration/AutoRegisterExportedCollectionsGenerator.cs` -- 后续 warning-reduction 仍应以 clean solution build 的真实输出为切片来源 +- 本轮直接执行仓库根目录 `dotnet clean` 仍在 `ValidateSolutionConfiguration` 阶段失败,输出未提供具体 error 文本 +- 本轮直接执行仓库根目录 `dotnet build` 成功,并给出 `1184 warning(s)` 的真实输出 +- `GFramework.Godot.SourceGenerators.Tests` 已通过测试辅助模板抽取与 `ConfigureAwait(false)` 修正,当前 `Debug` / `Release` 构建均为 `0 Warning(s)` +- 本轮已验证 `dotnet test GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release --no-build`,结果为 `Passed: 48` ## 当前风险 - 如果后续继续依赖增量 `dotnet build`,容易再次把 warning 数量误判为 0 - 缓解措施:每轮 warning 检查前先执行 `dotnet clean`,再执行目标 `dotnet build` -- 当前只验证了受影响项目 `GFramework.Godot.SourceGenerators`;整仓库 warning 总量仍应以用户确认的 clean solution build 为基线 - - 缓解措施:下一轮从 clean solution build 输出里选择新的低风险 warning 热点继续切片 +- 仓库根目录 `dotnet clean` 目前仍然无法给出新的 clean 基线 + - 缓解措施:若下一轮继续做整仓 warning reduction,先定位 `dotnet clean` 的 solution-level 失败原因,或明确继续沿用用户确认的 `1193 warning(s)` clean 基线与本轮 `1184 warning(s)` direct build 观测值 +- 当前 worktree 已存在与本批次无关的未提交改动 + - 缓解措施:提交当前批次时只暂存 `GFramework.Godot.SourceGenerators.Tests` 与对应 `ai-plan` 文件,避免混入其他 topic 变更 ## 活跃文档 @@ -42,14 +47,19 @@ ## 验证说明 -- `dotnet clean GFramework.Godot.SourceGenerators/GFramework.Godot.SourceGenerators.csproj -c Release` - - 结果:成功;`0 Warning(s)`、`0 Error(s)` -- `dotnet build GFramework.Godot.SourceGenerators/GFramework.Godot.SourceGenerators.csproj -c Release` - - 结果:成功;`0 Warning(s)`、`0 Error(s)` +- `dotnet clean` + - 结果:失败;停在 solution `ValidateSolutionConfiguration`,`0 Warning(s)`、`0 Error(s)`,未输出更具体的 error 文本 - `dotnet build` - - 结果:此前被误记为 `0 Warning(s)`;现已确认这是增量构建假阴性,不再作为有效基线 + - 结果:成功;`1184 Warning(s)`、`0 Error(s)` +- `dotnet build GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj` + - 初始结果:成功;`24 Warning(s)`、`0 Error(s)` + - 本轮收尾结果:成功;`0 Warning(s)`、`0 Error(s)` +- `dotnet build GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release` + - 结果:成功;`0 Warning(s)`、`0 Error(s)` +- `dotnet test GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release --no-build` + - 结果:成功;`Passed: 48`、`Failed: 0` ## 下一步建议 -1. 在仓库根目录先执行 `dotnet clean`、再执行 `dotnet build`,重新采集当前 solution 的真实 warning 列表 -2. 以 clean build 输出中的下一个低风险热点作为新切片,优先继续 source generator、测试或单模块可局部验证的问题 +1. 提交当前 `GFramework.Godot.SourceGenerators.Tests` 清理批次,并确认提交只包含本 topic 相关文件 +2. 如果继续 warning reduction,优先重新评估仓库根目录 `dotnet clean` 的 solution-level 失败,再决定是继续从整仓 `dotnet build` 输出挑热点,还是先修复 clean 基线采集问题 diff --git a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md index 725b2f08..b9393bd8 100644 --- a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md +++ b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md @@ -1,5 +1,36 @@ # Analyzer Warning Reduction 追踪 +## 2026-04-24 — RP-051 + +### 阶段:`GFramework.Godot.SourceGenerators.Tests` warning 清零 + +- 触发背景: + - 用户要求直接运行 `dotnet clean`,不再添加额外 shell 包装;solution-level `dotnet clean` 仍然在 `ValidateSolutionConfiguration` 阶段失败 + - 直接执行仓库根目录 `dotnet build` 成功,并输出 `1184 warning(s)`,说明当前真实热点已从 `GFramework.Godot.SourceGenerators` 转移到对应测试项目 +- 主线程实施: + - 以 `GFramework.Godot.SourceGenerators.Tests` 为独立批次,先确认该项目本地基线为 `24 warning(s)` + - 在 `BindNodeSignalGeneratorTests.cs`、`AutoSceneGeneratorTests.cs`、`AutoUiPageGeneratorTests.cs`、`GetNodeGeneratorTests.cs`、`AutoRegisterExportedCollectionsGeneratorTests.cs`、`GodotProjectMetadataGeneratorTests.cs` 中抽取共享 source / diagnostic helper,压缩重复长方法 + - 在 `Core/GeneratorTest.cs` 中补充 `ConfigureAwait(false)`,清除项目内唯一 `MA0004` + - 把 `GFramework.Godot.SourceGenerators.Tests` 项目 warning 从 `24` 降到 `0` +- 验证里程碑: + - `dotnet build` + - 结果:成功;`1184 Warning(s)`、`0 Error(s)` + - `dotnet build GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj` + - 初始结果:成功;`24 Warning(s)`、`0 Error(s)` + - 第一批(`BindNodeSignal` + `GeneratorTest`)后:`16 Warning(s)` + - 第二批(`AutoScene` / `AutoUiPage` / `GetNode`)后:`8 Warning(s)` + - 第三批(`Registration` / `Project`)后:`1 Warning(s)` + - 收尾修复后:成功;`0 Warning(s)`、`0 Error(s)` + - `dotnet build GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release` + - 结果:成功;`0 Warning(s)`、`0 Error(s)` + - `dotnet test GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release --no-build` + - 结果:成功;`Passed: 48`、`Failed: 0` +- 当前结论: + - `GFramework.Godot.SourceGenerators.Tests` 已在 `Debug` / `Release` 构建下达到 `0 warning(s)` + - 按 `origin/main` merge-base 计算并只纳入当前暂存批次时,累计分支 diff 为 `23` 个文件,低于 `$gframework-batch-boot 75` 的主停止阈值 + - 仓库根目录 `dotnet clean` 仍无法稳定产出新的 clean 基线,需要在下一轮单独排查 + - 当前 worktree 已有与本批次无关的既有改动;提交时必须只暂存 analyzer warning reduction 相关文件 + ## 2026-04-24 — RP-050 ### 阶段:clean-build 基线修正与 `GFramework.Godot.SourceGenerators` 切片清零 From 2187f179c36d9befd0b617e4b957729c655171f1 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:39:25 +0800 Subject: [PATCH 9/9] =?UTF-8?q?fix(pr-review):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E5=BF=AB=E7=85=A7=E6=AF=94=E8=BE=83=E5=99=A8?= =?UTF-8?q?=E5=A5=91=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 UnifiedSettingsFile 与 UnifiedSettingsDataRepository 的 comparer 契约,在无法恢复原比较器时显式回退到 StringComparer.Ordinal - 统一 AutoRegisterExportedCollectionsGeneratorTests 中剩余的 RunAsync 异步等待写法,并补齐 ConfigureAwait(false) - 更新 analyzer-warning-reduction 跟踪文档,记录 PR follow-up 的验证结果与恢复点 --- .../Data/UnifiedSettingsDataRepository.cs | 6 ++--- GFramework.Game/Data/UnifiedSettingsFile.cs | 6 ++++- ...gisterExportedCollectionsGeneratorTests.cs | 14 +++++----- .../analyzer-warning-reduction-tracking.md | 26 ++++++++++++------ .../analyzer-warning-reduction-trace.md | 27 +++++++++++++++++++ 5 files changed, 60 insertions(+), 19 deletions(-) diff --git a/GFramework.Game/Data/UnifiedSettingsDataRepository.cs b/GFramework.Game/Data/UnifiedSettingsDataRepository.cs index 7412c948..99672301 100644 --- a/GFramework.Game/Data/UnifiedSettingsDataRepository.cs +++ b/GFramework.Game/Data/UnifiedSettingsDataRepository.cs @@ -288,11 +288,11 @@ public class UnifiedSettingsDataRepository( { ArgumentNullException.ThrowIfNull(source); - // 反序列化后的运行时类型可能只是 IDictionary 实现;若底层仍是 Dictionary,则保留其 comparer, - // 否则退回到按当前内容复制,避免因为 API 抽象化而改变持久化前后的键比较语义。 + // 反序列化后的运行时类型可能只是 IDictionary 实现;若底层仍是 Dictionary,则保留其 comparer。 + // 若 comparer 已因接口抽象而不可恢复,则显式回退到 Ordinal,避免让默认 comparer 语义继续隐式存在。 var sections = source.Sections is Dictionary dictionary ? new Dictionary(dictionary, dictionary.Comparer) - : new Dictionary(source.Sections); + : new Dictionary(source.Sections, StringComparer.Ordinal); return new UnifiedSettingsFile { diff --git a/GFramework.Game/Data/UnifiedSettingsFile.cs b/GFramework.Game/Data/UnifiedSettingsFile.cs index 89e1425b..02fd66b6 100644 --- a/GFramework.Game/Data/UnifiedSettingsFile.cs +++ b/GFramework.Game/Data/UnifiedSettingsFile.cs @@ -11,6 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System; using System.Collections.Generic; using GFramework.Core.Abstractions.Versioning; @@ -28,8 +29,11 @@ internal sealed class UnifiedSettingsFile : IVersioned /// /// 这里公开为 而不是具体的 , /// 以避免暴露可替换的具体集合实现,同时继续兼容 Newtonsoft.Json 对字典对象的序列化与反序列化。 + /// 默认实例使用 ;若调用方提供其他实现,仓库在可以识别底层 + /// comparer 时会保留原语义,否则克隆快照时会显式回退到 + /// 。 /// - public IDictionary Sections { get; set; } = new Dictionary(); + public IDictionary Sections { get; set; } = new Dictionary(StringComparer.Ordinal); /// /// 配置文件版本号,用于版本控制和兼容性检查 diff --git a/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs b/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs index d57a74c6..ded1bb0b 100644 --- a/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs +++ b/GFramework.Godot.SourceGenerators.Tests/Registration/AutoRegisterExportedCollectionsGeneratorTests.cs @@ -135,7 +135,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests .WithLocation(0) .WithArguments("Values")); - await test.RunAsync(); + await test.RunAsync().ConfigureAwait(false); } [Test] @@ -302,7 +302,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests .WithLocation(0) .WithArguments("Register", "_registry", "Values")); - await test.RunAsync(); + await test.RunAsync().ConfigureAwait(false); } [Test] @@ -500,7 +500,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests .WithLocation(0) .WithArguments("_registry", "Values")); - await test.RunAsync(); + await test.RunAsync().ConfigureAwait(false); } [Test] @@ -555,7 +555,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests .WithLocation(0) .WithArguments("Register", "_registry", "Values")); - await test.RunAsync(); + await test.RunAsync().ConfigureAwait(false); } [Test] @@ -610,7 +610,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests .WithLocation(0) .WithArguments("Values")); - await test.RunAsync(); + await test.RunAsync().ConfigureAwait(false); } [Test] @@ -694,7 +694,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests """; } - private static Task VerifyDiagnosticsAsync( + private static async Task VerifyDiagnosticsAsync( string source, bool skipGeneratedSourcesCheck = false, params DiagnosticResult[] expectedDiagnostics) @@ -718,6 +718,6 @@ public class AutoRegisterExportedCollectionsGeneratorTests test.ExpectedDiagnostics.Add(expectedDiagnostic); } - return test.RunAsync(); + await test.RunAsync().ConfigureAwait(false); } } diff --git a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md index 1ee133e9..3fa5eb3a 100644 --- a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md +++ b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md @@ -6,13 +6,13 @@ ## 当前恢复点 -- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-051` -- 当前阶段:`Phase 51` +- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-052` +- 当前阶段:`Phase 52` - 当前焦点: - - `2026-04-24` 本轮已完成 `GFramework.Godot.SourceGenerators.Tests` warning 清理 - - 当前主线程切片从生成器实现转到对应测试项目,并已把 `GFramework.Godot.SourceGenerators.Tests` 从 `24` 个 warning 降到 `0` - - 当前批次按 `origin/main` merge-base 计算的累计分支 diff 预计为 `23` 个文件,仍低于 `$gframework-batch-boot 75` 的主阈值 - - 当前工作树除未跟踪的 `.codex` 目录外,还存在与本批次无关的既有文档 / 跟踪文件修改;提交当前批次时必须只包含本 topic 相关文件 + - `2026-04-24` 本轮从当前 PR review 的未解决线程回切到 `GFramework.Game` / `GFramework.Godot.SourceGenerators.Tests` + - `UnifiedSettingsFile.Sections` 与 `CloneFile` fallback 已对齐为“可保留原 comparer 时保留,否则显式回退到 `StringComparer.Ordinal`”的文档与实现契约 + - `AutoRegisterExportedCollectionsGeneratorTests` 中剩余的 `await test.RunAsync();` 已统一补齐 `.ConfigureAwait(false)`,并同步让 `VerifyDiagnosticsAsync` 内部消费异步等待 + - 当前批次仍需避免混入与 analyzer-warning-reduction 无关的既有工作树改动 ## 当前活跃事实 @@ -23,6 +23,8 @@ - 本轮直接执行仓库根目录 `dotnet build` 成功,并给出 `1184 warning(s)` 的真实输出 - `GFramework.Godot.SourceGenerators.Tests` 已通过测试辅助模板抽取与 `ConfigureAwait(false)` 修正,当前 `Debug` / `Release` 构建均为 `0 Warning(s)` - 本轮已验证 `dotnet test GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release --no-build`,结果为 `Passed: 48` +- 本轮已把 PR #283 中仍打开的 `UnifiedSettingsDataRepository.cs` comparer 契约线程落到代码与 XML 注释,避免 fallback 语义继续依赖隐式默认 comparer +- 本轮已确认 `AutoRegisterExportedCollectionsGeneratorTests` 的 5 处裸 `await test.RunAsync();` 不是当前 Release build 告警来源,但仍作为 PR review 一致性项一并修正 ## 当前风险 @@ -32,6 +34,8 @@ - 缓解措施:若下一轮继续做整仓 warning reduction,先定位 `dotnet clean` 的 solution-level 失败原因,或明确继续沿用用户确认的 `1193 warning(s)` clean 基线与本轮 `1184 warning(s)` direct build 观测值 - 当前 worktree 已存在与本批次无关的未提交改动 - 缓解措施:提交当前批次时只暂存 `GFramework.Godot.SourceGenerators.Tests` 与对应 `ai-plan` 文件,避免混入其他 topic 变更 +- `GFramework.Game` 当前 `Release` build 仍带有既有 analyzer warning 基线 + - 缓解措施:本轮仅验证改动未新增 `UnifiedSettingsDataRepository` / `UnifiedSettingsFile` 相关 warning;若继续在该模块做 warning reduction,需要另开切片处理现存基线 ## 活跃文档 @@ -58,8 +62,14 @@ - 结果:成功;`0 Warning(s)`、`0 Error(s)` - `dotnet test GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release --no-build` - 结果:成功;`Passed: 48`、`Failed: 0` +- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release` + - 结果:成功;`533 Warning(s)`、`0 Error(s)`;模块仍存在既有 warning 基线,本轮 follow-up 仅处理 PR review 指向的 comparer 契约与测试异步等待一致性 +- `dotnet build GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release` + - 结果:成功;`0 Warning(s)`、`0 Error(s)` +- `dotnet test GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release --no-build` + - 结果:成功;`Passed: 48`、`Failed: 0` ## 下一步建议 -1. 提交当前 `GFramework.Godot.SourceGenerators.Tests` 清理批次,并确认提交只包含本 topic 相关文件 -2. 如果继续 warning reduction,优先重新评估仓库根目录 `dotnet clean` 的 solution-level 失败,再决定是继续从整仓 `dotnet build` 输出挑热点,还是先修复 clean 基线采集问题 +1. 提交当前 comparer 契约与 `ConfigureAwait(false)` PR follow-up,并确认只纳入本 topic 相关文件 +2. 视 PR review 反馈决定是否继续收敛 `GFramework.Game` 现有 warning 基线,或返回下一轮整仓 warning 热点筛选 diff --git a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md index b9393bd8..95ac513a 100644 --- a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md +++ b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md @@ -1,5 +1,32 @@ # Analyzer Warning Reduction 追踪 +# Analyzer Warning Reduction 追踪 + +## 2026-04-24 — RP-052 + +### 阶段:PR review follow-up(comparer 契约 + `ConfigureAwait(false)` 收尾) + +- 触发背景: + - 当前分支 PR #283 的最新 review 中,`greptile-apps[bot]` 仍有一个未解决线程,指出 `UnifiedSettingsDataRepository.CloneFile` fallback 会静默丢失原 comparer + - CodeRabbit 另指出 `AutoRegisterExportedCollectionsGeneratorTests.cs` 中还残留 5 处 `await test.RunAsync();`,与同项目其他测试文件的 `.ConfigureAwait(false)` 风格不一致 +- 主线程实施: + - 复核 PR review JSON、`UnifiedSettingsDataRepository.cs`、`UnifiedSettingsFile.cs` 与 `AutoRegisterExportedCollectionsGeneratorTests.cs` 的当前代码,确认只有 comparer 契约线程仍属最新 head 上的实质问题 + - 将 `UnifiedSettingsFile.Sections` 的 XML 注释补充为显式 comparer 契约,并把默认字典初始化改为 `StringComparer.Ordinal` + - 将 `CloneFile` fallback 从隐式默认 comparer 改为显式 `StringComparer.Ordinal`,并同步修正文档注释,避免继续暗含“保留原语义”的错误表述 + - 把 `AutoRegisterExportedCollectionsGeneratorTests` 中剩余的 5 处 `await test.RunAsync();` 统一为 `.ConfigureAwait(false)`,同时让 `VerifyDiagnosticsAsync` 内部也消费 `ConfigureAwait(false)` +- 验证里程碑: + - `dotnet build GFramework.Game/GFramework.Game.csproj -c Release` + - 结果:成功;`533 Warning(s)`、`0 Error(s)`;`GFramework.Game` 仍有既有 warning 基线,本轮 follow-up 仅处理 PR review 指向的 comparer 契约与测试异步等待一致性 + - `dotnet build GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release` + - 结果:成功;`0 Warning(s)`、`0 Error(s)` + - `dotnet test GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release --no-build` + - 首次并行复验:失败;`FileNotFoundException`,原因是 `--no-build` 测试在 Release DLL 落盘前启动 + - 串行复验:成功;`Passed: 48`、`Failed: 0` +- 当前结论: + - PR #283 当前仍打开的 comparer review thread 已在本地代码与 XML 注释层面得到对应修复 + - `AutoRegisterExportedCollectionsGeneratorTests` 的异步等待风格已与同项目其他测试保持一致 + - 当前改动已通过直接受影响测试项目的 Release build 与串行 Release test 复验,可进入提交阶段 + ## 2026-04-24 — RP-051 ### 阶段:`GFramework.Godot.SourceGenerators.Tests` warning 清零