fix(source-generators): 收口PR269剩余review问题

- 修复 Cqrs handler registry 对 Roslyn error type 的直接引用,改走安全的运行时类型查找

- 补充 SchemaConfigGenerator 根 type 非字符串诊断回归与 Cqrs 未解析类型回归测试

- 更新 analyzer-warning-reduction 的 RP-024 跟踪与验证记录
This commit is contained in:
GeWuYou 2026-04-22 18:52:26 +08:00 committed by gewuyou
parent df68cdfd82
commit 4ef9406ee9
6 changed files with 184 additions and 3 deletions

View File

@ -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;
}

View File

@ -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(

View File

@ -72,6 +72,45 @@ public class SchemaConfigGeneratorTests
});
}
/// <summary>
/// 验证根节点 <c>type</c> 元数据不是字符串时,会返回根对象约束诊断,而不是抛出 JSON 访问异常。
/// </summary>
[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"));
});
}
/// <summary>
/// 验证 schema 文件名若生成无效根类型标识符时,会在生成前产生命名明确的诊断。
/// </summary>

View File

@ -1375,6 +1375,103 @@ public class CqrsHandlerRegistryGeneratorTests
});
}
/// <summary>
/// 验证 handler 合同里出现未解析错误类型时,生成器会改为运行时精确查找该类型,
/// 而不会把无效类型名直接写进生成代码中的 <c>typeof(...)</c>。
/// </summary>
[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<TResponse> { }
public interface INotification { }
public interface IStreamRequest<TResponse> { }
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse> { }
public interface INotificationHandler<in TNotification> where TNotification : INotification { }
public interface IStreamRequestHandler<in TRequest, out TResponse> where TRequest : IStreamRequest<TResponse> { }
}
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<MissingResponse>;
public sealed class BrokenHandler : IRequestHandler<BrokenRequest, MissingResponse>
{
}
}
""";
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"));
});
}
/// <summary>
/// 验证当 fallback metadata 仍然必需且 runtime 提供了承载契约时,
/// 生成器会继续产出注册器并发射程序集级 <c>CqrsReflectionFallbackAttribute</c>。

View File

@ -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<T>()` 的重复注册路径已恢复为 `ArgumentException`,以保持既有异常契约
- `Option<T>` 已声明 `IEquatable<Option<T>>`,与已有强类型 `Equals(Option<T>)` 契约对齐
@ -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 映射
## 当前风险

View File

@ -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