mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
- 重构 CqrsHandlerRegistryGenerator 为按职责拆分的 partial 生成器文件,保留现有注册输出与 fallback 契约 - 修复 ContextAwareGenerator 生成字段命名冲突并为 SetContextProvider 补充运行时 null 校验与异常文档 - 补充 Option<T> 的 XML remarks 契约说明与 ContextAwareGenerator 字段冲突快照测试 - 更新 analyzer-warning-reduction 跟踪与 trace,记录 PR #269 review follow-up 与验证结果
449 lines
22 KiB
C#
449 lines
22 KiB
C#
using GFramework.SourceGenerators.Common.Constants;
|
||
|
||
namespace GFramework.Cqrs.SourceGenerators.Cqrs;
|
||
|
||
/// <summary>
|
||
/// 为当前编译程序集生成 CQRS 处理器注册器,以减少运行时的程序集反射扫描成本。
|
||
/// </summary>
|
||
[Generator]
|
||
public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||
{
|
||
private const string CqrsContractsNamespace = $"{PathContests.CqrsAbstractionsNamespace}.Cqrs";
|
||
private const string CqrsRuntimeNamespace = PathContests.CqrsNamespace;
|
||
private const string LoggingNamespace = $"{PathContests.CoreAbstractionsNamespace}.Logging";
|
||
private const string IRequestHandlerMetadataName = $"{CqrsContractsNamespace}.IRequestHandler`2";
|
||
private const string INotificationHandlerMetadataName = $"{CqrsContractsNamespace}.INotificationHandler`1";
|
||
private const string IStreamRequestHandlerMetadataName = $"{CqrsContractsNamespace}.IStreamRequestHandler`2";
|
||
private const string ICqrsHandlerRegistryMetadataName = $"{CqrsRuntimeNamespace}.ICqrsHandlerRegistry";
|
||
|
||
private const string CqrsHandlerRegistryAttributeMetadataName =
|
||
$"{CqrsRuntimeNamespace}.CqrsHandlerRegistryAttribute";
|
||
|
||
private const string CqrsReflectionFallbackAttributeMetadataName =
|
||
$"{CqrsRuntimeNamespace}.CqrsReflectionFallbackAttribute";
|
||
|
||
private const string ILoggerMetadataName = $"{LoggingNamespace}.ILogger";
|
||
private const string IServiceCollectionMetadataName = "Microsoft.Extensions.DependencyInjection.IServiceCollection";
|
||
private const string GeneratedNamespace = "GFramework.Generated.Cqrs";
|
||
private const string GeneratedTypeName = "__GFrameworkGeneratedCqrsHandlerRegistry";
|
||
private const string HintName = "CqrsHandlerRegistry.g.cs";
|
||
|
||
private static readonly DiagnosticDescriptor MissingReflectionFallbackContractDiagnostic = new(
|
||
"GF_Cqrs_001",
|
||
"Cannot emit CQRS registry without reflection fallback contract",
|
||
"Cannot generate CQRS handler registry because fallback metadata is required for handler(s): {0}, but runtime contract '{1}' is unavailable",
|
||
"GFramework.Cqrs.SourceGenerators",
|
||
DiagnosticSeverity.Error,
|
||
true);
|
||
|
||
/// <inheritdoc />
|
||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||
{
|
||
var generationEnvironment = context.CompilationProvider
|
||
.Select(static (compilation, _) => CreateGenerationEnvironment(compilation));
|
||
|
||
// Restrict semantic analysis to type declarations that can actually contribute implemented interfaces.
|
||
var handlerCandidates = context.SyntaxProvider.CreateSyntaxProvider(
|
||
static (node, _) => IsHandlerCandidate(node),
|
||
static (syntaxContext, _) => TransformHandlerCandidate(syntaxContext))
|
||
.Where(static candidate => candidate is not null)
|
||
.Collect();
|
||
|
||
context.RegisterSourceOutput(
|
||
generationEnvironment.Combine(handlerCandidates),
|
||
static (productionContext, pair) => Execute(productionContext, pair.Left, pair.Right));
|
||
}
|
||
|
||
private static GenerationEnvironment CreateGenerationEnvironment(Compilation compilation)
|
||
{
|
||
var generationEnabled = compilation.GetTypeByMetadataName(IRequestHandlerMetadataName) is not null &&
|
||
compilation.GetTypeByMetadataName(INotificationHandlerMetadataName) is not null &&
|
||
compilation.GetTypeByMetadataName(IStreamRequestHandlerMetadataName) is not null &&
|
||
compilation.GetTypeByMetadataName(ICqrsHandlerRegistryMetadataName) is not null &&
|
||
compilation.GetTypeByMetadataName(
|
||
CqrsHandlerRegistryAttributeMetadataName) is not null &&
|
||
compilation.GetTypeByMetadataName(ILoggerMetadataName) is not null &&
|
||
compilation.GetTypeByMetadataName(IServiceCollectionMetadataName) is not null;
|
||
var supportsReflectionFallbackAttribute =
|
||
compilation.GetTypeByMetadataName(CqrsReflectionFallbackAttributeMetadataName) is not null;
|
||
|
||
return new GenerationEnvironment(generationEnabled, supportsReflectionFallbackAttribute);
|
||
}
|
||
|
||
private static bool IsHandlerCandidate(SyntaxNode node)
|
||
{
|
||
return node is TypeDeclarationSyntax
|
||
{
|
||
BaseList.Types.Count: > 0
|
||
};
|
||
}
|
||
|
||
private static HandlerCandidateAnalysis? TransformHandlerCandidate(GeneratorSyntaxContext context)
|
||
{
|
||
if (context.Node is not TypeDeclarationSyntax typeDeclaration)
|
||
return null;
|
||
|
||
if (context.SemanticModel.GetDeclaredSymbol(typeDeclaration) is not INamedTypeSymbol type)
|
||
return null;
|
||
|
||
if (!IsConcreteHandlerType(type))
|
||
return null;
|
||
|
||
var handlerInterfaces = GetSupportedHandlerInterfaces(type);
|
||
|
||
if (handlerInterfaces.IsDefaultOrEmpty)
|
||
return null;
|
||
|
||
return CreateHandlerCandidateAnalysis(context.SemanticModel.Compilation, type, handlerInterfaces);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 收集当前实现类型已经关闭的 CQRS handler 接口,并按稳定显示名排序以保证生成输出可重复。
|
||
/// </summary>
|
||
/// <param name="type">当前语义模型发现的具体 handler 实现类型。</param>
|
||
/// <returns>可由 CQRS 注册器生成器处理的 handler 接口集合。</returns>
|
||
private static ImmutableArray<INamedTypeSymbol> GetSupportedHandlerInterfaces(INamedTypeSymbol type)
|
||
{
|
||
return type.AllInterfaces
|
||
.Where(IsSupportedHandlerInterface)
|
||
.OrderBy(GetTypeSortKey, StringComparer.Ordinal)
|
||
.ToImmutableArray();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将单个实现类型的 handler 接口拆分为直接注册、实现类型反射注册、精确反射注册和兜底 fallback 四类结果。
|
||
/// </summary>
|
||
/// <param name="compilation">当前生成轮次的编译上下文,用于判断类型可访问性。</param>
|
||
/// <param name="type">需要分析的 handler 实现类型。</param>
|
||
/// <param name="handlerInterfaces">该实现类型声明的受支持 CQRS handler 接口。</param>
|
||
/// <returns>供最终生成阶段消费的 handler 候选分析结果。</returns>
|
||
private static HandlerCandidateAnalysis CreateHandlerCandidateAnalysis(
|
||
Compilation compilation,
|
||
INamedTypeSymbol type,
|
||
ImmutableArray<INamedTypeSymbol> handlerInterfaces)
|
||
{
|
||
var implementationTypeDisplayName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||
var implementationLogName = GetLogDisplayName(type);
|
||
var canReferenceImplementation = CanReferenceFromGeneratedRegistry(compilation, type);
|
||
var registrations = ImmutableArray.CreateBuilder<HandlerRegistrationSpec>(handlerInterfaces.Length);
|
||
var reflectedImplementationRegistrations =
|
||
ImmutableArray.CreateBuilder<ReflectedImplementationRegistrationSpec>(handlerInterfaces.Length);
|
||
var preciseReflectedRegistrations =
|
||
ImmutableArray.CreateBuilder<PreciseReflectedRegistrationSpec>(handlerInterfaces.Length);
|
||
string? reflectionFallbackHandlerTypeMetadataName = null;
|
||
foreach (var handlerInterface in handlerInterfaces)
|
||
{
|
||
if (TryAddStaticHandlerRegistration(
|
||
compilation,
|
||
handlerInterface,
|
||
canReferenceImplementation,
|
||
implementationTypeDisplayName,
|
||
implementationLogName,
|
||
registrations,
|
||
reflectedImplementationRegistrations,
|
||
preciseReflectedRegistrations))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// Concrete closed handler contracts should now always map to either direct registrations,
|
||
// reflected implementation registrations, or precise runtime type references.
|
||
// If a future Roslyn type shape still slips through this net, keep the generator conservative:
|
||
// preserve the static registrations we do understand, and let the runtime recover the remaining
|
||
// interfaces via the existing assembly-level targeted reflection fallback contract.
|
||
reflectionFallbackHandlerTypeMetadataName ??= GetReflectionTypeMetadataName(type);
|
||
}
|
||
|
||
return new HandlerCandidateAnalysis(
|
||
implementationTypeDisplayName,
|
||
implementationLogName,
|
||
registrations.ToImmutable(),
|
||
reflectedImplementationRegistrations.ToImmutable(),
|
||
preciseReflectedRegistrations.ToImmutable(),
|
||
canReferenceImplementation ? null : GetReflectionTypeMetadataName(type),
|
||
reflectionFallbackHandlerTypeMetadataName);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 尝试为单个 handler 接口选择无需程序集级 fallback 的注册表示。
|
||
/// </summary>
|
||
/// <param name="compilation">当前生成轮次的编译上下文。</param>
|
||
/// <param name="handlerInterface">正在分类的关闭 handler 接口。</param>
|
||
/// <param name="canReferenceImplementation">生成代码是否可直接引用实现类型。</param>
|
||
/// <param name="implementationTypeDisplayName">实现类型在生成源码中的全限定显示名。</param>
|
||
/// <param name="implementationLogName">实现类型用于日志输出的稳定显示名。</param>
|
||
/// <param name="registrations">直接类型注册集合。</param>
|
||
/// <param name="reflectedImplementationRegistrations">实现类型需要反射解析、接口可直接引用的注册集合。</param>
|
||
/// <param name="preciseReflectedRegistrations">接口类型需要运行时精确构造的注册集合。</param>
|
||
/// <returns>
|
||
/// 当当前接口已经被添加到某个静态注册集合时返回 <see langword="true" />;否则调用方应记录 reflection fallback 元数据。
|
||
/// </returns>
|
||
private static bool TryAddStaticHandlerRegistration(
|
||
Compilation compilation,
|
||
INamedTypeSymbol handlerInterface,
|
||
bool canReferenceImplementation,
|
||
string implementationTypeDisplayName,
|
||
string implementationLogName,
|
||
ImmutableArray<HandlerRegistrationSpec>.Builder registrations,
|
||
ImmutableArray<ReflectedImplementationRegistrationSpec>.Builder reflectedImplementationRegistrations,
|
||
ImmutableArray<PreciseReflectedRegistrationSpec>.Builder preciseReflectedRegistrations)
|
||
{
|
||
var canReferenceHandlerInterface = CanReferenceFromGeneratedRegistry(compilation, handlerInterface);
|
||
if (canReferenceImplementation && canReferenceHandlerInterface)
|
||
{
|
||
registrations.Add(new HandlerRegistrationSpec(
|
||
handlerInterface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
|
||
implementationTypeDisplayName,
|
||
GetLogDisplayName(handlerInterface),
|
||
implementationLogName));
|
||
return true;
|
||
}
|
||
|
||
if (!canReferenceImplementation && canReferenceHandlerInterface)
|
||
{
|
||
reflectedImplementationRegistrations.Add(new ReflectedImplementationRegistrationSpec(
|
||
handlerInterface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
|
||
GetLogDisplayName(handlerInterface)));
|
||
return true;
|
||
}
|
||
|
||
if (!TryCreatePreciseReflectedRegistration(compilation, handlerInterface, out var preciseReflectedRegistration))
|
||
return false;
|
||
|
||
preciseReflectedRegistrations.Add(preciseReflectedRegistration);
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行 CQRS handler registry 生成管线的最终发射阶段,负责将候选 handler 分析结果汇总为单个
|
||
/// <c>CqrsHandlerRegistry.g.cs</c>,并在需要时附带程序集级 reflection fallback 元数据。
|
||
/// </summary>
|
||
/// <param name="context">用于报告诊断并发射生成源码的源生产上下文。</param>
|
||
/// <param name="generationEnvironment">
|
||
/// 当前编译轮次可用的 runtime 合同快照。
|
||
/// 只有当 CQRS 注册器生成所需的基础契约齐备时,才允许继续生成;当存在
|
||
/// <c>CqrsReflectionFallbackAttribute</c> 时,才允许输出依赖 fallback 元数据恢复的注册结果。
|
||
/// </param>
|
||
/// <param name="candidates">
|
||
/// 来自语法和语义分析阶段的 handler 候选结果。
|
||
/// 集合中可能包含 <see langword="null" /> 占位项,且同一实现类型可能因 partial 声明重复出现,后续会统一去重并聚合。
|
||
/// </param>
|
||
/// <remarks>
|
||
/// <para>
|
||
/// 该方法负责发射两类生成结果:注册器类型本体,以及在静态类型信息不足时用于运行时补全注册的程序集级
|
||
/// <c>CqrsReflectionFallbackAttribute</c> 元数据。生成这些结果的目标是把可静态确定的 handler 注册尽量前移到编译期,
|
||
/// 从而减少运行时程序集扫描成本,同时保留对少数复杂类型形态的兼容回退路径。
|
||
/// </para>
|
||
/// <para>
|
||
/// 该阶段依赖两个语义前提:一是 runtime 已提供 CQRS 注册器生成所需的基础合同;二是只要存在任何 handler
|
||
/// 需要通过 reflection fallback 恢复,就必须同时存在承载该元数据的
|
||
/// <c>CqrsReflectionFallbackAttribute</c>。如果基础合同缺失,生成器会静默跳过本轮发射;如果候选集合去重后没有任何可注册
|
||
/// handler,也会直接跳过 <c>AddSource</c>,避免输出空注册器。
|
||
/// </para>
|
||
/// <para>
|
||
/// 当 fallback handler 元数据非空但 runtime 缺少 <c>CqrsReflectionFallbackAttribute</c> 时,
|
||
/// 该方法会报告 <c>GF_Cqrs_001</c> 并停止发射源码。这样可以避免生成一个表面可用、但会静默漏掉部分 handler 注册的半成品
|
||
/// registry。只有在静态注册结果与 fallback 契约同时成立时,才允许调用 <c>AddSource</c>。
|
||
/// </para>
|
||
/// </remarks>
|
||
private static void Execute(
|
||
SourceProductionContext context,
|
||
GenerationEnvironment generationEnvironment,
|
||
ImmutableArray<HandlerCandidateAnalysis?> candidates)
|
||
{
|
||
if (!generationEnvironment.GenerationEnabled)
|
||
return;
|
||
|
||
var registrations = CollectRegistrations(candidates);
|
||
|
||
if (registrations.Count == 0)
|
||
return;
|
||
|
||
var fallbackHandlerTypeMetadataNames = registrations
|
||
.Select(static registration => registration.ReflectionFallbackHandlerTypeMetadataName)
|
||
.Where(static typeMetadataName => !string.IsNullOrWhiteSpace(typeMetadataName))
|
||
.Distinct(StringComparer.Ordinal)
|
||
.Cast<string>()
|
||
.ToArray();
|
||
|
||
if (!CanEmitGeneratedRegistry(
|
||
generationEnvironment.SupportsReflectionFallbackAttribute,
|
||
fallbackHandlerTypeMetadataNames.Length))
|
||
{
|
||
ReportMissingReflectionFallbackContractDiagnostic(
|
||
context,
|
||
fallbackHandlerTypeMetadataNames);
|
||
return;
|
||
}
|
||
|
||
context.AddSource(
|
||
HintName,
|
||
GenerateSource(generationEnvironment, registrations, fallbackHandlerTypeMetadataNames));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 判断当前轮次是否允许输出生成注册器。
|
||
/// </summary>
|
||
/// <param name="supportsReflectionFallbackAttribute">
|
||
/// runtime 合同中是否存在 <c>CqrsReflectionFallbackAttribute</c>,以承载生成器无法静态精确表达的 handler 回退元数据。
|
||
/// </param>
|
||
/// <param name="fallbackHandlerTypeCount">
|
||
/// 当前轮次需要依赖程序集级 reflection fallback 元数据恢复的 handler 数量。
|
||
/// </param>
|
||
/// <returns>
|
||
/// 当没有 handler 依赖 fallback,或 runtime 已提供承载该元数据的特性契约时返回 <see langword="true" />;
|
||
/// 否则返回 <see langword="false" />,调用方必须放弃生成以避免输出会静默漏注册的半成品注册器。
|
||
/// </returns>
|
||
private static bool CanEmitGeneratedRegistry(
|
||
bool supportsReflectionFallbackAttribute,
|
||
int fallbackHandlerTypeCount)
|
||
{
|
||
return fallbackHandlerTypeCount == 0 || supportsReflectionFallbackAttribute;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 报告当前轮次因缺少 fallback 元数据承载契约而无法安全生成注册器的诊断。
|
||
/// </summary>
|
||
/// <param name="context">源生产上下文。</param>
|
||
/// <param name="fallbackHandlerTypeMetadataNames">需要通过程序集级 reflection fallback 元数据恢复的 handler 元数据名称。</param>
|
||
private static void ReportMissingReflectionFallbackContractDiagnostic(
|
||
SourceProductionContext context,
|
||
IReadOnlyList<string> fallbackHandlerTypeMetadataNames)
|
||
{
|
||
var handlerList = string.Join(
|
||
", ",
|
||
fallbackHandlerTypeMetadataNames.OrderBy(static name => name, StringComparer.Ordinal));
|
||
context.ReportDiagnostic(Diagnostic.Create(
|
||
MissingReflectionFallbackContractDiagnostic,
|
||
Location.None,
|
||
handlerList,
|
||
CqrsReflectionFallbackAttributeMetadataName));
|
||
}
|
||
|
||
private static List<ImplementationRegistrationSpec> CollectRegistrations(
|
||
ImmutableArray<HandlerCandidateAnalysis?> candidates)
|
||
{
|
||
var registrations = new List<ImplementationRegistrationSpec>();
|
||
|
||
// Partial declarations surface the same symbol through multiple syntax nodes.
|
||
// Collapse them by implementation type so direct and reflected registrations stay stable and duplicate-free.
|
||
var uniqueCandidates = new Dictionary<string, HandlerCandidateAnalysis>(StringComparer.Ordinal);
|
||
|
||
foreach (var candidate in candidates)
|
||
{
|
||
if (candidate is null)
|
||
continue;
|
||
|
||
uniqueCandidates[candidate.Value.ImplementationTypeDisplayName] = candidate.Value;
|
||
}
|
||
|
||
foreach (var candidate in uniqueCandidates.Values)
|
||
{
|
||
registrations.Add(new ImplementationRegistrationSpec(
|
||
candidate.ImplementationTypeDisplayName,
|
||
candidate.ImplementationLogName,
|
||
candidate.Registrations,
|
||
candidate.ReflectedImplementationRegistrations,
|
||
candidate.PreciseReflectedRegistrations,
|
||
candidate.ReflectionTypeMetadataName,
|
||
candidate.ReflectionFallbackHandlerTypeMetadataName));
|
||
}
|
||
|
||
registrations.Sort(static (left, right) =>
|
||
StringComparer.Ordinal.Compare(left.ImplementationLogName, right.ImplementationLogName));
|
||
return registrations;
|
||
}
|
||
|
||
private static bool IsConcreteHandlerType(INamedTypeSymbol type)
|
||
{
|
||
return type.TypeKind is TypeKind.Class or TypeKind.Struct &&
|
||
!type.IsAbstract &&
|
||
!ContainsGenericParameters(type);
|
||
}
|
||
|
||
private static bool ContainsGenericParameters(INamedTypeSymbol type)
|
||
{
|
||
for (var current = type; current is not null; current = current.ContainingType)
|
||
{
|
||
if (current.TypeParameters.Length > 0)
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private static bool IsSupportedHandlerInterface(INamedTypeSymbol interfaceType)
|
||
{
|
||
if (!interfaceType.IsGenericType)
|
||
return false;
|
||
|
||
var definitionMetadataName = GetFullyQualifiedMetadataName(interfaceType.OriginalDefinition);
|
||
return string.Equals(definitionMetadataName, IRequestHandlerMetadataName, StringComparison.Ordinal) ||
|
||
string.Equals(definitionMetadataName, INotificationHandlerMetadataName, StringComparison.Ordinal) ||
|
||
string.Equals(definitionMetadataName, IStreamRequestHandlerMetadataName, StringComparison.Ordinal);
|
||
}
|
||
|
||
private static string GetFullyQualifiedMetadataName(INamedTypeSymbol type)
|
||
{
|
||
var nestedTypes = new Stack<string>();
|
||
for (var current = type; current is not null; current = current.ContainingType)
|
||
{
|
||
nestedTypes.Push(current.MetadataName);
|
||
}
|
||
|
||
var builder = new StringBuilder();
|
||
if (!type.ContainingNamespace.IsGlobalNamespace)
|
||
{
|
||
builder.Append(type.ContainingNamespace.ToDisplayString());
|
||
builder.Append('.');
|
||
}
|
||
|
||
while (nestedTypes.Count > 0)
|
||
{
|
||
builder.Append(nestedTypes.Pop());
|
||
if (nestedTypes.Count > 0)
|
||
builder.Append('.');
|
||
}
|
||
|
||
return builder.ToString();
|
||
}
|
||
|
||
private static string GetReflectionTypeMetadataName(INamedTypeSymbol type)
|
||
{
|
||
var nestedTypes = new Stack<string>();
|
||
for (var current = type; current is not null; current = current.ContainingType)
|
||
{
|
||
nestedTypes.Push(current.MetadataName);
|
||
}
|
||
|
||
var builder = new StringBuilder();
|
||
if (!type.ContainingNamespace.IsGlobalNamespace)
|
||
{
|
||
builder.Append(type.ContainingNamespace.ToDisplayString());
|
||
builder.Append('.');
|
||
}
|
||
|
||
var isFirstType = true;
|
||
while (nestedTypes.Count > 0)
|
||
{
|
||
if (!isFirstType)
|
||
builder.Append('+');
|
||
|
||
builder.Append(nestedTypes.Pop());
|
||
isFirstType = false;
|
||
}
|
||
|
||
return builder.ToString();
|
||
}
|
||
|
||
private static string GetTypeSortKey(ITypeSymbol type)
|
||
{
|
||
return type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||
}
|
||
|
||
private static string GetLogDisplayName(ITypeSymbol type)
|
||
{
|
||
return GetTypeSortKey(type).Replace("global::", string.Empty);
|
||
}
|
||
}
|