fix(cqrs): 收敛 fallback 审查跟进

- 修复 generator preamble 的多实例 fallback 特性排版并移除死参数

- 补强 mixed/direct fallback 生成回归断言并拒绝空 marker

- 更新 CQRS 审查跟踪记录与 XML 文档
This commit is contained in:
gewuyou 2026-04-29 16:20:15 +08:00
parent 76fcdb8233
commit 8d8b94f608
6 changed files with 107 additions and 16 deletions

View File

@ -8,15 +8,12 @@ public sealed partial class CqrsHandlerRegistryGenerator
/// <summary>
/// 生成程序集级 CQRS handler 注册器源码。
/// </summary>
/// <param name="generationEnvironment">
/// 当前轮次的生成环境,用于决定 runtime 是否提供 <c>CqrsReflectionFallbackAttribute</c> 契约,以及是否需要在输出中发射对应的程序集级元数据。
/// </param>
/// <param name="registrations">
/// 已整理并排序的 handler 注册描述。方法会据此生成 <c>CqrsHandlerRegistry.g.cs</c>,其中包含直接注册、实现类型反射注册、精确运行时类型查找等分支。
/// </param>
/// <param name="reflectionFallbackEmission">
/// 当前轮次选定的程序集级 reflection fallback 元数据发射策略。
/// 调用方必须先确保:若该策略包含 fallback handlers <paramref name="generationEnvironment" /> 已声明支持对应的 fallback attribute 契约;
/// 调用方必须先确保:若该策略包含 fallback handlers当前 runtime 已声明支持对应的 fallback attribute 契约;
/// 否则应在进入本方法前报告诊断并放弃生成,而不是输出会静默漏注册的半成品注册器。
/// </param>
/// <returns>完整的注册器源代码文本。</returns>
@ -28,13 +25,12 @@ public sealed partial class CqrsHandlerRegistryGenerator
/// 该方法本身不报告诊断“fallback 必需但 runtime 契约缺失”的错误由调用方在进入本方法前处理。
/// </remarks>
private static string GenerateSource(
GenerationEnvironment generationEnvironment,
IReadOnlyList<ImplementationRegistrationSpec> registrations,
ReflectionFallbackEmissionSpec reflectionFallbackEmission)
{
var sourceShape = CreateGeneratedRegistrySourceShape(registrations);
var builder = new StringBuilder();
AppendGeneratedSourcePreamble(builder, generationEnvironment, reflectionFallbackEmission);
AppendGeneratedSourcePreamble(builder, reflectionFallbackEmission);
AppendGeneratedRegistryType(builder, registrations, sourceShape);
return builder.ToString();
}
@ -68,11 +64,9 @@ public sealed partial class CqrsHandlerRegistryGenerator
/// 发射生成文件头、nullable 指令以及注册器所需的程序集级元数据特性。
/// </summary>
/// <param name="builder">生成源码构造器。</param>
/// <param name="generationEnvironment">当前轮次的生成环境。</param>
/// <param name="reflectionFallbackEmission">需要写入程序集级 reflection fallback 特性的元数据策略。</param>
private static void AppendGeneratedSourcePreamble(
StringBuilder builder,
GenerationEnvironment generationEnvironment,
ReflectionFallbackEmissionSpec reflectionFallbackEmission)
{
builder.AppendLine("// <auto-generated />");
@ -102,10 +96,14 @@ public sealed partial class CqrsHandlerRegistryGenerator
StringBuilder builder,
ReflectionFallbackEmissionSpec reflectionFallbackEmission)
{
foreach (var attributeEmission in reflectionFallbackEmission.Attributes)
for (var index = 0; index < reflectionFallbackEmission.Attributes.Length; index++)
{
AppendReflectionFallbackAttribute(builder, attributeEmission);
builder.AppendLine();
if (index > 0)
{
builder.AppendLine();
}
AppendReflectionFallbackAttribute(builder, reflectionFallbackEmission.Attributes[index]);
}
}

