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)