mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
fix(cqrs): 修复指针合同生成回归
- 修复 CqrsHandlerRegistryGenerator 对 pointer 与 function pointer 的精确注册建模 - 补充生成器测试对输入源 CS0306 与 fallback 诊断的断言 - 更新 cqrs-rewrite 跟踪文档记录 PR #261 review follow-up
This commit is contained in:
parent
db65249315
commit
c1f9fa8b9a
@ -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:
|
||||
|
||||
@ -164,45 +164,6 @@ public class CqrsHandlerRegistryGeneratorTests
|
||||
|
||||
""";
|
||||
|
||||
private const string HiddenPointerResponseExpected = """
|
||||
// <auto-generated />
|
||||
#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<TestApp.Container.HiddenRequest, TestApp.Container.HiddenResponse*>.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
""";
|
||||
|
||||
private const string MixedDirectAndPreciseRegistrationsExpected = """
|
||||
// <auto-generated />
|
||||
#nullable enable
|
||||
@ -785,11 +746,11 @@ public class CqrsHandlerRegistryGeneratorTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证精确重建路径会递归覆盖隐藏指针元素类型,
|
||||
/// 使“隐藏 pointed-at 类型 + unsafe 指针响应”的 handler 也能直接生成 closed service type。
|
||||
/// 验证当 handler 合同把 pointer 响应类型放进 CQRS 泛型参数时,
|
||||
/// 生成器会保守回退而不是继续发射不可构造的精确注册代码。
|
||||
/// </summary>
|
||||
[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
|
||||
/// <param name="GeneratedSources">本轮生成产生的源文件集合。</param>
|
||||
/// <param name="GeneratorDiagnostics">生成器自身报告的诊断集合。</param>
|
||||
/// <param name="CompilationDiagnostics">将生成结果并回编译后的完整编译诊断集合。</param>
|
||||
/// <param name="InputCompilationDiagnostics">仅来自输入源文件的编译诊断集合。</param>
|
||||
/// <param name="GeneratedCompilationDiagnostics">仅来自生成源文件的编译诊断集合。</param>
|
||||
private sealed record GeneratorExecutionResult(
|
||||
(string filename, string content)[] GeneratedSources,
|
||||
Diagnostic[] GeneratorDiagnostics,
|
||||
Diagnostic[] CompilationDiagnostics,
|
||||
Diagnostic[] InputCompilationDiagnostics,
|
||||
Diagnostic[] GeneratedCompilationDiagnostics);
|
||||
}
|
||||
|
||||
@ -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 去重路径
|
||||
|
||||
@ -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` 收敛为单次构建的本地映射索引
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user