Merge pull request #269 from GeWuYou/fix/analyzer-warning-reduction-batch

Fix/analyzer warning reduction batch
This commit is contained in:
gewuyou 2026-04-23 10:09:52 +08:00 committed by GitHub
commit 9656393fbb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 4479 additions and 1779 deletions

View File

@ -31,6 +31,12 @@ All AI agents and contributors must follow these rules when writing, reviewing,
- Every completed task MUST pass at least one build validation before it is considered done.
- If the task changes multiple projects or shared abstractions, prefer a solution-level or affected-project
`dotnet build ... -c Release`; otherwise use the smallest build command that still proves the result compiles.
- When a task adds a feature or modifies code, contributors MUST run a Release build for every directly affected
module/project instead of relying on an unrelated project or solution slice that does not actually compile the touched
code.
- Warnings reported by those affected-module builds are part of the task scope. Contributors MUST resolve the touched
module's build warnings in the same change, or stop and explicitly report the exact warning IDs and blocker instead of
deferring them to a separate long-lived cleanup branch by default.
- If the required build passes and there are task-related staged or unstaged changes, contributors MUST create a Git
commit automatically instead of leaving the task uncommitted, unless the user explicitly says not to commit.
- Commit messages MUST use Conventional Commits format: `<type>(<scope>): <summary>`.

View File

@ -96,6 +96,7 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
var interfaceName = iContextAware.ToDisplayString(
SymbolDisplayFormat.FullyQualifiedFormat);
var memberNames = CreateGeneratedContextMemberNames(symbol);
sb.AppendLine("/// <summary>");
sb.AppendLine("/// 为当前规则类型补充自动生成的架构上下文访问实现。");
sb.AppendLine("/// </summary>");
@ -107,15 +108,15 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
sb.AppendLine(
"/// 已缓存的实例上下文需要通过 <see cref=\"GFramework.Core.Abstractions.Rule.IContextAware.SetContext(GFramework.Core.Abstractions.Architectures.IArchitectureContext)\" /> 显式覆盖。");
sb.AppendLine(
"/// 与手动继承 <see cref=\"global::GFramework.Core.Rule.ContextAwareBase\" /> 的路径相比,生成实现会使用 <c>_contextSync</c> 协调惰性初始化、provider 切换和显式上下文注入;");
$"/// 与手动继承 <see cref=\"global::GFramework.Core.Rule.ContextAwareBase\" /> 的路径相比,生成实现会使用 <c>{memberNames.SyncFieldName}</c> 协调惰性初始化、provider 切换和显式上下文注入;");
sb.AppendLine(
"/// <see cref=\"global::GFramework.Core.Rule.ContextAwareBase\" /> 则保持无锁的实例级缓存语义,更适合已经由调用方线程模型保证串行访问的简单场景。");
sb.AppendLine("/// </remarks>");
sb.AppendLine($"partial class {symbol.Name} : {interfaceName}");
sb.AppendLine("{");
GenerateContextProperty(sb);
GenerateInterfaceImplementations(sb, iContextAware);
GenerateContextProperty(sb, memberNames);
GenerateInterfaceImplementations(sb, iContextAware, memberNames);
sb.AppendLine("}");
return sb.ToString().TrimEnd();
@ -138,13 +139,40 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
/// 生成Context属性
/// </summary>
/// <param name="sb">字符串构建器</param>
private static void GenerateContextProperty(StringBuilder sb)
/// <param name="memberNames">当前目标类型应使用的上下文字段名。</param>
private static void GenerateContextProperty(
StringBuilder sb,
GeneratedContextMemberNames memberNames)
{
GenerateContextBackingFields(sb, memberNames);
GenerateContextGetter(sb, memberNames);
GenerateContextProviderConfiguration(sb, memberNames);
}
/// <summary>
/// 生成上下文缓存和同步所需的字段。
/// </summary>
/// <param name="sb">字符串构建器。</param>
private static void GenerateContextBackingFields(
StringBuilder sb,
GeneratedContextMemberNames memberNames)
{
sb.AppendLine(" private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _context;");
sb.AppendLine(
" private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? _contextProvider;");
sb.AppendLine(" private static readonly object _contextSync = new();");
$" private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? {memberNames.ContextFieldName};");
sb.AppendLine(
$" private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? {memberNames.ProviderFieldName};");
sb.AppendLine($" private static readonly object {memberNames.SyncFieldName} = new();");
sb.AppendLine();
}
/// <summary>
/// 生成实例上下文访问器,包含显式注入优先和 provider 惰性回退语义。
/// </summary>
/// <param name="sb">字符串构建器。</param>
private static void GenerateContextGetter(
StringBuilder sb,
GeneratedContextMemberNames memberNames)
{
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// 获取当前实例绑定的架构上下文。");
sb.AppendLine(" /// </summary>");
@ -158,7 +186,7 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
sb.AppendLine(
" /// 或 <see cref=\"ResetContextProvider\" /> 不会自动清除此缓存;如需覆盖,请显式调用 <c>IContextAware.SetContext(...)</c>。");
sb.AppendLine(
" /// 当前实现还假设 <see cref=\"GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider.GetContext\" /> 可在持有 <c>_contextSync</c> 时安全执行;");
$" /// 当前实现还假设 <see cref=\"GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider.GetContext\" /> 可在持有 <c>{memberNames.SyncFieldName}</c> 时安全执行;");
sb.AppendLine(
" /// 自定义 provider 不应在该调用链内重新进入当前类型的 provider 配置 API且应避免引入与外部全局锁相互等待的锁顺序。");
sb.AppendLine(" /// </remarks>");
@ -166,29 +194,35 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
sb.AppendLine(" {");
sb.AppendLine(" get");
sb.AppendLine(" {");
sb.AppendLine(" var context = _context;");
sb.AppendLine(" if (context is not null)");
sb.AppendLine(" {");
sb.AppendLine(" return context;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" // 在同一个同步域内协调懒加载与 provider 切换,避免读取到被并发重置的空提供者。");
sb.AppendLine(
" // provider 的 GetContext() 会在持有 _contextSync 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。");
sb.AppendLine(" lock (_contextSync)");
$" // provider 的 GetContext() 会在持有 {memberNames.SyncFieldName} 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。");
sb.AppendLine($" lock ({memberNames.SyncFieldName})");
sb.AppendLine(" {");
sb.AppendLine(
" _contextProvider ??= new global::GFramework.Core.Architectures.GameContextProvider();");
sb.AppendLine(" _context ??= _contextProvider.GetContext();");
sb.AppendLine(" return _context;");
$" {memberNames.ProviderFieldName} ??= new global::GFramework.Core.Architectures.GameContextProvider();");
sb.AppendLine($" {memberNames.ContextFieldName} ??= {memberNames.ProviderFieldName}.GetContext();");
sb.AppendLine($" return {memberNames.ContextFieldName};");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine();
}
/// <summary>
/// 生成静态 provider 配置 API供测试和宿主在懒加载前替换默认上下文来源。
/// </summary>
/// <param name="sb">字符串构建器。</param>
private static void GenerateContextProviderConfiguration(
StringBuilder sb,
GeneratedContextMemberNames memberNames)
{
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// 配置当前生成类型共享的上下文提供者。");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" /// <param name=\"provider\">后续懒加载上下文时要使用的提供者实例。</param>");
sb.AppendLine(
" /// <exception cref=\"global::System.ArgumentNullException\">当 <paramref name=\"provider\" /> 为 null 时抛出。</exception>");
sb.AppendLine(" /// <remarks>");
sb.AppendLine(" /// 该方法使用与 <see cref=\"Context\" /> 相同的同步锁,避免提供者切换与惰性初始化交错。");
sb.AppendLine(
@ -198,9 +232,10 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
sb.AppendLine(
" public static void SetContextProvider(global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider provider)");
sb.AppendLine(" {");
sb.AppendLine(" lock (_contextSync)");
sb.AppendLine(" global::System.ArgumentNullException.ThrowIfNull(provider);");
sb.AppendLine($" lock ({memberNames.SyncFieldName})");
sb.AppendLine(" {");
sb.AppendLine(" _contextProvider = provider;");
sb.AppendLine($" {memberNames.ProviderFieldName} = provider;");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine();
@ -215,9 +250,9 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
sb.AppendLine(" /// </remarks>");
sb.AppendLine(" public static void ResetContextProvider()");
sb.AppendLine(" {");
sb.AppendLine(" lock (_contextSync)");
sb.AppendLine($" lock ({memberNames.SyncFieldName})");
sb.AppendLine(" {");
sb.AppendLine(" _contextProvider = null;");
sb.AppendLine($" {memberNames.ProviderFieldName} = null;");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine();
@ -234,7 +269,8 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
/// <param name="interfaceSymbol">接口符号</param>
private static void GenerateInterfaceImplementations(
StringBuilder sb,
INamedTypeSymbol interfaceSymbol)
INamedTypeSymbol interfaceSymbol,
GeneratedContextMemberNames memberNames)
{
var interfaceName = interfaceSymbol.ToDisplayString(
SymbolDisplayFormat.FullyQualifiedFormat);
@ -244,7 +280,7 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
if (method.MethodKind != MethodKind.Ordinary)
continue;
GenerateMethod(sb, interfaceName, method);
GenerateMethod(sb, interfaceName, method, memberNames);
sb.AppendLine();
}
}
@ -258,7 +294,8 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
private static void GenerateMethod(
StringBuilder sb,
string interfaceName,
IMethodSymbol method)
IMethodSymbol method,
GeneratedContextMemberNames memberNames)
{
var returnType = method.ReturnType.ToDisplayString(
SymbolDisplayFormat.FullyQualifiedFormat);
@ -271,7 +308,7 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
$" {returnType} {interfaceName}.{method.Name}({parameters})");
sb.AppendLine(" {");
GenerateMethodBody(sb, method);
GenerateMethodBody(sb, method, memberNames);
sb.AppendLine(" }");
}
@ -283,15 +320,16 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
/// <param name="method">方法符号</param>
private static void GenerateMethodBody(
StringBuilder sb,
IMethodSymbol method)
IMethodSymbol method,
GeneratedContextMemberNames memberNames)
{
switch (method.Name)
{
case "SetContext":
sb.AppendLine(" // 与 Context getter 共享同一同步协议,避免显式注入被并发懒加载覆盖。");
sb.AppendLine(" lock (_contextSync)");
sb.AppendLine($" lock ({memberNames.SyncFieldName})");
sb.AppendLine(" {");
sb.AppendLine(" _context = context;");
sb.AppendLine($" {memberNames.ContextFieldName} = context;");
sb.AppendLine(" }");
break;
@ -307,4 +345,75 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
break;
}
}
/// <summary>
/// 为生成字段选择不会与目标类型现有成员冲突的稳定名称。
/// </summary>
/// <param name="symbol">当前需要补充 ContextAware 实现的目标类型。</param>
/// <returns>当前生成轮次应使用的上下文字段名集合。</returns>
private static GeneratedContextMemberNames CreateGeneratedContextMemberNames(INamedTypeSymbol symbol)
{
var reservedNames = CollectReservedContextMemberNames(symbol);
return new GeneratedContextMemberNames(
AllocateGeneratedMemberName(reservedNames, "_gFrameworkContextAwareContext"),
AllocateGeneratedMemberName(reservedNames, "_gFrameworkContextAwareProvider"),
AllocateGeneratedMemberName(reservedNames, "_gFrameworkContextAwareSync"));
}
/// <summary>
/// 收集当前类型及其基类链上所有显式声明的成员名,确保生成字段不会意外隐藏继承成员。
/// </summary>
/// <param name="symbol">当前需要补充 ContextAware 实现的目标类型。</param>
/// <returns>已被当前类型层级占用的成员名集合。</returns>
private static HashSet<string> CollectReservedContextMemberNames(INamedTypeSymbol symbol)
{
var reservedNames = new HashSet<string>(StringComparer.Ordinal);
// Walk the full inheritance chain so numeric suffix allocation also covers members introduced by base types.
for (var currentType = symbol; currentType is not null; currentType = currentType.BaseType)
{
foreach (var member in currentType.GetMembers())
{
if (!member.IsImplicitlyDeclared)
{
reservedNames.Add(member.Name);
}
}
}
return reservedNames;
}
/// <summary>
/// 在固定前缀基础上按顺序追加数字后缀,直到找到可安全写入的成员名。
/// </summary>
/// <param name="reservedNames">当前类型已占用或已为其他生成字段保留的名称集合。</param>
/// <param name="baseName">优先尝试的基础名称。</param>
/// <returns>本轮生成可以使用的唯一成员名。</returns>
private static string AllocateGeneratedMemberName(
ISet<string> reservedNames,
string baseName)
{
if (reservedNames.Add(baseName))
return baseName;
for (var suffix = 1; ; suffix++)
{
var candidateName = $"{baseName}{suffix}";
if (reservedNames.Add(candidateName))
return candidateName;
}
}
/// <summary>
/// 描述一次 ContextAware 代码生成中选定的上下文字段名。
/// </summary>
/// <param name="ContextFieldName">实例上下文缓存字段名。</param>
/// <param name="ProviderFieldName">共享上下文提供者字段名。</param>
/// <param name="SyncFieldName">用于串行化访问的同步字段名。</param>
private readonly record struct GeneratedContextMemberNames(
string ContextFieldName,
string ProviderFieldName,
string SyncFieldName);
}

View File

@ -99,7 +99,7 @@ public class EasyEventsTests
}
/// <summary>
/// 测试并发场景下AddEvent的行为
/// 测试 AddEvent 对重复事件类型保持兼容的参数异常类型。
/// </summary>
[Test]
public void AddEvent_Should_Throw_When_Already_Registered()
@ -167,4 +167,4 @@ public class EasyEventsTests
Assert.That(_easyEvents.GetEvent<Event<int, string>>(), Is.Not.Null);
Assert.That(_easyEvents.GetEvent<Event<double>>(), Is.Not.Null);
}
}
}

View File

@ -165,6 +165,30 @@ public class CollectionExtensionsTests
Assert.That(result["c"], Is.EqualTo(3));
}
/// <summary>
/// 测试ToDictionarySafe保持具体Dictionary返回类型避免公开API继续收窄。
/// </summary>
[Test]
public void ToDictionarySafe_Should_Preserve_Concrete_Return_Type()
{
var method = typeof(GFramework.Core.Extensions.CollectionExtensions)
.GetMethods()
.Single(static method => method.Name == nameof(GFramework.Core.Extensions.CollectionExtensions.ToDictionarySafe));
var methodGenericArguments = method.GetGenericArguments();
var returnTypeGenericArguments = method.ReturnType.GetGenericArguments();
Assert.Multiple(() =>
{
Assert.That(method.IsGenericMethodDefinition, Is.True);
Assert.That(method.ReturnType.IsGenericType, Is.True);
Assert.That(method.ReturnType.GetGenericTypeDefinition(), Is.EqualTo(typeof(Dictionary<,>)));
Assert.That(methodGenericArguments.Select(static argument => argument.Name), Is.EqualTo(new[] { "T", "TKey", "TValue" }));
Assert.That(returnTypeGenericArguments, Has.Length.EqualTo(2));
Assert.That(returnTypeGenericArguments[0], Is.SameAs(methodGenericArguments[1]));
Assert.That(returnTypeGenericArguments[1], Is.SameAs(methodGenericArguments[2]));
});
}
/// <summary>
/// 测试ToDictionarySafe方法在存在重复键时覆盖前面的值
/// </summary>
@ -224,4 +248,4 @@ public class CollectionExtensionsTests
Assert.Throws<ArgumentNullException>(() =>
items.ToDictionarySafe<(string, int), string, int>(x => x.Item1, null!));
}
}
}

View File

@ -39,6 +39,40 @@ public class LoggingConfigurationTests
Assert.That(config.LoggerLevels["GFramework.Core"], Is.EqualTo(LogLevel.Trace));
}
[Test]
public void Configuration_Collections_Should_Preserve_Public_Concrete_Types()
{
Assert.Multiple(() =>
{
Assert.That(
typeof(LoggingConfiguration).GetProperty(nameof(LoggingConfiguration.Appenders))!.PropertyType,
Is.EqualTo(typeof(List<AppenderConfiguration>)));
Assert.That(
typeof(LoggingConfiguration).GetProperty(nameof(LoggingConfiguration.LoggerLevels))!.PropertyType,
Is.EqualTo(typeof(Dictionary<string, LogLevel>)));
Assert.That(
typeof(FilterConfiguration).GetProperty(nameof(FilterConfiguration.Namespaces))!.PropertyType,
Is.EqualTo(typeof(List<string>)));
Assert.That(
typeof(FilterConfiguration).GetProperty(nameof(FilterConfiguration.Filters))!.PropertyType,
Is.EqualTo(typeof(List<FilterConfiguration>)));
});
}
[Test]
public void LoggerLevels_Should_Remain_Case_Sensitive_By_Default()
{
var config = new LoggingConfiguration();
config.LoggerLevels["GFramework.Core"] = LogLevel.Info;
Assert.Multiple(() =>
{
Assert.That(config.LoggerLevels.ContainsKey("GFramework.Core"), Is.True);
Assert.That(config.LoggerLevels["GFramework.Core"], Is.EqualTo(LogLevel.Info));
Assert.That(config.LoggerLevels.ContainsKey("gframework.core"), Is.False);
});
}
[Test]
public void LoadFromJsonString_WithInvalidJson_ShouldThrow()
{

View File

@ -41,7 +41,7 @@ public sealed class CoroutineScheduler(
private readonly Dictionary<CoroutineHandle, CoroutineCompletionStatus> _completionStatuses = new();
private readonly Queue<CoroutineHandle> _completionStatusOrder = new();
private readonly Dictionary<string, HashSet<CoroutineHandle>> _grouped = new();
private readonly Dictionary<string, HashSet<CoroutineHandle>> _grouped = new(StringComparer.Ordinal);
private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CoroutineScheduler));
private readonly Dictionary<CoroutineHandle, CoroutineMetadata> _metadata = new();
private readonly ConcurrentQueue<CoroutineHandle> _pendingKills = new();
@ -50,7 +50,7 @@ public sealed class CoroutineScheduler(
throw new ArgumentNullException(nameof(timeSource));
private readonly CoroutineStatistics? _statistics = enableStatistics ? new CoroutineStatistics() : null;
private readonly Dictionary<string, HashSet<CoroutineHandle>> _tagged = new();
private readonly Dictionary<string, HashSet<CoroutineHandle>> _tagged = new(StringComparer.Ordinal);
private readonly ITimeSource _timeSource = timeSource ?? throw new ArgumentNullException(nameof(timeSource));
private readonly Dictionary<CoroutineHandle, HashSet<CoroutineHandle>> _waiting = new();
private int _nextSlot;

View File

@ -53,12 +53,14 @@ public class EasyEvents
/// 添加指定类型的事件到事件字典中
/// </summary>
/// <typeparam name="T">事件类型必须实现IEasyEvent接口且具有无参构造函数</typeparam>
/// <exception cref="ArgumentException">当事件类型已存在时抛出</exception>
/// <exception cref="ArgumentException">当事件类型已存在时抛出</exception>
public void AddEvent<T>() where T : IEvent, new()
{
if (!_mTypeEvents.TryAdd(typeof(T), new T()))
{
#pragma warning disable MA0015 // Preserve the public ArgumentException contract without inventing a fake parameter name.
throw new ArgumentException($"Event type {typeof(T).Name} already registered.");
#pragma warning restore MA0015
}
}
@ -81,4 +83,4 @@ public class EasyEvents
{
return (T)_mTypeEvents.GetOrAdd(typeof(T), _ => new T());
}
}
}

View File

@ -81,10 +81,12 @@ public static class CollectionExtensions
/// // dict["a"] == 3 (最后一个值)
/// </code>
/// </example>
#pragma warning disable MA0016 // Preserve the established concrete return type for public API compatibility.
public static Dictionary<TKey, TValue> ToDictionarySafe<T, TKey, TValue>(
this IEnumerable<T> source,
Func<T, TKey> keySelector,
Func<T, TValue> valueSelector) where TKey : notnull
#pragma warning restore MA0016
{
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(keySelector);

View File

@ -16,8 +16,23 @@ namespace GFramework.Core.Functional;
/// <summary>
/// 表示可能存在或不存在的值,用于替代 null 引用的函数式编程类型
/// </summary>
/// <remarks>
/// <para>
/// <see cref="Option{T}" /> 只表示两种显式状态:通过 <see cref="Some(T)" /> 创建的有值状态,以及
/// <see cref="None" /> 表示的无值状态;调用方不应把 <see cref="None" /> 当作 <see langword="null" /> 的别名。
/// </para>
/// <para>
/// <see cref="Some(T)" /> 会拒绝 <see langword="null" />,因此引用类型和可空引用类型参数都必须包装真实值;访问方应优先通过
/// <see cref="IsSome" />、<see cref="IsNone" />、模式匹配或 <c>Match</c>/<c>Map</c> 等函数式 API 消费结果,而不是假设默认值
/// 与无值状态等价。
/// </para>
/// <para>
/// 该结构体是不可变值类型;一旦创建,其状态与内部值不会再改变。但在 <see cref="IsNone" /> 为 <see langword="true" /> 时,
/// 调用需要真实值的方法仍应遵守各成员自身的契约与异常说明。
/// </para>
/// </remarks>
/// <typeparam name="T">值的类型</typeparam>
public readonly struct Option<T>
public readonly struct Option<T> : IEquatable<Option<T>>
{
private readonly T _value;
private readonly bool _isSome;
@ -313,4 +328,4 @@ public readonly struct Option<T>
_isSome ? $"Some({_value})" : "None";
#endregion
}
}