View File

@ -296,7 +296,7 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
context.AddSource(
HintName,
GenerateSource(generationEnvironment, registrations, reflectionFallbackEmission));
GenerateSource(registrations, reflectionFallbackEmission));
}
/// <summary>

View File

@ -13,6 +13,9 @@ internal sealed class ReflectionFallbackNotificationContainer
/// <summary>
/// 获取可被直接引用、适合通过 <see cref="Type" /> 元数据补扫的处理器类型。
/// </summary>
/// <returns>
/// 可被生成注册器直接引用的 fallback 处理器类型,用于验证 runtime 会优先消费 <see cref="Type" /> 元数据。
/// </returns>
public static Type DirectFallbackHandlerType => typeof(DirectFallbackGeneratedRegistryNotificationHandler);
/// <summary>

View File

@ -1876,6 +1876,7 @@ public class CqrsHandlerRegistryGeneratorTests
var generatorErrors = execution.GeneratorDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
var generatedSource = execution.GeneratedSources[0].content;
Assert.Multiple(() =>
{
@ -1885,13 +1886,24 @@ public class CqrsHandlerRegistryGeneratorTests
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,
generatedSource,
Does.Contain(
"[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute(typeof(global::TestApp.Container.AlphaHandler), typeof(global::TestApp.Container.BetaHandler))]"));
Assert.That(
execution.GeneratedSources[0].content,
generatedSource,
Does.Not.Contain(
"[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute(\"TestApp.Container+AlphaHandler\", \"TestApp.Container+BetaHandler\")]"));
Assert.That(generatedSource, Does.Not.Contain("CqrsReflectionFallbackAttribute()"));
Assert.That(
CountOccurrences(
generatedSource,
"[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute"),
Is.EqualTo(1));
Assert.That(
CountOccurrences(
generatedSource,
"[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute(\""),
Is.Zero);
});
}
@ -1915,6 +1927,7 @@ public class CqrsHandlerRegistryGeneratorTests
var generatorErrors = execution.GeneratorDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
var generatedSource = execution.GeneratedSources[0].content;
Assert.Multiple(() =>
{
@ -1924,13 +1937,32 @@ public class CqrsHandlerRegistryGeneratorTests
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,
generatedSource,
Does.Contain(
"[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute(typeof(global::TestApp.Container.AlphaHandler))]"));
Assert.That(
execution.GeneratedSources[0].content,
generatedSource,
Does.Contain(
"[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute(\"TestApp.Container+BetaHandler\")]"));
Assert.That(generatedSource, Does.Not.Contain("CqrsReflectionFallbackAttribute()"));
Assert.That(
CountOccurrences(
generatedSource,
"[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute"),
Is.EqualTo(2));
Assert.That(
CountOccurrences(
generatedSource,
"[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute(\""),
Is.EqualTo(1));
Assert.That(
generatedSource,
Does.Contain(
"[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute(typeof(global::TestApp.Container.AlphaHandler))]" +
Environment.NewLine +
"[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute(\"TestApp.Container+BetaHandler\")]" +
Environment.NewLine +
"[assembly: global::GFramework.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))]"));
});
}
@ -1985,6 +2017,31 @@ public class CqrsHandlerRegistryGeneratorTests
return execution.GeneratedSources[0].content;
}
/// <summary>
/// 统计生成源码中某个固定片段的出现次数,用于锁定程序集级 fallback 特性的发射个数。
/// </summary>
/// <param name="text">待统计的完整生成源码。</param>
/// <param name="value">需要计数的固定片段。</param>
/// <returns><paramref name="value" /> 在 <paramref name="text" /> 中出现的次数。</returns>
private static int CountOccurrences(string text, string value)
{
if (string.IsNullOrEmpty(value))
throw new ArgumentException("The search value must not be null or empty.", nameof(value));
var count = 0;
var startIndex = 0;
while (true)
{
var nextIndex = text.IndexOf(value, startIndex, global::System.StringComparison.Ordinal);
if (nextIndex < 0)
return count;
count++;
startIndex = nextIndex + value.Length;
}
}
/// <summary>
/// 运行 CQRS handler registry generator并返回生成输出及相关诊断。
/// </summary>

