From c1f9fa8b9aac20e5a065bf43e5e18c493ee4ec5e Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Mon, 20 Apr 2026 17:55:41 +0800
Subject: [PATCH] =?UTF-8?q?fix(cqrs):=20=E4=BF=AE=E5=A4=8D=E6=8C=87?=
=?UTF-8?q?=E9=92=88=E5=90=88=E5=90=8C=E7=94=9F=E6=88=90=E5=9B=9E=E5=BD=92?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 修复 CqrsHandlerRegistryGenerator 对 pointer 与 function pointer 的精确注册建模
- 补充生成器测试对输入源 CS0306 与 fallback 诊断的断言
- 更新 cqrs-rewrite 跟踪文档记录 PR #261 review follow-up
---
.../Cqrs/CqrsHandlerRegistryGenerator.cs | 32 +++----
.../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 83 ++++++++-----------
.../todos/cqrs-rewrite-migration-tracking.md | 23 ++---
.../traces/cqrs-rewrite-migration-trace.md | 14 ++++
4 files changed, 75 insertions(+), 77 deletions(-)
diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs
index 7888f613..9b3f889e 100644
--- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs
+++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs
@@ -398,6 +398,15 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
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(
@@ -412,13 +421,6 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
return true;
}
- if (type is IPointerTypeSymbol pointerType &&
- TryCreateRuntimeTypeReference(compilation, pointerType.PointedAtType, out var pointedAtTypeReference))
- {
- runtimeTypeReference = RuntimeTypeReferenceSpec.FromPointer(pointedAtTypeReference!);
- return true;
- }
-
if (type is INamedTypeSymbol genericNamedType &&
genericNamedType.IsGenericType &&
!genericNamedType.IsUnboundGenericType &&
@@ -525,19 +527,9 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
}
return true;
- case IPointerTypeSymbol pointerType:
- return CanReferenceFromGeneratedRegistry(compilation, pointerType.PointedAtType);
- case IFunctionPointerTypeSymbol functionPointerType:
- if (!CanReferenceFromGeneratedRegistry(compilation, functionPointerType.Signature.ReturnType))
- return false;
-
- foreach (var parameter in functionPointerType.Signature.Parameters)
- {
- if (!CanReferenceFromGeneratedRegistry(compilation, parameter.Type))
- return false;
- }
-
- return true;
+ case IPointerTypeSymbol:
+ case IFunctionPointerTypeSymbol:
+ return false;
case ITypeParameterSymbol:
return false;
default:
diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs
index c0b4508f..e9a9bfa2 100644
--- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs
+++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs
@@ -164,45 +164,6 @@ public class CqrsHandlerRegistryGeneratorTests
""";
- private const string HiddenPointerResponseExpected = """
- //
- #nullable enable
-
- [assembly: global::GFramework.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))]
-
- namespace GFramework.Generated.Cqrs;
-
- internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry
- {
- public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::GFramework.Core.Abstractions.Logging.ILogger logger)
- {
- if (services is null)
- throw new global::System.ArgumentNullException(nameof(services));
- if (logger is null)
- throw new global::System.ArgumentNullException(nameof(logger));
-
- var registryAssembly = typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry).Assembly;
-
- var implementationType0 = typeof(global::TestApp.Container.HiddenHandler);
- if (implementationType0 is not null)
- {
- var serviceType0_0Argument0 = registryAssembly.GetType("TestApp.Container+HiddenRequest", throwOnError: false, ignoreCase: false);
- var serviceType0_0Argument1PointedAt = registryAssembly.GetType("TestApp.Container+HiddenResponse", throwOnError: false, ignoreCase: false);
- if (serviceType0_0Argument0 is not null && serviceType0_0Argument1PointedAt is not null)
- {
- var serviceType0_0 = typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType(serviceType0_0Argument0, serviceType0_0Argument1PointedAt.MakePointerType());
- global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient(
- services,
- serviceType0_0,
- implementationType0);
- logger.Debug("Registered CQRS handler TestApp.Container.HiddenHandler as GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler.");
- }
- }
- }
- }
-
- """;
-
private const string MixedDirectAndPreciseRegistrationsExpected = """
//
#nullable enable
@@ -785,11 +746,11 @@ public class CqrsHandlerRegistryGeneratorTests
}
///
- /// 验证精确重建路径会递归覆盖隐藏指针元素类型,
- /// 使“隐藏 pointed-at 类型 + unsafe 指针响应”的 handler 也能直接生成 closed service type。
+ /// 验证当 handler 合同把 pointer 响应类型放进 CQRS 泛型参数时,
+ /// 生成器会保守回退而不是继续发射不可构造的精确注册代码。
///
[Test]
- public async Task Generates_Precise_Service_Type_For_Hidden_Pointer_Response()
+ public void Reports_Compilation_Error_And_Skips_Precise_Registration_For_Hidden_Pointer_Response()
{
const string source = """
using System;
@@ -859,20 +820,31 @@ public class CqrsHandlerRegistryGeneratorTests
var execution = ExecuteGenerator(
source,
allowUnsafe: true);
+ 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();
+ var missingContractDiagnostic =
+ generatorErrors.SingleOrDefault(static diagnostic =>
+ string.Equals(diagnostic.Id, "GF_Cqrs_001", StringComparison.Ordinal));
Assert.Multiple(() =>
{
+ Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS0306"));
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"));
- Assert.That(execution.GeneratedSources[0].content, Is.EqualTo(HiddenPointerResponseExpected));
+ Assert.That(execution.GeneratedSources, Is.Empty);
+ Assert.That(missingContractDiagnostic, Is.Not.Null);
+ Assert.That(
+ missingContractDiagnostic!.GetMessage(),
+ Does.Contain("TestApp.Container+HiddenHandler"));
+ Assert.That(
+ missingContractDiagnostic.GetMessage(),
+ Does.Contain("GFramework.Cqrs.CqrsReflectionFallbackAttribute"));
});
}
@@ -1375,6 +1347,9 @@ public class CqrsHandlerRegistryGeneratorTests
var execution = ExecuteGenerator(
source,
allowUnsafe: true);
+ var inputCompilationErrors = execution.InputCompilationDiagnostics
+ .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
+ .ToArray();
var generatedCompilationErrors = execution.GeneratedCompilationDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
@@ -1382,10 +1357,12 @@ public class CqrsHandlerRegistryGeneratorTests
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
var missingContractDiagnostic =
- generatorErrors.SingleOrDefault(static diagnostic => diagnostic.Id == "GF_Cqrs_001");
+ generatorErrors.SingleOrDefault(static diagnostic =>
+ string.Equals(diagnostic.Id, "GF_Cqrs_001", StringComparison.Ordinal));
Assert.Multiple(() =>
{
+ Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS0306"));
Assert.That(generatedCompilationErrors, Is.Empty);
Assert.That(execution.GeneratedSources, Is.Empty);
Assert.That(missingContractDiagnostic, Is.Not.Null);
@@ -1490,6 +1467,9 @@ public class CqrsHandlerRegistryGeneratorTests
var execution = ExecuteGenerator(
source,
allowUnsafe: true);
+ var inputCompilationErrors = execution.InputCompilationDiagnostics
+ .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
+ .ToArray();
var generatedCompilationErrors = execution.GeneratedCompilationDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
@@ -1499,6 +1479,7 @@ public class CqrsHandlerRegistryGeneratorTests
Assert.Multiple(() =>
{
+ Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS0306"));
Assert.That(generatedCompilationErrors, Is.Empty);
Assert.That(generatorErrors, Is.Empty);
Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1));
@@ -1611,6 +1592,11 @@ public class CqrsHandlerRegistryGeneratorTests
(filename: sourceResult.HintName, content: sourceResult.SourceText.ToString()))
.ToArray();
var compilationDiagnostics = updatedCompilation.GetDiagnostics().ToArray();
+ var inputCompilationDiagnostics = compilationDiagnostics
+ .Where(diagnostic =>
+ diagnostic.Location.SourceTree is null ||
+ !generatedSyntaxTrees.Contains(diagnostic.Location.SourceTree))
+ .ToArray();
var generatedCompilationDiagnostics = compilationDiagnostics
.Where(diagnostic =>
diagnostic.Location.SourceTree is not null &&
@@ -1620,6 +1606,7 @@ public class CqrsHandlerRegistryGeneratorTests
generatedSources,
generatorDiagnostics.ToArray(),
compilationDiagnostics,
+ inputCompilationDiagnostics,
generatedCompilationDiagnostics);
}
@@ -1629,10 +1616,12 @@ public class CqrsHandlerRegistryGeneratorTests
/// 本轮生成产生的源文件集合。
/// 生成器自身报告的诊断集合。
/// 将生成结果并回编译后的完整编译诊断集合。
+ /// 仅来自输入源文件的编译诊断集合。
/// 仅来自生成源文件的编译诊断集合。
private sealed record GeneratorExecutionResult(
(string filename, string content)[] GeneratedSources,
Diagnostic[] GeneratorDiagnostics,
Diagnostic[] CompilationDiagnostics,
+ Diagnostic[] InputCompilationDiagnostics,
Diagnostic[] GeneratedCompilationDiagnostics);
}
diff --git a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md
index a2c18116..527cb1ad 100644
--- a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md
+++ b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md
@@ -7,14 +7,14 @@ CQRS 迁移与收敛。
## 当前恢复点
-- 恢复点编号:`CQRS-REWRITE-RP-049`
+- 恢复点编号:`CQRS-REWRITE-RP-050`
- 当前阶段:`Phase 8`
- 当前焦点:
- 当前功能历史已归档,active 跟踪仅保留 `Phase 8` 主线的恢复入口
- 已完成 generated registry 激活路径收敛:`CqrsHandlerRegistrar` 现优先复用缓存工厂委托,避免重复 `ConstructorInfo.Invoke`
- 已补充私有无参构造 generated registry 的回归测试,确保兼容现有生成器产物
- - 已补充 pointer 响应类型的 precise runtime type 生成,避免这类 handler 再退回程序集级 reflection fallback
- - 已收紧 function pointer 签名的可直接生成判定,仅在其返回值与参数类型都可安全引用时才走静态注册路径
+ - 已修正 pointer / function pointer 泛型合同的错误覆盖:生成器不再为这两类类型发射 precise runtime type 重建代码
+ - 已补充非法 CQRS 泛型合同的输入诊断断言,明确 `CS0306` 与 fallback / diagnostic 路径的组合语义
- 已为 registrar 的 reflection 注册路径补充 handler-interface 元数据缓存,减少跨容器重复注册时的 `GetInterfaces()` 反射
- 已将 registrar 的重复映射判定从线性扫描 `IServiceCollection` 收敛为本地映射索引,减少 fallback 注册路径的重复查找
- 中期上继续 `Phase 8` 主线:参考 `ai-libs/Mediator`,继续扩大 generator 覆盖,并选择下一个收益明确的 dispatch / invoker 反射收敛点
@@ -31,17 +31,17 @@ CQRS 迁移与收敛。
- `Phase 8` 仍是当前主线,不再回退到 `Phase 7`
- `2026-04-20` 已重新执行 `$gframework-pr-review`:
- - `PR #253` 当前状态为 `CLOSED`
- - latest reviewed commit 仍显示 `1` 条 open thread,但其内容针对的是已过时的 `Phase 7` 恢复建议
- - 当前 active tracking / trace 已统一到 `Phase 8`,因此该 thread 不再作为当前主线阻塞项
+ - 当前分支对应 `PR #261`,状态为 `OPEN`
+ - latest reviewed commit 上有 `2` 条 open CodeRabbit thread,均指向 pointer / function pointer 泛型合同处理
+ - 本地已接受并修复这两条建议:生成器拒绝为 pointer / function pointer 生成 precise registration,测试改为显式断言输入源 `CS0306`
- `2026-04-20` 已完成一轮冷启动反射收敛:
- generated registry 类型首次分析后,会缓存一个可复用的激活工厂,而不是在后续容器注册时重复走 `ConstructorInfo.Invoke`
- 若运行环境不允许动态方法,仍保留原有的反射激活回退,避免阻塞 generated registry 路径
- `GFramework.Cqrs.Tests` 已补充“私有无参构造 registry 仍可激活”的回归覆盖
- `2026-04-20` 已完成一轮 generator 覆盖面扩展:
- - `CqrsHandlerRegistryGenerator` 现可为 pointer 类型递归重建 runtime type,并通过 `MakePointerType()` 生成精确 service type
- - function pointer 签名不再默认视为“可直接引用”;只有当返回值与每个参数类型都可从 generated registry 安全引用时,才允许直接生成
- - 含隐藏类型的 function pointer handler 仍会保留原有 fallback / 诊断路径,避免此次覆盖扩展误伤已有回退边界
+ - `CqrsHandlerRegistryGenerator` 现会在 runtime type 建模入口直接拒绝 `IPointerTypeSymbol` 与 `IFunctionPointerTypeSymbol`
+ - `CanReferenceFromGeneratedRegistry` 不再递归判断 pointer / function pointer 的内部元素,而是统一返回 `false`
+ - 相关 source-generator 回归已改为区分输入源诊断与生成源诊断,避免把非法泛型合同误判为成功生成
- `2026-04-20` 已完成一轮 registrar reflection 路径收敛:
- `CqrsHandlerRegistrar` 现会按 `Type` 弱键缓存已筛选且排序好的 supported handler interface 列表
- 同一 handler 类型跨容器重复注册时,不再重复执行 `GetInterfaces()` 与支持接口筛选
@@ -76,7 +76,10 @@ CQRS 迁移与收敛。
- 备注:`63/63` 测试通过;当前沙箱限制了 MSBuild named pipe,验证需在提权环境下运行
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
- 结果:通过
- - 备注:`14/14` 测试通过;本轮覆盖 pointer precise registration 与 function pointer fallback 边界
+ - 备注:`14/14` 测试通过;本轮覆盖 pointer / function pointer 合同拒绝、fallback 诊断与现有精确注册路径
+- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~Reports_Compilation_Error_And_Skips_Precise_Registration_For_Hidden_Pointer_Response|FullyQualifiedName~Reports_Diagnostic_And_Skips_Registry_When_Fallback_Metadata_Is_Required_But_Runtime_Contract_Lacks_Fallback_Attribute|FullyQualifiedName~Emits_Assembly_Level_Fallback_Metadata_When_Fallback_Is_Required_And_Runtime_Contract_Is_Available"`
+ - 结果:通过
+ - 备注:`3/3` 测试通过;本轮直接覆盖 PR #261 指向的 3 个 pointer / function pointer 回归场景
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"`
- 结果:通过
- 备注:`11/11` 测试通过;本轮覆盖 registrar 的 supported handler interface 缓存与 duplicate mapping 去重路径
diff --git a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md
index 63ab4b33..7fd73df7 100644
--- a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md
+++ b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md
@@ -2,6 +2,20 @@
## 2026-04-20
+### 阶段:pointer / function pointer 泛型合同拒绝(CQRS-REWRITE-RP-050)
+
+- 重新执行 `$gframework-pr-review` 后,确认当前分支对应 `PR #261`,latest reviewed commit 上有 `2` 条仍未关闭的 CodeRabbit thread
+- 本地核对后确认这两条评论有效:`CqrsHandlerRegistryGenerator` 之前会为 `IPointerTypeSymbol` 递归构造 `MakePointerType()`,而测试只校验生成源诊断,未显式暴露输入源 `CS0306`
+- 已在 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs` 中收紧 `TryCreateRuntimeTypeReference` 与 `CanReferenceFromGeneratedRegistry`
+- pointer / function pointer 现统一视为不可精确生成的 CQRS 泛型合同,生成器会保守回退到既有 fallback / diagnostic 路径,而不再发射运行时 `MakeGenericType(...)` 风险代码
+- 已在 `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 中补充输入源诊断分离,并将相关测试改为显式断言 `CS0306` 与 fallback / diagnostic 结果
+- 定向验证已通过:
+ - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~Reports_Compilation_Error_And_Skips_Precise_Registration_For_Hidden_Pointer_Response|FullyQualifiedName~Reports_Diagnostic_And_Skips_Registry_When_Fallback_Metadata_Is_Required_But_Runtime_Contract_Lacks_Fallback_Attribute|FullyQualifiedName~Emits_Assembly_Level_Fallback_Metadata_When_Fallback_Is_Required_And_Runtime_Contract_Is_Available"`
+ - `3/3` passed
+- 扩展验证已通过:
+ - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
+ - `14/14` passed
+
### 阶段:registrar duplicate mapping 索引收敛(CQRS-REWRITE-RP-049)
- 已将 `CqrsHandlerRegistrar` 的重复 handler mapping 判定从逐条线性扫描 `IServiceCollection` 收敛为单次构建的本地映射索引