View File

@ -20,10 +20,14 @@ public sealed class FilterConfiguration
/// <summary>
/// 命名空间前缀列表(用于 Namespace 过滤器)。
/// </summary>
#pragma warning disable MA0016 // Preserve the established concrete configuration API surface.
public List<string>? Namespaces { get; set; }
#pragma warning restore MA0016
/// <summary>
/// 子过滤器列表(用于 Composite 过滤器)。
/// </summary>
#pragma warning disable MA0016 // Preserve the established concrete configuration API surface.
public List<FilterConfiguration>? Filters { get; set; }
#pragma warning restore MA0016
}

View File

@ -15,10 +15,15 @@ public sealed class LoggingConfiguration
/// <summary>
/// Appender 配置列表
/// </summary>
#pragma warning disable MA0016 // Preserve the established concrete configuration API surface.
public List<AppenderConfiguration> Appenders { get; set; } = new();
#pragma warning restore MA0016
/// <summary>
/// 特定 Logger 的日志级别配置
/// </summary>
public Dictionary<string, LogLevel> LoggerLevels { get; set; } = new(StringComparer.Ordinal);
#pragma warning disable MA0016 // Preserve the established concrete configuration API surface.
public Dictionary<string, LogLevel> LoggerLevels { get; set; } =
new Dictionary<string, LogLevel>(StringComparer.Ordinal);
#pragma warning restore MA0016
}

View File

@ -0,0 +1,284 @@
namespace GFramework.Cqrs.SourceGenerators.Cqrs;
/// <summary>
/// 为当前编译程序集生成 CQRS 处理器注册器,以减少运行时的程序集反射扫描成本。
/// </summary>
public sealed partial class CqrsHandlerRegistryGenerator
{
private readonly record struct HandlerRegistrationSpec(
string HandlerInterfaceDisplayName,
string ImplementationTypeDisplayName,
string HandlerInterfaceLogName,
string ImplementationLogName);
private readonly record struct ReflectedImplementationRegistrationSpec(
string HandlerInterfaceDisplayName,
string HandlerInterfaceLogName);
private readonly record struct OrderedRegistrationSpec(
string HandlerInterfaceLogName,
OrderedRegistrationKind Kind,
int Index);
private readonly record struct GeneratedRegistrySourceShape(
bool HasReflectedImplementationRegistrations,
bool HasPreciseReflectedRegistrations,
bool HasReflectionTypeLookups,
bool HasExternalAssemblyTypeLookups)
{
public bool RequiresRegistryAssemblyVariable =>
HasReflectedImplementationRegistrations ||
HasPreciseReflectedRegistrations ||
HasReflectionTypeLookups;
}
/// <summary>
/// 标记某条 handler 注册语句在生成阶段采用的表达策略。
/// </summary>
/// <remarks>
/// 该枚举只服务于输出排序与代码分支选择,用来保证生成注册器在“直接注册”
/// “反射实现类型查找”和“精确运行时类型解析”之间保持稳定顺序。
/// </remarks>
private enum OrderedRegistrationKind
{
Direct,
ReflectedImplementation,
PreciseReflected
}
/// <summary>
/// 描述生成注册器中某个运行时类型引用的构造方式。
/// </summary>
/// <remarks>
/// 某些 handler 服务类型可以直接以 <c>typeof(...)</c> 输出,某些则需要在运行时补充
/// 反射查找、数组封装或泛型实参重建。该记录把这些差异收敛为统一的递归结构,
/// 供源码输出阶段生成稳定的类型解析语句。
/// </remarks>
private sealed record RuntimeTypeReferenceSpec(
string? TypeDisplayName,
string? ReflectionTypeMetadataName,
string? ReflectionAssemblyName,
RuntimeTypeReferenceSpec? ArrayElementTypeReference,
int ArrayRank,
RuntimeTypeReferenceSpec? PointerElementTypeReference,
RuntimeTypeReferenceSpec? GenericTypeDefinitionReference,
ImmutableArray<RuntimeTypeReferenceSpec> GenericTypeArguments)
{
/// <summary>
/// 创建一个可直接通过 <c>typeof(...)</c> 表达的类型引用。
/// </summary>
public static RuntimeTypeReferenceSpec FromDirectReference(string typeDisplayName)
{
return new RuntimeTypeReferenceSpec(
typeDisplayName,
null,
null,
null,
0,
null,
null,
ImmutableArray<RuntimeTypeReferenceSpec>.Empty);
}
/// <summary>
/// 创建一个需要从当前消费端程序集反射解析的类型引用。
/// </summary>
public static RuntimeTypeReferenceSpec FromReflectionLookup(string reflectionTypeMetadataName)
{
return new RuntimeTypeReferenceSpec(
null,
reflectionTypeMetadataName,
null,
null,
0,
null,
null,
ImmutableArray<RuntimeTypeReferenceSpec>.Empty);
}
/// <summary>
/// 创建一个需要从被引用程序集反射解析的类型引用。
/// </summary>
public static RuntimeTypeReferenceSpec FromExternalReflectionLookup(
string reflectionAssemblyName,
string reflectionTypeMetadataName)
{
return new RuntimeTypeReferenceSpec(
null,
reflectionTypeMetadataName,
reflectionAssemblyName,
null,
0,
null,
null,
ImmutableArray<RuntimeTypeReferenceSpec>.Empty);
}
/// <summary>
/// 创建一个数组类型引用。
/// </summary>
public static RuntimeTypeReferenceSpec FromArray(RuntimeTypeReferenceSpec elementTypeReference, int arrayRank)
{
return new RuntimeTypeReferenceSpec(
null,
null,
null,
elementTypeReference,
arrayRank,
null,
null,
ImmutableArray<RuntimeTypeReferenceSpec>.Empty);
}
/// <summary>
/// 创建一个封闭泛型类型引用。
/// </summary>
public static RuntimeTypeReferenceSpec FromConstructedGeneric(
RuntimeTypeReferenceSpec genericTypeDefinitionReference,
ImmutableArray<RuntimeTypeReferenceSpec> genericTypeArguments)
{
return new RuntimeTypeReferenceSpec(
null,
null,
null,
null,
0,
null,
genericTypeDefinitionReference,
genericTypeArguments);
}
}
private readonly record struct PreciseReflectedRegistrationSpec(
string OpenHandlerTypeDisplayName,
string HandlerInterfaceLogName,
ImmutableArray<RuntimeTypeReferenceSpec> ServiceTypeArguments);
private readonly record struct ImplementationRegistrationSpec(
string ImplementationTypeDisplayName,
string ImplementationLogName,
ImmutableArray<HandlerRegistrationSpec> DirectRegistrations,
ImmutableArray<ReflectedImplementationRegistrationSpec> ReflectedImplementationRegistrations,
ImmutableArray<PreciseReflectedRegistrationSpec> PreciseReflectedRegistrations,
string? ReflectionTypeMetadataName,
string? ReflectionFallbackHandlerTypeMetadataName);
private readonly struct HandlerCandidateAnalysis : IEquatable<HandlerCandidateAnalysis>
{
public HandlerCandidateAnalysis(
string implementationTypeDisplayName,
string implementationLogName,
ImmutableArray<HandlerRegistrationSpec> registrations,
ImmutableArray<ReflectedImplementationRegistrationSpec> reflectedImplementationRegistrations,
ImmutableArray<PreciseReflectedRegistrationSpec> preciseReflectedRegistrations,
string? reflectionTypeMetadataName,
string? reflectionFallbackHandlerTypeMetadataName)
{
ImplementationTypeDisplayName = implementationTypeDisplayName;
ImplementationLogName = implementationLogName;
Registrations = registrations;
ReflectedImplementationRegistrations = reflectedImplementationRegistrations;
PreciseReflectedRegistrations = preciseReflectedRegistrations;
ReflectionTypeMetadataName = reflectionTypeMetadataName;
ReflectionFallbackHandlerTypeMetadataName = reflectionFallbackHandlerTypeMetadataName;
}
public string ImplementationTypeDisplayName { get; }
public string ImplementationLogName { get; }
public ImmutableArray<HandlerRegistrationSpec> Registrations { get; }
public ImmutableArray<ReflectedImplementationRegistrationSpec> ReflectedImplementationRegistrations { get; }
public ImmutableArray<PreciseReflectedRegistrationSpec> PreciseReflectedRegistrations { get; }
public string? ReflectionTypeMetadataName { get; }
public string? ReflectionFallbackHandlerTypeMetadataName { get; }
public bool Equals(HandlerCandidateAnalysis other)
{
if (!string.Equals(ImplementationTypeDisplayName, other.ImplementationTypeDisplayName,
StringComparison.Ordinal) ||
!string.Equals(ImplementationLogName, other.ImplementationLogName, StringComparison.Ordinal) ||
!string.Equals(ReflectionTypeMetadataName, other.ReflectionTypeMetadataName,
StringComparison.Ordinal) ||
!string.Equals(
ReflectionFallbackHandlerTypeMetadataName,
other.ReflectionFallbackHandlerTypeMetadataName,
StringComparison.Ordinal) ||
Registrations.Length != other.Registrations.Length ||
ReflectedImplementationRegistrations.Length != other.ReflectedImplementationRegistrations.Length ||
PreciseReflectedRegistrations.Length != other.PreciseReflectedRegistrations.Length)
{
return false;
}
for (var index = 0; index < Registrations.Length; index++)
{
if (!Registrations[index].Equals(other.Registrations[index]))
return false;
}
for (var index = 0; index < ReflectedImplementationRegistrations.Length; index++)
{
if (!ReflectedImplementationRegistrations[index].Equals(
other.ReflectedImplementationRegistrations[index]))
{
return false;
}
}
for (var index = 0; index < PreciseReflectedRegistrations.Length; index++)
{
if (!PreciseReflectedRegistrations[index].Equals(other.PreciseReflectedRegistrations[index]))
return false;
}
return true;
}
public override bool Equals(object? obj)
{
return obj is HandlerCandidateAnalysis other && Equals(other);
}
public override int GetHashCode()
{
unchecked
{
var hashCode = StringComparer.Ordinal.GetHashCode(ImplementationTypeDisplayName);
hashCode = (hashCode * 397) ^ StringComparer.Ordinal.GetHashCode(ImplementationLogName);
hashCode = (hashCode * 397) ^
(ReflectionTypeMetadataName is null
? 0
: StringComparer.Ordinal.GetHashCode(ReflectionTypeMetadataName));
hashCode = (hashCode * 397) ^
(ReflectionFallbackHandlerTypeMetadataName is null
? 0
: StringComparer.Ordinal.GetHashCode(ReflectionFallbackHandlerTypeMetadataName));
foreach (var registration in Registrations)
{
hashCode = (hashCode * 397) ^ registration.GetHashCode();
}
foreach (var reflectedImplementationRegistration in ReflectedImplementationRegistrations)
{
hashCode = (hashCode * 397) ^ reflectedImplementationRegistration.GetHashCode();
}
foreach (var preciseReflectedRegistration in PreciseReflectedRegistrations)
{
hashCode = (hashCode * 397) ^ preciseReflectedRegistration.GetHashCode();
}
return hashCode;
}
}
}
private readonly record struct GenerationEnvironment(
bool GenerationEnabled,
bool SupportsReflectionFallbackAttribute);
}

View File

