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` 收敛为单次构建的本地映射索引