View File

@ -66,6 +66,11 @@ CQRS 迁移与收敛。
- 当本轮 fallback 同时包含可直接引用与仅能按名称恢复的 handlers且 runtime 同时支持 `Type[]``string[]` 和多实例特性时,生成器会拆分输出两段 fallback 元数据
- `GFramework.Cqrs.Tests` 已补充 mixed fallback metadata 回归,锁定 registrar 只对字符串条目执行定向 `Assembly.GetType(...)`
- `GFramework.SourceGenerators.Tests` 已补充 mixed fallback emission 回归,锁定 generator 会输出两个程序集级 fallback 特性实例而不是整体退回字符串
- `2026-04-29` 已重新执行 `$gframework-pr-review`
- 当前分支对应 `PR #302`,状态为 `OPEN`
- latest reviewed commit 当前剩余 `3` 条 open AI review threads`2` 条 Greptile、`1` 条 CodeRabbit
- 本地核对后确认 `dotnet-format` 仍只有 `Restore operation failed` 噪音,没有附带当前仍成立的文件级格式诊断
- 已按 review triage 修正 generator source preamble 的多实例 fallback 特性排版、移除死参数,并补强 mixed/direct fallback 发射回归断言与 XML 文档
- 当前主线优先级:
- generator 覆盖面继续扩大
- dispatch/invoker 反射占比继续下降
@ -116,6 +121,18 @@ CQRS 迁移与收敛。
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
- 结果:通过
- 备注:`18/18` 测试通过;本轮覆盖 mixed fallback metadata 的双特性发射路径
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/gframework-pr-review.json`
- 结果:通过
- 备注:确认当前分支对应 `PR #302`latest head review 仍有 `3` 条 open AI threads其中 MegaLinter 仅报告 `dotnet-format` restore failure 噪音
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release`
- 结果:通过
- 备注:`0 warning / 0 error`
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
- 结果:通过
- 备注:`18/18` 测试通过;本轮直接覆盖 fallback preamble 排版与特性个数断言收紧
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"`
- 结果:通过
- 备注:`13/13` 测试通过;本轮确认 mixed fallback metadata 的 registrar 消费路径未回归
## 下一步

View File

@ -32,6 +32,22 @@
- `13/13` passed
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
- `18/18` passed
- 随后按 `$gframework-pr-review` 重新拉取当前分支 PR 审查数据:
- 当前 worktree `feat/cqrs-optimization` 已对应 `PR #302`
- latest head commit 仍有 `3` 条 open AI review threadsGreptile 指向 generator preamble 的死参数与多实例 fallback 特性空行CodeRabbit 指向 mixed/direct fallback 测试断言过宽
- MegaLinter 仍只暴露 `dotnet-format``Restore operation failed`,未给出本地仍成立的格式文件线索,因此按环境噪音处理
- 本轮已继续收口 `RP-052` 的 follow-up
- 在 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs` 中移除已不再参与判断的 `generationEnvironment` 透传参数
- 调整多实例 fallback 特性发射时的换行策略,避免最后一个 fallback 特性与 `CqrsHandlerRegistryAttribute` 之间保留多余空行
- 在 `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 中补强 direct/mixed fallback 发射断言,锁定特性实例个数、拒绝空 marker并确保 mixed 场景的程序集级 preamble 排版稳定
- 在 `GFramework.Cqrs.Tests/Cqrs/ReflectionFallbackNotificationContainer.cs` 中为 `DirectFallbackHandlerType` 补齐 `<returns>` XML 文档
- 本轮 review follow-up 验证已通过:
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release`
- `0 warning / 0 error`
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
- `18/18` passed
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"`
- `13/13` passed
## 2026-04-20