@ -0,0 +1,327 @@
namespace GFramework.Cqrs.SourceGenerators.Cqrs;
/// <summary>
/// 为当前编译程序集生成 CQRS 处理器注册器,以减少运行时的程序集反射扫描成本。
/// </summary>
public sealed partial class CqrsHandlerRegistryGenerator
{
/// <summary>
/// 为无法直接在生成代码中书写的关闭处理器接口构造精确的运行时注册描述。
/// </summary>
/// <param name="compilation">
/// 当前生成轮次对应的编译上下文,用于判断类型是否属于当前程序集,从而决定是生成直接类型引用还是延迟到运行时反射解析。
/// </param>
/// <param name="handlerInterface">
/// 需要注册的关闭处理器接口。调用方应保证它来自受支持的 CQRS 处理器接口定义,并且其泛型参数顺序与运行时注册约定一致。
/// </param>
/// <param name="registration">
/// 当方法返回 <see langword="true" /> 时,包含开放泛型处理器类型和每个运行时类型实参的精确描述;
/// 当方法返回 <see langword="false" /> 时,为默认值,调用方应回退到基于实现类型的宽松反射发现路径。
/// </param>
/// <returns>
/// 当接口上的所有运行时类型引用都能在生成阶段被稳定描述时返回 <see langword="true" />
/// 只要任一泛型实参无法安全编码到生成输出中,就返回 <see langword="false" />。
/// </returns>
private static bool TryCreatePreciseReflectedRegistration(
Compilation compilation,
INamedTypeSymbol handlerInterface,
out PreciseReflectedRegistrationSpec registration)
{
var openHandlerTypeDisplayName = handlerInterface.OriginalDefinition
.ConstructUnboundGenericType()
.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
var typeArguments =
ImmutableArray.CreateBuilder<RuntimeTypeReferenceSpec>(handlerInterface.TypeArguments.Length);
foreach (var typeArgument in handlerInterface.TypeArguments)
{
if (!TryCreateRuntimeTypeReference(compilation, typeArgument, out var runtimeTypeReference))
{
registration = default;
return false;
}
typeArguments.Add(runtimeTypeReference!);
}
registration = new PreciseReflectedRegistrationSpec(
openHandlerTypeDisplayName,
GetLogDisplayName(handlerInterface),
typeArguments.ToImmutable());
return true;
}
/// <summary>
/// 将 Roslyn 类型符号转换为生成注册器可消费的运行时类型引用描述。
/// </summary>
/// <param name="compilation">
/// 当前编译上下文,用于区分可直接引用的外部可访问类型与必须通过当前程序集运行时反射查找的内部类型。
/// </param>
/// <param name="type">
/// 需要转换的类型符号。该方法会递归处理数组元素类型和已构造泛型的类型实参,但不会为未绑定泛型或类型参数生成引用。
/// </param>
/// <param name="runtimeTypeReference">
/// 当方法返回 <see langword="true" /> 时,包含可直接引用、数组、已构造泛型或反射查找中的一种运行时表示;
/// 当方法返回 <see langword="false" /> 时为 <see langword="null" />,调用方应回退到更宽泛的实现类型反射扫描策略。
/// </param>
/// <returns>
/// 当 <paramref name="type" /> 及其递归子结构都能映射为稳定的运行时引用时返回 <see langword="true" />
/// 若遇到类型参数、无法访问的运行时结构,或任一递归分支无法表示,则返回 <see langword="false" />。
/// </returns>
private static bool TryCreateRuntimeTypeReference(
Compilation compilation,
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;
}
// Roslyn models dynamic as a pseudo-type, but generated C# cannot emit typeof(dynamic).
// Normalize it to the CLR runtime type so precise reflected registrations stay compilable.
if (type.TypeKind == TypeKind.Dynamic)
{
runtimeTypeReference = RuntimeTypeReferenceSpec.FromDirectReference("global::System.Object");
return true;
}
if (CanReferenceFromGeneratedRegistry(compilation, type))
{
runtimeTypeReference = RuntimeTypeReferenceSpec.FromDirectReference(
type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
return true;
}
if (type is IArrayTypeSymbol arrayType &&
TryCreateRuntimeTypeReference(compilation, arrayType.ElementType, out var elementTypeReference))
{
runtimeTypeReference = RuntimeTypeReferenceSpec.FromArray(elementTypeReference!, arrayType.Rank);
return true;
}
if (type is INamedTypeSymbol genericNamedType &&
genericNamedType.IsGenericType &&
!genericNamedType.IsUnboundGenericType)
{
return TryCreateConstructedGenericRuntimeTypeReference(
compilation,
genericNamedType,
out runtimeTypeReference);
}
if (type is INamedTypeSymbol namedType &&
TryCreateNamedRuntimeTypeReference(compilation, namedType, out var namedTypeReference))
{
runtimeTypeReference = namedTypeReference;
return true;
}
runtimeTypeReference = null;
return false;
}
/// <summary>
/// 为已构造泛型类型构造运行时类型引用,并递归验证每个泛型实参都可以稳定编码到生成输出中。
/// </summary>
/// <param name="compilation">当前生成轮次的编译上下文。</param>
/// <param name="genericNamedType">需要表示的已构造泛型类型。</param>
/// <param name="runtimeTypeReference">
/// 当方法返回 <see langword="true" /> 时,包含泛型定义和泛型实参的运行时重建描述。
/// </param>
/// <returns>当泛型定义和全部泛型实参都能表达时返回 <see langword="true" />。</returns>
private static bool TryCreateConstructedGenericRuntimeTypeReference(
Compilation compilation,
INamedTypeSymbol genericNamedType,
out RuntimeTypeReferenceSpec? runtimeTypeReference)
{
if (!TryCreateGenericTypeDefinitionReference(
compilation,
genericNamedType,
out var genericTypeDefinitionReference))
{
runtimeTypeReference = null;
return false;
}
var genericTypeArguments =
ImmutableArray.CreateBuilder<RuntimeTypeReferenceSpec>(genericNamedType.TypeArguments.Length);
foreach (var typeArgument in genericNamedType.TypeArguments)
{
if (!TryCreateRuntimeTypeReference(compilation, typeArgument, out var genericTypeArgumentReference))
{
runtimeTypeReference = null;
return false;
}
genericTypeArguments.Add(genericTypeArgumentReference!);
}
runtimeTypeReference = RuntimeTypeReferenceSpec.FromConstructedGeneric(
genericTypeDefinitionReference!,
genericTypeArguments.ToImmutable());
return true;
}
/// <summary>
/// 为无法直接书写的命名类型选择当前程序集反射查找或外部程序集反射查找表示。
/// </summary>
/// <param name="compilation">当前生成轮次的编译上下文。</param>
/// <param name="namedType">需要在运行时解析的命名类型。</param>
/// <param name="runtimeTypeReference">
/// 当方法返回 <see langword="true" /> 时,包含适合写入生成注册器的命名类型运行时引用;
/// 当返回 <see langword="false" /> 时,调用方应回退到更保守的注册路径。
/// </param>
/// <returns>当命名类型可安全编码为运行时引用时返回 <see langword="true" />。</returns>
private static bool TryCreateNamedRuntimeTypeReference(
Compilation compilation,
INamedTypeSymbol namedType,
out RuntimeTypeReferenceSpec? runtimeTypeReference)
{
return TryCreateReflectionLookupReference(
compilation,
namedType,
GetReflectionTypeMetadataName(namedType),
out runtimeTypeReference);
}
/// <summary>
/// 为已构造泛型类型解析其泛型定义的运行时引用描述。
/// </summary>
/// <param name="compilation">
/// 当前编译上下文,用于判断泛型定义是否应以内联类型引用形式生成,或在运行时通过当前程序集反射解析。
/// </param>
/// <param name="genericNamedType">
/// 已构造的泛型类型。该方法只处理其原始泛型定义,不负责递归解析类型实参。
/// </param>
/// <param name="genericTypeDefinitionReference">
/// 当方法返回 <see langword="true" /> 时,包含泛型定义的直接引用或反射查找描述;
/// 当方法返回 <see langword="false" /> 时为 <see langword="null" />,调用方应停止精确构造并回退到更保守的注册路径。
/// </param>
/// <returns>
/// 当泛型定义能够以稳定方式编码到生成输出中时返回 <see langword="true" />
/// 若泛型定义既不能直接引用,也不属于当前程序集可供反射查找,则返回 <see langword="false" />。
/// </returns>
private static bool TryCreateGenericTypeDefinitionReference(
Compilation compilation,
INamedTypeSymbol genericNamedType,
out RuntimeTypeReferenceSpec? genericTypeDefinitionReference)
{
var genericTypeDefinition = genericNamedType.OriginalDefinition;
if (CanReferenceFromGeneratedRegistry(compilation, genericTypeDefinition))
{
genericTypeDefinitionReference = RuntimeTypeReferenceSpec.FromDirectReference(
genericTypeDefinition
.ConstructUnboundGenericType()
.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
return true;
}
return TryCreateReflectionLookupReference(
compilation,
genericTypeDefinition,
GetReflectionTypeMetadataName(genericTypeDefinition),
out genericTypeDefinitionReference);
}
/// <summary>
/// 为当前程序集或外部程序集中的命名类型构造统一的运行时反射查找描述。
/// </summary>
/// <param name="compilation">当前生成轮次的编译上下文。</param>
/// <param name="namedType">需要在运行时解析的命名类型。</param>
/// <param name="metadataName">写入生成代码的反射元数据名称。</param>
/// <param name="runtimeTypeReference">成功时返回可直接写入注册器的运行时类型引用描述。</param>
/// <returns>当命名类型具备可稳定编码的程序集归属信息时返回 <see langword="true" />。</returns>
private static bool TryCreateReflectionLookupReference(
Compilation compilation,
INamedTypeSymbol namedType,
string metadataName,
out RuntimeTypeReferenceSpec? runtimeTypeReference)
{
if (SymbolEqualityComparer.Default.Equals(namedType.ContainingAssembly, compilation.Assembly))
{
runtimeTypeReference = RuntimeTypeReferenceSpec.FromReflectionLookup(metadataName);
return true;
}
if (namedType.ContainingAssembly is null)
{
runtimeTypeReference = null;
return false;
}
runtimeTypeReference = RuntimeTypeReferenceSpec.FromExternalReflectionLookup(
namedType.ContainingAssembly.Identity.ToString(),
metadataName);
return true;
}
private static bool CanReferenceFromGeneratedRegistry(Compilation compilation, ITypeSymbol type)
{
// Roslyn error symbols stringify to unresolved type names; emitting them via typeof(...) would turn
// an existing user-code error into a second generator-produced compile error instead of falling back.
if (type.TypeKind is TypeKind.Error or TypeKind.Dynamic)
return false;
switch (type)
{
case IArrayTypeSymbol arrayType:
return CanReferenceFromGeneratedRegistry(compilation, arrayType.ElementType);
case INamedTypeSymbol namedType:
if (!compilation.IsSymbolAccessibleWithin(namedType, compilation.Assembly, throughType: null))
return false;
foreach (var typeArgument in namedType.TypeArguments)
{
if (!CanReferenceFromGeneratedRegistry(compilation, typeArgument))
return false;
}
return true;
case IPointerTypeSymbol:
case IFunctionPointerTypeSymbol:
return false;
case ITypeParameterSymbol:
return false;
default:
// Remaining Roslyn type kinds that reach this branch have already been normalized by earlier guards
// and can continue through the direct-reference path without emitting fallback reflection code.
return true;
}
}
private static bool ContainsExternalAssemblyTypeLookup(RuntimeTypeReferenceSpec runtimeTypeReference)
{
if (!string.IsNullOrWhiteSpace(runtimeTypeReference.ReflectionAssemblyName))
return true;
if (runtimeTypeReference.ArrayElementTypeReference is not null &&
ContainsExternalAssemblyTypeLookup(runtimeTypeReference.ArrayElementTypeReference))
{
return true;
}
if (runtimeTypeReference.PointerElementTypeReference is not null &&
ContainsExternalAssemblyTypeLookup(runtimeTypeReference.PointerElementTypeReference))
{
return true;
}
if (runtimeTypeReference.GenericTypeDefinitionReference is not null &&
ContainsExternalAssemblyTypeLookup(runtimeTypeReference.GenericTypeDefinitionReference))
{
return true;
}
foreach (var genericTypeArgument in runtimeTypeReference.GenericTypeArguments)
{
if (ContainsExternalAssemblyTypeLookup(genericTypeArgument))
return true;
}
return false;
}
}

View File

@ -0,0 +1,846 @@
namespace GFramework.Cqrs.SourceGenerators.Cqrs;
/// <summary>
/// 为当前编译程序集生成 CQRS 处理器注册器,以减少运行时的程序集反射扫描成本。
/// </summary>
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="fallbackHandlerTypeMetadataNames">
/// 仍需依赖程序集级 reflection fallback 元数据恢复的 handler 元数据名称集合。
/// 调用方必须先确保:若该集合非空,则 <paramref name="generationEnvironment" /> 已声明支持对应的 fallback attribute 契约;
/// 否则应在进入本方法前报告诊断并放弃生成,而不是输出会静默漏注册的半成品注册器。
/// </param>
/// <returns>完整的注册器源代码文本。</returns>
/// <remarks>
/// 当 <paramref name="fallbackHandlerTypeMetadataNames" /> 为空时,输出只包含程序集级 <c>CqrsHandlerRegistryAttribute</c> 和注册器实现。
/// 当其非空且 runtime 合同可用时,输出还会附带程序集级 <c>CqrsReflectionFallbackAttribute</c>,让运行时补齐生成阶段无法精确表达的剩余 handler。
/// 该方法本身不报告诊断“fallback 必需但 runtime 契约缺失”的错误由调用方在进入本方法前处理。
/// </remarks>
private static string GenerateSource(
GenerationEnvironment generationEnvironment,
IReadOnlyList<ImplementationRegistrationSpec> registrations,
IReadOnlyList<string> fallbackHandlerTypeMetadataNames)
{
var sourceShape = CreateGeneratedRegistrySourceShape(registrations);
var builder = new StringBuilder();
AppendGeneratedSourcePreamble(builder, generationEnvironment, fallbackHandlerTypeMetadataNames);
AppendGeneratedRegistryType(builder, registrations, sourceShape);
return builder.ToString();
}
/// <summary>
/// 预先计算生成注册器需要的辅助分支,让主源码发射流程保持线性且避免重复扫描注册集合。
/// </summary>
/// <param name="registrations">已整理并排序的 handler 注册描述。</param>
/// <returns>当前生成输出需要启用的结构分支。</returns>
private static GeneratedRegistrySourceShape CreateGeneratedRegistrySourceShape(
IReadOnlyList<ImplementationRegistrationSpec> registrations)
{
var hasReflectedImplementationRegistrations = registrations.Any(static registration =>
!registration.ReflectedImplementationRegistrations.IsDefaultOrEmpty);
var hasPreciseReflectedRegistrations = registrations.Any(static registration =>
!registration.PreciseReflectedRegistrations.IsDefaultOrEmpty);
var hasReflectionTypeLookups = registrations.Any(static registration =>
!string.IsNullOrWhiteSpace(registration.ReflectionTypeMetadataName));
var hasExternalAssemblyTypeLookups = registrations.Any(static registration =>
registration.PreciseReflectedRegistrations.Any(static preciseRegistration =>
preciseRegistration.ServiceTypeArguments.Any(ContainsExternalAssemblyTypeLookup)));
return new GeneratedRegistrySourceShape(
hasReflectedImplementationRegistrations,
hasPreciseReflectedRegistrations,
hasReflectionTypeLookups,
hasExternalAssemblyTypeLookups);
}
/// <summary>
/// 发射生成文件头、nullable 指令以及注册器所需的程序集级元数据特性。
/// </summary>
/// <param name="builder">生成源码构造器。</param>
/// <param name="generationEnvironment">当前轮次的生成环境。</param>
/// <param name="fallbackHandlerTypeMetadataNames">需要程序集级 reflection fallback 的 handler 元数据名称。</param>
private static void AppendGeneratedSourcePreamble(
StringBuilder builder,
GenerationEnvironment generationEnvironment,
IReadOnlyList<string> fallbackHandlerTypeMetadataNames)
{
builder.AppendLine("// <auto-generated />");
builder.AppendLine("#nullable enable");
builder.AppendLine();
if (generationEnvironment.SupportsReflectionFallbackAttribute && fallbackHandlerTypeMetadataNames.Count > 0)
{
AppendReflectionFallbackAttribute(builder, fallbackHandlerTypeMetadataNames);
builder.AppendLine();
}
builder.Append("[assembly: global::");
builder.Append(CqrsRuntimeNamespace);
builder.Append(".CqrsHandlerRegistryAttribute(typeof(global::");
builder.Append(GeneratedNamespace);
builder.Append('.');
builder.Append(GeneratedTypeName);
builder.AppendLine("))]");
}
/// <summary>
/// 发射程序集级 reflection fallback 元数据特性,供运行时补齐生成阶段无法精确表达的 handler。
/// </summary>
/// <param name="builder">生成源码构造器。</param>
/// <param name="fallbackHandlerTypeMetadataNames">需要写入特性的 handler 元数据名称。</param>
private static void AppendReflectionFallbackAttribute(
StringBuilder builder,
IReadOnlyList<string> fallbackHandlerTypeMetadataNames)
{
builder.Append("[assembly: global::");
builder.Append(CqrsRuntimeNamespace);
builder.Append(".CqrsReflectionFallbackAttribute(");
for (var index = 0; index < fallbackHandlerTypeMetadataNames.Count; index++)
{
if (index > 0)
builder.Append(", ");
builder.Append('"');
builder.Append(EscapeStringLiteral(fallbackHandlerTypeMetadataNames[index]));
builder.Append('"');
}
builder.AppendLine(")]");
}
/// <summary>
/// 发射生成注册器类型本体,包括 <c>Register</c> 方法和运行时反射辅助方法。
/// </summary>
/// <param name="builder">生成源码构造器。</param>
/// <param name="registrations">已排序的 handler 注册描述。</param>
/// <param name="sourceShape">当前输出需要启用的结构分支。</param>
private static void AppendGeneratedRegistryType(
StringBuilder builder,
IReadOnlyList<ImplementationRegistrationSpec> registrations,
GeneratedRegistrySourceShape sourceShape)
{
builder.AppendLine();
builder.Append("namespace ");
builder.Append(GeneratedNamespace);
builder.AppendLine(";");
builder.AppendLine();
builder.Append("internal sealed class ");
builder.Append(GeneratedTypeName);
builder.Append(" : global::");
builder.Append(CqrsRuntimeNamespace);
builder.AppendLine(".ICqrsHandlerRegistry");
builder.AppendLine("{");
AppendRegisterMethod(builder, registrations, sourceShape);
if (sourceShape.HasExternalAssemblyTypeLookups)
{
builder.AppendLine();
AppendReflectionHelpers(builder);
}
builder.AppendLine("}");
}
/// <summary>
/// 发射注册器的 <c>Register</c> 方法,保持直接注册和反射注册之间的原始稳定排序。
/// </summary>
/// <param name="builder">生成源码构造器。</param>
/// <param name="registrations">已排序的 handler 注册描述。</param>
/// <param name="sourceShape">当前输出需要启用的结构分支。</param>
private static void AppendRegisterMethod(
StringBuilder builder,
IReadOnlyList<ImplementationRegistrationSpec> registrations,
GeneratedRegistrySourceShape sourceShape)
{
builder.Append(
" public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::");
builder.Append(LoggingNamespace);
builder.AppendLine(".ILogger logger)");
builder.AppendLine(" {");
builder.AppendLine(" if (services is null)");
builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(services));");
builder.AppendLine(" if (logger is null)");
builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(logger));");
if (sourceShape.RequiresRegistryAssemblyVariable)
{
builder.AppendLine();
builder.Append(" var registryAssembly = typeof(global::");
builder.Append(GeneratedNamespace);
builder.Append('.');
builder.Append(GeneratedTypeName);
builder.AppendLine(").Assembly;");
}
if (registrations.Count > 0)
builder.AppendLine();
for (var registrationIndex = 0; registrationIndex < registrations.Count; registrationIndex++)
{
var registration = registrations[registrationIndex];
if (!registration.ReflectedImplementationRegistrations.IsDefaultOrEmpty ||
!registration.PreciseReflectedRegistrations.IsDefaultOrEmpty)
{
AppendOrderedImplementationRegistrations(builder, registration, registrationIndex);
}
else if (!registration.DirectRegistrations.IsDefaultOrEmpty)
{
AppendDirectRegistrations(builder, registration);
}
}
builder.AppendLine(" }");
}
private static void AppendDirectRegistrations(
StringBuilder builder,
ImplementationRegistrationSpec registration)
{
foreach (var directRegistration in registration.DirectRegistrations)
{
AppendServiceRegistration(
builder,
$"typeof({directRegistration.HandlerInterfaceDisplayName})",
$"typeof({directRegistration.ImplementationTypeDisplayName})",
" ");
AppendRegistrationLog(
builder,
directRegistration.ImplementationLogName,
directRegistration.HandlerInterfaceLogName,
" ");
}
}
/// <summary>
/// 发射 <c>AddTransient</c> 调用,调用方负责传入已经按当前分支解析好的 service 和 implementation 表达式。
/// </summary>
/// <param name="builder">生成源码构造器。</param>
/// <param name="serviceTypeExpression">生成代码中的服务类型表达式。</param>
/// <param name="implementationTypeExpression">生成代码中的实现类型表达式。</param>
/// <param name="indent">当前生成语句的缩进。</param>
private static void AppendServiceRegistration(
StringBuilder builder,
string serviceTypeExpression,
string implementationTypeExpression,
string indent)
{
builder.Append(indent);
builder.AppendLine("global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient(");
builder.Append(indent);
builder.AppendLine(" services,");
builder.Append(indent);
builder.Append(" ");
builder.Append(serviceTypeExpression);
builder.AppendLine(",");
builder.Append(indent);
builder.Append(" ");
builder.Append(implementationTypeExpression);
builder.AppendLine(");");
}
/// <summary>
/// 发射与注册语句配套的调试日志,保持所有生成注册路径的日志文本完全一致。
/// </summary>
/// <param name="builder">生成源码构造器。</param>
/// <param name="implementationLogName">实现类型日志名。</param>
/// <param name="handlerInterfaceLogName">handler 接口日志名。</param>
/// <param name="indent">当前生成语句的缩进。</param>
private static void AppendRegistrationLog(
StringBuilder builder,
string implementationLogName,
string handlerInterfaceLogName,
string indent)
{
builder.Append(indent);
builder.Append("logger.Debug(\"Registered CQRS handler ");
builder.Append(EscapeStringLiteral(implementationLogName));
builder.Append(" as ");
builder.Append(EscapeStringLiteral(handlerInterfaceLogName));
builder.AppendLine(".\");");
}
private static void AppendOrderedImplementationRegistrations(
StringBuilder builder,
ImplementationRegistrationSpec registration,
int registrationIndex)
{
var orderedRegistrations = CreateOrderedRegistrations(registration);
var implementationVariableName = $"implementationType{registrationIndex}";
AppendImplementationTypeVariable(builder, registration, implementationVariableName);
builder.Append(" if (");
builder.Append(implementationVariableName);
builder.AppendLine(" is not null)");
builder.AppendLine(" {");
foreach (var orderedRegistration in orderedRegistrations)
{
AppendOrderedRegistration(
builder,
registration,
orderedRegistration,
registrationIndex,
implementationVariableName);
}
builder.AppendLine(" }");
}
/// <summary>
/// 合并直接注册、实现类型反射注册和精确反射注册,并按 handler 接口日志名排序以保持生成输出稳定。
/// </summary>
/// <param name="registration">单个实现类型聚合后的注册描述。</param>
/// <returns>带有来源类型和原始索引的有序注册列表。</returns>
private static List<OrderedRegistrationSpec> CreateOrderedRegistrations(ImplementationRegistrationSpec registration)
{
var orderedRegistrations = new List<OrderedRegistrationSpec>(
registration.DirectRegistrations.Length +
registration.ReflectedImplementationRegistrations.Length +
registration.PreciseReflectedRegistrations.Length);
for (var directIndex = 0; directIndex < registration.DirectRegistrations.Length; directIndex++)
{
orderedRegistrations.Add(new OrderedRegistrationSpec(
registration.DirectRegistrations[directIndex].HandlerInterfaceLogName,
OrderedRegistrationKind.Direct,
directIndex));
}
for (var reflectedIndex = 0;
reflectedIndex < registration.ReflectedImplementationRegistrations.Length;
reflectedIndex++)
{
orderedRegistrations.Add(new OrderedRegistrationSpec(
registration.ReflectedImplementationRegistrations[reflectedIndex].HandlerInterfaceLogName,
OrderedRegistrationKind.ReflectedImplementation,
reflectedIndex));
}
for (var preciseIndex = 0;
preciseIndex < registration.PreciseReflectedRegistrations.Length;
preciseIndex++)
{
orderedRegistrations.Add(new OrderedRegistrationSpec(
registration.PreciseReflectedRegistrations[preciseIndex].HandlerInterfaceLogName,
OrderedRegistrationKind.PreciseReflected,
preciseIndex));
}
orderedRegistrations.Sort(static (left, right) =>
StringComparer.Ordinal.Compare(left.HandlerInterfaceLogName, right.HandlerInterfaceLogName));
return orderedRegistrations;
}
/// <summary>
/// 发射实现类型变量。公开类型直接使用 <c>typeof</c>,不可直接引用的实现类型则从当前程序集反射解析。
/// </summary>
/// <param name="builder">生成源码构造器。</param>
/// <param name="registration">单个实现类型聚合后的注册描述。</param>
/// <param name="implementationVariableName">生成代码中的实现类型变量名。</param>
private static void AppendImplementationTypeVariable(
StringBuilder builder,
ImplementationRegistrationSpec registration,
string implementationVariableName)
{
if (string.IsNullOrWhiteSpace(registration.ReflectionTypeMetadataName))
{
builder.Append(" var ");
builder.Append(implementationVariableName);
builder.Append(" = typeof(");
builder.Append(registration.ImplementationTypeDisplayName);
builder.AppendLine(");");
}
else
{
builder.Append(" var ");
builder.Append(implementationVariableName);
builder.Append(" = registryAssembly.GetType(\"");
builder.Append(EscapeStringLiteral(registration.ReflectionTypeMetadataName!));
builder.AppendLine("\", throwOnError: false, ignoreCase: false);");
}
}
/// <summary>
/// 根据注册来源发射单条有序注册,确保混合直接和反射路径时仍按 handler 接口名稳定输出。
/// </summary>
/// <param name="builder">生成源码构造器。</param>
/// <param name="registration">单个实现类型聚合后的注册描述。</param>
/// <param name="orderedRegistration">带来源类型和原始索引的排序项。</param>
/// <param name="registrationIndex">实现类型在整体注册列表中的索引,用于生成稳定变量名。</param>
/// <param name="implementationVariableName">生成代码中的实现类型变量名。</param>
private static void AppendOrderedRegistration(
StringBuilder builder,
ImplementationRegistrationSpec registration,
OrderedRegistrationSpec orderedRegistration,
int registrationIndex,
string implementationVariableName)
{
switch (orderedRegistration.Kind)
{
case OrderedRegistrationKind.Direct:
AppendOrderedDirectRegistration(
builder,
registration,
registration.DirectRegistrations[orderedRegistration.Index],
implementationVariableName);
break;
case OrderedRegistrationKind.ReflectedImplementation:
AppendOrderedReflectedImplementationRegistration(
builder,
registration,
registration.ReflectedImplementationRegistrations[orderedRegistration.Index],
implementationVariableName);
break;
case OrderedRegistrationKind.PreciseReflected:
AppendOrderedPreciseReflectedRegistration(
builder,
registration,
registration.PreciseReflectedRegistrations[orderedRegistration.Index],
registrationIndex,
orderedRegistration.Index,
implementationVariableName);
break;
default:
throw new InvalidOperationException(
$"Unsupported ordered CQRS registration kind {orderedRegistration.Kind}.");
}
}
/// <summary>
/// 发射实现类型已通过变量解析、handler 接口可直接引用的直接注册语句。
/// </summary>
/// <param name="builder">生成源码构造器。</param>
/// <param name="registration">单个实现类型聚合后的注册描述。</param>
/// <param name="directRegistration">当前直接注册项。</param>
/// <param name="implementationVariableName">生成代码中的实现类型变量名。</param>
private static void AppendOrderedDirectRegistration(
StringBuilder builder,
ImplementationRegistrationSpec registration,
HandlerRegistrationSpec directRegistration,
string implementationVariableName)
{
AppendServiceRegistration(
builder,
$"typeof({directRegistration.HandlerInterfaceDisplayName})",
implementationVariableName,
" ");
AppendRegistrationLog(
builder,
registration.ImplementationLogName,
directRegistration.HandlerInterfaceLogName,
" ");
}
/// <summary>
/// 发射实现类型需要反射解析、handler 接口可直接引用的注册语句。
/// </summary>
/// <param name="builder">生成源码构造器。</param>
/// <param name="registration">单个实现类型聚合后的注册描述。</param>
/// <param name="reflectedRegistration">当前实现类型反射注册项。</param>
/// <param name="implementationVariableName">生成代码中的实现类型变量名。</param>
private static void AppendOrderedReflectedImplementationRegistration(
StringBuilder builder,
ImplementationRegistrationSpec registration,
ReflectedImplementationRegistrationSpec reflectedRegistration,
string implementationVariableName)
{
AppendServiceRegistration(
builder,
$"typeof({reflectedRegistration.HandlerInterfaceDisplayName})",
implementationVariableName,
" ");
AppendRegistrationLog(
builder,
registration.ImplementationLogName,
reflectedRegistration.HandlerInterfaceLogName,
" ");
}
/// <summary>
/// 发射 handler 接口需要运行时精确构造的注册语句。
/// </summary>
/// <param name="builder">生成源码构造器。</param>
/// <param name="registration">单个实现类型聚合后的注册描述。</param>
/// <param name="preciseRegistration">当前精确反射注册项。</param>
/// <param name="registrationIndex">实现类型在整体注册列表中的索引。</param>
/// <param name="orderedRegistrationIndex">当前注册项在原始精确反射注册集合中的索引。</param>
/// <param name="implementationVariableName">生成代码中的实现类型变量名。</param>
private static void AppendOrderedPreciseReflectedRegistration(
StringBuilder builder,
ImplementationRegistrationSpec registration,
PreciseReflectedRegistrationSpec preciseRegistration,
int registrationIndex,
int orderedRegistrationIndex,
string implementationVariableName)
{
var registrationVariablePrefix = $"serviceType{registrationIndex}_{orderedRegistrationIndex}";
AppendPreciseReflectedTypeResolution(
builder,
preciseRegistration.ServiceTypeArguments,
registrationVariablePrefix,
implementationVariableName,
preciseRegistration.OpenHandlerTypeDisplayName,
registration.ImplementationLogName,
preciseRegistration.HandlerInterfaceLogName,
3);
}
private static void AppendPreciseReflectedTypeResolution(
StringBuilder builder,
ImmutableArray<RuntimeTypeReferenceSpec> serviceTypeArguments,
string registrationVariablePrefix,
string implementationVariableName,
string openHandlerTypeDisplayName,
string implementationLogName,
string handlerInterfaceLogName,
int indentLevel)
{
var indent = new string(' ', indentLevel * 4);
var reflectedArgumentNames = new List<string>();
var resolvedArgumentNames = AppendServiceTypeArgumentResolutions(
builder,
serviceTypeArguments,
registrationVariablePrefix,
reflectedArgumentNames,
indent);
if (reflectedArgumentNames.Count > 0)
indent = AppendReflectedArgumentGuardStart(builder, reflectedArgumentNames, indent);
AppendClosedGenericServiceTypeCreation(
builder,
registrationVariablePrefix,
openHandlerTypeDisplayName,
resolvedArgumentNames,
indent);
AppendServiceRegistration(builder, registrationVariablePrefix, implementationVariableName, indent);
AppendRegistrationLog(builder, implementationLogName, handlerInterfaceLogName, indent);
if (reflectedArgumentNames.Count > 0)
{
builder.Append(new string(' ', indentLevel * 4));
builder.AppendLine("}");
}
}
/// <summary>
/// 递归发射每个 handler 泛型实参的运行时类型解析表达式。
/// </summary>
/// <param name="builder">生成源码构造器。</param>
/// <param name="serviceTypeArguments">handler 服务类型的运行时泛型实参描述。</param>
/// <param name="registrationVariablePrefix">当前注册项的稳定变量名前缀。</param>
/// <param name="reflectedArgumentNames">需要空值检查的反射解析变量集合。</param>
/// <param name="indent">当前生成语句的缩进。</param>
/// <returns>可传给 <c>MakeGenericType</c> 的实参表达式。</returns>
private static string[] AppendServiceTypeArgumentResolutions(
StringBuilder builder,
ImmutableArray<RuntimeTypeReferenceSpec> serviceTypeArguments,
string registrationVariablePrefix,
ICollection<string> reflectedArgumentNames,
string indent)
{
var resolvedArgumentNames = new string[serviceTypeArguments.Length];
for (var argumentIndex = 0; argumentIndex < serviceTypeArguments.Length; argumentIndex++)
{
resolvedArgumentNames[argumentIndex] = AppendRuntimeTypeReferenceResolution(
builder,
serviceTypeArguments[argumentIndex],
$"{registrationVariablePrefix}Argument{argumentIndex}",
reflectedArgumentNames,
indent);
}
return resolvedArgumentNames;
}
/// <summary>
/// 为运行时反射解析出的泛型实参发射空值保护块,避免生成注册器注册无法完整构造的服务类型。
/// </summary>
/// <param name="builder">生成源码构造器。</param>
/// <param name="reflectedArgumentNames">需要参与空值检查的变量名。</param>
/// <param name="indent">保护块开始前的缩进。</param>
/// <returns>保护块内部应使用的下一层缩进。</returns>
private static string AppendReflectedArgumentGuardStart(
StringBuilder builder,
IReadOnlyList<string> reflectedArgumentNames,
string indent)
{
builder.Append(indent);
builder.Append("if (");
for (var index = 0; index < reflectedArgumentNames.Count; index++)
{
if (index > 0)
builder.Append(" && ");
builder.Append(reflectedArgumentNames[index]);
builder.Append(" is not null");
}
builder.AppendLine(")");
builder.Append(indent);
builder.AppendLine("{");
return $"{indent} ";
}
/// <summary>
/// 发射关闭 handler 服务类型的 <c>MakeGenericType</c> 构造语句。
/// </summary>
/// <param name="builder">生成源码构造器。</param>
/// <param name="registrationVariablePrefix">生成代码中的服务类型变量名。</param>
/// <param name="openHandlerTypeDisplayName">开放 handler 接口类型显示名。</param>
/// <param name="resolvedArgumentNames">已解析的泛型实参表达式。</param>
/// <param name="indent">当前生成语句的缩进。</param>
private static void AppendClosedGenericServiceTypeCreation(
StringBuilder builder,
string registrationVariablePrefix,
string openHandlerTypeDisplayName,
IReadOnlyList<string> resolvedArgumentNames,
string indent)
{
builder.Append(indent);
builder.Append("var ");
builder.Append(registrationVariablePrefix);
builder.Append(" = typeof(");
builder.Append(openHandlerTypeDisplayName);
builder.Append(").MakeGenericType(");
for (var index = 0; index < resolvedArgumentNames.Count; index++)
{
if (index > 0)
builder.Append(", ");
builder.Append(resolvedArgumentNames[index]);
}
builder.AppendLine(");");
}
private static string AppendRuntimeTypeReferenceResolution(
StringBuilder builder,
RuntimeTypeReferenceSpec runtimeTypeReference,
string variableBaseName,
ICollection<string> reflectedArgumentNames,
string indent)
{
if (!string.IsNullOrWhiteSpace(runtimeTypeReference.TypeDisplayName))
return $"typeof({runtimeTypeReference.TypeDisplayName})";
if (runtimeTypeReference.ArrayElementTypeReference is not null)
return AppendArrayRuntimeTypeReferenceResolution(
builder,
runtimeTypeReference,
variableBaseName,
reflectedArgumentNames,
indent);
if (runtimeTypeReference.PointerElementTypeReference is not null)
return AppendPointerRuntimeTypeReferenceResolution(
builder,
runtimeTypeReference,
variableBaseName,
reflectedArgumentNames,
indent);
if (runtimeTypeReference.GenericTypeDefinitionReference is not null)
return AppendConstructedGenericRuntimeTypeReferenceResolution(
builder,
runtimeTypeReference,
variableBaseName,
reflectedArgumentNames,
indent);
return AppendReflectionRuntimeTypeReferenceResolution(
builder,
runtimeTypeReference,
variableBaseName,
reflectedArgumentNames,
indent);
}
/// <summary>
/// 发射数组类型引用的运行时重建表达式。
/// </summary>
/// <param name="builder">生成源码构造器。</param>
/// <param name="runtimeTypeReference">数组类型引用描述。</param>
/// <param name="variableBaseName">用于递归生成变量名的稳定前缀。</param>
/// <param name="reflectedArgumentNames">需要空值检查的反射解析变量集合。</param>
/// <param name="indent">当前生成语句的缩进。</param>
/// <returns>数组类型表达式。</returns>
private static string AppendArrayRuntimeTypeReferenceResolution(
StringBuilder builder,
RuntimeTypeReferenceSpec runtimeTypeReference,
string variableBaseName,
ICollection<string> reflectedArgumentNames,
string indent)
{
var elementExpression = AppendRuntimeTypeReferenceResolution(
builder,
runtimeTypeReference.ArrayElementTypeReference!,
$"{variableBaseName}Element",
reflectedArgumentNames,
indent);
return runtimeTypeReference.ArrayRank == 1
? $"{elementExpression}.MakeArrayType()"
: $"{elementExpression}.MakeArrayType({runtimeTypeReference.ArrayRank})";
}
/// <summary>
/// 发射指针类型引用的运行时重建表达式。
/// </summary>
/// <param name="builder">生成源码构造器。</param>
/// <param name="runtimeTypeReference">指针类型引用描述。</param>
/// <param name="variableBaseName">用于递归生成变量名的稳定前缀。</param>
/// <param name="reflectedArgumentNames">需要空值检查的反射解析变量集合。</param>
/// <param name="indent">当前生成语句的缩进。</param>
/// <returns>指针类型表达式。</returns>
private static string AppendPointerRuntimeTypeReferenceResolution(
StringBuilder builder,
RuntimeTypeReferenceSpec runtimeTypeReference,
string variableBaseName,
ICollection<string> reflectedArgumentNames,
string indent)
{
var pointedAtExpression = AppendRuntimeTypeReferenceResolution(
builder,
runtimeTypeReference.PointerElementTypeReference!,
$"{variableBaseName}PointedAt",
reflectedArgumentNames,
indent);
return $"{pointedAtExpression}.MakePointerType()";
}
/// <summary>
/// 发射已构造泛型类型引用的运行时重建表达式。
/// </summary>
/// <param name="builder">生成源码构造器。</param>
/// <param name="runtimeTypeReference">已构造泛型类型引用描述。</param>
/// <param name="variableBaseName">用于递归生成变量名的稳定前缀。</param>
/// <param name="reflectedArgumentNames">需要空值检查的反射解析变量集合。</param>
/// <param name="indent">当前生成语句的缩进。</param>
/// <returns>已构造泛型类型表达式。</returns>
private static string AppendConstructedGenericRuntimeTypeReferenceResolution(
StringBuilder builder,
RuntimeTypeReferenceSpec runtimeTypeReference,
string variableBaseName,
ICollection<string> reflectedArgumentNames,
string indent)
{
var genericTypeDefinitionExpression = AppendRuntimeTypeReferenceResolution(
builder,
runtimeTypeReference.GenericTypeDefinitionReference!,
$"{variableBaseName}GenericDefinition",
reflectedArgumentNames,
indent);
var genericArgumentExpressions = new string[runtimeTypeReference.GenericTypeArguments.Length];
for (var argumentIndex = 0;
argumentIndex < runtimeTypeReference.GenericTypeArguments.Length;
argumentIndex++)
{
genericArgumentExpressions[argumentIndex] = AppendRuntimeTypeReferenceResolution(
builder,
runtimeTypeReference.GenericTypeArguments[argumentIndex],
$"{variableBaseName}GenericArgument{argumentIndex}",
reflectedArgumentNames,
indent);
}
return $"{genericTypeDefinitionExpression}.MakeGenericType({string.Join(", ", genericArgumentExpressions)})";
}
/// <summary>
/// 发射命名类型的运行时反射查找语句,并返回后续服务类型构造应引用的变量名。
/// </summary>
/// <param name="builder">生成源码构造器。</param>
/// <param name="runtimeTypeReference">反射查找类型引用描述。</param>
/// <param name="variableBaseName">生成代码中的反射变量名。</param>
/// <param name="reflectedArgumentNames">需要空值检查的反射解析变量集合。</param>
/// <param name="indent">当前生成语句的缩进。</param>
/// <returns>生成代码中的反射变量名。</returns>
private static string AppendReflectionRuntimeTypeReferenceResolution(
StringBuilder builder,
RuntimeTypeReferenceSpec runtimeTypeReference,
string variableBaseName,
ICollection<string> reflectedArgumentNames,
string indent)
{
reflectedArgumentNames.Add(variableBaseName);
builder.Append(indent);
builder.Append("var ");
builder.Append(variableBaseName);
if (string.IsNullOrWhiteSpace(runtimeTypeReference.ReflectionAssemblyName))
{
builder.Append(" = registryAssembly.GetType(\"");
builder.Append(EscapeStringLiteral(runtimeTypeReference.ReflectionTypeMetadataName!));
builder.AppendLine("\", throwOnError: false, ignoreCase: false);");
}
else
{
builder.Append(" = ResolveReferencedAssemblyType(\"");
builder.Append(EscapeStringLiteral(runtimeTypeReference.ReflectionAssemblyName!));
builder.Append("\", \"");
builder.Append(EscapeStringLiteral(runtimeTypeReference.ReflectionTypeMetadataName!));
builder.AppendLine("\");");
}
return variableBaseName;
}
private static void AppendReflectionHelpers(StringBuilder builder)
{
builder.AppendLine(
" private static global::System.Type? ResolveReferencedAssemblyType(string assemblyIdentity, string typeMetadataName)");
builder.AppendLine(" {");
builder.AppendLine(" var assembly = ResolveReferencedAssembly(assemblyIdentity);");
builder.AppendLine(
" return assembly?.GetType(typeMetadataName, throwOnError: false, ignoreCase: false);");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(
" private static global::System.Reflection.Assembly? ResolveReferencedAssembly(string assemblyIdentity)");
builder.AppendLine(" {");
builder.AppendLine(" global::System.Reflection.AssemblyName targetAssemblyName;");
builder.AppendLine(" try");
builder.AppendLine(" {");
builder.AppendLine(
" targetAssemblyName = new global::System.Reflection.AssemblyName(assemblyIdentity);");
builder.AppendLine(" }");
builder.AppendLine(" catch");
builder.AppendLine(" {");
builder.AppendLine(" return null;");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(
" foreach (var assembly in global::System.AppDomain.CurrentDomain.GetAssemblies())");
builder.AppendLine(" {");
builder.AppendLine(
" if (global::System.Reflection.AssemblyName.ReferenceMatchesDefinition(targetAssemblyName, assembly.GetName()))");
builder.AppendLine(" return assembly;");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" try");
builder.AppendLine(" {");
builder.AppendLine(
" return global::System.Reflection.Assembly.Load(targetAssemblyName);");
builder.AppendLine(" }");
builder.AppendLine(" catch");
builder.AppendLine(" {");
builder.AppendLine(" return null;");
builder.AppendLine(" }");
builder.AppendLine(" }");
}
private static string EscapeStringLiteral(string value)
{
return value.Replace("\\", "\\\\")
.Replace("\"", "\\\"")
.Replace("\n", "\\n")
.Replace("\r", "\\r");
}
}

