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

Fix/analyzer warning reduction batch
This commit is contained in:
gewuyou 2026-04-21 09:31:32 +08:00 committed by GitHub
commit b553d7cbc6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 671 additions and 280 deletions

View File

@ -394,6 +394,35 @@ public class PauseStackManagerTests
Assert.That(_manager.IsPaused(PauseGroup.Audio), Is.False);
}
/// <summary>
/// 验证销毁时会向所有仍暂停的组补发恢复通知。
/// </summary>
[Test]
public async Task DestroyAsync_Should_NotifyResumedGroups()
{
var resumedGroups = new List<PauseGroup>();
var mockHandler = new MockPauseHandler();
_manager.RegisterHandler(mockHandler);
_manager.OnPauseStateChanged += (_, e) =>
{
if (!e.IsPaused)
{
resumedGroups.Add(e.Group);
}
};
_manager.Push("Global", PauseGroup.Global);
_manager.Push("Gameplay", PauseGroup.Gameplay);
mockHandler.Reset();
await _manager.DestroyAsync();
Assert.That(mockHandler.CallCount, Is.EqualTo(2));
Assert.That(mockHandler.LastIsPaused, Is.False);
Assert.That(resumedGroups, Is.EquivalentTo(new[] { PauseGroup.Global, PauseGroup.Gameplay }));
}
/// <summary>
/// 验证并发Push是线程安全的
/// </summary>
@ -469,4 +498,4 @@ public class PauseStackManagerTests
LastIsPaused = null;
}
}
}
}

View File

