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] =?UTF-8?q?fix(source-generators):=20=E6=94=B6=E5=8F=A3PR2?= =?UTF-8?q?69=E5=89=A9=E4=BD=99review=E4=B8=8E=E6=9E=84=E5=BB=BA=E8=A7=84?= =?UTF-8?q?=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)