View File

@ -18,3 +18,4 @@
GF_ConfigSchema_011 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
GF_ConfigSchema_012 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
GF_ConfigSchema_013 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
GF_ConfigSchema_014 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics

View File

@ -151,4 +151,15 @@ public static class ConfigSchemaDiagnostics
SourceGeneratorsConfigCategory,
DiagnosticSeverity.Error,
true);
/// <summary>
/// schema 字段名在标识符归一化后发生冲突。
/// </summary>
public static readonly DiagnosticDescriptor DuplicateGeneratedIdentifier = new(
"GF_ConfigSchema_014",
"Config schema property names collide after C# identifier normalization",
"Property '{1}' in schema file '{0}' uses schema key '{2}', which generates duplicate C# identifier '{3}' already produced by schema key '{4}'",
SourceGeneratorsConfigCategory,
DiagnosticSeverity.Error,
true);
}

View File

@ -6,6 +6,32 @@ namespace GFramework.SourceGenerators.Tests.Config;
[TestFixture]
public class SchemaConfigGeneratorTests
{
/// <summary>
/// 验证 AdditionalFiles 读取被取消时会向上传播取消,而不是伪造成 schema 诊断。
/// </summary>
[Test]
public void Run_Should_Propagate_Cancellation_When_AdditionalText_Read_Is_Cancelled()
{
var method = typeof(global::GFramework.Game.SourceGenerators.Config.SchemaConfigGenerator)
.GetMethod(
"TryReadSchemaText",
global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static);
var cancellationTokenSource = new CancellationTokenSource();
cancellationTokenSource.Cancel();
var invocationArguments = new object?[]
{
new ThrowingAdditionalText("monster.schema.json"),
cancellationTokenSource.Token,
null,
null
};
var exception = Assert.Throws<global::System.Reflection.TargetInvocationException>(() =>
method!.Invoke(null, invocationArguments));
Assert.That(exception!.InnerException, Is.TypeOf<OperationCanceledException>());
}
/// <summary>
/// 验证缺失必填 id 字段时会产生命名明确的诊断。
/// </summary>
@ -46,6 +72,111 @@ public class SchemaConfigGeneratorTests
});
}
/// <summary>
/// 验证根节点 <c>type</c> 元数据不是字符串时,会返回根对象约束诊断,而不是抛出 JSON 访问异常。
/// </summary>
[Test]
public void Run_Should_Report_Diagnostic_When_Root_Type_Metadata_Is_Not_A_String()
{
const string source = """
namespace TestApp
{
public sealed class Dummy
{
}
}
""";
const string schema = """
{
"type": 123,
"required": ["id"],
"properties": {
"id": { "type": "integer" }
}
}
""";
var result = SchemaGeneratorTestDriver.Run(
source,
("monster.schema.json", schema));
var diagnostic = result.Results.Single().Diagnostics.Single();
Assert.Multiple(() =>
{
Assert.That(diagnostic.Id, Is.EqualTo("GF_ConfigSchema_002"));
Assert.That(diagnostic.Severity, Is.EqualTo(DiagnosticSeverity.Error));
Assert.That(diagnostic.GetMessage(), Does.Contain("monster.schema.json"));
});
}
/// <summary>
/// 验证 schema 文件名若生成无效根类型标识符时,会在生成前产生命名明确的诊断。
/// </summary>
[Test]
public void Run_Should_Report_Diagnostic_When_Schema_File_Name_Generates_Invalid_Root_Type_Identifier()
{
const string source = """
namespace TestApp
{
public sealed class Dummy
{
}
}
""";
const string schema = """
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" }
}
}
""";
var result = SchemaGeneratorTestDriver.Run(
source,
("123-monster.schema.json", schema));
var diagnostic = result.Results.Single().Diagnostics.Single();
Assert.Multiple(() =>
{
Assert.That(diagnostic.Id, Is.EqualTo("GF_ConfigSchema_006"));
Assert.That(diagnostic.Severity, Is.EqualTo(DiagnosticSeverity.Error));
Assert.That(diagnostic.GetMessage(), Does.Contain("<root>"));
Assert.That(diagnostic.GetMessage(), Does.Contain("123-monster"));
Assert.That(diagnostic.GetMessage(), Does.Contain("123MonsterConfig"));
});
}
/// <summary>
/// 用于模拟 AdditionalFiles 读取阶段直接收到取消请求的测试桩。
/// </summary>
private sealed class ThrowingAdditionalText : AdditionalText
{
/// <summary>
/// 创建一个在读取时抛出取消异常的 AdditionalText。
/// </summary>
/// <param name="path">虚拟 schema 路径。</param>
public ThrowingAdditionalText(string path)
{
Path = path;
}
/// <inheritdoc />
public override string Path { get; }
/// <inheritdoc />
public override SourceText GetText(CancellationToken cancellationToken = default)
{
throw new OperationCanceledException(cancellationToken);
}
}
/// <summary>
/// 验证空字符串 <c>const</c> 不会在生成 XML 文档时被当成“缺失约束”跳过。
/// </summary>
@ -1844,6 +1975,49 @@ public class SchemaConfigGeneratorTests
});
}
/// <summary>
/// 验证同一对象内不同 schema key 若归一化后映射到同一属性名,会在生成前直接给出冲突诊断。
/// </summary>
[Test]
public void Run_Should_Report_Diagnostic_When_Schema_Keys_Collide_After_Identifier_Normalization()
{
const string source = """
namespace TestApp
{
public sealed class Dummy
{
}
}
""";
const string schema = """
{
"type": "object",
"required": ["id"],
"properties": {
"id": { "type": "integer" },
"foo-bar": { "type": "string" },
"foo_bar": { "type": "string" }
}
}
""";
var result = SchemaGeneratorTestDriver.Run(
source,
("monster.schema.json", schema));
var diagnostic = result.Results.Single().Diagnostics.Single();
Assert.Multiple(() =>
{
Assert.That(diagnostic.Id, Is.EqualTo("GF_ConfigSchema_014"));
Assert.That(diagnostic.Severity, Is.EqualTo(DiagnosticSeverity.Error));
Assert.That(diagnostic.GetMessage(), Does.Contain("foo_bar"));
Assert.That(diagnostic.GetMessage(), Does.Contain("FooBar"));
Assert.That(diagnostic.GetMessage(), Does.Contain("foo-bar"));
});
}
/// <summary>
/// 验证 schema 顶层允许通过元数据覆盖默认配置目录,并会统一路径分隔符。
/// </summary>
@ -2299,7 +2473,7 @@ public class SchemaConfigGeneratorTests
}
/// <summary>
/// 验证引用元数据成员名在不同路径规范化后发生碰撞时,生成器仍会分配全局唯一的成员名。
/// 验证引用元数据成员名在不同合法字段路径规范化后发生碰撞时,生成器仍会分配全局唯一的成员名。
/// </summary>
[Test]
public void Run_Should_Assign_Globally_Unique_Reference_Metadata_Member_Names()
@ -2360,12 +2534,21 @@ public class SchemaConfigGeneratorTests
"required": ["id"],
"properties": {
"id": { "type": "integer" },
"drop-items": {
"type": "array",
"items": { "type": "string" },
"x-gframework-ref-table": "item"
"drop": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": { "type": "string" },
"x-gframework-ref-table": "item"
},
"items1": {
"type": "string",
"x-gframework-ref-table": "item"
}
}
},
"drop_items": {
"dropItems": {
"type": "array",
"items": { "type": "string" },
"x-gframework-ref-table": "item"
@ -2394,6 +2577,7 @@ public class SchemaConfigGeneratorTests
Assert.That(generatedSources.TryGetValue("MonsterConfigBindings.g.cs", out var bindingsSource), Is.True);
Assert.That(bindingsSource, Does.Contain("public static readonly ReferenceMetadata DropItems ="));
Assert.That(bindingsSource, Does.Contain("public static readonly ReferenceMetadata DropItems1 ="));
Assert.That(bindingsSource, Does.Contain("public static readonly ReferenceMetadata DropItems2 ="));
Assert.That(bindingsSource, Does.Contain("public static readonly ReferenceMetadata DropItems11 ="));
}
@ -2637,6 +2821,12 @@ public class SchemaConfigGeneratorTests
Assert.That(catalogSource,
Does.Contain(
"public global::System.Collections.Generic.IEqualityComparer<int>? MonsterComparer { get; init; }"));
Assert.That(catalogSource,
Does.Contain(
"using <c>global::System.Collections.Generic.IEqualityComparer&lt;string&gt;?</c> when aggregate registration runs."));
Assert.That(catalogSource,
Does.Contain(
"using <c>global::System.Collections.Generic.IEqualityComparer&lt;int&gt;?</c> when aggregate registration runs."));
Assert.That(catalogSource, Does.Contain("return RegisterAllGeneratedConfigTables(loader, options: null);"));
Assert.That(catalogSource, Does.Contain("GeneratedConfigRegistrationOptions? options"));
Assert.That(catalogSource, Does.Contain("loader.RegisterItemTable(effectiveOptions.ItemComparer);"));

View File

@ -20,6 +20,23 @@ public static class SchemaGeneratorTestDriver
public static GeneratorDriverRunResult Run(
string source,
params (string path, string content)[] additionalFiles)
{
return Run(
source,
additionalFiles
.Select(static item => (AdditionalText)new InMemoryAdditionalText(item.path, item.content))
.ToArray());
}
/// <summary>
/// 运行 schema 配置生成器,并允许测试自定义 AdditionalText 行为。
/// </summary>
/// <param name="source">测试用源码。</param>
/// <param name="additionalTexts">自定义 AdditionalText 集合。</param>
/// <returns>生成器运行结果。</returns>
public static GeneratorDriverRunResult Run(
string source,
params AdditionalText[] additionalTexts)
{
var syntaxTree = CSharpSyntaxTree.ParseText(source);
var compilation = CSharpCompilation.Create(
@ -28,13 +45,9 @@ public static class SchemaGeneratorTestDriver
GetMetadataReferences(),
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
var additionalTexts = additionalFiles
.Select(static item => (AdditionalText)new InMemoryAdditionalText(item.path, item.content))
.ToImmutableArray();
GeneratorDriver driver = CSharpGeneratorDriver.Create(
generators: new[] { new SchemaConfigGenerator().AsSourceGenerator() },
additionalTexts: additionalTexts,
additionalTexts: additionalTexts.ToImmutableArray(),
parseOptions: (CSharpParseOptions)syntaxTree.Options);
driver = driver.RunGenerators(compilation);

View File

@ -236,7 +236,7 @@ public sealed class GeneratedConfigRegistrationOptions
public global::System.Predicate<GeneratedConfigCatalog.TableMetadata>? TableFilter { get; init; }
/// <summary>
/// Gets or sets the optional key comparer forwarded to MonsterConfigBindings.RegisterMonsterTable(global::GFramework.Game.Config.YamlConfigLoader, global::System.Collections.Generic.IEqualityComparer<int>?) when aggregate registration runs.
/// Gets or sets the optional key comparer forwarded to MonsterConfigBindings.RegisterMonsterTable using <c>global::System.Collections.Generic.IEqualityComparer&lt;int&gt;?</c> when aggregate registration runs.
/// </summary>
public global::System.Collections.Generic.IEqualityComparer<int>? MonsterComparer { get; init; }
}

View File

@ -1375,6 +1375,197 @@ public class CqrsHandlerRegistryGeneratorTests
});
}
/// <summary>
/// 验证 handler 合同里出现未解析错误类型时,生成器会改为运行时精确查找该类型,
/// 而不会把无效类型名直接写进生成代码中的 <c>typeof(...)</c>。
/// </summary>
[Test]
public void Emits_Runtime_Type_Lookup_When_Handler_Contract_Contains_Unresolved_Error_Types()
{
const string source = """
using System;
namespace Microsoft.Extensions.DependencyInjection
{
public interface IServiceCollection { }
public static class ServiceCollectionServiceExtensions
{
public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { }
}
}
namespace GFramework.Core.Abstractions.Logging
{
public interface ILogger
{
void Debug(string msg);
}
}
namespace GFramework.Cqrs.Abstractions.Cqrs
{
public interface IRequest<TResponse> { }
public interface INotification { }
public interface IStreamRequest<TResponse> { }
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse> { }
public interface INotificationHandler<in TNotification> where TNotification : INotification { }
public interface IStreamRequestHandler<in TRequest, out TResponse> where TRequest : IStreamRequest<TResponse> { }
}
namespace GFramework.Cqrs
{
public interface ICqrsHandlerRegistry
{
void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger);
}
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class CqrsHandlerRegistryAttribute : Attribute
{
public CqrsHandlerRegistryAttribute(Type registryType) { }
}
[AttributeUsage(AttributeTargets.Assembly)]
public sealed class CqrsReflectionFallbackAttribute : Attribute
{
public CqrsReflectionFallbackAttribute(params string[] fallbackHandlerTypeNames) { }
}
}
namespace TestApp
{
using GFramework.Cqrs.Abstractions.Cqrs;
public sealed record BrokenRequest() : IRequest<MissingResponse>;
public sealed class BrokenHandler : IRequestHandler<BrokenRequest, MissingResponse>
{
}
}
""";
var execution = ExecuteGenerator(source);
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();
Assert.Multiple(() =>
{
Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS0246"));
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"));
var generatedSource = execution.GeneratedSources[0].content;
Assert.That(
generatedSource,
Does.Contain("registryAssembly.GetType(\"MissingResponse\", throwOnError: false, ignoreCase: false);"));
Assert.That(
generatedSource,
Does.Contain("internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry"));
Assert.That(generatedSource, Does.Not.Contain("typeof(MissingResponse)"));
Assert.That(generatedSource, Does.Not.Contain("CqrsReflectionFallbackAttribute("));
});
}
/// <summary>
/// 验证 <see langword="dynamic" /> 响应类型会在生成阶段归一化为 <see cref="System.Object" />
/// 避免注册器发射非法的 <c>typeof(dynamic)</c>。
/// </summary>
[Test]
public void Emits_Object_Type_Reference_When_Handler_Response_Uses_Dynamic()
{
const string source = """
using System;
namespace Microsoft.Extensions.DependencyInjection
{
public interface IServiceCollection { }
public static class ServiceCollectionServiceExtensions
{
public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { }
}
}
namespace GFramework.Core.Abstractions.Logging
{
public interface ILogger
{
void Debug(string msg);
}
}
namespace GFramework.Cqrs.Abstractions.Cqrs
{
public interface IRequest<TResponse> { }
public interface INotification { }
public interface IStreamRequest<TResponse> { }
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse> { }
public interface INotificationHandler<in TNotification> where TNotification : INotification { }
public interface IStreamRequestHandler<in TRequest, out TResponse> where TRequest : IStreamRequest<TResponse> { }
}
namespace GFramework.Cqrs
{
public interface ICqrsHandlerRegistry
{
void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger);
}
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class CqrsHandlerRegistryAttribute : Attribute
{
public CqrsHandlerRegistryAttribute(Type registryType) { }
}
}
namespace TestApp
{
using GFramework.Cqrs.Abstractions.Cqrs;
public sealed record DynamicRequest() : IRequest<dynamic>;
public sealed class DynamicHandler : IRequestHandler<DynamicRequest, dynamic>
{
}
}
""";
var execution = ExecuteGenerator(source);
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();
Assert.Multiple(() =>
{
Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS1966"));
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"));
var generatedSource = execution.GeneratedSources[0].content;
Assert.That(generatedSource, Does.Contain("typeof(global::System.Object)"));
Assert.That(generatedSource, Does.Not.Contain("typeof(dynamic)"));
Assert.That(generatedSource, Does.Contain("internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry"));
});
}
/// <summary>
/// 验证当 fallback metadata 仍然必需且 runtime 提供了承载契约时,
/// 生成器会继续产出注册器并发射程序集级 <c>CqrsReflectionFallbackAttribute</c>。