@ -145,69 +145,34 @@ internal sealed class ArchitectureLifecycle(
{
logger.Info($"Initializing {_pendingInitializableList.Count} components");
// 按类型分组初始化(保持原有的阶段划分)
var utilities = _pendingInitializableList.OfType<IContextUtility>().ToList();
var models = _pendingInitializableList.OfType<IModel>().ToList();
var systems = _pendingInitializableList.OfType<ISystem>().ToList();
var initializationPlan = CreateInitializationPlan();
// 1. 工具初始化阶段
EnterPhase(ArchitecturePhase.BeforeUtilityInit);
await InitializePhaseComponentsAsync(
initializationPlan.Utilities,
ArchitecturePhase.BeforeUtilityInit,
ArchitecturePhase.AfterUtilityInit,
"context utilities",
"utility",
asyncMode)
.ConfigureAwait(false);
await InitializePhaseComponentsAsync(
initializationPlan.Models,
ArchitecturePhase.BeforeModelInit,
ArchitecturePhase.AfterModelInit,
"models",
"model",
asyncMode)
.ConfigureAwait(false);
await InitializePhaseComponentsAsync(
initializationPlan.Systems,
ArchitecturePhase.BeforeSystemInit,
ArchitecturePhase.AfterSystemInit,
"systems",
"system",
asyncMode)
.ConfigureAwait(false);
if (utilities.Count != 0)
{
logger.Info($"Initializing {utilities.Count} context utilities");
foreach (var utility in utilities)
{
logger.Debug($"Initializing utility: {utility.GetType().Name}");
await InitializeComponentAsync(utility, asyncMode).ConfigureAwait(false);
}
logger.Info("All context utilities initialized");
}
EnterPhase(ArchitecturePhase.AfterUtilityInit);
// 2. 模型初始化阶段
EnterPhase(ArchitecturePhase.BeforeModelInit);
if (models.Count != 0)
{
logger.Info($"Initializing {models.Count} models");
foreach (var model in models)
{
logger.Debug($"Initializing model: {model.GetType().Name}");
await InitializeComponentAsync(model, asyncMode).ConfigureAwait(false);
}
logger.Info("All models initialized");
}
EnterPhase(ArchitecturePhase.AfterModelInit);
// 3. 系统初始化阶段
EnterPhase(ArchitecturePhase.BeforeSystemInit);
if (systems.Count != 0)
{
logger.Info($"Initializing {systems.Count} systems");
foreach (var system in systems)
{
logger.Debug($"Initializing system: {system.GetType().Name}");
await InitializeComponentAsync(system, asyncMode).ConfigureAwait(false);
}
logger.Info("All systems initialized");
}
EnterPhase(ArchitecturePhase.AfterSystemInit);
_pendingInitializableList.Clear();
_pendingInitializableSet.Clear();
_initialized = true;
logger.Info("All components initialized");
MarkInitializationCompleted();
}
/// <summary>
@ -223,6 +188,67 @@ internal sealed class ArchitectureLifecycle(
component.Initialize();
}
/// <summary>
/// 按架构既有阶段语义把待初始化组件拆分为 utility、model 和 system 三个批次。
/// 这样可以在压缩主流程复杂度的同时,继续复用注册顺序和接口类型决定的初始化分层。
/// </summary>
/// <returns>当前待初始化组件的阶段化批次。</returns>
private InitializationPlan CreateInitializationPlan()
{
return new InitializationPlan(
_pendingInitializableList.OfType<IContextUtility>().ToList(),
_pendingInitializableList.OfType<IModel>().ToList(),
_pendingInitializableList.OfType<ISystem>().ToList());
}
/// <summary>
/// 执行单个生命周期阶段的批量初始化,并统一维护阶段切换、日志输出和异步初始化策略。
/// </summary>
/// <typeparam name="TComponent">当前阶段要初始化的组件类型。</typeparam>
/// <param name="components">当前阶段的组件列表。</param>
/// <param name="beforePhase">阶段开始前要进入的生命周期状态。</param>
/// <param name="afterPhase">阶段结束后要进入的生命周期状态。</param>
/// <param name="componentGroupName">用于批量日志的组件组名称。</param>
/// <param name="componentLogName">用于单个组件日志的组件角色名称。</param>
/// <param name="asyncMode">是否允许优先走异步初始化契约。</param>
private async Task InitializePhaseComponentsAsync<TComponent>(
IReadOnlyList<TComponent> components,
ArchitecturePhase beforePhase,
ArchitecturePhase afterPhase,
string componentGroupName,
string componentLogName,
bool asyncMode)
where TComponent : class, IInitializable
{
EnterPhase(beforePhase);
if (components.Count != 0)
{
logger.Info($"Initializing {components.Count} {componentGroupName}");
foreach (var component in components)
{
logger.Debug($"Initializing {componentLogName}: {component.GetType().Name}");
await InitializeComponentAsync(component, asyncMode).ConfigureAwait(false);
}
logger.Info($"All {componentGroupName} initialized");
}
EnterPhase(afterPhase);
}
/// <summary>
/// 在所有阶段初始化完成后清理挂起列表,并把生命周期状态切换到“已初始化”。
/// </summary>
private void MarkInitializationCompleted()
{
_pendingInitializableList.Clear();
_pendingInitializableSet.Clear();
_initialized = true;
logger.Info("All components initialized");
}
/// <summary>
/// 立即初始化在常规初始化批次完成后新增的组件。
/// 当启用 <c>AllowLateRegistration</c> 时,生命周期层需要和注册层保持一致,
@ -258,6 +284,17 @@ internal sealed class ArchitectureLifecycle(
#endregion
/// <summary>
/// 保存一次完整初始化流程所需的三个阶段批次。
/// </summary>
/// <param name="Utilities">Utility 初始化批次。</param>
/// <param name="Models">Model 初始化批次。</param>
/// <param name="Systems">System 初始化批次。</param>
private readonly record struct InitializationPlan(
IReadOnlyList<IContextUtility> Utilities,
IReadOnlyList<IModel> Models,
IReadOnlyList<ISystem> Systems);
#region Ready State
/// <summary>

View File

@ -26,69 +26,17 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
public ValueTask DestroyAsync()
{
if (_disposed)
{
return ValueTask.CompletedTask;
List<PauseGroup> pausedGroups;
IPauseHandler[] handlersSnapshot;
_lock.EnterWriteLock();
try
{
if (_disposed)
return ValueTask.CompletedTask;
_disposed = true;
// 收集所有当前暂停的组
pausedGroups = _pauseStacks
.Where(kvp => kvp.Value.Count > 0)
.Select(kvp => kvp.Key)
.ToList();
// 获取处理器快照
handlersSnapshot = _handlers.ToArray();
// 清理所有数据结构
_pauseStacks.Clear();
_tokenMap.Clear();
_handlers.Clear();
_logger.Debug("PauseStackManager destroyed");
}
finally
{
_lock.ExitWriteLock();
}
// 在锁外通知所有之前暂停的组恢复,保持生命周期信号一致
foreach (var group in pausedGroups)
var destroySnapshot = TryBeginDestroy();
if (destroySnapshot == null)
{
_logger.Debug($"Notifying handlers of destruction: Group={group}, IsPaused=false");
foreach (var handler in handlersSnapshot.OrderBy(h => h.Priority))
{
try
{
handler.OnPauseStateChanged(group, false);
}
catch (Exception ex)
{
_logger.Error($"Handler {handler.GetType().Name} failed during destruction", ex);
}
}
// 触发事件
try
{
RaisePauseStateChanged(group, false);
}
catch (Exception ex)
{
_logger.Error($"Event subscriber failed during destruction for group {group}", ex);
}
return ValueTask.CompletedTask;
}
// 释放锁资源
NotifyDestroyedGroups(destroySnapshot.Value);
_lock.Dispose();
return ValueTask.CompletedTask;
@ -163,74 +111,17 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
public bool Pop(PauseToken token)
{
if (!token.IsValid)
{
return false;
bool found;
bool shouldNotify = false;
PauseGroup notifyGroup = PauseGroup.Global;
_lock.EnterWriteLock();
try
{
ThrowIfDisposed();
if (!_tokenMap.TryGetValue(token.Id, out var entry))
{
_logger.Warn($"Attempted to pop invalid/expired token: {token.Id}");
return false;
}
var group = entry.Group;
var stack = _pauseStacks[group];
var wasPaused = stack.Count > 0;
// 从栈中移除
var tempStack = new Stack<PauseEntry>();
found = false;
while (stack.Count > 0)
{
var current = stack.Pop();
if (current.TokenId == token.Id)
{
found = true;
break;
}
tempStack.Push(current);
}
// 恢复栈结构
while (tempStack.Count > 0)
{
stack.Push(tempStack.Pop());
}
if (found)
{
_tokenMap.Remove(token.Id);
_logger.Debug($"Pause popped: {entry.Reason} (Group: {group}, Remaining: {stack.Count})");
// 状态变化检测:从暂停 → 未暂停
if (wasPaused && stack.Count == 0)
{
shouldNotify = true;
notifyGroup = group;
}
}
}
finally
{
_lock.ExitWriteLock();
}
// 在锁外通知处理器,避免死锁
if (shouldNotify)
var result = TryPopEntry(token);
if (result.ShouldNotify)
{
NotifyHandlers(notifyGroup, false);
NotifyHandlers(result.NotifyGroup, false);
}
return found;
return result.Found;
}
/// <summary>
@ -443,6 +334,200 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
}
}
/// <summary>
/// 采集销毁所需的快照并清空内部状态。
/// </summary>
/// <returns>
/// 成功进入销毁阶段时返回销毁快照;如果其他线程已先完成销毁,则返回 <see langword="null" />。
/// </returns>
/// <remarks>
/// 该方法只负责锁内状态迁移,把外部回调与事件派发留到锁外执行,
/// 以避免在生命周期结束阶段持锁调用用户代码。
/// </remarks>
private DestroySnapshot? TryBeginDestroy()
{
_lock.EnterWriteLock();
try
{
if (_disposed)
{
return null;
}
_disposed = true;
var pausedGroups = CollectPausedGroups();
var handlersSnapshot = CreateHandlerSnapshot();
_pauseStacks.Clear();
_tokenMap.Clear();
_handlers.Clear();
_logger.Debug("PauseStackManager destroyed");
return new DestroySnapshot(pausedGroups, handlersSnapshot);
}
finally
{
_lock.ExitWriteLock();
}
}
/// <summary>
/// 在销毁后向所有先前处于暂停状态的分组补发恢复通知。
/// </summary>
/// <param name="destroySnapshot">销毁阶段采集的分组与处理器快照。</param>
private void NotifyDestroyedGroups(DestroySnapshot destroySnapshot)
{
foreach (var group in destroySnapshot.PausedGroups)
{
_logger.Debug($"Notifying handlers of destruction: Group={group}, IsPaused=false");
NotifyHandlersSnapshot(group, false, destroySnapshot.HandlersSnapshot, isDestroying: true);
RaiseDestroyStateChanged(group);
}
}
/// <summary>
/// 在锁内执行令牌移除,并返回锁外通知所需的信息。
/// </summary>
/// <param name="token">要移除的暂停令牌。</param>
/// <returns>包含本次弹出结果和后续通知决策的快照。</returns>
/// <remarks>
/// Pop 支持移除非栈顶令牌,因此这里会先临时转移栈元素,再恢复原有顺序,
/// 只在最后一个暂停请求被移除时触发恢复通知。
/// </remarks>
private PopResult TryPopEntry(PauseToken token)
{
_lock.EnterWriteLock();
try
{
ThrowIfDisposed();
if (!_tokenMap.TryGetValue(token.Id, out var entry))
{
_logger.Warn($"Attempted to pop invalid/expired token: {token.Id}");
return PopResult.NotFound;
}
var stack = _pauseStacks[entry.Group];
var wasPaused = stack.Count > 0;
var found = RemoveEntryFromStack(stack, token.Id);
if (!found)
{
return PopResult.NotFound;
}
_tokenMap.Remove(token.Id);
_logger.Debug($"Pause popped: {entry.Reason} (Group: {entry.Group}, Remaining: {stack.Count})");
return new PopResult(true, wasPaused && stack.Count == 0, entry.Group);
}
finally
{
_lock.ExitWriteLock();
}
}
/// <summary>
/// 从指定暂停栈中移除目标令牌,并保持其他暂停请求的原始顺序。
/// </summary>
/// <param name="stack">要修改的暂停栈。</param>
/// <param name="tokenId">目标令牌标识。</param>
/// <returns>如果找到了目标令牌则返回 <see langword="true" />。</returns>
private static bool RemoveEntryFromStack(Stack<PauseEntry> stack, Guid tokenId)
{
var tempStack = new Stack<PauseEntry>();
var found = false;
while (stack.Count > 0)
{
var current = stack.Pop();
if (current.TokenId == tokenId)
{
found = true;
break;
}
tempStack.Push(current);
}
while (tempStack.Count > 0)
{
stack.Push(tempStack.Pop());
}
return found;
}
/// <summary>
/// 收集当前仍处于暂停状态的分组列表。
/// </summary>
/// <returns>包含所有暂停中的分组的数组。</returns>
private PauseGroup[] CollectPausedGroups()
{
return _pauseStacks
.Where(kvp => kvp.Value.Count > 0)
.Select(kvp => kvp.Key)
.ToArray();
}
/// <summary>
/// 按优先级创建处理器快照,确保锁外通知仍保持确定性顺序。
/// </summary>
/// <returns>已按优先级排序的处理器快照。</returns>
private IPauseHandler[] CreateHandlerSnapshot()
{
return _handlers
.OrderBy(handler => handler.Priority)
.ToArray();
}
/// <summary>
/// 统一使用给定的处理器快照派发暂停状态变化通知。
/// </summary>
/// <param name="group">发生状态变化的暂停组。</param>
/// <param name="isPaused">新的暂停状态。</param>
/// <param name="handlersSnapshot">要通知的处理器快照。</param>
/// <param name="isDestroying">是否处于销毁补发路径。</param>
private void NotifyHandlersSnapshot(
PauseGroup group,
bool isPaused,
IReadOnlyList<IPauseHandler> handlersSnapshot,
bool isDestroying)
{
foreach (var handler in handlersSnapshot)
{
try
{
handler.OnPauseStateChanged(group, isPaused);
}
catch (Exception ex)
{
var message = isDestroying
? $"Handler {handler.GetType().Name} failed during destruction"
: $"Handler {handler.GetType().Name} failed";
_logger.Error(message, ex);
}
}
}
/// <summary>
/// 在销毁路径中独立保护事件通知,避免订阅方异常中断其他分组的恢复信号。
/// </summary>
/// <param name="group">需要补发恢复事件的暂停组。</param>
private void RaiseDestroyStateChanged(PauseGroup group)
{
try
{
RaisePauseStateChanged(group, false);
}
catch (Exception ex)
{
_logger.Error($"Event subscriber failed during destruction for group {group}", ex);
}
}
/// <summary>
/// 内部查询暂停状态的方法,不加锁。
/// </summary>
@ -467,7 +552,7 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
_lock.EnterReadLock();
try
{
handlersSnapshot = _handlers.OrderBy(h => h.Priority).ToArray();
handlersSnapshot = CreateHandlerSnapshot();
}
finally
{
@ -475,17 +560,7 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
}
// 在锁外遍历快照并通知处理器
foreach (var handler in handlersSnapshot)
{
try
{
handler.OnPauseStateChanged(group, isPaused);
}
catch (Exception ex)
{
_logger.Error($"Handler {handler.GetType().Name} failed", ex);
}
}
NotifyHandlersSnapshot(group, isPaused, handlersSnapshot, isDestroying: false);
// 触发事件
RaisePauseStateChanged(group, isPaused);
@ -508,4 +583,25 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
protected override void OnInit()
{
}
}
/// <summary>
/// 锁内采集的销毁快照,供锁外补发恢复通知使用。
/// </summary>
/// <param name="PausedGroups">销毁前仍处于暂停状态的分组。</param>
/// <param name="HandlersSnapshot">按优先级排序后的处理器快照。</param>
private readonly record struct DestroySnapshot(PauseGroup[] PausedGroups, IPauseHandler[] HandlersSnapshot);
/// <summary>
/// Pop 操作的锁内结果快照。
/// </summary>
/// <param name="Found">是否成功移除了目标令牌。</param>
/// <param name="ShouldNotify">是否需要在锁外发出恢复通知。</param>
/// <param name="NotifyGroup">需要通知的暂停组。</param>
private readonly record struct PopResult(bool Found, bool ShouldNotify, PauseGroup NotifyGroup)
{
/// <summary>
/// 表示未找到目标令牌时的默认结果。
/// </summary>
public static PopResult NotFound { get; } = new(false, false, PauseGroup.Global);
}
}

View File

@ -1,6 +1,7 @@
using GFramework.Core.Abstractions.Ioc;
using GFramework.Core.Abstractions.Logging;
using GFramework.Cqrs.Abstractions.Cqrs;
using System.Diagnostics.CodeAnalysis;
using System.Reflection.Emit;
namespace GFramework.Cqrs.Internal;
@ -88,63 +89,14 @@ internal static class CqrsHandlerRegistrar
if (registryTypes.Count == 0)
return GeneratedRegistrationResult.NoGeneratedRegistry();
var registries = new List<ICqrsHandlerRegistry>(registryTypes.Count);
foreach (var registryType in registryTypes)
{
var activationMetadata = RegistryActivationMetadataCache.GetOrAdd(
registryType,
AnalyzeRegistryActivation);
if (!TryCreateGeneratedRegistries(registryTypes, assemblyName, logger, out var registries))
return GeneratedRegistrationResult.NoGeneratedRegistry();
if (!activationMetadata.ImplementsRegistryContract)
{
logger.Warn(
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it does not implement {typeof(ICqrsHandlerRegistry).FullName}.");
return GeneratedRegistrationResult.NoGeneratedRegistry();
}
if (activationMetadata.IsAbstract)
{
logger.Warn(
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it is abstract.");
return GeneratedRegistrationResult.NoGeneratedRegistry();
}
if (activationMetadata.Factory is null)
{
logger.Warn(
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it does not expose an accessible parameterless constructor.");
return GeneratedRegistrationResult.NoGeneratedRegistry();
}
var registry = activationMetadata.Factory();
registries.Add(registry);
}
foreach (var registry in registries)
{
logger.Debug(
$"Registering CQRS handlers for assembly {assemblyName} via generated registry {registry.GetType().FullName}.");
registry.Register(services, logger);
}
var reflectionFallbackMetadata = assemblyMetadata.ReflectionFallbackMetadata;
if (reflectionFallbackMetadata is not null)
{
if (reflectionFallbackMetadata.HasExplicitTypes)
{
logger.Debug(
$"Generated CQRS registry for assembly {assemblyName} requested targeted reflection fallback for {reflectionFallbackMetadata.Types.Count} unsupported handler type(s).");
}
else
{
logger.Debug(
$"Generated CQRS registry for assembly {assemblyName} requested full reflection fallback for unsupported handlers.");
}
return GeneratedRegistrationResult.WithReflectionFallback(reflectionFallbackMetadata);
}
return GeneratedRegistrationResult.FullyHandled();
RegisterGeneratedRegistries(services, registries, assemblyName, logger);
return BuildGeneratedRegistrationResult(
assemblyMetadata.ReflectionFallbackMetadata,
assemblyName,
logger);
}
catch (Exception exception)
{
@ -186,12 +138,138 @@ internal static class CqrsHandlerRegistrar
// Request/notification handlers receive context injection before every dispatch.
// Transient registration avoids sharing mutable Context across concurrent requests.
services.AddTransient(handlerInterface, implementationType);
logger.Debug(
logger.Debug(
$"Registered CQRS handler {implementationType.FullName} as {handlerInterface.FullName}.");
}
}
}
/// <summary>
/// 激活当前程序集声明的所有 generated registry若任一 registry 不满足运行时契约,则整批回退到反射扫描。
/// </summary>
/// <param name="registryTypes">程序集声明的 generated registry 类型列表。</param>
/// <param name="assemblyName">用于诊断的程序集稳定名称。</param>
/// <param name="logger">日志记录器。</param>
/// <param name="registries">成功激活后的 registry 实例。</param>
/// <returns>当全部 registry 都可安全激活时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
private static bool TryCreateGeneratedRegistries(
IReadOnlyList<Type> registryTypes,
string assemblyName,
ILogger logger,
out IReadOnlyList<ICqrsHandlerRegistry> registries)
{
var activatedRegistries = new List<ICqrsHandlerRegistry>(registryTypes.Count);
foreach (var registryType in registryTypes)
{
if (!TryCreateGeneratedRegistry(registryType, assemblyName, logger, out var registry))
{
registries = Array.Empty<ICqrsHandlerRegistry>();
return false;
}
activatedRegistries.Add(registry);
}
registries = activatedRegistries;
return true;
}
/// <summary>
/// 激活单个 generated registry并在契约不满足时输出与原先完全一致的回退诊断。
/// </summary>
/// <param name="registryType">要分析的 generated registry 类型。</param>
/// <param name="assemblyName">当前程序集的稳定名称。</param>
/// <param name="logger">日志记录器。</param>
/// <param name="registry">激活成功后的 registry 实例。</param>
/// <returns>当 registry 可安全使用时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
private static bool TryCreateGeneratedRegistry(
Type registryType,
string assemblyName,
ILogger logger,
[NotNullWhen(true)] out ICqrsHandlerRegistry? registry)
{
var activationMetadata = RegistryActivationMetadataCache.GetOrAdd(
registryType,
AnalyzeRegistryActivation);
if (!activationMetadata.ImplementsRegistryContract)
{
logger.Warn(
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it does not implement {typeof(ICqrsHandlerRegistry).FullName}.");
registry = null;
return false;
}
if (activationMetadata.IsAbstract)
{
logger.Warn(
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it is abstract.");
registry = null;
return false;
}
if (activationMetadata.Factory is null)
{
logger.Warn(
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it does not expose an accessible parameterless constructor.");
registry = null;
return false;
}
registry = activationMetadata.Factory();
return true;
}
/// <summary>
/// 调用所有已激活的 generated registry 完成 CQRS handler 注册,并保留稳定的调试日志顺序。
/// </summary>
/// <param name="services">目标服务集合。</param>
/// <param name="registries">已通过契约校验的 registry 实例。</param>
/// <param name="assemblyName">当前程序集的稳定名称。</param>
/// <param name="logger">日志记录器。</param>
private static void RegisterGeneratedRegistries(
IServiceCollection services,
IReadOnlyList<ICqrsHandlerRegistry> registries,
string assemblyName,
ILogger logger)
{
foreach (var registry in registries)
{
logger.Debug(
$"Registering CQRS handlers for assembly {assemblyName} via generated registry {registry.GetType().FullName}.");
registry.Register(services, logger);
}
}
/// <summary>
/// 将 generated registry 的 fallback 元数据转换为统一的注册结果,并记录下一阶段是定向补扫还是整程序集扫描。
/// </summary>
/// <param name="reflectionFallbackMetadata">生成注册器声明的反射补扫元数据。</param>
/// <param name="assemblyName">当前程序集的稳定名称。</param>
/// <param name="logger">日志记录器。</param>
/// <returns>描述 generated registry 是否已完全处理当前程序集的结果对象。</returns>
private static GeneratedRegistrationResult BuildGeneratedRegistrationResult(
ReflectionFallbackMetadata? reflectionFallbackMetadata,
string assemblyName,
ILogger logger)
{
if (reflectionFallbackMetadata is null)
return GeneratedRegistrationResult.FullyHandled();
if (reflectionFallbackMetadata.HasExplicitTypes)
{
logger.Debug(
$"Generated CQRS registry for assembly {assemblyName} requested targeted reflection fallback for {reflectionFallbackMetadata.Types.Count} unsupported handler type(s).");
}
else
{
logger.Debug(
$"Generated CQRS registry for assembly {assemblyName} requested full reflection fallback for unsupported handlers.");
}
return GeneratedRegistrationResult.WithReflectionFallback(reflectionFallbackMetadata);
}
/// <summary>
/// 获取指定实现类型上所有受支持的 CQRS handler 接口,并缓存筛选与排序结果。
/// </summary>
@ -255,6 +333,29 @@ internal static class CqrsHandlerRegistrar
return null;
var resolvedTypes = new List<Type>();
AppendDirectFallbackTypes(fallbackAttributes, resolvedTypes, assemblyName, logger);
AppendNamedFallbackTypes(assembly, fallbackAttributes, resolvedTypes, assemblyName, logger);
return new ReflectionFallbackMetadata(
resolvedTypes
.Distinct()
.OrderBy(GetTypeSortKey, StringComparer.Ordinal)
.ToArray());
}
/// <summary>
/// 追加 attribute 里直接携带的 fallback 类型,并过滤掉跨程序集误声明的条目。
/// </summary>
/// <param name="fallbackAttributes">当前程序集上的 fallback attribute 集合。</param>
/// <param name="resolvedTypes">待补充的已解析类型集合。</param>
/// <param name="assemblyName">当前程序集的稳定名称。</param>
/// <param name="logger">日志记录器。</param>
private static void AppendDirectFallbackTypes(
IReadOnlyList<CqrsReflectionFallbackAttribute> fallbackAttributes,
ICollection<Type> resolvedTypes,
string assemblyName,
ILogger logger)
{
foreach (var fallbackType in fallbackAttributes
.SelectMany(static attribute => attribute.FallbackHandlerTypes)
.Where(static type => type is not null)
@ -273,37 +374,65 @@ internal static class CqrsHandlerRegistrar
resolvedTypes.Add(fallbackType);
}
}
/// <summary>
/// 追加 attribute 里以类型名声明的 fallback 条目,并保留逐项失败的诊断能力。
/// </summary>
/// <param name="assembly">当前待解析的程序集。</param>
/// <param name="fallbackAttributes">当前程序集上的 fallback attribute 集合。</param>
/// <param name="resolvedTypes">待补充的已解析类型集合。</param>
/// <param name="assemblyName">当前程序集的稳定名称。</param>
/// <param name="logger">日志记录器。</param>
private static void AppendNamedFallbackTypes(
Assembly assembly,
IReadOnlyList<CqrsReflectionFallbackAttribute> fallbackAttributes,
ICollection<Type> resolvedTypes,
string assemblyName,
ILogger logger)
{
foreach (var typeName in fallbackAttributes
.SelectMany(static attribute => attribute.FallbackHandlerTypeNames)
.Where(static name => !string.IsNullOrWhiteSpace(name))
.Distinct(StringComparer.Ordinal)
.OrderBy(static name => name, StringComparer.Ordinal))
{
try
{
var type = assembly.GetType(typeName, throwOnError: false, ignoreCase: false);
if (type is null)
{
logger.Warn(
$"Generated CQRS reflection fallback type {typeName} could not be resolved in assembly {assemblyName}. Skipping targeted fallback entry.");
continue;
}
TryAppendNamedFallbackType(assembly, resolvedTypes, assemblyName, typeName, logger);
}
}
resolvedTypes.Add(type);
}
catch (Exception exception)
/// <summary>
/// 解析并追加单个按名称声明的 fallback 类型,同时保留“找不到”与“加载异常”两类不同日志语义。
/// </summary>
/// <param name="assembly">当前待解析的程序集。</param>
/// <param name="resolvedTypes">待补充的已解析类型集合。</param>
/// <param name="assemblyName">当前程序集的稳定名称。</param>
/// <param name="typeName">要解析的完整类型名。</param>
/// <param name="logger">日志记录器。</param>
private static void TryAppendNamedFallbackType(
Assembly assembly,
ICollection<Type> resolvedTypes,
string assemblyName,
string typeName,
ILogger logger)
{
try
{
var type = assembly.GetType(typeName, throwOnError: false, ignoreCase: false);
if (type is null)
{
logger.Warn(
$"Generated CQRS reflection fallback type {typeName} failed to load in assembly {assemblyName}: {exception.Message}");
$"Generated CQRS reflection fallback type {typeName} could not be resolved in assembly {assemblyName}. Skipping targeted fallback entry.");
return;
}
}
return new ReflectionFallbackMetadata(
resolvedTypes
.Distinct()
.OrderBy(GetTypeSortKey, StringComparer.Ordinal)
.ToArray());
resolvedTypes.Add(type);
}
catch (Exception exception)
{
logger.Warn(
$"Generated CQRS reflection fallback type {typeName} failed to load in assembly {assemblyName}: {exception.Message}");
}
}
/// <summary>

View File

@ -7,28 +7,36 @@
## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-001`
- 当前阶段:`Phase 1`
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-005`
- 当前阶段:`Phase 5`
- 当前焦点:
- 已将旧 `local-plan/` 迁入 `ai-plan/public/analyzer-warning-reduction/`active 入口只保留当前恢复信息
- 基于现有剩余热点,评估 `MA0051``MA0048``MA0046` 与少量 `MA0016` 是否适合继续在同一主线上处理
- 若继续推进,优先选择不引入 API rename、公共契约漂移或 Godot 宿主不稳定测试的切入点
- 已完成 `GFramework.Core/Pause/PauseStackManager.cs``MA0051` 收口:将 `DestroyAsync``Pop` 拆分为锁内状态迁移、
栈调整和锁外通知三个阶段,同时保持日志、事件与销毁补发语义不变
- 已为销毁路径补充 `PauseStackManagerTests.DestroyAsync_Should_NotifyResumedGroups` 回归测试,覆盖“销毁时向所有仍暂停组补发恢复通知”
- 下一轮若继续推进,优先在 `CoroutineScheduler``Store` 的剩余 `MA0051` 中只选一个切入点,不回到已完成的
`PauseStackManager`
## 当前状态摘要
- 已完成 `GFramework.Core``GFramework.Cqrs``GFramework.Godot` 与部分 source generator 的低风险 warning 清理
- 已完成多轮 CodeRabbit follow-up 修复,并用定向测试与项目/解决方案构建验证了关键回归风险
- 当前剩余 warning 已集中到长方法、文件/类型命名冲突、delegate 形状和少量公共集合抽象接口问题
- 当前 `PauseStackManager` 的长方法 warning 已从 active 入口移除;主题内剩余 warning 主要集中在
`GFramework.Core/Coroutine/CoroutineScheduler.cs``GFramework.Core/StateManagement/Store.cs`、文件/类型命名冲突、
delegate 形状和少量公共集合抽象接口问题
## 当前活跃事实
- 当前主题仍是 active topic因为剩余结构性 warning 是否继续推进尚未决策
- `RP-001` 的详细实现历史、测试记录和 warning 热点清单已归档到主题内 `archive/`
- `RP-002` 已在不改公共契约的前提下完成 `CqrsHandlerRegistrar` 结构拆分,并通过定向 build/test 验证
- `RP-003` 已在不改生命周期契约的前提下完成 `ArchitectureLifecycle` 初始化主流程拆分,并通过定向 build/test 验证
- `RP-004` 已完成当前 PR review follow-up修复 `TryCreateGeneratedRegistry` 的可空 `out` 契约并清理 trace 文档重复标题
- `RP-005` 已在不改公共 API 的前提下完成 `PauseStackManager` 两个 `MA0051` 的结构拆分,并补充销毁通知回归测试
- 当前工作树分支 `fix/analyzer-warning-reduction-batch` 已在 `ai-plan/public/README.md` 建立 topic 映射
## 当前风险
- 结构性重构风险:剩余 `MA0051` 与 `MA0048` 可能要求较大的文件拆分或类型重命名
- 结构性重构风险:剩余 `GFramework.Core` 侧 `MA0051` 与 `MA0048` 可能要求较大的文件拆分或类型重命名
- 缓解措施:只在下一轮明确接受结构调整成本时再继续推进,不在恢复点模糊的情况下顺手扩面
- 测试宿主稳定性风险:部分 Godot 失败路径在当前 .NET 测试宿主下仍不稳定
- 缓解措施:继续优先使用稳定的 targeted test、项目构建和相邻 smoke test 组合验证
@ -43,10 +51,25 @@
## 验证说明
- `RP-001` 的详细 warning 清理、回归修复与定向验证命令均已迁入主题内历史归档
- `RP-002` 的定向验证结果:
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:UseSharedCompilation=false -p:RestoreFallbackFolders=`
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter FullyQualifiedName~CqrsHandlerRegistrarTests -p:RestoreFallbackFolders=`
- `RP-003` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders= -nologo -clp:Summary;WarningsOnly`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~ArchitectureLifecycleBehaviorTests -p:RestoreFallbackFolders=`
- `RP-004` 的定向验证结果:
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:UseSharedCompilation=false -p:RestoreFallbackFolders=`
- 结果:`0 Warning(s)``0 Error(s)`
- `RP-005` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
- 结果:`27 Warning(s)``0 Error(s)``PauseStackManager.cs` 已不再出现在 `MA0051` 列表中
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~PauseStackManagerTests -p:RestoreFallbackFolders=`
- 结果:`25 Passed``0 Failed`
- active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史
## 下一步
1. 若要继续该主题,先读 active tracking再按需展开历史归档中的 warning 热点与验证记录
2. 从 `MA0051``MA0048``MA0046` 中只选一个结构性切入点继续,不要在同一轮同时扩多个风险面
2. 优先在 `GFramework.Core/Coroutine/CoroutineScheduler.cs``GFramework.Core/StateManagement/Store.cs`
`MA0051` 中只选一个继续,不要在同一轮同时扩多个风险面
3. 若本主题确认暂缓,可保持当前归档状态,不需要再恢复 `local-plan/`

View File

@ -1,5 +1,82 @@
# Analyzer Warning Reduction 追踪
## 2026-04-21 — RP-005
### 阶段PauseStackManager `MA0051` 收口RP-005
- 按 active tracking 中“继续只选一个 `GFramework.Core` 结构性切入点”的约束,本轮选择
`GFramework.Core/Pause/PauseStackManager.cs`,因为该文件体量明显小于 `CoroutineScheduler``Store`
且已有稳定的 `PauseStackManagerTests` 覆盖暂停栈、跨组独立性、事件通知与并发 `Push/Pop` 行为
- 先用 `warnings-only` 定向构建确认 `DestroyAsync``Pop` 仍分别命中 `MA0051`,再把逻辑拆分为:
- `TryBeginDestroy`
- `NotifyDestroyedGroups`
- `TryPopEntry`
- `RemoveEntryFromStack`
- 额外抽出 `CreateHandlerSnapshot``NotifyHandlersSnapshot`,统一普通通知与销毁补发路径的处理器排序和异常日志,
保持原有“锁内采集快照、锁外调用处理器与事件”的并发策略不变
- 为销毁路径新增 `DestroyAsync_Should_NotifyResumedGroups`,验证当多个暂停组在销毁前仍为暂停态时,
处理器和事件订阅者都会收到 `IsPaused=false` 的恢复信号
- 验证通过:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
- 结果:`27 Warning(s)``0 Error(s)``PauseStackManager.cs` 已不再出现在 `MA0051` 列表
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~PauseStackManagerTests -p:RestoreFallbackFolders=`
- 结果:`25 Passed``0 Failed`
- 下一步保持原节奏:只在 `CoroutineScheduler``Store` 中二选一继续,不与其他 warning 家族混做
## 2026-04-21 — RP-003
### 阶段Architecture 生命周期 `MA0051` 收口RP-003
- 依据 active tracking 中“继续只选一个 `GFramework.Core` 结构性切入点”的约束,选定
`GFramework.Core/Architectures/ArchitectureLifecycle.cs`,因为文件体量适中且已有
`ArchitectureLifecycleBehaviorTests` 覆盖阶段流转、销毁顺序和 late registration 行为
- 先用 `warnings-only` 定向构建确认 `ArchitectureLifecycle.InitializeAllComponentsAsync` 仍在报
`MA0051`,随后把主流程拆成:
- `CreateInitializationPlan`
- `InitializePhaseComponentsAsync`
- `MarkInitializationCompleted`
- 保持原有阶段顺序 `Before* -> After*`、批量日志文本和异步初始化策略不变,只压缩主方法长度
- 修正新增 `InitializationPlan` 记录类型的 XML `<param>` 名称大小写,避免引入文档告警
- 验证通过:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders= -nologo -clp:Summary;WarningsOnly`
- 结果:`29 Warning(s)``0 Error(s)``ArchitectureLifecycle.cs` 已不再出现在 warning 列表
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~ArchitectureLifecycleBehaviorTests -p:RestoreFallbackFolders=`
- 结果:`6 Passed``0 Failed`
## 2026-04-21 — RP-004
### 阶段PR review follow-upRP-004
- 使用 `gframework-pr-review` 抓取当前分支 PR #263 的最新 CodeRabbit review threads、MegaLinter 摘要与 CTRF 测试结果,
只接受仍能在本地工作树复现的 review 点
- 在 `GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs` 中将 `TryCreateGeneratedRegistry``out` 参数改为
`[NotNullWhen(true)] out ICqrsHandlerRegistry?`,移除三处 `null!` 抑制,保持激活失败时的日志文本与回退语义不变
- 修正 active trace 中重复的 `## 2026-04-21` 二级标题,消除 CodeRabbit 报告的 markdownlint `MD024`
- 核实 PR 信号后确认:当前 CTRF 报告为 `2134 passed / 0 failed`MegaLinter 唯一告警来自 CI 环境中的 `dotnet-format`
restore 失败,不是本地代码格式问题
- 验证通过:
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:UseSharedCompilation=false -p:RestoreFallbackFolders=`
- 结果:`0 Warning(s)``0 Error(s)`
## 2026-04-21 — RP-002
### 阶段CQRS `MA0051` 收口RP-002
- 依据 active tracking 中“先只选一个结构性切入点”的约束,选定 `GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs`
作为低风险下一步,因为它已有稳定的 targeted test 覆盖 generated registry、reflection fallback、缓存和重复注册行为
- 将 `TryRegisterGeneratedHandlers` 拆分为 registry 激活、批量注册和 fallback 结果构建三个辅助阶段,同时把
`GetReflectionFallbackMetadata` 的直接类型解析与按名称解析拆开,降低长方法复杂度但不改日志文本与回退语义
- 顺手修正 `RegisterAssemblyHandlers` 内部调试日志的缩进,未改注册顺序、生命周期或服务描述符写入逻辑
- 验证通过:
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:UseSharedCompilation=false -p:RestoreFallbackFolders=`
- 结果:`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter FullyQualifiedName~CqrsHandlerRegistrarTests -p:RestoreFallbackFolders=`
- 结果:`11 Passed``0 Failed`
- 新发现的环境注意事项:
- 当前 WSL worktree 下若不显式传入 `-p:RestoreFallbackFolders=`Linux `dotnet` 会读取不存在的 Windows fallback package
folder 并导致 `ResolvePackageAssets` 失败
- sandbox 内运行 `dotnet` 会因 MSBuild named-pipe 限制失败;需要在提权上下文中执行 .NET 验证
## 2026-04-19
### 阶段local-plan 迁移收口RP-001
@ -28,5 +105,5 @@
### 下一步
1. 后续若继续 analyzer warning reduction只从 `ai-plan/public/analyzer-warning-reduction/` 进入,不再恢复 `local-plan/`
2. 若 active 入口再次积累多轮已完成且已验证阶段,继续按同一模式迁入该主题自己的 `archive/`
1. 若继续 analyzer warning reduction优先回到 `GFramework.Core` 剩余 `MA0051` 热点,并继续保持“单 warning family、单切入点”的节奏
2. 后续所有 WSL 下的 .NET 定向验证命令继续显式附带 `-p:RestoreFallbackFolders=`,避免把环境问题误判成代码回归