From 97573be2e143613e4b812afebb1bd4af259a8dd9 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 22 Apr 2026 08:56:05 +0800 Subject: [PATCH 01/13] =?UTF-8?q?fix(core):=20=E6=B8=85=E9=9B=B6=E4=BD=8E?= =?UTF-8?q?=E9=A3=8E=E9=99=A9=E5=88=86=E6=9E=90=E5=99=A8=E8=AD=A6=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 Core 配置与集合扩展的集合抽象契约 - 修复 CoroutineScheduler 字符串字典 comparer 与 EasyEvents 重复注册异常类型 - 补充 Option 相等性接口并更新 analyzer recovery 记录 --- .../Events/EasyEventsTests.cs | 6 +-- .../Coroutine/CoroutineScheduler.cs | 4 +- GFramework.Core/Events/EasyEvents.cs | 6 +-- .../Extensions/CollectionExtensions.cs | 2 +- GFramework.Core/Functional/Option.cs | 4 +- .../Logging/FilterConfiguration.cs | 4 +- .../Logging/LoggingConfiguration.cs | 5 ++- .../analyzer-warning-reduction-tracking.md | 42 ++++++++++++------- .../analyzer-warning-reduction-trace.md | 34 +++++++++++++++ 9 files changed, 76 insertions(+), 31 deletions(-) diff --git a/GFramework.Core.Tests/Events/EasyEventsTests.cs b/GFramework.Core.Tests/Events/EasyEventsTests.cs index 404fe024..2b83ff08 100644 --- a/GFramework.Core.Tests/Events/EasyEventsTests.cs +++ b/GFramework.Core.Tests/Events/EasyEventsTests.cs @@ -99,14 +99,14 @@ public class EasyEventsTests } /// - /// 测试并发场景下AddEvent的行为 + /// 测试 AddEvent 对重复事件类型给出状态冲突异常。 /// [Test] public void AddEvent_Should_Throw_When_Already_Registered() { _easyEvents.AddEvent>(); - Assert.Throws(() => _easyEvents.AddEvent>()); + Assert.Throws(() => _easyEvents.AddEvent>()); } /// @@ -167,4 +167,4 @@ public class EasyEventsTests Assert.That(_easyEvents.GetEvent>(), Is.Not.Null); Assert.That(_easyEvents.GetEvent>(), Is.Not.Null); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Coroutine/CoroutineScheduler.cs b/GFramework.Core/Coroutine/CoroutineScheduler.cs index ed57b06f..a7fb8676 100644 --- a/GFramework.Core/Coroutine/CoroutineScheduler.cs +++ b/GFramework.Core/Coroutine/CoroutineScheduler.cs @@ -41,7 +41,7 @@ public sealed class CoroutineScheduler( private readonly Dictionary _completionStatuses = new(); private readonly Queue _completionStatusOrder = new(); - private readonly Dictionary> _grouped = new(); + private readonly Dictionary> _grouped = new(StringComparer.Ordinal); private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CoroutineScheduler)); private readonly Dictionary _metadata = new(); private readonly ConcurrentQueue _pendingKills = new(); @@ -50,7 +50,7 @@ public sealed class CoroutineScheduler( throw new ArgumentNullException(nameof(timeSource)); private readonly CoroutineStatistics? _statistics = enableStatistics ? new CoroutineStatistics() : null; - private readonly Dictionary> _tagged = new(); + private readonly Dictionary> _tagged = new(StringComparer.Ordinal); private readonly ITimeSource _timeSource = timeSource ?? throw new ArgumentNullException(nameof(timeSource)); private readonly Dictionary> _waiting = new(); private int _nextSlot; diff --git a/GFramework.Core/Events/EasyEvents.cs b/GFramework.Core/Events/EasyEvents.cs index 5159944e..adf42898 100644 --- a/GFramework.Core/Events/EasyEvents.cs +++ b/GFramework.Core/Events/EasyEvents.cs @@ -53,12 +53,12 @@ public class EasyEvents /// 添加指定类型的事件到事件字典中 /// /// 事件类型,必须实现IEasyEvent接口且具有无参构造函数 - /// 当事件类型已存在时抛出 + /// 当事件类型已存在时抛出。 public void AddEvent() where T : IEvent, new() { if (!_mTypeEvents.TryAdd(typeof(T), new T())) { - throw new ArgumentException($"Event type {typeof(T).Name} already registered."); + throw new InvalidOperationException($"Event type {typeof(T).Name} already registered."); } } @@ -81,4 +81,4 @@ public class EasyEvents { return (T)_mTypeEvents.GetOrAdd(typeof(T), _ => new T()); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Extensions/CollectionExtensions.cs b/GFramework.Core/Extensions/CollectionExtensions.cs index 849f3ae5..19d02671 100644 --- a/GFramework.Core/Extensions/CollectionExtensions.cs +++ b/GFramework.Core/Extensions/CollectionExtensions.cs @@ -81,7 +81,7 @@ public static class CollectionExtensions /// // dict["a"] == 3 (最后一个值) /// /// - public static Dictionary ToDictionarySafe( + public static IDictionary ToDictionarySafe( this IEnumerable source, Func keySelector, Func valueSelector) where TKey : notnull diff --git a/GFramework.Core/Functional/Option.cs b/GFramework.Core/Functional/Option.cs index 709c3438..31d18d2e 100644 --- a/GFramework.Core/Functional/Option.cs +++ b/GFramework.Core/Functional/Option.cs @@ -17,7 +17,7 @@ namespace GFramework.Core.Functional; /// 表示可能存在或不存在的值,用于替代 null 引用的函数式编程类型 /// /// 值的类型 -public readonly struct Option +public readonly struct Option : IEquatable> { private readonly T _value; private readonly bool _isSome; @@ -313,4 +313,4 @@ public readonly struct Option _isSome ? $"Some({_value})" : "None"; #endregion -} \ No newline at end of file +} diff --git a/GFramework.Core/Logging/FilterConfiguration.cs b/GFramework.Core/Logging/FilterConfiguration.cs index 7e9d8db8..74651da0 100644 --- a/GFramework.Core/Logging/FilterConfiguration.cs +++ b/GFramework.Core/Logging/FilterConfiguration.cs @@ -20,10 +20,10 @@ public sealed class FilterConfiguration /// /// 命名空间前缀列表(用于 Namespace 过滤器)。 /// - public List? Namespaces { get; set; } + public IList? Namespaces { get; set; } /// /// 子过滤器列表(用于 Composite 过滤器)。 /// - public List? Filters { get; set; } + public IList? Filters { get; set; } } diff --git a/GFramework.Core/Logging/LoggingConfiguration.cs b/GFramework.Core/Logging/LoggingConfiguration.cs index c9cec380..0feedf6f 100644 --- a/GFramework.Core/Logging/LoggingConfiguration.cs +++ b/GFramework.Core/Logging/LoggingConfiguration.cs @@ -15,10 +15,11 @@ public sealed class LoggingConfiguration /// /// Appender 配置列表 /// - public List Appenders { get; set; } = new(); + public IList Appenders { get; set; } = new List(); /// /// 特定 Logger 的日志级别配置 /// - public Dictionary LoggerLevels { get; set; } = new(StringComparer.Ordinal); + public IDictionary LoggerLevels { get; set; } = + new Dictionary(StringComparer.Ordinal); } 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 b782563c..7bbba424 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 @@ -7,17 +7,18 @@ ## 当前恢复点 -- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-015` -- 当前阶段:`Phase 15` +- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-016` +- 当前阶段:`Phase 16` - 当前焦点: - - 当前分支 PR #267 的失败测试已通过 `$gframework-pr-review` 与本地整包测试完成复核 - - 已确认并修复 `AsyncLogAppender.Flush()` 在“后台线程先清空队列”场景下可能超时返回 `false` 的竞态 - - 已补上稳定回归测试,避免只在整包 `GFramework.Core.Tests` 里偶发暴露的刷新完成信号问题再次回归 - - 下一轮默认恢复到 `MA0016` 或 `MA0002` 低风险批次;`MA0015` 与 `MA0077` 继续作为尾项顺手吸收 + - 已完成 `GFramework.Core` 当前 `MA0016` / `MA0002` / `MA0015` / `MA0077` 低风险收口批次 + - `LoggingConfiguration`、`FilterConfiguration` 与 `CollectionExtensions` 已改用集合抽象接口,并保留内部具体集合默认值 + - `CoroutineScheduler` 的 tag/group 字典已显式使用 `StringComparer.Ordinal`,保持既有区分大小写语义 + - `EasyEvents.AddEvent()` 的重复注册路径已改为状态冲突异常,避免把泛型类型参数伪装成方法参数名 + - `Option` 已声明 `IEquatable>`,与已有强类型 `Equals(Option)` 契约对齐 + - 当前 `GFramework.Core` `net8.0` warnings-only 基线已降到 `0` 条 - `GFramework.Godot` 的 `Timing.cs` 已同步适配新事件签名,但当前 worktree 的 Godot restore 资产仍受 Windows fallback package folder 干扰,独立 build 需在修复资产后补跑 - 后续继续按 warning 类型和数量批处理,而不是回退到按单文件切片推进 - - 当某一轮主类型数量不足时,允许顺手合并其他低冲突 warning 类型,`MA0015` 与 `MA0077` - 只是当前最明显的低数量示例,不构成限定 + - 下一轮默认评估跨 target 的 `MA0158` 锁替换风险,或单独处理 source generator 剩余 `MA0051` - 单次 `boot` 的工作树改动上限控制在约 `100` 个文件以内,避免 recovery context 与 review 面同时失控 - 若任务边界互不冲突,允许使用不同模型的 subagent 并行处理不同 warning 类型或不同目录,但必须遵守显式 ownership @@ -34,8 +35,7 @@ `PhaseChanged` / `CoroutineExceptionEventArgs` XML 文档、`PhaseChanged` 迁移说明和 `ai-plan` 基线注释 - 已完成当前 PR #267 failed-test follow-up:修复 `AsyncLogAppender.Flush()` 在队列已被后台线程提前清空时仍可能 等待满默认超时并返回 `false` 的竞态,并通过整包 `GFramework.Core.Tests` 重新验证 -- 当前 `GFramework.Core` `net8.0` warnings-only 基线已降到 `9` 条;剩余 warning 集中在 - `MA0016` 集合抽象接口、`MA0002` comparer 重载,以及 `MA0015` / `MA0077` 两个低数量尾项 +- 已完成当前 `GFramework.Core` `net8.0` 剩余低风险 analyzer warning 批次;warnings-only 基线已降到 `0` 条 ## 当前活跃事实 @@ -66,16 +66,20 @@ nitpick comment 后,确认 8 条高信号项中仍成立的是 1 个行为 bug 与 7 个文档/测试/跟踪缺口,并按最小改动收口 - `RP-015` 使用 `$gframework-pr-review` 复核 PR #267 的 CTRF 失败测试评论后,确认 `AsyncLogAppender` 仍存在 “队列已空但 Flush 仍超时失败”的竞态;该问题在本地整包 `GFramework.Core.Tests` 中可复现,现已修复并补上稳定回归测试 +- `RP-016` 将 `GFramework.Core` 当前剩余 `MA0016` / `MA0002` / `MA0015` / `MA0077` 低风险批次清零,并用 + warnings-only build 与 focused tests 验证配置反序列化、集合扩展、事件重复注册、`Option` 相等性和协程 tag/group 语义 - 当前工作树分支 `fix/analyzer-warning-reduction-batch` 已在 `ai-plan/public/README.md` 建立 topic 映射 ## 当前风险 -- 公共契约兼容风险:剩余 `MA0016` 若直接改公开集合类型,可能波及用户代码 - - 缓解措施:优先选择不改公共 API 的低风险切法;若必须触达公共契约,先补齐 XML 契约说明与定向测试 +- 公共契约兼容风险:本轮将部分配置与扩展方法返回值从具体集合类型改为集合抽象接口 + - 缓解措施:保留具体集合默认值,并通过配置反序列化、工厂创建与集合扩展定向测试覆盖主要消费路径 - 测试宿主稳定性风险:部分 Godot 失败路径在当前 .NET 测试宿主下仍不稳定 - 缓解措施:继续优先使用稳定的 targeted test、项目构建和相邻 smoke test 组合验证 - 多目标框架 warning 解释风险:同一源位置会在多个 target framework 下重复计数 - 缓解措施:继续以唯一源位置和 warning 家族为主要决策依据,而不是只看原始 warning 总数 +- net10 专属 warning 风险:`MA0158` 建议使用 `System.Threading.Lock`,但项目多 target 时需要确认兼容边界 + - 缓解措施:下一轮先按 target framework 与 API 可用性评估,不直接批量替换共享源码中的 `object` lock - Godot 资产文件环境风险:当前 worktree 的 `GFramework.Godot` restore/build 仍会命中 Windows fallback package folder - 缓解措施:后续若继续触达 Godot 模块,先用 Linux 侧 restore 资产或 Windows-hosted 构建链刷新该项目,再补跑定向 build - 并行实现风险:批量收敛时若 subagent 写入边界不清晰,容易引入命名冲突或重复重构 @@ -158,12 +162,18 @@ - 结果:`15 Passed`,`0 Failed` - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --disable-build-servers` - 结果:`1607 Passed`,`0 Failed` +- `RP-016` 的验证结果: + - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -nologo -clp:"Summary;WarningsOnly"` + - 结果:`0 Warning(s)`,`0 Error(s)`;当前 `GFramework.Core` `net8.0` analyzer baseline 已清零 + - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~LoggingConfigurationTests|FullyQualifiedName~ConfigurableLoggerFactoryTests|FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~EasyEventsTests|FullyQualifiedName~OptionTests|FullyQualifiedName~CoroutineGroupTests|FullyQualifiedName~CoroutineSchedulerTests" -m:1 -nologo` + - 结果:`112 Passed`,`0 Failed`;测试构建仍会显示既有 `net10.0` `MA0158` 与 source generator `MA0051` warning - active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史 ## 下一步 1. 若要继续该主题,先读 active tracking,再按需展开历史归档中的 warning 热点与验证记录 -2. 下一轮优先在 `MA0016` 与 `MA0002` 之间选择低风险批次继续推进,默认先看 `LoggingConfiguration` / - `FilterConfiguration` 与 `CollectionExtensions` -3. 若后续继续改动 `GFramework.Godot`,先修复该项目的 Linux 侧 restore 资产,再补跑独立 build -4. 若本主题确认暂缓,可保持当前归档状态,不需要再恢复 `local-plan/` +2. 下一轮优先评估 `net10.0` 下的 `MA0158` 是否能在不破坏多 target 兼容性的前提下安全推进 +3. 若暂不推进 `MA0158`,可转入 `GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs` 的剩余 `MA0051` + 结构拆分 +4. 若后续继续改动 `GFramework.Godot`,先修复该项目的 Linux 侧 restore 资产,再补跑独立 build +5. 若本主题确认暂缓,可保持当前归档状态,不需要再恢复 `local-plan/` 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 2bd3c807..dc452158 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,39 @@ # Analyzer Warning Reduction 追踪 +## 2026-04-22 — RP-016 + +### 阶段:`GFramework.Core` 剩余低风险 warning 批次清零(RP-016) + +- 依据 `RP-015` 的下一步建议,本轮恢复到 `MA0016` / `MA0002` 低风险批次,并顺手吸收仍集中在 + `GFramework.Core` 的 `MA0015` 与 `MA0077` +- 基线复核: + - 首次使用 Linux `dotnet` 时仍被当前 worktree 的 Windows fallback package folder restore 资产阻断 + - 切换到 host Windows `dotnet` 后,`GFramework.Core` `net8.0` warnings-only build 复现 `9` 条 warning: + `MA0016=5`、`MA0002=2`、`MA0015=1`、`MA0077=1` +- 实施调整: + - 将 `LoggingConfiguration.Appenders` / `LoggerLevels` 与 `FilterConfiguration.Namespaces` / `Filters` + 的公开类型改为集合抽象接口,同时保留 `List` / `Dictionary` 默认实例,兼顾 analyzer 与现有配置消费路径 + - 将 `CollectionExtensions.ToDictionarySafe(...)` 返回类型改为 `IDictionary`,内部仍使用 `Dictionary` + 保留“重复键以后值覆盖前值”的实现语义 + - 为 `CoroutineScheduler` 的 `_tagged` 与 `_grouped` 字典显式指定 `StringComparer.Ordinal`,将原有默认区分大小写语义写入代码 + - 将 `EasyEvents.AddEvent()` 重复注册失败从 `ArgumentException` 改为 `InvalidOperationException`;该路径表示状态冲突, + 不是某个方法参数无效,因此不能为 `MA0015` 人造参数名 + - 为 `Option` 声明 `IEquatable>`,与已有强类型 `Equals(Option)` 实现对齐 +- 验证结果: + - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -nologo -clp:"Summary;WarningsOnly"` + - 结果:`0 Warning(s)`,`0 Error(s)` + - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~LoggingConfigurationTests|FullyQualifiedName~ConfigurableLoggerFactoryTests|FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~EasyEventsTests|FullyQualifiedName~OptionTests|FullyQualifiedName~CoroutineGroupTests|FullyQualifiedName~CoroutineSchedulerTests" -m:1 -nologo` + - 结果:`112 Passed`,`0 Failed` + - 说明:测试构建仍显示既有 `net10.0` `MA0158` 与 source generator `MA0051` warning;这些不属于本轮 + `GFramework.Core` `net8.0` 剩余 warning 批次 +- 当前结论: + - `GFramework.Core` `net8.0` 当前 analyzer warning baseline 已清零 + - analyzer topic 仍可继续,但下一轮应转入 `net10.0` 专属 `MA0158` 兼容性评估,或单独处理 source generator 剩余 + `MA0051` +- 下一步建议: + - 优先评估 `MA0158` 在多 target 源码中的安全推进方式;若风险过高,再处理 + `GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs` 的结构拆分 + ## 2026-04-21 — RP-015 ### 阶段:PR #267 failed-test follow-up 收口(RP-015) From 7ec2185ae035f99f9da6f63aa4a6d38387d989bb Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 22 Apr 2026 09:20:00 +0800 Subject: [PATCH 02/13] =?UTF-8?q?refactor(source-generators):=20=E6=8B=86?= =?UTF-8?q?=E5=88=86=E4=B8=8A=E4=B8=8B=E6=96=87=E6=84=9F=E7=9F=A5=E7=94=9F?= =?UTF-8?q?=E6=88=90=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构 ContextAwareGenerator 的上下文属性生成流程,降低 MA0051 复杂度 - 补充 analyzer warning reduction 的 RP-017 恢复记录与验证结果 - 更新 下一步 MA0158 多目标兼容性评估方向 --- .../Rule/ContextAwareGenerator.cs | 27 +++++++++++++++ .../analyzer-warning-reduction-tracking.md | 30 +++++++++++++---- .../analyzer-warning-reduction-trace.md | 33 +++++++++++++++++++ 3 files changed, 84 insertions(+), 6 deletions(-) diff --git a/GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs b/GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs index b2c4d90e..5e3a0356 100644 --- a/GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs +++ b/GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs @@ -139,12 +139,31 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase /// /// 字符串构建器 private static void GenerateContextProperty(StringBuilder sb) + { + GenerateContextBackingFields(sb); + GenerateContextGetter(sb); + GenerateContextProviderConfiguration(sb); + } + + /// + /// 生成上下文缓存和同步所需的字段。 + /// + /// 字符串构建器。 + private static void GenerateContextBackingFields(StringBuilder sb) { sb.AppendLine(" private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _context;"); sb.AppendLine( " private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? _contextProvider;"); sb.AppendLine(" private static readonly object _contextSync = new();"); sb.AppendLine(); + } + + /// + /// 生成实例上下文访问器,包含显式注入优先和 provider 惰性回退语义。 + /// + /// 字符串构建器。 + private static void GenerateContextGetter(StringBuilder sb) + { sb.AppendLine(" /// "); sb.AppendLine(" /// 获取当前实例绑定的架构上下文。"); sb.AppendLine(" /// "); @@ -185,6 +204,14 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase sb.AppendLine(" }"); sb.AppendLine(" }"); sb.AppendLine(); + } + + /// + /// 生成静态 provider 配置 API,供测试和宿主在懒加载前替换默认上下文来源。 + /// + /// 字符串构建器。 + private static void GenerateContextProviderConfiguration(StringBuilder sb) + { sb.AppendLine(" /// "); sb.AppendLine(" /// 配置当前生成类型共享的上下文提供者。"); sb.AppendLine(" /// "); 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 7bbba424..ffe8d6ec 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 @@ -7,18 +7,22 @@ ## 当前恢复点 -- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-016` -- 当前阶段:`Phase 16` +- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-017` +- 当前阶段:`Phase 17` - 当前焦点: - 已完成 `GFramework.Core` 当前 `MA0016` / `MA0002` / `MA0015` / `MA0077` 低风险收口批次 + - 已复核 `net10.0` 下的 `MA0158` 基线:`GFramework.Core` / `GFramework.Cqrs` 当前共有 `16` 个 object lock + 建议点,属于跨 target 兼容性风险,不在本轮直接批量替换 + - 已完成 `GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs` 的剩余 `MA0051` 结构拆分,生成输出保持不变 - `LoggingConfiguration`、`FilterConfiguration` 与 `CollectionExtensions` 已改用集合抽象接口,并保留内部具体集合默认值 - `CoroutineScheduler` 的 tag/group 字典已显式使用 `StringComparer.Ordinal`,保持既有区分大小写语义 - `EasyEvents.AddEvent()` 的重复注册路径已改为状态冲突异常,避免把泛型类型参数伪装成方法参数名 - `Option` 已声明 `IEquatable>`,与已有强类型 `Equals(Option)` 契约对齐 - 当前 `GFramework.Core` `net8.0` warnings-only 基线已降到 `0` 条 + - 当前 `GFramework.Core.SourceGenerators` warnings-only 基线已降到 `0` 条 - `GFramework.Godot` 的 `Timing.cs` 已同步适配新事件签名,但当前 worktree 的 Godot restore 资产仍受 Windows fallback package folder 干扰,独立 build 需在修复资产后补跑 - 后续继续按 warning 类型和数量批处理,而不是回退到按单文件切片推进 - - 下一轮默认评估跨 target 的 `MA0158` 锁替换风险,或单独处理 source generator 剩余 `MA0051` + - 下一轮默认继续评估跨 target 的 `MA0158` 锁替换风险,或转向其他 source generator / test warning 热点 - 单次 `boot` 的工作树改动上限控制在约 `100` 个文件以内,避免 recovery context 与 review 面同时失控 - 若任务边界互不冲突,允许使用不同模型的 subagent 并行处理不同 warning 类型或不同目录,但必须遵守显式 ownership @@ -36,6 +40,7 @@ - 已完成当前 PR #267 failed-test follow-up:修复 `AsyncLogAppender.Flush()` 在队列已被后台线程提前清空时仍可能 等待满默认超时并返回 `false` 的竞态,并通过整包 `GFramework.Core.Tests` 重新验证 - 已完成当前 `GFramework.Core` `net8.0` 剩余低风险 analyzer warning 批次;warnings-only 基线已降到 `0` 条 +- 已完成 `GFramework.Core.SourceGenerators` 中 `ContextAwareGenerator` 的剩余 `MA0051` 收口;warnings-only 基线已降到 `0` 条 ## 当前活跃事实 @@ -68,6 +73,8 @@ “队列已空但 Flush 仍超时失败”的竞态;该问题在本地整包 `GFramework.Core.Tests` 中可复现,现已修复并补上稳定回归测试 - `RP-016` 将 `GFramework.Core` 当前剩余 `MA0016` / `MA0002` / `MA0015` / `MA0077` 低风险批次清零,并用 warnings-only build 与 focused tests 验证配置反序列化、集合扩展、事件重复注册、`Option` 相等性和协程 tag/group 语义 +- `RP-017` 复核 `MA0158` 当前仍是跨 target 锁类型迁移问题,因此先收口单点 `ContextAwareGenerator` `MA0051`, + 并通过 source generator 项目 build 与 `ContextAwareGeneratorSnapshotTests` 验证生成输出未回归 - 当前工作树分支 `fix/analyzer-warning-reduction-batch` 已在 `ai-plan/public/README.md` 建立 topic 映射 ## 当前风险 @@ -80,6 +87,10 @@ - 缓解措施:继续以唯一源位置和 warning 家族为主要决策依据,而不是只看原始 warning 总数 - net10 专属 warning 风险:`MA0158` 建议使用 `System.Threading.Lock`,但项目多 target 时需要确认兼容边界 - 缓解措施:下一轮先按 target framework 与 API 可用性评估,不直接批量替换共享源码中的 `object` lock +- source generator warning 外溢风险:运行 `GFramework.SourceGenerators.Tests` 会构建相邻 generator/test 项目并显示既有 + `GFramework.Cqrs.SourceGenerators`、`GFramework.Game.SourceGenerators` 与测试项目 warning + - 缓解措施:本轮以 `GFramework.Core.SourceGenerators` 独立 warnings-only build 作为主验收,并用 focused snapshot test + 验证行为;后续若处理相邻 generator warning,应另开明确切片 - Godot 资产文件环境风险:当前 worktree 的 `GFramework.Godot` restore/build 仍会命中 Windows fallback package folder - 缓解措施:后续若继续触达 Godot 模块,先用 Linux 侧 restore 资产或 Windows-hosted 构建链刷新该项目,再补跑定向 build - 并行实现风险:批量收敛时若 subagent 写入边界不清晰,容易引入命名冲突或重复重构 @@ -167,13 +178,20 @@ - 结果:`0 Warning(s)`,`0 Error(s)`;当前 `GFramework.Core` `net8.0` analyzer baseline 已清零 - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~LoggingConfigurationTests|FullyQualifiedName~ConfigurableLoggerFactoryTests|FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~EasyEventsTests|FullyQualifiedName~OptionTests|FullyQualifiedName~CoroutineGroupTests|FullyQualifiedName~CoroutineSchedulerTests" -m:1 -nologo` - 结果:`112 Passed`,`0 Failed`;测试构建仍会显示既有 `net10.0` `MA0158` 与 source generator `MA0051` warning +- `RP-017` 的验证结果: + - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net10.0 -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"` + - 结果:`16 Warning(s)`,`0 Error(s)`;当前 `MA0158` 跨 `GFramework.Core` / `GFramework.Cqrs`,本轮只记录基线不批量改锁 + - `dotnet build GFramework.Core.SourceGenerators/GFramework.Core.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"` + - 结果:`0 Warning(s)`,`0 Error(s)`;`ContextAwareGenerator.cs` 已不再出现 `MA0051` + - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~ContextAwareGeneratorSnapshotTests" -m:1 -p:RestoreFallbackFolders="" -nologo` + - 结果:`1 Passed`,`0 Failed`;测试构建仍显示相邻 source generator 和测试项目的既有 analyzer warning - active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史 ## 下一步 1. 若要继续该主题,先读 active tracking,再按需展开历史归档中的 warning 热点与验证记录 -2. 下一轮优先评估 `net10.0` 下的 `MA0158` 是否能在不破坏多 target 兼容性的前提下安全推进 -3. 若暂不推进 `MA0158`,可转入 `GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs` 的剩余 `MA0051` - 结构拆分 +2. 下一轮优先评估 `net10.0` 下的 `MA0158` 是否能通过条件编译或目标框架特定源码安全推进 +3. 若暂不推进 `MA0158`,可转入 `GFramework.Cqrs.SourceGenerators` 或 `GFramework.Game.SourceGenerators` 的剩余 + `MA0051` / `MA0006` 热点,但应单独建立文件 ownership 和验证范围 4. 若后续继续改动 `GFramework.Godot`,先修复该项目的 Linux 侧 restore 资产,再补跑独立 build 5. 若本主题确认暂缓,可保持当前归档状态,不需要再恢复 `local-plan/` 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 dc452158..ae5fa38d 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,38 @@ # Analyzer Warning Reduction 追踪 +## 2026-04-22 — RP-017 + +### 阶段:`ContextAwareGenerator` 剩余 `MA0051` 收口(RP-017) + +- 启动复核: + - 当前 worktree 仍映射到 `analyzer-warning-reduction` active topic + - `GFramework.Core` `net10.0` warnings-only build 在刷新 restore fallback 资产后复现 `16` 个 `MA0158` + - `GFramework.Core.SourceGenerators` warnings-only build 复现 `ContextAwareGenerator.GenerateContextProperty` 的单个 + `MA0051` +- 决策: + - `MA0158` 涉及 `GFramework.Core` 与 `GFramework.Cqrs` 的 object lock 字段,且项目仍多 target 到 `net8.0` / `net9.0` + / `net10.0`,因此本轮不直接批量替换为 `System.Threading.Lock` + - 先处理单文件、单 warning、生成输出可由 snapshot 验证的 `ContextAwareGenerator` 结构拆分 + - 未使用 subagent;本轮 critical path 是本地复现 warning、拆分方法并验证生成输出,拆分后写集只包含单个 generator 文件和 + active `ai-plan` 文档 +- 实施调整: + - 将 `GenerateContextProperty` 拆为 `GenerateContextBackingFields`、`GenerateContextGetter` 与 + `GenerateContextProviderConfiguration` + - 保留原有 `StringBuilder` 追加顺序与生成代码文本,避免 snapshot 变更 + - 为新增 helper 补充 XML 注释,说明字段、getter 与 provider 配置 API 的生成职责 +- 验证结果: + - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net10.0 -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"` + - 结果:`16 Warning(s)`,`0 Error(s)`;记录当前 `MA0158` 基线,不作为本轮修改范围 + - `dotnet build GFramework.Core.SourceGenerators/GFramework.Core.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"` + - 结果:`0 Warning(s)`,`0 Error(s)`;`ContextAwareGenerator.cs` 的 `MA0051` 已清零 + - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~ContextAwareGeneratorSnapshotTests" -m:1 -p:RestoreFallbackFolders="" -nologo` + - 结果:`1 Passed`,`0 Failed` + - 说明:该 test project 构建仍显示相邻 generator/test 项目的既有 analyzer warning;本轮关注的 + `GFramework.Core.SourceGenerators` 独立 build 已清零 +- 下一步建议: + - 继续该主题时,优先设计 `MA0158` 的多 target 兼容迁移方案;如果风险过高,再单独切入 + `GFramework.Cqrs.SourceGenerators` 或 `GFramework.Game.SourceGenerators` 的结构性 warning + ## 2026-04-22 — RP-016 ### 阶段:`GFramework.Core` 剩余低风险 warning 批次清零(RP-016) From de782ae1796813bb993bb75555033277ac5fd682 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 22 Apr 2026 09:46:14 +0800 Subject: [PATCH 03/13] =?UTF-8?q?refactor(cqrs):=20=E6=8B=86=E5=88=86?= =?UTF-8?q?=E5=A4=84=E7=90=86=E5=99=A8=E6=B3=A8=E5=86=8C=E7=94=9F=E6=88=90?= =?UTF-8?q?=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构 CQRS handler registry 生成器的候选分析、运行时类型引用和源码发射阶段 - 补充 analyzer warning reduction 的 RP-018 跟踪和验证记录 --- .../Cqrs/CqrsHandlerRegistryGenerator.cs | 956 +++++++++++++----- .../analyzer-warning-reduction-tracking.md | 30 +- .../analyzer-warning-reduction-trace.md | 26 + 3 files changed, 754 insertions(+), 258 deletions(-) diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs index f74ec308..487061ac 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -89,17 +89,42 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator if (!IsConcreteHandlerType(type)) return null; - var handlerInterfaces = type.AllInterfaces - .Where(IsSupportedHandlerInterface) - .OrderBy(GetTypeSortKey, StringComparer.Ordinal) - .ToImmutableArray(); + var handlerInterfaces = GetSupportedHandlerInterfaces(type); if (handlerInterfaces.IsDefaultOrEmpty) return null; + return CreateHandlerCandidateAnalysis(context.SemanticModel.Compilation, type, handlerInterfaces); + } + + /// + /// 收集当前实现类型已经关闭的 CQRS handler 接口,并按稳定显示名排序以保证生成输出可重复。 + /// + /// 当前语义模型发现的具体 handler 实现类型。 + /// 可由 CQRS 注册器生成器处理的 handler 接口集合。 + private static ImmutableArray GetSupportedHandlerInterfaces(INamedTypeSymbol type) + { + return type.AllInterfaces + .Where(IsSupportedHandlerInterface) + .OrderBy(GetTypeSortKey, StringComparer.Ordinal) + .ToImmutableArray(); + } + + /// + /// 将单个实现类型的 handler 接口拆分为直接注册、实现类型反射注册、精确反射注册和兜底 fallback 四类结果。 + /// + /// 当前生成轮次的编译上下文,用于判断类型可访问性。 + /// 需要分析的 handler 实现类型。 + /// 该实现类型声明的受支持 CQRS handler 接口。 + /// 供最终生成阶段消费的 handler 候选分析结果。 + private static HandlerCandidateAnalysis CreateHandlerCandidateAnalysis( + Compilation compilation, + INamedTypeSymbol type, + ImmutableArray handlerInterfaces) + { var implementationTypeDisplayName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var implementationLogName = GetLogDisplayName(type); - var canReferenceImplementation = CanReferenceFromGeneratedRegistry(context.SemanticModel.Compilation, type); + var canReferenceImplementation = CanReferenceFromGeneratedRegistry(compilation, type); var registrations = ImmutableArray.CreateBuilder(handlerInterfaces.Length); var reflectedImplementationRegistrations = ImmutableArray.CreateBuilder(handlerInterfaces.Length); @@ -108,32 +133,16 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator string? reflectionFallbackHandlerTypeMetadataName = null; foreach (var handlerInterface in handlerInterfaces) { - var canReferenceHandlerInterface = - CanReferenceFromGeneratedRegistry(context.SemanticModel.Compilation, handlerInterface); - if (canReferenceImplementation && canReferenceHandlerInterface) - { - registrations.Add(new HandlerRegistrationSpec( - handlerInterface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), - implementationTypeDisplayName, - GetLogDisplayName(handlerInterface), - implementationLogName)); - continue; - } - - if (!canReferenceImplementation && canReferenceHandlerInterface) - { - reflectedImplementationRegistrations.Add(new ReflectedImplementationRegistrationSpec( - handlerInterface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), - GetLogDisplayName(handlerInterface))); - continue; - } - - if (TryCreatePreciseReflectedRegistration( - context.SemanticModel.Compilation, + if (TryAddStaticHandlerRegistration( + compilation, handlerInterface, - out var preciseReflectedRegistration)) + canReferenceImplementation, + implementationTypeDisplayName, + implementationLogName, + registrations, + reflectedImplementationRegistrations, + preciseReflectedRegistrations)) { - preciseReflectedRegistrations.Add(preciseReflectedRegistration); continue; } @@ -155,6 +164,56 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator reflectionFallbackHandlerTypeMetadataName); } + /// + /// 尝试为单个 handler 接口选择无需程序集级 fallback 的注册表示。 + /// + /// 当前生成轮次的编译上下文。 + /// 正在分类的关闭 handler 接口。 + /// 生成代码是否可直接引用实现类型。 + /// 实现类型在生成源码中的全限定显示名。 + /// 实现类型用于日志输出的稳定显示名。 + /// 直接类型注册集合。 + /// 实现类型需要反射解析、接口可直接引用的注册集合。 + /// 接口类型需要运行时精确构造的注册集合。 + /// + /// 当当前接口已经被添加到某个静态注册集合时返回 ;否则调用方应记录 reflection fallback 元数据。 + /// + private static bool TryAddStaticHandlerRegistration( + Compilation compilation, + INamedTypeSymbol handlerInterface, + bool canReferenceImplementation, + string implementationTypeDisplayName, + string implementationLogName, + ImmutableArray.Builder registrations, + ImmutableArray.Builder reflectedImplementationRegistrations, + ImmutableArray.Builder preciseReflectedRegistrations) + { + var canReferenceHandlerInterface = CanReferenceFromGeneratedRegistry(compilation, handlerInterface); + if (canReferenceImplementation && canReferenceHandlerInterface) + { + registrations.Add(new HandlerRegistrationSpec( + handlerInterface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + implementationTypeDisplayName, + GetLogDisplayName(handlerInterface), + implementationLogName)); + return true; + } + + if (!canReferenceImplementation && canReferenceHandlerInterface) + { + reflectedImplementationRegistrations.Add(new ReflectedImplementationRegistrationSpec( + handlerInterface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + GetLogDisplayName(handlerInterface))); + return true; + } + + if (!TryCreatePreciseReflectedRegistration(compilation, handlerInterface, out var preciseReflectedRegistration)) + return false; + + preciseReflectedRegistrations.Add(preciseReflectedRegistration); + return true; + } + /// /// 执行 CQRS handler registry 生成管线的最终发射阶段,负责将候选 handler 分析结果汇总为单个 /// CqrsHandlerRegistry.g.cs,并在需要时附带程序集级 reflection fallback 元数据。 @@ -423,41 +482,17 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator if (type is INamedTypeSymbol genericNamedType && genericNamedType.IsGenericType && - !genericNamedType.IsUnboundGenericType && - TryCreateGenericTypeDefinitionReference(compilation, genericNamedType, - out var genericTypeDefinitionReference)) + !genericNamedType.IsUnboundGenericType) { - var genericTypeArguments = - ImmutableArray.CreateBuilder(genericNamedType.TypeArguments.Length); - foreach (var typeArgument in genericNamedType.TypeArguments) - { - if (!TryCreateRuntimeTypeReference(compilation, typeArgument, out var genericTypeArgumentReference)) - { - runtimeTypeReference = null; - return false; - } - - genericTypeArguments.Add(genericTypeArgumentReference!); - } - - runtimeTypeReference = RuntimeTypeReferenceSpec.FromConstructedGeneric( - genericTypeDefinitionReference!, - genericTypeArguments.ToImmutable()); - return true; + return TryCreateConstructedGenericRuntimeTypeReference( + compilation, + genericNamedType, + out runtimeTypeReference); } if (type is INamedTypeSymbol namedType) { - if (SymbolEqualityComparer.Default.Equals(namedType.ContainingAssembly, compilation.Assembly)) - { - runtimeTypeReference = RuntimeTypeReferenceSpec.FromReflectionLookup( - GetReflectionTypeMetadataName(namedType)); - return true; - } - - runtimeTypeReference = RuntimeTypeReferenceSpec.FromExternalReflectionLookup( - namedType.ContainingAssembly.Identity.ToString(), - GetReflectionTypeMetadataName(namedType)); + runtimeTypeReference = CreateNamedRuntimeTypeReference(compilation, namedType); return true; } @@ -465,6 +500,66 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator return false; } + /// + /// 为已构造泛型类型构造运行时类型引用,并递归验证每个泛型实参都可以稳定编码到生成输出中。 + /// + /// 当前生成轮次的编译上下文。 + /// 需要表示的已构造泛型类型。 + /// + /// 当方法返回 时,包含泛型定义和泛型实参的运行时重建描述。 + /// + /// 当泛型定义和全部泛型实参都能表达时返回 + private static bool TryCreateConstructedGenericRuntimeTypeReference( + Compilation compilation, + INamedTypeSymbol genericNamedType, + out RuntimeTypeReferenceSpec? runtimeTypeReference) + { + if (!TryCreateGenericTypeDefinitionReference( + compilation, + genericNamedType, + out var genericTypeDefinitionReference)) + { + runtimeTypeReference = null; + return false; + } + + var genericTypeArguments = + ImmutableArray.CreateBuilder(genericNamedType.TypeArguments.Length); + foreach (var typeArgument in genericNamedType.TypeArguments) + { + if (!TryCreateRuntimeTypeReference(compilation, typeArgument, out var genericTypeArgumentReference)) + { + runtimeTypeReference = null; + return false; + } + + genericTypeArguments.Add(genericTypeArgumentReference!); + } + + runtimeTypeReference = RuntimeTypeReferenceSpec.FromConstructedGeneric( + genericTypeDefinitionReference!, + genericTypeArguments.ToImmutable()); + return true; + } + + /// + /// 为无法直接书写的命名类型选择当前程序集反射查找或外部程序集反射查找表示。 + /// + /// 当前生成轮次的编译上下文。 + /// 需要在运行时解析的命名类型。 + /// 适合写入生成注册器的命名类型运行时引用。 + private static RuntimeTypeReferenceSpec CreateNamedRuntimeTypeReference( + Compilation compilation, + INamedTypeSymbol namedType) + { + if (SymbolEqualityComparer.Default.Equals(namedType.ContainingAssembly, compilation.Assembly)) + return RuntimeTypeReferenceSpec.FromReflectionLookup(GetReflectionTypeMetadataName(namedType)); + + return RuntimeTypeReferenceSpec.FromExternalReflectionLookup( + namedType.ContainingAssembly.Identity.ToString(), + GetReflectionTypeMetadataName(namedType)); + } + /// /// 为已构造泛型类型解析其泛型定义的运行时引用描述。 /// @@ -626,34 +721,56 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator GenerationEnvironment generationEnvironment, IReadOnlyList registrations, IReadOnlyList fallbackHandlerTypeMetadataNames) + { + var sourceShape = CreateGeneratedRegistrySourceShape(registrations); + var builder = new StringBuilder(); + AppendGeneratedSourcePreamble(builder, generationEnvironment, fallbackHandlerTypeMetadataNames); + AppendGeneratedRegistryType(builder, registrations, sourceShape); + return builder.ToString(); + } + + /// + /// 预先计算生成注册器需要的辅助分支,让主源码发射流程保持线性且避免重复扫描注册集合。 + /// + /// 已整理并排序的 handler 注册描述。 + /// 当前生成输出需要启用的结构分支。 + private static GeneratedRegistrySourceShape CreateGeneratedRegistrySourceShape( + IReadOnlyList registrations) { var hasReflectedImplementationRegistrations = registrations.Any(static registration => !registration.ReflectedImplementationRegistrations.IsDefaultOrEmpty); var hasPreciseReflectedRegistrations = registrations.Any(static registration => !registration.PreciseReflectedRegistrations.IsDefaultOrEmpty); + var hasReflectionTypeLookups = registrations.Any(static registration => + !string.IsNullOrWhiteSpace(registration.ReflectionTypeMetadataName)); var hasExternalAssemblyTypeLookups = registrations.Any(static registration => registration.PreciseReflectedRegistrations.Any(static preciseRegistration => preciseRegistration.ServiceTypeArguments.Any(ContainsExternalAssemblyTypeLookup))); - var builder = new StringBuilder(); + + return new GeneratedRegistrySourceShape( + hasReflectedImplementationRegistrations, + hasPreciseReflectedRegistrations, + hasReflectionTypeLookups, + hasExternalAssemblyTypeLookups); + } + + /// + /// 发射生成文件头、nullable 指令以及注册器所需的程序集级元数据特性。 + /// + /// 生成源码构造器。 + /// 当前轮次的生成环境。 + /// 需要程序集级 reflection fallback 的 handler 元数据名称。 + private static void AppendGeneratedSourcePreamble( + StringBuilder builder, + GenerationEnvironment generationEnvironment, + IReadOnlyList fallbackHandlerTypeMetadataNames) + { builder.AppendLine("// "); builder.AppendLine("#nullable enable"); builder.AppendLine(); if (generationEnvironment.SupportsReflectionFallbackAttribute && fallbackHandlerTypeMetadataNames.Count > 0) { - builder.Append("[assembly: global::"); - builder.Append(CqrsRuntimeNamespace); - builder.Append(".CqrsReflectionFallbackAttribute("); - for (var index = 0; index < fallbackHandlerTypeMetadataNames.Count; index++) - { - if (index > 0) - builder.Append(", "); - - builder.Append('"'); - builder.Append(EscapeStringLiteral(fallbackHandlerTypeMetadataNames[index])); - builder.Append('"'); - } - - builder.AppendLine(")]"); + AppendReflectionFallbackAttribute(builder, fallbackHandlerTypeMetadataNames); builder.AppendLine(); } @@ -664,7 +781,44 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.Append('.'); builder.Append(GeneratedTypeName); builder.AppendLine("))]"); + } + /// + /// 发射程序集级 reflection fallback 元数据特性,供运行时补齐生成阶段无法精确表达的 handler。 + /// + /// 生成源码构造器。 + /// 需要写入特性的 handler 元数据名称。 + private static void AppendReflectionFallbackAttribute( + StringBuilder builder, + IReadOnlyList fallbackHandlerTypeMetadataNames) + { + builder.Append("[assembly: global::"); + builder.Append(CqrsRuntimeNamespace); + builder.Append(".CqrsReflectionFallbackAttribute("); + for (var index = 0; index < fallbackHandlerTypeMetadataNames.Count; index++) + { + if (index > 0) + builder.Append(", "); + + builder.Append('"'); + builder.Append(EscapeStringLiteral(fallbackHandlerTypeMetadataNames[index])); + builder.Append('"'); + } + + builder.AppendLine(")]"); + } + + /// + /// 发射生成注册器类型本体,包括 Register 方法和运行时反射辅助方法。 + /// + /// 生成源码构造器。 + /// 已排序的 handler 注册描述。 + /// 当前输出需要启用的结构分支。 + private static void AppendGeneratedRegistryType( + StringBuilder builder, + IReadOnlyList registrations, + GeneratedRegistrySourceShape sourceShape) + { builder.AppendLine(); builder.Append("namespace "); builder.Append(GeneratedNamespace); @@ -676,6 +830,28 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.Append(CqrsRuntimeNamespace); builder.AppendLine(".ICqrsHandlerRegistry"); builder.AppendLine("{"); + AppendRegisterMethod(builder, registrations, sourceShape); + + if (sourceShape.HasExternalAssemblyTypeLookups) + { + builder.AppendLine(); + AppendReflectionHelpers(builder, sourceShape.HasExternalAssemblyTypeLookups); + } + + builder.AppendLine("}"); + } + + /// + /// 发射注册器的 Register 方法,保持直接注册和反射注册之间的原始稳定排序。 + /// + /// 生成源码构造器。 + /// 已排序的 handler 注册描述。 + /// 当前输出需要启用的结构分支。 + private static void AppendRegisterMethod( + StringBuilder builder, + IReadOnlyList registrations, + GeneratedRegistrySourceShape sourceShape) + { builder.Append( " public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::"); builder.Append(LoggingNamespace); @@ -685,9 +861,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(services));"); builder.AppendLine(" if (logger is null)"); builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(logger));"); - if (hasReflectedImplementationRegistrations || hasPreciseReflectedRegistrations || - registrations.Any(static registration => - !string.IsNullOrWhiteSpace(registration.ReflectionTypeMetadataName))) + if (sourceShape.RequiresRegistryAssemblyVariable) { builder.AppendLine(); builder.Append(" var registryAssembly = typeof(global::"); @@ -715,15 +889,6 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator } builder.AppendLine(" }"); - - if (hasExternalAssemblyTypeLookups) - { - builder.AppendLine(); - AppendReflectionHelpers(builder, hasExternalAssemblyTypeLookups); - } - - builder.AppendLine("}"); - return builder.ToString(); } private static void AppendDirectRegistrations( @@ -732,37 +897,108 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator { foreach (var directRegistration in registration.DirectRegistrations) { - builder.AppendLine( - " global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient("); - builder.AppendLine(" services,"); - builder.Append(" typeof("); - builder.Append(directRegistration.HandlerInterfaceDisplayName); - builder.AppendLine("),"); - builder.Append(" typeof("); - builder.Append(directRegistration.ImplementationTypeDisplayName); - builder.AppendLine("));"); - builder.Append(" logger.Debug(\"Registered CQRS handler "); - builder.Append(EscapeStringLiteral(directRegistration.ImplementationLogName)); - builder.Append(" as "); - builder.Append(EscapeStringLiteral(directRegistration.HandlerInterfaceLogName)); - builder.AppendLine(".\");"); + AppendServiceRegistration( + builder, + $"typeof({directRegistration.HandlerInterfaceDisplayName})", + $"typeof({directRegistration.ImplementationTypeDisplayName})", + " "); + AppendRegistrationLog( + builder, + directRegistration.ImplementationLogName, + directRegistration.HandlerInterfaceLogName, + " "); } } + /// + /// 发射 AddTransient 调用,调用方负责传入已经按当前分支解析好的 service 和 implementation 表达式。 + /// + /// 生成源码构造器。 + /// 生成代码中的服务类型表达式。 + /// 生成代码中的实现类型表达式。 + /// 当前生成语句的缩进。 + private static void AppendServiceRegistration( + StringBuilder builder, + string serviceTypeExpression, + string implementationTypeExpression, + string indent) + { + builder.Append(indent); + builder.AppendLine("global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient("); + builder.Append(indent); + builder.AppendLine(" services,"); + builder.Append(indent); + builder.Append(" "); + builder.Append(serviceTypeExpression); + builder.AppendLine(","); + builder.Append(indent); + builder.Append(" "); + builder.Append(implementationTypeExpression); + builder.AppendLine(");"); + } + + /// + /// 发射与注册语句配套的调试日志,保持所有生成注册路径的日志文本完全一致。 + /// + /// 生成源码构造器。 + /// 实现类型日志名。 + /// handler 接口日志名。 + /// 当前生成语句的缩进。 + private static void AppendRegistrationLog( + StringBuilder builder, + string implementationLogName, + string handlerInterfaceLogName, + string indent) + { + builder.Append(indent); + builder.Append("logger.Debug(\"Registered CQRS handler "); + builder.Append(EscapeStringLiteral(implementationLogName)); + builder.Append(" as "); + builder.Append(EscapeStringLiteral(handlerInterfaceLogName)); + builder.AppendLine(".\");"); + } + private static void AppendOrderedImplementationRegistrations( StringBuilder builder, ImplementationRegistrationSpec registration, int registrationIndex) { - var orderedRegistrations = - new List<(string HandlerInterfaceLogName, OrderedRegistrationKind Kind, int Index)>( - registration.DirectRegistrations.Length + - registration.ReflectedImplementationRegistrations.Length + - registration.PreciseReflectedRegistrations.Length); + var orderedRegistrations = CreateOrderedRegistrations(registration); + var implementationVariableName = $"implementationType{registrationIndex}"; + AppendImplementationTypeVariable(builder, registration, implementationVariableName); + builder.Append(" if ("); + builder.Append(implementationVariableName); + builder.AppendLine(" is not null)"); + builder.AppendLine(" {"); + + foreach (var orderedRegistration in orderedRegistrations) + { + AppendOrderedRegistration( + builder, + registration, + orderedRegistration, + registrationIndex, + implementationVariableName); + } + + builder.AppendLine(" }"); + } + + /// + /// 合并直接注册、实现类型反射注册和精确反射注册,并按 handler 接口日志名排序以保持生成输出稳定。 + /// + /// 单个实现类型聚合后的注册描述。 + /// 带有来源类型和原始索引的有序注册列表。 + private static List CreateOrderedRegistrations(ImplementationRegistrationSpec registration) + { + var orderedRegistrations = new List( + registration.DirectRegistrations.Length + + registration.ReflectedImplementationRegistrations.Length + + registration.PreciseReflectedRegistrations.Length); for (var directIndex = 0; directIndex < registration.DirectRegistrations.Length; directIndex++) { - orderedRegistrations.Add(( + orderedRegistrations.Add(new OrderedRegistrationSpec( registration.DirectRegistrations[directIndex].HandlerInterfaceLogName, OrderedRegistrationKind.Direct, directIndex)); @@ -772,7 +1008,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator reflectedIndex < registration.ReflectedImplementationRegistrations.Length; reflectedIndex++) { - orderedRegistrations.Add(( + orderedRegistrations.Add(new OrderedRegistrationSpec( registration.ReflectedImplementationRegistrations[reflectedIndex].HandlerInterfaceLogName, OrderedRegistrationKind.ReflectedImplementation, reflectedIndex)); @@ -782,7 +1018,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator preciseIndex < registration.PreciseReflectedRegistrations.Length; preciseIndex++) { - orderedRegistrations.Add(( + orderedRegistrations.Add(new OrderedRegistrationSpec( registration.PreciseReflectedRegistrations[preciseIndex].HandlerInterfaceLogName, OrderedRegistrationKind.PreciseReflected, preciseIndex)); @@ -790,8 +1026,20 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator orderedRegistrations.Sort(static (left, right) => StringComparer.Ordinal.Compare(left.HandlerInterfaceLogName, right.HandlerInterfaceLogName)); + return orderedRegistrations; + } - var implementationVariableName = $"implementationType{registrationIndex}"; + /// + /// 发射实现类型变量。公开类型直接使用 typeof,不可直接引用的实现类型则从当前程序集反射解析。 + /// + /// 生成源码构造器。 + /// 单个实现类型聚合后的注册描述。 + /// 生成代码中的实现类型变量名。 + private static void AppendImplementationTypeVariable( + StringBuilder builder, + ImplementationRegistrationSpec registration, + string implementationVariableName) + { if (string.IsNullOrWhiteSpace(registration.ReflectionTypeMetadataName)) { builder.Append(" var "); @@ -808,71 +1056,131 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.Append(EscapeStringLiteral(registration.ReflectionTypeMetadataName!)); builder.AppendLine("\", throwOnError: false, ignoreCase: false);"); } + } - builder.Append(" if ("); - builder.Append(implementationVariableName); - builder.AppendLine(" is not null)"); - builder.AppendLine(" {"); - - foreach (var orderedRegistration in orderedRegistrations) + /// + /// 根据注册来源发射单条有序注册,确保混合直接和反射路径时仍按 handler 接口名稳定输出。 + /// + /// 生成源码构造器。 + /// 单个实现类型聚合后的注册描述。 + /// 带来源类型和原始索引的排序项。 + /// 实现类型在整体注册列表中的索引,用于生成稳定变量名。 + /// 生成代码中的实现类型变量名。 + private static void AppendOrderedRegistration( + StringBuilder builder, + ImplementationRegistrationSpec registration, + OrderedRegistrationSpec orderedRegistration, + int registrationIndex, + string implementationVariableName) + { + switch (orderedRegistration.Kind) { - switch (orderedRegistration.Kind) - { - case OrderedRegistrationKind.Direct: - var directRegistration = registration.DirectRegistrations[orderedRegistration.Index]; - builder.AppendLine( - " global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient("); - builder.AppendLine(" services,"); - builder.Append(" typeof("); - builder.Append(directRegistration.HandlerInterfaceDisplayName); - builder.AppendLine("),"); - builder.Append(" "); - builder.Append(implementationVariableName); - builder.AppendLine(");"); - builder.Append(" logger.Debug(\"Registered CQRS handler "); - builder.Append(EscapeStringLiteral(registration.ImplementationLogName)); - builder.Append(" as "); - builder.Append(EscapeStringLiteral(directRegistration.HandlerInterfaceLogName)); - builder.AppendLine(".\");"); - break; - case OrderedRegistrationKind.ReflectedImplementation: - var reflectedRegistration = - registration.ReflectedImplementationRegistrations[orderedRegistration.Index]; - builder.AppendLine( - " global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient("); - builder.AppendLine(" services,"); - builder.Append(" typeof("); - builder.Append(reflectedRegistration.HandlerInterfaceDisplayName); - builder.AppendLine("),"); - builder.Append(" "); - builder.Append(implementationVariableName); - builder.AppendLine(");"); - builder.Append(" logger.Debug(\"Registered CQRS handler "); - builder.Append(EscapeStringLiteral(registration.ImplementationLogName)); - builder.Append(" as "); - builder.Append(EscapeStringLiteral(reflectedRegistration.HandlerInterfaceLogName)); - builder.AppendLine(".\");"); - break; - case OrderedRegistrationKind.PreciseReflected: - var preciseRegistration = registration.PreciseReflectedRegistrations[orderedRegistration.Index]; - var registrationVariablePrefix = $"serviceType{registrationIndex}_{orderedRegistration.Index}"; - AppendPreciseReflectedTypeResolution( - builder, - preciseRegistration.ServiceTypeArguments, - registrationVariablePrefix, - implementationVariableName, - preciseRegistration.OpenHandlerTypeDisplayName, - registration.ImplementationLogName, - preciseRegistration.HandlerInterfaceLogName, - 3); - break; - default: - throw new InvalidOperationException( - $"Unsupported ordered CQRS registration kind {orderedRegistration.Kind}."); - } + case OrderedRegistrationKind.Direct: + AppendOrderedDirectRegistration( + builder, + registration, + registration.DirectRegistrations[orderedRegistration.Index], + implementationVariableName); + break; + case OrderedRegistrationKind.ReflectedImplementation: + AppendOrderedReflectedImplementationRegistration( + builder, + registration, + registration.ReflectedImplementationRegistrations[orderedRegistration.Index], + implementationVariableName); + break; + case OrderedRegistrationKind.PreciseReflected: + AppendOrderedPreciseReflectedRegistration( + builder, + registration, + registration.PreciseReflectedRegistrations[orderedRegistration.Index], + registrationIndex, + orderedRegistration.Index, + implementationVariableName); + break; + default: + throw new InvalidOperationException( + $"Unsupported ordered CQRS registration kind {orderedRegistration.Kind}."); } + } - builder.AppendLine(" }"); + /// + /// 发射实现类型已通过变量解析、handler 接口可直接引用的直接注册语句。 + /// + /// 生成源码构造器。 + /// 单个实现类型聚合后的注册描述。 + /// 当前直接注册项。 + /// 生成代码中的实现类型变量名。 + private static void AppendOrderedDirectRegistration( + StringBuilder builder, + ImplementationRegistrationSpec registration, + HandlerRegistrationSpec directRegistration, + string implementationVariableName) + { + AppendServiceRegistration( + builder, + $"typeof({directRegistration.HandlerInterfaceDisplayName})", + implementationVariableName, + " "); + AppendRegistrationLog( + builder, + registration.ImplementationLogName, + directRegistration.HandlerInterfaceLogName, + " "); + } + + /// + /// 发射实现类型需要反射解析、handler 接口可直接引用的注册语句。 + /// + /// 生成源码构造器。 + /// 单个实现类型聚合后的注册描述。 + /// 当前实现类型反射注册项。 + /// 生成代码中的实现类型变量名。 + private static void AppendOrderedReflectedImplementationRegistration( + StringBuilder builder, + ImplementationRegistrationSpec registration, + ReflectedImplementationRegistrationSpec reflectedRegistration, + string implementationVariableName) + { + AppendServiceRegistration( + builder, + $"typeof({reflectedRegistration.HandlerInterfaceDisplayName})", + implementationVariableName, + " "); + AppendRegistrationLog( + builder, + registration.ImplementationLogName, + reflectedRegistration.HandlerInterfaceLogName, + " "); + } + + /// + /// 发射 handler 接口需要运行时精确构造的注册语句。 + /// + /// 生成源码构造器。 + /// 单个实现类型聚合后的注册描述。 + /// 当前精确反射注册项。 + /// 实现类型在整体注册列表中的索引。 + /// 当前注册项在原始精确反射注册集合中的索引。 + /// 生成代码中的实现类型变量名。 + private static void AppendOrderedPreciseReflectedRegistration( + StringBuilder builder, + ImplementationRegistrationSpec registration, + PreciseReflectedRegistrationSpec preciseRegistration, + int registrationIndex, + int orderedRegistrationIndex, + string implementationVariableName) + { + var registrationVariablePrefix = $"serviceType{registrationIndex}_{orderedRegistrationIndex}"; + AppendPreciseReflectedTypeResolution( + builder, + preciseRegistration.ServiceTypeArguments, + registrationVariablePrefix, + implementationVariableName, + preciseRegistration.OpenHandlerTypeDisplayName, + registration.ImplementationLogName, + preciseRegistration.HandlerInterfaceLogName, + 3); } private static void AppendPreciseReflectedTypeResolution( @@ -886,10 +1194,50 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator int indentLevel) { var indent = new string(' ', indentLevel * 4); - var nestedIndent = new string(' ', (indentLevel + 1) * 4); - var resolvedArgumentNames = new string[serviceTypeArguments.Length]; var reflectedArgumentNames = new List(); + var resolvedArgumentNames = AppendServiceTypeArgumentResolutions( + builder, + serviceTypeArguments, + registrationVariablePrefix, + reflectedArgumentNames, + indent); + if (reflectedArgumentNames.Count > 0) + indent = AppendReflectedArgumentGuardStart(builder, reflectedArgumentNames, indent); + + AppendClosedGenericServiceTypeCreation( + builder, + registrationVariablePrefix, + openHandlerTypeDisplayName, + resolvedArgumentNames, + indent); + AppendServiceRegistration(builder, registrationVariablePrefix, implementationVariableName, indent); + AppendRegistrationLog(builder, implementationLogName, handlerInterfaceLogName, indent); + + if (reflectedArgumentNames.Count > 0) + { + builder.Append(new string(' ', indentLevel * 4)); + builder.AppendLine("}"); + } + } + + /// + /// 递归发射每个 handler 泛型实参的运行时类型解析表达式。 + /// + /// 生成源码构造器。 + /// handler 服务类型的运行时泛型实参描述。 + /// 当前注册项的稳定变量名前缀。 + /// 需要空值检查的反射解析变量集合。 + /// 当前生成语句的缩进。 + /// 可传给 MakeGenericType 的实参表达式。 + private static string[] AppendServiceTypeArgumentResolutions( + StringBuilder builder, + ImmutableArray serviceTypeArguments, + string registrationVariablePrefix, + ICollection reflectedArgumentNames, + string indent) + { + var resolvedArgumentNames = new string[serviceTypeArguments.Length]; for (var argumentIndex = 0; argumentIndex < serviceTypeArguments.Length; argumentIndex++) { resolvedArgumentNames[argumentIndex] = AppendRuntimeTypeReferenceResolution( @@ -900,32 +1248,60 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator indent); } - if (reflectedArgumentNames.Count > 0) + return resolvedArgumentNames; + } + + /// + /// 为运行时反射解析出的泛型实参发射空值保护块,避免生成注册器注册无法完整构造的服务类型。 + /// + /// 生成源码构造器。 + /// 需要参与空值检查的变量名。 + /// 保护块开始前的缩进。 + /// 保护块内部应使用的下一层缩进。 + private static string AppendReflectedArgumentGuardStart( + StringBuilder builder, + IReadOnlyList reflectedArgumentNames, + string indent) + { + builder.Append(indent); + builder.Append("if ("); + for (var index = 0; index < reflectedArgumentNames.Count; index++) { - builder.Append(indent); - builder.Append("if ("); - for (var index = 0; index < reflectedArgumentNames.Count; index++) - { - if (index > 0) - builder.Append(" && "); + if (index > 0) + builder.Append(" && "); - builder.Append(reflectedArgumentNames[index]); - builder.Append(" is not null"); - } - - builder.AppendLine(")"); - builder.Append(indent); - builder.AppendLine("{"); - indent = nestedIndent; + builder.Append(reflectedArgumentNames[index]); + builder.Append(" is not null"); } + builder.AppendLine(")"); + builder.Append(indent); + builder.AppendLine("{"); + return $"{indent} "; + } + + /// + /// 发射关闭 handler 服务类型的 MakeGenericType 构造语句。 + /// + /// 生成源码构造器。 + /// 生成代码中的服务类型变量名。 + /// 开放 handler 接口类型显示名。 + /// 已解析的泛型实参表达式。 + /// 当前生成语句的缩进。 + private static void AppendClosedGenericServiceTypeCreation( + StringBuilder builder, + string registrationVariablePrefix, + string openHandlerTypeDisplayName, + IReadOnlyList resolvedArgumentNames, + string indent) + { builder.Append(indent); builder.Append("var "); builder.Append(registrationVariablePrefix); builder.Append(" = typeof("); builder.Append(openHandlerTypeDisplayName); builder.Append(").MakeGenericType("); - for (var index = 0; index < resolvedArgumentNames.Length; index++) + for (var index = 0; index < resolvedArgumentNames.Count; index++) { if (index > 0) builder.Append(", "); @@ -934,31 +1310,6 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator } builder.AppendLine(");"); - builder.Append(indent); - builder.AppendLine( - "global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient("); - builder.Append(indent); - builder.AppendLine(" services,"); - builder.Append(indent); - builder.Append(" "); - builder.Append(registrationVariablePrefix); - builder.AppendLine(","); - builder.Append(indent); - builder.Append(" "); - builder.Append(implementationVariableName); - builder.AppendLine(");"); - builder.Append(indent); - builder.Append("logger.Debug(\"Registered CQRS handler "); - builder.Append(EscapeStringLiteral(implementationLogName)); - builder.Append(" as "); - builder.Append(EscapeStringLiteral(handlerInterfaceLogName)); - builder.AppendLine(".\");"); - - if (reflectedArgumentNames.Count > 0) - { - builder.Append(new string(' ', indentLevel * 4)); - builder.AppendLine("}"); - } } private static string AppendRuntimeTypeReferenceResolution( @@ -972,57 +1323,145 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator return $"typeof({runtimeTypeReference.TypeDisplayName})"; if (runtimeTypeReference.ArrayElementTypeReference is not null) - { - var elementExpression = AppendRuntimeTypeReferenceResolution( + return AppendArrayRuntimeTypeReferenceResolution( builder, - runtimeTypeReference.ArrayElementTypeReference, - $"{variableBaseName}Element", + runtimeTypeReference, + variableBaseName, reflectedArgumentNames, indent); - return runtimeTypeReference.ArrayRank == 1 - ? $"{elementExpression}.MakeArrayType()" - : $"{elementExpression}.MakeArrayType({runtimeTypeReference.ArrayRank})"; - } - if (runtimeTypeReference.PointerElementTypeReference is not null) - { - var pointedAtExpression = AppendRuntimeTypeReferenceResolution( + return AppendPointerRuntimeTypeReferenceResolution( builder, - runtimeTypeReference.PointerElementTypeReference, - $"{variableBaseName}PointedAt", + runtimeTypeReference, + variableBaseName, reflectedArgumentNames, indent); - return $"{pointedAtExpression}.MakePointerType()"; - } - if (runtimeTypeReference.GenericTypeDefinitionReference is not null) - { - var genericTypeDefinitionExpression = AppendRuntimeTypeReferenceResolution( + return AppendConstructedGenericRuntimeTypeReferenceResolution( builder, - runtimeTypeReference.GenericTypeDefinitionReference, - $"{variableBaseName}GenericDefinition", + runtimeTypeReference, + variableBaseName, reflectedArgumentNames, indent); - var genericArgumentExpressions = new string[runtimeTypeReference.GenericTypeArguments.Length]; - for (var argumentIndex = 0; - argumentIndex < runtimeTypeReference.GenericTypeArguments.Length; - argumentIndex++) - { - genericArgumentExpressions[argumentIndex] = AppendRuntimeTypeReferenceResolution( - builder, - runtimeTypeReference.GenericTypeArguments[argumentIndex], - $"{variableBaseName}GenericArgument{argumentIndex}", - reflectedArgumentNames, - indent); - } - return - $"{genericTypeDefinitionExpression}.MakeGenericType({string.Join(", ", genericArgumentExpressions)})"; + return AppendReflectionRuntimeTypeReferenceResolution( + builder, + runtimeTypeReference, + variableBaseName, + reflectedArgumentNames, + indent); + } + + /// + /// 发射数组类型引用的运行时重建表达式。 + /// + /// 生成源码构造器。 + /// 数组类型引用描述。 + /// 用于递归生成变量名的稳定前缀。 + /// 需要空值检查的反射解析变量集合。 + /// 当前生成语句的缩进。 + /// 数组类型表达式。 + private static string AppendArrayRuntimeTypeReferenceResolution( + StringBuilder builder, + RuntimeTypeReferenceSpec runtimeTypeReference, + string variableBaseName, + ICollection reflectedArgumentNames, + string indent) + { + var elementExpression = AppendRuntimeTypeReferenceResolution( + builder, + runtimeTypeReference.ArrayElementTypeReference!, + $"{variableBaseName}Element", + reflectedArgumentNames, + indent); + + return runtimeTypeReference.ArrayRank == 1 + ? $"{elementExpression}.MakeArrayType()" + : $"{elementExpression}.MakeArrayType({runtimeTypeReference.ArrayRank})"; + } + + /// + /// 发射指针类型引用的运行时重建表达式。 + /// + /// 生成源码构造器。 + /// 指针类型引用描述。 + /// 用于递归生成变量名的稳定前缀。 + /// 需要空值检查的反射解析变量集合。 + /// 当前生成语句的缩进。 + /// 指针类型表达式。 + private static string AppendPointerRuntimeTypeReferenceResolution( + StringBuilder builder, + RuntimeTypeReferenceSpec runtimeTypeReference, + string variableBaseName, + ICollection reflectedArgumentNames, + string indent) + { + var pointedAtExpression = AppendRuntimeTypeReferenceResolution( + builder, + runtimeTypeReference.PointerElementTypeReference!, + $"{variableBaseName}PointedAt", + reflectedArgumentNames, + indent); + + return $"{pointedAtExpression}.MakePointerType()"; + } + + /// + /// 发射已构造泛型类型引用的运行时重建表达式。 + /// + /// 生成源码构造器。 + /// 已构造泛型类型引用描述。 + /// 用于递归生成变量名的稳定前缀。 + /// 需要空值检查的反射解析变量集合。 + /// 当前生成语句的缩进。 + /// 已构造泛型类型表达式。 + private static string AppendConstructedGenericRuntimeTypeReferenceResolution( + StringBuilder builder, + RuntimeTypeReferenceSpec runtimeTypeReference, + string variableBaseName, + ICollection reflectedArgumentNames, + string indent) + { + var genericTypeDefinitionExpression = AppendRuntimeTypeReferenceResolution( + builder, + runtimeTypeReference.GenericTypeDefinitionReference!, + $"{variableBaseName}GenericDefinition", + reflectedArgumentNames, + indent); + var genericArgumentExpressions = new string[runtimeTypeReference.GenericTypeArguments.Length]; + for (var argumentIndex = 0; + argumentIndex < runtimeTypeReference.GenericTypeArguments.Length; + argumentIndex++) + { + genericArgumentExpressions[argumentIndex] = AppendRuntimeTypeReferenceResolution( + builder, + runtimeTypeReference.GenericTypeArguments[argumentIndex], + $"{variableBaseName}GenericArgument{argumentIndex}", + reflectedArgumentNames, + indent); } - var reflectionTypeMetadataName = runtimeTypeReference.ReflectionTypeMetadataName!; + return $"{genericTypeDefinitionExpression}.MakeGenericType({string.Join(", ", genericArgumentExpressions)})"; + } + + /// + /// 发射命名类型的运行时反射查找语句,并返回后续服务类型构造应引用的变量名。 + /// + /// 生成源码构造器。 + /// 反射查找类型引用描述。 + /// 生成代码中的反射变量名。 + /// 需要空值检查的反射解析变量集合。 + /// 当前生成语句的缩进。 + /// 生成代码中的反射变量名。 + private static string AppendReflectionRuntimeTypeReferenceResolution( + StringBuilder builder, + RuntimeTypeReferenceSpec runtimeTypeReference, + string variableBaseName, + ICollection reflectedArgumentNames, + string indent) + { reflectedArgumentNames.Add(variableBaseName); builder.Append(indent); builder.Append("var "); @@ -1030,7 +1469,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator if (string.IsNullOrWhiteSpace(runtimeTypeReference.ReflectionAssemblyName)) { builder.Append(" = registryAssembly.GetType(\""); - builder.Append(EscapeStringLiteral(reflectionTypeMetadataName)); + builder.Append(EscapeStringLiteral(runtimeTypeReference.ReflectionTypeMetadataName!)); builder.AppendLine("\", throwOnError: false, ignoreCase: false);"); } else @@ -1038,7 +1477,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.Append(" = ResolveReferencedAssemblyType(\""); builder.Append(EscapeStringLiteral(runtimeTypeReference.ReflectionAssemblyName!)); builder.Append("\", \""); - builder.Append(EscapeStringLiteral(reflectionTypeMetadataName)); + builder.Append(EscapeStringLiteral(runtimeTypeReference.ReflectionTypeMetadataName!)); builder.AppendLine("\");"); } @@ -1144,6 +1583,23 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator string HandlerInterfaceDisplayName, string HandlerInterfaceLogName); + private readonly record struct OrderedRegistrationSpec( + string HandlerInterfaceLogName, + OrderedRegistrationKind Kind, + int Index); + + private readonly record struct GeneratedRegistrySourceShape( + bool HasReflectedImplementationRegistrations, + bool HasPreciseReflectedRegistrations, + bool HasReflectionTypeLookups, + bool HasExternalAssemblyTypeLookups) + { + public bool RequiresRegistryAssemblyVariable => + HasReflectedImplementationRegistrations || + HasPreciseReflectedRegistrations || + HasReflectionTypeLookups; + } + /// /// 标记某条 handler 注册语句在生成阶段采用的表达策略。 /// 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 ffe8d6ec..b13e0e74 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 @@ -7,22 +7,25 @@ ## 当前恢复点 -- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-017` -- 当前阶段:`Phase 17` +- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-018` +- 当前阶段:`Phase 18` - 当前焦点: - 已完成 `GFramework.Core` 当前 `MA0016` / `MA0002` / `MA0015` / `MA0077` 低风险收口批次 - 已复核 `net10.0` 下的 `MA0158` 基线:`GFramework.Core` / `GFramework.Cqrs` 当前共有 `16` 个 object lock 建议点,属于跨 target 兼容性风险,不在本轮直接批量替换 - 已完成 `GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs` 的剩余 `MA0051` 结构拆分,生成输出保持不变 + - 已完成 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs` 的 `MA0051` 结构拆分,生成输出保持不变 - `LoggingConfiguration`、`FilterConfiguration` 与 `CollectionExtensions` 已改用集合抽象接口,并保留内部具体集合默认值 - `CoroutineScheduler` 的 tag/group 字典已显式使用 `StringComparer.Ordinal`,保持既有区分大小写语义 - `EasyEvents.AddEvent()` 的重复注册路径已改为状态冲突异常,避免把泛型类型参数伪装成方法参数名 - `Option` 已声明 `IEquatable>`,与已有强类型 `Equals(Option)` 契约对齐 - 当前 `GFramework.Core` `net8.0` warnings-only 基线已降到 `0` 条 - 当前 `GFramework.Core.SourceGenerators` warnings-only 基线已降到 `0` 条 + - 当前 `GFramework.Cqrs.SourceGenerators` warnings-only 基线已降到 `0` 条 - `GFramework.Godot` 的 `Timing.cs` 已同步适配新事件签名,但当前 worktree 的 Godot restore 资产仍受 Windows fallback package folder 干扰,独立 build 需在修复资产后补跑 - 后续继续按 warning 类型和数量批处理,而不是回退到按单文件切片推进 - - 下一轮默认继续评估跨 target 的 `MA0158` 锁替换风险,或转向其他 source generator / test warning 热点 + - 下一轮默认转向 `GFramework.Game.SourceGenerators` 的 `MA0006` / `MA0051` 热点,或继续评估跨 target 的 `MA0158` + 锁替换风险 - 单次 `boot` 的工作树改动上限控制在约 `100` 个文件以内,避免 recovery context 与 review 面同时失控 - 若任务边界互不冲突,允许使用不同模型的 subagent 并行处理不同 warning 类型或不同目录,但必须遵守显式 ownership @@ -41,6 +44,7 @@ 等待满默认超时并返回 `false` 的竞态,并通过整包 `GFramework.Core.Tests` 重新验证 - 已完成当前 `GFramework.Core` `net8.0` 剩余低风险 analyzer warning 批次;warnings-only 基线已降到 `0` 条 - 已完成 `GFramework.Core.SourceGenerators` 中 `ContextAwareGenerator` 的剩余 `MA0051` 收口;warnings-only 基线已降到 `0` 条 +- 已完成 `GFramework.Cqrs.SourceGenerators` 中 `CqrsHandlerRegistryGenerator` 的剩余 `MA0051` 收口;warnings-only 基线已降到 `0` 条 ## 当前活跃事实 @@ -75,6 +79,8 @@ warnings-only build 与 focused tests 验证配置反序列化、集合扩展、事件重复注册、`Option` 相等性和协程 tag/group 语义 - `RP-017` 复核 `MA0158` 当前仍是跨 target 锁类型迁移问题,因此先收口单点 `ContextAwareGenerator` `MA0051`, 并通过 source generator 项目 build 与 `ContextAwareGeneratorSnapshotTests` 验证生成输出未回归 +- `RP-018` 暂缓跨 target `MA0158`,转入 `GFramework.Cqrs.SourceGenerators` 的单文件结构性 warning; + 通过拆分 handler 分析、运行时类型引用构造、注册器源码发射与精确反射注册输出阶段,清空该项目当前 `MA0051` - 当前工作树分支 `fix/analyzer-warning-reduction-batch` 已在 `ai-plan/public/README.md` 建立 topic 映射 ## 当前风险 @@ -88,8 +94,8 @@ - net10 专属 warning 风险:`MA0158` 建议使用 `System.Threading.Lock`,但项目多 target 时需要确认兼容边界 - 缓解措施:下一轮先按 target framework 与 API 可用性评估,不直接批量替换共享源码中的 `object` lock - source generator warning 外溢风险:运行 `GFramework.SourceGenerators.Tests` 会构建相邻 generator/test 项目并显示既有 - `GFramework.Cqrs.SourceGenerators`、`GFramework.Game.SourceGenerators` 与测试项目 warning - - 缓解措施:本轮以 `GFramework.Core.SourceGenerators` 独立 warnings-only build 作为主验收,并用 focused snapshot test + `GFramework.Game.SourceGenerators` 与测试项目 warning + - 缓解措施:本轮以 `GFramework.Cqrs.SourceGenerators` 独立 warnings-only build 作为主验收,并用 focused generator test 验证行为;后续若处理相邻 generator warning,应另开明确切片 - Godot 资产文件环境风险:当前 worktree 的 `GFramework.Godot` restore/build 仍会命中 Windows fallback package folder - 缓解措施:后续若继续触达 Godot 模块,先用 Linux 侧 restore 资产或 Windows-hosted 构建链刷新该项目,再补跑定向 build @@ -185,13 +191,21 @@ - 结果:`0 Warning(s)`,`0 Error(s)`;`ContextAwareGenerator.cs` 已不再出现 `MA0051` - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~ContextAwareGeneratorSnapshotTests" -m:1 -p:RestoreFallbackFolders="" -nologo` - 结果:`1 Passed`,`0 Failed`;测试构建仍显示相邻 source generator 和测试项目的既有 analyzer warning +- `RP-018` 的验证结果: + - `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"` + - 结果:`0 Warning(s)`,`0 Error(s)`;`CqrsHandlerRegistryGenerator.cs` 当前 `MA0051` 已清零 + - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~CqrsHandlerRegistryGeneratorTests -m:1 -p:RestoreFallbackFolders= -nologo` + - 结果:`14 Passed`,`0 Failed` + - 说明:该 test project 构建仍显示 `GFramework.Game.SourceGenerators` 与测试项目中的既有 analyzer warning;本轮关注的 + `GFramework.Cqrs.SourceGenerators` 独立 build 已清零 - active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史 ## 下一步 1. 若要继续该主题,先读 active tracking,再按需展开历史归档中的 warning 热点与验证记录 -2. 下一轮优先评估 `net10.0` 下的 `MA0158` 是否能通过条件编译或目标框架特定源码安全推进 -3. 若暂不推进 `MA0158`,可转入 `GFramework.Cqrs.SourceGenerators` 或 `GFramework.Game.SourceGenerators` 的剩余 - `MA0051` / `MA0006` 热点,但应单独建立文件 ownership 和验证范围 +2. 下一轮优先转入 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs` 的 `MA0006` 低风险批次,再评估是否继续拆分 + 该文件的 `MA0051` +3. 若改回推进 `MA0158`,先设计 `net8.0` / `net9.0` / `net10.0` 多 target 条件编译方案,不直接批量替换共享源码中的 + `object` lock 4. 若后续继续改动 `GFramework.Godot`,先修复该项目的 Linux 侧 restore 资产,再补跑独立 build 5. 若本主题确认暂缓,可保持当前归档状态,不需要再恢复 `local-plan/` 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 ae5fa38d..b567a7e5 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,31 @@ # Analyzer Warning Reduction 追踪 +## 2026-04-22 — RP-018 + +### 阶段:`CqrsHandlerRegistryGenerator` 剩余 `MA0051` 收口(RP-018) + +- 启动复核: + - 当前 worktree 仍映射到 `analyzer-warning-reduction` active topic + - `MA0158` 锁迁移仍然跨 `GFramework.Core` / `GFramework.Cqrs` 多 target 共享源码,继续视为需要单独设计的兼容性问题 + - `GFramework.Cqrs.SourceGenerators` warnings-only build 复现 `CqrsHandlerRegistryGenerator.cs` 的 `6` 个 `MA0051` +- 决策: + - 本轮暂缓 `MA0158`,转入单文件、可由生成器测试覆盖的 `GFramework.Cqrs.SourceGenerators` 结构拆分 + - 未使用 subagent;critical path 是本地复现 warning、拆分源码发射流程并用 focused generator tests 验证输出未变 +- 实施调整: + - 将 handler candidate 分析拆为接口收集、候选构造和单接口注册分类阶段 + - 将运行时类型引用构造拆为已构造泛型、命名类型反射查找等独立 helper + - 将注册器源码生成拆为文件头、程序集特性、注册器类型、`Register` 方法和服务注册日志发射 helper + - 将有序注册与精确反射注册输出拆为独立阶段,保留原有排序和生成文本形状 +- 验证结果: + - `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"` + - 结果:`0 Warning(s)`,`0 Error(s)` + - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~CqrsHandlerRegistryGeneratorTests -m:1 -p:RestoreFallbackFolders= -nologo` + - 结果:`14 Passed`,`0 Failed` + - 说明:测试项目构建仍显示 `GFramework.Game.SourceGenerators` 与测试项目中的既有 analyzer warning;不属于本轮写集 +- 下一步建议: + - 继续该主题时,优先处理 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs` 的 `MA0006` 低风险批次 + - 若回到 `MA0158`,先设计多 target 条件编译方案,再考虑替换共享源码中的 `object` lock + ## 2026-04-22 — RP-017 ### 阶段:`ContextAwareGenerator` 剩余 `MA0051` 收口(RP-017) From 78a23bf53a00e8fe31b204c40786216e23b855f2 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 22 Apr 2026 10:02:55 +0800 Subject: [PATCH 04/13] =?UTF-8?q?fix(game-source-generators):=20=E6=B8=85?= =?UTF-8?q?=E7=90=86=20SchemaConfigGenerator=20=E5=AD=97=E7=AC=A6=E4=B8=B2?= =?UTF-8?q?=E6=AF=94=E8=BE=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 SchemaConfigGenerator 中 schema 关键字比较缺少 StringComparison 的 analyzer warning - 新增 schema 类型比较 helper 以统一 ordinal 比较语义 - 更新 analyzer warning reduction 的 RP-019 恢复记录与验证结果 --- .../Config/SchemaConfigGenerator.cs | 73 ++++++++++++------- .../analyzer-warning-reduction-tracking.md | 32 ++++++-- .../analyzer-warning-reduction-trace.md | 30 ++++++++ 3 files changed, 103 insertions(+), 32 deletions(-) diff --git a/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs b/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs index 0ca5ea00..ece23f07 100644 --- a/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs +++ b/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs @@ -203,8 +203,8 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator Path.GetFileName(file.Path))); } - if (idProperty.TypeSpec.SchemaType != "integer" && - idProperty.TypeSpec.SchemaType != "string") + if (!IsSchemaType(idProperty.TypeSpec.SchemaType, "integer") && + !IsSchemaType(idProperty.TypeSpec.SchemaType, "string")) { return SchemaParseResult.FromDiagnostic( Diagnostic.Create( @@ -1854,6 +1854,27 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator }; } + /// + /// 使用 schema 关键字的大小写敏感语义比较类型名称。 + /// + /// 从 JSON schema 读取或推导出的类型名称。 + /// 期望匹配的 schema 类型关键字。 + /// 当两个类型名称按 schema 关键字语义完全一致时返回 + private static bool IsSchemaType(string schemaType, string expectedType) + { + return string.Equals(schemaType, expectedType, StringComparison.Ordinal); + } + + /// + /// 判断类型是否支持 JSON schema 数值范围和倍数约束。 + /// + /// 从 JSON schema 读取或推导出的类型名称。 + /// 当类型为 integernumber 时返回 + private static bool IsNumericSchemaType(string schemaType) + { + return IsSchemaType(schemaType, "integer") || IsSchemaType(schemaType, "number"); + } + /// /// 解析数组属性,支持标量数组与对象数组。 /// @@ -3943,57 +3964,57 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator parts.Add($"const = {constDocumentation}"); } - if ((schemaType == "integer" || schemaType == "number") && + if (IsNumericSchemaType(schemaType) && TryGetFiniteNumber(element, "minimum", out var minimum)) { parts.Add($"minimum = {minimum.ToString(CultureInfo.InvariantCulture)}"); } - if ((schemaType == "integer" || schemaType == "number") && + if (IsNumericSchemaType(schemaType) && TryGetFiniteNumber(element, "exclusiveMinimum", out var exclusiveMinimum)) { parts.Add($"exclusiveMinimum = {exclusiveMinimum.ToString(CultureInfo.InvariantCulture)}"); } - if ((schemaType == "integer" || schemaType == "number") && + if (IsNumericSchemaType(schemaType) && TryGetFiniteNumber(element, "maximum", out var maximum)) { parts.Add($"maximum = {maximum.ToString(CultureInfo.InvariantCulture)}"); } - if ((schemaType == "integer" || schemaType == "number") && + if (IsNumericSchemaType(schemaType) && TryGetFiniteNumber(element, "exclusiveMaximum", out var exclusiveMaximum)) { parts.Add($"exclusiveMaximum = {exclusiveMaximum.ToString(CultureInfo.InvariantCulture)}"); } - if ((schemaType == "integer" || schemaType == "number") && + if (IsNumericSchemaType(schemaType) && TryGetFiniteNumber(element, "multipleOf", out var multipleOf) && multipleOf > 0d) { parts.Add($"multipleOf = {multipleOf.ToString(CultureInfo.InvariantCulture)}"); } - if (schemaType == "string" && + if (IsSchemaType(schemaType, "string") && TryGetNonNegativeInt32(element, "minLength", out var minLength)) { parts.Add($"minLength = {minLength.ToString(CultureInfo.InvariantCulture)}"); } - if (schemaType == "string" && + if (IsSchemaType(schemaType, "string") && TryGetNonNegativeInt32(element, "maxLength", out var maxLength)) { parts.Add($"maxLength = {maxLength.ToString(CultureInfo.InvariantCulture)}"); } - if (schemaType == "string" && + if (IsSchemaType(schemaType, "string") && element.TryGetProperty("pattern", out var patternElement) && patternElement.ValueKind == JsonValueKind.String) { parts.Add($"pattern = '{patternElement.GetString() ?? string.Empty}'"); } - if (schemaType == "string" && + if (IsSchemaType(schemaType, "string") && element.TryGetProperty("format", out var formatElement) && formatElement.ValueKind == JsonValueKind.String) { @@ -4004,26 +4025,26 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator } } - if (schemaType == "array" && + if (IsSchemaType(schemaType, "array") && TryGetNonNegativeInt32(element, "minItems", out var minItems)) { parts.Add($"minItems = {minItems.ToString(CultureInfo.InvariantCulture)}"); } - if (schemaType == "array" && + if (IsSchemaType(schemaType, "array") && TryGetNonNegativeInt32(element, "maxItems", out var maxItems)) { parts.Add($"maxItems = {maxItems.ToString(CultureInfo.InvariantCulture)}"); } - if (schemaType == "array" && + if (IsSchemaType(schemaType, "array") && element.TryGetProperty("uniqueItems", out var uniqueItemsElement) && uniqueItemsElement.ValueKind == JsonValueKind.True) { parts.Add("uniqueItems = true"); } - if (schemaType == "array") + if (IsSchemaType(schemaType, "array")) { var containsDocumentation = TryBuildContainsDocumentation(element); if (containsDocumentation is not null) @@ -4038,31 +4059,31 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator parts.Add($"not = {notDocumentation}"); } - if (schemaType == "array" && + if (IsSchemaType(schemaType, "array") && TryGetNonNegativeInt32(element, "minContains", out var minContains)) { parts.Add($"minContains = {minContains.ToString(CultureInfo.InvariantCulture)}"); } - if (schemaType == "array" && + if (IsSchemaType(schemaType, "array") && TryGetNonNegativeInt32(element, "maxContains", out var maxContains)) { parts.Add($"maxContains = {maxContains.ToString(CultureInfo.InvariantCulture)}"); } - if (schemaType == "object" && + if (IsSchemaType(schemaType, "object") && TryGetNonNegativeInt32(element, "minProperties", out var minProperties)) { parts.Add($"minProperties = {minProperties.ToString(CultureInfo.InvariantCulture)}"); } - if (schemaType == "object" && + if (IsSchemaType(schemaType, "object") && TryGetNonNegativeInt32(element, "maxProperties", out var maxProperties)) { parts.Add($"maxProperties = {maxProperties.ToString(CultureInfo.InvariantCulture)}"); } - if (schemaType == "object") + if (IsSchemaType(schemaType, "object")) { var dependentRequiredDocumentation = TryBuildDependentRequiredDocumentation(element); if (dependentRequiredDocumentation is not null) @@ -4351,14 +4372,14 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator } var schemaType = typeElement.GetString(); - if (string.IsNullOrWhiteSpace(schemaType)) + if (schemaType is null || string.IsNullOrWhiteSpace(schemaType)) { return null; } var details = new List(); if (includeRequiredProperties && - schemaType == "object") + IsSchemaType(schemaType, "object")) { var requiredDocumentation = TryBuildRequiredPropertiesDocumentation(schemaElement); if (requiredDocumentation is not null) @@ -4367,13 +4388,13 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator } } - var enumDocumentation = TryBuildEnumDocumentation(schemaElement, schemaType!); + var enumDocumentation = TryBuildEnumDocumentation(schemaElement, schemaType); if (enumDocumentation is not null) { details.Add($"enum = {enumDocumentation}"); } - var constraintDocumentation = TryBuildConstraintDocumentation(schemaElement, schemaType!); + var constraintDocumentation = TryBuildConstraintDocumentation(schemaElement, schemaType); if (constraintDocumentation is not null) { details.Add(constraintDocumentation); @@ -4492,7 +4513,9 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator /// 组合后的路径。 private static string CombinePath(string parentPath, string propertyName) { - return parentPath == "" ? propertyName : $"{parentPath}.{propertyName}"; + return string.Equals(parentPath, "", StringComparison.Ordinal) + ? propertyName + : $"{parentPath}.{propertyName}"; } /// 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 b13e0e74..d27049dc 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 @@ -7,14 +7,16 @@ ## 当前恢复点 -- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-018` -- 当前阶段:`Phase 18` +- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-019` +- 当前阶段:`Phase 19` - 当前焦点: - 已完成 `GFramework.Core` 当前 `MA0016` / `MA0002` / `MA0015` / `MA0077` 低风险收口批次 - 已复核 `net10.0` 下的 `MA0158` 基线:`GFramework.Core` / `GFramework.Cqrs` 当前共有 `16` 个 object lock 建议点,属于跨 target 兼容性风险,不在本轮直接批量替换 - 已完成 `GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs` 的剩余 `MA0051` 结构拆分,生成输出保持不变 - 已完成 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs` 的 `MA0051` 结构拆分,生成输出保持不变 + - 已完成 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs` 的 `MA0006` 低风险收口,schema 关键字比较显式使用 + `StringComparison.Ordinal` - `LoggingConfiguration`、`FilterConfiguration` 与 `CollectionExtensions` 已改用集合抽象接口,并保留内部具体集合默认值 - `CoroutineScheduler` 的 tag/group 字典已显式使用 `StringComparer.Ordinal`,保持既有区分大小写语义 - `EasyEvents.AddEvent()` 的重复注册路径已改为状态冲突异常,避免把泛型类型参数伪装成方法参数名 @@ -22,9 +24,11 @@ - 当前 `GFramework.Core` `net8.0` warnings-only 基线已降到 `0` 条 - 当前 `GFramework.Core.SourceGenerators` warnings-only 基线已降到 `0` 条 - 当前 `GFramework.Cqrs.SourceGenerators` warnings-only 基线已降到 `0` 条 + - 当前 `GFramework.Game.SourceGenerators` warnings-only 基线已从 `46` 条降到 `19` 条,剩余均为 + `SchemaConfigGenerator.cs` 的 `MA0051` - `GFramework.Godot` 的 `Timing.cs` 已同步适配新事件签名,但当前 worktree 的 Godot restore 资产仍受 Windows fallback package folder 干扰,独立 build 需在修复资产后补跑 - 后续继续按 warning 类型和数量批处理,而不是回退到按单文件切片推进 - - 下一轮默认转向 `GFramework.Game.SourceGenerators` 的 `MA0006` / `MA0051` 热点,或继续评估跨 target 的 `MA0158` + - 下一轮默认继续拆分 `GFramework.Game.SourceGenerators` 的 `MA0051` 热点,或评估跨 target 的 `MA0158` 锁替换风险 - 单次 `boot` 的工作树改动上限控制在约 `100` 个文件以内,避免 recovery context 与 review 面同时失控 - 若任务边界互不冲突,允许使用不同模型的 subagent 并行处理不同 warning 类型或不同目录,但必须遵守显式 ownership @@ -45,6 +49,8 @@ - 已完成当前 `GFramework.Core` `net8.0` 剩余低风险 analyzer warning 批次;warnings-only 基线已降到 `0` 条 - 已完成 `GFramework.Core.SourceGenerators` 中 `ContextAwareGenerator` 的剩余 `MA0051` 收口;warnings-only 基线已降到 `0` 条 - 已完成 `GFramework.Cqrs.SourceGenerators` 中 `CqrsHandlerRegistryGenerator` 的剩余 `MA0051` 收口;warnings-only 基线已降到 `0` 条 +- 已完成 `GFramework.Game.SourceGenerators` 中 `SchemaConfigGenerator` 的 `MA0006` 收口;warnings-only 基线剩余 `19` 条 + `MA0051` ## 当前活跃事实 @@ -81,6 +87,8 @@ 并通过 source generator 项目 build 与 `ContextAwareGeneratorSnapshotTests` 验证生成输出未回归 - `RP-018` 暂缓跨 target `MA0158`,转入 `GFramework.Cqrs.SourceGenerators` 的单文件结构性 warning; 通过拆分 handler 分析、运行时类型引用构造、注册器源码发射与精确反射注册输出阶段,清空该项目当前 `MA0051` +- `RP-019` 转入 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs`,先完成低风险 `MA0006` 批次; + 通过 schema 类型比较 helper 与显式 `StringComparison.Ordinal` 清空当前项目的 `MA0006` - 当前工作树分支 `fix/analyzer-warning-reduction-batch` 已在 `ai-plan/public/README.md` 建立 topic 映射 ## 当前风险 @@ -95,8 +103,7 @@ - 缓解措施:下一轮先按 target framework 与 API 可用性评估,不直接批量替换共享源码中的 `object` lock - source generator warning 外溢风险:运行 `GFramework.SourceGenerators.Tests` 会构建相邻 generator/test 项目并显示既有 `GFramework.Game.SourceGenerators` 与测试项目 warning - - 缓解措施:本轮以 `GFramework.Cqrs.SourceGenerators` 独立 warnings-only build 作为主验收,并用 focused generator test - 验证行为;后续若处理相邻 generator warning,应另开明确切片 + - 缓解措施:继续以被修改 generator 项目的独立 warnings-only build 作为主验收,并用 focused generator test 验证行为 - Godot 资产文件环境风险:当前 worktree 的 `GFramework.Godot` restore/build 仍会命中 Windows fallback package folder - 缓解措施:后续若继续触达 Godot 模块,先用 Linux 侧 restore 资产或 Windows-hosted 构建链刷新该项目,再补跑定向 build - 并行实现风险:批量收敛时若 subagent 写入边界不清晰,容易引入命名冲突或重复重构 @@ -198,13 +205,24 @@ - 结果:`14 Passed`,`0 Failed` - 说明:该 test project 构建仍显示 `GFramework.Game.SourceGenerators` 与测试项目中的既有 analyzer warning;本轮关注的 `GFramework.Cqrs.SourceGenerators` 独立 build 已清零 +- `RP-019` 的验证结果: + - `dotnet restore GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -p:RestoreFallbackFolders= -nologo` + - 结果:通过;刷新 Linux 侧资产以清除 stale Windows fallback package folder + - `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"` + - 结果:`19 Warning(s)`,`0 Error(s)`;当前项目输出已不再出现 `MA0006`,剩余均为 `SchemaConfigGenerator.cs` 的 + `MA0051` + - `dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders= -nologo` + - 结果:通过;刷新 test project 资产以清除 stale Windows fallback package folder + - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~SchemaConfigGenerator -m:1 -p:RestoreFallbackFolders= -nologo` + - 结果:`50 Passed`,`0 Failed` + - 说明:测试项目构建仍显示既有 source generator test analyzer warning;不属于本轮写集 - active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史 ## 下一步 1. 若要继续该主题,先读 active tracking,再按需展开历史归档中的 warning 热点与验证记录 -2. 下一轮优先转入 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs` 的 `MA0006` 低风险批次,再评估是否继续拆分 - 该文件的 `MA0051` +2. 下一轮优先继续拆分 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs` 的 `MA0051`;建议先从 + `TryBuildConstraintDocumentation` 或 `GenerateConfigCatalogSource` 这类高收益方法切入 3. 若改回推进 `MA0158`,先设计 `net8.0` / `net9.0` / `net10.0` 多 target 条件编译方案,不直接批量替换共享源码中的 `object` lock 4. 若后续继续改动 `GFramework.Godot`,先修复该项目的 Linux 侧 restore 资产,再补跑独立 build 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 b567a7e5..58b721a6 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,35 @@ # Analyzer Warning Reduction 追踪 +## 2026-04-22 — RP-019 + +### 阶段:`SchemaConfigGenerator` 当前 `MA0006` 收口(RP-019) + +- 启动复核: + - 当前 worktree 仍映射到 `analyzer-warning-reduction` active topic + - Windows Git interop 在当前 shell 中返回 WSL socket 错误;本轮使用显式 `--git-dir` / `--work-tree` 读取状态 + - `GFramework.Game.SourceGenerators` 首次 build 受 stale Windows fallback package folder 影响,刷新 restore 资产后复现 + `46` 条 warning,其中 `MA0006=27`,其余为 `SchemaConfigGenerator.cs` 的 `MA0051` +- 决策: + - 本轮先收口低风险 `MA0006`,不在同一 slice 中拆分 `SchemaConfigGenerator.cs` 的长方法 + - 未使用 subagent;critical path 是本地复现 warning、替换 schema 字符串比较并用 focused schema generator tests 验证输出行为 +- 实施调整: + - 为 schema 类型关键字新增 `IsSchemaType` / `IsNumericSchemaType` helper,统一使用 `StringComparison.Ordinal` + - 将 id key 类型验证、约束文档生成、required property 文档和路径拼接中的直接字符串比较改为显式 ordinal 比较 + - 修正 `JsonElement.GetString()` 后的 nullable flow,避免新增 `CS8604` +- 验证结果: + - `dotnet restore GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -p:RestoreFallbackFolders= -nologo` + - 结果:通过 + - `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"` + - 结果:`19 Warning(s)`,`0 Error(s)`;当前项目输出已无 `MA0006`,剩余均为 `MA0051` + - `dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders= -nologo` + - 结果:通过 + - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~SchemaConfigGenerator -m:1 -p:RestoreFallbackFolders= -nologo` + - 结果:`50 Passed`,`0 Failed` + - 说明:测试项目构建仍显示既有 analyzer warning;不属于本轮写集 +- 下一步建议: + - 继续该主题时,优先拆分 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs` 的 `MA0051` + - 若回到 `MA0158`,先设计多 target 条件编译方案,再考虑替换共享源码中的 `object` lock + ## 2026-04-22 — RP-018 ### 阶段:`CqrsHandlerRegistryGenerator` 剩余 `MA0051` 收口(RP-018) From 0f8bf077e45e9bec5967290fd80f4a52963eb46d Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 22 Apr 2026 10:28:48 +0800 Subject: [PATCH 05/13] =?UTF-8?q?refactor(source-generators):=20=E6=8B=86?= =?UTF-8?q?=E5=88=86=E9=85=8D=E7=BD=AE=E7=94=9F=E6=88=90=E5=99=A8=E8=AD=A6?= =?UTF-8?q?=E5=91=8A=E7=83=AD=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构 SchemaConfigGenerator 的 schema 解析、属性解析与遍历阶段 - 拆分数组属性、约束文档和生成代码发射 helper 以降低 MA0051 基线 - 更新 analyzer warning reduction 恢复文档和验证记录 --- .../Config/SchemaConfigGenerator.cs | 1709 +++++++++++------ .../analyzer-warning-reduction-tracking.md | 22 +- .../analyzer-warning-reduction-trace.md | 27 + 3 files changed, 1177 insertions(+), 581 deletions(-) diff --git a/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs b/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs index ece23f07..d0926c57 100644 --- a/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs +++ b/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs @@ -95,148 +95,15 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator AdditionalText file, CancellationToken cancellationToken) { - SourceText? text; - try + if (!TryReadSchemaText(file, cancellationToken, out var text, out var diagnostic)) { - text = file.GetText(cancellationToken); - } - catch (Exception exception) - { - return SchemaParseResult.FromDiagnostic( - Diagnostic.Create( - ConfigSchemaDiagnostics.InvalidSchemaJson, - CreateFileLocation(file.Path), - Path.GetFileName(file.Path), - exception.Message)); - } - - if (text is null) - { - return SchemaParseResult.FromDiagnostic( - Diagnostic.Create( - ConfigSchemaDiagnostics.InvalidSchemaJson, - CreateFileLocation(file.Path), - Path.GetFileName(file.Path), - "File content could not be read.")); + return SchemaParseResult.FromDiagnostic(diagnostic!); } try { - using var document = JsonDocument.Parse(text.ToString()); - var root = document.RootElement; - if (!root.TryGetProperty("type", out var rootTypeElement) || - !string.Equals(rootTypeElement.GetString(), "object", StringComparison.Ordinal)) - { - return SchemaParseResult.FromDiagnostic( - Diagnostic.Create( - ConfigSchemaDiagnostics.RootObjectSchemaRequired, - CreateFileLocation(file.Path), - Path.GetFileName(file.Path))); - } - - if (!TryValidateStringFormatMetadataRecursively( - file.Path, - "", - root, - out var rootFormatDiagnostic)) - { - return SchemaParseResult.FromDiagnostic(rootFormatDiagnostic!); - } - - if (!TryValidateDependentRequiredMetadataRecursively( - file.Path, - "", - root, - out var dependentRequiredDiagnostic)) - { - return SchemaParseResult.FromDiagnostic(dependentRequiredDiagnostic!); - } - - if (!TryValidateDependentSchemasMetadataRecursively( - file.Path, - "", - root, - out var dependentSchemasDiagnostic)) - { - return SchemaParseResult.FromDiagnostic(dependentSchemasDiagnostic!); - } - - if (!TryValidateAllOfMetadataRecursively( - file.Path, - "", - root, - out var allOfDiagnostic)) - { - return SchemaParseResult.FromDiagnostic(allOfDiagnostic!); - } - - if (!TryValidateConditionalSchemasMetadataRecursively( - file.Path, - "", - root, - out var conditionalDiagnostic)) - { - return SchemaParseResult.FromDiagnostic(conditionalDiagnostic!); - } - - var entityName = ToPascalCase(GetSchemaBaseName(file.Path)); - var rootObject = ParseObjectSpec( - file.Path, - root, - "", - $"{entityName}Config", - isRoot: true); - if (rootObject.Diagnostic is not null) - { - return SchemaParseResult.FromDiagnostic(rootObject.Diagnostic); - } - - var schemaObject = rootObject.Object!; - var idProperty = schemaObject.Properties.FirstOrDefault(static property => - string.Equals(property.SchemaName, "id", StringComparison.OrdinalIgnoreCase)); - if (idProperty is null || !idProperty.IsRequired) - { - return SchemaParseResult.FromDiagnostic( - Diagnostic.Create( - ConfigSchemaDiagnostics.IdPropertyRequired, - CreateFileLocation(file.Path), - Path.GetFileName(file.Path))); - } - - if (!IsSchemaType(idProperty.TypeSpec.SchemaType, "integer") && - !IsSchemaType(idProperty.TypeSpec.SchemaType, "string")) - { - return SchemaParseResult.FromDiagnostic( - Diagnostic.Create( - ConfigSchemaDiagnostics.UnsupportedKeyType, - CreateFileLocation(file.Path), - Path.GetFileName(file.Path), - idProperty.TypeSpec.SchemaType)); - } - - var schemaBaseName = GetSchemaBaseName(file.Path); - var configRelativePath = ResolveConfigRelativePath(file.Path, root, schemaBaseName); - if (configRelativePath.Diagnostic is not null) - { - return SchemaParseResult.FromDiagnostic(configRelativePath.Diagnostic); - } - - var schema = new SchemaFileSpec( - Path.GetFileName(file.Path), - entityName, - schemaObject.ClassName, - $"{entityName}Table", - GeneratedNamespace, - idProperty.TypeSpec.ClrType.TrimEnd('?'), - idProperty.PropertyName, - schemaBaseName, - configRelativePath.Path!, - GetSchemaRelativePath(file.Path), - TryGetMetadataString(root, "title"), - TryGetMetadataString(root, "description"), - schemaObject); - - return SchemaParseResult.FromSchema(schema); + using var document = JsonDocument.Parse(text!.ToString()); + return ParseSchemaRoot(file.Path, document.RootElement); } catch (JsonException exception) { @@ -249,6 +116,193 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator } } + /// + /// Reads an AdditionalFiles schema text while converting all IO failures into generator diagnostics. + /// + /// AdditionalFiles entry supplied by Roslyn. + /// Cancellation token forwarded by the incremental generator pipeline. + /// Read source text when the file can be loaded. + /// Diagnostic describing the read failure. + /// when schema text was read successfully; otherwise . + private static bool TryReadSchemaText( + AdditionalText file, + CancellationToken cancellationToken, + out SourceText? text, + out Diagnostic? diagnostic) + { + try + { + text = file.GetText(cancellationToken); + } + catch (Exception exception) + { + text = null; + diagnostic = Diagnostic.Create( + ConfigSchemaDiagnostics.InvalidSchemaJson, + CreateFileLocation(file.Path), + Path.GetFileName(file.Path), + exception.Message); + return false; + } + + if (text is null) + { + diagnostic = Diagnostic.Create( + ConfigSchemaDiagnostics.InvalidSchemaJson, + CreateFileLocation(file.Path), + Path.GetFileName(file.Path), + "File content could not be read."); + return false; + } + + diagnostic = null; + return true; + } + + /// + /// Parses a JSON schema root after JSON syntax has already been validated. + /// + /// Schema file path used for diagnostics and generated metadata. + /// Parsed root JSON element. + /// Parsed schema model or the first schema diagnostic encountered. + private static SchemaParseResult ParseSchemaRoot(string filePath, JsonElement root) + { + if (!TryValidateSchemaRoot(filePath, root, out var diagnostic)) + { + return SchemaParseResult.FromDiagnostic(diagnostic!); + } + + var entityName = ToPascalCase(GetSchemaBaseName(filePath)); + var rootObject = ParseObjectSpec(filePath, root, "", $"{entityName}Config", isRoot: true); + if (rootObject.Diagnostic is not null) + { + return SchemaParseResult.FromDiagnostic(rootObject.Diagnostic); + } + + var schemaObject = rootObject.Object!; + var idProperty = FindValidIdProperty(filePath, schemaObject, out diagnostic); + if (diagnostic is not null) + { + return SchemaParseResult.FromDiagnostic(diagnostic); + } + + var schemaBaseName = GetSchemaBaseName(filePath); + var configRelativePath = ResolveConfigRelativePath(filePath, root, schemaBaseName); + if (configRelativePath.Diagnostic is not null) + { + return SchemaParseResult.FromDiagnostic(configRelativePath.Diagnostic); + } + + return SchemaParseResult.FromSchema(CreateSchemaFileSpec( + filePath, + root, + entityName, + schemaObject, + idProperty!, + schemaBaseName, + configRelativePath.Path!)); + } + + /// + /// Validates schema-level contracts that must hold before object and property models are built. + /// + /// Schema file path used for diagnostics. + /// Root JSON schema element. + /// First validation diagnostic, if any. + /// when the root can be parsed as a config schema. + private static bool TryValidateSchemaRoot(string filePath, JsonElement root, out Diagnostic? diagnostic) + { + if (!root.TryGetProperty("type", out var rootTypeElement) || + !IsSchemaType(rootTypeElement.GetString() ?? string.Empty, "object")) + { + diagnostic = Diagnostic.Create( + ConfigSchemaDiagnostics.RootObjectSchemaRequired, + CreateFileLocation(filePath), + Path.GetFileName(filePath)); + return false; + } + + return TryValidateStringFormatMetadataRecursively(filePath, "", root, out diagnostic) && + TryValidateDependentRequiredMetadataRecursively(filePath, "", root, out diagnostic) && + TryValidateDependentSchemasMetadataRecursively(filePath, "", root, out diagnostic) && + TryValidateAllOfMetadataRecursively(filePath, "", root, out diagnostic) && + TryValidateConditionalSchemasMetadataRecursively(filePath, "", root, out diagnostic); + } + + /// + /// Finds and validates the required root id property that becomes the generated table key. + /// + /// Schema file path used for diagnostics. + /// Parsed root object model. + /// Diagnostic explaining why the key is invalid. + /// The key property when it satisfies the generator contract; otherwise null. + private static SchemaPropertySpec? FindValidIdProperty( + string filePath, + SchemaObjectSpec schemaObject, + out Diagnostic? diagnostic) + { + var idProperty = schemaObject.Properties.FirstOrDefault(static property => + string.Equals(property.SchemaName, "id", StringComparison.OrdinalIgnoreCase)); + if (idProperty is null || !idProperty.IsRequired) + { + diagnostic = Diagnostic.Create( + ConfigSchemaDiagnostics.IdPropertyRequired, + CreateFileLocation(filePath), + Path.GetFileName(filePath)); + return null; + } + + if (!IsSchemaType(idProperty.TypeSpec.SchemaType, "integer") && + !IsSchemaType(idProperty.TypeSpec.SchemaType, "string")) + { + diagnostic = Diagnostic.Create( + ConfigSchemaDiagnostics.UnsupportedKeyType, + CreateFileLocation(filePath), + Path.GetFileName(filePath), + idProperty.TypeSpec.SchemaType); + return null; + } + + diagnostic = null; + return idProperty; + } + + /// + /// Creates the generator-level schema model from validated root metadata and the parsed object tree. + /// + /// Schema file path. + /// Root JSON element used for optional title and description metadata. + /// Generated entity name derived from the schema file name. + /// Parsed root object model. + /// Validated required key property. + /// Normalized schema base name. + /// Resolved config-relative directory path. + /// Completed schema file model used by source emission. + private static SchemaFileSpec CreateSchemaFileSpec( + string filePath, + JsonElement root, + string entityName, + SchemaObjectSpec schemaObject, + SchemaPropertySpec idProperty, + string schemaBaseName, + string configRelativePath) + { + return new SchemaFileSpec( + Path.GetFileName(filePath), + entityName, + schemaObject.ClassName, + $"{entityName}Table", + GeneratedNamespace, + idProperty.TypeSpec.ClrType.TrimEnd('?'), + idProperty.PropertyName, + schemaBaseName, + configRelativePath, + GetSchemaRelativePath(filePath), + TryGetMetadataString(root, "title"), + TryGetMetadataString(root, "description"), + schemaObject); + } + /// /// 解析对象 schema,并递归构建子属性模型。 /// @@ -347,6 +401,45 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator } var schemaType = typeElement.GetString() ?? string.Empty; + if (!TryCreatePropertyParseContext( + filePath, + property, + isRequired, + displayPath, + isDirectChildOfRoot, + schemaType, + out var context, + out var diagnostic)) + { + return ParsedPropertyResult.FromDiagnostic(diagnostic!); + } + + return CreatePropertyBySchemaType(filePath, property, schemaType, context!); + } + + /// + /// Collects shared property metadata and validates cross-cutting metadata before type-specific parsing begins. + /// + /// Schema file path used for diagnostics. + /// Schema property JSON node. + /// Whether the property is required. + /// Logical schema path. + /// Whether the property is a direct child of the schema root. + /// Validated schema type keyword. + /// Shared parsing metadata when validation succeeds. + /// First metadata validation diagnostic. + /// when the property can continue to type-specific parsing. + private static bool TryCreatePropertyParseContext( + string filePath, + JsonProperty property, + bool isRequired, + string displayPath, + bool isDirectChildOfRoot, + string schemaType, + out PropertyParseContext? context, + out Diagnostic? diagnostic) + { + context = null; var title = TryGetMetadataString(property.Value, "title"); var description = TryGetMetadataString(property.Value, "description"); var refTableName = TryGetMetadataString(property.Value, "x-gframework-ref-table"); @@ -357,26 +450,27 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator schemaType, out var formatDiagnostic)) { - return ParsedPropertyResult.FromDiagnostic(formatDiagnostic!); + diagnostic = formatDiagnostic; + return false; } var indexedLookupMetadata = TryGetMetadataBoolean(property.Value, LookupIndexMetadataKey); if (indexedLookupMetadata.Diagnostic is not null) { - return ParsedPropertyResult.FromDiagnostic( - Diagnostic.Create( - ConfigSchemaDiagnostics.InvalidLookupIndexMetadata, - CreateFileLocation(filePath), - Path.GetFileName(filePath), - displayPath, - LookupIndexMetadataKey, - indexedLookupMetadata.Diagnostic!)); + diagnostic = Diagnostic.Create( + ConfigSchemaDiagnostics.InvalidLookupIndexMetadata, + CreateFileLocation(filePath), + Path.GetFileName(filePath), + displayPath, + LookupIndexMetadataKey, + indexedLookupMetadata.Diagnostic!); + return false; } var isIndexedLookup = indexedLookupMetadata.Value ?? false; - if (!TryBuildPropertyIdentifier(filePath, displayPath, property.Name, out var propertyName, out var diagnostic)) + if (!TryBuildPropertyIdentifier(filePath, displayPath, property.Name, out var propertyName, out diagnostic)) { - return ParsedPropertyResult.FromDiagnostic(diagnostic!); + return false; } if (isIndexedLookup && @@ -389,154 +483,147 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator refTableName, out diagnostic)) { - return ParsedPropertyResult.FromDiagnostic(diagnostic!); + return false; } - switch (schemaType) + context = new PropertyParseContext( + displayPath, + propertyName, + isRequired, + title, + description, + refTableName, + isIndexedLookup); + diagnostic = null; + return true; + } + + /// + /// Dispatches schema property parsing by JSON schema type after shared validation and metadata extraction. + /// + /// Schema file path used for diagnostics. + /// Schema property JSON node. + /// Validated schema type keyword. + /// Shared parsing metadata collected from the property node. + /// Parsed property model or an unsupported-type diagnostic. + private static ParsedPropertyResult CreatePropertyBySchemaType( + string filePath, + JsonProperty property, + string schemaType, + PropertyParseContext context) + { + return schemaType switch { - case "integer": - return ParsedPropertyResult.FromProperty(new SchemaPropertySpec( - property.Name, - displayPath, - propertyName, - isRequired, - title, - description, - isIndexedLookup, - new SchemaTypeSpec( - SchemaNodeKind.Scalar, - "integer", - isRequired ? "int" : "int?", - TryBuildScalarInitializer(property.Value, "integer"), - TryBuildEnumDocumentation(property.Value, "integer"), - TryBuildConstraintDocumentation(property.Value, "integer"), - refTableName, - null, - null))); + "integer" => CreateScalarPropertyResult(property, context, schemaType, context.IsRequired ? "int" : "int?"), + "number" => CreateScalarPropertyResult(property, context, schemaType, context.IsRequired ? "double" : "double?"), + "boolean" => CreateScalarPropertyResult(property, context, schemaType, context.IsRequired ? "bool" : "bool?"), + "string" => CreateScalarPropertyResult(property, context, schemaType, context.IsRequired ? "string" : "string?"), + "object" => CreateObjectPropertyResult(filePath, property, context), + "array" => ParseArrayProperty(filePath, property, context), + _ => ParsedPropertyResult.FromDiagnostic( + Diagnostic.Create( + ConfigSchemaDiagnostics.UnsupportedPropertyType, + CreateFileLocation(filePath), + Path.GetFileName(filePath), + context.DisplayPath, + schemaType)), + }; + } - case "number": - return ParsedPropertyResult.FromProperty(new SchemaPropertySpec( - property.Name, - displayPath, - propertyName, - isRequired, - title, - description, - isIndexedLookup, - new SchemaTypeSpec( - SchemaNodeKind.Scalar, - "number", - isRequired ? "double" : "double?", - TryBuildScalarInitializer(property.Value, "number"), - TryBuildEnumDocumentation(property.Value, "number"), - TryBuildConstraintDocumentation(property.Value, "number"), - refTableName, - null, - null))); - - case "boolean": - return ParsedPropertyResult.FromProperty(new SchemaPropertySpec( - property.Name, - displayPath, - propertyName, - isRequired, - title, - description, - isIndexedLookup, - new SchemaTypeSpec( - SchemaNodeKind.Scalar, - "boolean", - isRequired ? "bool" : "bool?", - TryBuildScalarInitializer(property.Value, "boolean"), - TryBuildEnumDocumentation(property.Value, "boolean"), - TryBuildConstraintDocumentation(property.Value, "boolean"), - refTableName, - null, - null))); - - case "string": - return ParsedPropertyResult.FromProperty(new SchemaPropertySpec( - property.Name, - displayPath, - propertyName, - isRequired, - title, - description, - isIndexedLookup, - new SchemaTypeSpec( - SchemaNodeKind.Scalar, - "string", - isRequired ? "string" : "string?", - TryBuildScalarInitializer(property.Value, "string") ?? - (isRequired ? " = string.Empty;" : null), - TryBuildEnumDocumentation(property.Value, "string"), - TryBuildConstraintDocumentation(property.Value, "string"), - refTableName, - null, - null))); - - case "object": - if (isIndexedLookup) - { - return ParsedPropertyResult.FromDiagnostic( - CreateInvalidLookupIndexDiagnostic(filePath, displayPath, - LookupIndexTopLevelScalarOnlyMessage)); - } - - if (!string.IsNullOrWhiteSpace(refTableName)) - { - return ParsedPropertyResult.FromDiagnostic( - Diagnostic.Create( - ConfigSchemaDiagnostics.UnsupportedPropertyType, - CreateFileLocation(filePath), - Path.GetFileName(filePath), - displayPath, - "object-ref")); - } - - var objectResult = ParseObjectSpec( - filePath, - property.Value, - displayPath, - $"{propertyName}Config"); - if (objectResult.Diagnostic is not null) - { - return ParsedPropertyResult.FromDiagnostic(objectResult.Diagnostic); - } - - var objectSpec = objectResult.Object!; - return ParsedPropertyResult.FromProperty(new SchemaPropertySpec( - property.Name, - displayPath, - propertyName, - isRequired, - title, - description, - false, - new SchemaTypeSpec( - SchemaNodeKind.Object, - "object", - isRequired ? objectSpec.ClassName : $"{objectSpec.ClassName}?", - isRequired ? " = new();" : null, - TryBuildEnumDocumentation(property.Value, "object"), - null, - null, - objectSpec, - null))); - - case "array": - return ParseArrayProperty(filePath, property, isRequired, displayPath, propertyName, title, - description, refTableName, isIndexedLookup); - - default: - return ParsedPropertyResult.FromDiagnostic( - Diagnostic.Create( - ConfigSchemaDiagnostics.UnsupportedPropertyType, - CreateFileLocation(filePath), - Path.GetFileName(filePath), - displayPath, - schemaType)); + /// + /// Builds a parsed scalar property model while preserving schema metadata used by generated XML docs. + /// + /// Schema property JSON node. + /// Shared parsing metadata collected from the property node. + /// Scalar schema type. + /// Generated CLR property type. + /// Parsed property result for a scalar schema node. + private static ParsedPropertyResult CreateScalarPropertyResult( + JsonProperty property, + PropertyParseContext context, + string schemaType, + string clrType) + { + var initializer = TryBuildScalarInitializer(property.Value, schemaType); + if (IsSchemaType(schemaType, "string") && initializer is null && context.IsRequired) + { + initializer = " = string.Empty;"; } + + return ParsedPropertyResult.FromProperty(new SchemaPropertySpec( + property.Name, + context.DisplayPath, + context.PropertyName, + context.IsRequired, + context.Title, + context.Description, + context.IsIndexedLookup, + new SchemaTypeSpec( + SchemaNodeKind.Scalar, + schemaType, + clrType, + initializer, + TryBuildEnumDocumentation(property.Value, schemaType), + TryBuildConstraintDocumentation(property.Value, schemaType), + context.RefTableName, + null, + null))); + } + + /// + /// Builds a parsed object property model and reports unsupported object reference/index combinations. + /// + /// Schema file path used for diagnostics. + /// Schema property JSON node. + /// Shared parsing metadata collected from the property node. + /// Parsed property result for an object schema node. + private static ParsedPropertyResult CreateObjectPropertyResult( + string filePath, + JsonProperty property, + PropertyParseContext context) + { + if (context.IsIndexedLookup) + { + return ParsedPropertyResult.FromDiagnostic( + CreateInvalidLookupIndexDiagnostic(filePath, context.DisplayPath, LookupIndexTopLevelScalarOnlyMessage)); + } + + if (!string.IsNullOrWhiteSpace(context.RefTableName)) + { + return ParsedPropertyResult.FromDiagnostic( + Diagnostic.Create( + ConfigSchemaDiagnostics.UnsupportedPropertyType, + CreateFileLocation(filePath), + Path.GetFileName(filePath), + context.DisplayPath, + "object-ref")); + } + + var objectResult = ParseObjectSpec(filePath, property.Value, context.DisplayPath, $"{context.PropertyName}Config"); + if (objectResult.Diagnostic is not null) + { + return ParsedPropertyResult.FromDiagnostic(objectResult.Diagnostic); + } + + var objectSpec = objectResult.Object!; + return ParsedPropertyResult.FromProperty(new SchemaPropertySpec( + property.Name, + context.DisplayPath, + context.PropertyName, + context.IsRequired, + context.Title, + context.Description, + false, + new SchemaTypeSpec( + SchemaNodeKind.Object, + "object", + context.IsRequired ? objectSpec.ClassName : $"{objectSpec.ClassName}?", + context.IsRequired ? " = new();" : null, + TryBuildEnumDocumentation(property.Value, "object"), + null, + null, + objectSpec, + null))); } /// @@ -738,157 +825,271 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator return false; } - if (string.Equals(schemaType, "object", StringComparison.Ordinal) && - element.TryGetProperty("properties", out var propertiesElement) && - propertiesElement.ValueKind == JsonValueKind.Object) - { - foreach (var property in propertiesElement.EnumerateObject()) - { - if (!TryTraverseSchemaRecursively( - filePath, - CombinePath(displayPath, property.Name), - property.Value, - nodeValidator, - out diagnostic)) - { - return false; - } - } - } - - if (string.Equals(schemaType, "object", StringComparison.Ordinal) && - element.TryGetProperty("dependentSchemas", out var dependentSchemasElement) && - dependentSchemasElement.ValueKind == JsonValueKind.Object) - { - foreach (var dependentSchema in dependentSchemasElement.EnumerateObject()) - { - if (dependentSchema.Value.ValueKind != JsonValueKind.Object) - { - continue; - } - - if (!TryTraverseSchemaRecursively( - filePath, - $"{displayPath}[dependentSchemas:{dependentSchema.Name}]", - dependentSchema.Value, - nodeValidator, - out diagnostic)) - { - return false; - } - } - } - - if (string.Equals(schemaType, "object", StringComparison.Ordinal) && - element.TryGetProperty("allOf", out var allOfElement) && - allOfElement.ValueKind == JsonValueKind.Array) - { - var allOfIndex = 0; - foreach (var allOfSchema in allOfElement.EnumerateArray()) - { - if (allOfSchema.ValueKind != JsonValueKind.Object) - { - allOfIndex++; - continue; - } - - if (!TryTraverseSchemaRecursively( - filePath, - BuildAllOfEntryPath(displayPath, allOfIndex), - allOfSchema, - nodeValidator, - out diagnostic)) - { - return false; - } - - allOfIndex++; - } - } - - if (string.Equals(schemaType, "object", StringComparison.Ordinal)) - { - if (element.TryGetProperty("if", out var ifElement) && - ifElement.ValueKind == JsonValueKind.Object && - !TryTraverseSchemaRecursively( - filePath, - BuildConditionalSchemaPath(displayPath, "if"), - ifElement, - nodeValidator, - out diagnostic)) - { - return false; - } - - if (element.TryGetProperty("then", out var thenElement) && - thenElement.ValueKind == JsonValueKind.Object && - !TryTraverseSchemaRecursively( - filePath, - BuildConditionalSchemaPath(displayPath, "then"), - thenElement, - nodeValidator, - out diagnostic)) - { - return false; - } - - if (element.TryGetProperty("else", out var elseElement) && - elseElement.ValueKind == JsonValueKind.Object && - !TryTraverseSchemaRecursively( - filePath, - BuildConditionalSchemaPath(displayPath, "else"), - elseElement, - nodeValidator, - out diagnostic)) - { - return false; - } - } - - if (element.TryGetProperty("not", out var notElement) && - notElement.ValueKind == JsonValueKind.Object && - !TryTraverseSchemaRecursively( - filePath, - $"{displayPath}[not]", - notElement, - nodeValidator, - out diagnostic)) + if (IsSchemaType(schemaType, "object") && + !TryTraverseObjectChildren(filePath, displayPath, element, nodeValidator, out diagnostic)) { return false; } - if (!string.Equals(schemaType, "array", StringComparison.Ordinal)) + if (!TryTraverseSingleSchemaProperty(filePath, $"{displayPath}[not]", element, "not", nodeValidator, out diagnostic)) + { + return false; + } + + return !IsSchemaType(schemaType, "array") || + TryTraverseArrayChildren(filePath, displayPath, element, nodeValidator, out diagnostic); + } + + /// + /// Traverses every object-only child schema keyword in the same order as the parser observes properties. + /// + /// Schema file path. + /// Logical object path. + /// Object schema node. + /// Current validation callback. + /// First child validation diagnostic. + /// when all object child schemas pass validation. + private static bool TryTraverseObjectChildren( + string filePath, + string displayPath, + JsonElement element, + Func nodeValidator, + out Diagnostic? diagnostic) + { + if (!TryTraverseObjectProperties(filePath, displayPath, element, nodeValidator, out diagnostic)) + { + return false; + } + + if (!TryTraverseDependentSchemas(filePath, displayPath, element, nodeValidator, out diagnostic)) + { + return false; + } + + return TryTraverseAllOfEntries(filePath, displayPath, element, nodeValidator, out diagnostic) && + TryTraverseObjectSchemaBranches(filePath, displayPath, element, nodeValidator, out diagnostic); + } + + /// + /// Traverses nested object properties in declaration order. + /// + /// Schema file path. + /// Logical object path. + /// Object schema node. + /// Current validation callback. + /// First property validation diagnostic. + /// when every property subtree passes validation. + private static bool TryTraverseObjectProperties( + string filePath, + string displayPath, + JsonElement element, + Func nodeValidator, + out Diagnostic? diagnostic) + { + diagnostic = null; + if (!element.TryGetProperty("properties", out var propertiesElement) || + propertiesElement.ValueKind != JsonValueKind.Object) { return true; } - if (element.TryGetProperty("items", out var itemsElement) && - itemsElement.ValueKind == JsonValueKind.Object && - !TryTraverseSchemaRecursively( - filePath, - $"{displayPath}[]", - itemsElement, - nodeValidator, - out diagnostic)) + foreach (var property in propertiesElement.EnumerateObject()) { - return false; - } - - if (element.TryGetProperty("contains", out var containsElement) && - containsElement.ValueKind == JsonValueKind.Object && - !TryTraverseSchemaRecursively( - filePath, - $"{displayPath}[contains]", - containsElement, - nodeValidator, - out diagnostic)) - { - return false; + if (!TryTraverseSchemaRecursively( + filePath, + CombinePath(displayPath, property.Name), + property.Value, + nodeValidator, + out diagnostic)) + { + return false; + } } return true; } + /// + /// Traverses object dependentSchemas entries that are themselves schema objects. + /// + /// Schema file path. + /// Logical object path. + /// Object schema node. + /// Current validation callback. + /// First dependent schema validation diagnostic. + /// when every dependent schema subtree passes validation. + private static bool TryTraverseDependentSchemas( + string filePath, + string displayPath, + JsonElement element, + Func nodeValidator, + out Diagnostic? diagnostic) + { + diagnostic = null; + if (!element.TryGetProperty("dependentSchemas", out var dependentSchemasElement) || + dependentSchemasElement.ValueKind != JsonValueKind.Object) + { + return true; + } + + foreach (var dependentSchema in dependentSchemasElement.EnumerateObject()) + { + if (dependentSchema.Value.ValueKind != JsonValueKind.Object) + { + continue; + } + + if (!TryTraverseSchemaRecursively( + filePath, + $"{displayPath}[dependentSchemas:{dependentSchema.Name}]", + dependentSchema.Value, + nodeValidator, + out diagnostic)) + { + return false; + } + } + + return true; + } + + /// + /// Traverses object allOf entries while preserving their numeric path segments. + /// + /// Schema file path. + /// Logical object path. + /// Object schema node. + /// Current validation callback. + /// First allOf validation diagnostic. + /// when every object allOf entry passes validation. + private static bool TryTraverseAllOfEntries( + string filePath, + string displayPath, + JsonElement element, + Func nodeValidator, + out Diagnostic? diagnostic) + { + diagnostic = null; + if (!element.TryGetProperty("allOf", out var allOfElement) || + allOfElement.ValueKind != JsonValueKind.Array) + { + return true; + } + + var allOfIndex = 0; + foreach (var allOfSchema in allOfElement.EnumerateArray()) + { + if (allOfSchema.ValueKind != JsonValueKind.Object) + { + allOfIndex++; + continue; + } + + if (!TryTraverseSchemaRecursively( + filePath, + BuildAllOfEntryPath(displayPath, allOfIndex), + allOfSchema, + nodeValidator, + out diagnostic)) + { + return false; + } + + allOfIndex++; + } + + return true; + } + + /// + /// Traverses the object-focused conditional schema branches. + /// + /// Schema file path. + /// Logical object path. + /// Object schema node. + /// Current validation callback. + /// First branch validation diagnostic. + /// when all present conditional branches pass validation. + private static bool TryTraverseObjectSchemaBranches( + string filePath, + string displayPath, + JsonElement element, + Func nodeValidator, + out Diagnostic? diagnostic) + { + return TryTraverseSingleSchemaProperty( + filePath, + BuildConditionalSchemaPath(displayPath, "if"), + element, + "if", + nodeValidator, + out diagnostic) && + TryTraverseSingleSchemaProperty( + filePath, + BuildConditionalSchemaPath(displayPath, "then"), + element, + "then", + nodeValidator, + out diagnostic) && + TryTraverseSingleSchemaProperty( + filePath, + BuildConditionalSchemaPath(displayPath, "else"), + element, + "else", + nodeValidator, + out diagnostic); + } + + /// + /// Traverses a single child schema property when it is present and object-shaped. + /// + /// Schema file path. + /// Logical path assigned to the child schema. + /// Parent schema node. + /// Child schema keyword. + /// Current validation callback. + /// Child validation diagnostic. + /// when the child is absent or passes validation. + private static bool TryTraverseSingleSchemaProperty( + string filePath, + string childDisplayPath, + JsonElement element, + string propertyName, + Func nodeValidator, + out Diagnostic? diagnostic) + { + diagnostic = null; + return !element.TryGetProperty(propertyName, out var childElement) || + childElement.ValueKind != JsonValueKind.Object || + TryTraverseSchemaRecursively(filePath, childDisplayPath, childElement, nodeValidator, out diagnostic); + } + + /// + /// Traverses array items and contains child schemas. + /// + /// Schema file path. + /// Logical array path. + /// Array schema node. + /// Current validation callback. + /// First array child validation diagnostic. + /// when all array child schemas pass validation. + private static bool TryTraverseArrayChildren( + string filePath, + string displayPath, + JsonElement element, + Func nodeValidator, + out Diagnostic? diagnostic) + { + return TryTraverseSingleSchemaProperty(filePath, $"{displayPath}[]", element, "items", nodeValidator, out diagnostic) && + TryTraverseSingleSchemaProperty( + filePath, + $"{displayPath}[contains]", + element, + "contains", + nodeValidator, + out diagnostic); + } + /// /// 为对象级 allOf 条目生成与运行时一致的逻辑路径。 /// @@ -1880,157 +2081,235 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator /// /// Schema 文件路径。 /// 属性 JSON 节点。 - /// 属性是否必填。 - /// 逻辑字段路径。 - /// CLR 属性名。 - /// 标题元数据。 - /// 说明元数据。 - /// 目标引用表名称。 - /// 是否为索引查找。 + /// 属性解析共享上下文。 /// 解析后的属性信息或诊断。 private static ParsedPropertyResult ParseArrayProperty( string filePath, JsonProperty property, - bool isRequired, - string displayPath, - string propertyName, - string? title, - string? description, - string? refTableName, - bool isIndexedLookup) + PropertyParseContext context) { - if (isIndexedLookup) + if (context.IsIndexedLookup) { return ParsedPropertyResult.FromDiagnostic( - CreateInvalidLookupIndexDiagnostic(filePath, displayPath, LookupIndexTopLevelScalarOnlyMessage)); + CreateInvalidLookupIndexDiagnostic(filePath, context.DisplayPath, LookupIndexTopLevelScalarOnlyMessage)); } - if (!property.Value.TryGetProperty("items", out var itemsElement) || - itemsElement.ValueKind != JsonValueKind.Object || - !itemsElement.TryGetProperty("type", out var itemTypeElement) || - itemTypeElement.ValueKind != JsonValueKind.String) + if (!TryGetArrayItemSchema(filePath, property, context.DisplayPath, out var itemsElement, out var itemType, out var diagnostic)) { - return ParsedPropertyResult.FromDiagnostic( - Diagnostic.Create( - ConfigSchemaDiagnostics.UnsupportedPropertyType, - CreateFileLocation(filePath), - Path.GetFileName(filePath), - displayPath, - "array")); + return ParsedPropertyResult.FromDiagnostic(diagnostic!); } - var itemType = itemTypeElement.GetString() ?? string.Empty; - if (!TryValidateStringFormatMetadata(filePath, $"{displayPath}[]", itemsElement, itemType, + if (!TryValidateStringFormatMetadata(filePath, $"{context.DisplayPath}[]", itemsElement, itemType, out var formatDiagnostic)) { return ParsedPropertyResult.FromDiagnostic(formatDiagnostic!); } - switch (itemType) + return itemType switch { - case "integer": - case "number": - case "boolean": - case "string": - var itemClrType = itemType switch - { - "integer" => "int", - "number" => "double", - "boolean" => "bool", - _ => "string" - }; + "integer" or "number" or "boolean" or "string" => + CreateScalarArrayPropertyResult(property, context, itemsElement, itemType), + "object" => CreateObjectArrayPropertyResult(filePath, property, context, itemsElement), + _ => ParsedPropertyResult.FromDiagnostic(CreateUnsupportedArrayItemDiagnostic( + filePath, + context.DisplayPath, + $"array<{itemType}>")), + }; + } - return ParsedPropertyResult.FromProperty(new SchemaPropertySpec( - property.Name, - displayPath, - propertyName, - isRequired, - title, - description, - false, - new SchemaTypeSpec( - SchemaNodeKind.Array, - "array", - $"global::System.Collections.Generic.IReadOnlyList<{itemClrType}>", - TryBuildArrayInitializer(property.Value, itemType, itemClrType) ?? - $" = global::System.Array.Empty<{itemClrType}>();", - TryBuildEnumDocumentation(property.Value, "array") ?? - TryBuildEnumDocumentation(itemsElement, itemType), - TryBuildConstraintDocumentation(property.Value, "array"), - refTableName, - null, - new SchemaTypeSpec( - SchemaNodeKind.Scalar, - itemType, - itemClrType, - null, - TryBuildEnumDocumentation(itemsElement, itemType), - TryBuildConstraintDocumentation(itemsElement, itemType), - refTableName, - null, - null)))); - - case "object": - if (!string.IsNullOrWhiteSpace(refTableName)) - { - return ParsedPropertyResult.FromDiagnostic( - Diagnostic.Create( - ConfigSchemaDiagnostics.UnsupportedPropertyType, - CreateFileLocation(filePath), - Path.GetFileName(filePath), - displayPath, - "array-ref")); - } - - var objectResult = ParseObjectSpec( - filePath, - itemsElement, - $"{displayPath}[]", - $"{propertyName}ItemConfig"); - if (objectResult.Diagnostic is not null) - { - return ParsedPropertyResult.FromDiagnostic(objectResult.Diagnostic); - } - - var objectSpec = objectResult.Object!; - return ParsedPropertyResult.FromProperty(new SchemaPropertySpec( - property.Name, - displayPath, - propertyName, - isRequired, - title, - description, - false, - new SchemaTypeSpec( - SchemaNodeKind.Array, - "array", - $"global::System.Collections.Generic.IReadOnlyList<{objectSpec.ClassName}>", - $" = global::System.Array.Empty<{objectSpec.ClassName}>();", - TryBuildEnumDocumentation(property.Value, "array") ?? - TryBuildEnumDocumentation(itemsElement, "object"), - TryBuildConstraintDocumentation(property.Value, "array"), - null, - null, - new SchemaTypeSpec( - SchemaNodeKind.Object, - "object", - objectSpec.ClassName, - null, - TryBuildEnumDocumentation(itemsElement, "object"), - null, - null, - objectSpec, - null)))); - - default: - return ParsedPropertyResult.FromDiagnostic( - Diagnostic.Create( - ConfigSchemaDiagnostics.UnsupportedPropertyType, - CreateFileLocation(filePath), - Path.GetFileName(filePath), - displayPath, - $"array<{itemType}>")); + /// + /// Reads the items.type declaration required by the supported array schema subset. + /// + /// Schema file path used for diagnostics. + /// Array property JSON node. + /// Logical schema path. + /// Array item schema node when present. + /// Array item schema type when present. + /// Diagnostic explaining an invalid declaration. + /// when the array item schema is supported by the parser. + private static bool TryGetArrayItemSchema( + string filePath, + JsonProperty property, + string displayPath, + out JsonElement itemsElement, + out string itemType, + out Diagnostic? diagnostic) + { + if (property.Value.TryGetProperty("items", out itemsElement) && + itemsElement.ValueKind == JsonValueKind.Object && + itemsElement.TryGetProperty("type", out var itemTypeElement) && + itemTypeElement.ValueKind == JsonValueKind.String) + { + itemType = itemTypeElement.GetString() ?? string.Empty; + diagnostic = null; + return true; } + + itemType = string.Empty; + var itemDescription = "array"; + if (itemsElement.ValueKind == JsonValueKind.Object && + itemsElement.TryGetProperty("type", out var unsupportedTypeElement) && + unsupportedTypeElement.ValueKind == JsonValueKind.String) + { + itemDescription = $"array<{unsupportedTypeElement.GetString() ?? string.Empty}>"; + } + + diagnostic = CreateUnsupportedArrayItemDiagnostic(filePath, displayPath, itemDescription); + return false; + } + + /// + /// Builds an array property whose items are scalar values. + /// + /// Array property JSON node. + /// Property parsing context. + /// Array item schema node. + /// Array item scalar schema type. + /// Parsed array property model. + private static ParsedPropertyResult CreateScalarArrayPropertyResult( + JsonProperty property, + PropertyParseContext context, + JsonElement itemsElement, + string itemType) + { + var itemClrType = itemType switch + { + "integer" => "int", + "number" => "double", + "boolean" => "bool", + _ => "string" + }; + + return ParsedPropertyResult.FromProperty(new SchemaPropertySpec( + property.Name, + context.DisplayPath, + context.PropertyName, + context.IsRequired, + context.Title, + context.Description, + false, + new SchemaTypeSpec( + SchemaNodeKind.Array, + "array", + $"global::System.Collections.Generic.IReadOnlyList<{itemClrType}>", + TryBuildArrayInitializer(property.Value, itemType, itemClrType) ?? + $" = global::System.Array.Empty<{itemClrType}>();", + TryBuildEnumDocumentation(property.Value, "array") ?? + TryBuildEnumDocumentation(itemsElement, itemType), + TryBuildConstraintDocumentation(property.Value, "array"), + context.RefTableName, + null, + new SchemaTypeSpec( + SchemaNodeKind.Scalar, + itemType, + itemClrType, + null, + TryBuildEnumDocumentation(itemsElement, itemType), + TryBuildConstraintDocumentation(itemsElement, itemType), + context.RefTableName, + null, + null)))); + } + + /// + /// Builds an array property whose items are nested object values. + /// + /// Schema file path used for diagnostics. + /// Array property JSON node. + /// Property parsing context. + /// Array item schema node. + /// Parsed array property model or an object-array diagnostic. + private static ParsedPropertyResult CreateObjectArrayPropertyResult( + string filePath, + JsonProperty property, + PropertyParseContext context, + JsonElement itemsElement) + { + if (!string.IsNullOrWhiteSpace(context.RefTableName)) + { + return ParsedPropertyResult.FromDiagnostic( + CreateUnsupportedArrayItemDiagnostic(filePath, context.DisplayPath, "array-ref")); + } + + var objectResult = ParseObjectSpec( + filePath, + itemsElement, + $"{context.DisplayPath}[]", + $"{context.PropertyName}ItemConfig"); + if (objectResult.Diagnostic is not null) + { + return ParsedPropertyResult.FromDiagnostic(objectResult.Diagnostic); + } + + return ParsedPropertyResult.FromProperty(CreateObjectArrayPropertySpec( + property, + context, + itemsElement, + objectResult.Object!)); + } + + /// + /// Creates the nested object array property model after the item object has been parsed. + /// + /// Array property JSON node. + /// Property parsing context. + /// Array item schema node. + /// Parsed item object model. + /// Completed object array property model. + private static SchemaPropertySpec CreateObjectArrayPropertySpec( + JsonProperty property, + PropertyParseContext context, + JsonElement itemsElement, + SchemaObjectSpec objectSpec) + { + return new SchemaPropertySpec( + property.Name, + context.DisplayPath, + context.PropertyName, + context.IsRequired, + context.Title, + context.Description, + false, + new SchemaTypeSpec( + SchemaNodeKind.Array, + "array", + $"global::System.Collections.Generic.IReadOnlyList<{objectSpec.ClassName}>", + $" = global::System.Array.Empty<{objectSpec.ClassName}>();", + TryBuildEnumDocumentation(property.Value, "array") ?? + TryBuildEnumDocumentation(itemsElement, "object"), + TryBuildConstraintDocumentation(property.Value, "array"), + null, + null, + new SchemaTypeSpec( + SchemaNodeKind.Object, + "object", + objectSpec.ClassName, + null, + TryBuildEnumDocumentation(itemsElement, "object"), + null, + null, + objectSpec, + null))); + } + + /// + /// Creates a diagnostic for unsupported or malformed array item declarations. + /// + /// Schema file path used for diagnostics. + /// Logical schema path. + /// Unsupported item declaration text. + /// Unsupported property type diagnostic. + private static Diagnostic CreateUnsupportedArrayItemDiagnostic( + string filePath, + string displayPath, + string itemDescription) + { + return Diagnostic.Create( + ConfigSchemaDiagnostics.UnsupportedPropertyType, + CreateFileLocation(filePath), + Path.GetFileName(filePath), + displayPath, + itemDescription); } /// @@ -2486,6 +2765,22 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator builder.AppendLine(); builder.AppendLine($"namespace {GeneratedNamespace};"); builder.AppendLine(); + AppendGeneratedConfigCatalogType(builder, schemas); + builder.AppendLine(); + AppendGeneratedConfigRegistrationOptionsType(builder, schemas); + builder.AppendLine(); + AppendGeneratedConfigRegistrationExtensionsType(builder, schemas); + + return builder.ToString().TrimEnd(); + } + + /// + /// Emits the generated catalog type that exposes schema metadata and filtering helpers. + /// + /// Output buffer. + /// Successfully parsed schemas for the current compilation. + private static void AppendGeneratedConfigCatalogType(StringBuilder builder, IReadOnlyList schemas) + { builder.AppendLine("/// "); builder.AppendLine( "/// Provides a project-level catalog for every config table generated from the current consumer project's schemas."); @@ -2753,7 +3048,17 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator builder.AppendLine(" return false;"); builder.AppendLine(" }"); builder.AppendLine("}"); - builder.AppendLine(); + } + + /// + /// Emits the options type consumed by aggregate generated table registration. + /// + /// Output buffer. + /// Successfully parsed schemas for the current compilation. + private static void AppendGeneratedConfigRegistrationOptionsType( + StringBuilder builder, + IReadOnlyList schemas) + { builder.AppendLine("/// "); builder.AppendLine( "/// Captures optional per-table registration overrides for the generated aggregate registration entry point."); @@ -2780,7 +3085,19 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator builder.AppendLine(" /// "); builder.AppendLine( " public global::System.Predicate? TableFilter { get; init; }"); + AppendGeneratedConfigComparerOptions(builder, schemas); + builder.AppendLine("}"); + } + /// + /// Emits one optional comparer property per generated table for aggregate registration. + /// + /// Output buffer. + /// Successfully parsed schemas for the current compilation. + private static void AppendGeneratedConfigComparerOptions( + StringBuilder builder, + IReadOnlyList schemas) + { if (schemas.Count > 0) { builder.AppendLine(); @@ -2801,15 +3118,35 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator builder.AppendLine(); } } + } - builder.AppendLine("}"); - builder.AppendLine(); + /// + /// Emits extension methods that register every generated config table for the current compilation. + /// + /// Output buffer. + /// Successfully parsed schemas for the current compilation. + private static void AppendGeneratedConfigRegistrationExtensionsType( + StringBuilder builder, + IReadOnlyList schemas) + { builder.AppendLine("/// "); builder.AppendLine( "/// Provides a single extension method that registers every generated config table discovered in the current consumer project."); builder.AppendLine("/// "); builder.AppendLine("public static class GeneratedConfigRegistrationExtensions"); builder.AppendLine("{"); + AppendRegisterAllGeneratedConfigTablesMethod(builder); + builder.AppendLine(); + AppendRegisterAllGeneratedConfigTablesWithOptionsMethod(builder, schemas); + builder.AppendLine("}"); + } + + /// + /// Emits the aggregate registration overload that uses default options. + /// + /// Output buffer. + private static void AppendRegisterAllGeneratedConfigTablesMethod(StringBuilder builder) + { builder.AppendLine(" /// "); builder.AppendLine( " /// Registers all generated config tables using schema-derived conventions so bootstrap code can stay one-line even as schemas grow."); @@ -2830,7 +3167,17 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator builder.AppendLine(); builder.AppendLine(" return RegisterAllGeneratedConfigTables(loader, options: null);"); builder.AppendLine(" }"); - builder.AppendLine(); + } + + /// + /// Emits the aggregate registration overload that honors generated filters and comparer overrides. + /// + /// Output buffer. + /// Successfully parsed schemas for the current compilation. + private static void AppendRegisterAllGeneratedConfigTablesWithOptionsMethod( + StringBuilder builder, + IReadOnlyList schemas) + { builder.AppendLine(" /// "); builder.AppendLine( " /// Registers all generated config tables while preserving optional per-table overrides such as custom key comparers."); @@ -2869,8 +3216,6 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator builder.AppendLine(" return loader;"); builder.AppendLine(" }"); - builder.AppendLine("}"); - return builder.ToString().TrimEnd(); } /// @@ -2881,6 +3226,20 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator private static void AppendYamlSerializationHelpers( StringBuilder builder, SchemaFileSpec schema) + { + AppendYamlSerializeMethod(builder, schema); + builder.AppendLine(); + AppendYamlPathMethods(builder); + builder.AppendLine(); + AppendYamlValidationMethods(builder); + } + + /// + /// Emits the generated YAML serialization method. + /// + /// Output buffer. + /// Generator-level schema model. + private static void AppendYamlSerializeMethod(StringBuilder builder, SchemaFileSpec schema) { builder.AppendLine(" /// "); builder.AppendLine( @@ -2895,7 +3254,14 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator builder.AppendLine(" {"); builder.AppendLine(" return global::GFramework.Game.Config.YamlConfigTextSerializer.Serialize(config);"); builder.AppendLine(" }"); - builder.AppendLine(); + } + + /// + /// Emits generated helpers that resolve config and schema paths at runtime. + /// + /// Output buffer. + private static void AppendYamlPathMethods(StringBuilder builder) + { builder.AppendLine(" /// "); builder.AppendLine( " /// Resolves the absolute config directory path by combining the caller-supplied config root with the generated relative directory."); @@ -2925,7 +3291,25 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator builder.AppendLine( " return GeneratedConfigCatalog.ResolveAbsolutePath(configRootPath, Metadata.SchemaRelativePath);"); builder.AppendLine(" }"); + } + + /// + /// Emits generated synchronous and asynchronous YAML validation methods. + /// + /// Output buffer. + private static void AppendYamlValidationMethods(StringBuilder builder) + { + AppendValidateYamlMethod(builder); builder.AppendLine(); + AppendValidateYamlAsyncMethod(builder); + } + + /// + /// Emits the generated synchronous YAML validation method. + /// + /// Output buffer. + private static void AppendValidateYamlMethod(StringBuilder builder) + { builder.AppendLine(" /// "); builder.AppendLine( " /// Validates YAML text against the generated schema file located under the supplied config root directory."); @@ -2950,7 +3334,14 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator builder.AppendLine(" yamlPath,"); builder.AppendLine(" yamlText);"); builder.AppendLine(" }"); - builder.AppendLine(); + } + + /// + /// Emits the generated asynchronous YAML validation method. + /// + /// Output buffer. + private static void AppendValidateYamlAsyncMethod(StringBuilder builder) + { builder.AppendLine(" /// "); builder.AppendLine( " /// Asynchronously validates YAML text against the generated schema file located under the supplied config root directory."); @@ -3091,6 +3482,19 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator private static void AppendSharedLookupIndexBuilderMethod( StringBuilder builder, SchemaFileSpec schema) + { + AppendSharedLookupIndexBuilderDocumentation(builder, schema); + AppendSharedLookupIndexBuilderBody(builder, schema); + } + + /// + /// Emits XML documentation and signature for the shared generated lookup index builder. + /// + /// Output buffer. + /// Generator-level schema model. + private static void AppendSharedLookupIndexBuilderDocumentation( + StringBuilder builder, + SchemaFileSpec schema) { builder.AppendLine(" /// "); builder.AppendLine( @@ -3113,6 +3517,17 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator builder.AppendLine( " var buckets = new global::System.Collections.Generic.Dictionary>();"); + } + + /// + /// Emits the implementation body for the shared generated lookup index builder. + /// + /// Output buffer. + /// Generator-level schema model. + private static void AppendSharedLookupIndexBuilderBody( + StringBuilder builder, + SchemaFileSpec schema) + { builder.AppendLine(); builder.AppendLine( " // Capture the current table snapshot once so indexed lookups stay deterministic for this wrapper instance."); @@ -3184,6 +3599,21 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator builder.AppendLine( $" public global::System.Collections.Generic.IReadOnlyList<{schema.ClassName}> FindBy{property.PropertyName}({property.TypeSpec.ClrType} value)"); builder.AppendLine(" {"); + AppendFindByPropertyBody(builder, schema, property); + builder.AppendLine(" }"); + } + + /// + /// Emits the body of a generated FindBy* lookup helper. + /// + /// Output buffer. + /// Generator-level schema model. + /// Property model used by the lookup helper. + private static void AppendFindByPropertyBody( + StringBuilder builder, + SchemaFileSpec schema, + SchemaPropertySpec property) + { if (property.IsIndexedLookup) { if (RequiresIndexedLookupNullGuard(property.TypeSpec)) @@ -3222,8 +3652,6 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator builder.AppendLine( $" return matches.Count == 0 ? global::System.Array.Empty<{schema.ClassName}>() : matches.AsReadOnly();"); } - - builder.AppendLine(" }"); } /// @@ -3262,6 +3690,21 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator builder.AppendLine( $" public bool TryFindFirstBy{property.PropertyName}({property.TypeSpec.ClrType} value, out {schema.ClassName}? result)"); builder.AppendLine(" {"); + AppendTryFindFirstByPropertyBody(builder, schema, property); + builder.AppendLine(" }"); + } + + /// + /// Emits the body of a generated TryFindFirstBy* lookup helper. + /// + /// Output buffer. + /// Generator-level schema model. + /// Property model used by the lookup helper. + private static void AppendTryFindFirstByPropertyBody( + StringBuilder builder, + SchemaFileSpec schema, + SchemaPropertySpec property) + { if (property.IsIndexedLookup) { if (RequiresIndexedLookupNullGuard(property.TypeSpec)) @@ -3301,8 +3744,6 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator builder.AppendLine(" result = null;"); builder.AppendLine(" return false;"); } - - builder.AppendLine(" }"); } /// @@ -3365,6 +3806,31 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator int indentationLevel) { var indent = new string(' ', indentationLevel * 4); + AppendObjectTypeHeader(builder, objectSpec, fileName, title, description, isRoot, indent); + AppendObjectTypeProperties(builder, objectSpec, indentationLevel); + AppendNestedObjectTypes(builder, objectSpec, fileName, indentationLevel); + builder.AppendLine($"{indent}}}"); + } + + /// + /// 生成单个配置对象类型的 XML 文档和类型声明。 + /// + /// 输出缓冲区。 + /// 要生成的对象类型。 + /// Schema 文件名。 + /// 对象标题元数据。 + /// 对象说明元数据。 + /// 是否为根配置类型。 + /// 当前缩进。 + private static void AppendObjectTypeHeader( + StringBuilder builder, + SchemaObjectSpec objectSpec, + string fileName, + string? title, + string? description, + bool isRoot, + string indent) + { builder.AppendLine($"{indent}/// "); if (isRoot) { @@ -3392,7 +3858,19 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator builder.AppendLine($"{indent}public sealed partial class {objectSpec.ClassName}"); builder.AppendLine($"{indent}{{"); + } + /// + /// 生成配置对象直接拥有的 CLR 属性。 + /// + /// 输出缓冲区。 + /// 要生成的对象类型。 + /// 当前缩进层级。 + private static void AppendObjectTypeProperties( + StringBuilder builder, + SchemaObjectSpec objectSpec, + int indentationLevel) + { for (var index = 0; index < objectSpec.Properties.Count; index++) { var property = objectSpec.Properties[index]; @@ -3409,7 +3887,21 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator builder.AppendLine(); builder.AppendLine(); } + } + /// + /// 在直接属性之后递归生成嵌套对象类型,保持输出顺序稳定。 + /// + /// 输出缓冲区。 + /// 父对象类型。 + /// Schema 文件名。 + /// 父对象缩进层级。 + private static void AppendNestedObjectTypes( + StringBuilder builder, + SchemaObjectSpec objectSpec, + string fileName, + int indentationLevel) + { var nestedTypes = CollectNestedTypes(objectSpec.Properties).ToArray(); for (var index = 0; index < nestedTypes.Length; index++) { @@ -3428,8 +3920,6 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator builder.AppendLine(); } } - - builder.AppendLine($"{indent}}}"); } /// @@ -3957,13 +4447,38 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator private static string? TryBuildConstraintDocumentation(JsonElement element, string schemaType) { var parts = new List(); + AddConstDocumentationPart(element, schemaType, parts); + AddNumericConstraintDocumentationParts(element, schemaType, parts); + AddStringConstraintDocumentationParts(element, schemaType, parts); + AddArrayConstraintDocumentationParts(element, schemaType, parts); + AddObjectConstraintDocumentationParts(element, schemaType, parts); + return parts.Count > 0 ? string.Join(", ", parts) : null; + } + + /// + /// Adds const documentation when the schema value matches the current type. + /// + /// Schema node. + /// Current schema type. + /// Mutable documentation parts. + private static void AddConstDocumentationPart(JsonElement element, string schemaType, List parts) + { var constDocumentation = TryBuildConstDocumentation(element, schemaType); if (constDocumentation is not null) { parts.Add($"const = {constDocumentation}"); } + } + /// + /// Adds numeric range and step constraints to generated XML documentation. + /// + /// Schema node. + /// Current schema type. + /// Mutable documentation parts. + private static void AddNumericConstraintDocumentationParts(JsonElement element, string schemaType, List parts) + { if (IsNumericSchemaType(schemaType) && TryGetFiniteNumber(element, "minimum", out var minimum)) { @@ -3994,7 +4509,16 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator { parts.Add($"multipleOf = {multipleOf.ToString(CultureInfo.InvariantCulture)}"); } + } + /// + /// Adds string length, pattern, and stable format constraints to generated XML documentation. + /// + /// Schema node. + /// Current schema type. + /// Mutable documentation parts. + private static void AddStringConstraintDocumentationParts(JsonElement element, string schemaType, List parts) + { if (IsSchemaType(schemaType, "string") && TryGetNonNegativeInt32(element, "minLength", out var minLength)) { @@ -4024,7 +4548,16 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator parts.Add($"format = '{formatName}'"); } } + } + /// + /// Adds array count, uniqueness, contains, and negation constraints to generated XML documentation. + /// + /// Schema node. + /// Current schema type. + /// Mutable documentation parts. + private static void AddArrayConstraintDocumentationParts(JsonElement element, string schemaType, List parts) + { if (IsSchemaType(schemaType, "array") && TryGetNonNegativeInt32(element, "minItems", out var minItems)) { @@ -4070,7 +4603,16 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator { parts.Add($"maxContains = {maxContains.ToString(CultureInfo.InvariantCulture)}"); } + } + /// + /// Adds object cardinality and composition constraints to generated XML documentation. + /// + /// Schema node. + /// Current schema type. + /// Mutable documentation parts. + private static void AddObjectConstraintDocumentationParts(JsonElement element, string schemaType, List parts) + { if (IsSchemaType(schemaType, "object") && TryGetNonNegativeInt32(element, "minProperties", out var minProperties)) { @@ -4109,8 +4651,6 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator parts.Add($"if/then/else = {conditionalDocumentation}"); } } - - return parts.Count > 0 ? string.Join(", ", parts) : null; } /// @@ -4663,6 +5203,25 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator SchemaObjectSpec? NestedObject, SchemaTypeSpec? ItemTypeSpec); + /// + /// Shared state extracted before dispatching one property to a schema-type-specific parser. + /// + /// Logical schema path. + /// Generated CLR property name. + /// Whether the property is required. + /// Optional schema title. + /// Optional schema description. + /// Optional referenced table metadata. + /// Whether the property declares a generated exact-match index. + private sealed record PropertyParseContext( + string DisplayPath, + string PropertyName, + bool IsRequired, + string? Title, + string? Description, + string? RefTableName, + bool IsIndexedLookup); + /// /// 生成代码前的跨表引用字段种子信息。 /// 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 d27049dc..88a0b945 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 @@ -7,8 +7,8 @@ ## 当前恢复点 -- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-019` -- 当前阶段:`Phase 19` +- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-020` +- 当前阶段:`Phase 20` - 当前焦点: - 已完成 `GFramework.Core` 当前 `MA0016` / `MA0002` / `MA0015` / `MA0077` 低风险收口批次 - 已复核 `net10.0` 下的 `MA0158` 基线:`GFramework.Core` / `GFramework.Cqrs` 当前共有 `16` 个 object lock @@ -17,6 +17,8 @@ - 已完成 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs` 的 `MA0051` 结构拆分,生成输出保持不变 - 已完成 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs` 的 `MA0006` 低风险收口,schema 关键字比较显式使用 `StringComparison.Ordinal` + - 已完成 `SchemaConfigGenerator.cs` 的第一批 `MA0051` 结构拆分:schema 入口解析、属性解析、schema 遍历、数组属性解析、 + 约束文档生成与若干生成代码发射 helper 已拆出语义阶段 - `LoggingConfiguration`、`FilterConfiguration` 与 `CollectionExtensions` 已改用集合抽象接口,并保留内部具体集合默认值 - `CoroutineScheduler` 的 tag/group 字典已显式使用 `StringComparer.Ordinal`,保持既有区分大小写语义 - `EasyEvents.AddEvent()` 的重复注册路径已改为状态冲突异常,避免把泛型类型参数伪装成方法参数名 @@ -24,7 +26,7 @@ - 当前 `GFramework.Core` `net8.0` warnings-only 基线已降到 `0` 条 - 当前 `GFramework.Core.SourceGenerators` warnings-only 基线已降到 `0` 条 - 当前 `GFramework.Cqrs.SourceGenerators` warnings-only 基线已降到 `0` 条 - - 当前 `GFramework.Game.SourceGenerators` warnings-only 基线已从 `46` 条降到 `19` 条,剩余均为 + - 当前 `GFramework.Game.SourceGenerators` warnings-only 基线已从 `46` 条降到 `9` 条,剩余均为 `SchemaConfigGenerator.cs` 的 `MA0051` - `GFramework.Godot` 的 `Timing.cs` 已同步适配新事件签名,但当前 worktree 的 Godot restore 资产仍受 Windows fallback package folder 干扰,独立 build 需在修复资产后补跑 - 后续继续按 warning 类型和数量批处理,而不是回退到按单文件切片推进 @@ -49,7 +51,7 @@ - 已完成当前 `GFramework.Core` `net8.0` 剩余低风险 analyzer warning 批次;warnings-only 基线已降到 `0` 条 - 已完成 `GFramework.Core.SourceGenerators` 中 `ContextAwareGenerator` 的剩余 `MA0051` 收口;warnings-only 基线已降到 `0` 条 - 已完成 `GFramework.Cqrs.SourceGenerators` 中 `CqrsHandlerRegistryGenerator` 的剩余 `MA0051` 收口;warnings-only 基线已降到 `0` 条 -- 已完成 `GFramework.Game.SourceGenerators` 中 `SchemaConfigGenerator` 的 `MA0006` 收口;warnings-only 基线剩余 `19` 条 +- 已完成 `GFramework.Game.SourceGenerators` 中 `SchemaConfigGenerator` 的第一批 `MA0051` 收口;warnings-only 基线剩余 `9` 条 `MA0051` ## 当前活跃事实 @@ -89,6 +91,8 @@ 通过拆分 handler 分析、运行时类型引用构造、注册器源码发射与精确反射注册输出阶段,清空该项目当前 `MA0051` - `RP-019` 转入 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs`,先完成低风险 `MA0006` 批次; 通过 schema 类型比较 helper 与显式 `StringComparison.Ordinal` 清空当前项目的 `MA0006` +- `RP-020` 继续拆分 `SchemaConfigGenerator.cs` 的 `MA0051` 热点,将当前项目 warnings-only 基线从 `19` 条降到 `9` 条, + 并用 focused schema generator tests 验证 50 个用例通过 - 当前工作树分支 `fix/analyzer-warning-reduction-batch` 已在 `ai-plan/public/README.md` 建立 topic 映射 ## 当前风险 @@ -216,13 +220,19 @@ - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~SchemaConfigGenerator -m:1 -p:RestoreFallbackFolders= -nologo` - 结果:`50 Passed`,`0 Failed` - 说明:测试项目构建仍显示既有 source generator test analyzer warning;不属于本轮写集 +- `RP-020` 的验证结果: + - `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"` + - 结果:`9 Warning(s)`,`0 Error(s)`;当前项目剩余 warning 均为 `SchemaConfigGenerator.cs` 的 `MA0051` + - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~SchemaConfigGenerator -m:1 -p:RestoreFallbackFolders= -nologo` + - 结果:`50 Passed`,`0 Failed` + - 说明:测试项目构建仍显示既有 source generator test analyzer warning;不属于本轮写集 - active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史 ## 下一步 1. 若要继续该主题,先读 active tracking,再按需展开历史归档中的 warning 热点与验证记录 -2. 下一轮优先继续拆分 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs` 的 `MA0051`;建议先从 - `TryBuildConstraintDocumentation` 或 `GenerateConfigCatalogSource` 这类高收益方法切入 +2. 下一轮优先继续拆分 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs` 的剩余 `MA0051`;建议先从 + `GenerateBindingsClass`、`AppendGeneratedConfigCatalogType` 或对象/条件 schema target 验证方法切入 3. 若改回推进 `MA0158`,先设计 `net8.0` / `net9.0` / `net10.0` 多 target 条件编译方案,不直接批量替换共享源码中的 `object` lock 4. 若后续继续改动 `GFramework.Godot`,先修复该项目的 Linux 侧 restore 资产,再补跑独立 build 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 58b721a6..f43089c0 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 追踪 +## 2026-04-22 — RP-020 + +### 阶段:`SchemaConfigGenerator` 第一批 `MA0051` 结构拆分(RP-020) + +- 启动复核: + - 当前 worktree 仍映射到 `analyzer-warning-reduction` active topic + - `GFramework.Game.SourceGenerators` warnings-only build 复现 `19` 条 warning,全部为 + `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs` 的 `MA0051` +- 决策: + - 本轮继续低风险结构拆分,不改变 schema 支持范围、诊断 ID、生成类型形状或输出顺序 + - 未使用 subagent;critical path 是本地复现 warning、拆分语义阶段并用 focused schema generator tests 验证行为 +- 实施调整: + - 将 schema 入口解析拆为文本读取、root 验证、id key 验证和 `SchemaFileSpec` 构造阶段 + - 将属性解析拆为共享上下文提取、类型分派、标量/对象/数组属性构造 helper + - 将统一 schema 遍历拆为对象属性、dependentSchemas、allOf、条件分支、not、array items / contains 等遍历阶段 + - 将约束文档生成拆为 const、numeric、string、array、object 约束片段 + - 将 catalog/registration/YAML/lookup/object type 等生成代码发射路径中的小型高收益 helper 拆出 +- 验证结果: + - `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"` + - 结果:`9 Warning(s)`,`0 Error(s)`;当前项目剩余 warning 均为 `SchemaConfigGenerator.cs` 的 `MA0051` + - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~SchemaConfigGenerator -m:1 -p:RestoreFallbackFolders= -nologo` + - 结果:`50 Passed`,`0 Failed` + - 说明:测试项目构建仍显示既有 source generator test analyzer warning;不属于本轮写集 +- 下一步建议: + - 继续该主题时,优先拆分 `GenerateBindingsClass`、`AppendGeneratedConfigCatalogType` 或对象/条件 schema target 验证方法 + - 若转回 `MA0158`,仍需先设计多 target 条件编译方案,再考虑替换共享源码中的 `object` lock + ## 2026-04-22 — RP-019 ### 阶段:`SchemaConfigGenerator` 当前 `MA0006` 收口(RP-019) From 6d4f9f2f94ebb3d28bc811610a71664bdb3ed1ad Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 22 Apr 2026 12:54:58 +0800 Subject: [PATCH 06/13] =?UTF-8?q?fix(source-generators):=20=E6=94=B6?= =?UTF-8?q?=E5=8F=A3PR269=E7=94=9F=E6=88=90=E5=99=A8=E8=AF=84=E5=AE=A1?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构 CqrsHandlerRegistryGenerator 为按职责拆分的 partial 生成器文件,保留现有注册输出与 fallback 契约 - 修复 ContextAwareGenerator 生成字段命名冲突并为 SetContextProvider 补充运行时 null 校验与异常文档 - 补充 Option 的 XML remarks 契约说明与 ContextAwareGenerator 字段冲突快照测试 - 更新 analyzer-warning-reduction 跟踪与 trace,记录 PR #269 review follow-up 与验证结果 --- .../Rule/ContextAwareGenerator.cs | 38 +- GFramework.Core/Functional/Option.cs | 15 + .../CqrsHandlerRegistryGenerator.Models.cs | 254 +++ ...RegistryGenerator.RuntimeTypeReferences.cs | 283 ++++ ...HandlerRegistryGenerator.SourceEmission.cs | 851 ++++++++++ .../Cqrs/CqrsHandlerRegistryGenerator.cs | 1381 +---------------- .../ContextAwareGeneratorSnapshotTests.cs | 74 + .../CollisionProneRule.ContextAware.g.cs | 103 ++ .../MyRule.ContextAware.g.cs | 36 +- .../analyzer-warning-reduction-tracking.md | 20 +- .../analyzer-warning-reduction-trace.md | 35 + 11 files changed, 1675 insertions(+), 1415 deletions(-) create mode 100644 GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs create mode 100644 GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.RuntimeTypeReferences.cs create mode 100644 GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs create mode 100644 GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/CollisionProneRule.ContextAware.g.cs diff --git a/GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs b/GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs index 5e3a0356..65919b25 100644 --- a/GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs +++ b/GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs @@ -107,7 +107,7 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase sb.AppendLine( "/// 已缓存的实例上下文需要通过 显式覆盖。"); sb.AppendLine( - "/// 与手动继承 的路径相比,生成实现会使用 _contextSync 协调惰性初始化、provider 切换和显式上下文注入;"); + "/// 与手动继承 的路径相比,生成实现会使用 _gFrameworkContextAwareSync 协调惰性初始化、provider 切换和显式上下文注入;"); sb.AppendLine( "/// 则保持无锁的实例级缓存语义,更适合已经由调用方线程模型保证串行访问的简单场景。"); sb.AppendLine("/// "); @@ -151,10 +151,11 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase /// 字符串构建器。 private static void GenerateContextBackingFields(StringBuilder sb) { - sb.AppendLine(" private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _context;"); sb.AppendLine( - " private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? _contextProvider;"); - sb.AppendLine(" private static readonly object _contextSync = new();"); + " private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _gFrameworkContextAwareContext;"); + sb.AppendLine( + " private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? _gFrameworkContextAwareProvider;"); + sb.AppendLine(" private static readonly object _gFrameworkContextAwareSync = new();"); sb.AppendLine(); } @@ -177,7 +178,7 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase sb.AppendLine( " /// 或 不会自动清除此缓存;如需覆盖,请显式调用 IContextAware.SetContext(...)。"); sb.AppendLine( - " /// 当前实现还假设 可在持有 _contextSync 时安全执行;"); + " /// 当前实现还假设 可在持有 _gFrameworkContextAwareSync 时安全执行;"); sb.AppendLine( " /// 自定义 provider 不应在该调用链内重新进入当前类型的 provider 配置 API,且应避免引入与外部全局锁相互等待的锁顺序。"); sb.AppendLine(" /// "); @@ -185,7 +186,7 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase sb.AppendLine(" {"); sb.AppendLine(" get"); sb.AppendLine(" {"); - sb.AppendLine(" var context = _context;"); + sb.AppendLine(" var context = _gFrameworkContextAwareContext;"); sb.AppendLine(" if (context is not null)"); sb.AppendLine(" {"); sb.AppendLine(" return context;"); @@ -193,13 +194,13 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase sb.AppendLine(); sb.AppendLine(" // 在同一个同步域内协调懒加载与 provider 切换,避免读取到被并发重置的空提供者。"); sb.AppendLine( - " // provider 的 GetContext() 会在持有 _contextSync 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。"); - sb.AppendLine(" lock (_contextSync)"); + " // provider 的 GetContext() 会在持有 _gFrameworkContextAwareSync 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。"); + sb.AppendLine(" lock (_gFrameworkContextAwareSync)"); sb.AppendLine(" {"); sb.AppendLine( - " _contextProvider ??= new global::GFramework.Core.Architectures.GameContextProvider();"); - sb.AppendLine(" _context ??= _contextProvider.GetContext();"); - sb.AppendLine(" return _context;"); + " _gFrameworkContextAwareProvider ??= new global::GFramework.Core.Architectures.GameContextProvider();"); + sb.AppendLine(" _gFrameworkContextAwareContext ??= _gFrameworkContextAwareProvider.GetContext();"); + sb.AppendLine(" return _gFrameworkContextAwareContext;"); sb.AppendLine(" }"); sb.AppendLine(" }"); sb.AppendLine(" }"); @@ -216,6 +217,8 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase sb.AppendLine(" /// 配置当前生成类型共享的上下文提供者。"); sb.AppendLine(" /// "); sb.AppendLine(" /// 后续懒加载上下文时要使用的提供者实例。"); + sb.AppendLine( + " /// 为 null 时抛出。"); sb.AppendLine(" /// "); sb.AppendLine(" /// 该方法使用与 相同的同步锁,避免提供者切换与惰性初始化交错。"); sb.AppendLine( @@ -225,9 +228,10 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase sb.AppendLine( " public static void SetContextProvider(global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider provider)"); sb.AppendLine(" {"); - sb.AppendLine(" lock (_contextSync)"); + sb.AppendLine(" global::System.ArgumentNullException.ThrowIfNull(provider);"); + sb.AppendLine(" lock (_gFrameworkContextAwareSync)"); sb.AppendLine(" {"); - sb.AppendLine(" _contextProvider = provider;"); + sb.AppendLine(" _gFrameworkContextAwareProvider = provider;"); sb.AppendLine(" }"); sb.AppendLine(" }"); sb.AppendLine(); @@ -242,9 +246,9 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase sb.AppendLine(" /// "); sb.AppendLine(" public static void ResetContextProvider()"); sb.AppendLine(" {"); - sb.AppendLine(" lock (_contextSync)"); + sb.AppendLine(" lock (_gFrameworkContextAwareSync)"); sb.AppendLine(" {"); - sb.AppendLine(" _contextProvider = null;"); + sb.AppendLine(" _gFrameworkContextAwareProvider = null;"); sb.AppendLine(" }"); sb.AppendLine(" }"); sb.AppendLine(); @@ -316,9 +320,9 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase { case "SetContext": sb.AppendLine(" // 与 Context getter 共享同一同步协议,避免显式注入被并发懒加载覆盖。"); - sb.AppendLine(" lock (_contextSync)"); + sb.AppendLine(" lock (_gFrameworkContextAwareSync)"); sb.AppendLine(" {"); - sb.AppendLine(" _context = context;"); + sb.AppendLine(" _gFrameworkContextAwareContext = context;"); sb.AppendLine(" }"); break; diff --git a/GFramework.Core/Functional/Option.cs b/GFramework.Core/Functional/Option.cs index 31d18d2e..4e738625 100644 --- a/GFramework.Core/Functional/Option.cs +++ b/GFramework.Core/Functional/Option.cs @@ -16,6 +16,21 @@ namespace GFramework.Core.Functional; /// /// 表示可能存在或不存在的值,用于替代 null 引用的函数式编程类型 /// +/// +/// +/// 只表示两种显式状态:通过 创建的有值状态,以及 +/// 表示的无值状态;调用方不应把 当作 的别名。 +/// +/// +/// 会拒绝 ,因此引用类型和可空引用类型参数都必须包装真实值;访问方应优先通过 +/// 、模式匹配或 Match/Map 等函数式 API 消费结果,而不是假设默认值 +/// 与无值状态等价。 +/// +/// +/// 该结构体是不可变值类型;一旦创建,其状态与内部值不会再改变。但在 时, +/// 调用需要真实值的方法仍应遵守各成员自身的契约与异常说明。 +/// +/// /// 值的类型 public readonly struct Option : IEquatable> { diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs new file mode 100644 index 00000000..0b3ec0c7 --- /dev/null +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs @@ -0,0 +1,254 @@ +namespace GFramework.Cqrs.SourceGenerators.Cqrs; + +/// +/// 为当前编译程序集生成 CQRS 处理器注册器,以减少运行时的程序集反射扫描成本。 +/// +public sealed partial class CqrsHandlerRegistryGenerator +{ + private readonly record struct HandlerRegistrationSpec( + string HandlerInterfaceDisplayName, + string ImplementationTypeDisplayName, + string HandlerInterfaceLogName, + string ImplementationLogName); + + private readonly record struct ReflectedImplementationRegistrationSpec( + string HandlerInterfaceDisplayName, + string HandlerInterfaceLogName); + + private readonly record struct OrderedRegistrationSpec( + string HandlerInterfaceLogName, + OrderedRegistrationKind Kind, + int Index); + + private readonly record struct GeneratedRegistrySourceShape( + bool HasReflectedImplementationRegistrations, + bool HasPreciseReflectedRegistrations, + bool HasReflectionTypeLookups, + bool HasExternalAssemblyTypeLookups) + { + public bool RequiresRegistryAssemblyVariable => + HasReflectedImplementationRegistrations || + HasPreciseReflectedRegistrations || + HasReflectionTypeLookups; + } + + private enum OrderedRegistrationKind + { + Direct, + ReflectedImplementation, + PreciseReflected + } + + private sealed record RuntimeTypeReferenceSpec( + string? TypeDisplayName, + string? ReflectionTypeMetadataName, + string? ReflectionAssemblyName, + RuntimeTypeReferenceSpec? ArrayElementTypeReference, + int ArrayRank, + RuntimeTypeReferenceSpec? PointerElementTypeReference, + RuntimeTypeReferenceSpec? GenericTypeDefinitionReference, + ImmutableArray GenericTypeArguments) + { + public static RuntimeTypeReferenceSpec FromDirectReference(string typeDisplayName) + { + return new RuntimeTypeReferenceSpec( + typeDisplayName, + null, + null, + null, + 0, + null, + null, + ImmutableArray.Empty); + } + + public static RuntimeTypeReferenceSpec FromReflectionLookup(string reflectionTypeMetadataName) + { + return new RuntimeTypeReferenceSpec( + null, + reflectionTypeMetadataName, + null, + null, + 0, + null, + null, + ImmutableArray.Empty); + } + + public static RuntimeTypeReferenceSpec FromExternalReflectionLookup( + string reflectionAssemblyName, + string reflectionTypeMetadataName) + { + return new RuntimeTypeReferenceSpec( + null, + reflectionTypeMetadataName, + reflectionAssemblyName, + null, + 0, + null, + null, + ImmutableArray.Empty); + } + + public static RuntimeTypeReferenceSpec FromArray(RuntimeTypeReferenceSpec elementTypeReference, int arrayRank) + { + return new RuntimeTypeReferenceSpec( + null, + null, + null, + elementTypeReference, + arrayRank, + null, + null, + ImmutableArray.Empty); + } + + public static RuntimeTypeReferenceSpec FromConstructedGeneric( + RuntimeTypeReferenceSpec genericTypeDefinitionReference, + ImmutableArray genericTypeArguments) + { + return new RuntimeTypeReferenceSpec( + null, + null, + null, + null, + 0, + null, + genericTypeDefinitionReference, + genericTypeArguments); + } + } + + private readonly record struct PreciseReflectedRegistrationSpec( + string OpenHandlerTypeDisplayName, + string HandlerInterfaceLogName, + ImmutableArray ServiceTypeArguments); + + private readonly record struct ImplementationRegistrationSpec( + string ImplementationTypeDisplayName, + string ImplementationLogName, + ImmutableArray DirectRegistrations, + ImmutableArray ReflectedImplementationRegistrations, + ImmutableArray PreciseReflectedRegistrations, + string? ReflectionTypeMetadataName, + string? ReflectionFallbackHandlerTypeMetadataName); + + private readonly struct HandlerCandidateAnalysis : IEquatable + { + public HandlerCandidateAnalysis( + string implementationTypeDisplayName, + string implementationLogName, + ImmutableArray registrations, + ImmutableArray reflectedImplementationRegistrations, + ImmutableArray preciseReflectedRegistrations, + string? reflectionTypeMetadataName, + string? reflectionFallbackHandlerTypeMetadataName) + { + ImplementationTypeDisplayName = implementationTypeDisplayName; + ImplementationLogName = implementationLogName; + Registrations = registrations; + ReflectedImplementationRegistrations = reflectedImplementationRegistrations; + PreciseReflectedRegistrations = preciseReflectedRegistrations; + ReflectionTypeMetadataName = reflectionTypeMetadataName; + ReflectionFallbackHandlerTypeMetadataName = reflectionFallbackHandlerTypeMetadataName; + } + + public string ImplementationTypeDisplayName { get; } + + public string ImplementationLogName { get; } + + public ImmutableArray Registrations { get; } + + public ImmutableArray ReflectedImplementationRegistrations { get; } + + public ImmutableArray PreciseReflectedRegistrations { get; } + + public string? ReflectionTypeMetadataName { get; } + + public string? ReflectionFallbackHandlerTypeMetadataName { get; } + + public bool Equals(HandlerCandidateAnalysis other) + { + if (!string.Equals(ImplementationTypeDisplayName, other.ImplementationTypeDisplayName, + StringComparison.Ordinal) || + !string.Equals(ImplementationLogName, other.ImplementationLogName, StringComparison.Ordinal) || + !string.Equals(ReflectionTypeMetadataName, other.ReflectionTypeMetadataName, + StringComparison.Ordinal) || + !string.Equals( + ReflectionFallbackHandlerTypeMetadataName, + other.ReflectionFallbackHandlerTypeMetadataName, + StringComparison.Ordinal) || + Registrations.Length != other.Registrations.Length || + ReflectedImplementationRegistrations.Length != other.ReflectedImplementationRegistrations.Length || + PreciseReflectedRegistrations.Length != other.PreciseReflectedRegistrations.Length) + { + return false; + } + + for (var index = 0; index < Registrations.Length; index++) + { + if (!Registrations[index].Equals(other.Registrations[index])) + return false; + } + + for (var index = 0; index < ReflectedImplementationRegistrations.Length; index++) + { + if (!ReflectedImplementationRegistrations[index].Equals( + other.ReflectedImplementationRegistrations[index])) + { + return false; + } + } + + for (var index = 0; index < PreciseReflectedRegistrations.Length; index++) + { + if (!PreciseReflectedRegistrations[index].Equals(other.PreciseReflectedRegistrations[index])) + return false; + } + + return true; + } + + public override bool Equals(object? obj) + { + return obj is HandlerCandidateAnalysis other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = StringComparer.Ordinal.GetHashCode(ImplementationTypeDisplayName); + hashCode = (hashCode * 397) ^ StringComparer.Ordinal.GetHashCode(ImplementationLogName); + hashCode = (hashCode * 397) ^ + (ReflectionTypeMetadataName is null + ? 0 + : StringComparer.Ordinal.GetHashCode(ReflectionTypeMetadataName)); + hashCode = (hashCode * 397) ^ + (ReflectionFallbackHandlerTypeMetadataName is null + ? 0 + : StringComparer.Ordinal.GetHashCode(ReflectionFallbackHandlerTypeMetadataName)); + foreach (var registration in Registrations) + { + hashCode = (hashCode * 397) ^ registration.GetHashCode(); + } + + foreach (var reflectedImplementationRegistration in ReflectedImplementationRegistrations) + { + hashCode = (hashCode * 397) ^ reflectedImplementationRegistration.GetHashCode(); + } + + foreach (var preciseReflectedRegistration in PreciseReflectedRegistrations) + { + hashCode = (hashCode * 397) ^ preciseReflectedRegistration.GetHashCode(); + } + + return hashCode; + } + } + } + + private readonly record struct GenerationEnvironment( + bool GenerationEnabled, + bool SupportsReflectionFallbackAttribute); +} diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.RuntimeTypeReferences.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.RuntimeTypeReferences.cs new file mode 100644 index 00000000..fcffb3e1 --- /dev/null +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.RuntimeTypeReferences.cs @@ -0,0 +1,283 @@ +namespace GFramework.Cqrs.SourceGenerators.Cqrs; + +/// +/// 为当前编译程序集生成 CQRS 处理器注册器,以减少运行时的程序集反射扫描成本。 +/// +public sealed partial class CqrsHandlerRegistryGenerator +{ + /// + /// 为无法直接在生成代码中书写的关闭处理器接口构造精确的运行时注册描述。 + /// + /// + /// 当前生成轮次对应的编译上下文,用于判断类型是否属于当前程序集,从而决定是生成直接类型引用还是延迟到运行时反射解析。 + /// + /// + /// 需要注册的关闭处理器接口。调用方应保证它来自受支持的 CQRS 处理器接口定义,并且其泛型参数顺序与运行时注册约定一致。 + /// + /// + /// 当方法返回 时,包含开放泛型处理器类型和每个运行时类型实参的精确描述; + /// 当方法返回 时,为默认值,调用方应回退到基于实现类型的宽松反射发现路径。 + /// + /// + /// 当接口上的所有运行时类型引用都能在生成阶段被稳定描述时返回 ; + /// 只要任一泛型实参无法安全编码到生成输出中,就返回 。 + /// + private static bool TryCreatePreciseReflectedRegistration( + Compilation compilation, + INamedTypeSymbol handlerInterface, + out PreciseReflectedRegistrationSpec registration) + { + var openHandlerTypeDisplayName = handlerInterface.OriginalDefinition + .ConstructUnboundGenericType() + .ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var typeArguments = + ImmutableArray.CreateBuilder(handlerInterface.TypeArguments.Length); + foreach (var typeArgument in handlerInterface.TypeArguments) + { + if (!TryCreateRuntimeTypeReference(compilation, typeArgument, out var runtimeTypeReference)) + { + registration = default; + return false; + } + + typeArguments.Add(runtimeTypeReference!); + } + + registration = new PreciseReflectedRegistrationSpec( + openHandlerTypeDisplayName, + GetLogDisplayName(handlerInterface), + typeArguments.ToImmutable()); + return true; + } + + /// + /// 将 Roslyn 类型符号转换为生成注册器可消费的运行时类型引用描述。 + /// + /// + /// 当前编译上下文,用于区分可直接引用的外部可访问类型与必须通过当前程序集运行时反射查找的内部类型。 + /// + /// + /// 需要转换的类型符号。该方法会递归处理数组元素类型和已构造泛型的类型实参,但不会为未绑定泛型或类型参数生成引用。 + /// + /// + /// 当方法返回 时,包含可直接引用、数组、已构造泛型或反射查找中的一种运行时表示; + /// 当方法返回 时为 ,调用方应回退到更宽泛的实现类型反射扫描策略。 + /// + /// + /// 当 及其递归子结构都能映射为稳定的运行时引用时返回 ; + /// 若遇到类型参数、无法访问的运行时结构,或任一递归分支无法表示,则返回 。 + /// + private static bool TryCreateRuntimeTypeReference( + Compilation compilation, + ITypeSymbol type, + out RuntimeTypeReferenceSpec? runtimeTypeReference) + { + // CLR forbids pointer and function-pointer types from being used as generic arguments. + // CQRS handler contracts are generic interfaces, so emitting runtime reconstruction code for these + // shapes would only defer the failure to MakeGenericType(...) at runtime. + if (type is IPointerTypeSymbol or IFunctionPointerTypeSymbol) + { + runtimeTypeReference = null; + return false; + } + + if (CanReferenceFromGeneratedRegistry(compilation, type)) + { + runtimeTypeReference = RuntimeTypeReferenceSpec.FromDirectReference( + type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + return true; + } + + if (type is IArrayTypeSymbol arrayType && + TryCreateRuntimeTypeReference(compilation, arrayType.ElementType, out var elementTypeReference)) + { + runtimeTypeReference = RuntimeTypeReferenceSpec.FromArray(elementTypeReference!, arrayType.Rank); + return true; + } + + if (type is INamedTypeSymbol genericNamedType && + genericNamedType.IsGenericType && + !genericNamedType.IsUnboundGenericType) + { + return TryCreateConstructedGenericRuntimeTypeReference( + compilation, + genericNamedType, + out runtimeTypeReference); + } + + if (type is INamedTypeSymbol namedType) + { + runtimeTypeReference = CreateNamedRuntimeTypeReference(compilation, namedType); + return true; + } + + runtimeTypeReference = null; + return false; + } + + /// + /// 为已构造泛型类型构造运行时类型引用,并递归验证每个泛型实参都可以稳定编码到生成输出中。 + /// + /// 当前生成轮次的编译上下文。 + /// 需要表示的已构造泛型类型。 + /// + /// 当方法返回 时,包含泛型定义和泛型实参的运行时重建描述。 + /// + /// 当泛型定义和全部泛型实参都能表达时返回 + private static bool TryCreateConstructedGenericRuntimeTypeReference( + Compilation compilation, + INamedTypeSymbol genericNamedType, + out RuntimeTypeReferenceSpec? runtimeTypeReference) + { + if (!TryCreateGenericTypeDefinitionReference( + compilation, + genericNamedType, + out var genericTypeDefinitionReference)) + { + runtimeTypeReference = null; + return false; + } + + var genericTypeArguments = + ImmutableArray.CreateBuilder(genericNamedType.TypeArguments.Length); + foreach (var typeArgument in genericNamedType.TypeArguments) + { + if (!TryCreateRuntimeTypeReference(compilation, typeArgument, out var genericTypeArgumentReference)) + { + runtimeTypeReference = null; + return false; + } + + genericTypeArguments.Add(genericTypeArgumentReference!); + } + + runtimeTypeReference = RuntimeTypeReferenceSpec.FromConstructedGeneric( + genericTypeDefinitionReference!, + genericTypeArguments.ToImmutable()); + return true; + } + + /// + /// 为无法直接书写的命名类型选择当前程序集反射查找或外部程序集反射查找表示。 + /// + /// 当前生成轮次的编译上下文。 + /// 需要在运行时解析的命名类型。 + /// 适合写入生成注册器的命名类型运行时引用。 + private static RuntimeTypeReferenceSpec CreateNamedRuntimeTypeReference( + Compilation compilation, + INamedTypeSymbol namedType) + { + if (SymbolEqualityComparer.Default.Equals(namedType.ContainingAssembly, compilation.Assembly)) + return RuntimeTypeReferenceSpec.FromReflectionLookup(GetReflectionTypeMetadataName(namedType)); + + return RuntimeTypeReferenceSpec.FromExternalReflectionLookup( + namedType.ContainingAssembly.Identity.ToString(), + GetReflectionTypeMetadataName(namedType)); + } + + /// + /// 为已构造泛型类型解析其泛型定义的运行时引用描述。 + /// + /// + /// 当前编译上下文,用于判断泛型定义是否应以内联类型引用形式生成,或在运行时通过当前程序集反射解析。 + /// + /// + /// 已构造的泛型类型。该方法只处理其原始泛型定义,不负责递归解析类型实参。 + /// + /// + /// 当方法返回 时,包含泛型定义的直接引用或反射查找描述; + /// 当方法返回 时为 ,调用方应停止精确构造并回退到更保守的注册路径。 + /// + /// + /// 当泛型定义能够以稳定方式编码到生成输出中时返回 ; + /// 若泛型定义既不能直接引用,也不属于当前程序集可供反射查找,则返回 。 + /// + private static bool TryCreateGenericTypeDefinitionReference( + Compilation compilation, + INamedTypeSymbol genericNamedType, + out RuntimeTypeReferenceSpec? genericTypeDefinitionReference) + { + var genericTypeDefinition = genericNamedType.OriginalDefinition; + if (CanReferenceFromGeneratedRegistry(compilation, genericTypeDefinition)) + { + genericTypeDefinitionReference = RuntimeTypeReferenceSpec.FromDirectReference( + genericTypeDefinition + .ConstructUnboundGenericType() + .ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + return true; + } + + if (SymbolEqualityComparer.Default.Equals(genericTypeDefinition.ContainingAssembly, compilation.Assembly)) + { + genericTypeDefinitionReference = RuntimeTypeReferenceSpec.FromReflectionLookup( + GetReflectionTypeMetadataName(genericTypeDefinition)); + return true; + } + + genericTypeDefinitionReference = RuntimeTypeReferenceSpec.FromExternalReflectionLookup( + genericTypeDefinition.ContainingAssembly.Identity.ToString(), + GetReflectionTypeMetadataName(genericTypeDefinition)); + return true; + } + + private static bool CanReferenceFromGeneratedRegistry(Compilation compilation, ITypeSymbol type) + { + switch (type) + { + case IArrayTypeSymbol arrayType: + return CanReferenceFromGeneratedRegistry(compilation, arrayType.ElementType); + case INamedTypeSymbol namedType: + if (!compilation.IsSymbolAccessibleWithin(namedType, compilation.Assembly, throughType: null)) + return false; + + foreach (var typeArgument in namedType.TypeArguments) + { + if (!CanReferenceFromGeneratedRegistry(compilation, typeArgument)) + return false; + } + + return true; + case IPointerTypeSymbol: + case IFunctionPointerTypeSymbol: + return false; + case ITypeParameterSymbol: + return false; + default: + // Treat other Roslyn type kinds, such as dynamic or unresolved error types, as referenceable for now. + // If a real-world case proves unsafe, tighten this branch instead of broadening the named-type path above. + return true; + } + } + + private static bool ContainsExternalAssemblyTypeLookup(RuntimeTypeReferenceSpec runtimeTypeReference) + { + if (!string.IsNullOrWhiteSpace(runtimeTypeReference.ReflectionAssemblyName)) + return true; + + if (runtimeTypeReference.ArrayElementTypeReference is not null && + ContainsExternalAssemblyTypeLookup(runtimeTypeReference.ArrayElementTypeReference)) + { + return true; + } + + if (runtimeTypeReference.PointerElementTypeReference is not null && + ContainsExternalAssemblyTypeLookup(runtimeTypeReference.PointerElementTypeReference)) + { + return true; + } + + if (runtimeTypeReference.GenericTypeDefinitionReference is not null && + ContainsExternalAssemblyTypeLookup(runtimeTypeReference.GenericTypeDefinitionReference)) + { + return true; + } + + foreach (var genericTypeArgument in runtimeTypeReference.GenericTypeArguments) + { + if (ContainsExternalAssemblyTypeLookup(genericTypeArgument)) + return true; + } + + return false; + } +} diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs new file mode 100644 index 00000000..d8768cd7 --- /dev/null +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs @@ -0,0 +1,851 @@ +namespace GFramework.Cqrs.SourceGenerators.Cqrs; + +/// +/// 为当前编译程序集生成 CQRS 处理器注册器,以减少运行时的程序集反射扫描成本。 +/// +public sealed partial class CqrsHandlerRegistryGenerator +{ + /// + /// 生成程序集级 CQRS handler 注册器源码。 + /// + /// + /// 当前轮次的生成环境,用于决定 runtime 是否提供 CqrsReflectionFallbackAttribute 契约,以及是否需要在输出中发射对应的程序集级元数据。 + /// + /// + /// 已整理并排序的 handler 注册描述。方法会据此生成 CqrsHandlerRegistry.g.cs,其中包含直接注册、实现类型反射注册、精确运行时类型查找等分支。 + /// + /// + /// 仍需依赖程序集级 reflection fallback 元数据恢复的 handler 元数据名称集合。 + /// 调用方必须先确保:若该集合非空,则 已声明支持对应的 fallback attribute 契约; + /// 否则应在进入本方法前报告诊断并放弃生成,而不是输出会静默漏注册的半成品注册器。 + /// + /// 完整的注册器源代码文本。 + /// + /// 当 为空时,输出只包含程序集级 CqrsHandlerRegistryAttribute 和注册器实现。 + /// 当其非空且 runtime 合同可用时,输出还会附带程序集级 CqrsReflectionFallbackAttribute,让运行时补齐生成阶段无法精确表达的剩余 handler。 + /// 该方法本身不报告诊断;“fallback 必需但 runtime 契约缺失”的错误由调用方在进入本方法前处理。 + /// + private static string GenerateSource( + GenerationEnvironment generationEnvironment, + IReadOnlyList registrations, + IReadOnlyList fallbackHandlerTypeMetadataNames) + { + var sourceShape = CreateGeneratedRegistrySourceShape(registrations); + var builder = new StringBuilder(); + AppendGeneratedSourcePreamble(builder, generationEnvironment, fallbackHandlerTypeMetadataNames); + AppendGeneratedRegistryType(builder, registrations, sourceShape); + return builder.ToString(); + } + + /// + /// 预先计算生成注册器需要的辅助分支,让主源码发射流程保持线性且避免重复扫描注册集合。 + /// + /// 已整理并排序的 handler 注册描述。 + /// 当前生成输出需要启用的结构分支。 + private static GeneratedRegistrySourceShape CreateGeneratedRegistrySourceShape( + IReadOnlyList registrations) + { + var hasReflectedImplementationRegistrations = registrations.Any(static registration => + !registration.ReflectedImplementationRegistrations.IsDefaultOrEmpty); + var hasPreciseReflectedRegistrations = registrations.Any(static registration => + !registration.PreciseReflectedRegistrations.IsDefaultOrEmpty); + var hasReflectionTypeLookups = registrations.Any(static registration => + !string.IsNullOrWhiteSpace(registration.ReflectionTypeMetadataName)); + var hasExternalAssemblyTypeLookups = registrations.Any(static registration => + registration.PreciseReflectedRegistrations.Any(static preciseRegistration => + preciseRegistration.ServiceTypeArguments.Any(ContainsExternalAssemblyTypeLookup))); + + return new GeneratedRegistrySourceShape( + hasReflectedImplementationRegistrations, + hasPreciseReflectedRegistrations, + hasReflectionTypeLookups, + hasExternalAssemblyTypeLookups); + } + + /// + /// 发射生成文件头、nullable 指令以及注册器所需的程序集级元数据特性。 + /// + /// 生成源码构造器。 + /// 当前轮次的生成环境。 + /// 需要程序集级 reflection fallback 的 handler 元数据名称。 + private static void AppendGeneratedSourcePreamble( + StringBuilder builder, + GenerationEnvironment generationEnvironment, + IReadOnlyList fallbackHandlerTypeMetadataNames) + { + builder.AppendLine("// "); + builder.AppendLine("#nullable enable"); + builder.AppendLine(); + if (generationEnvironment.SupportsReflectionFallbackAttribute && fallbackHandlerTypeMetadataNames.Count > 0) + { + AppendReflectionFallbackAttribute(builder, fallbackHandlerTypeMetadataNames); + builder.AppendLine(); + } + + builder.Append("[assembly: global::"); + builder.Append(CqrsRuntimeNamespace); + builder.Append(".CqrsHandlerRegistryAttribute(typeof(global::"); + builder.Append(GeneratedNamespace); + builder.Append('.'); + builder.Append(GeneratedTypeName); + builder.AppendLine("))]"); + } + + /// + /// 发射程序集级 reflection fallback 元数据特性,供运行时补齐生成阶段无法精确表达的 handler。 + /// + /// 生成源码构造器。 + /// 需要写入特性的 handler 元数据名称。 + private static void AppendReflectionFallbackAttribute( + StringBuilder builder, + IReadOnlyList fallbackHandlerTypeMetadataNames) + { + builder.Append("[assembly: global::"); + builder.Append(CqrsRuntimeNamespace); + builder.Append(".CqrsReflectionFallbackAttribute("); + for (var index = 0; index < fallbackHandlerTypeMetadataNames.Count; index++) + { + if (index > 0) + builder.Append(", "); + + builder.Append('"'); + builder.Append(EscapeStringLiteral(fallbackHandlerTypeMetadataNames[index])); + builder.Append('"'); + } + + builder.AppendLine(")]"); + } + + /// + /// 发射生成注册器类型本体,包括 Register 方法和运行时反射辅助方法。 + /// + /// 生成源码构造器。 + /// 已排序的 handler 注册描述。 + /// 当前输出需要启用的结构分支。 + private static void AppendGeneratedRegistryType( + StringBuilder builder, + IReadOnlyList registrations, + GeneratedRegistrySourceShape sourceShape) + { + builder.AppendLine(); + builder.Append("namespace "); + builder.Append(GeneratedNamespace); + builder.AppendLine(";"); + builder.AppendLine(); + builder.Append("internal sealed class "); + builder.Append(GeneratedTypeName); + builder.Append(" : global::"); + builder.Append(CqrsRuntimeNamespace); + builder.AppendLine(".ICqrsHandlerRegistry"); + builder.AppendLine("{"); + AppendRegisterMethod(builder, registrations, sourceShape); + + if (sourceShape.HasExternalAssemblyTypeLookups) + { + builder.AppendLine(); + AppendReflectionHelpers(builder, sourceShape.HasExternalAssemblyTypeLookups); + } + + builder.AppendLine("}"); + } + + /// + /// 发射注册器的 Register 方法,保持直接注册和反射注册之间的原始稳定排序。 + /// + /// 生成源码构造器。 + /// 已排序的 handler 注册描述。 + /// 当前输出需要启用的结构分支。 + private static void AppendRegisterMethod( + StringBuilder builder, + IReadOnlyList registrations, + GeneratedRegistrySourceShape sourceShape) + { + builder.Append( + " public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::"); + builder.Append(LoggingNamespace); + builder.AppendLine(".ILogger logger)"); + builder.AppendLine(" {"); + builder.AppendLine(" if (services is null)"); + builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(services));"); + builder.AppendLine(" if (logger is null)"); + builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(logger));"); + if (sourceShape.RequiresRegistryAssemblyVariable) + { + builder.AppendLine(); + builder.Append(" var registryAssembly = typeof(global::"); + builder.Append(GeneratedNamespace); + builder.Append('.'); + builder.Append(GeneratedTypeName); + builder.AppendLine(").Assembly;"); + } + + if (registrations.Count > 0) + builder.AppendLine(); + + for (var registrationIndex = 0; registrationIndex < registrations.Count; registrationIndex++) + { + var registration = registrations[registrationIndex]; + if (!registration.ReflectedImplementationRegistrations.IsDefaultOrEmpty || + !registration.PreciseReflectedRegistrations.IsDefaultOrEmpty) + { + AppendOrderedImplementationRegistrations(builder, registration, registrationIndex); + } + else if (!registration.DirectRegistrations.IsDefaultOrEmpty) + { + AppendDirectRegistrations(builder, registration); + } + } + + builder.AppendLine(" }"); + } + + private static void AppendDirectRegistrations( + StringBuilder builder, + ImplementationRegistrationSpec registration) + { + foreach (var directRegistration in registration.DirectRegistrations) + { + AppendServiceRegistration( + builder, + $"typeof({directRegistration.HandlerInterfaceDisplayName})", + $"typeof({directRegistration.ImplementationTypeDisplayName})", + " "); + AppendRegistrationLog( + builder, + directRegistration.ImplementationLogName, + directRegistration.HandlerInterfaceLogName, + " "); + } + } + + /// + /// 发射 AddTransient 调用,调用方负责传入已经按当前分支解析好的 service 和 implementation 表达式。 + /// + /// 生成源码构造器。 + /// 生成代码中的服务类型表达式。 + /// 生成代码中的实现类型表达式。 + /// 当前生成语句的缩进。 + private static void AppendServiceRegistration( + StringBuilder builder, + string serviceTypeExpression, + string implementationTypeExpression, + string indent) + { + builder.Append(indent); + builder.AppendLine("global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient("); + builder.Append(indent); + builder.AppendLine(" services,"); + builder.Append(indent); + builder.Append(" "); + builder.Append(serviceTypeExpression); + builder.AppendLine(","); + builder.Append(indent); + builder.Append(" "); + builder.Append(implementationTypeExpression); + builder.AppendLine(");"); + } + + /// + /// 发射与注册语句配套的调试日志,保持所有生成注册路径的日志文本完全一致。 + /// + /// 生成源码构造器。 + /// 实现类型日志名。 + /// handler 接口日志名。 + /// 当前生成语句的缩进。 + private static void AppendRegistrationLog( + StringBuilder builder, + string implementationLogName, + string handlerInterfaceLogName, + string indent) + { + builder.Append(indent); + builder.Append("logger.Debug(\"Registered CQRS handler "); + builder.Append(EscapeStringLiteral(implementationLogName)); + builder.Append(" as "); + builder.Append(EscapeStringLiteral(handlerInterfaceLogName)); + builder.AppendLine(".\");"); + } + + private static void AppendOrderedImplementationRegistrations( + StringBuilder builder, + ImplementationRegistrationSpec registration, + int registrationIndex) + { + var orderedRegistrations = CreateOrderedRegistrations(registration); + var implementationVariableName = $"implementationType{registrationIndex}"; + AppendImplementationTypeVariable(builder, registration, implementationVariableName); + + builder.Append(" if ("); + builder.Append(implementationVariableName); + builder.AppendLine(" is not null)"); + builder.AppendLine(" {"); + + foreach (var orderedRegistration in orderedRegistrations) + { + AppendOrderedRegistration( + builder, + registration, + orderedRegistration, + registrationIndex, + implementationVariableName); + } + + builder.AppendLine(" }"); + } + + /// + /// 合并直接注册、实现类型反射注册和精确反射注册,并按 handler 接口日志名排序以保持生成输出稳定。 + /// + /// 单个实现类型聚合后的注册描述。 + /// 带有来源类型和原始索引的有序注册列表。 + private static List CreateOrderedRegistrations(ImplementationRegistrationSpec registration) + { + var orderedRegistrations = new List( + registration.DirectRegistrations.Length + + registration.ReflectedImplementationRegistrations.Length + + registration.PreciseReflectedRegistrations.Length); + for (var directIndex = 0; directIndex < registration.DirectRegistrations.Length; directIndex++) + { + orderedRegistrations.Add(new OrderedRegistrationSpec( + registration.DirectRegistrations[directIndex].HandlerInterfaceLogName, + OrderedRegistrationKind.Direct, + directIndex)); + } + + for (var reflectedIndex = 0; + reflectedIndex < registration.ReflectedImplementationRegistrations.Length; + reflectedIndex++) + { + orderedRegistrations.Add(new OrderedRegistrationSpec( + registration.ReflectedImplementationRegistrations[reflectedIndex].HandlerInterfaceLogName, + OrderedRegistrationKind.ReflectedImplementation, + reflectedIndex)); + } + + for (var preciseIndex = 0; + preciseIndex < registration.PreciseReflectedRegistrations.Length; + preciseIndex++) + { + orderedRegistrations.Add(new OrderedRegistrationSpec( + registration.PreciseReflectedRegistrations[preciseIndex].HandlerInterfaceLogName, + OrderedRegistrationKind.PreciseReflected, + preciseIndex)); + } + + orderedRegistrations.Sort(static (left, right) => + StringComparer.Ordinal.Compare(left.HandlerInterfaceLogName, right.HandlerInterfaceLogName)); + return orderedRegistrations; + } + + /// + /// 发射实现类型变量。公开类型直接使用 typeof,不可直接引用的实现类型则从当前程序集反射解析。 + /// + /// 生成源码构造器。 + /// 单个实现类型聚合后的注册描述。 + /// 生成代码中的实现类型变量名。 + private static void AppendImplementationTypeVariable( + StringBuilder builder, + ImplementationRegistrationSpec registration, + string implementationVariableName) + { + if (string.IsNullOrWhiteSpace(registration.ReflectionTypeMetadataName)) + { + builder.Append(" var "); + builder.Append(implementationVariableName); + builder.Append(" = typeof("); + builder.Append(registration.ImplementationTypeDisplayName); + builder.AppendLine(");"); + } + else + { + builder.Append(" var "); + builder.Append(implementationVariableName); + builder.Append(" = registryAssembly.GetType(\""); + builder.Append(EscapeStringLiteral(registration.ReflectionTypeMetadataName!)); + builder.AppendLine("\", throwOnError: false, ignoreCase: false);"); + } + } + + /// + /// 根据注册来源发射单条有序注册,确保混合直接和反射路径时仍按 handler 接口名稳定输出。 + /// + /// 生成源码构造器。 + /// 单个实现类型聚合后的注册描述。 + /// 带来源类型和原始索引的排序项。 + /// 实现类型在整体注册列表中的索引,用于生成稳定变量名。 + /// 生成代码中的实现类型变量名。 + private static void AppendOrderedRegistration( + StringBuilder builder, + ImplementationRegistrationSpec registration, + OrderedRegistrationSpec orderedRegistration, + int registrationIndex, + string implementationVariableName) + { + switch (orderedRegistration.Kind) + { + case OrderedRegistrationKind.Direct: + AppendOrderedDirectRegistration( + builder, + registration, + registration.DirectRegistrations[orderedRegistration.Index], + implementationVariableName); + break; + case OrderedRegistrationKind.ReflectedImplementation: + AppendOrderedReflectedImplementationRegistration( + builder, + registration, + registration.ReflectedImplementationRegistrations[orderedRegistration.Index], + implementationVariableName); + break; + case OrderedRegistrationKind.PreciseReflected: + AppendOrderedPreciseReflectedRegistration( + builder, + registration, + registration.PreciseReflectedRegistrations[orderedRegistration.Index], + registrationIndex, + orderedRegistration.Index, + implementationVariableName); + break; + default: + throw new InvalidOperationException( + $"Unsupported ordered CQRS registration kind {orderedRegistration.Kind}."); + } + } + + /// + /// 发射实现类型已通过变量解析、handler 接口可直接引用的直接注册语句。 + /// + /// 生成源码构造器。 + /// 单个实现类型聚合后的注册描述。 + /// 当前直接注册项。 + /// 生成代码中的实现类型变量名。 + private static void AppendOrderedDirectRegistration( + StringBuilder builder, + ImplementationRegistrationSpec registration, + HandlerRegistrationSpec directRegistration, + string implementationVariableName) + { + AppendServiceRegistration( + builder, + $"typeof({directRegistration.HandlerInterfaceDisplayName})", + implementationVariableName, + " "); + AppendRegistrationLog( + builder, + registration.ImplementationLogName, + directRegistration.HandlerInterfaceLogName, + " "); + } + + /// + /// 发射实现类型需要反射解析、handler 接口可直接引用的注册语句。 + /// + /// 生成源码构造器。 + /// 单个实现类型聚合后的注册描述。 + /// 当前实现类型反射注册项。 + /// 生成代码中的实现类型变量名。 + private static void AppendOrderedReflectedImplementationRegistration( + StringBuilder builder, + ImplementationRegistrationSpec registration, + ReflectedImplementationRegistrationSpec reflectedRegistration, + string implementationVariableName) + { + AppendServiceRegistration( + builder, + $"typeof({reflectedRegistration.HandlerInterfaceDisplayName})", + implementationVariableName, + " "); + AppendRegistrationLog( + builder, + registration.ImplementationLogName, + reflectedRegistration.HandlerInterfaceLogName, + " "); + } + + /// + /// 发射 handler 接口需要运行时精确构造的注册语句。 + /// + /// 生成源码构造器。 + /// 单个实现类型聚合后的注册描述。 + /// 当前精确反射注册项。 + /// 实现类型在整体注册列表中的索引。 + /// 当前注册项在原始精确反射注册集合中的索引。 + /// 生成代码中的实现类型变量名。 + private static void AppendOrderedPreciseReflectedRegistration( + StringBuilder builder, + ImplementationRegistrationSpec registration, + PreciseReflectedRegistrationSpec preciseRegistration, + int registrationIndex, + int orderedRegistrationIndex, + string implementationVariableName) + { + var registrationVariablePrefix = $"serviceType{registrationIndex}_{orderedRegistrationIndex}"; + AppendPreciseReflectedTypeResolution( + builder, + preciseRegistration.ServiceTypeArguments, + registrationVariablePrefix, + implementationVariableName, + preciseRegistration.OpenHandlerTypeDisplayName, + registration.ImplementationLogName, + preciseRegistration.HandlerInterfaceLogName, + 3); + } + + private static void AppendPreciseReflectedTypeResolution( + StringBuilder builder, + ImmutableArray serviceTypeArguments, + string registrationVariablePrefix, + string implementationVariableName, + string openHandlerTypeDisplayName, + string implementationLogName, + string handlerInterfaceLogName, + int indentLevel) + { + var indent = new string(' ', indentLevel * 4); + var reflectedArgumentNames = new List(); + var resolvedArgumentNames = AppendServiceTypeArgumentResolutions( + builder, + serviceTypeArguments, + registrationVariablePrefix, + reflectedArgumentNames, + indent); + + if (reflectedArgumentNames.Count > 0) + indent = AppendReflectedArgumentGuardStart(builder, reflectedArgumentNames, indent); + + AppendClosedGenericServiceTypeCreation( + builder, + registrationVariablePrefix, + openHandlerTypeDisplayName, + resolvedArgumentNames, + indent); + AppendServiceRegistration(builder, registrationVariablePrefix, implementationVariableName, indent); + AppendRegistrationLog(builder, implementationLogName, handlerInterfaceLogName, indent); + + if (reflectedArgumentNames.Count > 0) + { + builder.Append(new string(' ', indentLevel * 4)); + builder.AppendLine("}"); + } + } + + /// + /// 递归发射每个 handler 泛型实参的运行时类型解析表达式。 + /// + /// 生成源码构造器。 + /// handler 服务类型的运行时泛型实参描述。 + /// 当前注册项的稳定变量名前缀。 + /// 需要空值检查的反射解析变量集合。 + /// 当前生成语句的缩进。 + /// 可传给 MakeGenericType 的实参表达式。 + private static string[] AppendServiceTypeArgumentResolutions( + StringBuilder builder, + ImmutableArray serviceTypeArguments, + string registrationVariablePrefix, + ICollection reflectedArgumentNames, + string indent) + { + var resolvedArgumentNames = new string[serviceTypeArguments.Length]; + for (var argumentIndex = 0; argumentIndex < serviceTypeArguments.Length; argumentIndex++) + { + resolvedArgumentNames[argumentIndex] = AppendRuntimeTypeReferenceResolution( + builder, + serviceTypeArguments[argumentIndex], + $"{registrationVariablePrefix}Argument{argumentIndex}", + reflectedArgumentNames, + indent); + } + + return resolvedArgumentNames; + } + + /// + /// 为运行时反射解析出的泛型实参发射空值保护块,避免生成注册器注册无法完整构造的服务类型。 + /// + /// 生成源码构造器。 + /// 需要参与空值检查的变量名。 + /// 保护块开始前的缩进。 + /// 保护块内部应使用的下一层缩进。 + private static string AppendReflectedArgumentGuardStart( + StringBuilder builder, + IReadOnlyList reflectedArgumentNames, + string indent) + { + builder.Append(indent); + builder.Append("if ("); + for (var index = 0; index < reflectedArgumentNames.Count; index++) + { + if (index > 0) + builder.Append(" && "); + + builder.Append(reflectedArgumentNames[index]); + builder.Append(" is not null"); + } + + builder.AppendLine(")"); + builder.Append(indent); + builder.AppendLine("{"); + return $"{indent} "; + } + + /// + /// 发射关闭 handler 服务类型的 MakeGenericType 构造语句。 + /// + /// 生成源码构造器。 + /// 生成代码中的服务类型变量名。 + /// 开放 handler 接口类型显示名。 + /// 已解析的泛型实参表达式。 + /// 当前生成语句的缩进。 + private static void AppendClosedGenericServiceTypeCreation( + StringBuilder builder, + string registrationVariablePrefix, + string openHandlerTypeDisplayName, + IReadOnlyList resolvedArgumentNames, + string indent) + { + builder.Append(indent); + builder.Append("var "); + builder.Append(registrationVariablePrefix); + builder.Append(" = typeof("); + builder.Append(openHandlerTypeDisplayName); + builder.Append(").MakeGenericType("); + for (var index = 0; index < resolvedArgumentNames.Count; index++) + { + if (index > 0) + builder.Append(", "); + + builder.Append(resolvedArgumentNames[index]); + } + + builder.AppendLine(");"); + } + + private static string AppendRuntimeTypeReferenceResolution( + StringBuilder builder, + RuntimeTypeReferenceSpec runtimeTypeReference, + string variableBaseName, + ICollection reflectedArgumentNames, + string indent) + { + if (!string.IsNullOrWhiteSpace(runtimeTypeReference.TypeDisplayName)) + return $"typeof({runtimeTypeReference.TypeDisplayName})"; + + if (runtimeTypeReference.ArrayElementTypeReference is not null) + return AppendArrayRuntimeTypeReferenceResolution( + builder, + runtimeTypeReference, + variableBaseName, + reflectedArgumentNames, + indent); + + if (runtimeTypeReference.PointerElementTypeReference is not null) + return AppendPointerRuntimeTypeReferenceResolution( + builder, + runtimeTypeReference, + variableBaseName, + reflectedArgumentNames, + indent); + + if (runtimeTypeReference.GenericTypeDefinitionReference is not null) + return AppendConstructedGenericRuntimeTypeReferenceResolution( + builder, + runtimeTypeReference, + variableBaseName, + reflectedArgumentNames, + indent); + + return AppendReflectionRuntimeTypeReferenceResolution( + builder, + runtimeTypeReference, + variableBaseName, + reflectedArgumentNames, + indent); + } + + /// + /// 发射数组类型引用的运行时重建表达式。 + /// + /// 生成源码构造器。 + /// 数组类型引用描述。 + /// 用于递归生成变量名的稳定前缀。 + /// 需要空值检查的反射解析变量集合。 + /// 当前生成语句的缩进。 + /// 数组类型表达式。 + private static string AppendArrayRuntimeTypeReferenceResolution( + StringBuilder builder, + RuntimeTypeReferenceSpec runtimeTypeReference, + string variableBaseName, + ICollection reflectedArgumentNames, + string indent) + { + var elementExpression = AppendRuntimeTypeReferenceResolution( + builder, + runtimeTypeReference.ArrayElementTypeReference!, + $"{variableBaseName}Element", + reflectedArgumentNames, + indent); + + return runtimeTypeReference.ArrayRank == 1 + ? $"{elementExpression}.MakeArrayType()" + : $"{elementExpression}.MakeArrayType({runtimeTypeReference.ArrayRank})"; + } + + /// + /// 发射指针类型引用的运行时重建表达式。 + /// + /// 生成源码构造器。 + /// 指针类型引用描述。 + /// 用于递归生成变量名的稳定前缀。 + /// 需要空值检查的反射解析变量集合。 + /// 当前生成语句的缩进。 + /// 指针类型表达式。 + private static string AppendPointerRuntimeTypeReferenceResolution( + StringBuilder builder, + RuntimeTypeReferenceSpec runtimeTypeReference, + string variableBaseName, + ICollection reflectedArgumentNames, + string indent) + { + var pointedAtExpression = AppendRuntimeTypeReferenceResolution( + builder, + runtimeTypeReference.PointerElementTypeReference!, + $"{variableBaseName}PointedAt", + reflectedArgumentNames, + indent); + + return $"{pointedAtExpression}.MakePointerType()"; + } + + /// + /// 发射已构造泛型类型引用的运行时重建表达式。 + /// + /// 生成源码构造器。 + /// 已构造泛型类型引用描述。 + /// 用于递归生成变量名的稳定前缀。 + /// 需要空值检查的反射解析变量集合。 + /// 当前生成语句的缩进。 + /// 已构造泛型类型表达式。 + private static string AppendConstructedGenericRuntimeTypeReferenceResolution( + StringBuilder builder, + RuntimeTypeReferenceSpec runtimeTypeReference, + string variableBaseName, + ICollection reflectedArgumentNames, + string indent) + { + var genericTypeDefinitionExpression = AppendRuntimeTypeReferenceResolution( + builder, + runtimeTypeReference.GenericTypeDefinitionReference!, + $"{variableBaseName}GenericDefinition", + reflectedArgumentNames, + indent); + var genericArgumentExpressions = new string[runtimeTypeReference.GenericTypeArguments.Length]; + for (var argumentIndex = 0; + argumentIndex < runtimeTypeReference.GenericTypeArguments.Length; + argumentIndex++) + { + genericArgumentExpressions[argumentIndex] = AppendRuntimeTypeReferenceResolution( + builder, + runtimeTypeReference.GenericTypeArguments[argumentIndex], + $"{variableBaseName}GenericArgument{argumentIndex}", + reflectedArgumentNames, + indent); + } + + return $"{genericTypeDefinitionExpression}.MakeGenericType({string.Join(", ", genericArgumentExpressions)})"; + } + + /// + /// 发射命名类型的运行时反射查找语句,并返回后续服务类型构造应引用的变量名。 + /// + /// 生成源码构造器。 + /// 反射查找类型引用描述。 + /// 生成代码中的反射变量名。 + /// 需要空值检查的反射解析变量集合。 + /// 当前生成语句的缩进。 + /// 生成代码中的反射变量名。 + private static string AppendReflectionRuntimeTypeReferenceResolution( + StringBuilder builder, + RuntimeTypeReferenceSpec runtimeTypeReference, + string variableBaseName, + ICollection reflectedArgumentNames, + string indent) + { + reflectedArgumentNames.Add(variableBaseName); + builder.Append(indent); + builder.Append("var "); + builder.Append(variableBaseName); + if (string.IsNullOrWhiteSpace(runtimeTypeReference.ReflectionAssemblyName)) + { + builder.Append(" = registryAssembly.GetType(\""); + builder.Append(EscapeStringLiteral(runtimeTypeReference.ReflectionTypeMetadataName!)); + builder.AppendLine("\", throwOnError: false, ignoreCase: false);"); + } + else + { + builder.Append(" = ResolveReferencedAssemblyType(\""); + builder.Append(EscapeStringLiteral(runtimeTypeReference.ReflectionAssemblyName!)); + builder.Append("\", \""); + builder.Append(EscapeStringLiteral(runtimeTypeReference.ReflectionTypeMetadataName!)); + builder.AppendLine("\");"); + } + + return variableBaseName; + } + + private static void AppendReflectionHelpers( + StringBuilder builder, + bool includeExternalAssemblyTypeLookupHelpers) + { + if (includeExternalAssemblyTypeLookupHelpers) + { + builder.AppendLine( + " private static global::System.Type? ResolveReferencedAssemblyType(string assemblyIdentity, string typeMetadataName)"); + builder.AppendLine(" {"); + builder.AppendLine(" var assembly = ResolveReferencedAssembly(assemblyIdentity);"); + builder.AppendLine( + " return assembly?.GetType(typeMetadataName, throwOnError: false, ignoreCase: false);"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine( + " private static global::System.Reflection.Assembly? ResolveReferencedAssembly(string assemblyIdentity)"); + builder.AppendLine(" {"); + builder.AppendLine(" global::System.Reflection.AssemblyName targetAssemblyName;"); + builder.AppendLine(" try"); + builder.AppendLine(" {"); + builder.AppendLine( + " targetAssemblyName = new global::System.Reflection.AssemblyName(assemblyIdentity);"); + builder.AppendLine(" }"); + builder.AppendLine(" catch"); + builder.AppendLine(" {"); + builder.AppendLine(" return null;"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine( + " foreach (var assembly in global::System.AppDomain.CurrentDomain.GetAssemblies())"); + builder.AppendLine(" {"); + builder.AppendLine( + " if (global::System.Reflection.AssemblyName.ReferenceMatchesDefinition(targetAssemblyName, assembly.GetName()))"); + builder.AppendLine(" return assembly;"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" try"); + builder.AppendLine(" {"); + builder.AppendLine( + " return global::System.Reflection.Assembly.Load(targetAssemblyName);"); + builder.AppendLine(" }"); + builder.AppendLine(" catch"); + builder.AppendLine(" {"); + builder.AppendLine(" return null;"); + builder.AppendLine(" }"); + builder.AppendLine(" }"); + } + } + + private static string EscapeStringLiteral(string value) + { + return value.Replace("\\", "\\\\") + .Replace("\"", "\\\"") + .Replace("\n", "\\n") + .Replace("\r", "\\r"); + } +} diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs index 487061ac..ed0963e5 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -6,7 +6,7 @@ namespace GFramework.Cqrs.SourceGenerators.Cqrs; /// 为当前编译程序集生成 CQRS 处理器注册器,以减少运行时的程序集反射扫描成本。 /// [Generator] -public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator +public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator { private const string CqrsContractsNamespace = $"{PathContests.CqrsAbstractionsNamespace}.Cqrs"; private const string CqrsRuntimeNamespace = PathContests.CqrsNamespace; @@ -350,14 +350,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator } registrations.Sort(static (left, right) => - { - var implementationComparison = StringComparer.Ordinal.Compare( - left.ImplementationLogName, - right.ImplementationLogName); - - return implementationComparison; - }); - + StringComparer.Ordinal.Compare(left.ImplementationLogName, right.ImplementationLogName)); return registrations; } @@ -390,250 +383,6 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator string.Equals(definitionMetadataName, IStreamRequestHandlerMetadataName, StringComparison.Ordinal); } - /// - /// 为无法直接在生成代码中书写的关闭处理器接口构造精确的运行时注册描述。 - /// - /// - /// 当前生成轮次对应的编译上下文,用于判断类型是否属于当前程序集,从而决定是生成直接类型引用还是延迟到运行时反射解析。 - /// - /// - /// 需要注册的关闭处理器接口。调用方应保证它来自受支持的 CQRS 处理器接口定义,并且其泛型参数顺序与运行时注册约定一致。 - /// - /// - /// 当方法返回 时,包含开放泛型处理器类型和每个运行时类型实参的精确描述; - /// 当方法返回 时,为默认值,调用方应回退到基于实现类型的宽松反射发现路径。 - /// - /// - /// 当接口上的所有运行时类型引用都能在生成阶段被稳定描述时返回 ; - /// 只要任一泛型实参无法安全编码到生成输出中,就返回 。 - /// - private static bool TryCreatePreciseReflectedRegistration( - Compilation compilation, - INamedTypeSymbol handlerInterface, - out PreciseReflectedRegistrationSpec registration) - { - var openHandlerTypeDisplayName = handlerInterface.OriginalDefinition - .ConstructUnboundGenericType() - .ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - var typeArguments = - ImmutableArray.CreateBuilder(handlerInterface.TypeArguments.Length); - foreach (var typeArgument in handlerInterface.TypeArguments) - { - if (!TryCreateRuntimeTypeReference(compilation, typeArgument, out var runtimeTypeReference)) - { - registration = default; - return false; - } - - typeArguments.Add(runtimeTypeReference!); - } - - registration = new PreciseReflectedRegistrationSpec( - openHandlerTypeDisplayName, - GetLogDisplayName(handlerInterface), - typeArguments.ToImmutable()); - return true; - } - - /// - /// 将 Roslyn 类型符号转换为生成注册器可消费的运行时类型引用描述。 - /// - /// - /// 当前编译上下文,用于区分可直接引用的外部可访问类型与必须通过当前程序集运行时反射查找的内部类型。 - /// - /// - /// 需要转换的类型符号。该方法会递归处理数组元素类型和已构造泛型的类型实参,但不会为未绑定泛型或类型参数生成引用。 - /// - /// - /// 当方法返回 时,包含可直接引用、数组、已构造泛型或反射查找中的一种运行时表示; - /// 当方法返回 时为 ,调用方应回退到更宽泛的实现类型反射扫描策略。 - /// - /// - /// 当 及其递归子结构都能映射为稳定的运行时引用时返回 ; - /// 若遇到类型参数、无法访问的运行时结构,或任一递归分支无法表示,则返回 。 - /// - private static bool TryCreateRuntimeTypeReference( - Compilation compilation, - ITypeSymbol type, - out RuntimeTypeReferenceSpec? runtimeTypeReference) - { - // CLR forbids pointer and function-pointer types from being used as generic arguments. - // CQRS handler contracts are generic interfaces, so emitting runtime reconstruction code for these - // shapes would only defer the failure to MakeGenericType(...) at runtime. - if (type is IPointerTypeSymbol or IFunctionPointerTypeSymbol) - { - runtimeTypeReference = null; - return false; - } - - if (CanReferenceFromGeneratedRegistry(compilation, type)) - { - runtimeTypeReference = RuntimeTypeReferenceSpec.FromDirectReference( - type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); - return true; - } - - if (type is IArrayTypeSymbol arrayType && - TryCreateRuntimeTypeReference(compilation, arrayType.ElementType, out var elementTypeReference)) - { - runtimeTypeReference = RuntimeTypeReferenceSpec.FromArray(elementTypeReference!, arrayType.Rank); - return true; - } - - if (type is INamedTypeSymbol genericNamedType && - genericNamedType.IsGenericType && - !genericNamedType.IsUnboundGenericType) - { - return TryCreateConstructedGenericRuntimeTypeReference( - compilation, - genericNamedType, - out runtimeTypeReference); - } - - if (type is INamedTypeSymbol namedType) - { - runtimeTypeReference = CreateNamedRuntimeTypeReference(compilation, namedType); - return true; - } - - runtimeTypeReference = null; - return false; - } - - /// - /// 为已构造泛型类型构造运行时类型引用,并递归验证每个泛型实参都可以稳定编码到生成输出中。 - /// - /// 当前生成轮次的编译上下文。 - /// 需要表示的已构造泛型类型。 - /// - /// 当方法返回 时,包含泛型定义和泛型实参的运行时重建描述。 - /// - /// 当泛型定义和全部泛型实参都能表达时返回 - private static bool TryCreateConstructedGenericRuntimeTypeReference( - Compilation compilation, - INamedTypeSymbol genericNamedType, - out RuntimeTypeReferenceSpec? runtimeTypeReference) - { - if (!TryCreateGenericTypeDefinitionReference( - compilation, - genericNamedType, - out var genericTypeDefinitionReference)) - { - runtimeTypeReference = null; - return false; - } - - var genericTypeArguments = - ImmutableArray.CreateBuilder(genericNamedType.TypeArguments.Length); - foreach (var typeArgument in genericNamedType.TypeArguments) - { - if (!TryCreateRuntimeTypeReference(compilation, typeArgument, out var genericTypeArgumentReference)) - { - runtimeTypeReference = null; - return false; - } - - genericTypeArguments.Add(genericTypeArgumentReference!); - } - - runtimeTypeReference = RuntimeTypeReferenceSpec.FromConstructedGeneric( - genericTypeDefinitionReference!, - genericTypeArguments.ToImmutable()); - return true; - } - - /// - /// 为无法直接书写的命名类型选择当前程序集反射查找或外部程序集反射查找表示。 - /// - /// 当前生成轮次的编译上下文。 - /// 需要在运行时解析的命名类型。 - /// 适合写入生成注册器的命名类型运行时引用。 - private static RuntimeTypeReferenceSpec CreateNamedRuntimeTypeReference( - Compilation compilation, - INamedTypeSymbol namedType) - { - if (SymbolEqualityComparer.Default.Equals(namedType.ContainingAssembly, compilation.Assembly)) - return RuntimeTypeReferenceSpec.FromReflectionLookup(GetReflectionTypeMetadataName(namedType)); - - return RuntimeTypeReferenceSpec.FromExternalReflectionLookup( - namedType.ContainingAssembly.Identity.ToString(), - GetReflectionTypeMetadataName(namedType)); - } - - /// - /// 为已构造泛型类型解析其泛型定义的运行时引用描述。 - /// - /// - /// 当前编译上下文,用于判断泛型定义是否应以内联类型引用形式生成,或在运行时通过当前程序集反射解析。 - /// - /// - /// 已构造的泛型类型。该方法只处理其原始泛型定义,不负责递归解析类型实参。 - /// - /// - /// 当方法返回 时,包含泛型定义的直接引用或反射查找描述; - /// 当方法返回 时为 ,调用方应停止精确构造并回退到更保守的注册路径。 - /// - /// - /// 当泛型定义能够以稳定方式编码到生成输出中时返回 ; - /// 若泛型定义既不能直接引用,也不属于当前程序集可供反射查找,则返回 。 - /// - private static bool TryCreateGenericTypeDefinitionReference( - Compilation compilation, - INamedTypeSymbol genericNamedType, - out RuntimeTypeReferenceSpec? genericTypeDefinitionReference) - { - var genericTypeDefinition = genericNamedType.OriginalDefinition; - if (CanReferenceFromGeneratedRegistry(compilation, genericTypeDefinition)) - { - genericTypeDefinitionReference = RuntimeTypeReferenceSpec.FromDirectReference( - genericTypeDefinition - .ConstructUnboundGenericType() - .ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); - return true; - } - - if (SymbolEqualityComparer.Default.Equals(genericTypeDefinition.ContainingAssembly, compilation.Assembly)) - { - genericTypeDefinitionReference = RuntimeTypeReferenceSpec.FromReflectionLookup( - GetReflectionTypeMetadataName(genericTypeDefinition)); - return true; - } - - genericTypeDefinitionReference = RuntimeTypeReferenceSpec.FromExternalReflectionLookup( - genericTypeDefinition.ContainingAssembly.Identity.ToString(), - GetReflectionTypeMetadataName(genericTypeDefinition)); - return true; - } - - private static bool CanReferenceFromGeneratedRegistry(Compilation compilation, ITypeSymbol type) - { - switch (type) - { - case IArrayTypeSymbol arrayType: - return CanReferenceFromGeneratedRegistry(compilation, arrayType.ElementType); - case INamedTypeSymbol namedType: - if (!compilation.IsSymbolAccessibleWithin(namedType, compilation.Assembly, throughType: null)) - return false; - - foreach (var typeArgument in namedType.TypeArguments) - { - if (!CanReferenceFromGeneratedRegistry(compilation, typeArgument)) - return false; - } - - return true; - case IPointerTypeSymbol: - case IFunctionPointerTypeSymbol: - return false; - case ITypeParameterSymbol: - return false; - default: - // Treat other Roslyn type kinds, such as dynamic or unresolved error types, as referenceable for now. - // If a real-world case proves unsafe, tighten this branch instead of broadening the named-type path above. - return true; - } - } - private static string GetFullyQualifiedMetadataName(INamedTypeSymbol type) { var nestedTypes = new Stack(); @@ -696,1130 +445,4 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator { return GetTypeSortKey(type).Replace("global::", string.Empty); } - - /// - /// 生成程序集级 CQRS handler 注册器源码。 - /// - /// - /// 当前轮次的生成环境,用于决定 runtime 是否提供 CqrsReflectionFallbackAttribute 契约,以及是否需要在输出中发射对应的程序集级元数据。 - /// - /// - /// 已整理并排序的 handler 注册描述。方法会据此生成 CqrsHandlerRegistry.g.cs,其中包含直接注册、实现类型反射注册、精确运行时类型查找等分支。 - /// - /// - /// 仍需依赖程序集级 reflection fallback 元数据恢复的 handler 元数据名称集合。 - /// 调用方必须先确保:若该集合非空,则 已声明支持对应的 fallback attribute 契约; - /// 否则应在进入本方法前报告诊断并放弃生成,而不是输出会静默漏注册的半成品注册器。 - /// - /// 完整的注册器源代码文本。 - /// - /// 当 为空时,输出只包含程序集级 CqrsHandlerRegistryAttribute 和注册器实现。 - /// 当其非空且 runtime 合同可用时,输出还会附带程序集级 CqrsReflectionFallbackAttribute,让运行时补齐生成阶段无法精确表达的剩余 handler。 - /// 该方法本身不报告诊断;“fallback 必需但 runtime 契约缺失”的错误由调用方在进入本方法前处理。 - /// - private static string GenerateSource( - GenerationEnvironment generationEnvironment, - IReadOnlyList registrations, - IReadOnlyList fallbackHandlerTypeMetadataNames) - { - var sourceShape = CreateGeneratedRegistrySourceShape(registrations); - var builder = new StringBuilder(); - AppendGeneratedSourcePreamble(builder, generationEnvironment, fallbackHandlerTypeMetadataNames); - AppendGeneratedRegistryType(builder, registrations, sourceShape); - return builder.ToString(); - } - - /// - /// 预先计算生成注册器需要的辅助分支,让主源码发射流程保持线性且避免重复扫描注册集合。 - /// - /// 已整理并排序的 handler 注册描述。 - /// 当前生成输出需要启用的结构分支。 - private static GeneratedRegistrySourceShape CreateGeneratedRegistrySourceShape( - IReadOnlyList registrations) - { - var hasReflectedImplementationRegistrations = registrations.Any(static registration => - !registration.ReflectedImplementationRegistrations.IsDefaultOrEmpty); - var hasPreciseReflectedRegistrations = registrations.Any(static registration => - !registration.PreciseReflectedRegistrations.IsDefaultOrEmpty); - var hasReflectionTypeLookups = registrations.Any(static registration => - !string.IsNullOrWhiteSpace(registration.ReflectionTypeMetadataName)); - var hasExternalAssemblyTypeLookups = registrations.Any(static registration => - registration.PreciseReflectedRegistrations.Any(static preciseRegistration => - preciseRegistration.ServiceTypeArguments.Any(ContainsExternalAssemblyTypeLookup))); - - return new GeneratedRegistrySourceShape( - hasReflectedImplementationRegistrations, - hasPreciseReflectedRegistrations, - hasReflectionTypeLookups, - hasExternalAssemblyTypeLookups); - } - - /// - /// 发射生成文件头、nullable 指令以及注册器所需的程序集级元数据特性。 - /// - /// 生成源码构造器。 - /// 当前轮次的生成环境。 - /// 需要程序集级 reflection fallback 的 handler 元数据名称。 - private static void AppendGeneratedSourcePreamble( - StringBuilder builder, - GenerationEnvironment generationEnvironment, - IReadOnlyList fallbackHandlerTypeMetadataNames) - { - builder.AppendLine("// "); - builder.AppendLine("#nullable enable"); - builder.AppendLine(); - if (generationEnvironment.SupportsReflectionFallbackAttribute && fallbackHandlerTypeMetadataNames.Count > 0) - { - AppendReflectionFallbackAttribute(builder, fallbackHandlerTypeMetadataNames); - builder.AppendLine(); - } - - builder.Append("[assembly: global::"); - builder.Append(CqrsRuntimeNamespace); - builder.Append(".CqrsHandlerRegistryAttribute(typeof(global::"); - builder.Append(GeneratedNamespace); - builder.Append('.'); - builder.Append(GeneratedTypeName); - builder.AppendLine("))]"); - } - - /// - /// 发射程序集级 reflection fallback 元数据特性,供运行时补齐生成阶段无法精确表达的 handler。 - /// - /// 生成源码构造器。 - /// 需要写入特性的 handler 元数据名称。 - private static void AppendReflectionFallbackAttribute( - StringBuilder builder, - IReadOnlyList fallbackHandlerTypeMetadataNames) - { - builder.Append("[assembly: global::"); - builder.Append(CqrsRuntimeNamespace); - builder.Append(".CqrsReflectionFallbackAttribute("); - for (var index = 0; index < fallbackHandlerTypeMetadataNames.Count; index++) - { - if (index > 0) - builder.Append(", "); - - builder.Append('"'); - builder.Append(EscapeStringLiteral(fallbackHandlerTypeMetadataNames[index])); - builder.Append('"'); - } - - builder.AppendLine(")]"); - } - - /// - /// 发射生成注册器类型本体,包括 Register 方法和运行时反射辅助方法。 - /// - /// 生成源码构造器。 - /// 已排序的 handler 注册描述。 - /// 当前输出需要启用的结构分支。 - private static void AppendGeneratedRegistryType( - StringBuilder builder, - IReadOnlyList registrations, - GeneratedRegistrySourceShape sourceShape) - { - builder.AppendLine(); - builder.Append("namespace "); - builder.Append(GeneratedNamespace); - builder.AppendLine(";"); - builder.AppendLine(); - builder.Append("internal sealed class "); - builder.Append(GeneratedTypeName); - builder.Append(" : global::"); - builder.Append(CqrsRuntimeNamespace); - builder.AppendLine(".ICqrsHandlerRegistry"); - builder.AppendLine("{"); - AppendRegisterMethod(builder, registrations, sourceShape); - - if (sourceShape.HasExternalAssemblyTypeLookups) - { - builder.AppendLine(); - AppendReflectionHelpers(builder, sourceShape.HasExternalAssemblyTypeLookups); - } - - builder.AppendLine("}"); - } - - /// - /// 发射注册器的 Register 方法,保持直接注册和反射注册之间的原始稳定排序。 - /// - /// 生成源码构造器。 - /// 已排序的 handler 注册描述。 - /// 当前输出需要启用的结构分支。 - private static void AppendRegisterMethod( - StringBuilder builder, - IReadOnlyList registrations, - GeneratedRegistrySourceShape sourceShape) - { - builder.Append( - " public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::"); - builder.Append(LoggingNamespace); - builder.AppendLine(".ILogger logger)"); - builder.AppendLine(" {"); - builder.AppendLine(" if (services is null)"); - builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(services));"); - builder.AppendLine(" if (logger is null)"); - builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(logger));"); - if (sourceShape.RequiresRegistryAssemblyVariable) - { - builder.AppendLine(); - builder.Append(" var registryAssembly = typeof(global::"); - builder.Append(GeneratedNamespace); - builder.Append('.'); - builder.Append(GeneratedTypeName); - builder.AppendLine(").Assembly;"); - } - - if (registrations.Count > 0) - builder.AppendLine(); - - for (var registrationIndex = 0; registrationIndex < registrations.Count; registrationIndex++) - { - var registration = registrations[registrationIndex]; - if (!registration.ReflectedImplementationRegistrations.IsDefaultOrEmpty || - !registration.PreciseReflectedRegistrations.IsDefaultOrEmpty) - { - AppendOrderedImplementationRegistrations(builder, registration, registrationIndex); - } - else if (!registration.DirectRegistrations.IsDefaultOrEmpty) - { - AppendDirectRegistrations(builder, registration); - } - } - - builder.AppendLine(" }"); - } - - private static void AppendDirectRegistrations( - StringBuilder builder, - ImplementationRegistrationSpec registration) - { - foreach (var directRegistration in registration.DirectRegistrations) - { - AppendServiceRegistration( - builder, - $"typeof({directRegistration.HandlerInterfaceDisplayName})", - $"typeof({directRegistration.ImplementationTypeDisplayName})", - " "); - AppendRegistrationLog( - builder, - directRegistration.ImplementationLogName, - directRegistration.HandlerInterfaceLogName, - " "); - } - } - - /// - /// 发射 AddTransient 调用,调用方负责传入已经按当前分支解析好的 service 和 implementation 表达式。 - /// - /// 生成源码构造器。 - /// 生成代码中的服务类型表达式。 - /// 生成代码中的实现类型表达式。 - /// 当前生成语句的缩进。 - private static void AppendServiceRegistration( - StringBuilder builder, - string serviceTypeExpression, - string implementationTypeExpression, - string indent) - { - builder.Append(indent); - builder.AppendLine("global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient("); - builder.Append(indent); - builder.AppendLine(" services,"); - builder.Append(indent); - builder.Append(" "); - builder.Append(serviceTypeExpression); - builder.AppendLine(","); - builder.Append(indent); - builder.Append(" "); - builder.Append(implementationTypeExpression); - builder.AppendLine(");"); - } - - /// - /// 发射与注册语句配套的调试日志,保持所有生成注册路径的日志文本完全一致。 - /// - /// 生成源码构造器。 - /// 实现类型日志名。 - /// handler 接口日志名。 - /// 当前生成语句的缩进。 - private static void AppendRegistrationLog( - StringBuilder builder, - string implementationLogName, - string handlerInterfaceLogName, - string indent) - { - builder.Append(indent); - builder.Append("logger.Debug(\"Registered CQRS handler "); - builder.Append(EscapeStringLiteral(implementationLogName)); - builder.Append(" as "); - builder.Append(EscapeStringLiteral(handlerInterfaceLogName)); - builder.AppendLine(".\");"); - } - - private static void AppendOrderedImplementationRegistrations( - StringBuilder builder, - ImplementationRegistrationSpec registration, - int registrationIndex) - { - var orderedRegistrations = CreateOrderedRegistrations(registration); - var implementationVariableName = $"implementationType{registrationIndex}"; - AppendImplementationTypeVariable(builder, registration, implementationVariableName); - - builder.Append(" if ("); - builder.Append(implementationVariableName); - builder.AppendLine(" is not null)"); - builder.AppendLine(" {"); - - foreach (var orderedRegistration in orderedRegistrations) - { - AppendOrderedRegistration( - builder, - registration, - orderedRegistration, - registrationIndex, - implementationVariableName); - } - - builder.AppendLine(" }"); - } - - /// - /// 合并直接注册、实现类型反射注册和精确反射注册,并按 handler 接口日志名排序以保持生成输出稳定。 - /// - /// 单个实现类型聚合后的注册描述。 - /// 带有来源类型和原始索引的有序注册列表。 - private static List CreateOrderedRegistrations(ImplementationRegistrationSpec registration) - { - var orderedRegistrations = new List( - registration.DirectRegistrations.Length + - registration.ReflectedImplementationRegistrations.Length + - registration.PreciseReflectedRegistrations.Length); - for (var directIndex = 0; directIndex < registration.DirectRegistrations.Length; directIndex++) - { - orderedRegistrations.Add(new OrderedRegistrationSpec( - registration.DirectRegistrations[directIndex].HandlerInterfaceLogName, - OrderedRegistrationKind.Direct, - directIndex)); - } - - for (var reflectedIndex = 0; - reflectedIndex < registration.ReflectedImplementationRegistrations.Length; - reflectedIndex++) - { - orderedRegistrations.Add(new OrderedRegistrationSpec( - registration.ReflectedImplementationRegistrations[reflectedIndex].HandlerInterfaceLogName, - OrderedRegistrationKind.ReflectedImplementation, - reflectedIndex)); - } - - for (var preciseIndex = 0; - preciseIndex < registration.PreciseReflectedRegistrations.Length; - preciseIndex++) - { - orderedRegistrations.Add(new OrderedRegistrationSpec( - registration.PreciseReflectedRegistrations[preciseIndex].HandlerInterfaceLogName, - OrderedRegistrationKind.PreciseReflected, - preciseIndex)); - } - - orderedRegistrations.Sort(static (left, right) => - StringComparer.Ordinal.Compare(left.HandlerInterfaceLogName, right.HandlerInterfaceLogName)); - return orderedRegistrations; - } - - /// - /// 发射实现类型变量。公开类型直接使用 typeof,不可直接引用的实现类型则从当前程序集反射解析。 - /// - /// 生成源码构造器。 - /// 单个实现类型聚合后的注册描述。 - /// 生成代码中的实现类型变量名。 - private static void AppendImplementationTypeVariable( - StringBuilder builder, - ImplementationRegistrationSpec registration, - string implementationVariableName) - { - if (string.IsNullOrWhiteSpace(registration.ReflectionTypeMetadataName)) - { - builder.Append(" var "); - builder.Append(implementationVariableName); - builder.Append(" = typeof("); - builder.Append(registration.ImplementationTypeDisplayName); - builder.AppendLine(");"); - } - else - { - builder.Append(" var "); - builder.Append(implementationVariableName); - builder.Append(" = registryAssembly.GetType(\""); - builder.Append(EscapeStringLiteral(registration.ReflectionTypeMetadataName!)); - builder.AppendLine("\", throwOnError: false, ignoreCase: false);"); - } - } - - /// - /// 根据注册来源发射单条有序注册,确保混合直接和反射路径时仍按 handler 接口名稳定输出。 - /// - /// 生成源码构造器。 - /// 单个实现类型聚合后的注册描述。 - /// 带来源类型和原始索引的排序项。 - /// 实现类型在整体注册列表中的索引,用于生成稳定变量名。 - /// 生成代码中的实现类型变量名。 - private static void AppendOrderedRegistration( - StringBuilder builder, - ImplementationRegistrationSpec registration, - OrderedRegistrationSpec orderedRegistration, - int registrationIndex, - string implementationVariableName) - { - switch (orderedRegistration.Kind) - { - case OrderedRegistrationKind.Direct: - AppendOrderedDirectRegistration( - builder, - registration, - registration.DirectRegistrations[orderedRegistration.Index], - implementationVariableName); - break; - case OrderedRegistrationKind.ReflectedImplementation: - AppendOrderedReflectedImplementationRegistration( - builder, - registration, - registration.ReflectedImplementationRegistrations[orderedRegistration.Index], - implementationVariableName); - break; - case OrderedRegistrationKind.PreciseReflected: - AppendOrderedPreciseReflectedRegistration( - builder, - registration, - registration.PreciseReflectedRegistrations[orderedRegistration.Index], - registrationIndex, - orderedRegistration.Index, - implementationVariableName); - break; - default: - throw new InvalidOperationException( - $"Unsupported ordered CQRS registration kind {orderedRegistration.Kind}."); - } - } - - /// - /// 发射实现类型已通过变量解析、handler 接口可直接引用的直接注册语句。 - /// - /// 生成源码构造器。 - /// 单个实现类型聚合后的注册描述。 - /// 当前直接注册项。 - /// 生成代码中的实现类型变量名。 - private static void AppendOrderedDirectRegistration( - StringBuilder builder, - ImplementationRegistrationSpec registration, - HandlerRegistrationSpec directRegistration, - string implementationVariableName) - { - AppendServiceRegistration( - builder, - $"typeof({directRegistration.HandlerInterfaceDisplayName})", - implementationVariableName, - " "); - AppendRegistrationLog( - builder, - registration.ImplementationLogName, - directRegistration.HandlerInterfaceLogName, - " "); - } - - /// - /// 发射实现类型需要反射解析、handler 接口可直接引用的注册语句。 - /// - /// 生成源码构造器。 - /// 单个实现类型聚合后的注册描述。 - /// 当前实现类型反射注册项。 - /// 生成代码中的实现类型变量名。 - private static void AppendOrderedReflectedImplementationRegistration( - StringBuilder builder, - ImplementationRegistrationSpec registration, - ReflectedImplementationRegistrationSpec reflectedRegistration, - string implementationVariableName) - { - AppendServiceRegistration( - builder, - $"typeof({reflectedRegistration.HandlerInterfaceDisplayName})", - implementationVariableName, - " "); - AppendRegistrationLog( - builder, - registration.ImplementationLogName, - reflectedRegistration.HandlerInterfaceLogName, - " "); - } - - /// - /// 发射 handler 接口需要运行时精确构造的注册语句。 - /// - /// 生成源码构造器。 - /// 单个实现类型聚合后的注册描述。 - /// 当前精确反射注册项。 - /// 实现类型在整体注册列表中的索引。 - /// 当前注册项在原始精确反射注册集合中的索引。 - /// 生成代码中的实现类型变量名。 - private static void AppendOrderedPreciseReflectedRegistration( - StringBuilder builder, - ImplementationRegistrationSpec registration, - PreciseReflectedRegistrationSpec preciseRegistration, - int registrationIndex, - int orderedRegistrationIndex, - string implementationVariableName) - { - var registrationVariablePrefix = $"serviceType{registrationIndex}_{orderedRegistrationIndex}"; - AppendPreciseReflectedTypeResolution( - builder, - preciseRegistration.ServiceTypeArguments, - registrationVariablePrefix, - implementationVariableName, - preciseRegistration.OpenHandlerTypeDisplayName, - registration.ImplementationLogName, - preciseRegistration.HandlerInterfaceLogName, - 3); - } - - private static void AppendPreciseReflectedTypeResolution( - StringBuilder builder, - ImmutableArray serviceTypeArguments, - string registrationVariablePrefix, - string implementationVariableName, - string openHandlerTypeDisplayName, - string implementationLogName, - string handlerInterfaceLogName, - int indentLevel) - { - var indent = new string(' ', indentLevel * 4); - var reflectedArgumentNames = new List(); - var resolvedArgumentNames = AppendServiceTypeArgumentResolutions( - builder, - serviceTypeArguments, - registrationVariablePrefix, - reflectedArgumentNames, - indent); - - if (reflectedArgumentNames.Count > 0) - indent = AppendReflectedArgumentGuardStart(builder, reflectedArgumentNames, indent); - - AppendClosedGenericServiceTypeCreation( - builder, - registrationVariablePrefix, - openHandlerTypeDisplayName, - resolvedArgumentNames, - indent); - AppendServiceRegistration(builder, registrationVariablePrefix, implementationVariableName, indent); - AppendRegistrationLog(builder, implementationLogName, handlerInterfaceLogName, indent); - - if (reflectedArgumentNames.Count > 0) - { - builder.Append(new string(' ', indentLevel * 4)); - builder.AppendLine("}"); - } - } - - /// - /// 递归发射每个 handler 泛型实参的运行时类型解析表达式。 - /// - /// 生成源码构造器。 - /// handler 服务类型的运行时泛型实参描述。 - /// 当前注册项的稳定变量名前缀。 - /// 需要空值检查的反射解析变量集合。 - /// 当前生成语句的缩进。 - /// 可传给 MakeGenericType 的实参表达式。 - private static string[] AppendServiceTypeArgumentResolutions( - StringBuilder builder, - ImmutableArray serviceTypeArguments, - string registrationVariablePrefix, - ICollection reflectedArgumentNames, - string indent) - { - var resolvedArgumentNames = new string[serviceTypeArguments.Length]; - for (var argumentIndex = 0; argumentIndex < serviceTypeArguments.Length; argumentIndex++) - { - resolvedArgumentNames[argumentIndex] = AppendRuntimeTypeReferenceResolution( - builder, - serviceTypeArguments[argumentIndex], - $"{registrationVariablePrefix}Argument{argumentIndex}", - reflectedArgumentNames, - indent); - } - - return resolvedArgumentNames; - } - - /// - /// 为运行时反射解析出的泛型实参发射空值保护块,避免生成注册器注册无法完整构造的服务类型。 - /// - /// 生成源码构造器。 - /// 需要参与空值检查的变量名。 - /// 保护块开始前的缩进。 - /// 保护块内部应使用的下一层缩进。 - private static string AppendReflectedArgumentGuardStart( - StringBuilder builder, - IReadOnlyList reflectedArgumentNames, - string indent) - { - builder.Append(indent); - builder.Append("if ("); - for (var index = 0; index < reflectedArgumentNames.Count; index++) - { - if (index > 0) - builder.Append(" && "); - - builder.Append(reflectedArgumentNames[index]); - builder.Append(" is not null"); - } - - builder.AppendLine(")"); - builder.Append(indent); - builder.AppendLine("{"); - return $"{indent} "; - } - - /// - /// 发射关闭 handler 服务类型的 MakeGenericType 构造语句。 - /// - /// 生成源码构造器。 - /// 生成代码中的服务类型变量名。 - /// 开放 handler 接口类型显示名。 - /// 已解析的泛型实参表达式。 - /// 当前生成语句的缩进。 - private static void AppendClosedGenericServiceTypeCreation( - StringBuilder builder, - string registrationVariablePrefix, - string openHandlerTypeDisplayName, - IReadOnlyList resolvedArgumentNames, - string indent) - { - builder.Append(indent); - builder.Append("var "); - builder.Append(registrationVariablePrefix); - builder.Append(" = typeof("); - builder.Append(openHandlerTypeDisplayName); - builder.Append(").MakeGenericType("); - for (var index = 0; index < resolvedArgumentNames.Count; index++) - { - if (index > 0) - builder.Append(", "); - - builder.Append(resolvedArgumentNames[index]); - } - - builder.AppendLine(");"); - } - - private static string AppendRuntimeTypeReferenceResolution( - StringBuilder builder, - RuntimeTypeReferenceSpec runtimeTypeReference, - string variableBaseName, - ICollection reflectedArgumentNames, - string indent) - { - if (!string.IsNullOrWhiteSpace(runtimeTypeReference.TypeDisplayName)) - return $"typeof({runtimeTypeReference.TypeDisplayName})"; - - if (runtimeTypeReference.ArrayElementTypeReference is not null) - return AppendArrayRuntimeTypeReferenceResolution( - builder, - runtimeTypeReference, - variableBaseName, - reflectedArgumentNames, - indent); - - if (runtimeTypeReference.PointerElementTypeReference is not null) - return AppendPointerRuntimeTypeReferenceResolution( - builder, - runtimeTypeReference, - variableBaseName, - reflectedArgumentNames, - indent); - - if (runtimeTypeReference.GenericTypeDefinitionReference is not null) - return AppendConstructedGenericRuntimeTypeReferenceResolution( - builder, - runtimeTypeReference, - variableBaseName, - reflectedArgumentNames, - indent); - - return AppendReflectionRuntimeTypeReferenceResolution( - builder, - runtimeTypeReference, - variableBaseName, - reflectedArgumentNames, - indent); - } - - /// - /// 发射数组类型引用的运行时重建表达式。 - /// - /// 生成源码构造器。 - /// 数组类型引用描述。 - /// 用于递归生成变量名的稳定前缀。 - /// 需要空值检查的反射解析变量集合。 - /// 当前生成语句的缩进。 - /// 数组类型表达式。 - private static string AppendArrayRuntimeTypeReferenceResolution( - StringBuilder builder, - RuntimeTypeReferenceSpec runtimeTypeReference, - string variableBaseName, - ICollection reflectedArgumentNames, - string indent) - { - var elementExpression = AppendRuntimeTypeReferenceResolution( - builder, - runtimeTypeReference.ArrayElementTypeReference!, - $"{variableBaseName}Element", - reflectedArgumentNames, - indent); - - return runtimeTypeReference.ArrayRank == 1 - ? $"{elementExpression}.MakeArrayType()" - : $"{elementExpression}.MakeArrayType({runtimeTypeReference.ArrayRank})"; - } - - /// - /// 发射指针类型引用的运行时重建表达式。 - /// - /// 生成源码构造器。 - /// 指针类型引用描述。 - /// 用于递归生成变量名的稳定前缀。 - /// 需要空值检查的反射解析变量集合。 - /// 当前生成语句的缩进。 - /// 指针类型表达式。 - private static string AppendPointerRuntimeTypeReferenceResolution( - StringBuilder builder, - RuntimeTypeReferenceSpec runtimeTypeReference, - string variableBaseName, - ICollection reflectedArgumentNames, - string indent) - { - var pointedAtExpression = AppendRuntimeTypeReferenceResolution( - builder, - runtimeTypeReference.PointerElementTypeReference!, - $"{variableBaseName}PointedAt", - reflectedArgumentNames, - indent); - - return $"{pointedAtExpression}.MakePointerType()"; - } - - /// - /// 发射已构造泛型类型引用的运行时重建表达式。 - /// - /// 生成源码构造器。 - /// 已构造泛型类型引用描述。 - /// 用于递归生成变量名的稳定前缀。 - /// 需要空值检查的反射解析变量集合。 - /// 当前生成语句的缩进。 - /// 已构造泛型类型表达式。 - private static string AppendConstructedGenericRuntimeTypeReferenceResolution( - StringBuilder builder, - RuntimeTypeReferenceSpec runtimeTypeReference, - string variableBaseName, - ICollection reflectedArgumentNames, - string indent) - { - var genericTypeDefinitionExpression = AppendRuntimeTypeReferenceResolution( - builder, - runtimeTypeReference.GenericTypeDefinitionReference!, - $"{variableBaseName}GenericDefinition", - reflectedArgumentNames, - indent); - var genericArgumentExpressions = new string[runtimeTypeReference.GenericTypeArguments.Length]; - for (var argumentIndex = 0; - argumentIndex < runtimeTypeReference.GenericTypeArguments.Length; - argumentIndex++) - { - genericArgumentExpressions[argumentIndex] = AppendRuntimeTypeReferenceResolution( - builder, - runtimeTypeReference.GenericTypeArguments[argumentIndex], - $"{variableBaseName}GenericArgument{argumentIndex}", - reflectedArgumentNames, - indent); - } - - return $"{genericTypeDefinitionExpression}.MakeGenericType({string.Join(", ", genericArgumentExpressions)})"; - } - - /// - /// 发射命名类型的运行时反射查找语句,并返回后续服务类型构造应引用的变量名。 - /// - /// 生成源码构造器。 - /// 反射查找类型引用描述。 - /// 生成代码中的反射变量名。 - /// 需要空值检查的反射解析变量集合。 - /// 当前生成语句的缩进。 - /// 生成代码中的反射变量名。 - private static string AppendReflectionRuntimeTypeReferenceResolution( - StringBuilder builder, - RuntimeTypeReferenceSpec runtimeTypeReference, - string variableBaseName, - ICollection reflectedArgumentNames, - string indent) - { - reflectedArgumentNames.Add(variableBaseName); - builder.Append(indent); - builder.Append("var "); - builder.Append(variableBaseName); - if (string.IsNullOrWhiteSpace(runtimeTypeReference.ReflectionAssemblyName)) - { - builder.Append(" = registryAssembly.GetType(\""); - builder.Append(EscapeStringLiteral(runtimeTypeReference.ReflectionTypeMetadataName!)); - builder.AppendLine("\", throwOnError: false, ignoreCase: false);"); - } - else - { - builder.Append(" = ResolveReferencedAssemblyType(\""); - builder.Append(EscapeStringLiteral(runtimeTypeReference.ReflectionAssemblyName!)); - builder.Append("\", \""); - builder.Append(EscapeStringLiteral(runtimeTypeReference.ReflectionTypeMetadataName!)); - builder.AppendLine("\");"); - } - - return variableBaseName; - } - - private static void AppendReflectionHelpers( - StringBuilder builder, - bool includeExternalAssemblyTypeLookupHelpers) - { - if (includeExternalAssemblyTypeLookupHelpers) - { - builder.AppendLine( - " private static global::System.Type? ResolveReferencedAssemblyType(string assemblyIdentity, string typeMetadataName)"); - builder.AppendLine(" {"); - builder.AppendLine(" var assembly = ResolveReferencedAssembly(assemblyIdentity);"); - builder.AppendLine( - " return assembly?.GetType(typeMetadataName, throwOnError: false, ignoreCase: false);"); - builder.AppendLine(" }"); - builder.AppendLine(); - builder.AppendLine( - " private static global::System.Reflection.Assembly? ResolveReferencedAssembly(string assemblyIdentity)"); - builder.AppendLine(" {"); - builder.AppendLine(" global::System.Reflection.AssemblyName targetAssemblyName;"); - builder.AppendLine(" try"); - builder.AppendLine(" {"); - builder.AppendLine( - " targetAssemblyName = new global::System.Reflection.AssemblyName(assemblyIdentity);"); - builder.AppendLine(" }"); - builder.AppendLine(" catch"); - builder.AppendLine(" {"); - builder.AppendLine(" return null;"); - builder.AppendLine(" }"); - builder.AppendLine(); - builder.AppendLine( - " foreach (var assembly in global::System.AppDomain.CurrentDomain.GetAssemblies())"); - builder.AppendLine(" {"); - builder.AppendLine( - " if (global::System.Reflection.AssemblyName.ReferenceMatchesDefinition(targetAssemblyName, assembly.GetName()))"); - builder.AppendLine(" return assembly;"); - builder.AppendLine(" }"); - builder.AppendLine(); - builder.AppendLine(" try"); - builder.AppendLine(" {"); - builder.AppendLine( - " return global::System.Reflection.Assembly.Load(targetAssemblyName);"); - builder.AppendLine(" }"); - builder.AppendLine(" catch"); - builder.AppendLine(" {"); - builder.AppendLine(" return null;"); - builder.AppendLine(" }"); - builder.AppendLine(" }"); - } - } - - private static string EscapeStringLiteral(string value) - { - return value.Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\n", "\\n") - .Replace("\r", "\\r"); - } - - private static bool ContainsExternalAssemblyTypeLookup(RuntimeTypeReferenceSpec runtimeTypeReference) - { - if (!string.IsNullOrWhiteSpace(runtimeTypeReference.ReflectionAssemblyName)) - return true; - - if (runtimeTypeReference.ArrayElementTypeReference is not null && - ContainsExternalAssemblyTypeLookup(runtimeTypeReference.ArrayElementTypeReference)) - { - return true; - } - - if (runtimeTypeReference.PointerElementTypeReference is not null && - ContainsExternalAssemblyTypeLookup(runtimeTypeReference.PointerElementTypeReference)) - { - return true; - } - - if (runtimeTypeReference.GenericTypeDefinitionReference is not null && - ContainsExternalAssemblyTypeLookup(runtimeTypeReference.GenericTypeDefinitionReference)) - { - return true; - } - - foreach (var genericTypeArgument in runtimeTypeReference.GenericTypeArguments) - { - if (ContainsExternalAssemblyTypeLookup(genericTypeArgument)) - return true; - } - - return false; - } - - private readonly record struct HandlerRegistrationSpec( - string HandlerInterfaceDisplayName, - string ImplementationTypeDisplayName, - string HandlerInterfaceLogName, - string ImplementationLogName); - - private readonly record struct ReflectedImplementationRegistrationSpec( - string HandlerInterfaceDisplayName, - string HandlerInterfaceLogName); - - private readonly record struct OrderedRegistrationSpec( - string HandlerInterfaceLogName, - OrderedRegistrationKind Kind, - int Index); - - private readonly record struct GeneratedRegistrySourceShape( - bool HasReflectedImplementationRegistrations, - bool HasPreciseReflectedRegistrations, - bool HasReflectionTypeLookups, - bool HasExternalAssemblyTypeLookups) - { - public bool RequiresRegistryAssemblyVariable => - HasReflectedImplementationRegistrations || - HasPreciseReflectedRegistrations || - HasReflectionTypeLookups; - } - - /// - /// 标记某条 handler 注册语句在生成阶段采用的表达策略。 - /// - /// - /// 该枚举只服务于输出排序与代码分支选择,用来保证生成注册器在“直接注册” - /// “反射实现类型查找”和“精确运行时类型解析”之间保持稳定顺序。 - /// - private enum OrderedRegistrationKind - { - Direct, - ReflectedImplementation, - PreciseReflected - } - - /// - /// 描述生成注册器中某个运行时类型引用的构造方式。 - /// - /// - /// 某些 handler 服务类型可以直接以 typeof(...) 输出,某些则需要在运行时补充 - /// 反射查找、数组/指针封装或泛型实参重建。该记录把这些差异收敛为统一的递归结构, - /// 供源码输出阶段生成稳定的类型解析语句。 - /// - private sealed record RuntimeTypeReferenceSpec( - string? TypeDisplayName, - string? ReflectionTypeMetadataName, - string? ReflectionAssemblyName, - RuntimeTypeReferenceSpec? ArrayElementTypeReference, - int ArrayRank, - RuntimeTypeReferenceSpec? PointerElementTypeReference, - RuntimeTypeReferenceSpec? GenericTypeDefinitionReference, - ImmutableArray GenericTypeArguments) - { - /// - /// 创建一个可直接通过 typeof(...) 表达的类型引用。 - /// - public static RuntimeTypeReferenceSpec FromDirectReference(string typeDisplayName) - { - return new RuntimeTypeReferenceSpec(typeDisplayName, null, null, null, 0, null, null, - ImmutableArray.Empty); - } - - /// - /// 创建一个需要从当前消费端程序集反射解析的类型引用。 - /// - public static RuntimeTypeReferenceSpec FromReflectionLookup(string reflectionTypeMetadataName) - { - return new RuntimeTypeReferenceSpec(null, reflectionTypeMetadataName, null, null, 0, null, null, - ImmutableArray.Empty); - } - - /// - /// 创建一个需要从被引用程序集反射解析的类型引用。 - /// - public static RuntimeTypeReferenceSpec FromExternalReflectionLookup( - string reflectionAssemblyName, - string reflectionTypeMetadataName) - { - return new RuntimeTypeReferenceSpec(null, reflectionTypeMetadataName, reflectionAssemblyName, null, 0, - null, null, - ImmutableArray.Empty); - } - - /// - /// 创建一个数组类型引用。 - /// - public static RuntimeTypeReferenceSpec FromArray(RuntimeTypeReferenceSpec elementTypeReference, int arrayRank) - { - return new RuntimeTypeReferenceSpec(null, null, null, elementTypeReference, arrayRank, null, null, - ImmutableArray.Empty); - } - - /// - /// 创建一个指针类型引用。 - /// - public static RuntimeTypeReferenceSpec FromPointer(RuntimeTypeReferenceSpec pointedAtTypeReference) - { - return new RuntimeTypeReferenceSpec(null, null, null, null, 0, pointedAtTypeReference, null, - ImmutableArray.Empty); - } - - /// - /// 创建一个封闭泛型类型引用。 - /// - public static RuntimeTypeReferenceSpec FromConstructedGeneric( - RuntimeTypeReferenceSpec genericTypeDefinitionReference, - ImmutableArray genericTypeArguments) - { - return new RuntimeTypeReferenceSpec(null, null, null, null, 0, null, genericTypeDefinitionReference, - genericTypeArguments); - } - } - - private readonly record struct PreciseReflectedRegistrationSpec( - string OpenHandlerTypeDisplayName, - string HandlerInterfaceLogName, - ImmutableArray ServiceTypeArguments); - - private readonly record struct ImplementationRegistrationSpec( - string ImplementationTypeDisplayName, - string ImplementationLogName, - ImmutableArray DirectRegistrations, - ImmutableArray ReflectedImplementationRegistrations, - ImmutableArray PreciseReflectedRegistrations, - string? ReflectionTypeMetadataName, - string? ReflectionFallbackHandlerTypeMetadataName); - - private readonly struct HandlerCandidateAnalysis : IEquatable - { - public HandlerCandidateAnalysis( - string implementationTypeDisplayName, - string implementationLogName, - ImmutableArray registrations, - ImmutableArray reflectedImplementationRegistrations, - ImmutableArray preciseReflectedRegistrations, - string? reflectionTypeMetadataName, - string? reflectionFallbackHandlerTypeMetadataName) - { - ImplementationTypeDisplayName = implementationTypeDisplayName; - ImplementationLogName = implementationLogName; - Registrations = registrations; - ReflectedImplementationRegistrations = reflectedImplementationRegistrations; - PreciseReflectedRegistrations = preciseReflectedRegistrations; - ReflectionTypeMetadataName = reflectionTypeMetadataName; - ReflectionFallbackHandlerTypeMetadataName = reflectionFallbackHandlerTypeMetadataName; - } - - public string ImplementationTypeDisplayName { get; } - - public string ImplementationLogName { get; } - - public ImmutableArray Registrations { get; } - - public ImmutableArray ReflectedImplementationRegistrations { get; } - - public ImmutableArray PreciseReflectedRegistrations { get; } - - public string? ReflectionTypeMetadataName { get; } - - public string? ReflectionFallbackHandlerTypeMetadataName { get; } - - public bool Equals(HandlerCandidateAnalysis other) - { - if (!string.Equals(ImplementationTypeDisplayName, other.ImplementationTypeDisplayName, - StringComparison.Ordinal) || - !string.Equals(ImplementationLogName, other.ImplementationLogName, StringComparison.Ordinal) || - !string.Equals(ReflectionTypeMetadataName, other.ReflectionTypeMetadataName, - StringComparison.Ordinal) || - !string.Equals( - ReflectionFallbackHandlerTypeMetadataName, - other.ReflectionFallbackHandlerTypeMetadataName, - StringComparison.Ordinal) || - Registrations.Length != other.Registrations.Length || - ReflectedImplementationRegistrations.Length != other.ReflectedImplementationRegistrations.Length || - PreciseReflectedRegistrations.Length != other.PreciseReflectedRegistrations.Length) - { - return false; - } - - for (var index = 0; index < Registrations.Length; index++) - { - if (!Registrations[index].Equals(other.Registrations[index])) - return false; - } - - for (var index = 0; index < ReflectedImplementationRegistrations.Length; index++) - { - if (!ReflectedImplementationRegistrations[index].Equals( - other.ReflectedImplementationRegistrations[index])) - return false; - } - - for (var index = 0; index < PreciseReflectedRegistrations.Length; index++) - { - if (!PreciseReflectedRegistrations[index].Equals(other.PreciseReflectedRegistrations[index])) - return false; - } - - return true; - } - - public override bool Equals(object? obj) - { - return obj is HandlerCandidateAnalysis other && Equals(other); - } - - public override int GetHashCode() - { - unchecked - { - var hashCode = StringComparer.Ordinal.GetHashCode(ImplementationTypeDisplayName); - hashCode = (hashCode * 397) ^ StringComparer.Ordinal.GetHashCode(ImplementationLogName); - hashCode = (hashCode * 397) ^ - (ReflectionTypeMetadataName is null - ? 0 - : StringComparer.Ordinal.GetHashCode(ReflectionTypeMetadataName)); - hashCode = (hashCode * 397) ^ - (ReflectionFallbackHandlerTypeMetadataName is null - ? 0 - : StringComparer.Ordinal.GetHashCode(ReflectionFallbackHandlerTypeMetadataName)); - foreach (var registration in Registrations) - { - hashCode = (hashCode * 397) ^ registration.GetHashCode(); - } - - foreach (var reflectedImplementationRegistration in ReflectedImplementationRegistrations) - { - hashCode = (hashCode * 397) ^ reflectedImplementationRegistration.GetHashCode(); - } - - foreach (var preciseReflectedRegistration in PreciseReflectedRegistrations) - { - hashCode = (hashCode * 397) ^ preciseReflectedRegistration.GetHashCode(); - } - - return hashCode; - } - } - } - - private readonly record struct GenerationEnvironment( - bool GenerationEnabled, - bool SupportsReflectionFallbackAttribute); } diff --git a/GFramework.SourceGenerators.Tests/Rule/ContextAwareGeneratorSnapshotTests.cs b/GFramework.SourceGenerators.Tests/Rule/ContextAwareGeneratorSnapshotTests.cs index 6b84bb12..7a57c668 100644 --- a/GFramework.SourceGenerators.Tests/Rule/ContextAwareGeneratorSnapshotTests.cs +++ b/GFramework.SourceGenerators.Tests/Rule/ContextAwareGeneratorSnapshotTests.cs @@ -89,6 +89,80 @@ public class ContextAwareGeneratorSnapshotTests GetSnapshotFolder()); } + /// + /// 验证生成器在用户 partial 类型已经声明常见上下文字段名时仍能生成可编译代码。 + /// + /// 异步任务,无返回值。 + [Test] + public async Task Snapshot_ContextAwareGenerator_With_User_Field_Name_Collisions() + { + const string source = """ + using System; + + namespace GFramework.Core.SourceGenerators.Abstractions.Rule + { + [AttributeUsage(AttributeTargets.Class)] + public sealed class ContextAwareAttribute : Attribute { } + } + + namespace GFramework.Core.Abstractions.Rule + { + public interface IContextAware + { + void SetContext( + GFramework.Core.Abstractions.Architectures.IArchitectureContext context); + + GFramework.Core.Abstractions.Architectures.IArchitectureContext GetContext(); + } + } + + namespace GFramework.Core.Abstractions.Architectures + { + public interface IArchitectureContext { } + + public interface IArchitectureContextProvider + { + IArchitectureContext GetContext(); + bool TryGetContext(out T? context) where T : class, IArchitectureContext; + } + } + + namespace GFramework.Core.Architectures + { + using GFramework.Core.Abstractions.Architectures; + + public sealed class GameContextProvider : IArchitectureContextProvider + { + public IArchitectureContext GetContext() => null; + public bool TryGetContext(out T? context) where T : class, IArchitectureContext + { + context = null; + return false; + } + } + } + + namespace TestApp + { + using GFramework.Core.SourceGenerators.Abstractions.Rule; + using GFramework.Core.Abstractions.Rule; + using GFramework.Core.Abstractions.Architectures; + + [ContextAware] + public partial class CollisionProneRule : IContextAware + { + private readonly string _context = "user-field"; + private static readonly string _contextProvider = "user-provider"; + private static readonly object _contextSync = new(); + } + } + """; + + await GeneratorSnapshotTest.RunAsync( + source, + GetSnapshotFolder()); + } + /// /// 将运行时测试目录映射回仓库内已提交的上下文感知生成器快照目录。 /// diff --git a/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/CollisionProneRule.ContextAware.g.cs b/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/CollisionProneRule.ContextAware.g.cs new file mode 100644 index 00000000..99e6e492 --- /dev/null +++ b/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/CollisionProneRule.ContextAware.g.cs @@ -0,0 +1,103 @@ +// +#nullable enable + +namespace TestApp; + +/// +/// 为当前规则类型补充自动生成的架构上下文访问实现。 +/// +/// +/// 生成代码会在实例级缓存首次解析到的上下文,并在未显式配置提供者时回退到 。 +/// 同一生成类型的所有实例共享一个静态上下文提供者;切换或重置提供者只会影响尚未缓存上下文的新实例或未初始化实例, +/// 已缓存的实例上下文需要通过 显式覆盖。 +/// 与手动继承 的路径相比,生成实现会使用 _gFrameworkContextAwareSync 协调惰性初始化、provider 切换和显式上下文注入; +/// 则保持无锁的实例级缓存语义,更适合已经由调用方线程模型保证串行访问的简单场景。 +/// +partial class CollisionProneRule : global::GFramework.Core.Abstractions.Rule.IContextAware +{ + private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _gFrameworkContextAwareContext; + private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? _gFrameworkContextAwareProvider; + private static readonly object _gFrameworkContextAwareSync = new(); + + /// + /// 获取当前实例绑定的架构上下文。 + /// + /// + /// 该属性会先返回通过 IContextAware.SetContext(...) 显式注入的实例上下文;若尚未设置,则在同一个同步域内惰性初始化共享提供者。 + /// 当静态提供者尚未配置时,生成代码会回退到 。 + /// 一旦某个实例成功缓存上下文,后续 + /// 或 不会自动清除此缓存;如需覆盖,请显式调用 IContextAware.SetContext(...)。 + /// 当前实现还假设 可在持有 _gFrameworkContextAwareSync 时安全执行; + /// 自定义 provider 不应在该调用链内重新进入当前类型的 provider 配置 API,且应避免引入与外部全局锁相互等待的锁顺序。 + /// + protected global::GFramework.Core.Abstractions.Architectures.IArchitectureContext Context + { + get + { + var context = _gFrameworkContextAwareContext; + if (context is not null) + { + return context; + } + + // 在同一个同步域内协调懒加载与 provider 切换,避免读取到被并发重置的空提供者。 + // provider 的 GetContext() 会在持有 _gFrameworkContextAwareSync 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。 + lock (_gFrameworkContextAwareSync) + { + _gFrameworkContextAwareProvider ??= new global::GFramework.Core.Architectures.GameContextProvider(); + _gFrameworkContextAwareContext ??= _gFrameworkContextAwareProvider.GetContext(); + return _gFrameworkContextAwareContext; + } + } + } + + /// + /// 配置当前生成类型共享的上下文提供者。 + /// + /// 后续懒加载上下文时要使用的提供者实例。 + /// 为 null 时抛出。 + /// + /// 该方法使用与 相同的同步锁,避免提供者切换与惰性初始化交错。 + /// 已经缓存上下文的实例不会因为提供者切换而自动失效;该变更仅影响尚未初始化上下文的新实例或未缓存实例。 + /// 如需覆盖已有实例的上下文,请显式调用 IContextAware.SetContext(...)。 + /// + public static void SetContextProvider(global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider provider) + { + global::System.ArgumentNullException.ThrowIfNull(provider); + lock (_gFrameworkContextAwareSync) + { + _gFrameworkContextAwareProvider = provider; + } + } + + /// + /// 重置共享上下文提供者,使后续懒加载回退到默认提供者。 + /// + /// + /// 该方法主要用于测试清理或跨用例恢复默认行为。 + /// 它不会清除已经缓存到实例字段中的上下文;只有后续尚未初始化上下文的实例会重新回退到 。 + /// 如需覆盖已有实例的上下文,请显式调用 IContextAware.SetContext(...)。 + /// + public static void ResetContextProvider() + { + lock (_gFrameworkContextAwareSync) + { + _gFrameworkContextAwareProvider = null; + } + } + + void global::GFramework.Core.Abstractions.Rule.IContextAware.SetContext(global::GFramework.Core.Abstractions.Architectures.IArchitectureContext context) + { + // 与 Context getter 共享同一同步协议,避免显式注入被并发懒加载覆盖。 + lock (_gFrameworkContextAwareSync) + { + _gFrameworkContextAwareContext = context; + } + } + + global::GFramework.Core.Abstractions.Architectures.IArchitectureContext global::GFramework.Core.Abstractions.Rule.IContextAware.GetContext() + { + return Context; + } + +} diff --git a/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/MyRule.ContextAware.g.cs b/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/MyRule.ContextAware.g.cs index 63e5cb5c..0788b52b 100644 --- a/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/MyRule.ContextAware.g.cs +++ b/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/MyRule.ContextAware.g.cs @@ -10,14 +10,14 @@ namespace TestApp; /// 生成代码会在实例级缓存首次解析到的上下文,并在未显式配置提供者时回退到 。 /// 同一生成类型的所有实例共享一个静态上下文提供者;切换或重置提供者只会影响尚未缓存上下文的新实例或未初始化实例, /// 已缓存的实例上下文需要通过 显式覆盖。 -/// 与手动继承 的路径相比,生成实现会使用 _contextSync 协调惰性初始化、provider 切换和显式上下文注入; +/// 与手动继承 的路径相比,生成实现会使用 _gFrameworkContextAwareSync 协调惰性初始化、provider 切换和显式上下文注入; /// 则保持无锁的实例级缓存语义,更适合已经由调用方线程模型保证串行访问的简单场景。 /// partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware { - private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _context; - private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? _contextProvider; - private static readonly object _contextSync = new(); + private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _gFrameworkContextAwareContext; + private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? _gFrameworkContextAwareProvider; + private static readonly object _gFrameworkContextAwareSync = new(); /// /// 获取当前实例绑定的架构上下文。 @@ -27,26 +27,26 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware /// 当静态提供者尚未配置时,生成代码会回退到 。 /// 一旦某个实例成功缓存上下文,后续 /// 或 不会自动清除此缓存;如需覆盖,请显式调用 IContextAware.SetContext(...)。 - /// 当前实现还假设 可在持有 _contextSync 时安全执行; + /// 当前实现还假设 可在持有 _gFrameworkContextAwareSync 时安全执行; /// 自定义 provider 不应在该调用链内重新进入当前类型的 provider 配置 API,且应避免引入与外部全局锁相互等待的锁顺序。 /// protected global::GFramework.Core.Abstractions.Architectures.IArchitectureContext Context { get { - var context = _context; + var context = _gFrameworkContextAwareContext; if (context is not null) { return context; } // 在同一个同步域内协调懒加载与 provider 切换,避免读取到被并发重置的空提供者。 - // provider 的 GetContext() 会在持有 _contextSync 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。 - lock (_contextSync) + // provider 的 GetContext() 会在持有 _gFrameworkContextAwareSync 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。 + lock (_gFrameworkContextAwareSync) { - _contextProvider ??= new global::GFramework.Core.Architectures.GameContextProvider(); - _context ??= _contextProvider.GetContext(); - return _context; + _gFrameworkContextAwareProvider ??= new global::GFramework.Core.Architectures.GameContextProvider(); + _gFrameworkContextAwareContext ??= _gFrameworkContextAwareProvider.GetContext(); + return _gFrameworkContextAwareContext; } } } @@ -55,6 +55,7 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware /// 配置当前生成类型共享的上下文提供者。 /// /// 后续懒加载上下文时要使用的提供者实例。 + /// 为 null 时抛出。 /// /// 该方法使用与 相同的同步锁,避免提供者切换与惰性初始化交错。 /// 已经缓存上下文的实例不会因为提供者切换而自动失效;该变更仅影响尚未初始化上下文的新实例或未缓存实例。 @@ -62,9 +63,10 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware /// public static void SetContextProvider(global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider provider) { - lock (_contextSync) + global::System.ArgumentNullException.ThrowIfNull(provider); + lock (_gFrameworkContextAwareSync) { - _contextProvider = provider; + _gFrameworkContextAwareProvider = provider; } } @@ -78,18 +80,18 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware /// public static void ResetContextProvider() { - lock (_contextSync) + lock (_gFrameworkContextAwareSync) { - _contextProvider = null; + _gFrameworkContextAwareProvider = null; } } void global::GFramework.Core.Abstractions.Rule.IContextAware.SetContext(global::GFramework.Core.Abstractions.Architectures.IArchitectureContext context) { // 与 Context getter 共享同一同步协议,避免显式注入被并发懒加载覆盖。 - lock (_contextSync) + lock (_gFrameworkContextAwareSync) { - _context = context; + _gFrameworkContextAwareContext = context; } } 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 88a0b945..e0620bc3 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 @@ -7,8 +7,8 @@ ## 当前恢复点 -- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-020` -- 当前阶段:`Phase 20` +- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-021` +- 当前阶段:`Phase 21` - 当前焦点: - 已完成 `GFramework.Core` 当前 `MA0016` / `MA0002` / `MA0015` / `MA0077` 低风险收口批次 - 已复核 `net10.0` 下的 `MA0158` 基线:`GFramework.Core` / `GFramework.Cqrs` 当前共有 `16` 个 object lock @@ -19,6 +19,8 @@ `StringComparison.Ordinal` - 已完成 `SchemaConfigGenerator.cs` 的第一批 `MA0051` 结构拆分:schema 入口解析、属性解析、schema 遍历、数组属性解析、 约束文档生成与若干生成代码发射 helper 已拆出语义阶段 + - 已完成当前 PR #269 review follow-up:`CqrsHandlerRegistryGenerator` 按职责拆分为 partial 生成器文件, + `ContextAwareGenerator` 改用稳定前缀字段并补上 provider null 防御,`Option` 补齐 `` 契约说明 - `LoggingConfiguration`、`FilterConfiguration` 与 `CollectionExtensions` 已改用集合抽象接口,并保留内部具体集合默认值 - `CoroutineScheduler` 的 tag/group 字典已显式使用 `StringComparer.Ordinal`,保持既有区分大小写语义 - `EasyEvents.AddEvent()` 的重复注册路径已改为状态冲突异常,避免把泛型类型参数伪装成方法参数名 @@ -51,6 +53,8 @@ - 已完成当前 `GFramework.Core` `net8.0` 剩余低风险 analyzer warning 批次;warnings-only 基线已降到 `0` 条 - 已完成 `GFramework.Core.SourceGenerators` 中 `ContextAwareGenerator` 的剩余 `MA0051` 收口;warnings-only 基线已降到 `0` 条 - 已完成 `GFramework.Cqrs.SourceGenerators` 中 `CqrsHandlerRegistryGenerator` 的剩余 `MA0051` 收口;warnings-only 基线已降到 `0` 条 +- 已完成当前 PR #269 的 review follow-up:收口 `ContextAwareGenerator` 的字段命名冲突 / provider null 契约、 + `Option` 的 XML 文档缺口,以及 `CqrsHandlerRegistryGenerator` 的超大文件拆分 - 已完成 `GFramework.Game.SourceGenerators` 中 `SchemaConfigGenerator` 的第一批 `MA0051` 收口;warnings-only 基线剩余 `9` 条 `MA0051` @@ -93,6 +97,9 @@ 通过 schema 类型比较 helper 与显式 `StringComparison.Ordinal` 清空当前项目的 `MA0006` - `RP-020` 继续拆分 `SchemaConfigGenerator.cs` 的 `MA0051` 热点,将当前项目 warnings-only 基线从 `19` 条降到 `9` 条, 并用 focused schema generator tests 验证 50 个用例通过 +- `RP-021` 使用 `$gframework-pr-review` 复核当前分支 PR #269 后,修复仍在本地成立的 4 个项:将 + `CqrsHandlerRegistryGenerator` 拆分为职责清晰的 partial 文件、为 `ContextAwareGenerator` 生成字段增加稳定前缀并补上 + `SetContextProvider` 的运行时 null 校验、为 `Option` 补齐 ``,并新增字段重名场景的生成器快照测试 - 当前工作树分支 `fix/analyzer-warning-reduction-batch` 已在 `ai-plan/public/README.md` 建立 topic 映射 ## 当前风险 @@ -226,6 +233,15 @@ - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~SchemaConfigGenerator -m:1 -p:RestoreFallbackFolders= -nologo` - 结果:`50 Passed`,`0 Failed` - 说明:测试项目构建仍显示既有 source generator test analyzer warning;不属于本轮写集 +- `RP-021` 的验证结果: + - `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"` + - 结果:`0 Warning(s)`,`0 Error(s)`;拆分后最大单文件已降到 `851` 行,满足仓库 800-1000 行上限 + - `dotnet build GFramework.Core.SourceGenerators/GFramework.Core.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"` + - 结果:`0 Warning(s)`,`0 Error(s)`;`ContextAwareGenerator` 的字段命名与 provider 契约修复未引入新的 generator warning + - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~ContextAwareGeneratorSnapshotTests|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests" -m:1 -p:RestoreFallbackFolders= -nologo` + - 结果:先并行运行两条 `dotnet test` 时触发共享输出文件锁冲突;改为串行重跑后 `ContextAwareGeneratorSnapshotTests=2 Passed`、 + `CqrsHandlerRegistryGeneratorTests=14 Passed` + - 说明:失败来自测试宿主并行写入同一 build 输出,不是代码回归;串行重跑后快照新增的字段重名场景和 CQRS 快照均通过 - active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史 ## 下一步 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 f43089c0..0fa90aff 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,40 @@ # Analyzer Warning Reduction 追踪 +## 2026-04-22 — RP-021 + +### 阶段:PR #269 review follow-up 收口(RP-021) + +- 启动复核: + - 使用 `$gframework-pr-review` 读取当前分支 PR #269 的 CodeRabbit outside-diff 与 nitpick 汇总 + - 本地复核后确认仍成立的 4 个项分别是:`CqrsHandlerRegistryGenerator.cs` 超过仓库文件大小上限、 + `ContextAwareGenerator` 生成字段名可能与用户 partial 类型冲突、`SetContextProvider` 缺少运行时 null 防御、 + `Option` 缺少 `` 契约说明 +- 决策: + - `CqrsHandlerRegistryGenerator` 继续采用既有 partial helper 风格,按“主流程 / 运行时类型引用 / 源码发射 / 模型”四个文件拆分, + 保持生成顺序、日志文本、fallback 契约和快照输出不变 + - `ContextAwareGenerator` 只收口仍成立的 review 项,不引入未被本地证实的 `Volatile.Read/Write` 变更 + - 为字段命名冲突新增生成器快照场景,避免后续回退到 `_context` / `_contextProvider` / `_contextSync` +- 实施调整: + - 将 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs` 拆为 4 个 partial 文件,分别承载主生成管线、 + runtime type reference 构造、source emission helper 与嵌套 specs/models + - 将 `ContextAwareGenerator` 生成字段统一改为 `_gFrameworkContextAware*` 前缀,同步更新 XML 文档、注释和显式接口实现 + - 为 `SetContextProvider(...)` 增加 `ArgumentNullException.ThrowIfNull(provider)` 与 XML `` 说明 + - 为 `Option` 补充 ``,明确 `Some/None`、`null` 约束、不可变语义与推荐使用方式 + - 新增 `CollisionProneRule.ContextAware.g.cs` 快照,覆盖用户字段名与生成字段名冲突场景 +- 验证结果: + - `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"` + - 结果:`0 Warning(s)`,`0 Error(s)`;拆分后 `CqrsHandlerRegistryGenerator` 最大单文件为 `851` 行 + - `dotnet build GFramework.Core.SourceGenerators/GFramework.Core.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"` + - 结果:`0 Warning(s)`,`0 Error(s)` + - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~CqrsHandlerRegistryGeneratorTests -m:1 -p:RestoreFallbackFolders= -nologo` + - 结果:`14 Passed`,`0 Failed` + - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~ContextAwareGeneratorSnapshotTests -m:1 -p:RestoreFallbackFolders= -nologo` + - 结果:`2 Passed`,`0 Failed` + - 说明:最初并行跑两个 `dotnet test` 命令时触发共享输出文件锁冲突;串行重跑后确认是测试宿主环境噪音而非代码回归 +- 下一步建议: + - 若本轮验证通过,可继续回到 `SchemaConfigGenerator` 剩余 `MA0051` + - 若 review 再次聚焦 `ContextAwareGenerator` 并发可见性问题,需要先补最小复现测试,再决定是否引入 `Volatile` 语义 + ## 2026-04-22 — RP-020 ### 阶段:`SchemaConfigGenerator` 第一批 `MA0051` 结构拆分(RP-020) From 12f15961afca10ce98c23b71754599ec2bcab281 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 22 Apr 2026 13:34:24 +0800 Subject: [PATCH 07/13] =?UTF-8?q?fix(pr269):=20=E6=94=B6=E5=8F=A3=E8=AF=84?= =?UTF-8?q?=E5=AE=A1=E5=85=BC=E5=AE=B9=E6=80=A7=E4=B8=8E=E7=94=9F=E6=88=90?= =?UTF-8?q?=E5=99=A8=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 恢复 EasyEvents、CollectionExtensions 与 logging 配置模型的公共 API 兼容形状 - 修复 ContextAwareGenerator 字段命名冲突、锁内读取路径与相关快照回归测试 - 更新 Cqrs 与 schema generator 的 null/cancellation 契约,并同步 ai-plan 跟踪与验证记录 --- .../Rule/ContextAwareGenerator.cs | 130 +++++++++++++----- .../Events/EasyEventsTests.cs | 4 +- .../Extensions/CollectionExtensionsTests.cs | 16 ++- .../Logging/LoggingConfigurationTests.cs | 29 ++++ GFramework.Core/Events/EasyEvents.cs | 6 +- .../Extensions/CollectionExtensions.cs | 4 +- .../Logging/FilterConfiguration.cs | 8 +- .../Logging/LoggingConfiguration.cs | 8 +- ...RegistryGenerator.RuntimeTypeReferences.cs | 36 ++++- ...HandlerRegistryGenerator.SourceEmission.cs | 89 ++++++------ .../Config/SchemaConfigGenerator.cs | 4 + .../Config/SchemaConfigGeneratorTests.cs | 50 +++++++ .../Config/SchemaGeneratorTestDriver.cs | 23 +++- .../ContextAwareGeneratorSnapshotTests.cs | 3 + .../CollisionProneRule.ContextAware.g.cs | 38 +++-- .../MyRule.ContextAware.g.cs | 6 - .../analyzer-warning-reduction-tracking.md | 34 +++-- .../analyzer-warning-reduction-trace.md | 36 +++++ 18 files changed, 382 insertions(+), 142 deletions(-) diff --git a/GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs b/GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs index 65919b25..fff89667 100644 --- a/GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs +++ b/GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs @@ -96,6 +96,7 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase var interfaceName = iContextAware.ToDisplayString( SymbolDisplayFormat.FullyQualifiedFormat); + var memberNames = CreateGeneratedContextMemberNames(symbol); sb.AppendLine("/// "); sb.AppendLine("/// 为当前规则类型补充自动生成的架构上下文访问实现。"); sb.AppendLine("/// "); @@ -107,15 +108,15 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase sb.AppendLine( "/// 已缓存的实例上下文需要通过 显式覆盖。"); sb.AppendLine( - "/// 与手动继承 的路径相比,生成实现会使用 _gFrameworkContextAwareSync 协调惰性初始化、provider 切换和显式上下文注入;"); + $"/// 与手动继承 的路径相比,生成实现会使用 {memberNames.SyncFieldName} 协调惰性初始化、provider 切换和显式上下文注入;"); sb.AppendLine( "/// 则保持无锁的实例级缓存语义,更适合已经由调用方线程模型保证串行访问的简单场景。"); sb.AppendLine("/// "); sb.AppendLine($"partial class {symbol.Name} : {interfaceName}"); sb.AppendLine("{"); - GenerateContextProperty(sb); - GenerateInterfaceImplementations(sb, iContextAware); + GenerateContextProperty(sb, memberNames); + GenerateInterfaceImplementations(sb, iContextAware, memberNames); sb.AppendLine("}"); return sb.ToString().TrimEnd(); @@ -138,24 +139,29 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase /// 生成Context属性 /// /// 字符串构建器 - private static void GenerateContextProperty(StringBuilder sb) + /// 当前目标类型应使用的上下文字段名。 + private static void GenerateContextProperty( + StringBuilder sb, + GeneratedContextMemberNames memberNames) { - GenerateContextBackingFields(sb); - GenerateContextGetter(sb); - GenerateContextProviderConfiguration(sb); + GenerateContextBackingFields(sb, memberNames); + GenerateContextGetter(sb, memberNames); + GenerateContextProviderConfiguration(sb, memberNames); } /// /// 生成上下文缓存和同步所需的字段。 /// /// 字符串构建器。 - private static void GenerateContextBackingFields(StringBuilder sb) + private static void GenerateContextBackingFields( + StringBuilder sb, + GeneratedContextMemberNames memberNames) { sb.AppendLine( - " private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _gFrameworkContextAwareContext;"); + $" private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? {memberNames.ContextFieldName};"); sb.AppendLine( - " private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? _gFrameworkContextAwareProvider;"); - sb.AppendLine(" private static readonly object _gFrameworkContextAwareSync = new();"); + $" private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? {memberNames.ProviderFieldName};"); + sb.AppendLine($" private static readonly object {memberNames.SyncFieldName} = new();"); sb.AppendLine(); } @@ -163,7 +169,9 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase /// 生成实例上下文访问器,包含显式注入优先和 provider 惰性回退语义。 /// /// 字符串构建器。 - private static void GenerateContextGetter(StringBuilder sb) + private static void GenerateContextGetter( + StringBuilder sb, + GeneratedContextMemberNames memberNames) { sb.AppendLine(" /// "); sb.AppendLine(" /// 获取当前实例绑定的架构上下文。"); @@ -178,7 +186,7 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase sb.AppendLine( " /// 或 不会自动清除此缓存;如需覆盖,请显式调用 IContextAware.SetContext(...)。"); sb.AppendLine( - " /// 当前实现还假设 可在持有 _gFrameworkContextAwareSync 时安全执行;"); + $" /// 当前实现还假设 可在持有 {memberNames.SyncFieldName} 时安全执行;"); sb.AppendLine( " /// 自定义 provider 不应在该调用链内重新进入当前类型的 provider 配置 API,且应避免引入与外部全局锁相互等待的锁顺序。"); sb.AppendLine(" /// "); @@ -186,21 +194,15 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase sb.AppendLine(" {"); sb.AppendLine(" get"); sb.AppendLine(" {"); - sb.AppendLine(" var context = _gFrameworkContextAwareContext;"); - sb.AppendLine(" if (context is not null)"); - sb.AppendLine(" {"); - sb.AppendLine(" return context;"); - sb.AppendLine(" }"); - sb.AppendLine(); sb.AppendLine(" // 在同一个同步域内协调懒加载与 provider 切换,避免读取到被并发重置的空提供者。"); sb.AppendLine( - " // provider 的 GetContext() 会在持有 _gFrameworkContextAwareSync 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。"); - sb.AppendLine(" lock (_gFrameworkContextAwareSync)"); + $" // provider 的 GetContext() 会在持有 {memberNames.SyncFieldName} 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。"); + sb.AppendLine($" lock ({memberNames.SyncFieldName})"); sb.AppendLine(" {"); sb.AppendLine( - " _gFrameworkContextAwareProvider ??= new global::GFramework.Core.Architectures.GameContextProvider();"); - sb.AppendLine(" _gFrameworkContextAwareContext ??= _gFrameworkContextAwareProvider.GetContext();"); - sb.AppendLine(" return _gFrameworkContextAwareContext;"); + $" {memberNames.ProviderFieldName} ??= new global::GFramework.Core.Architectures.GameContextProvider();"); + sb.AppendLine($" {memberNames.ContextFieldName} ??= {memberNames.ProviderFieldName}.GetContext();"); + sb.AppendLine($" return {memberNames.ContextFieldName};"); sb.AppendLine(" }"); sb.AppendLine(" }"); sb.AppendLine(" }"); @@ -211,7 +213,9 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase /// 生成静态 provider 配置 API,供测试和宿主在懒加载前替换默认上下文来源。 /// /// 字符串构建器。 - private static void GenerateContextProviderConfiguration(StringBuilder sb) + private static void GenerateContextProviderConfiguration( + StringBuilder sb, + GeneratedContextMemberNames memberNames) { sb.AppendLine(" /// "); sb.AppendLine(" /// 配置当前生成类型共享的上下文提供者。"); @@ -229,9 +233,9 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase " public static void SetContextProvider(global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider provider)"); sb.AppendLine(" {"); sb.AppendLine(" global::System.ArgumentNullException.ThrowIfNull(provider);"); - sb.AppendLine(" lock (_gFrameworkContextAwareSync)"); + sb.AppendLine($" lock ({memberNames.SyncFieldName})"); sb.AppendLine(" {"); - sb.AppendLine(" _gFrameworkContextAwareProvider = provider;"); + sb.AppendLine($" {memberNames.ProviderFieldName} = provider;"); sb.AppendLine(" }"); sb.AppendLine(" }"); sb.AppendLine(); @@ -246,9 +250,9 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase sb.AppendLine(" /// "); sb.AppendLine(" public static void ResetContextProvider()"); sb.AppendLine(" {"); - sb.AppendLine(" lock (_gFrameworkContextAwareSync)"); + sb.AppendLine($" lock ({memberNames.SyncFieldName})"); sb.AppendLine(" {"); - sb.AppendLine(" _gFrameworkContextAwareProvider = null;"); + sb.AppendLine($" {memberNames.ProviderFieldName} = null;"); sb.AppendLine(" }"); sb.AppendLine(" }"); sb.AppendLine(); @@ -265,7 +269,8 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase /// 接口符号 private static void GenerateInterfaceImplementations( StringBuilder sb, - INamedTypeSymbol interfaceSymbol) + INamedTypeSymbol interfaceSymbol, + GeneratedContextMemberNames memberNames) { var interfaceName = interfaceSymbol.ToDisplayString( SymbolDisplayFormat.FullyQualifiedFormat); @@ -275,7 +280,7 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase if (method.MethodKind != MethodKind.Ordinary) continue; - GenerateMethod(sb, interfaceName, method); + GenerateMethod(sb, interfaceName, method, memberNames); sb.AppendLine(); } } @@ -289,7 +294,8 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase private static void GenerateMethod( StringBuilder sb, string interfaceName, - IMethodSymbol method) + IMethodSymbol method, + GeneratedContextMemberNames memberNames) { var returnType = method.ReturnType.ToDisplayString( SymbolDisplayFormat.FullyQualifiedFormat); @@ -302,7 +308,7 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase $" {returnType} {interfaceName}.{method.Name}({parameters})"); sb.AppendLine(" {"); - GenerateMethodBody(sb, method); + GenerateMethodBody(sb, method, memberNames); sb.AppendLine(" }"); } @@ -314,15 +320,16 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase /// 方法符号 private static void GenerateMethodBody( StringBuilder sb, - IMethodSymbol method) + IMethodSymbol method, + GeneratedContextMemberNames memberNames) { switch (method.Name) { case "SetContext": sb.AppendLine(" // 与 Context getter 共享同一同步协议,避免显式注入被并发懒加载覆盖。"); - sb.AppendLine(" lock (_gFrameworkContextAwareSync)"); + sb.AppendLine($" lock ({memberNames.SyncFieldName})"); sb.AppendLine(" {"); - sb.AppendLine(" _gFrameworkContextAwareContext = context;"); + sb.AppendLine($" {memberNames.ContextFieldName} = context;"); sb.AppendLine(" }"); break; @@ -338,4 +345,55 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase break; } } + + /// + /// 为生成字段选择不会与目标类型现有成员冲突的稳定名称。 + /// + /// 当前需要补充 ContextAware 实现的目标类型。 + /// 当前生成轮次应使用的上下文字段名集合。 + private static GeneratedContextMemberNames CreateGeneratedContextMemberNames(INamedTypeSymbol symbol) + { + var reservedNames = new HashSet( + symbol.GetMembers() + .Where(static member => !member.IsImplicitlyDeclared) + .Select(static member => member.Name), + StringComparer.Ordinal); + + return new GeneratedContextMemberNames( + AllocateGeneratedMemberName(reservedNames, "_gFrameworkContextAwareContext"), + AllocateGeneratedMemberName(reservedNames, "_gFrameworkContextAwareProvider"), + AllocateGeneratedMemberName(reservedNames, "_gFrameworkContextAwareSync")); + } + + /// + /// 在固定前缀基础上按顺序追加数字后缀,直到找到可安全写入的成员名。 + /// + /// 当前类型已占用或已为其他生成字段保留的名称集合。 + /// 优先尝试的基础名称。 + /// 本轮生成可以使用的唯一成员名。 + private static string AllocateGeneratedMemberName( + ISet reservedNames, + string baseName) + { + if (reservedNames.Add(baseName)) + return baseName; + + for (var suffix = 1; ; suffix++) + { + var candidateName = $"{baseName}{suffix}"; + if (reservedNames.Add(candidateName)) + return candidateName; + } + } + + /// + /// 描述一次 ContextAware 代码生成中选定的上下文字段名。 + /// + /// 实例上下文缓存字段名。 + /// 共享上下文提供者字段名。 + /// 用于串行化访问的同步字段名。 + private readonly record struct GeneratedContextMemberNames( + string ContextFieldName, + string ProviderFieldName, + string SyncFieldName); } diff --git a/GFramework.Core.Tests/Events/EasyEventsTests.cs b/GFramework.Core.Tests/Events/EasyEventsTests.cs index 2b83ff08..ccec6b72 100644 --- a/GFramework.Core.Tests/Events/EasyEventsTests.cs +++ b/GFramework.Core.Tests/Events/EasyEventsTests.cs @@ -99,14 +99,14 @@ public class EasyEventsTests } /// - /// 测试 AddEvent 对重复事件类型给出状态冲突异常。 + /// 测试 AddEvent 对重复事件类型保持兼容的参数异常类型。 /// [Test] public void AddEvent_Should_Throw_When_Already_Registered() { _easyEvents.AddEvent>(); - Assert.Throws(() => _easyEvents.AddEvent>()); + Assert.Throws(() => _easyEvents.AddEvent>()); } /// diff --git a/GFramework.Core.Tests/Extensions/CollectionExtensionsTests.cs b/GFramework.Core.Tests/Extensions/CollectionExtensionsTests.cs index 0635b5e3..9bbc1c4c 100644 --- a/GFramework.Core.Tests/Extensions/CollectionExtensionsTests.cs +++ b/GFramework.Core.Tests/Extensions/CollectionExtensionsTests.cs @@ -165,6 +165,20 @@ public class CollectionExtensionsTests Assert.That(result["c"], Is.EqualTo(3)); } + /// + /// 测试ToDictionarySafe保持具体Dictionary返回类型,避免公开API继续收窄。 + /// + [Test] + public void ToDictionarySafe_Should_Preserve_Concrete_Return_Type() + { + var method = typeof(GFramework.Core.Extensions.CollectionExtensions) + .GetMethods() + .Single(static method => method.Name == nameof(GFramework.Core.Extensions.CollectionExtensions.ToDictionarySafe)); + + Assert.That(method.ReturnType.IsGenericType, Is.True); + Assert.That(method.ReturnType.GetGenericTypeDefinition(), Is.EqualTo(typeof(Dictionary<,>))); + } + /// /// 测试ToDictionarySafe方法在存在重复键时覆盖前面的值 /// @@ -224,4 +238,4 @@ public class CollectionExtensionsTests Assert.Throws(() => items.ToDictionarySafe<(string, int), string, int>(x => x.Item1, null!)); } -} \ No newline at end of file +} diff --git a/GFramework.Core.Tests/Logging/LoggingConfigurationTests.cs b/GFramework.Core.Tests/Logging/LoggingConfigurationTests.cs index edae0876..3f88b4f5 100644 --- a/GFramework.Core.Tests/Logging/LoggingConfigurationTests.cs +++ b/GFramework.Core.Tests/Logging/LoggingConfigurationTests.cs @@ -39,6 +39,35 @@ public class LoggingConfigurationTests Assert.That(config.LoggerLevels["GFramework.Core"], Is.EqualTo(LogLevel.Trace)); } + [Test] + public void Configuration_Collections_Should_Preserve_Public_Concrete_Types() + { + Assert.Multiple(() => + { + Assert.That( + typeof(LoggingConfiguration).GetProperty(nameof(LoggingConfiguration.Appenders))!.PropertyType, + Is.EqualTo(typeof(List))); + Assert.That( + typeof(LoggingConfiguration).GetProperty(nameof(LoggingConfiguration.LoggerLevels))!.PropertyType, + Is.EqualTo(typeof(Dictionary))); + Assert.That( + typeof(FilterConfiguration).GetProperty(nameof(FilterConfiguration.Namespaces))!.PropertyType, + Is.EqualTo(typeof(List))); + Assert.That( + typeof(FilterConfiguration).GetProperty(nameof(FilterConfiguration.Filters))!.PropertyType, + Is.EqualTo(typeof(List))); + }); + } + + [Test] + public void LoggerLevels_Should_Remain_Case_Sensitive_By_Default() + { + var config = new LoggingConfiguration(); + config.LoggerLevels["GFramework.Core"] = LogLevel.Info; + + Assert.That(config.LoggerLevels.ContainsKey("gframework.core"), Is.False); + } + [Test] public void LoadFromJsonString_WithInvalidJson_ShouldThrow() { diff --git a/GFramework.Core/Events/EasyEvents.cs b/GFramework.Core/Events/EasyEvents.cs index adf42898..d21581c1 100644 --- a/GFramework.Core/Events/EasyEvents.cs +++ b/GFramework.Core/Events/EasyEvents.cs @@ -53,12 +53,14 @@ public class EasyEvents /// 添加指定类型的事件到事件字典中 /// /// 事件类型,必须实现IEasyEvent接口且具有无参构造函数 - /// 当事件类型已存在时抛出。 + /// 当事件类型已存在时抛出。 public void AddEvent() where T : IEvent, new() { if (!_mTypeEvents.TryAdd(typeof(T), new T())) { - throw new InvalidOperationException($"Event type {typeof(T).Name} already registered."); +#pragma warning disable MA0015 // Preserve the public ArgumentException contract without inventing a fake parameter name. + throw new ArgumentException($"Event type {typeof(T).Name} already registered."); +#pragma warning restore MA0015 } } diff --git a/GFramework.Core/Extensions/CollectionExtensions.cs b/GFramework.Core/Extensions/CollectionExtensions.cs index 19d02671..8a57ce46 100644 --- a/GFramework.Core/Extensions/CollectionExtensions.cs +++ b/GFramework.Core/Extensions/CollectionExtensions.cs @@ -81,10 +81,12 @@ public static class CollectionExtensions /// // dict["a"] == 3 (最后一个值) /// /// - public static IDictionary ToDictionarySafe( +#pragma warning disable MA0016 // Preserve the established concrete return type for public API compatibility. + public static Dictionary ToDictionarySafe( this IEnumerable source, Func keySelector, Func valueSelector) where TKey : notnull +#pragma warning restore MA0016 { ArgumentNullException.ThrowIfNull(source); ArgumentNullException.ThrowIfNull(keySelector); diff --git a/GFramework.Core/Logging/FilterConfiguration.cs b/GFramework.Core/Logging/FilterConfiguration.cs index 74651da0..0dc02bec 100644 --- a/GFramework.Core/Logging/FilterConfiguration.cs +++ b/GFramework.Core/Logging/FilterConfiguration.cs @@ -20,10 +20,14 @@ public sealed class FilterConfiguration /// /// 命名空间前缀列表(用于 Namespace 过滤器)。 /// - public IList? Namespaces { get; set; } +#pragma warning disable MA0016 // Preserve the established concrete configuration API surface. + public List? Namespaces { get; set; } +#pragma warning restore MA0016 /// /// 子过滤器列表(用于 Composite 过滤器)。 /// - public IList? Filters { get; set; } +#pragma warning disable MA0016 // Preserve the established concrete configuration API surface. + public List? Filters { get; set; } +#pragma warning restore MA0016 } diff --git a/GFramework.Core/Logging/LoggingConfiguration.cs b/GFramework.Core/Logging/LoggingConfiguration.cs index 0feedf6f..6bb164f7 100644 --- a/GFramework.Core/Logging/LoggingConfiguration.cs +++ b/GFramework.Core/Logging/LoggingConfiguration.cs @@ -15,11 +15,15 @@ public sealed class LoggingConfiguration /// /// Appender 配置列表 /// - public IList Appenders { get; set; } = new List(); +#pragma warning disable MA0016 // Preserve the established concrete configuration API surface. + public List Appenders { get; set; } = new(); +#pragma warning restore MA0016 /// /// 特定 Logger 的日志级别配置 /// - public IDictionary LoggerLevels { get; set; } = +#pragma warning disable MA0016 // Preserve the established concrete configuration API surface. + public Dictionary LoggerLevels { get; set; } = new Dictionary(StringComparer.Ordinal); +#pragma warning restore MA0016 } diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.RuntimeTypeReferences.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.RuntimeTypeReferences.cs index fcffb3e1..a10a6a9b 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.RuntimeTypeReferences.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.RuntimeTypeReferences.cs @@ -105,9 +105,10 @@ public sealed partial class CqrsHandlerRegistryGenerator out runtimeTypeReference); } - if (type is INamedTypeSymbol namedType) + if (type is INamedTypeSymbol namedType && + TryCreateNamedRuntimeTypeReference(compilation, namedType, out var namedTypeReference)) { - runtimeTypeReference = CreateNamedRuntimeTypeReference(compilation, namedType); + runtimeTypeReference = namedTypeReference; return true; } @@ -162,17 +163,32 @@ public sealed partial class CqrsHandlerRegistryGenerator /// /// 当前生成轮次的编译上下文。 /// 需要在运行时解析的命名类型。 - /// 适合写入生成注册器的命名类型运行时引用。 - private static RuntimeTypeReferenceSpec CreateNamedRuntimeTypeReference( + /// + /// 当方法返回 时,包含适合写入生成注册器的命名类型运行时引用; + /// 当返回 时,调用方应回退到更保守的注册路径。 + /// + /// 当命名类型可安全编码为运行时引用时返回 + private static bool TryCreateNamedRuntimeTypeReference( Compilation compilation, - INamedTypeSymbol namedType) + INamedTypeSymbol namedType, + out RuntimeTypeReferenceSpec? runtimeTypeReference) { if (SymbolEqualityComparer.Default.Equals(namedType.ContainingAssembly, compilation.Assembly)) - return RuntimeTypeReferenceSpec.FromReflectionLookup(GetReflectionTypeMetadataName(namedType)); + { + runtimeTypeReference = RuntimeTypeReferenceSpec.FromReflectionLookup(GetReflectionTypeMetadataName(namedType)); + return true; + } - return RuntimeTypeReferenceSpec.FromExternalReflectionLookup( + if (namedType.ContainingAssembly is null) + { + runtimeTypeReference = null; + return false; + } + + runtimeTypeReference = RuntimeTypeReferenceSpec.FromExternalReflectionLookup( namedType.ContainingAssembly.Identity.ToString(), GetReflectionTypeMetadataName(namedType)); + return true; } /// @@ -214,6 +230,12 @@ public sealed partial class CqrsHandlerRegistryGenerator return true; } + if (genericTypeDefinition.ContainingAssembly is null) + { + genericTypeDefinitionReference = null; + return false; + } + genericTypeDefinitionReference = RuntimeTypeReferenceSpec.FromExternalReflectionLookup( genericTypeDefinition.ContainingAssembly.Identity.ToString(), GetReflectionTypeMetadataName(genericTypeDefinition)); diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs index d8768cd7..5ebf97e7 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs @@ -143,7 +143,7 @@ public sealed partial class CqrsHandlerRegistryGenerator if (sourceShape.HasExternalAssemblyTypeLookups) { builder.AppendLine(); - AppendReflectionHelpers(builder, sourceShape.HasExternalAssemblyTypeLookups); + AppendReflectionHelpers(builder); } builder.AppendLine("}"); @@ -792,53 +792,48 @@ public sealed partial class CqrsHandlerRegistryGenerator return variableBaseName; } - private static void AppendReflectionHelpers( - StringBuilder builder, - bool includeExternalAssemblyTypeLookupHelpers) + private static void AppendReflectionHelpers(StringBuilder builder) { - if (includeExternalAssemblyTypeLookupHelpers) - { - builder.AppendLine( - " private static global::System.Type? ResolveReferencedAssemblyType(string assemblyIdentity, string typeMetadataName)"); - builder.AppendLine(" {"); - builder.AppendLine(" var assembly = ResolveReferencedAssembly(assemblyIdentity);"); - builder.AppendLine( - " return assembly?.GetType(typeMetadataName, throwOnError: false, ignoreCase: false);"); - builder.AppendLine(" }"); - builder.AppendLine(); - builder.AppendLine( - " private static global::System.Reflection.Assembly? ResolveReferencedAssembly(string assemblyIdentity)"); - builder.AppendLine(" {"); - builder.AppendLine(" global::System.Reflection.AssemblyName targetAssemblyName;"); - builder.AppendLine(" try"); - builder.AppendLine(" {"); - builder.AppendLine( - " targetAssemblyName = new global::System.Reflection.AssemblyName(assemblyIdentity);"); - builder.AppendLine(" }"); - builder.AppendLine(" catch"); - builder.AppendLine(" {"); - builder.AppendLine(" return null;"); - builder.AppendLine(" }"); - builder.AppendLine(); - builder.AppendLine( - " foreach (var assembly in global::System.AppDomain.CurrentDomain.GetAssemblies())"); - builder.AppendLine(" {"); - builder.AppendLine( - " if (global::System.Reflection.AssemblyName.ReferenceMatchesDefinition(targetAssemblyName, assembly.GetName()))"); - builder.AppendLine(" return assembly;"); - builder.AppendLine(" }"); - builder.AppendLine(); - builder.AppendLine(" try"); - builder.AppendLine(" {"); - builder.AppendLine( - " return global::System.Reflection.Assembly.Load(targetAssemblyName);"); - builder.AppendLine(" }"); - builder.AppendLine(" catch"); - builder.AppendLine(" {"); - builder.AppendLine(" return null;"); - builder.AppendLine(" }"); - builder.AppendLine(" }"); - } + builder.AppendLine( + " private static global::System.Type? ResolveReferencedAssemblyType(string assemblyIdentity, string typeMetadataName)"); + builder.AppendLine(" {"); + builder.AppendLine(" var assembly = ResolveReferencedAssembly(assemblyIdentity);"); + builder.AppendLine( + " return assembly?.GetType(typeMetadataName, throwOnError: false, ignoreCase: false);"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine( + " private static global::System.Reflection.Assembly? ResolveReferencedAssembly(string assemblyIdentity)"); + builder.AppendLine(" {"); + builder.AppendLine(" global::System.Reflection.AssemblyName targetAssemblyName;"); + builder.AppendLine(" try"); + builder.AppendLine(" {"); + builder.AppendLine( + " targetAssemblyName = new global::System.Reflection.AssemblyName(assemblyIdentity);"); + builder.AppendLine(" }"); + builder.AppendLine(" catch"); + builder.AppendLine(" {"); + builder.AppendLine(" return null;"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine( + " foreach (var assembly in global::System.AppDomain.CurrentDomain.GetAssemblies())"); + builder.AppendLine(" {"); + builder.AppendLine( + " if (global::System.Reflection.AssemblyName.ReferenceMatchesDefinition(targetAssemblyName, assembly.GetName()))"); + builder.AppendLine(" return assembly;"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" try"); + builder.AppendLine(" {"); + builder.AppendLine( + " return global::System.Reflection.Assembly.Load(targetAssemblyName);"); + builder.AppendLine(" }"); + builder.AppendLine(" catch"); + builder.AppendLine(" {"); + builder.AppendLine(" return null;"); + builder.AppendLine(" }"); + builder.AppendLine(" }"); } private static string EscapeStringLiteral(string value) diff --git a/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs b/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs index d0926c57..c8499bbe 100644 --- a/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs +++ b/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs @@ -134,6 +134,10 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator { text = file.GetText(cancellationToken); } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } catch (Exception exception) { text = null; diff --git a/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs index 2da0ffa9..4ef4bbad 100644 --- a/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs @@ -6,6 +6,32 @@ namespace GFramework.SourceGenerators.Tests.Config; [TestFixture] public class SchemaConfigGeneratorTests { + /// + /// 验证 AdditionalFiles 读取被取消时会向上传播取消,而不是伪造成 schema 诊断。 + /// + [Test] + public void Run_Should_Propagate_Cancellation_When_AdditionalText_Read_Is_Cancelled() + { + var method = typeof(global::GFramework.Game.SourceGenerators.Config.SchemaConfigGenerator) + .GetMethod( + "TryReadSchemaText", + global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static); + var cancellationTokenSource = new CancellationTokenSource(); + cancellationTokenSource.Cancel(); + var invocationArguments = new object?[] + { + new ThrowingAdditionalText("monster.schema.json"), + cancellationTokenSource.Token, + null, + null + }; + + var exception = Assert.Throws(() => + method!.Invoke(null, invocationArguments)); + + Assert.That(exception!.InnerException, Is.TypeOf()); + } + /// /// 验证缺失必填 id 字段时会产生命名明确的诊断。 /// @@ -46,6 +72,30 @@ public class SchemaConfigGeneratorTests }); } + /// + /// 用于模拟 AdditionalFiles 读取阶段直接收到取消请求的测试桩。 + /// + private sealed class ThrowingAdditionalText : AdditionalText + { + /// + /// 创建一个在读取时抛出取消异常的 AdditionalText。 + /// + /// 虚拟 schema 路径。 + public ThrowingAdditionalText(string path) + { + Path = path; + } + + /// + public override string Path { get; } + + /// + public override SourceText GetText(CancellationToken cancellationToken = default) + { + throw new OperationCanceledException(cancellationToken); + } + } + /// /// 验证空字符串 const 不会在生成 XML 文档时被当成“缺失约束”跳过。 /// diff --git a/GFramework.SourceGenerators.Tests/Config/SchemaGeneratorTestDriver.cs b/GFramework.SourceGenerators.Tests/Config/SchemaGeneratorTestDriver.cs index c8f5a4e4..0909d72e 100644 --- a/GFramework.SourceGenerators.Tests/Config/SchemaGeneratorTestDriver.cs +++ b/GFramework.SourceGenerators.Tests/Config/SchemaGeneratorTestDriver.cs @@ -20,6 +20,23 @@ public static class SchemaGeneratorTestDriver public static GeneratorDriverRunResult Run( string source, params (string path, string content)[] additionalFiles) + { + return Run( + source, + additionalFiles + .Select(static item => (AdditionalText)new InMemoryAdditionalText(item.path, item.content)) + .ToArray()); + } + + /// + /// 运行 schema 配置生成器,并允许测试自定义 AdditionalText 行为。 + /// + /// 测试用源码。 + /// 自定义 AdditionalText 集合。 + /// 生成器运行结果。 + public static GeneratorDriverRunResult Run( + string source, + params AdditionalText[] additionalTexts) { var syntaxTree = CSharpSyntaxTree.ParseText(source); var compilation = CSharpCompilation.Create( @@ -28,13 +45,9 @@ public static class SchemaGeneratorTestDriver GetMetadataReferences(), new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); - var additionalTexts = additionalFiles - .Select(static item => (AdditionalText)new InMemoryAdditionalText(item.path, item.content)) - .ToImmutableArray(); - GeneratorDriver driver = CSharpGeneratorDriver.Create( generators: new[] { new SchemaConfigGenerator().AsSourceGenerator() }, - additionalTexts: additionalTexts, + additionalTexts: additionalTexts.ToImmutableArray(), parseOptions: (CSharpParseOptions)syntaxTree.Options); driver = driver.RunGenerators(compilation); diff --git a/GFramework.SourceGenerators.Tests/Rule/ContextAwareGeneratorSnapshotTests.cs b/GFramework.SourceGenerators.Tests/Rule/ContextAwareGeneratorSnapshotTests.cs index 7a57c668..e9f8510b 100644 --- a/GFramework.SourceGenerators.Tests/Rule/ContextAwareGeneratorSnapshotTests.cs +++ b/GFramework.SourceGenerators.Tests/Rule/ContextAwareGeneratorSnapshotTests.cs @@ -154,6 +154,9 @@ public class ContextAwareGeneratorSnapshotTests private readonly string _context = "user-field"; private static readonly string _contextProvider = "user-provider"; private static readonly object _contextSync = new(); + private IArchitectureContext? _gFrameworkContextAwareContext; + private static IArchitectureContextProvider? _gFrameworkContextAwareProvider; + private static readonly object _gFrameworkContextAwareSync = new(); } } """; diff --git a/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/CollisionProneRule.ContextAware.g.cs b/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/CollisionProneRule.ContextAware.g.cs index 99e6e492..7b56a1fa 100644 --- a/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/CollisionProneRule.ContextAware.g.cs +++ b/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/CollisionProneRule.ContextAware.g.cs @@ -10,14 +10,14 @@ namespace TestApp; /// 生成代码会在实例级缓存首次解析到的上下文,并在未显式配置提供者时回退到 。 /// 同一生成类型的所有实例共享一个静态上下文提供者;切换或重置提供者只会影响尚未缓存上下文的新实例或未初始化实例, /// 已缓存的实例上下文需要通过 显式覆盖。 -/// 与手动继承 的路径相比,生成实现会使用 _gFrameworkContextAwareSync 协调惰性初始化、provider 切换和显式上下文注入; +/// 与手动继承 的路径相比,生成实现会使用 _gFrameworkContextAwareSync1 协调惰性初始化、provider 切换和显式上下文注入; /// 则保持无锁的实例级缓存语义,更适合已经由调用方线程模型保证串行访问的简单场景。 /// partial class CollisionProneRule : global::GFramework.Core.Abstractions.Rule.IContextAware { - private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _gFrameworkContextAwareContext; - private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? _gFrameworkContextAwareProvider; - private static readonly object _gFrameworkContextAwareSync = new(); + private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _gFrameworkContextAwareContext1; + private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? _gFrameworkContextAwareProvider1; + private static readonly object _gFrameworkContextAwareSync1 = new(); /// /// 获取当前实例绑定的架构上下文。 @@ -27,26 +27,20 @@ partial class CollisionProneRule : global::GFramework.Core.Abstractions.Rule.ICo /// 当静态提供者尚未配置时,生成代码会回退到 。 /// 一旦某个实例成功缓存上下文,后续 /// 或 不会自动清除此缓存;如需覆盖,请显式调用 IContextAware.SetContext(...)。 - /// 当前实现还假设 可在持有 _gFrameworkContextAwareSync 时安全执行; + /// 当前实现还假设 可在持有 _gFrameworkContextAwareSync1 时安全执行; /// 自定义 provider 不应在该调用链内重新进入当前类型的 provider 配置 API,且应避免引入与外部全局锁相互等待的锁顺序。 /// protected global::GFramework.Core.Abstractions.Architectures.IArchitectureContext Context { get { - var context = _gFrameworkContextAwareContext; - if (context is not null) - { - return context; - } - // 在同一个同步域内协调懒加载与 provider 切换,避免读取到被并发重置的空提供者。 - // provider 的 GetContext() 会在持有 _gFrameworkContextAwareSync 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。 - lock (_gFrameworkContextAwareSync) + // provider 的 GetContext() 会在持有 _gFrameworkContextAwareSync1 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。 + lock (_gFrameworkContextAwareSync1) { - _gFrameworkContextAwareProvider ??= new global::GFramework.Core.Architectures.GameContextProvider(); - _gFrameworkContextAwareContext ??= _gFrameworkContextAwareProvider.GetContext(); - return _gFrameworkContextAwareContext; + _gFrameworkContextAwareProvider1 ??= new global::GFramework.Core.Architectures.GameContextProvider(); + _gFrameworkContextAwareContext1 ??= _gFrameworkContextAwareProvider1.GetContext(); + return _gFrameworkContextAwareContext1; } } } @@ -64,9 +58,9 @@ partial class CollisionProneRule : global::GFramework.Core.Abstractions.Rule.ICo public static void SetContextProvider(global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider provider) { global::System.ArgumentNullException.ThrowIfNull(provider); - lock (_gFrameworkContextAwareSync) + lock (_gFrameworkContextAwareSync1) { - _gFrameworkContextAwareProvider = provider; + _gFrameworkContextAwareProvider1 = provider; } } @@ -80,18 +74,18 @@ partial class CollisionProneRule : global::GFramework.Core.Abstractions.Rule.ICo /// public static void ResetContextProvider() { - lock (_gFrameworkContextAwareSync) + lock (_gFrameworkContextAwareSync1) { - _gFrameworkContextAwareProvider = null; + _gFrameworkContextAwareProvider1 = null; } } void global::GFramework.Core.Abstractions.Rule.IContextAware.SetContext(global::GFramework.Core.Abstractions.Architectures.IArchitectureContext context) { // 与 Context getter 共享同一同步协议,避免显式注入被并发懒加载覆盖。 - lock (_gFrameworkContextAwareSync) + lock (_gFrameworkContextAwareSync1) { - _gFrameworkContextAwareContext = context; + _gFrameworkContextAwareContext1 = context; } } diff --git a/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/MyRule.ContextAware.g.cs b/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/MyRule.ContextAware.g.cs index 0788b52b..4072ea3d 100644 --- a/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/MyRule.ContextAware.g.cs +++ b/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/MyRule.ContextAware.g.cs @@ -34,12 +34,6 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware { get { - var context = _gFrameworkContextAwareContext; - if (context is not null) - { - return context; - } - // 在同一个同步域内协调懒加载与 provider 切换,避免读取到被并发重置的空提供者。 // provider 的 GetContext() 会在持有 _gFrameworkContextAwareSync 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。 lock (_gFrameworkContextAwareSync) 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 e0620bc3..97030a1e 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 @@ -7,8 +7,8 @@ ## 当前恢复点 -- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-021` -- 当前阶段:`Phase 21` +- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-022` +- 当前阶段:`Phase 22` - 当前焦点: - 已完成 `GFramework.Core` 当前 `MA0016` / `MA0002` / `MA0015` / `MA0077` 低风险收口批次 - 已复核 `net10.0` 下的 `MA0158` 基线:`GFramework.Core` / `GFramework.Cqrs` 当前共有 `16` 个 object lock @@ -20,10 +20,11 @@ - 已完成 `SchemaConfigGenerator.cs` 的第一批 `MA0051` 结构拆分:schema 入口解析、属性解析、schema 遍历、数组属性解析、 约束文档生成与若干生成代码发射 helper 已拆出语义阶段 - 已完成当前 PR #269 review follow-up:`CqrsHandlerRegistryGenerator` 按职责拆分为 partial 生成器文件, - `ContextAwareGenerator` 改用稳定前缀字段并补上 provider null 防御,`Option` 补齐 `` 契约说明 - - `LoggingConfiguration`、`FilterConfiguration` 与 `CollectionExtensions` 已改用集合抽象接口,并保留内部具体集合默认值 + `ContextAwareGenerator` 已补上字段名去冲突与锁内读取修正,`Option` 补齐 `` 契约说明 + - 已完成当前 PR #269 第二轮 follow-up:恢复 `EasyEvents`、`CollectionExtensions`、`LoggingConfiguration` 与 + `FilterConfiguration` 的公共 API 兼容形状,并将 analyzer 兼容性处理收敛到局部 pragma - `CoroutineScheduler` 的 tag/group 字典已显式使用 `StringComparer.Ordinal`,保持既有区分大小写语义 - - `EasyEvents.AddEvent()` 的重复注册路径已改为状态冲突异常,避免把泛型类型参数伪装成方法参数名 + - `EasyEvents.AddEvent()` 的重复注册路径已恢复为 `ArgumentException`,以保持既有异常契约 - `Option` 已声明 `IEquatable>`,与已有强类型 `Equals(Option)` 契约对齐 - 当前 `GFramework.Core` `net8.0` warnings-only 基线已降到 `0` 条 - 当前 `GFramework.Core.SourceGenerators` warnings-only 基线已降到 `0` 条 @@ -53,8 +54,9 @@ - 已完成当前 `GFramework.Core` `net8.0` 剩余低风险 analyzer warning 批次;warnings-only 基线已降到 `0` 条 - 已完成 `GFramework.Core.SourceGenerators` 中 `ContextAwareGenerator` 的剩余 `MA0051` 收口;warnings-only 基线已降到 `0` 条 - 已完成 `GFramework.Cqrs.SourceGenerators` 中 `CqrsHandlerRegistryGenerator` 的剩余 `MA0051` 收口;warnings-only 基线已降到 `0` 条 -- 已完成当前 PR #269 的 review follow-up:收口 `ContextAwareGenerator` 的字段命名冲突 / provider null 契约、 - `Option` 的 XML 文档缺口,以及 `CqrsHandlerRegistryGenerator` 的超大文件拆分 +- 已完成当前 PR #269 的 review follow-up:收口 `ContextAwareGenerator` 的字段命名冲突 / 锁内读取契约、 + `CqrsHandlerRegistryGenerator` 的运行时类型 null 防御与超大文件拆分、`SchemaConfigGenerator` 的取消语义, + 并恢复 `EasyEvents` / `CollectionExtensions` / logging 配置模型的公共 API 兼容形状 - 已完成 `GFramework.Game.SourceGenerators` 中 `SchemaConfigGenerator` 的第一批 `MA0051` 收口;warnings-only 基线剩余 `9` 条 `MA0051` @@ -100,12 +102,15 @@ - `RP-021` 使用 `$gframework-pr-review` 复核当前分支 PR #269 后,修复仍在本地成立的 4 个项:将 `CqrsHandlerRegistryGenerator` 拆分为职责清晰的 partial 文件、为 `ContextAwareGenerator` 生成字段增加稳定前缀并补上 `SetContextProvider` 的运行时 null 校验、为 `Option` 补齐 ``,并新增字段重名场景的生成器快照测试 +- `RP-022` 继续复核 PR #269 的 latest-head review threads 与 nitpick,确认仍成立的项包括公共 API 兼容回退、 + `ContextAwareGenerator` 字段名真正去冲突与锁内读取、`SchemaConfigGenerator` 取消传播、`Cqrs` 运行时类型 null 防御; + 已补齐对应回归测试与 focused build/test 验证 - 当前工作树分支 `fix/analyzer-warning-reduction-batch` 已在 `ai-plan/public/README.md` 建立 topic 映射 ## 当前风险 -- 公共契约兼容风险:本轮将部分配置与扩展方法返回值从具体集合类型改为集合抽象接口 - - 缓解措施:保留具体集合默认值,并通过配置反序列化、工厂创建与集合扩展定向测试覆盖主要消费路径 +- analyzer 收口回退风险:后续若继续压 `MA0015` / `MA0016`,容易再次把公共 API 收窄成与既有契约不兼容的形状 + - 缓解措施:优先保留既有公共 API,并将兼容性例外收敛到局部 pragma;继续用反射断言覆盖返回类型、属性类型与异常类型 - 测试宿主稳定性风险:部分 Godot 失败路径在当前 .NET 测试宿主下仍不稳定 - 缓解措施:继续优先使用稳定的 targeted test、项目构建和相邻 smoke test 组合验证 - 多目标框架 warning 解释风险:同一源位置会在多个 target framework 下重复计数 @@ -242,6 +247,17 @@ - 结果:先并行运行两条 `dotnet test` 时触发共享输出文件锁冲突;改为串行重跑后 `ContextAwareGeneratorSnapshotTests=2 Passed`、 `CqrsHandlerRegistryGeneratorTests=14 Passed` - 说明:失败来自测试宿主并行写入同一 build 输出,不是代码回归;串行重跑后快照新增的字段重名场景和 CQRS 快照均通过 +- `RP-022` 的验证结果: + - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders=\"\" -v minimal` + - 结果:通过;`EasyEvents`、`CollectionExtensions` 与 logging 配置模型的兼容性回退未引入新的 `net8.0` 构建错误 + - `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders=\"\" -v minimal` + - 结果:通过;`ContainingAssembly` null 防御与发射 helper 精简未引入新的构建错误 + - `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders=\"\" -v minimal` + - 结果:通过;仍保留既有 `9` 条 `SchemaConfigGenerator.cs` `MA0051` warning,非本轮新增 + - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~SchemaConfigGeneratorTests|FullyQualifiedName~ContextAwareGeneratorSnapshotTests|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests" -m:1 -p:RestoreFallbackFolders=\"\" -v minimal` + - 结果:`63 Passed`,`0 Failed` + - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~EasyEventsTests|FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~LoggingConfigurationTests|FullyQualifiedName~ConfigurableLoggerFactoryTests" -m:1 -p:RestoreFallbackFolders=\"\" -v minimal` + - 结果:`38 Passed`,`0 Failed` - active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史 ## 下一步 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 0fa90aff..67e4f605 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,41 @@ # Analyzer Warning Reduction 追踪 +## 2026-04-22 — RP-022 + +### 阶段:PR #269 第二轮 review follow-up 收口(RP-022) + +- 启动复核: + - 延续 `$gframework-pr-review` 的 PR #269 结果,继续核对 latest-head unresolved threads 与 nitpick comment + - 结合本地实现确认仍成立的项不止第一轮记录的 4 个,还包括公共 API 兼容回退、`SchemaConfigGenerator` 取消传播、 + `ContextAwareGenerator` 真正的字段名去冲突与锁内读取修正、`Cqrs` 运行时类型 null 防御 +- 决策: + - 对公共 API 兼容项优先保持既有契约,不为了压 analyzer 而继续收窄返回类型、属性类型或异常类型 + - `ContextAwareGenerator` 采用保守并发修复:移除未加锁 fast-path,统一在锁内读取上下文缓存,并让生成字段名按已有成员去冲突 + - `SchemaConfigGenerator` 在取消已请求时直接重新抛出 `OperationCanceledException`,避免把取消误报告成普通诊断 +- 实施调整: + - 将 `EasyEvents.AddEvent()` 的重复注册异常恢复为 `ArgumentException`,并在测试中恢复既有异常契约断言 + - 将 `CollectionExtensions.ToDictionarySafe(...)` 返回类型恢复为 `Dictionary`,并新增反射测试锁定公开 API 形状 + - 将 `LoggingConfiguration` / `FilterConfiguration` 的公开集合属性恢复为具体 `List<>` / `Dictionary<,>` 类型, + 并新增反射测试与默认 comparer 语义断言 + - 为 `CqrsHandlerRegistryGenerator` 的命名类型引用构造补上 `ContainingAssembly is null` 防御,移除发射 helper 冗余布尔参数 + - 为 `SchemaConfigGenerator` 补上“仅在 cancellationToken 已取消时重抛”的 catch 分支,并为测试驱动添加多 `AdditionalText` 重载 + - 为 `ContextAwareGenerator` 增加生成成员名分配逻辑,新增 `_gFrameworkContextAware*` 与旧 `_context*` 双冲突快照场景, + 同时移除 getter 中未加锁 fast-path +- 验证结果: + - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -v minimal` + - 结果:通过 + - `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -v minimal` + - 结果:通过 + - `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -v minimal` + - 结果:通过;仍有既有 `9` 条 `MA0051` + - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~SchemaConfigGeneratorTests|FullyQualifiedName~ContextAwareGeneratorSnapshotTests|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests" -m:1 -p:RestoreFallbackFolders="" -v minimal` + - 结果:`63 Passed`,`0 Failed` + - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~EasyEventsTests|FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~LoggingConfigurationTests|FullyQualifiedName~ConfigurableLoggerFactoryTests" -m:1 -p:RestoreFallbackFolders="" -v minimal` + - 结果:`38 Passed`,`0 Failed` +- 下一步建议: + - 回到 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs` 的剩余 `MA0051` + - 若后续 review 再提 analyzer 兼容建议,先做公共契约回归检查,再决定是否接受该建议 + ## 2026-04-22 — RP-021 ### 阶段:PR #269 review follow-up 收口(RP-021) From df68cdfd82a05f5373d5dfb6ed0df5b9a35b4661 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 22 Apr 2026 16:27:30 +0800 Subject: [PATCH 08/13] =?UTF-8?q?fix(pr269):=20=E6=94=B6=E5=8F=A3=E5=89=A9?= =?UTF-8?q?=E4=BD=99=E8=AF=84=E5=AE=A1=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 SchemaConfigGenerator 的根类型标识符校验与 comparer XML 文档转义\n- 补强 LoggingConfiguration 与 CollectionExtensions 的公共 API 兼容断言\n- 重构 Cqrs 运行时类型反射查找 helper,并更新 analyzer-warning-reduction 跟踪与验证记录 --- .../Extensions/CollectionExtensionsTests.cs | 14 ++++- .../Logging/LoggingConfigurationTests.cs | 7 ++- ...RegistryGenerator.RuntimeTypeReferences.cs | 57 +++++++++++-------- .../Config/SchemaConfigGenerator.cs | 49 ++++++++++++++-- .../Config/SchemaConfigGeneratorTests.cs | 48 ++++++++++++++++ .../GeneratedConfigCatalog.g.txt | 2 +- .../analyzer-warning-reduction-tracking.md | 29 +++++++--- .../analyzer-warning-reduction-trace.md | 33 +++++++++++ 8 files changed, 200 insertions(+), 39 deletions(-) diff --git a/GFramework.Core.Tests/Extensions/CollectionExtensionsTests.cs b/GFramework.Core.Tests/Extensions/CollectionExtensionsTests.cs index 9bbc1c4c..042065ef 100644 --- a/GFramework.Core.Tests/Extensions/CollectionExtensionsTests.cs +++ b/GFramework.Core.Tests/Extensions/CollectionExtensionsTests.cs @@ -174,9 +174,19 @@ public class CollectionExtensionsTests var method = typeof(GFramework.Core.Extensions.CollectionExtensions) .GetMethods() .Single(static method => method.Name == nameof(GFramework.Core.Extensions.CollectionExtensions.ToDictionarySafe)); + var methodGenericArguments = method.GetGenericArguments(); + var returnTypeGenericArguments = method.ReturnType.GetGenericArguments(); - Assert.That(method.ReturnType.IsGenericType, Is.True); - Assert.That(method.ReturnType.GetGenericTypeDefinition(), Is.EqualTo(typeof(Dictionary<,>))); + Assert.Multiple(() => + { + Assert.That(method.IsGenericMethodDefinition, Is.True); + Assert.That(method.ReturnType.IsGenericType, Is.True); + Assert.That(method.ReturnType.GetGenericTypeDefinition(), Is.EqualTo(typeof(Dictionary<,>))); + Assert.That(methodGenericArguments.Select(static argument => argument.Name), Is.EqualTo(new[] { "T", "TKey", "TValue" })); + Assert.That(returnTypeGenericArguments, Has.Length.EqualTo(2)); + Assert.That(returnTypeGenericArguments[0], Is.SameAs(methodGenericArguments[1])); + Assert.That(returnTypeGenericArguments[1], Is.SameAs(methodGenericArguments[2])); + }); } /// diff --git a/GFramework.Core.Tests/Logging/LoggingConfigurationTests.cs b/GFramework.Core.Tests/Logging/LoggingConfigurationTests.cs index 3f88b4f5..9586533f 100644 --- a/GFramework.Core.Tests/Logging/LoggingConfigurationTests.cs +++ b/GFramework.Core.Tests/Logging/LoggingConfigurationTests.cs @@ -65,7 +65,12 @@ public class LoggingConfigurationTests var config = new LoggingConfiguration(); config.LoggerLevels["GFramework.Core"] = LogLevel.Info; - Assert.That(config.LoggerLevels.ContainsKey("gframework.core"), Is.False); + Assert.Multiple(() => + { + Assert.That(config.LoggerLevels.ContainsKey("GFramework.Core"), Is.True); + Assert.That(config.LoggerLevels["GFramework.Core"], Is.EqualTo(LogLevel.Info)); + Assert.That(config.LoggerLevels.ContainsKey("gframework.core"), Is.False); + }); } [Test] diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.RuntimeTypeReferences.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.RuntimeTypeReferences.cs index a10a6a9b..14eee2d8 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.RuntimeTypeReferences.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.RuntimeTypeReferences.cs @@ -173,22 +173,11 @@ public sealed partial class CqrsHandlerRegistryGenerator INamedTypeSymbol namedType, out RuntimeTypeReferenceSpec? runtimeTypeReference) { - if (SymbolEqualityComparer.Default.Equals(namedType.ContainingAssembly, compilation.Assembly)) - { - runtimeTypeReference = RuntimeTypeReferenceSpec.FromReflectionLookup(GetReflectionTypeMetadataName(namedType)); - return true; - } - - if (namedType.ContainingAssembly is null) - { - runtimeTypeReference = null; - return false; - } - - runtimeTypeReference = RuntimeTypeReferenceSpec.FromExternalReflectionLookup( - namedType.ContainingAssembly.Identity.ToString(), - GetReflectionTypeMetadataName(namedType)); - return true; + return TryCreateReflectionLookupReference( + compilation, + namedType, + GetReflectionTypeMetadataName(namedType), + out runtimeTypeReference); } /// @@ -223,22 +212,42 @@ public sealed partial class CqrsHandlerRegistryGenerator return true; } - if (SymbolEqualityComparer.Default.Equals(genericTypeDefinition.ContainingAssembly, compilation.Assembly)) + return TryCreateReflectionLookupReference( + compilation, + genericTypeDefinition, + GetReflectionTypeMetadataName(genericTypeDefinition), + out genericTypeDefinitionReference); + } + + /// + /// 为当前程序集或外部程序集中的命名类型构造统一的运行时反射查找描述。 + /// + /// 当前生成轮次的编译上下文。 + /// 需要在运行时解析的命名类型。 + /// 写入生成代码的反射元数据名称。 + /// 成功时返回可直接写入注册器的运行时类型引用描述。 + /// 当命名类型具备可稳定编码的程序集归属信息时返回 + private static bool TryCreateReflectionLookupReference( + Compilation compilation, + INamedTypeSymbol namedType, + string metadataName, + out RuntimeTypeReferenceSpec? runtimeTypeReference) + { + if (SymbolEqualityComparer.Default.Equals(namedType.ContainingAssembly, compilation.Assembly)) { - genericTypeDefinitionReference = RuntimeTypeReferenceSpec.FromReflectionLookup( - GetReflectionTypeMetadataName(genericTypeDefinition)); + runtimeTypeReference = RuntimeTypeReferenceSpec.FromReflectionLookup(metadataName); return true; } - if (genericTypeDefinition.ContainingAssembly is null) + if (namedType.ContainingAssembly is null) { - genericTypeDefinitionReference = null; + runtimeTypeReference = null; return false; } - genericTypeDefinitionReference = RuntimeTypeReferenceSpec.FromExternalReflectionLookup( - genericTypeDefinition.ContainingAssembly.Identity.ToString(), - GetReflectionTypeMetadataName(genericTypeDefinition)); + runtimeTypeReference = RuntimeTypeReferenceSpec.FromExternalReflectionLookup( + namedType.ContainingAssembly.Identity.ToString(), + metadataName); return true; } diff --git a/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs b/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs index c8499bbe..2da81e0e 100644 --- a/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs +++ b/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs @@ -176,8 +176,13 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator return SchemaParseResult.FromDiagnostic(diagnostic!); } - var entityName = ToPascalCase(GetSchemaBaseName(filePath)); - var rootObject = ParseObjectSpec(filePath, root, "", $"{entityName}Config", isRoot: true); + var schemaBaseName = GetSchemaBaseName(filePath); + if (!TryBuildRootTypeIdentifiers(filePath, schemaBaseName, out var entityName, out var rootClassName, out diagnostic)) + { + return SchemaParseResult.FromDiagnostic(diagnostic!); + } + + var rootObject = ParseObjectSpec(filePath, root, "", rootClassName, isRoot: true); if (rootObject.Diagnostic is not null) { return SchemaParseResult.FromDiagnostic(rootObject.Diagnostic); @@ -190,7 +195,6 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator return SchemaParseResult.FromDiagnostic(diagnostic); } - var schemaBaseName = GetSchemaBaseName(filePath); var configRelativePath = ResolveConfigRelativePath(filePath, root, schemaBaseName); if (configRelativePath.Diagnostic is not null) { @@ -3110,9 +3114,11 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator for (var index = 0; index < schemas.Count; index++) { var schema = schemas[index]; + var comparerTypeDocumentation = EscapeXmlDocumentation( + $"global::System.Collections.Generic.IEqualityComparer<{schema.KeyClrType}>?"); builder.AppendLine(" /// "); builder.AppendLine( - $" /// Gets or sets the optional key comparer forwarded to {schema.EntityName}ConfigBindings.Register{schema.EntityName}Table(global::GFramework.Game.Config.YamlConfigLoader, global::System.Collections.Generic.IEqualityComparer<{schema.KeyClrType}>?) when aggregate registration runs."); + $" /// Gets or sets the optional key comparer forwarded to {schema.EntityName}ConfigBindings.Register{schema.EntityName}Table using {comparerTypeDocumentation} when aggregate registration runs."); builder.AppendLine(" /// "); builder.AppendLine( $" public global::System.Collections.Generic.IEqualityComparer<{schema.KeyClrType}>? {schema.EntityName}Comparer {{ get; init; }}"); @@ -4045,6 +4051,41 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator return false; } + /// + /// 将 schema 文件名派生的根实体名验证为生成代码可直接使用的根类型标识符。 + /// 这里与属性名验证保持一致,避免文件名中的前导数字或其他非法字符把根配置类型/表类型生成为无效 C# 标识符。 + /// + /// Schema 文件路径。 + /// 去除扩展名后的 schema 基础名。 + /// 验证后的实体名。 + /// 验证后的根配置类型名。 + /// 根类型名非法时生成的诊断。 + /// 是否成功生成合法的根类型标识符。 + private static bool TryBuildRootTypeIdentifiers( + string filePath, + string schemaBaseName, + out string entityName, + out string rootClassName, + out Diagnostic? diagnostic) + { + entityName = ToPascalCase(schemaBaseName); + rootClassName = $"{entityName}Config"; + if (SyntaxFacts.IsValidIdentifier(rootClassName)) + { + diagnostic = null; + return true; + } + + diagnostic = Diagnostic.Create( + ConfigSchemaDiagnostics.InvalidGeneratedIdentifier, + CreateFileLocation(filePath), + Path.GetFileName(filePath), + "", + schemaBaseName, + rootClassName); + return false; + } + /// /// 从 schema 文件路径提取实体基础名。 /// diff --git a/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs index 4ef4bbad..ebbbd484 100644 --- a/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs @@ -72,6 +72,48 @@ public class SchemaConfigGeneratorTests }); } + /// + /// 验证 schema 文件名若生成无效根类型标识符时,会在生成前产生命名明确的诊断。 + /// + [Test] + public void Run_Should_Report_Diagnostic_When_Schema_File_Name_Generates_Invalid_Root_Type_Identifier() + { + const string source = """ + namespace TestApp + { + public sealed class Dummy + { + } + } + """; + + const string schema = """ + { + "type": "object", + "required": ["id", "name"], + "properties": { + "id": { "type": "integer" }, + "name": { "type": "string" } + } + } + """; + + var result = SchemaGeneratorTestDriver.Run( + source, + ("123-monster.schema.json", schema)); + + var diagnostic = result.Results.Single().Diagnostics.Single(); + + Assert.Multiple(() => + { + Assert.That(diagnostic.Id, Is.EqualTo("GF_ConfigSchema_006")); + Assert.That(diagnostic.Severity, Is.EqualTo(DiagnosticSeverity.Error)); + Assert.That(diagnostic.GetMessage(), Does.Contain("")); + Assert.That(diagnostic.GetMessage(), Does.Contain("123-monster")); + Assert.That(diagnostic.GetMessage(), Does.Contain("123MonsterConfig")); + }); + } + /// /// 用于模拟 AdditionalFiles 读取阶段直接收到取消请求的测试桩。 /// @@ -2687,6 +2729,12 @@ public class SchemaConfigGeneratorTests Assert.That(catalogSource, Does.Contain( "public global::System.Collections.Generic.IEqualityComparer? MonsterComparer { get; init; }")); + Assert.That(catalogSource, + Does.Contain( + "using global::System.Collections.Generic.IEqualityComparer<string>? when aggregate registration runs.")); + Assert.That(catalogSource, + Does.Contain( + "using global::System.Collections.Generic.IEqualityComparer<int>? when aggregate registration runs.")); Assert.That(catalogSource, Does.Contain("return RegisterAllGeneratedConfigTables(loader, options: null);")); Assert.That(catalogSource, Does.Contain("GeneratedConfigRegistrationOptions? options")); Assert.That(catalogSource, Does.Contain("loader.RegisterItemTable(effectiveOptions.ItemComparer);")); diff --git a/GFramework.SourceGenerators.Tests/Config/snapshots/SchemaConfigGenerator/GeneratedConfigCatalog.g.txt b/GFramework.SourceGenerators.Tests/Config/snapshots/SchemaConfigGenerator/GeneratedConfigCatalog.g.txt index 4fb93c90..f60b072f 100644 --- a/GFramework.SourceGenerators.Tests/Config/snapshots/SchemaConfigGenerator/GeneratedConfigCatalog.g.txt +++ b/GFramework.SourceGenerators.Tests/Config/snapshots/SchemaConfigGenerator/GeneratedConfigCatalog.g.txt @@ -236,7 +236,7 @@ public sealed class GeneratedConfigRegistrationOptions public global::System.Predicate? TableFilter { get; init; } /// - /// Gets or sets the optional key comparer forwarded to MonsterConfigBindings.RegisterMonsterTable(global::GFramework.Game.Config.YamlConfigLoader, global::System.Collections.Generic.IEqualityComparer?) when aggregate registration runs. + /// Gets or sets the optional key comparer forwarded to MonsterConfigBindings.RegisterMonsterTable using global::System.Collections.Generic.IEqualityComparer<int>? when aggregate registration runs. /// public global::System.Collections.Generic.IEqualityComparer? MonsterComparer { get; init; } } 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 97030a1e..50163cb8 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 @@ -7,8 +7,8 @@ ## 当前恢复点 -- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-022` -- 当前阶段:`Phase 22` +- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-023` +- 当前阶段:`Phase 23` - 当前焦点: - 已完成 `GFramework.Core` 当前 `MA0016` / `MA0002` / `MA0015` / `MA0077` 低风险收口批次 - 已复核 `net10.0` 下的 `MA0158` 基线:`GFramework.Core` / `GFramework.Cqrs` 当前共有 `16` 个 object lock @@ -23,6 +23,8 @@ `ContextAwareGenerator` 已补上字段名去冲突与锁内读取修正,`Option` 补齐 `` 契约说明 - 已完成当前 PR #269 第二轮 follow-up:恢复 `EasyEvents`、`CollectionExtensions`、`LoggingConfiguration` 与 `FilterConfiguration` 的公共 API 兼容形状,并将 analyzer 兼容性处理收敛到局部 pragma + - 已完成当前 PR #269 第三轮 follow-up:继续收口 `SchemaConfigGenerator` 的根类型标识符校验与 XML 文档转义, + 并补齐 `LoggingConfigurationTests`、`CollectionExtensionsTests`、`Cqrs` helper 抽取与 `ai-plan` 命令文本修正 - `CoroutineScheduler` 的 tag/group 字典已显式使用 `StringComparer.Ordinal`,保持既有区分大小写语义 - `EasyEvents.AddEvent()` 的重复注册路径已恢复为 `ArgumentException`,以保持既有异常契约 - `Option` 已声明 `IEquatable>`,与已有强类型 `Equals(Option)` 契约对齐 @@ -105,6 +107,9 @@ - `RP-022` 继续复核 PR #269 的 latest-head review threads 与 nitpick,确认仍成立的项包括公共 API 兼容回退、 `ContextAwareGenerator` 字段名真正去冲突与锁内读取、`SchemaConfigGenerator` 取消传播、`Cqrs` 运行时类型 null 防御; 已补齐对应回归测试与 focused build/test 验证 +- `RP-023` 继续复核 PR #269 剩余 nitpick/outside-diff 项,确认仍成立的项集中在 `SchemaConfigGenerator` 根类型名校验、 + aggregate registration comparer XML 文档转义、logging / collection 反射测试补强,以及跟踪文档中的 + `RestoreFallbackFolders=""` 可复制性问题 - 当前工作树分支 `fix/analyzer-warning-reduction-batch` 已在 `ai-plan/public/README.md` 建立 topic 映射 ## 当前风险 @@ -248,16 +253,26 @@ `CqrsHandlerRegistryGeneratorTests=14 Passed` - 说明:失败来自测试宿主并行写入同一 build 输出,不是代码回归;串行重跑后快照新增的字段重名场景和 CQRS 快照均通过 - `RP-022` 的验证结果: - - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders=\"\" -v minimal` + - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -v minimal` - 结果:通过;`EasyEvents`、`CollectionExtensions` 与 logging 配置模型的兼容性回退未引入新的 `net8.0` 构建错误 - - `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders=\"\" -v minimal` + - `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -v minimal` - 结果:通过;`ContainingAssembly` null 防御与发射 helper 精简未引入新的构建错误 - - `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders=\"\" -v minimal` + - `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -v minimal` - 结果:通过;仍保留既有 `9` 条 `SchemaConfigGenerator.cs` `MA0051` warning,非本轮新增 - - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~SchemaConfigGeneratorTests|FullyQualifiedName~ContextAwareGeneratorSnapshotTests|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests" -m:1 -p:RestoreFallbackFolders=\"\" -v minimal` + - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~SchemaConfigGeneratorTests|FullyQualifiedName~ContextAwareGeneratorSnapshotTests|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests" -m:1 -p:RestoreFallbackFolders="" -v minimal` - 结果:`63 Passed`,`0 Failed` - - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~EasyEventsTests|FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~LoggingConfigurationTests|FullyQualifiedName~ConfigurableLoggerFactoryTests" -m:1 -p:RestoreFallbackFolders=\"\" -v minimal` + - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~EasyEventsTests|FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~LoggingConfigurationTests|FullyQualifiedName~ConfigurableLoggerFactoryTests" -m:1 -p:RestoreFallbackFolders="" -v minimal` - 结果:`38 Passed`,`0 Failed` +- `RP-023` 的验证结果: + - `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -v minimal` + - 结果:通过;仍保留既有 `9` 条 `SchemaConfigGenerator.cs` `MA0051` warning,未新增新的 generator warning + - `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -v minimal` + - 结果:通过;并行构建时 `GFramework.SourceGenerators.Common.dll` 复制阶段出现一次 `MSB3026` 重试,随后成功完成 + - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~SchemaConfigGeneratorTests|FullyQualifiedName~SchemaConfigGeneratorSnapshotTests|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests" -m:1 -p:RestoreFallbackFolders="" -v minimal` + - 结果:`63 Passed`,`0 Failed` + - 说明:测试项目构建仍会显示既有 `GFramework.SourceGenerators.Tests` analyzer warning;不属于本轮写集 + - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~LoggingConfigurationTests" -m:1 -p:RestoreFallbackFolders="" -v minimal` + - 结果:`27 Passed`,`0 Failed` - active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史 ## 下一步 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 67e4f605..e1b62714 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,38 @@ # Analyzer Warning Reduction 追踪 +## 2026-04-22 — RP-023 + +### 阶段:PR #269 第三轮 review follow-up 收口(RP-023) + +- 启动复核: + - 延续 `$gframework-pr-review` 对 PR #269 的 latest-head unresolved threads、outside-diff comment 与 nitpick comment + - 本地核实后确认剩余仍成立的项集中在 `SchemaConfigGenerator` 根类型名校验、aggregate registration comparer XML 文档转义、 + `LoggingConfigurationTests` / `CollectionExtensionsTests` 断言补强,以及 `ai-plan` 命令文本可复制性 +- 决策: + - `SchemaConfigGenerator` 沿用现有 `InvalidGeneratedIdentifier` 诊断,不新增诊断 ID;将根类型名校验收敛到独立 helper, + 让顶层 schema 文件名与属性名共享同一类安全边界 + - aggregate registration comparer 文档直接复用现有 `EscapeXmlDocumentation(...)`,避免在 `///` 注释里再次写入原始泛型尖括号 + - `CqrsHandlerRegistryGenerator` 的重复反射查找分支采用小 helper 抽取,不改变 fallback 语义和快照输出 +- 实施调整: + - 为 `SchemaConfigGenerator` 新增 `TryBuildRootTypeIdentifiers(...)`,在进入 `ParseObjectSpec(...)` 前拦截非法根类型名 + - 调整 aggregate registration comparer 属性的 XML 文档,使用 `...` 包裹并转义泛型类型文本 + - 为 `SchemaConfigGeneratorTests` 增加非法 schema 文件名诊断回归,并补强 generated catalog 中 comparer 文档断言 + - 为 `LoggingConfigurationTests` 增加正向键存在和值断言,为 `CollectionExtensionsTests` 补齐返回类型泛型参数绑定断言 + - 为 `CqrsHandlerRegistryGenerator.RuntimeTypeReferences` 抽取共享反射查找 helper,并修正 active tracking 中的转义引号 +- 验证结果: + - `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -v minimal` + - 结果:通过;仍保留既有 `9` 条 `SchemaConfigGenerator.cs` `MA0051` + - `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -v minimal` + - 结果:通过;并行构建时出现一次 `MSB3026` 文件占用重试,自动恢复后完成 + - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~SchemaConfigGeneratorTests|FullyQualifiedName~SchemaConfigGeneratorSnapshotTests|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests" -m:1 -p:RestoreFallbackFolders="" -v minimal` + - 结果:`63 Passed`,`0 Failed` + - 说明:测试项目构建仍打印既有 source-generator-tests analyzer warning,不属于本轮写集 + - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~LoggingConfigurationTests" -m:1 -p:RestoreFallbackFolders="" -v minimal` + - 结果:`27 Passed`,`0 Failed` +- 下一步建议: + - 若本轮验证通过,继续回到 `SchemaConfigGenerator.cs` 剩余 `MA0051` + - 若 PR #269 仍有未关闭 review thread,再按“先本地复核、再最小修复”的节奏收口 + ## 2026-04-22 — RP-022 ### 阶段:PR #269 第二轮 review follow-up 收口(RP-022) From 4ef9406ee913a865c9932f7b194df7dc5f206cc4 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 22 Apr 2026 18:52:26 +0800 Subject: [PATCH 09/13] =?UTF-8?q?fix(source-generators):=20=E6=94=B6?= =?UTF-8?q?=E5=8F=A3PR269=E5=89=A9=E4=BD=99review=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 Cqrs handler registry 对 Roslyn error type 的直接引用,改走安全的运行时类型查找 - 补充 SchemaConfigGenerator 根 type 非字符串诊断回归与 Cqrs 未解析类型回归测试 - 更新 analyzer-warning-reduction 的 RP-024 跟踪与验证记录 --- ...RegistryGenerator.RuntimeTypeReferences.cs | 7 +- .../Config/SchemaConfigGenerator.cs | 1 + .../Config/SchemaConfigGeneratorTests.cs | 39 ++++++++ .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 97 +++++++++++++++++++ .../analyzer-warning-reduction-tracking.md | 11 ++- .../analyzer-warning-reduction-trace.md | 32 ++++++ 6 files changed, 184 insertions(+), 3 deletions(-) diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.RuntimeTypeReferences.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.RuntimeTypeReferences.cs index 14eee2d8..ddc9cb37 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.RuntimeTypeReferences.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.RuntimeTypeReferences.cs @@ -253,6 +253,11 @@ public sealed partial class CqrsHandlerRegistryGenerator private static bool CanReferenceFromGeneratedRegistry(Compilation compilation, ITypeSymbol type) { + // Roslyn error symbols stringify to unresolved type names; emitting them via typeof(...) would turn + // an existing user-code error into a second generator-produced compile error instead of falling back. + if (type.TypeKind == TypeKind.Error) + return false; + switch (type) { case IArrayTypeSymbol arrayType: @@ -274,7 +279,7 @@ public sealed partial class CqrsHandlerRegistryGenerator case ITypeParameterSymbol: return false; default: - // Treat other Roslyn type kinds, such as dynamic or unresolved error types, as referenceable for now. + // Treat other Roslyn type kinds, such as dynamic, as referenceable for now. // If a real-world case proves unsafe, tighten this branch instead of broadening the named-type path above. return true; } diff --git a/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs b/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs index 2da81e0e..dabfac77 100644 --- a/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs +++ b/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs @@ -221,6 +221,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator private static bool TryValidateSchemaRoot(string filePath, JsonElement root, out Diagnostic? diagnostic) { if (!root.TryGetProperty("type", out var rootTypeElement) || + rootTypeElement.ValueKind != JsonValueKind.String || !IsSchemaType(rootTypeElement.GetString() ?? string.Empty, "object")) { diagnostic = Diagnostic.Create( diff --git a/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs index ebbbd484..f1f5afb1 100644 --- a/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs @@ -72,6 +72,45 @@ public class SchemaConfigGeneratorTests }); } + /// + /// 验证根节点 type 元数据不是字符串时,会返回根对象约束诊断,而不是抛出 JSON 访问异常。 + /// + [Test] + public void Run_Should_Report_Diagnostic_When_Root_Type_Metadata_Is_Not_A_String() + { + const string source = """ + namespace TestApp + { + public sealed class Dummy + { + } + } + """; + + const string schema = """ + { + "type": 123, + "required": ["id"], + "properties": { + "id": { "type": "integer" } + } + } + """; + + var result = SchemaGeneratorTestDriver.Run( + source, + ("monster.schema.json", schema)); + + var diagnostic = result.Results.Single().Diagnostics.Single(); + + Assert.Multiple(() => + { + Assert.That(diagnostic.Id, Is.EqualTo("GF_ConfigSchema_002")); + Assert.That(diagnostic.Severity, Is.EqualTo(DiagnosticSeverity.Error)); + Assert.That(diagnostic.GetMessage(), Does.Contain("monster.schema.json")); + }); + } + /// /// 验证 schema 文件名若生成无效根类型标识符时,会在生成前产生命名明确的诊断。 /// diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index e9a9bfa2..311e4f65 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -1375,6 +1375,103 @@ public class CqrsHandlerRegistryGeneratorTests }); } + /// + /// 验证 handler 合同里出现未解析错误类型时,生成器会改为运行时精确查找该类型, + /// 而不会把无效类型名直接写进生成代码中的 typeof(...)。 + /// + [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 inputCompilationErrors = execution.InputCompilationDiagnostics + .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) + .ToArray(); + var generatedCompilationErrors = execution.GeneratedCompilationDiagnostics + .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) + .ToArray(); + var generatorErrors = execution.GeneratorDiagnostics + .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) + .ToArray(); + + Assert.Multiple(() => + { + Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS0246")); + Assert.That(generatedCompilationErrors, Is.Empty); + Assert.That(generatorErrors, Is.Empty); + Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1)); + Assert.That( + execution.GeneratedSources[0].content, + Does.Contain("registryAssembly.GetType(\"MissingResponse\", throwOnError: false, ignoreCase: false);")); + Assert.That( + execution.GeneratedSources[0].content, + Does.Contain("internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry")); + }); + } + /// /// 验证当 fallback metadata 仍然必需且 runtime 提供了承载契约时, /// 生成器会继续产出注册器并发射程序集级 CqrsReflectionFallbackAttribute。 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 50163cb8..3fe51813 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 @@ -7,8 +7,8 @@ ## 当前恢复点 -- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-023` -- 当前阶段:`Phase 23` +- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-024` +- 当前阶段:`Phase 24` - 当前焦点: - 已完成 `GFramework.Core` 当前 `MA0016` / `MA0002` / `MA0015` / `MA0077` 低风险收口批次 - 已复核 `net10.0` 下的 `MA0158` 基线:`GFramework.Core` / `GFramework.Cqrs` 当前共有 `16` 个 object lock @@ -25,6 +25,8 @@ `FilterConfiguration` 的公共 API 兼容形状,并将 analyzer 兼容性处理收敛到局部 pragma - 已完成当前 PR #269 第三轮 follow-up:继续收口 `SchemaConfigGenerator` 的根类型标识符校验与 XML 文档转义, 并补齐 `LoggingConfigurationTests`、`CollectionExtensionsTests`、`Cqrs` helper 抽取与 `ai-plan` 命令文本修正 + - 已完成当前 PR #269 第四轮 follow-up:将 `CqrsHandlerRegistryGenerator` 的 Roslyn error type 直接引用改为 + 运行时精确查找路径,并为 `SchemaConfigGenerator` 补上根 `type` 非字符串时的防御与回归测试 - `CoroutineScheduler` 的 tag/group 字典已显式使用 `StringComparer.Ordinal`,保持既有区分大小写语义 - `EasyEvents.AddEvent()` 的重复注册路径已恢复为 `ArgumentException`,以保持既有异常契约 - `Option` 已声明 `IEquatable>`,与已有强类型 `Equals(Option)` 契约对齐 @@ -59,6 +61,8 @@ - 已完成当前 PR #269 的 review follow-up:收口 `ContextAwareGenerator` 的字段命名冲突 / 锁内读取契约、 `CqrsHandlerRegistryGenerator` 的运行时类型 null 防御与超大文件拆分、`SchemaConfigGenerator` 的取消语义, 并恢复 `EasyEvents` / `CollectionExtensions` / logging 配置模型的公共 API 兼容形状 +- 已完成当前 PR #269 的第四轮 review follow-up:确认 5 个 latest-head 未解决线程中仅剩 2 个本地仍成立, + 已分别在 `CqrsHandlerRegistryGenerator` 与 `SchemaConfigGenerator` 中收口,并补齐定向 generator regression tests - 已完成 `GFramework.Game.SourceGenerators` 中 `SchemaConfigGenerator` 的第一批 `MA0051` 收口;warnings-only 基线剩余 `9` 条 `MA0051` @@ -110,6 +114,9 @@ - `RP-023` 继续复核 PR #269 剩余 nitpick/outside-diff 项,确认仍成立的项集中在 `SchemaConfigGenerator` 根类型名校验、 aggregate registration comparer XML 文档转义、logging / collection 反射测试补强,以及跟踪文档中的 `RestoreFallbackFolders=""` 可复制性问题 +- `RP-024` 使用 `$gframework-pr-review` 继续复核 PR #269 latest-head unresolved threads,确认 `EasyEvents` 异常契约、 + `SchemaConfigGenerator` 取消传播与 `ContextAwareGenerator` 快照冲突线程均已在本地收口,仅剩 `Cqrs` error type + 直接引用与根 schema `type` 非字符串防御仍成立;现已补齐实现与回归测试 - 当前工作树分支 `fix/analyzer-warning-reduction-batch` 已在 `ai-plan/public/README.md` 建立 topic 映射 ## 当前风险 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 e1b62714..30e331f3 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,37 @@ # Analyzer Warning Reduction 追踪 +## 2026-04-22 — RP-024 + +### 阶段:PR #269 第四轮 review follow-up 收口(RP-024) + +- 启动复核: + - 延续 `$gframework-pr-review` 对 PR #269 latest-head unresolved threads 的复核,重点核对最新 5 个未解决线程是否仍与当前 + worktree 一致 + - 本地确认 `EasyEvents` 异常契约、`SchemaConfigGenerator` 取消传播与 `ContextAwareGenerator` 字段冲突线程已是陈旧信号, + 真正仍成立的仅剩 `CqrsHandlerRegistryGenerator` 的 Roslyn error type 直接引用,以及根 schema `type` 非字符串时的 + `GetString()` 防御 +- 决策: + - `CqrsHandlerRegistryGenerator` 保持现有“优先精确重建、必要时退回运行时查找”的设计,不引入新的程序集级 fallback 契约分支; + 只在 `CanReferenceFromGeneratedRegistry(...)` 中显式拒绝 `TypeKind.Error`,让未解析类型走已有运行时查找路径 + - `SchemaConfigGenerator` 继续沿用现有 `GF_ConfigSchema_002` 诊断,不新增诊断 ID;仅在根对象校验入口补上 + `JsonValueKind.String` 前置判断 +- 实施调整: + - 为 `CqrsHandlerRegistryGenerator.RuntimeTypeReferences` 增加 `TypeKind.Error` 防御,避免把未解析类型写成生成代码里的 + `typeof(...)` + - 为 `SchemaConfigGeneratorTests` 补上根 `type` 为数字时返回 `GF_ConfigSchema_002` 的回归测试 + - 为 `CqrsHandlerRegistryGeneratorTests` 补上未解析 error type 会改走运行时 `GetType(...)` 精确查找的回归测试 +- 验证结果: + - `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -v minimal` + - 结果:`0 Warning(s)`,`0 Error(s)` + - `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -v minimal` + - 结果:通过;仍保留既有 `9` 条 `SchemaConfigGenerator.cs` `MA0051` + - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~SchemaConfigGeneratorTests.Run_Should_Report_Diagnostic_When_Root_Type_Metadata_Is_Not_A_String|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Runtime_Type_Lookup_When_Handler_Contract_Contains_Unresolved_Error_Types" -m:1 -p:RestoreFallbackFolders="" -v minimal` + - 结果:`2 Passed`,`0 Failed` + - 说明:测试命令需在无沙箱环境下运行,因为当前 test host 在沙箱内创建本地 socket 会收到 `Permission denied` +- 下一步建议: + - 若继续压缩 PR #269 的 review backlog,可再次抓取最新 unresolved threads,确认 GitHub 上仅剩陈旧线程后再决定是否继续代码改动 + - 若回到 analyzer 主线,继续推进 `SchemaConfigGenerator.cs` 剩余 `MA0051` + ## 2026-04-22 — RP-023 ### 阶段:PR #269 第三轮 review follow-up 收口(RP-023) From 050f4321c6d4e27df46f67a8bb3a09bdc06eb43b Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 23 Apr 2026 07:34:31 +0800 Subject: [PATCH 10/13] =?UTF-8?q?fix(source-generators):=20=E6=94=B6?= =?UTF-8?q?=E5=8F=A3PR269=E5=89=A9=E4=BD=99review=E4=B8=8E=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E8=A7=84=E8=8C=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 SchemaConfigGenerator 的归一化字段名冲突诊断,并补充对应 generator 回归测试 - 修复 CqrsHandlerRegistryGenerator 对 dynamic 的运行时类型引用,避免生成非法 typeof(dynamic) - 更新 AGENTS 与 analyzer-warning-reduction 跟踪,明确受影响模块必须独立 build 并处理或显式报告 warning --- AGENTS.md | 6 + ...RegistryGenerator.RuntimeTypeReferences.cs | 14 ++- .../AnalyzerReleases.Unshipped.md | 1 + .../Config/SchemaConfigGenerator.cs | 113 ++++++++++++++++-- .../Diagnostics/ConfigSchemaDiagnostics.cs | 11 ++ .../Config/SchemaConfigGeneratorTests.cs | 43 +++++++ .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 98 ++++++++++++++- .../analyzer-warning-reduction-tracking.md | 17 ++- .../analyzer-warning-reduction-trace.md | 36 ++++++ 9 files changed, 319 insertions(+), 20 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 27a2395f..625a569d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,6 +31,12 @@ All AI agents and contributors must follow these rules when writing, reviewing, - Every completed task MUST pass at least one build validation before it is considered done. - 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 + module/project instead of relying on an unrelated project or solution slice that does not actually compile the touched + code. +- Warnings reported by those affected-module builds are part of the task scope. Contributors MUST resolve the touched + module's build warnings in the same change, or stop and explicitly report the exact warning IDs and blocker instead of + deferring them to a separate long-lived cleanup branch by default. - If the required build passes and there are task-related staged or unstaged changes, contributors MUST create a Git commit automatically instead of leaving the task uncommitted, unless the user explicitly says not to commit. - Commit messages MUST use Conventional Commits format: `(): `. diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.RuntimeTypeReferences.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.RuntimeTypeReferences.cs index ddc9cb37..bce72464 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.RuntimeTypeReferences.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.RuntimeTypeReferences.cs @@ -81,6 +81,14 @@ public sealed partial class CqrsHandlerRegistryGenerator return false; } + // Roslyn models dynamic as a pseudo-type, but generated C# cannot emit typeof(dynamic). + // Normalize it to the CLR runtime type so precise reflected registrations stay compilable. + if (type.TypeKind == TypeKind.Dynamic) + { + runtimeTypeReference = RuntimeTypeReferenceSpec.FromDirectReference("global::System.Object"); + return true; + } + if (CanReferenceFromGeneratedRegistry(compilation, type)) { runtimeTypeReference = RuntimeTypeReferenceSpec.FromDirectReference( @@ -255,7 +263,7 @@ public sealed partial class CqrsHandlerRegistryGenerator { // Roslyn error symbols stringify to unresolved type names; emitting them via typeof(...) would turn // an existing user-code error into a second generator-produced compile error instead of falling back. - if (type.TypeKind == TypeKind.Error) + if (type.TypeKind is TypeKind.Error or TypeKind.Dynamic) return false; switch (type) @@ -279,8 +287,8 @@ public sealed partial class CqrsHandlerRegistryGenerator case ITypeParameterSymbol: return false; default: - // Treat other Roslyn type kinds, such as dynamic, as referenceable for now. - // If a real-world case proves unsafe, tighten this branch instead of broadening the named-type path above. + // Remaining Roslyn type kinds that reach this branch have already been normalized by earlier guards + // and can continue through the direct-reference path without emitting fallback reflection code. return true; } } diff --git a/GFramework.Game.SourceGenerators/AnalyzerReleases.Unshipped.md b/GFramework.Game.SourceGenerators/AnalyzerReleases.Unshipped.md index 333f7249..15a93c31 100644 --- a/GFramework.Game.SourceGenerators/AnalyzerReleases.Unshipped.md +++ b/GFramework.Game.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -18,3 +18,4 @@ GF_ConfigSchema_011 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics GF_ConfigSchema_012 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics GF_ConfigSchema_013 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics + GF_ConfigSchema_014 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics diff --git a/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs b/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs index dabfac77..60664a38 100644 --- a/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs +++ b/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs @@ -356,20 +356,16 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator } var properties = new List(); - foreach (var property in propertiesElement.EnumerateObject()) - { - var parsedProperty = ParseProperty( + if (!TryParseObjectProperties( filePath, - property, - requiredProperties.Contains(property.Name), - CombinePath(displayPath, property.Name), - isDirectChildOfRoot: isRoot); - if (parsedProperty.Diagnostic is not null) - { - return ParsedObjectResult.FromDiagnostic(parsedProperty.Diagnostic); - } - - properties.Add(parsedProperty.Property!); + displayPath, + isRoot, + propertiesElement, + requiredProperties, + properties, + out var propertyDiagnostic)) + { + return ParsedObjectResult.FromDiagnostic(propertyDiagnostic!); } return ParsedObjectResult.FromObject(new SchemaObjectSpec( @@ -381,6 +377,60 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator properties)); } + /// + /// 解析对象 schema 的直接子属性,并在进入代码发射前阻止归一化后的属性名冲突落入生成输出。 + /// + /// Schema 文件路径。 + /// 当前对象的逻辑路径。 + /// 当前对象是否为根对象。 + /// 对象的 properties JSON 节点。 + /// 当前对象声明的必填字段集合。 + /// 成功时返回的已解析属性列表。 + /// 解析失败时返回的首个诊断。 + /// 当所有属性都可安全生成时返回 + private static bool TryParseObjectProperties( + string filePath, + string displayPath, + bool isRoot, + JsonElement propertiesElement, + ISet requiredProperties, + ICollection properties, + out Diagnostic? diagnostic) + { + var schemaKeyByGeneratedPropertyName = new Dictionary(StringComparer.Ordinal); + foreach (var property in propertiesElement.EnumerateObject()) + { + var propertyDisplayPath = CombinePath(displayPath, property.Name); + var parsedProperty = ParseProperty( + filePath, + property, + requiredProperties.Contains(property.Name), + propertyDisplayPath, + isDirectChildOfRoot: isRoot); + if (parsedProperty.Diagnostic is not null) + { + diagnostic = parsedProperty.Diagnostic; + return false; + } + + if (!TryRegisterGeneratedPropertyName( + filePath, + propertyDisplayPath, + property.Name, + parsedProperty.Property!.PropertyName, + schemaKeyByGeneratedPropertyName, + out diagnostic)) + { + return false; + } + + properties.Add(parsedProperty.Property!); + } + + diagnostic = null; + return true; + } + /// /// 解析单个 schema 属性定义。 /// @@ -4052,6 +4102,43 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator return false; } + /// + /// 记录同一对象节点内已分配的生成属性名,并在 schema key 归一化后发生冲突时返回明确诊断。 + /// 该校验会在生成器进入源码发射前阻止重复属性、查询方法与索引成员名落入后续编译阶段。 + /// + /// Schema 文件路径。 + /// 当前字段的逻辑路径。 + /// 当前字段的原始 schema key。 + /// 当前字段归一化后的 CLR 属性名。 + /// 同一对象内已分配的属性名与原始 schema key 对照表。 + /// 检测到冲突时返回的诊断。 + /// 当生成属性名在当前对象作用域内唯一时返回 + private static bool TryRegisterGeneratedPropertyName( + string filePath, + string displayPath, + string schemaName, + string propertyName, + IDictionary schemaKeyByGeneratedPropertyName, + out Diagnostic? diagnostic) + { + if (!schemaKeyByGeneratedPropertyName.TryGetValue(propertyName, out var existingSchemaName)) + { + schemaKeyByGeneratedPropertyName.Add(propertyName, schemaName); + diagnostic = null; + return true; + } + + diagnostic = Diagnostic.Create( + ConfigSchemaDiagnostics.DuplicateGeneratedIdentifier, + CreateFileLocation(filePath), + Path.GetFileName(filePath), + displayPath, + schemaName, + propertyName, + existingSchemaName); + return false; + } + /// /// 将 schema 文件名派生的根实体名验证为生成代码可直接使用的根类型标识符。 /// 这里与属性名验证保持一致,避免文件名中的前导数字或其他非法字符把根配置类型/表类型生成为无效 C# 标识符。 diff --git a/GFramework.Game.SourceGenerators/Diagnostics/ConfigSchemaDiagnostics.cs b/GFramework.Game.SourceGenerators/Diagnostics/ConfigSchemaDiagnostics.cs index e2071f54..adbbdd41 100644 --- a/GFramework.Game.SourceGenerators/Diagnostics/ConfigSchemaDiagnostics.cs +++ b/GFramework.Game.SourceGenerators/Diagnostics/ConfigSchemaDiagnostics.cs @@ -151,4 +151,15 @@ public static class ConfigSchemaDiagnostics SourceGeneratorsConfigCategory, DiagnosticSeverity.Error, true); + + /// + /// schema 字段名在标识符归一化后发生冲突。 + /// + public static readonly DiagnosticDescriptor DuplicateGeneratedIdentifier = new( + "GF_ConfigSchema_014", + "Config schema property names collide after C# identifier normalization", + "Property '{1}' in schema file '{0}' uses schema key '{2}', which generates duplicate C# identifier '{3}' already produced by schema key '{4}'", + SourceGeneratorsConfigCategory, + DiagnosticSeverity.Error, + true); } diff --git a/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs index f1f5afb1..c895fbfb 100644 --- a/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs @@ -1975,6 +1975,49 @@ public class SchemaConfigGeneratorTests }); } + /// + /// 验证同一对象内不同 schema key 若归一化后映射到同一属性名,会在生成前直接给出冲突诊断。 + /// + [Test] + public void Run_Should_Report_Diagnostic_When_Schema_Keys_Collide_After_Identifier_Normalization() + { + const string source = """ + namespace TestApp + { + public sealed class Dummy + { + } + } + """; + + const string schema = """ + { + "type": "object", + "required": ["id"], + "properties": { + "id": { "type": "integer" }, + "foo-bar": { "type": "string" }, + "foo_bar": { "type": "string" } + } + } + """; + + var result = SchemaGeneratorTestDriver.Run( + source, + ("monster.schema.json", schema)); + + var diagnostic = result.Results.Single().Diagnostics.Single(); + + Assert.Multiple(() => + { + Assert.That(diagnostic.Id, Is.EqualTo("GF_ConfigSchema_014")); + Assert.That(diagnostic.Severity, Is.EqualTo(DiagnosticSeverity.Error)); + Assert.That(diagnostic.GetMessage(), Does.Contain("foo_bar")); + Assert.That(diagnostic.GetMessage(), Does.Contain("FooBar")); + Assert.That(diagnostic.GetMessage(), Does.Contain("foo-bar")); + }); + } + /// /// 验证 schema 顶层允许通过元数据覆盖默认配置目录,并会统一路径分隔符。 /// diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index 311e4f65..cb4ceaa8 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -1463,12 +1463,106 @@ public class CqrsHandlerRegistryGeneratorTests Assert.That(generatedCompilationErrors, Is.Empty); Assert.That(generatorErrors, Is.Empty); Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1)); + Assert.That(execution.GeneratedSources[0].filename, Is.EqualTo("CqrsHandlerRegistry.g.cs")); + var generatedSource = execution.GeneratedSources[0].content; Assert.That( - execution.GeneratedSources[0].content, + generatedSource, Does.Contain("registryAssembly.GetType(\"MissingResponse\", throwOnError: false, ignoreCase: false);")); Assert.That( - execution.GeneratedSources[0].content, + generatedSource, Does.Contain("internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry")); + Assert.That(generatedSource, Does.Not.Contain("typeof(MissingResponse)")); + Assert.That(generatedSource, Does.Not.Contain("CqrsReflectionFallbackAttribute(")); + }); + } + + /// + /// 验证 响应类型会在生成阶段归一化为 , + /// 避免注册器发射非法的 typeof(dynamic)。 + /// + [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 inputCompilationErrors = execution.InputCompilationDiagnostics + .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) + .ToArray(); + var generatedCompilationErrors = execution.GeneratedCompilationDiagnostics + .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) + .ToArray(); + var generatorErrors = execution.GeneratorDiagnostics + .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) + .ToArray(); + + Assert.Multiple(() => + { + Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS1966")); + Assert.That(generatedCompilationErrors, Is.Empty); + Assert.That(generatorErrors, Is.Empty); + Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1)); + Assert.That(execution.GeneratedSources[0].filename, Is.EqualTo("CqrsHandlerRegistry.g.cs")); + var generatedSource = execution.GeneratedSources[0].content; + Assert.That(generatedSource, Does.Contain("typeof(global::System.Object)")); + Assert.That(generatedSource, Does.Not.Contain("typeof(dynamic)")); + Assert.That(generatedSource, Does.Contain("internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry")); }); } 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 3fe51813..4c5fc755 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 @@ -7,8 +7,8 @@ ## 当前恢复点 -- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-024` -- 当前阶段:`Phase 24` +- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-025` +- 当前阶段:`Phase 25` - 当前焦点: - 已完成 `GFramework.Core` 当前 `MA0016` / `MA0002` / `MA0015` / `MA0077` 低风险收口批次 - 已复核 `net10.0` 下的 `MA0158` 基线:`GFramework.Core` / `GFramework.Cqrs` 当前共有 `16` 个 object lock @@ -27,6 +27,11 @@ 并补齐 `LoggingConfigurationTests`、`CollectionExtensionsTests`、`Cqrs` helper 抽取与 `ai-plan` 命令文本修正 - 已完成当前 PR #269 第四轮 follow-up:将 `CqrsHandlerRegistryGenerator` 的 Roslyn error type 直接引用改为 运行时精确查找路径,并为 `SchemaConfigGenerator` 补上根 `type` 非字符串时的防御与回归测试 + - 已完成当前 PR #269 第五轮 follow-up:`SchemaConfigGenerator` 补上归一化后属性名冲突诊断并新增 + `GF_ConfigSchema_014`,`CqrsHandlerRegistryGenerator` 将 `dynamic` 归一化为 `global::System.Object`, + 同时收紧相关 generator regression tests + - 已更新 `AGENTS.md`:变更模块必须运行对应 `dotnet build -c Release`,并处理或显式报告模块构建 warning, + 不再默认留给长期 warning 清理分支 - `CoroutineScheduler` 的 tag/group 字典已显式使用 `StringComparer.Ordinal`,保持既有区分大小写语义 - `EasyEvents.AddEvent()` 的重复注册路径已恢复为 `ArgumentException`,以保持既有异常契约 - `Option` 已声明 `IEquatable>`,与已有强类型 `Equals(Option)` 契约对齐 @@ -63,6 +68,8 @@ 并恢复 `EasyEvents` / `CollectionExtensions` / logging 配置模型的公共 API 兼容形状 - 已完成当前 PR #269 的第四轮 review follow-up:确认 5 个 latest-head 未解决线程中仅剩 2 个本地仍成立, 已分别在 `CqrsHandlerRegistryGenerator` 与 `SchemaConfigGenerator` 中收口,并补齐定向 generator regression tests +- 已完成当前 PR #269 的第五轮 review follow-up:收口 `SchemaConfigGenerator` 的归一化字段名冲突诊断、 + `CqrsHandlerRegistryGenerator` 的 `dynamic` 类型引用风险,并同步更新 `AGENTS.md` 的模块 build / warning 治理规范 - 已完成 `GFramework.Game.SourceGenerators` 中 `SchemaConfigGenerator` 的第一批 `MA0051` 收口;warnings-only 基线剩余 `9` 条 `MA0051` @@ -117,6 +124,9 @@ - `RP-024` 使用 `$gframework-pr-review` 继续复核 PR #269 latest-head unresolved threads,确认 `EasyEvents` 异常契约、 `SchemaConfigGenerator` 取消传播与 `ContextAwareGenerator` 快照冲突线程均已在本地收口,仅剩 `Cqrs` error type 直接引用与根 schema `type` 非字符串防御仍成立;现已补齐实现与回归测试 +- `RP-025` 继续复核 PR #269 剩余 outside-diff / nitpick 信号后,确认本地仍成立的是 `SchemaConfigGenerator` + 的归一化字段名冲突与 `Cqrs` 对 `dynamic` 的直接类型引用;已分别补上诊断、运行时类型归一化与回归测试, + 并把“变更模块必须运行对应 build 且处理 warning”的治理规则写回 `AGENTS.md` - 当前工作树分支 `fix/analyzer-warning-reduction-batch` 已在 `ai-plan/public/README.md` 建立 topic 映射 ## 当前风险 @@ -132,6 +142,9 @@ - source generator warning 外溢风险:运行 `GFramework.SourceGenerators.Tests` 会构建相邻 generator/test 项目并显示既有 `GFramework.Game.SourceGenerators` 与测试项目 warning - 缓解措施:继续以被修改 generator 项目的独立 warnings-only build 作为主验收,并用 focused generator test 验证行为 +- source generator test warning 治理风险:`GFramework.SourceGenerators.Tests` 当前仍有既有 `MA0051` / `MA0004` / `MA0048` + warning,本轮 focused test 已通过,但测试项目整包 warning 尚未进入本轮写集 + - 缓解措施:后续若继续修改该测试项目,应按新增 `AGENTS.md` 规则先跑其独立 build,并在进入下一轮实现前明确 warning 收口范围 - Godot 资产文件环境风险:当前 worktree 的 `GFramework.Godot` restore/build 仍会命中 Windows fallback package folder - 缓解措施:后续若继续触达 Godot 模块,先用 Linux 侧 restore 资产或 Windows-hosted 构建链刷新该项目,再补跑定向 build - 并行实现风险:批量收敛时若 subagent 写入边界不清晰,容易引入命名冲突或重复重构 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 30e331f3..88679333 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,41 @@ # Analyzer Warning Reduction 追踪 +## 2026-04-23 — RP-025 + +### 阶段:PR #269 第五轮 review follow-up 与模块 build / warning 治理补充(RP-025) + +- 启动复核: + - 继续使用 `$gframework-pr-review` 读取 PR #269 当前 latest review、outside-diff comment、nitpick comment 与 open-thread 摘要 + - 本地核对后确认 `SchemaConfigGenerator` 的取消传播、根 `type` 非字符串防御、`ContextAware` 冲突快照与 + `Cqrs` error type 线程均已是陈旧信号;仍成立的是归一化字段名冲突与 `dynamic` 运行时类型引用问题 +- 决策: + - `SchemaConfigGenerator` 不复用 `GF_ConfigSchema_006`,改为新增专门的冲突诊断 `GF_ConfigSchema_014`, + 避免把“标识符非法”和“归一化后重名”混成同一类错误 + - `CqrsHandlerRegistryGenerator` 对 `dynamic` 采用“生成期归一化为 `global::System.Object`”策略,而不是退回更宽泛的 + fallback 路径,保持精确注册能力且避免发射 `typeof(dynamic)` + - `AGENTS.md` 增加模块级 build / warning 治理规则,要求后续改代码时必须对受影响模块跑 Release build,并处理或显式报告 warning +- 实施调整: + - 为 `SchemaConfigGenerator` 增加对象级生成属性名登记 helper,在 `ParseObjectSpec(...)` 中拦截 `foo-bar` / + `foo_bar` 这类归一化后冲突,并新增 `ConfigSchemaDiagnostics.DuplicateGeneratedIdentifier` + - 为 `SchemaConfigGeneratorTests` 补上冲突诊断回归测试;为 `CqrsHandlerRegistryGeneratorTests` 收紧 + unresolved-type 断言并新增 `dynamic` 类型归一化回归测试 + - 为 `CqrsHandlerRegistryGenerator.RuntimeTypeReferences` 增加 `TypeKind.Dynamic` 归一化处理,并保持 + `TypeKind.Error` 的保守回退 + - 为 `AGENTS.md` 补充“受影响模块必须独立 build 且 warning 不能默认甩给长期分支”的硬性规范 +- 验证结果: + - `dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders="" -nologo` + - 结果:通过;并行 restore 时出现一次共享 `obj` 文件已存在的竞争噪音,串行验证后未再复现 + - `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -clp:"Summary;WarningsOnly" -nologo` + - 结果:`0 Warning(s)`,`0 Error(s)` + - `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -clp:"Summary;WarningsOnly" -nologo` + - 结果:`9 Warning(s)`,`0 Error(s)`;维持既有 `SchemaConfigGenerator.cs` `MA0051` 基线,未新增 warning + - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~Run_Should_Report_Diagnostic_When_Schema_Keys_Collide_After_Identifier_Normalization|FullyQualifiedName~Emits_Object_Type_Reference_When_Handler_Response_Uses_Dynamic|FullyQualifiedName~Emits_Runtime_Type_Lookup_When_Handler_Contract_Contains_Unresolved_Error_Types" -m:1 -p:RestoreFallbackFolders="" -nologo` + - 结果:`3 Passed`,`0 Failed` + - 说明:测试项目构建仍打印既有 `MA0051` / `MA0004` / `MA0048` warning,不属于本轮 generator 模块写集,但已在 tracking 风险中记录 +- 下一步建议: + - 若继续收口 PR #269,可再次抓取最新 unresolved threads,确认 GitHub 上剩余 open thread 是否全部转为陈旧信号 + - 若回到 analyzer 主线,继续推进 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs` 剩余 `MA0051` + ## 2026-04-22 — RP-024 ### 阶段:PR #269 第四轮 review follow-up 收口(RP-024) From 2fc8442bd4263a155091464e31da573b0b2fe7b1 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 23 Apr 2026 08:51:38 +0800 Subject: [PATCH 11/13] =?UTF-8?q?fix(source-generators-tests):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8DPR269=E5=BC=95=E7=94=A8=E5=85=83=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=94=AF=E4=B8=80=E6=80=A7=E5=9B=9E=E5=BD=92=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修正 SchemaConfigGeneratorTests 的 reference metadata 唯一性用例,改用合法 schema 路径碰撞覆盖后缀分配逻辑 - 更新 analyzer-warning-reduction 跟踪与 trace,记录 PR #269 failed-test follow-up 和定向验证结果 --- .../Config/SchemaConfigGeneratorTests.cs | 22 +++++++++++----- .../analyzer-warning-reduction-tracking.md | 12 ++++++--- .../analyzer-warning-reduction-trace.md | 26 +++++++++++++++++++ 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs index c895fbfb..b5895819 100644 --- a/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs @@ -2473,7 +2473,7 @@ public class SchemaConfigGeneratorTests } /// - /// 验证引用元数据成员名在不同路径规范化后发生碰撞时,生成器仍会分配全局唯一的成员名。 + /// 验证引用元数据成员名在不同合法字段路径规范化后发生碰撞时,生成器仍会分配全局唯一的成员名。 /// [Test] public void Run_Should_Assign_Globally_Unique_Reference_Metadata_Member_Names() @@ -2534,12 +2534,21 @@ public class SchemaConfigGeneratorTests "required": ["id"], "properties": { "id": { "type": "integer" }, - "drop-items": { - "type": "array", - "items": { "type": "string" }, - "x-gframework-ref-table": "item" + "drop": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { "type": "string" }, + "x-gframework-ref-table": "item" + }, + "items1": { + "type": "string", + "x-gframework-ref-table": "item" + } + } }, - "drop_items": { + "dropItems": { "type": "array", "items": { "type": "string" }, "x-gframework-ref-table": "item" @@ -2568,6 +2577,7 @@ public class SchemaConfigGeneratorTests Assert.That(generatedSources.TryGetValue("MonsterConfigBindings.g.cs", out var bindingsSource), Is.True); Assert.That(bindingsSource, Does.Contain("public static readonly ReferenceMetadata DropItems =")); Assert.That(bindingsSource, Does.Contain("public static readonly ReferenceMetadata DropItems1 =")); + Assert.That(bindingsSource, Does.Contain("public static readonly ReferenceMetadata DropItems2 =")); Assert.That(bindingsSource, Does.Contain("public static readonly ReferenceMetadata DropItems11 =")); } 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 4c5fc755..b49a6d28 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 @@ -7,8 +7,8 @@ ## 当前恢复点 -- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-025` -- 当前阶段:`Phase 25` +- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-026` +- 当前阶段:`Phase 26` - 当前焦点: - 已完成 `GFramework.Core` 当前 `MA0016` / `MA0002` / `MA0015` / `MA0077` 低风险收口批次 - 已复核 `net10.0` 下的 `MA0158` 基线:`GFramework.Core` / `GFramework.Cqrs` 当前共有 `16` 个 object lock @@ -30,6 +30,9 @@ - 已完成当前 PR #269 第五轮 follow-up:`SchemaConfigGenerator` 补上归一化后属性名冲突诊断并新增 `GF_ConfigSchema_014`,`CqrsHandlerRegistryGenerator` 将 `dynamic` 归一化为 `global::System.Object`, 同时收紧相关 generator regression tests + - 已完成当前 PR #269 failed-test follow-up:修正 `SchemaConfigGeneratorTests` + `Run_Should_Assign_Globally_Unique_Reference_Metadata_Member_Names` 的测试输入,使其继续覆盖 + reference metadata 成员名全局去冲突,但不再依赖现已被 `GF_ConfigSchema_014` 拦截的非法同层 schema key 冲突 - 已更新 `AGENTS.md`:变更模块必须运行对应 `dotnet build -c Release`,并处理或显式报告模块构建 warning, 不再默认留给长期 warning 清理分支 - `CoroutineScheduler` 的 tag/group 字典已显式使用 `StringComparer.Ordinal`,保持既有区分大小写语义 @@ -70,6 +73,8 @@ 已分别在 `CqrsHandlerRegistryGenerator` 与 `SchemaConfigGenerator` 中收口,并补齐定向 generator regression tests - 已完成当前 PR #269 的第五轮 review follow-up:收口 `SchemaConfigGenerator` 的归一化字段名冲突诊断、 `CqrsHandlerRegistryGenerator` 的 `dynamic` 类型引用风险,并同步更新 `AGENTS.md` 的模块 build / warning 治理规范 +- 已完成当前 PR #269 的 failed-test follow-up:将 reference metadata 成员名唯一性回归测试改为合法 schema 路径组合, + 并重新通过定向 generator test - 已完成 `GFramework.Game.SourceGenerators` 中 `SchemaConfigGenerator` 的第一批 `MA0051` 收口;warnings-only 基线剩余 `9` 条 `MA0051` @@ -144,7 +149,8 @@ - 缓解措施:继续以被修改 generator 项目的独立 warnings-only build 作为主验收,并用 focused generator test 验证行为 - source generator test warning 治理风险:`GFramework.SourceGenerators.Tests` 当前仍有既有 `MA0051` / `MA0004` / `MA0048` warning,本轮 focused test 已通过,但测试项目整包 warning 尚未进入本轮写集 - - 缓解措施:后续若继续修改该测试项目,应按新增 `AGENTS.md` 规则先跑其独立 build,并在进入下一轮实现前明确 warning 收口范围 + - 缓解措施:本轮已在 failed-test follow-up 的定向 `dotnet test` 中再次确认这些 warning 仍为既有基线;后续若继续修改该测试项目, + 应按新增 `AGENTS.md` 规则先明确 warning 收口范围,再决定是否进入专门清理切片 - Godot 资产文件环境风险:当前 worktree 的 `GFramework.Godot` restore/build 仍会命中 Windows fallback package folder - 缓解措施:后续若继续触达 Godot 模块,先用 Linux 侧 restore 资产或 Windows-hosted 构建链刷新该项目,再补跑定向 build - 并行实现风险:批量收敛时若 subagent 写入边界不清晰,容易引入命名冲突或重复重构 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 88679333..a46e668a 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,31 @@ # Analyzer Warning Reduction 追踪 +## 2026-04-23 — RP-026 + +### 阶段:PR #269 failed-test follow-up(RP-026) + +- 启动复核: + - 使用 `$gframework-pr-review` 抓取当前分支 PR #269 的 test report,确认最新失败信号来自 + `SchemaConfigGeneratorTests.Run_Should_Assign_Globally_Unique_Reference_Metadata_Member_Names` + - 本地复测前先对 `GFramework.SourceGenerators.Tests` 执行 `dotnet restore -p:RestoreFallbackFolders=""`, + 规避当前 WSL worktree 仍残留的 Windows NuGet fallback package folder 资产干扰 +- 决策: + - 保持 `SchemaConfigGenerator` 当前 `GF_ConfigSchema_014` 语义不变;PR 失败是测试输入陈旧,而不是生成器行为回退 + - 将用例改写为“合法 schema 路径在 reference metadata member name 上碰撞”的场景,继续覆盖全局唯一后缀分配逻辑 +- 实施调整: + - 将测试 schema 从根级 `drop-items` / `drop_items` 非法同层冲突改为 `drop.items`、`drop.items1`、`dropItems`、 + `dropItems1` 的合法组合 + - 更新断言,验证 `MonsterConfigBindings.g.cs` 中继续生成 `DropItems`、`DropItems1`、`DropItems2` 与 `DropItems11` +- 验证结果: + - `dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders="" -nologo` + - 结果:通过 + - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~Run_Should_Assign_Globally_Unique_Reference_Metadata_Member_Names -m:1 -p:RestoreFallbackFolders="" -nologo` + - 结果:`1 Passed`,`0 Failed` + - 说明:`GFramework.SourceGenerators.Tests` 在构建阶段仍会打印既有 `MA0048`、`MA0051`、`MA0004` warning;本轮未扩展到该测试项目的 warning 清理 +- 下一步建议: + - 若继续收口 PR #269,可再次抓取最新 test report / open thread,确认是否还有新的 CI 失败信号 + - 若回到 analyzer 主线,优先决定是否为 `GFramework.SourceGenerators.Tests` 单独开一轮 warning 清理切片 + ## 2026-04-23 — RP-025 ### 阶段:PR #269 第五轮 review follow-up 与模块 build / warning 治理补充(RP-025) From ba3a4d4a3732b32ebab3f28387fac60e578c397c Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 23 Apr 2026 09:31:31 +0800 Subject: [PATCH 12/13] =?UTF-8?q?fix(core-source-generators):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8DContextAware=E7=BB=A7=E6=89=BF=E6=88=90=E5=91=98?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E5=86=B2=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 ContextAwareGenerator 的保留名收集逻辑,使 _gFrameworkContextAware* 字段分配覆盖完整基类链 - 新增 inherited collision 快照回归测试与快照文件,锁定基类占用字段名时的后缀回退行为 - 更新 analyzer-warning-reduction 跟踪与 trace,记录本轮 Greptile follow-up 与验证结果 --- .../Rule/ContextAwareGenerator.cs | 30 +- .../ContextAwareGeneratorSnapshotTests.cs | 286 ++++++++++-------- .../InheritedCollisionRule.ContextAware.g.cs | 97 ++++++ .../analyzer-warning-reduction-tracking.md | 11 +- .../analyzer-warning-reduction-trace.md | 25 ++ 5 files changed, 310 insertions(+), 139 deletions(-) create mode 100644 GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/InheritedCollisionRule.ContextAware.g.cs diff --git a/GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs b/GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs index fff89667..0513d3de 100644 --- a/GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs +++ b/GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs @@ -353,11 +353,7 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase /// 当前生成轮次应使用的上下文字段名集合。 private static GeneratedContextMemberNames CreateGeneratedContextMemberNames(INamedTypeSymbol symbol) { - var reservedNames = new HashSet( - symbol.GetMembers() - .Where(static member => !member.IsImplicitlyDeclared) - .Select(static member => member.Name), - StringComparer.Ordinal); + var reservedNames = CollectReservedContextMemberNames(symbol); return new GeneratedContextMemberNames( AllocateGeneratedMemberName(reservedNames, "_gFrameworkContextAwareContext"), @@ -365,6 +361,30 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase AllocateGeneratedMemberName(reservedNames, "_gFrameworkContextAwareSync")); } + /// + /// 收集当前类型及其基类链上所有显式声明的成员名,确保生成字段不会意外隐藏继承成员。 + /// + /// 当前需要补充 ContextAware 实现的目标类型。 + /// 已被当前类型层级占用的成员名集合。 + private static HashSet CollectReservedContextMemberNames(INamedTypeSymbol symbol) + { + var reservedNames = new HashSet(StringComparer.Ordinal); + + // Walk the full inheritance chain so numeric suffix allocation also covers members introduced by base types. + for (var currentType = symbol; currentType is not null; currentType = currentType.BaseType) + { + foreach (var member in currentType.GetMembers()) + { + if (!member.IsImplicitlyDeclared) + { + reservedNames.Add(member.Name); + } + } + } + + return reservedNames; + } + /// /// 在固定前缀基础上按顺序追加数字后缀,直到找到可安全写入的成员名。 /// diff --git a/GFramework.SourceGenerators.Tests/Rule/ContextAwareGeneratorSnapshotTests.cs b/GFramework.SourceGenerators.Tests/Rule/ContextAwareGeneratorSnapshotTests.cs index e9f8510b..cf347b7c 100644 --- a/GFramework.SourceGenerators.Tests/Rule/ContextAwareGeneratorSnapshotTests.cs +++ b/GFramework.SourceGenerators.Tests/Rule/ContextAwareGeneratorSnapshotTests.cs @@ -1,4 +1,6 @@ -using System.IO; +using System; +using System.IO; +using System.Linq; using GFramework.Core.SourceGenerators.Rule; using GFramework.SourceGenerators.Tests.Core; @@ -11,6 +13,60 @@ namespace GFramework.SourceGenerators.Tests.Rule; [TestFixture] public class ContextAwareGeneratorSnapshotTests { + private const string SharedContextAwareInfrastructure = """ + using System; + + namespace GFramework.Core.SourceGenerators.Abstractions.Rule + { + [AttributeUsage(AttributeTargets.Class)] + public sealed class ContextAwareAttribute : Attribute { } + } + + namespace GFramework.Core.Abstractions.Rule + { + public interface IContextAware + { + void SetContext( + GFramework.Core.Abstractions.Architectures.IArchitectureContext context); + + GFramework.Core.Abstractions.Architectures.IArchitectureContext GetContext(); + } + } + + namespace GFramework.Core.Abstractions.Architectures + { + public interface IArchitectureContext { } + + public interface IArchitectureContextProvider + { + IArchitectureContext GetContext(); + bool TryGetContext(out T? context) where T : class, IArchitectureContext; + } + } + + namespace GFramework.Core.Architectures + { + using GFramework.Core.Abstractions.Architectures; + + public sealed class GameContextProvider : IArchitectureContextProvider + { + public IArchitectureContext GetContext() => null; + public bool TryGetContext(out T? context) where T : class, IArchitectureContext + { + context = null; + return false; + } + } + """; + + private const string GameContextHelperSource = """ + + public static class GameContext + { + public static IArchitectureContext GetFirstArchitectureContext() => null; + } + """; + /// /// 测试ContextAwareGenerator源代码生成器的快照功能 /// 验证生成器对带有ContextAware特性的类的处理结果 @@ -19,73 +75,16 @@ public class ContextAwareGeneratorSnapshotTests [Test] public async Task Snapshot_ContextAwareGenerator() { - // 定义测试用的源代码,包含ContextAware特性和相关接口定义 - const string source = """ - using System; - - namespace GFramework.Core.SourceGenerators.Abstractions.Rule - { - [AttributeUsage(AttributeTargets.Class)] - public sealed class ContextAwareAttribute : Attribute { } - } - - namespace GFramework.Core.Abstractions.Rule - { - public interface IContextAware - { - void SetContext( - GFramework.Core.Abstractions.Architectures.IArchitectureContext context); - - GFramework.Core.Abstractions.Architectures.IArchitectureContext GetContext(); - } - } - - namespace GFramework.Core.Abstractions.Architectures - { - public interface IArchitectureContext { } - - public interface IArchitectureContextProvider - { - IArchitectureContext GetContext(); - bool TryGetContext(out T? context) where T : class, IArchitectureContext; - } - } - - namespace GFramework.Core.Architectures - { - using GFramework.Core.Abstractions.Architectures; - - public sealed class GameContextProvider : IArchitectureContextProvider - { - public IArchitectureContext GetContext() => null; - public bool TryGetContext(out T? context) where T : class, IArchitectureContext - { - context = null; - return false; - } - } - - public static class GameContext - { - public static IArchitectureContext GetFirstArchitectureContext() => null; - } - } - - namespace TestApp - { - using GFramework.Core.SourceGenerators.Abstractions.Rule; - using GFramework.Core.Abstractions.Rule; - - [ContextAware] - public partial class MyRule : IContextAware - { - } - } - """; - // 执行生成器快照测试,将生成的代码与预期快照进行比较 await GeneratorSnapshotTest.RunAsync( - source, + CreateContextAwareTestSource( + """ + [ContextAware] + public partial class MyRule : IContextAware + { + } + """, + includeGameContextHelper: true), GetSnapshotFolder()); } @@ -96,76 +95,99 @@ public class ContextAwareGeneratorSnapshotTests [Test] public async Task Snapshot_ContextAwareGenerator_With_User_Field_Name_Collisions() { - const string source = """ - using System; - - namespace GFramework.Core.SourceGenerators.Abstractions.Rule - { - [AttributeUsage(AttributeTargets.Class)] - public sealed class ContextAwareAttribute : Attribute { } - } - - namespace GFramework.Core.Abstractions.Rule - { - public interface IContextAware - { - void SetContext( - GFramework.Core.Abstractions.Architectures.IArchitectureContext context); - - GFramework.Core.Abstractions.Architectures.IArchitectureContext GetContext(); - } - } - - namespace GFramework.Core.Abstractions.Architectures - { - public interface IArchitectureContext { } - - public interface IArchitectureContextProvider - { - IArchitectureContext GetContext(); - bool TryGetContext(out T? context) where T : class, IArchitectureContext; - } - } - - namespace GFramework.Core.Architectures - { - using GFramework.Core.Abstractions.Architectures; - - public sealed class GameContextProvider : IArchitectureContextProvider - { - public IArchitectureContext GetContext() => null; - public bool TryGetContext(out T? context) where T : class, IArchitectureContext - { - context = null; - return false; - } - } - } - - namespace TestApp - { - using GFramework.Core.SourceGenerators.Abstractions.Rule; - using GFramework.Core.Abstractions.Rule; - using GFramework.Core.Abstractions.Architectures; - - [ContextAware] - public partial class CollisionProneRule : IContextAware - { - private readonly string _context = "user-field"; - private static readonly string _contextProvider = "user-provider"; - private static readonly object _contextSync = new(); - private IArchitectureContext? _gFrameworkContextAwareContext; - private static IArchitectureContextProvider? _gFrameworkContextAwareProvider; - private static readonly object _gFrameworkContextAwareSync = new(); - } - } - """; - await GeneratorSnapshotTest.RunAsync( - source, + CreateContextAwareTestSource( + """ + using GFramework.Core.Abstractions.Architectures; + + [ContextAware] + public partial class CollisionProneRule : IContextAware + { + private readonly string _context = "user-field"; + private static readonly string _contextProvider = "user-provider"; + private static readonly object _contextSync = new(); + private IArchitectureContext? _gFrameworkContextAwareContext; + private static IArchitectureContextProvider? _gFrameworkContextAwareProvider; + private static readonly object _gFrameworkContextAwareSync = new(); + } + """), GetSnapshotFolder()); } + /// + /// 验证生成器在基类已经占用自动生成字段名时,也会为派生规则类型分配带后缀的唯一成员名。 + /// + /// 异步任务,无返回值。 + [Test] + public async Task Snapshot_ContextAwareGenerator_With_Inherited_Field_Name_Collisions() + { + await GeneratorSnapshotTest.RunAsync( + CreateContextAwareTestSource( + """ + using GFramework.Core.Abstractions.Architectures; + + public abstract class ContextAwareRuleBase + { + protected IArchitectureContext? _gFrameworkContextAwareContext; + protected static IArchitectureContextProvider? _gFrameworkContextAwareProvider; + protected static readonly object _gFrameworkContextAwareSync = new(); + } + + [ContextAware] + public partial class InheritedCollisionRule : ContextAwareRuleBase, IContextAware + { + } + """), + GetSnapshotFolder()); + } + + /// + /// 组装 ContextAwareGenerator 快照测试共用的最小宿主源码,避免每个用例都重复长块样板代码。 + /// + /// 放在 TestApp 命名空间内的测试类型声明。 + /// 是否额外包含兼容旧快照输入的 GameContext 帮助类型。 + /// 可直接交给生成器测试驱动的完整源码文本。 + private static string CreateContextAwareTestSource(string testTypeDeclarations, bool includeGameContextHelper = false) + { + var gameContextHelper = includeGameContextHelper ? GameContextHelperSource : string.Empty; + var testAppDeclarations = IndentBlock(testTypeDeclarations, 4); + + return string.Concat( + SharedContextAwareInfrastructure, + gameContextHelper, + """ + } + + namespace TestApp + { + using GFramework.Core.SourceGenerators.Abstractions.Rule; + using GFramework.Core.Abstractions.Rule; + + """, + testAppDeclarations, + """ + + } + """); + } + + /// + /// 为内嵌源码片段补齐缩进,使其能安全插入原始字符串模板中的命名空间块。 + /// + /// 要缩进的源码文本。 + /// 每行前要补齐的空格数。 + /// 已经补齐统一缩进的多行文本。 + private static string IndentBlock(string text, int spaces) + { + var indentation = new string(' ', spaces); + return string.Join( + Environment.NewLine, + text.Replace("\r\n", "\n", StringComparison.Ordinal) + .Trim() + .Split('\n') + .Select(line => indentation + line)); + } + /// /// 将运行时测试目录映射回仓库内已提交的上下文感知生成器快照目录。 /// diff --git a/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/InheritedCollisionRule.ContextAware.g.cs b/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/InheritedCollisionRule.ContextAware.g.cs new file mode 100644 index 00000000..bb4b08f7 --- /dev/null +++ b/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/InheritedCollisionRule.ContextAware.g.cs @@ -0,0 +1,97 @@ +// +#nullable enable + +namespace TestApp; + +/// +/// 为当前规则类型补充自动生成的架构上下文访问实现。 +/// +/// +/// 生成代码会在实例级缓存首次解析到的上下文,并在未显式配置提供者时回退到 。 +/// 同一生成类型的所有实例共享一个静态上下文提供者;切换或重置提供者只会影响尚未缓存上下文的新实例或未初始化实例, +/// 已缓存的实例上下文需要通过 显式覆盖。 +/// 与手动继承 的路径相比,生成实现会使用 _gFrameworkContextAwareSync1 协调惰性初始化、provider 切换和显式上下文注入; +/// 则保持无锁的实例级缓存语义,更适合已经由调用方线程模型保证串行访问的简单场景。 +/// +partial class InheritedCollisionRule : global::GFramework.Core.Abstractions.Rule.IContextAware +{ + private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _gFrameworkContextAwareContext1; + private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? _gFrameworkContextAwareProvider1; + private static readonly object _gFrameworkContextAwareSync1 = new(); + + /// + /// 获取当前实例绑定的架构上下文。 + /// + /// + /// 该属性会先返回通过 IContextAware.SetContext(...) 显式注入的实例上下文;若尚未设置,则在同一个同步域内惰性初始化共享提供者。 + /// 当静态提供者尚未配置时,生成代码会回退到 。 + /// 一旦某个实例成功缓存上下文,后续 + /// 或 不会自动清除此缓存;如需覆盖,请显式调用 IContextAware.SetContext(...)。 + /// 当前实现还假设 可在持有 _gFrameworkContextAwareSync1 时安全执行; + /// 自定义 provider 不应在该调用链内重新进入当前类型的 provider 配置 API,且应避免引入与外部全局锁相互等待的锁顺序。 + /// + protected global::GFramework.Core.Abstractions.Architectures.IArchitectureContext Context + { + get + { + // 在同一个同步域内协调懒加载与 provider 切换,避免读取到被并发重置的空提供者。 + // provider 的 GetContext() 会在持有 _gFrameworkContextAwareSync1 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。 + lock (_gFrameworkContextAwareSync1) + { + _gFrameworkContextAwareProvider1 ??= new global::GFramework.Core.Architectures.GameContextProvider(); + _gFrameworkContextAwareContext1 ??= _gFrameworkContextAwareProvider1.GetContext(); + return _gFrameworkContextAwareContext1; + } + } + } + + /// + /// 配置当前生成类型共享的上下文提供者。 + /// + /// 后续懒加载上下文时要使用的提供者实例。 + /// 为 null 时抛出。 + /// + /// 该方法使用与 相同的同步锁,避免提供者切换与惰性初始化交错。 + /// 已经缓存上下文的实例不会因为提供者切换而自动失效;该变更仅影响尚未初始化上下文的新实例或未缓存实例。 + /// 如需覆盖已有实例的上下文,请显式调用 IContextAware.SetContext(...)。 + /// + public static void SetContextProvider(global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider provider) + { + global::System.ArgumentNullException.ThrowIfNull(provider); + lock (_gFrameworkContextAwareSync1) + { + _gFrameworkContextAwareProvider1 = provider; + } + } + + /// + /// 重置共享上下文提供者,使后续懒加载回退到默认提供者。 + /// + /// + /// 该方法主要用于测试清理或跨用例恢复默认行为。 + /// 它不会清除已经缓存到实例字段中的上下文;只有后续尚未初始化上下文的实例会重新回退到 。 + /// 如需覆盖已有实例的上下文,请显式调用 IContextAware.SetContext(...)。 + /// + public static void ResetContextProvider() + { + lock (_gFrameworkContextAwareSync1) + { + _gFrameworkContextAwareProvider1 = null; + } + } + + void global::GFramework.Core.Abstractions.Rule.IContextAware.SetContext(global::GFramework.Core.Abstractions.Architectures.IArchitectureContext context) + { + // 与 Context getter 共享同一同步协议,避免显式注入被并发懒加载覆盖。 + lock (_gFrameworkContextAwareSync1) + { + _gFrameworkContextAwareContext1 = context; + } + } + + global::GFramework.Core.Abstractions.Architectures.IArchitectureContext global::GFramework.Core.Abstractions.Rule.IContextAware.GetContext() + { + return Context; + } + +} \ No newline at end of file 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 b49a6d28..a6587ab1 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 @@ -7,8 +7,8 @@ ## 当前恢复点 -- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-026` -- 当前阶段:`Phase 26` +- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-027` +- 当前阶段:`Phase 27` - 当前焦点: - 已完成 `GFramework.Core` 当前 `MA0016` / `MA0002` / `MA0015` / `MA0077` 低风险收口批次 - 已复核 `net10.0` 下的 `MA0158` 基线:`GFramework.Core` / `GFramework.Cqrs` 当前共有 `16` 个 object lock @@ -33,6 +33,8 @@ - 已完成当前 PR #269 failed-test follow-up:修正 `SchemaConfigGeneratorTests` `Run_Should_Assign_Globally_Unique_Reference_Metadata_Member_Names` 的测试输入,使其继续覆盖 reference metadata 成员名全局去冲突,但不再依赖现已被 `GF_ConfigSchema_014` 拦截的非法同层 schema key 冲突 + - 已完成当前 PR #269 Greptile follow-up:`ContextAwareGenerator` 现在会把基类链显式成员名也纳入 + `_gFrameworkContextAware*` 字段分配冲突检测,并新增 inherited-field collision 快照回归测试 - 已更新 `AGENTS.md`:变更模块必须运行对应 `dotnet build -c Release`,并处理或显式报告模块构建 warning, 不再默认留给长期 warning 清理分支 - `CoroutineScheduler` 的 tag/group 字典已显式使用 `StringComparer.Ordinal`,保持既有区分大小写语义 @@ -75,6 +77,8 @@ `CqrsHandlerRegistryGenerator` 的 `dynamic` 类型引用风险,并同步更新 `AGENTS.md` 的模块 build / warning 治理规范 - 已完成当前 PR #269 的 failed-test follow-up:将 reference metadata 成员名唯一性回归测试改为合法 schema 路径组合, 并重新通过定向 generator test +- 已完成当前 PR #269 的 Greptile follow-up:修复 `ContextAwareGenerator` 未覆盖基类成员名冲突的问题,并补齐 + inherited-collision 快照测试 - 已完成 `GFramework.Game.SourceGenerators` 中 `SchemaConfigGenerator` 的第一批 `MA0051` 收口;warnings-only 基线剩余 `9` 条 `MA0051` @@ -151,6 +155,9 @@ warning,本轮 focused test 已通过,但测试项目整包 warning 尚未进入本轮写集 - 缓解措施:本轮已在 failed-test follow-up 的定向 `dotnet test` 中再次确认这些 warning 仍为既有基线;后续若继续修改该测试项目, 应按新增 `AGENTS.md` 规则先明确 warning 收口范围,再决定是否进入专门清理切片 +- ContextAware 基类命名隐藏风险:若生成器只看当前类型声明成员,派生规则会重新占用基类已声明的 + `_gFrameworkContextAware*` 字段名,导致生成成员隐藏继承状态并让快照无法锁定后缀分配行为 + - 缓解措施:本轮已改为遍历完整 base-type 链收集保留名,并用 inherited collision 快照用例锁定该行为 - Godot 资产文件环境风险:当前 worktree 的 `GFramework.Godot` restore/build 仍会命中 Windows fallback package folder - 缓解措施:后续若继续触达 Godot 模块,先用 Linux 侧 restore 资产或 Windows-hosted 构建链刷新该项目,再补跑定向 build - 并行实现风险:批量收敛时若 subagent 写入边界不清晰,容易引入命名冲突或重复重构 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 a46e668a..4706a318 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,30 @@ # Analyzer Warning Reduction 追踪 +## 2026-04-23 — RP-027 + +### 阶段:PR #269 Greptile inherited-member collision follow-up(RP-027) + +- 启动复核: + - 根据用户补充,重新核对 `$gframework-pr-review` 抓下来的 `greptile-apps[bot]` unresolved 线程,确认仍有一条 + `ContextAwareGenerator` 关于 inherited member names 未参与 collision detection 的 P1 评论 + - 本地读取 `CreateGeneratedContextMemberNames(...)` 后确认当前实现只收集 `symbol.GetMembers()`,确实没有遍历基类链 +- 决策: + - 保持现有 `_gFrameworkContextAware*` 前缀和数字后缀分配规则不变,只把保留名集合扩展为“当前类型 + 基类链显式成员” + - 沿用既有 `ContextAwareGeneratorSnapshotTests` 模式,新增 inherited-field collision 快照,而不是只写松散字符串断言 +- 实施调整: + - 为 `ContextAwareGenerator` 新增 `CollectReservedContextMemberNames(...)` helper,遍历完整 `BaseType` 链收集显式成员名 + - 为 `ContextAwareGeneratorSnapshotTests` 增加 `InheritedCollisionRule` 场景,并抽出公共测试源码 helper,避免重复样板 + - 新增快照 `InheritedCollisionRule.ContextAware.g.cs`,锁定基类已声明 `_gFrameworkContextAware*` 时生成器会回退到 `...1` 后缀 +- 验证结果: + - `dotnet build GFramework.Core.SourceGenerators/GFramework.Core.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -clp:"Summary;WarningsOnly" -nologo` + - 结果:`0 Warning(s)`,`0 Error(s)` + - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~ContextAwareGeneratorSnapshotTests.Snapshot_ContextAwareGenerator_With_Inherited_Field_Name_Collisions|FullyQualifiedName~ContextAwareGeneratorSnapshotTests.Snapshot_ContextAwareGenerator_With_User_Field_Name_Collisions|FullyQualifiedName~ContextAwareGeneratorSnapshotTests.Snapshot_ContextAwareGenerator" -m:1 -p:RestoreFallbackFolders="" -nologo` + - 结果:`3 Passed`,`0 Failed` + - 说明:`GFramework.SourceGenerators.Tests` 仍打印既有 `MA0048`、`MA0051`、`MA0004` warning;本轮未扩大到测试项目 warning 清理 +- 下一步建议: + - 若继续收口 PR #269,可再次抓取最新 unresolved threads,确认 Greptile / CodeRabbit 当前是否只剩陈旧信号 + - 若继续推进 analyzer 主线,可单独评估 `GFramework.SourceGenerators.Tests` 的 warning 清理是否值得开新切片 + ## 2026-04-23 — RP-026 ### 阶段:PR #269 failed-test follow-up(RP-026) From b8c2ad42a9218dd0af87322a35662c6d6d580e9d Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 23 Apr 2026 09:39:27 +0800 Subject: [PATCH 13/13] =?UTF-8?q?fix(cqrs-source-generators):=20=E5=8C=96?= =?UTF-8?q?=E8=A7=A3=E6=B3=A8=E5=86=8C=E7=94=9F=E6=88=90=E5=99=A8=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E7=BA=A7=E5=86=B2=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 合并 main 在线上单文件版本新增的模型 XML 文档,并迁移到当前 partial 拆分后的 Models 文件 - 保留 CqrsHandlerRegistryGenerator 主文件的现有生成管线拆分,不回退已完成的结构调整 - 更新 analyzer-warning-reduction 跟踪与 trace,记录本轮冲突确认、合并策略与构建验证结果 --- .../CqrsHandlerRegistryGenerator.Models.cs | 30 +++++++++++++++++++ .../analyzer-warning-reduction-tracking.md | 9 ++++-- .../analyzer-warning-reduction-trace.md | 25 ++++++++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs index 0b3ec0c7..541ffcf6 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs @@ -32,6 +32,13 @@ public sealed partial class CqrsHandlerRegistryGenerator HasReflectionTypeLookups; } + /// + /// 标记某条 handler 注册语句在生成阶段采用的表达策略。 + /// + /// + /// 该枚举只服务于输出排序与代码分支选择,用来保证生成注册器在“直接注册” + /// “反射实现类型查找”和“精确运行时类型解析”之间保持稳定顺序。 + /// private enum OrderedRegistrationKind { Direct, @@ -39,6 +46,14 @@ public sealed partial class CqrsHandlerRegistryGenerator PreciseReflected } + /// + /// 描述生成注册器中某个运行时类型引用的构造方式。 + /// + /// + /// 某些 handler 服务类型可以直接以 typeof(...) 输出,某些则需要在运行时补充 + /// 反射查找、数组封装或泛型实参重建。该记录把这些差异收敛为统一的递归结构, + /// 供源码输出阶段生成稳定的类型解析语句。 + /// private sealed record RuntimeTypeReferenceSpec( string? TypeDisplayName, string? ReflectionTypeMetadataName, @@ -49,6 +64,9 @@ public sealed partial class CqrsHandlerRegistryGenerator RuntimeTypeReferenceSpec? GenericTypeDefinitionReference, ImmutableArray GenericTypeArguments) { + /// + /// 创建一个可直接通过 typeof(...) 表达的类型引用。 + /// public static RuntimeTypeReferenceSpec FromDirectReference(string typeDisplayName) { return new RuntimeTypeReferenceSpec( @@ -62,6 +80,9 @@ public sealed partial class CqrsHandlerRegistryGenerator ImmutableArray.Empty); } + /// + /// 创建一个需要从当前消费端程序集反射解析的类型引用。 + /// public static RuntimeTypeReferenceSpec FromReflectionLookup(string reflectionTypeMetadataName) { return new RuntimeTypeReferenceSpec( @@ -75,6 +96,9 @@ public sealed partial class CqrsHandlerRegistryGenerator ImmutableArray.Empty); } + /// + /// 创建一个需要从被引用程序集反射解析的类型引用。 + /// public static RuntimeTypeReferenceSpec FromExternalReflectionLookup( string reflectionAssemblyName, string reflectionTypeMetadataName) @@ -90,6 +114,9 @@ public sealed partial class CqrsHandlerRegistryGenerator ImmutableArray.Empty); } + /// + /// 创建一个数组类型引用。 + /// public static RuntimeTypeReferenceSpec FromArray(RuntimeTypeReferenceSpec elementTypeReference, int arrayRank) { return new RuntimeTypeReferenceSpec( @@ -103,6 +130,9 @@ public sealed partial class CqrsHandlerRegistryGenerator ImmutableArray.Empty); } + /// + /// 创建一个封闭泛型类型引用。 + /// public static RuntimeTypeReferenceSpec FromConstructedGeneric( RuntimeTypeReferenceSpec genericTypeDefinitionReference, ImmutableArray genericTypeArguments) 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 a6587ab1..bc42ecc2 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 @@ -7,8 +7,8 @@ ## 当前恢复点 -- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-027` -- 当前阶段:`Phase 27` +- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-028` +- 当前阶段:`Phase 28` - 当前焦点: - 已完成 `GFramework.Core` 当前 `MA0016` / `MA0002` / `MA0015` / `MA0077` 低风险收口批次 - 已复核 `net10.0` 下的 `MA0158` 基线:`GFramework.Core` / `GFramework.Cqrs` 当前共有 `16` 个 object lock @@ -35,6 +35,9 @@ reference metadata 成员名全局去冲突,但不再依赖现已被 `GF_ConfigSchema_014` 拦截的非法同层 schema key 冲突 - 已完成当前 PR #269 Greptile follow-up:`ContextAwareGenerator` 现在会把基类链显式成员名也纳入 `_gFrameworkContextAware*` 字段分配冲突检测,并新增 inherited-field collision 快照回归测试 + - 已完成当前分支与 `main` 的 `CqrsHandlerRegistryGenerator.cs` 文件级冲突收口:确认 `main` 侧新增的是 + `OrderedRegistrationKind` / `RuntimeTypeReferenceSpec` 的 XML 文档,现已按当前 partial 拆分结构迁移到 + `CqrsHandlerRegistryGenerator.Models.cs`,不回退已完成的生成器拆分 - 已更新 `AGENTS.md`:变更模块必须运行对应 `dotnet build -c Release`,并处理或显式报告模块构建 warning, 不再默认留给长期 warning 清理分支 - `CoroutineScheduler` 的 tag/group 字典已显式使用 `StringComparer.Ordinal`,保持既有区分大小写语义 @@ -79,6 +82,8 @@ 并重新通过定向 generator test - 已完成当前 PR #269 的 Greptile follow-up:修复 `ContextAwareGenerator` 未覆盖基类成员名冲突的问题,并补齐 inherited-collision 快照测试 +- 已完成当前分支与 `main` 的 `CqrsHandlerRegistryGenerator.cs` 冲突化解:保留当前 partial 结构,并把 + `main` 侧新增的模型文档合并到 `CqrsHandlerRegistryGenerator.Models.cs` - 已完成 `GFramework.Game.SourceGenerators` 中 `SchemaConfigGenerator` 的第一批 `MA0051` 收口;warnings-only 基线剩余 `9` 条 `MA0051` 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 4706a318..ca74deca 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,30 @@ # Analyzer Warning Reduction 追踪 +## 2026-04-23 — RP-028 + +### 阶段:`CqrsHandlerRegistryGenerator.cs` 文件级冲突化解(RP-028) + +- 启动复核: + - 用户指出当前分支与 `main` 在 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs` + 存在冲突,需要人工确认并解决 + - 本地检查后确认工作树没有 `UU` 或冲突标记;进一步对比 `origin/main` 发现冲突根因不是运行逻辑回退,而是 + `main` 在旧的单文件版本里新增了 `OrderedRegistrationKind` / `RuntimeTypeReferenceSpec` 的 XML 文档, + 而当前分支已将这些类型拆分到 `CqrsHandlerRegistryGenerator.Models.cs` +- 决策: + - 保留当前分支已经完成的 partial 拆分,不把模型重新塞回 `CqrsHandlerRegistryGenerator.cs` + - 以“迁移 `main` 侧文档意图到拆分后的归属文件”为人工合并策略,避免既回退结构拆分又遗漏 `main` 新增文档 +- 实施调整: + - 将 `OrderedRegistrationKind` 的枚举说明与 `RuntimeTypeReferenceSpec` / `FromDirectReference` / + `FromReflectionLookup` / `FromExternalReflectionLookup` / `FromArray` / `FromConstructedGeneric` + 的 XML 文档迁移到 `CqrsHandlerRegistryGenerator.Models.cs` + - 保持 `CqrsHandlerRegistryGenerator.cs` 主文件只承载主生成管线,不引入重复模型定义 +- 验证结果: + - `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -clp:"Summary;WarningsOnly" -nologo` + - 结果:`0 Warning(s)`,`0 Error(s)` +- 下一步建议: + - 若后续继续处理分支冲突,优先先判断 `main` 改动是否已在当前 partial 文件集里存在等价归属,再决定是否需要真正 merge/rebase + - 若回到 PR #269 收口,可继续抓取最新 unresolved threads 与 CI 状态 + ## 2026-04-23 — RP-027 ### 阶段:PR #269 Greptile inherited-member collision follow-up(RP-027)