View File

@ -1,4 +1,6 @@
using System.IO;
using System;
using System.IO;
using System.Linq;
using GFramework.Core.SourceGenerators.Rule;
using GFramework.SourceGenerators.Tests.Core;
@ -11,6 +13,60 @@ namespace GFramework.SourceGenerators.Tests.Rule;
[TestFixture]
public class ContextAwareGeneratorSnapshotTests
{
private const string SharedContextAwareInfrastructure = """
using System;
namespace GFramework.Core.SourceGenerators.Abstractions.Rule
{
[AttributeUsage(AttributeTargets.Class)]
public sealed class ContextAwareAttribute : Attribute { }
}
namespace GFramework.Core.Abstractions.Rule
{
public interface IContextAware
{
void SetContext(
GFramework.Core.Abstractions.Architectures.IArchitectureContext context);
GFramework.Core.Abstractions.Architectures.IArchitectureContext GetContext();
}
}
namespace GFramework.Core.Abstractions.Architectures
{
public interface IArchitectureContext { }
public interface IArchitectureContextProvider
{
IArchitectureContext GetContext();
bool TryGetContext<T>(out T? context) where T : class, IArchitectureContext;
}
}
namespace GFramework.Core.Architectures
{
using GFramework.Core.Abstractions.Architectures;
public sealed class GameContextProvider : IArchitectureContextProvider
{
public IArchitectureContext GetContext() => null;
public bool TryGetContext<T>(out T? context) where T : class, IArchitectureContext
{
context = null;
return false;
}
}
""";
private const string GameContextHelperSource = """
public static class GameContext
{
public static IArchitectureContext GetFirstArchitectureContext() => null;
}
""";
/// <summary>
/// 测试ContextAwareGenerator源代码生成器的快照功能
/// 验证生成器对带有ContextAware特性的类的处理结果
@ -19,76 +75,119 @@ public class ContextAwareGeneratorSnapshotTests
[Test]
public async Task Snapshot_ContextAwareGenerator()
{
// 定义测试用的源代码包含ContextAware特性和相关接口定义
const string source = """
using System;
namespace GFramework.Core.SourceGenerators.Abstractions.Rule
{
[AttributeUsage(AttributeTargets.Class)]
public sealed class ContextAwareAttribute : Attribute { }
}
namespace GFramework.Core.Abstractions.Rule
{
public interface IContextAware
{
void SetContext(
GFramework.Core.Abstractions.Architectures.IArchitectureContext context);
GFramework.Core.Abstractions.Architectures.IArchitectureContext GetContext();
}
}
namespace GFramework.Core.Abstractions.Architectures
{
public interface IArchitectureContext { }
public interface IArchitectureContextProvider
{
IArchitectureContext GetContext();
bool TryGetContext<T>(out T? context) where T : class, IArchitectureContext;
}
}
namespace GFramework.Core.Architectures
{
using GFramework.Core.Abstractions.Architectures;
public sealed class GameContextProvider : IArchitectureContextProvider
{
public IArchitectureContext GetContext() => null;
public bool TryGetContext<T>(out T? context) where T : class, IArchitectureContext
{
context = null;
return false;
}
}
public static class GameContext
{
public static IArchitectureContext GetFirstArchitectureContext() => null;
}
}
namespace TestApp
{
using GFramework.Core.SourceGenerators.Abstractions.Rule;
using GFramework.Core.Abstractions.Rule;
[ContextAware]
public partial class MyRule : IContextAware
{
}
}
""";
// 执行生成器快照测试,将生成的代码与预期快照进行比较
await GeneratorSnapshotTest<ContextAwareGenerator>.RunAsync(
source,
CreateContextAwareTestSource(
"""
[ContextAware]
public partial class MyRule : IContextAware
{
}
""",
includeGameContextHelper: true),
GetSnapshotFolder());
}
/// <summary>
/// 验证生成器在用户 partial 类型已经声明常见上下文字段名时仍能生成可编译代码。
/// </summary>
/// <returns>异步任务,无返回值。</returns>
[Test]
public async Task Snapshot_ContextAwareGenerator_With_User_Field_Name_Collisions()
{
await GeneratorSnapshotTest<ContextAwareGenerator>.RunAsync(
CreateContextAwareTestSource(
"""
using GFramework.Core.Abstractions.Architectures;
[ContextAware]
public partial class CollisionProneRule : IContextAware
{
private readonly string _context = "user-field";
private static readonly string _contextProvider = "user-provider";
private static readonly object _contextSync = new();
private IArchitectureContext? _gFrameworkContextAwareContext;
private static IArchitectureContextProvider? _gFrameworkContextAwareProvider;
private static readonly object _gFrameworkContextAwareSync = new();
}
"""),
GetSnapshotFolder());
}
/// <summary>
/// 验证生成器在基类已经占用自动生成字段名时,也会为派生规则类型分配带后缀的唯一成员名。
/// </summary>
/// <returns>异步任务,无返回值。</returns>
[Test]
public async Task Snapshot_ContextAwareGenerator_With_Inherited_Field_Name_Collisions()
{
await GeneratorSnapshotTest<ContextAwareGenerator>.RunAsync(
CreateContextAwareTestSource(
"""
using GFramework.Core.Abstractions.Architectures;
public abstract class ContextAwareRuleBase
{
protected IArchitectureContext? _gFrameworkContextAwareContext;
protected static IArchitectureContextProvider? _gFrameworkContextAwareProvider;
protected static readonly object _gFrameworkContextAwareSync = new();
}
[ContextAware]
public partial class InheritedCollisionRule : ContextAwareRuleBase, IContextAware
{
}
"""),
GetSnapshotFolder());
}
/// <summary>
/// 组装 ContextAwareGenerator 快照测试共用的最小宿主源码,避免每个用例都重复长块样板代码。
/// </summary>
/// <param name="testTypeDeclarations">放在 <c>TestApp</c> 命名空间内的测试类型声明。</param>
/// <param name="includeGameContextHelper">是否额外包含兼容旧快照输入的 <c>GameContext</c> 帮助类型。</param>
/// <returns>可直接交给生成器测试驱动的完整源码文本。</returns>
private static string CreateContextAwareTestSource(string testTypeDeclarations, bool includeGameContextHelper = false)
{
var gameContextHelper = includeGameContextHelper ? GameContextHelperSource : string.Empty;
var testAppDeclarations = IndentBlock(testTypeDeclarations, 4);
return string.Concat(
SharedContextAwareInfrastructure,
gameContextHelper,
"""
}
namespace TestApp
{
using GFramework.Core.SourceGenerators.Abstractions.Rule;
using GFramework.Core.Abstractions.Rule;
""",
testAppDeclarations,
"""
}
""");
}
/// <summary>
/// 为内嵌源码片段补齐缩进,使其能安全插入原始字符串模板中的命名空间块。
/// </summary>
/// <param name="text">要缩进的源码文本。</param>
/// <param name="spaces">每行前要补齐的空格数。</param>
/// <returns>已经补齐统一缩进的多行文本。</returns>
private static string IndentBlock(string text, int spaces)
{
var indentation = new string(' ', spaces);
return string.Join(
Environment.NewLine,
text.Replace("\r\n", "\n", StringComparison.Ordinal)
.Trim()
.Split('\n')
.Select(line => indentation + line));
}
/// <summary>
/// 将运行时测试目录映射回仓库内已提交的上下文感知生成器快照目录。
/// </summary>

View File

@ -0,0 +1,97 @@
// <auto-generated/>
#nullable enable
namespace TestApp;
/// <summary>
/// 为当前规则类型补充自动生成的架构上下文访问实现。
/// </summary>
/// <remarks>
/// 生成代码会在实例级缓存首次解析到的上下文,并在未显式配置提供者时回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
/// 同一生成类型的所有实例共享一个静态上下文提供者;切换或重置提供者只会影响尚未缓存上下文的新实例或未初始化实例,
/// 已缓存的实例上下文需要通过 <see cref="GFramework.Core.Abstractions.Rule.IContextAware.SetContext(GFramework.Core.Abstractions.Architectures.IArchitectureContext)" /> 显式覆盖。
/// 与手动继承 <see cref="global::GFramework.Core.Rule.ContextAwareBase" /> 的路径相比,生成实现会使用 <c>_gFrameworkContextAwareSync1</c> 协调惰性初始化、provider 切换和显式上下文注入;
/// <see cref="global::GFramework.Core.Rule.ContextAwareBase" /> 则保持无锁的实例级缓存语义,更适合已经由调用方线程模型保证串行访问的简单场景。
/// </remarks>
partial class CollisionProneRule : global::GFramework.Core.Abstractions.Rule.IContextAware
{
private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _gFrameworkContextAwareContext1;
private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? _gFrameworkContextAwareProvider1;
private static readonly object _gFrameworkContextAwareSync1 = new();
/// <summary>
/// 获取当前实例绑定的架构上下文。
/// </summary>
/// <remarks>
/// 该属性会先返回通过 <c>IContextAware.SetContext(...)</c> 显式注入的实例上下文;若尚未设置,则在同一个同步域内惰性初始化共享提供者。
/// 当静态提供者尚未配置时,生成代码会回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
/// 一旦某个实例成功缓存上下文,后续 <see cref="SetContextProvider(GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider)" />
/// 或 <see cref="ResetContextProvider" /> 不会自动清除此缓存;如需覆盖,请显式调用 <c>IContextAware.SetContext(...)</c>。
/// 当前实现还假设 <see cref="GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider.GetContext" /> 可在持有 <c>_gFrameworkContextAwareSync1</c> 时安全执行;
/// 自定义 provider 不应在该调用链内重新进入当前类型的 provider 配置 API且应避免引入与外部全局锁相互等待的锁顺序。
/// </remarks>
protected global::GFramework.Core.Abstractions.Architectures.IArchitectureContext Context
{
get
{
// 在同一个同步域内协调懒加载与 provider 切换,避免读取到被并发重置的空提供者。
// provider 的 GetContext() 会在持有 _gFrameworkContextAwareSync1 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。
lock (_gFrameworkContextAwareSync1)
{
_gFrameworkContextAwareProvider1 ??= new global::GFramework.Core.Architectures.GameContextProvider();
_gFrameworkContextAwareContext1 ??= _gFrameworkContextAwareProvider1.GetContext();
return _gFrameworkContextAwareContext1;
}
}
}
/// <summary>
/// 配置当前生成类型共享的上下文提供者。
/// </summary>
/// <param name="provider">后续懒加载上下文时要使用的提供者实例。</param>
/// <exception cref="global::System.ArgumentNullException">当 <paramref name="provider" /> 为 null 时抛出。</exception>
/// <remarks>
/// 该方法使用与 <see cref="Context" /> 相同的同步锁,避免提供者切换与惰性初始化交错。
/// 已经缓存上下文的实例不会因为提供者切换而自动失效;该变更仅影响尚未初始化上下文的新实例或未缓存实例。
/// 如需覆盖已有实例的上下文,请显式调用 <c>IContextAware.SetContext(...)</c>。
/// </remarks>
public static void SetContextProvider(global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider provider)
{
global::System.ArgumentNullException.ThrowIfNull(provider);
lock (_gFrameworkContextAwareSync1)
{
_gFrameworkContextAwareProvider1 = provider;
}
}
/// <summary>
/// 重置共享上下文提供者,使后续懒加载回退到默认提供者。
/// </summary>
/// <remarks>
/// 该方法主要用于测试清理或跨用例恢复默认行为。
/// 它不会清除已经缓存到实例字段中的上下文;只有后续尚未初始化上下文的实例会重新回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
/// 如需覆盖已有实例的上下文,请显式调用 <c>IContextAware.SetContext(...)</c>。
/// </remarks>
public static void ResetContextProvider()
{
lock (_gFrameworkContextAwareSync1)
{
_gFrameworkContextAwareProvider1 = null;
}
}
void global::GFramework.Core.Abstractions.Rule.IContextAware.SetContext(global::GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
{
// 与 Context getter 共享同一同步协议,避免显式注入被并发懒加载覆盖。
lock (_gFrameworkContextAwareSync1)
{
_gFrameworkContextAwareContext1 = context;
}
}
global::GFramework.Core.Abstractions.Architectures.IArchitectureContext global::GFramework.Core.Abstractions.Rule.IContextAware.GetContext()
{
return Context;
}
}

View File

@ -0,0 +1,97 @@
// <auto-generated/>
#nullable enable
namespace TestApp;
/// <summary>
/// 为当前规则类型补充自动生成的架构上下文访问实现。
/// </summary>
/// <remarks>
/// 生成代码会在实例级缓存首次解析到的上下文,并在未显式配置提供者时回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
/// 同一生成类型的所有实例共享一个静态上下文提供者;切换或重置提供者只会影响尚未缓存上下文的新实例或未初始化实例,
/// 已缓存的实例上下文需要通过 <see cref="GFramework.Core.Abstractions.Rule.IContextAware.SetContext(GFramework.Core.Abstractions.Architectures.IArchitectureContext)" /> 显式覆盖。
/// 与手动继承 <see cref="global::GFramework.Core.Rule.ContextAwareBase" /> 的路径相比,生成实现会使用 <c>_gFrameworkContextAwareSync1</c> 协调惰性初始化、provider 切换和显式上下文注入;
/// <see cref="global::GFramework.Core.Rule.ContextAwareBase" /> 则保持无锁的实例级缓存语义,更适合已经由调用方线程模型保证串行访问的简单场景。
/// </remarks>
partial class InheritedCollisionRule : global::GFramework.Core.Abstractions.Rule.IContextAware
{
private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _gFrameworkContextAwareContext1;
private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? _gFrameworkContextAwareProvider1;
private static readonly object _gFrameworkContextAwareSync1 = new();
/// <summary>
/// 获取当前实例绑定的架构上下文。
/// </summary>
/// <remarks>
/// 该属性会先返回通过 <c>IContextAware.SetContext(...)</c> 显式注入的实例上下文;若尚未设置,则在同一个同步域内惰性初始化共享提供者。
/// 当静态提供者尚未配置时,生成代码会回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
/// 一旦某个实例成功缓存上下文,后续 <see cref="SetContextProvider(GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider)" />
/// 或 <see cref="ResetContextProvider" /> 不会自动清除此缓存;如需覆盖,请显式调用 <c>IContextAware.SetContext(...)</c>。
/// 当前实现还假设 <see cref="GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider.GetContext" /> 可在持有 <c>_gFrameworkContextAwareSync1</c> 时安全执行;
/// 自定义 provider 不应在该调用链内重新进入当前类型的 provider 配置 API且应避免引入与外部全局锁相互等待的锁顺序。
/// </remarks>
protected global::GFramework.Core.Abstractions.Architectures.IArchitectureContext Context
{
get
{
// 在同一个同步域内协调懒加载与 provider 切换,避免读取到被并发重置的空提供者。
// provider 的 GetContext() 会在持有 _gFrameworkContextAwareSync1 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。
lock (_gFrameworkContextAwareSync1)
{
_gFrameworkContextAwareProvider1 ??= new global::GFramework.Core.Architectures.GameContextProvider();
_gFrameworkContextAwareContext1 ??= _gFrameworkContextAwareProvider1.GetContext();
return _gFrameworkContextAwareContext1;
}
}
}
/// <summary>
/// 配置当前生成类型共享的上下文提供者。
/// </summary>
/// <param name="provider">后续懒加载上下文时要使用的提供者实例。</param>
/// <exception cref="global::System.ArgumentNullException">当 <paramref name="provider" /> 为 null 时抛出。</exception>
/// <remarks>
/// 该方法使用与 <see cref="Context" /> 相同的同步锁,避免提供者切换与惰性初始化交错。
/// 已经缓存上下文的实例不会因为提供者切换而自动失效;该变更仅影响尚未初始化上下文的新实例或未缓存实例。
/// 如需覆盖已有实例的上下文,请显式调用 <c>IContextAware.SetContext(...)</c>。
/// </remarks>
public static void SetContextProvider(global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider provider)
{
global::System.ArgumentNullException.ThrowIfNull(provider);
lock (_gFrameworkContextAwareSync1)
{
_gFrameworkContextAwareProvider1 = provider;
}
}
/// <summary>
/// 重置共享上下文提供者,使后续懒加载回退到默认提供者。
/// </summary>
/// <remarks>
/// 该方法主要用于测试清理或跨用例恢复默认行为。
/// 它不会清除已经缓存到实例字段中的上下文;只有后续尚未初始化上下文的实例会重新回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
/// 如需覆盖已有实例的上下文,请显式调用 <c>IContextAware.SetContext(...)</c>。
/// </remarks>
public static void ResetContextProvider()
{
lock (_gFrameworkContextAwareSync1)
{
_gFrameworkContextAwareProvider1 = null;
}
}
void global::GFramework.Core.Abstractions.Rule.IContextAware.SetContext(global::GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
{
// 与 Context getter 共享同一同步协议,避免显式注入被并发懒加载覆盖。
lock (_gFrameworkContextAwareSync1)
{
_gFrameworkContextAwareContext1 = context;
}
}
global::GFramework.Core.Abstractions.Architectures.IArchitectureContext global::GFramework.Core.Abstractions.Rule.IContextAware.GetContext()
{
return Context;
}
}

View File

@ -10,14 +10,14 @@ namespace TestApp;
/// 生成代码会在实例级缓存首次解析到的上下文,并在未显式配置提供者时回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
/// 同一生成类型的所有实例共享一个静态上下文提供者;切换或重置提供者只会影响尚未缓存上下文的新实例或未初始化实例,
/// 已缓存的实例上下文需要通过 <see cref="GFramework.Core.Abstractions.Rule.IContextAware.SetContext(GFramework.Core.Abstractions.Architectures.IArchitectureContext)" /> 显式覆盖。
/// 与手动继承 <see cref="global::GFramework.Core.Rule.ContextAwareBase" /> 的路径相比,生成实现会使用 <c>_contextSync</c> 协调惰性初始化、provider 切换和显式上下文注入;
/// 与手动继承 <see cref="global::GFramework.Core.Rule.ContextAwareBase" /> 的路径相比,生成实现会使用 <c>_gFrameworkContextAwareSync</c> 协调惰性初始化、provider 切换和显式上下文注入;
/// <see cref="global::GFramework.Core.Rule.ContextAwareBase" /> 则保持无锁的实例级缓存语义,更适合已经由调用方线程模型保证串行访问的简单场景。
/// </remarks>
partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware
{
private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _context;
private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? _contextProvider;
private static readonly object _contextSync = new();
private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _gFrameworkContextAwareContext;
private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? _gFrameworkContextAwareProvider;
private static readonly object _gFrameworkContextAwareSync = new();
/// <summary>
/// 获取当前实例绑定的架构上下文。
@ -27,26 +27,20 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware
/// 当静态提供者尚未配置时,生成代码会回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
/// 一旦某个实例成功缓存上下文,后续 <see cref="SetContextProvider(GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider)" />
/// 或 <see cref="ResetContextProvider" /> 不会自动清除此缓存;如需覆盖,请显式调用 <c>IContextAware.SetContext(...)</c>。
/// 当前实现还假设 <see cref="GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider.GetContext" /> 可在持有 <c>_contextSync</c> 时安全执行;
/// 当前实现还假设 <see cref="GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider.GetContext" /> 可在持有 <c>_gFrameworkContextAwareSync</c> 时安全执行;
/// 自定义 provider 不应在该调用链内重新进入当前类型的 provider 配置 API且应避免引入与外部全局锁相互等待的锁顺序。
/// </remarks>
protected global::GFramework.Core.Abstractions.Architectures.IArchitectureContext Context
{
get
{
var context = _context;
if (context is not null)
{
return context;
}
// 在同一个同步域内协调懒加载与 provider 切换,避免读取到被并发重置的空提供者。
// provider 的 GetContext() 会在持有 _contextSync 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。
lock (_contextSync)
// provider 的 GetContext() 会在持有 _gFrameworkContextAwareSync 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。
lock (_gFrameworkContextAwareSync)
{
_contextProvider ??= new global::GFramework.Core.Architectures.GameContextProvider();
_context ??= _contextProvider.GetContext();
return _context;
_gFrameworkContextAwareProvider ??= new global::GFramework.Core.Architectures.GameContextProvider();
_gFrameworkContextAwareContext ??= _gFrameworkContextAwareProvider.GetContext();
return _gFrameworkContextAwareContext;
}
}
}
@ -55,6 +49,7 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware
/// 配置当前生成类型共享的上下文提供者。
/// </summary>
/// <param name="provider">后续懒加载上下文时要使用的提供者实例。</param>
/// <exception cref="global::System.ArgumentNullException">当 <paramref name="provider" /> 为 null 时抛出。</exception>
/// <remarks>
/// 该方法使用与 <see cref="Context" /> 相同的同步锁,避免提供者切换与惰性初始化交错。
/// 已经缓存上下文的实例不会因为提供者切换而自动失效;该变更仅影响尚未初始化上下文的新实例或未缓存实例。
@ -62,9 +57,10 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware
/// </remarks>
public static void SetContextProvider(global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider provider)
{
lock (_contextSync)
global::System.ArgumentNullException.ThrowIfNull(provider);
lock (_gFrameworkContextAwareSync)
{
_contextProvider = provider;
_gFrameworkContextAwareProvider = provider;
}
}
@ -78,18 +74,18 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware
/// </remarks>
public static void ResetContextProvider()
{
lock (_contextSync)
lock (_gFrameworkContextAwareSync)
{
_contextProvider = null;
_gFrameworkContextAwareProvider = null;
}
}
void global::GFramework.Core.Abstractions.Rule.IContextAware.SetContext(global::GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
{
// 与 Context getter 共享同一同步协议,避免显式注入被并发懒加载覆盖。
lock (_contextSync)
lock (_gFrameworkContextAwareSync)
{
_context = context;
_gFrameworkContextAwareContext = context;
}
}

View File

@ -7,17 +7,51 @@
## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-015`
- 当前阶段:`Phase 15`
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-028`
- 当前阶段:`Phase 28`
- 当前焦点:
- 当前分支 PR #267 的失败测试已通过 `$gframework-pr-review` 与本地整包测试完成复核
- 已确认并修复 `AsyncLogAppender.Flush()` 在“后台线程先清空队列”场景下可能超时返回 `false` 的竞态
- 已补上稳定回归测试,避免只在整包 `GFramework.Core.Tests` 里偶发暴露的刷新完成信号问题再次回归
- 下一轮默认恢复到 `MA0016``MA0002` 低风险批次;`MA0015``MA0077` 继续作为尾项顺手吸收
- 已完成 `GFramework.Core` 当前 `MA0016` / `MA0002` / `MA0015` / `MA0077` 低风险收口批次
- 已复核 `net10.0` 下的 `MA0158` 基线:`GFramework.Core` / `GFramework.Cqrs` 当前共有 `16` 个 object lock
建议点,属于跨 target 兼容性风险,不在本轮直接批量替换
- 已完成 `GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs` 的剩余 `MA0051` 结构拆分,生成输出保持不变
- 已完成 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs``MA0051` 结构拆分,生成输出保持不变
- 已完成 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs``MA0006` 低风险收口schema 关键字比较显式使用
`StringComparison.Ordinal`
- 已完成 `SchemaConfigGenerator.cs` 的第一批 `MA0051` 结构拆分schema 入口解析、属性解析、schema 遍历、数组属性解析、
约束文档生成与若干生成代码发射 helper 已拆出语义阶段
- 已完成当前 PR #269 review follow-up`CqrsHandlerRegistryGenerator` 按职责拆分为 partial 生成器文件,
`ContextAwareGenerator` 已补上字段名去冲突与锁内读取修正,`Option<T>` 补齐 `<remarks>` 契约说明
- 已完成当前 PR #269 第二轮 follow-up恢复 `EasyEvents``CollectionExtensions``LoggingConfiguration`
`FilterConfiguration` 的公共 API 兼容形状,并将 analyzer 兼容性处理收敛到局部 pragma
- 已完成当前 PR #269 第三轮 follow-up继续收口 `SchemaConfigGenerator` 的根类型标识符校验与 XML 文档转义,
并补齐 `LoggingConfigurationTests``CollectionExtensionsTests``Cqrs` helper 抽取与 `ai-plan` 命令文本修正
- 已完成当前 PR #269 第四轮 follow-up`CqrsHandlerRegistryGenerator` 的 Roslyn error type 直接引用改为
运行时精确查找路径,并为 `SchemaConfigGenerator` 补上根 `type` 非字符串时的防御与回归测试
- 已完成当前 PR #269 第五轮 follow-up`SchemaConfigGenerator` 补上归一化后属性名冲突诊断并新增
`GF_ConfigSchema_014``CqrsHandlerRegistryGenerator``dynamic` 归一化为 `global::System.Object`
同时收紧相关 generator regression tests
- 已完成当前 PR #269 failed-test follow-up修正 `SchemaConfigGeneratorTests`
`Run_Should_Assign_Globally_Unique_Reference_Metadata_Member_Names` 的测试输入,使其继续覆盖
reference metadata 成员名全局去冲突,但不再依赖现已被 `GF_ConfigSchema_014` 拦截的非法同层 schema key 冲突
- 已完成当前 PR #269 Greptile follow-up`ContextAwareGenerator` 现在会把基类链显式成员名也纳入
`_gFrameworkContextAware*` 字段分配冲突检测,并新增 inherited-field collision 快照回归测试
- 已完成当前分支与 `main``CqrsHandlerRegistryGenerator.cs` 文件级冲突收口:确认 `main` 侧新增的是
`OrderedRegistrationKind` / `RuntimeTypeReferenceSpec` 的 XML 文档,现已按当前 partial 拆分结构迁移到
`CqrsHandlerRegistryGenerator.Models.cs`,不回退已完成的生成器拆分
- 已更新 `AGENTS.md`:变更模块必须运行对应 `dotnet build -c Release`,并处理或显式报告模块构建 warning
不再默认留给长期 warning 清理分支
- `CoroutineScheduler` 的 tag/group 字典已显式使用 `StringComparer.Ordinal`,保持既有区分大小写语义
- `EasyEvents.AddEvent<T>()` 的重复注册路径已恢复为 `ArgumentException`,以保持既有异常契约
- `Option<T>` 已声明 `IEquatable<Option<T>>`,与已有强类型 `Equals(Option<T>)` 契约对齐
- 当前 `GFramework.Core` `net8.0` warnings-only 基线已降到 `0`
- 当前 `GFramework.Core.SourceGenerators` warnings-only 基线已降到 `0`
- 当前 `GFramework.Cqrs.SourceGenerators` warnings-only 基线已降到 `0`
- 当前 `GFramework.Game.SourceGenerators` warnings-only 基线已从 `46` 条降到 `9` 条,剩余均为
`SchemaConfigGenerator.cs``MA0051`
- `GFramework.Godot``Timing.cs` 已同步适配新事件签名,但当前 worktree 的 Godot restore 资产仍受 Windows fallback package folder 干扰,独立 build 需在修复资产后补跑
- 后续继续按 warning 类型和数量批处理,而不是回退到按单文件切片推进
- 当某一轮主类型数量不足时,允许顺手合并其他低冲突 warning 类型,`MA0015``MA0077`
只是当前最明显的低数量示例,不构成限定
- 下一轮默认继续拆分 `GFramework.Game.SourceGenerators``MA0051` 热点,或评估跨 target 的 `MA0158`
锁替换风险
- 单次 `boot` 的工作树改动上限控制在约 `100` 个文件以内,避免 recovery context 与 review 面同时失控
- 若任务边界互不冲突,允许使用不同模型的 subagent 并行处理不同 warning 类型或不同目录,但必须遵守显式 ownership
@ -34,8 +68,24 @@
`PhaseChanged` / `CoroutineExceptionEventArgs` XML 文档、`PhaseChanged` 迁移说明和 `ai-plan` 基线注释
- 已完成当前 PR #267 failed-test follow-up修复 `AsyncLogAppender.Flush()` 在队列已被后台线程提前清空时仍可能
等待满默认超时并返回 `false` 的竞态,并通过整包 `GFramework.Core.Tests` 重新验证
- 当前 `GFramework.Core` `net8.0` warnings-only 基线已降到 `9` 条;剩余 warning 集中在
`MA0016` 集合抽象接口、`MA0002` comparer 重载,以及 `MA0015` / `MA0077` 两个低数量尾项
- 已完成当前 `GFramework.Core` `net8.0` 剩余低风险 analyzer warning 批次warnings-only 基线已降到 `0`
- 已完成 `GFramework.Core.SourceGenerators``ContextAwareGenerator` 的剩余 `MA0051` 收口warnings-only 基线已降到 `0`
- 已完成 `GFramework.Cqrs.SourceGenerators``CqrsHandlerRegistryGenerator` 的剩余 `MA0051` 收口warnings-only 基线已降到 `0`
- 已完成当前 PR #269 的 review follow-up收口 `ContextAwareGenerator` 的字段命名冲突 / 锁内读取契约、
`CqrsHandlerRegistryGenerator` 的运行时类型 null 防御与超大文件拆分、`SchemaConfigGenerator` 的取消语义,
并恢复 `EasyEvents` / `CollectionExtensions` / logging 配置模型的公共 API 兼容形状
- 已完成当前 PR #269 的第四轮 review follow-up确认 5 个 latest-head 未解决线程中仅剩 2 个本地仍成立,
已分别在 `CqrsHandlerRegistryGenerator``SchemaConfigGenerator` 中收口,并补齐定向 generator regression tests
- 已完成当前 PR #269 的第五轮 review follow-up收口 `SchemaConfigGenerator` 的归一化字段名冲突诊断、
`CqrsHandlerRegistryGenerator``dynamic` 类型引用风险,并同步更新 `AGENTS.md` 的模块 build / warning 治理规范
- 已完成当前 PR #269 的 failed-test follow-up将 reference metadata 成员名唯一性回归测试改为合法 schema 路径组合,
并重新通过定向 generator test
- 已完成当前 PR #269 的 Greptile follow-up修复 `ContextAwareGenerator` 未覆盖基类成员名冲突的问题,并补齐
inherited-collision 快照测试
- 已完成当前分支与 `main``CqrsHandlerRegistryGenerator.cs` 冲突化解:保留当前 partial 结构,并把
`main` 侧新增的模型文档合并到 `CqrsHandlerRegistryGenerator.Models.cs`
- 已完成 `GFramework.Game.SourceGenerators``SchemaConfigGenerator` 的第一批 `MA0051` 收口warnings-only 基线剩余 `9`
`MA0051`
## 当前活跃事实
@ -66,16 +116,53 @@
nitpick comment 后,确认 8 条高信号项中仍成立的是 1 个行为 bug 与 7 个文档/测试/跟踪缺口,并按最小改动收口
- `RP-015` 使用 `$gframework-pr-review` 复核 PR #267 的 CTRF 失败测试评论后,确认 `AsyncLogAppender` 仍存在
“队列已空但 Flush 仍超时失败”的竞态;该问题在本地整包 `GFramework.Core.Tests` 中可复现,现已修复并补上稳定回归测试
- `RP-016``GFramework.Core` 当前剩余 `MA0016` / `MA0002` / `MA0015` / `MA0077` 低风险批次清零,并用
warnings-only build 与 focused tests 验证配置反序列化、集合扩展、事件重复注册、`Option<T>` 相等性和协程 tag/group 语义
- `RP-017` 复核 `MA0158` 当前仍是跨 target 锁类型迁移问题,因此先收口单点 `ContextAwareGenerator` `MA0051`
并通过 source generator 项目 build 与 `ContextAwareGeneratorSnapshotTests` 验证生成输出未回归
- `RP-018` 暂缓跨 target `MA0158`,转入 `GFramework.Cqrs.SourceGenerators` 的单文件结构性 warning
通过拆分 handler 分析、运行时类型引用构造、注册器源码发射与精确反射注册输出阶段,清空该项目当前 `MA0051`
- `RP-019` 转入 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs`,先完成低风险 `MA0006` 批次;
通过 schema 类型比较 helper 与显式 `StringComparison.Ordinal` 清空当前项目的 `MA0006`
- `RP-020` 继续拆分 `SchemaConfigGenerator.cs``MA0051` 热点,将当前项目 warnings-only 基线从 `19` 条降到 `9` 条,
并用 focused schema generator tests 验证 50 个用例通过
- `RP-021` 使用 `$gframework-pr-review` 复核当前分支 PR #269 后,修复仍在本地成立的 4 个项:将
`CqrsHandlerRegistryGenerator` 拆分为职责清晰的 partial 文件、为 `ContextAwareGenerator` 生成字段增加稳定前缀并补上
`SetContextProvider` 的运行时 null 校验、为 `Option<T>` 补齐 `<remarks>`,并新增字段重名场景的生成器快照测试
- `RP-022` 继续复核 PR #269 的 latest-head review threads 与 nitpick确认仍成立的项包括公共 API 兼容回退、
`ContextAwareGenerator` 字段名真正去冲突与锁内读取、`SchemaConfigGenerator` 取消传播、`Cqrs` 运行时类型 null 防御;
已补齐对应回归测试与 focused build/test 验证
- `RP-023` 继续复核 PR #269 剩余 nitpick/outside-diff 项,确认仍成立的项集中在 `SchemaConfigGenerator` 根类型名校验、
aggregate registration comparer XML 文档转义、logging / collection 反射测试补强,以及跟踪文档中的
`RestoreFallbackFolders=""` 可复制性问题
- `RP-024` 使用 `$gframework-pr-review` 继续复核 PR #269 latest-head unresolved threads确认 `EasyEvents` 异常契约、
`SchemaConfigGenerator` 取消传播与 `ContextAwareGenerator` 快照冲突线程均已在本地收口,仅剩 `Cqrs` error type
直接引用与根 schema `type` 非字符串防御仍成立;现已补齐实现与回归测试
- `RP-025` 继续复核 PR #269 剩余 outside-diff / nitpick 信号后,确认本地仍成立的是 `SchemaConfigGenerator`
的归一化字段名冲突与 `Cqrs``dynamic` 的直接类型引用;已分别补上诊断、运行时类型归一化与回归测试,
并把“变更模块必须运行对应 build 且处理 warning”的治理规则写回 `AGENTS.md`
- 当前工作树分支 `fix/analyzer-warning-reduction-batch` 已在 `ai-plan/public/README.md` 建立 topic 映射
## 当前风险
- 公共契约兼容风险:剩余 `MA0016` 若直接改公开集合类型,可能波及用户代码
- 缓解措施:优先选择不改公共 API 的低风险切法;若必须触达公共契约,先补齐 XML 契约说明与定向测试
- analyzer 收口回退风险:后续若继续压 `MA0015` / `MA0016`,容易再次把公共 API 收窄成与既有契约不兼容的形状
- 缓解措施:优先保留既有公共 API并将兼容性例外收敛到局部 pragma继续用反射断言覆盖返回类型、属性类型与异常类型
- 测试宿主稳定性风险:部分 Godot 失败路径在当前 .NET 测试宿主下仍不稳定
- 缓解措施:继续优先使用稳定的 targeted test、项目构建和相邻 smoke test 组合验证
- 多目标框架 warning 解释风险:同一源位置会在多个 target framework 下重复计数
- 缓解措施:继续以唯一源位置和 warning 家族为主要决策依据,而不是只看原始 warning 总数
- net10 专属 warning 风险:`MA0158` 建议使用 `System.Threading.Lock`,但项目多 target 时需要确认兼容边界
- 缓解措施:下一轮先按 target framework 与 API 可用性评估,不直接批量替换共享源码中的 `object` lock
- source generator warning 外溢风险:运行 `GFramework.SourceGenerators.Tests` 会构建相邻 generator/test 项目并显示既有
`GFramework.Game.SourceGenerators` 与测试项目 warning
- 缓解措施:继续以被修改 generator 项目的独立 warnings-only build 作为主验收,并用 focused generator test 验证行为
- source generator test warning 治理风险:`GFramework.SourceGenerators.Tests` 当前仍有既有 `MA0051` / `MA0004` / `MA0048`
warning本轮 focused test 已通过,但测试项目整包 warning 尚未进入本轮写集
- 缓解措施:本轮已在 failed-test follow-up 的定向 `dotnet test` 中再次确认这些 warning 仍为既有基线;后续若继续修改该测试项目,
应按新增 `AGENTS.md` 规则先明确 warning 收口范围,再决定是否进入专门清理切片
- ContextAware 基类命名隐藏风险:若生成器只看当前类型声明成员,派生规则会重新占用基类已声明的
`_gFrameworkContextAware*` 字段名,导致生成成员隐藏继承状态并让快照无法锁定后缀分配行为
- 缓解措施:本轮已改为遍历完整 base-type 链收集保留名,并用 inherited collision 快照用例锁定该行为
- Godot 资产文件环境风险:当前 worktree 的 `GFramework.Godot` restore/build 仍会命中 Windows fallback package folder
- 缓解措施:后续若继续触达 Godot 模块,先用 Linux 侧 restore 资产或 Windows-hosted 构建链刷新该项目,再补跑定向 build
- 并行实现风险:批量收敛时若 subagent 写入边界不清晰,容易引入命名冲突或重复重构
@ -158,12 +245,80 @@
- 结果:`15 Passed``0 Failed`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --disable-build-servers`
- 结果:`1607 Passed``0 Failed`
- `RP-016` 的验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -nologo -clp:"Summary;WarningsOnly"`
- 结果:`0 Warning(s)``0 Error(s)`;当前 `GFramework.Core` `net8.0` analyzer baseline 已清零
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~LoggingConfigurationTests|FullyQualifiedName~ConfigurableLoggerFactoryTests|FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~EasyEventsTests|FullyQualifiedName~OptionTests|FullyQualifiedName~CoroutineGroupTests|FullyQualifiedName~CoroutineSchedulerTests" -m:1 -nologo`
- 结果:`112 Passed``0 Failed`;测试构建仍会显示既有 `net10.0` `MA0158` 与 source generator `MA0051` warning
- `RP-017` 的验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net10.0 -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
- 结果:`16 Warning(s)``0 Error(s)`;当前 `MA0158``GFramework.Core` / `GFramework.Cqrs`,本轮只记录基线不批量改锁
- `dotnet build GFramework.Core.SourceGenerators/GFramework.Core.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
- 结果:`0 Warning(s)``0 Error(s)``ContextAwareGenerator.cs` 已不再出现 `MA0051`
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~ContextAwareGeneratorSnapshotTests" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`1 Passed``0 Failed`;测试构建仍显示相邻 source generator 和测试项目的既有 analyzer warning
- `RP-018` 的验证结果:
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
- 结果:`0 Warning(s)``0 Error(s)``CqrsHandlerRegistryGenerator.cs` 当前 `MA0051` 已清零
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~CqrsHandlerRegistryGeneratorTests -m:1 -p:RestoreFallbackFolders= -nologo`
- 结果:`14 Passed``0 Failed`
- 说明:该 test project 构建仍显示 `GFramework.Game.SourceGenerators` 与测试项目中的既有 analyzer warning本轮关注的
`GFramework.Cqrs.SourceGenerators` 独立 build 已清零
- `RP-019` 的验证结果:
- `dotnet restore GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -p:RestoreFallbackFolders= -nologo`
- 结果:通过;刷新 Linux 侧资产以清除 stale Windows fallback package folder
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
- 结果:`19 Warning(s)``0 Error(s)`;当前项目输出已不再出现 `MA0006`,剩余均为 `SchemaConfigGenerator.cs`
`MA0051`
- `dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders= -nologo`
- 结果:通过;刷新 test project 资产以清除 stale Windows fallback package folder
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~SchemaConfigGenerator -m:1 -p:RestoreFallbackFolders= -nologo`
- 结果:`50 Passed``0 Failed`
- 说明:测试项目构建仍显示既有 source generator test analyzer warning不属于本轮写集
- `RP-020` 的验证结果:
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
- 结果:`9 Warning(s)``0 Error(s)`;当前项目剩余 warning 均为 `SchemaConfigGenerator.cs``MA0051`
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~SchemaConfigGenerator -m:1 -p:RestoreFallbackFolders= -nologo`
- 结果:`50 Passed``0 Failed`
- 说明:测试项目构建仍显示既有 source generator test analyzer warning不属于本轮写集
- `RP-021` 的验证结果:
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
- 结果:`0 Warning(s)``0 Error(s)`;拆分后最大单文件已降到 `851` 行,满足仓库 800-1000 行上限
- `dotnet build GFramework.Core.SourceGenerators/GFramework.Core.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
- 结果:`0 Warning(s)``0 Error(s)``ContextAwareGenerator` 的字段命名与 provider 契约修复未引入新的 generator warning
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~ContextAwareGeneratorSnapshotTests|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests" -m:1 -p:RestoreFallbackFolders= -nologo`
- 结果:先并行运行两条 `dotnet test` 时触发共享输出文件锁冲突;改为串行重跑后 `ContextAwareGeneratorSnapshotTests=2 Passed`
`CqrsHandlerRegistryGeneratorTests=14 Passed`
- 说明:失败来自测试宿主并行写入同一 build 输出,不是代码回归;串行重跑后快照新增的字段重名场景和 CQRS 快照均通过
- `RP-022` 的验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:通过;`EasyEvents``CollectionExtensions` 与 logging 配置模型的兼容性回退未引入新的 `net8.0` 构建错误
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -v minimal`
- 结果:通过;`ContainingAssembly` null 防御与发射 helper 精简未引入新的构建错误
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -v minimal`
- 结果:通过;仍保留既有 `9``SchemaConfigGenerator.cs` `MA0051` warning非本轮新增
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~SchemaConfigGeneratorTests|FullyQualifiedName~ContextAwareGeneratorSnapshotTests|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
- 结果:`63 Passed``0 Failed`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~EasyEventsTests|FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~LoggingConfigurationTests|FullyQualifiedName~ConfigurableLoggerFactoryTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
- 结果:`38 Passed``0 Failed`
- `RP-023` 的验证结果:
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -v minimal`
- 结果:通过;仍保留既有 `9``SchemaConfigGenerator.cs` `MA0051` warning未新增新的 generator warning
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -v minimal`
- 结果:通过;并行构建时 `GFramework.SourceGenerators.Common.dll` 复制阶段出现一次 `MSB3026` 重试,随后成功完成
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~SchemaConfigGeneratorTests|FullyQualifiedName~SchemaConfigGeneratorSnapshotTests|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
- 结果:`63 Passed``0 Failed`
- 说明:测试项目构建仍会显示既有 `GFramework.SourceGenerators.Tests` analyzer warning不属于本轮写集
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~LoggingConfigurationTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
- 结果:`27 Passed``0 Failed`
- active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史
## 下一步
1. 若要继续该主题,先读 active tracking再按需展开历史归档中的 warning 热点与验证记录
2. 下一轮优先在 `MA0016``MA0002` 之间选择低风险批次继续推进,默认先看 `LoggingConfiguration` /
`FilterConfiguration``CollectionExtensions`
3. 若后续继续改动 `GFramework.Godot`,先修复该项目的 Linux 侧 restore 资产,再补跑独立 build
4. 若本主题确认暂缓,可保持当前归档状态,不需要再恢复 `local-plan/`
2. 下一轮优先继续拆分 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs` 的剩余 `MA0051`;建议先从
`GenerateBindingsClass``AppendGeneratedConfigCatalogType` 或对象/条件 schema target 验证方法切入
3. 若改回推进 `MA0158`,先设计 `net8.0` / `net9.0` / `net10.0` 多 target 条件编译方案,不直接批量替换共享源码中的
`object` lock
4. 若后续继续改动 `GFramework.Godot`,先修复该项目的 Linux 侧 restore 资产,再补跑独立 build
5. 若本主题确认暂缓,可保持当前归档状态,不需要再恢复 `local-plan/`

View File

@ -1,5 +1,403 @@
# Analyzer Warning Reduction 追踪
## 2026-04-23 — RP-028
### 阶段:`CqrsHandlerRegistryGenerator.cs` 文件级冲突化解RP-028
- 启动复核:
- 用户指出当前分支与 `main``GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs`
存在冲突,需要人工确认并解决
- 本地检查后确认工作树没有 `UU` 或冲突标记;进一步对比 `origin/main` 发现冲突根因不是运行逻辑回退,而是
`main` 在旧的单文件版本里新增了 `OrderedRegistrationKind` / `RuntimeTypeReferenceSpec` 的 XML 文档,
而当前分支已将这些类型拆分到 `CqrsHandlerRegistryGenerator.Models.cs`
- 决策:
- 保留当前分支已经完成的 partial 拆分,不把模型重新塞回 `CqrsHandlerRegistryGenerator.cs`
- 以“迁移 `main` 侧文档意图到拆分后的归属文件”为人工合并策略,避免既回退结构拆分又遗漏 `main` 新增文档
- 实施调整:
- 将 `OrderedRegistrationKind` 的枚举说明与 `RuntimeTypeReferenceSpec` / `FromDirectReference` /
`FromReflectionLookup` / `FromExternalReflectionLookup` / `FromArray` / `FromConstructedGeneric`
的 XML 文档迁移到 `CqrsHandlerRegistryGenerator.Models.cs`
- 保持 `CqrsHandlerRegistryGenerator.cs` 主文件只承载主生成管线,不引入重复模型定义
- 验证结果:
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -clp:"Summary;WarningsOnly" -nologo`
- 结果:`0 Warning(s)``0 Error(s)`
- 下一步建议:
- 若后续继续处理分支冲突,优先先判断 `main` 改动是否已在当前 partial 文件集里存在等价归属,再决定是否需要真正 merge/rebase
- 若回到 PR #269 收口,可继续抓取最新 unresolved threads 与 CI 状态
## 2026-04-23 — RP-027
### 阶段PR #269 Greptile inherited-member collision follow-upRP-027
- 启动复核:
- 根据用户补充,重新核对 `$gframework-pr-review` 抓下来的 `greptile-apps[bot]` unresolved 线程,确认仍有一条
`ContextAwareGenerator` 关于 inherited member names 未参与 collision detection 的 P1 评论
- 本地读取 `CreateGeneratedContextMemberNames(...)` 后确认当前实现只收集 `symbol.GetMembers()`,确实没有遍历基类链
- 决策:
- 保持现有 `_gFrameworkContextAware*` 前缀和数字后缀分配规则不变,只把保留名集合扩展为“当前类型 + 基类链显式成员”
- 沿用既有 `ContextAwareGeneratorSnapshotTests` 模式,新增 inherited-field collision 快照,而不是只写松散字符串断言
- 实施调整:
- 为 `ContextAwareGenerator` 新增 `CollectReservedContextMemberNames(...)` helper遍历完整 `BaseType` 链收集显式成员名
- 为 `ContextAwareGeneratorSnapshotTests` 增加 `InheritedCollisionRule` 场景,并抽出公共测试源码 helper避免重复样板
- 新增快照 `InheritedCollisionRule.ContextAware.g.cs`,锁定基类已声明 `_gFrameworkContextAware*` 时生成器会回退到 `...1` 后缀
- 验证结果:
- `dotnet build GFramework.Core.SourceGenerators/GFramework.Core.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -clp:"Summary;WarningsOnly" -nologo`
- 结果:`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~ContextAwareGeneratorSnapshotTests.Snapshot_ContextAwareGenerator_With_Inherited_Field_Name_Collisions|FullyQualifiedName~ContextAwareGeneratorSnapshotTests.Snapshot_ContextAwareGenerator_With_User_Field_Name_Collisions|FullyQualifiedName~ContextAwareGeneratorSnapshotTests.Snapshot_ContextAwareGenerator" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`3 Passed``0 Failed`
- 说明:`GFramework.SourceGenerators.Tests` 仍打印既有 `MA0048``MA0051``MA0004` warning本轮未扩大到测试项目 warning 清理
- 下一步建议:
- 若继续收口 PR #269,可再次抓取最新 unresolved threads确认 Greptile / CodeRabbit 当前是否只剩陈旧信号
- 若继续推进 analyzer 主线,可单独评估 `GFramework.SourceGenerators.Tests` 的 warning 清理是否值得开新切片
## 2026-04-23 — RP-026
### 阶段PR #269 failed-test follow-upRP-026
- 启动复核:
- 使用 `$gframework-pr-review` 抓取当前分支 PR #269 的 test report确认最新失败信号来自
`SchemaConfigGeneratorTests.Run_Should_Assign_Globally_Unique_Reference_Metadata_Member_Names`
- 本地复测前先对 `GFramework.SourceGenerators.Tests` 执行 `dotnet restore -p:RestoreFallbackFolders=""`
规避当前 WSL worktree 仍残留的 Windows NuGet fallback package folder 资产干扰
- 决策:
- 保持 `SchemaConfigGenerator` 当前 `GF_ConfigSchema_014` 语义不变PR 失败是测试输入陈旧,而不是生成器行为回退
- 将用例改写为“合法 schema 路径在 reference metadata member name 上碰撞”的场景,继续覆盖全局唯一后缀分配逻辑
- 实施调整:
- 将测试 schema 从根级 `drop-items` / `drop_items` 非法同层冲突改为 `drop.items``drop.items1``dropItems`
`dropItems1` 的合法组合
- 更新断言,验证 `MonsterConfigBindings.g.cs` 中继续生成 `DropItems``DropItems1``DropItems2``DropItems11`
- 验证结果:
- `dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders="" -nologo`
- 结果:通过
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~Run_Should_Assign_Globally_Unique_Reference_Metadata_Member_Names -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`1 Passed``0 Failed`
- 说明:`GFramework.SourceGenerators.Tests` 在构建阶段仍会打印既有 `MA0048``MA0051``MA0004` warning本轮未扩展到该测试项目的 warning 清理
- 下一步建议:
- 若继续收口 PR #269,可再次抓取最新 test report / open thread确认是否还有新的 CI 失败信号
- 若回到 analyzer 主线,优先决定是否为 `GFramework.SourceGenerators.Tests` 单独开一轮 warning 清理切片
## 2026-04-23 — RP-025
### 阶段PR #269 第五轮 review follow-up 与模块 build / warning 治理补充RP-025
- 启动复核:
- 继续使用 `$gframework-pr-review` 读取 PR #269 当前 latest review、outside-diff comment、nitpick comment 与 open-thread 摘要
- 本地核对后确认 `SchemaConfigGenerator` 的取消传播、根 `type` 非字符串防御、`ContextAware` 冲突快照与
`Cqrs` error type 线程均已是陈旧信号;仍成立的是归一化字段名冲突与 `dynamic` 运行时类型引用问题
- 决策:
- `SchemaConfigGenerator` 不复用 `GF_ConfigSchema_006`,改为新增专门的冲突诊断 `GF_ConfigSchema_014`
避免把“标识符非法”和“归一化后重名”混成同一类错误
- `CqrsHandlerRegistryGenerator``dynamic` 采用“生成期归一化为 `global::System.Object`”策略,而不是退回更宽泛的
fallback 路径,保持精确注册能力且避免发射 `typeof(dynamic)`
- `AGENTS.md` 增加模块级 build / warning 治理规则,要求后续改代码时必须对受影响模块跑 Release build并处理或显式报告 warning
- 实施调整:
- 为 `SchemaConfigGenerator` 增加对象级生成属性名登记 helper`ParseObjectSpec(...)` 中拦截 `foo-bar` /
`foo_bar` 这类归一化后冲突,并新增 `ConfigSchemaDiagnostics.DuplicateGeneratedIdentifier`
- 为 `SchemaConfigGeneratorTests` 补上冲突诊断回归测试;为 `CqrsHandlerRegistryGeneratorTests` 收紧
unresolved-type 断言并新增 `dynamic` 类型归一化回归测试
- 为 `CqrsHandlerRegistryGenerator.RuntimeTypeReferences` 增加 `TypeKind.Dynamic` 归一化处理,并保持
`TypeKind.Error` 的保守回退
- 为 `AGENTS.md` 补充“受影响模块必须独立 build 且 warning 不能默认甩给长期分支”的硬性规范
- 验证结果:
- `dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders="" -nologo`
- 结果:通过;并行 restore 时出现一次共享 `obj` 文件已存在的竞争噪音,串行验证后未再复现
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -clp:"Summary;WarningsOnly" -nologo`
- 结果:`0 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -clp:"Summary;WarningsOnly" -nologo`
- 结果:`9 Warning(s)``0 Error(s)`;维持既有 `SchemaConfigGenerator.cs` `MA0051` 基线,未新增 warning
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~Run_Should_Report_Diagnostic_When_Schema_Keys_Collide_After_Identifier_Normalization|FullyQualifiedName~Emits_Object_Type_Reference_When_Handler_Response_Uses_Dynamic|FullyQualifiedName~Emits_Runtime_Type_Lookup_When_Handler_Contract_Contains_Unresolved_Error_Types" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`3 Passed``0 Failed`
- 说明:测试项目构建仍打印既有 `MA0051` / `MA0004` / `MA0048` warning不属于本轮 generator 模块写集,但已在 tracking 风险中记录
- 下一步建议:
- 若继续收口 PR #269,可再次抓取最新 unresolved threads确认 GitHub 上剩余 open thread 是否全部转为陈旧信号
- 若回到 analyzer 主线,继续推进 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs` 剩余 `MA0051`
## 2026-04-22 — RP-024
### 阶段PR #269 第四轮 review follow-up 收口RP-024
- 启动复核:
- 延续 `$gframework-pr-review` 对 PR #269 latest-head unresolved threads 的复核,重点核对最新 5 个未解决线程是否仍与当前
worktree 一致
- 本地确认 `EasyEvents` 异常契约、`SchemaConfigGenerator` 取消传播与 `ContextAwareGenerator` 字段冲突线程已是陈旧信号,
真正仍成立的仅剩 `CqrsHandlerRegistryGenerator` 的 Roslyn error type 直接引用,以及根 schema `type` 非字符串时的
`GetString()` 防御
- 决策:
- `CqrsHandlerRegistryGenerator` 保持现有“优先精确重建、必要时退回运行时查找”的设计,不引入新的程序集级 fallback 契约分支;
只在 `CanReferenceFromGeneratedRegistry(...)` 中显式拒绝 `TypeKind.Error`,让未解析类型走已有运行时查找路径
- `SchemaConfigGenerator` 继续沿用现有 `GF_ConfigSchema_002` 诊断,不新增诊断 ID仅在根对象校验入口补上
`JsonValueKind.String` 前置判断
- 实施调整:
- 为 `CqrsHandlerRegistryGenerator.RuntimeTypeReferences` 增加 `TypeKind.Error` 防御,避免把未解析类型写成生成代码里的
`typeof(...)`
- 为 `SchemaConfigGeneratorTests` 补上根 `type` 为数字时返回 `GF_ConfigSchema_002` 的回归测试
- 为 `CqrsHandlerRegistryGeneratorTests` 补上未解析 error type 会改走运行时 `GetType(...)` 精确查找的回归测试
- 验证结果:
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -v minimal`
- 结果:`0 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -v minimal`
- 结果:通过;仍保留既有 `9``SchemaConfigGenerator.cs` `MA0051`
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~SchemaConfigGeneratorTests.Run_Should_Report_Diagnostic_When_Root_Type_Metadata_Is_Not_A_String|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Runtime_Type_Lookup_When_Handler_Contract_Contains_Unresolved_Error_Types" -m:1 -p:RestoreFallbackFolders="" -v minimal`
- 结果:`2 Passed``0 Failed`
- 说明:测试命令需在无沙箱环境下运行,因为当前 test host 在沙箱内创建本地 socket 会收到 `Permission denied`
- 下一步建议:
- 若继续压缩 PR #269 的 review backlog可再次抓取最新 unresolved threads确认 GitHub 上仅剩陈旧线程后再决定是否继续代码改动
- 若回到 analyzer 主线,继续推进 `SchemaConfigGenerator.cs` 剩余 `MA0051`
## 2026-04-22 — RP-023
### 阶段PR #269 第三轮 review follow-up 收口RP-023
- 启动复核:
- 延续 `$gframework-pr-review` 对 PR #269 的 latest-head unresolved threads、outside-diff comment 与 nitpick comment
- 本地核实后确认剩余仍成立的项集中在 `SchemaConfigGenerator` 根类型名校验、aggregate registration comparer XML 文档转义、
`LoggingConfigurationTests` / `CollectionExtensionsTests` 断言补强,以及 `ai-plan` 命令文本可复制性
- 决策:
- `SchemaConfigGenerator` 沿用现有 `InvalidGeneratedIdentifier` 诊断,不新增诊断 ID将根类型名校验收敛到独立 helper
让顶层 schema 文件名与属性名共享同一类安全边界
- aggregate registration comparer 文档直接复用现有 `EscapeXmlDocumentation(...)`,避免在 `///` 注释里再次写入原始泛型尖括号
- `CqrsHandlerRegistryGenerator` 的重复反射查找分支采用小 helper 抽取,不改变 fallback 语义和快照输出
- 实施调整:
- 为 `SchemaConfigGenerator` 新增 `TryBuildRootTypeIdentifiers(...)`,在进入 `ParseObjectSpec(...)` 前拦截非法根类型名
- 调整 aggregate registration comparer 属性的 XML 文档,使用 `<c>...</c>` 包裹并转义泛型类型文本
- 为 `SchemaConfigGeneratorTests` 增加非法 schema 文件名诊断回归,并补强 generated catalog 中 comparer 文档断言
- 为 `LoggingConfigurationTests` 增加正向键存在和值断言,为 `CollectionExtensionsTests` 补齐返回类型泛型参数绑定断言
- 为 `CqrsHandlerRegistryGenerator.RuntimeTypeReferences` 抽取共享反射查找 helper并修正 active tracking 中的转义引号
- 验证结果:
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -v minimal`
- 结果:通过;仍保留既有 `9``SchemaConfigGenerator.cs` `MA0051`
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -v minimal`
- 结果:通过;并行构建时出现一次 `MSB3026` 文件占用重试,自动恢复后完成
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~SchemaConfigGeneratorTests|FullyQualifiedName~SchemaConfigGeneratorSnapshotTests|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
- 结果:`63 Passed``0 Failed`
- 说明:测试项目构建仍打印既有 source-generator-tests analyzer warning不属于本轮写集
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~LoggingConfigurationTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
- 结果:`27 Passed``0 Failed`
- 下一步建议:
- 若本轮验证通过,继续回到 `SchemaConfigGenerator.cs` 剩余 `MA0051`
- 若 PR #269 仍有未关闭 review thread再按“先本地复核、再最小修复”的节奏收口
## 2026-04-22 — RP-022
### 阶段PR #269 第二轮 review follow-up 收口RP-022
- 启动复核:
- 延续 `$gframework-pr-review` 的 PR #269 结果,继续核对 latest-head unresolved threads 与 nitpick comment
- 结合本地实现确认仍成立的项不止第一轮记录的 4 个,还包括公共 API 兼容回退、`SchemaConfigGenerator` 取消传播、
`ContextAwareGenerator` 真正的字段名去冲突与锁内读取修正、`Cqrs` 运行时类型 null 防御
- 决策:
- 对公共 API 兼容项优先保持既有契约,不为了压 analyzer 而继续收窄返回类型、属性类型或异常类型
- `ContextAwareGenerator` 采用保守并发修复:移除未加锁 fast-path统一在锁内读取上下文缓存并让生成字段名按已有成员去冲突
- `SchemaConfigGenerator` 在取消已请求时直接重新抛出 `OperationCanceledException`,避免把取消误报告成普通诊断
- 实施调整:
- 将 `EasyEvents.AddEvent<T>()` 的重复注册异常恢复为 `ArgumentException`,并在测试中恢复既有异常契约断言
- 将 `CollectionExtensions.ToDictionarySafe(...)` 返回类型恢复为 `Dictionary<TKey, TValue>`,并新增反射测试锁定公开 API 形状
- 将 `LoggingConfiguration` / `FilterConfiguration` 的公开集合属性恢复为具体 `List<>` / `Dictionary<,>` 类型,
并新增反射测试与默认 comparer 语义断言
- 为 `CqrsHandlerRegistryGenerator` 的命名类型引用构造补上 `ContainingAssembly is null` 防御,移除发射 helper 冗余布尔参数
- 为 `SchemaConfigGenerator` 补上“仅在 cancellationToken 已取消时重抛”的 catch 分支,并为测试驱动添加多 `AdditionalText` 重载
- 为 `ContextAwareGenerator` 增加生成成员名分配逻辑,新增 `_gFrameworkContextAware*` 与旧 `_context*` 双冲突快照场景,
同时移除 getter 中未加锁 fast-path
- 验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:通过
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -v minimal`
- 结果:通过
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -v minimal`
- 结果:通过;仍有既有 `9``MA0051`
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~SchemaConfigGeneratorTests|FullyQualifiedName~ContextAwareGeneratorSnapshotTests|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
- 结果:`63 Passed``0 Failed`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~EasyEventsTests|FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~LoggingConfigurationTests|FullyQualifiedName~ConfigurableLoggerFactoryTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
- 结果:`38 Passed``0 Failed`
- 下一步建议:
- 回到 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs` 的剩余 `MA0051`
- 若后续 review 再提 analyzer 兼容建议,先做公共契约回归检查,再决定是否接受该建议
## 2026-04-22 — RP-021
### 阶段PR #269 review follow-up 收口RP-021
- 启动复核:
- 使用 `$gframework-pr-review` 读取当前分支 PR #269 的 CodeRabbit outside-diff 与 nitpick 汇总
- 本地复核后确认仍成立的 4 个项分别是:`CqrsHandlerRegistryGenerator.cs` 超过仓库文件大小上限、
`ContextAwareGenerator` 生成字段名可能与用户 partial 类型冲突、`SetContextProvider` 缺少运行时 null 防御、
`Option<T>` 缺少 `<remarks>` 契约说明
- 决策:
- `CqrsHandlerRegistryGenerator` 继续采用既有 partial helper 风格,按“主流程 / 运行时类型引用 / 源码发射 / 模型”四个文件拆分,
保持生成顺序、日志文本、fallback 契约和快照输出不变
- `ContextAwareGenerator` 只收口仍成立的 review 项,不引入未被本地证实的 `Volatile.Read/Write` 变更
- 为字段命名冲突新增生成器快照场景,避免后续回退到 `_context` / `_contextProvider` / `_contextSync`
- 实施调整:
- 将 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs` 拆为 4 个 partial 文件,分别承载主生成管线、
runtime type reference 构造、source emission helper 与嵌套 specs/models
- 将 `ContextAwareGenerator` 生成字段统一改为 `_gFrameworkContextAware*` 前缀,同步更新 XML 文档、注释和显式接口实现
- 为 `SetContextProvider(...)` 增加 `ArgumentNullException.ThrowIfNull(provider)` 与 XML `<exception>` 说明
- 为 `Option<T>` 补充 `<remarks>`,明确 `Some/None``null` 约束、不可变语义与推荐使用方式
- 新增 `CollisionProneRule.ContextAware.g.cs` 快照,覆盖用户字段名与生成字段名冲突场景
- 验证结果:
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
- 结果:`0 Warning(s)``0 Error(s)`;拆分后 `CqrsHandlerRegistryGenerator` 最大单文件为 `851`
- `dotnet build GFramework.Core.SourceGenerators/GFramework.Core.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
- 结果:`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~CqrsHandlerRegistryGeneratorTests -m:1 -p:RestoreFallbackFolders= -nologo`
- 结果:`14 Passed``0 Failed`
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~ContextAwareGeneratorSnapshotTests -m:1 -p:RestoreFallbackFolders= -nologo`
- 结果:`2 Passed``0 Failed`
- 说明:最初并行跑两个 `dotnet test` 命令时触发共享输出文件锁冲突;串行重跑后确认是测试宿主环境噪音而非代码回归
- 下一步建议:
- 若本轮验证通过,可继续回到 `SchemaConfigGenerator` 剩余 `MA0051`
- 若 review 再次聚焦 `ContextAwareGenerator` 并发可见性问题,需要先补最小复现测试,再决定是否引入 `Volatile` 语义
## 2026-04-22 — RP-020
### 阶段:`SchemaConfigGenerator` 第一批 `MA0051` 结构拆分RP-020
- 启动复核:
- 当前 worktree 仍映射到 `analyzer-warning-reduction` active topic
- `GFramework.Game.SourceGenerators` warnings-only build 复现 `19` 条 warning全部为
`GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs``MA0051`
- 决策:
- 本轮继续低风险结构拆分,不改变 schema 支持范围、诊断 ID、生成类型形状或输出顺序
- 未使用 subagentcritical path 是本地复现 warning、拆分语义阶段并用 focused schema generator tests 验证行为
- 实施调整:
- 将 schema 入口解析拆为文本读取、root 验证、id key 验证和 `SchemaFileSpec` 构造阶段
- 将属性解析拆为共享上下文提取、类型分派、标量/对象/数组属性构造 helper
- 将统一 schema 遍历拆为对象属性、dependentSchemas、allOf、条件分支、not、array items / contains 等遍历阶段
- 将约束文档生成拆为 const、numeric、string、array、object 约束片段
- 将 catalog/registration/YAML/lookup/object type 等生成代码发射路径中的小型高收益 helper 拆出
- 验证结果:
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
- 结果:`9 Warning(s)``0 Error(s)`;当前项目剩余 warning 均为 `SchemaConfigGenerator.cs``MA0051`
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~SchemaConfigGenerator -m:1 -p:RestoreFallbackFolders= -nologo`
- 结果:`50 Passed``0 Failed`
- 说明:测试项目构建仍显示既有 source generator test analyzer warning不属于本轮写集
- 下一步建议:
- 继续该主题时,优先拆分 `GenerateBindingsClass``AppendGeneratedConfigCatalogType` 或对象/条件 schema target 验证方法
- 若转回 `MA0158`,仍需先设计多 target 条件编译方案,再考虑替换共享源码中的 `object` lock
## 2026-04-22 — RP-019
### 阶段:`SchemaConfigGenerator` 当前 `MA0006` 收口RP-019
- 启动复核:
- 当前 worktree 仍映射到 `analyzer-warning-reduction` active topic
- Windows Git interop 在当前 shell 中返回 WSL socket 错误;本轮使用显式 `--git-dir` / `--work-tree` 读取状态
- `GFramework.Game.SourceGenerators` 首次 build 受 stale Windows fallback package folder 影响,刷新 restore 资产后复现
`46` 条 warning其中 `MA0006=27`,其余为 `SchemaConfigGenerator.cs``MA0051`
- 决策:
- 本轮先收口低风险 `MA0006`,不在同一 slice 中拆分 `SchemaConfigGenerator.cs` 的长方法
- 未使用 subagentcritical path 是本地复现 warning、替换 schema 字符串比较并用 focused schema generator tests 验证输出行为
- 实施调整:
- 为 schema 类型关键字新增 `IsSchemaType` / `IsNumericSchemaType` helper统一使用 `StringComparison.Ordinal`
- 将 id key 类型验证、约束文档生成、required property 文档和路径拼接中的直接字符串比较改为显式 ordinal 比较
- 修正 `JsonElement.GetString()` 后的 nullable flow避免新增 `CS8604`
- 验证结果:
- `dotnet restore GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -p:RestoreFallbackFolders= -nologo`
- 结果:通过
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
- 结果:`19 Warning(s)``0 Error(s)`;当前项目输出已无 `MA0006`,剩余均为 `MA0051`
- `dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders= -nologo`
- 结果:通过
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~SchemaConfigGenerator -m:1 -p:RestoreFallbackFolders= -nologo`
- 结果:`50 Passed``0 Failed`
- 说明:测试项目构建仍显示既有 analyzer warning不属于本轮写集
- 下一步建议:
- 继续该主题时,优先拆分 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs``MA0051`
- 若回到 `MA0158`,先设计多 target 条件编译方案,再考虑替换共享源码中的 `object` lock
## 2026-04-22 — RP-018
### 阶段:`CqrsHandlerRegistryGenerator` 剩余 `MA0051` 收口RP-018
- 启动复核:
- 当前 worktree 仍映射到 `analyzer-warning-reduction` active topic
- `MA0158` 锁迁移仍然跨 `GFramework.Core` / `GFramework.Cqrs` 多 target 共享源码,继续视为需要单独设计的兼容性问题
- `GFramework.Cqrs.SourceGenerators` warnings-only build 复现 `CqrsHandlerRegistryGenerator.cs``6``MA0051`
- 决策:
- 本轮暂缓 `MA0158`,转入单文件、可由生成器测试覆盖的 `GFramework.Cqrs.SourceGenerators` 结构拆分
- 未使用 subagentcritical path 是本地复现 warning、拆分源码发射流程并用 focused generator tests 验证输出未变
- 实施调整:
- 将 handler candidate 分析拆为接口收集、候选构造和单接口注册分类阶段
- 将运行时类型引用构造拆为已构造泛型、命名类型反射查找等独立 helper
- 将注册器源码生成拆为文件头、程序集特性、注册器类型、`Register` 方法和服务注册日志发射 helper
- 将有序注册与精确反射注册输出拆为独立阶段,保留原有排序和生成文本形状
- 验证结果:
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
- 结果:`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~CqrsHandlerRegistryGeneratorTests -m:1 -p:RestoreFallbackFolders= -nologo`
- 结果:`14 Passed``0 Failed`
- 说明:测试项目构建仍显示 `GFramework.Game.SourceGenerators` 与测试项目中的既有 analyzer warning不属于本轮写集
- 下一步建议:
- 继续该主题时,优先处理 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs``MA0006` 低风险批次
- 若回到 `MA0158`,先设计多 target 条件编译方案,再考虑替换共享源码中的 `object` lock
## 2026-04-22 — RP-017
### 阶段:`ContextAwareGenerator` 剩余 `MA0051` 收口RP-017
- 启动复核:
- 当前 worktree 仍映射到 `analyzer-warning-reduction` active topic
- `GFramework.Core` `net10.0` warnings-only build 在刷新 restore fallback 资产后复现 `16``MA0158`
- `GFramework.Core.SourceGenerators` warnings-only build 复现 `ContextAwareGenerator.GenerateContextProperty` 的单个
`MA0051`
- 决策:
- `MA0158` 涉及 `GFramework.Core``GFramework.Cqrs` 的 object lock 字段,且项目仍多 target 到 `net8.0` / `net9.0`
/ `net10.0`,因此本轮不直接批量替换为 `System.Threading.Lock`
- 先处理单文件、单 warning、生成输出可由 snapshot 验证的 `ContextAwareGenerator` 结构拆分
- 未使用 subagent本轮 critical path 是本地复现 warning、拆分方法并验证生成输出拆分后写集只包含单个 generator 文件和
active `ai-plan` 文档
- 实施调整:
- 将 `GenerateContextProperty` 拆为 `GenerateContextBackingFields``GenerateContextGetter`
`GenerateContextProviderConfiguration`
- 保留原有 `StringBuilder` 追加顺序与生成代码文本,避免 snapshot 变更
- 为新增 helper 补充 XML 注释说明字段、getter 与 provider 配置 API 的生成职责
- 验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net10.0 -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
- 结果:`16 Warning(s)``0 Error(s)`;记录当前 `MA0158` 基线,不作为本轮修改范围
- `dotnet build GFramework.Core.SourceGenerators/GFramework.Core.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
- 结果:`0 Warning(s)``0 Error(s)``ContextAwareGenerator.cs``MA0051` 已清零
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~ContextAwareGeneratorSnapshotTests" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`1 Passed``0 Failed`
- 说明:该 test project 构建仍显示相邻 generator/test 项目的既有 analyzer warning本轮关注的
`GFramework.Core.SourceGenerators` 独立 build 已清零
- 下一步建议:
- 继续该主题时,优先设计 `MA0158` 的多 target 兼容迁移方案;如果风险过高,再单独切入
`GFramework.Cqrs.SourceGenerators``GFramework.Game.SourceGenerators` 的结构性 warning
## 2026-04-22 — RP-016
### 阶段:`GFramework.Core` 剩余低风险 warning 批次清零RP-016
- 依据 `RP-015` 的下一步建议,本轮恢复到 `MA0016` / `MA0002` 低风险批次,并顺手吸收仍集中在
`GFramework.Core``MA0015``MA0077`
- 基线复核:
- 首次使用 Linux `dotnet` 时仍被当前 worktree 的 Windows fallback package folder restore 资产阻断
- 切换到 host Windows `dotnet` 后,`GFramework.Core` `net8.0` warnings-only build 复现 `9` 条 warning
`MA0016=5``MA0002=2``MA0015=1``MA0077=1`
- 实施调整:
- 将 `LoggingConfiguration.Appenders` / `LoggerLevels``FilterConfiguration.Namespaces` / `Filters`
的公开类型改为集合抽象接口,同时保留 `List<T>` / `Dictionary<TKey,TValue>` 默认实例,兼顾 analyzer 与现有配置消费路径
- 将 `CollectionExtensions.ToDictionarySafe(...)` 返回类型改为 `IDictionary<TKey,TValue>`,内部仍使用 `Dictionary<TKey,TValue>`
保留“重复键以后值覆盖前值”的实现语义
- 为 `CoroutineScheduler``_tagged``_grouped` 字典显式指定 `StringComparer.Ordinal`,将原有默认区分大小写语义写入代码
- 将 `EasyEvents.AddEvent<T>()` 重复注册失败从 `ArgumentException` 改为 `InvalidOperationException`;该路径表示状态冲突,
不是某个方法参数无效,因此不能为 `MA0015` 人造参数名
- 为 `Option<T>` 声明 `IEquatable<Option<T>>`,与已有强类型 `Equals(Option<T>)` 实现对齐
- 验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -nologo -clp:"Summary;WarningsOnly"`
- 结果:`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~LoggingConfigurationTests|FullyQualifiedName~ConfigurableLoggerFactoryTests|FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~EasyEventsTests|FullyQualifiedName~OptionTests|FullyQualifiedName~CoroutineGroupTests|FullyQualifiedName~CoroutineSchedulerTests" -m:1 -nologo`
- 结果:`112 Passed``0 Failed`
- 说明:测试构建仍显示既有 `net10.0` `MA0158` 与 source generator `MA0051` warning这些不属于本轮
`GFramework.Core` `net8.0` 剩余 warning 批次
- 当前结论:
- `GFramework.Core` `net8.0` 当前 analyzer warning baseline 已清零
- analyzer topic 仍可继续,但下一轮应转入 `net10.0` 专属 `MA0158` 兼容性评估,或单独处理 source generator 剩余
`MA0051`
- 下一步建议:
- 优先评估 `MA0158` 在多 target 源码中的安全推进方式;若风险过高,再处理
`GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs` 的结构拆分
## 2026-04-21 — RP-015
### 阶段PR #267 failed-test follow-up 收口RP-015