fix(analyzer): 收敛 Store 长方法 warning

- 重构 Store 的 dispatch 进入提交退出阶段,降低 MA0051 并保持锁与通知语义
- 重构 reducer 快照创建流程,保留多态匹配的稳定排序规则
- 更新 analyzer warning reduction 的恢复点与验证记录
This commit is contained in:
GeWuYou 2026-04-21 10:30:20 +08:00
parent b553d7cbc6
commit ec0c9a7bc8
3 changed files with 217 additions and 85 deletions

View File

@ -212,10 +212,6 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
ArgumentNullException.ThrowIfNull(action);
Action<TState>[] listenersSnapshot = Array.Empty<Action<TState>>();
IStoreMiddleware<TState>[] middlewaresSnapshot = Array.Empty<IStoreMiddleware<TState>>();
IStoreReducerAdapter[] reducersSnapshot = Array.Empty<IStoreReducerAdapter>();
IEqualityComparer<TState> stateComparerSnapshot = _stateComparer;
StoreDispatchContext<TState>? context = null;
TState notificationState = default!;
var hasNotification = false;
var enteredDispatchScope = false;
@ -224,49 +220,25 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
{
try
{
lock (_lock)
{
EnsureNotDispatching();
_isDispatching = true;
enteredDispatchScope = true;
context = new StoreDispatchContext<TState>(action!, _state);
stateComparerSnapshot = _stateComparer;
middlewaresSnapshot = CreateMiddlewareSnapshotCore();
reducersSnapshot = CreateReducerSnapshotCore(context.ActionType);
}
var context = EnterDispatchScope(
action,
out var middlewaresSnapshot,
out var reducersSnapshot,
out var stateComparerSnapshot);
enteredDispatchScope = true;
// middleware 和 reducer 可能包含较重的同步逻辑,因此仅持有 dispatch 串行门,
// 不占用状态锁,让读取、订阅和注册操作只在需要访问共享状态时短暂阻塞。
ExecuteDispatchPipeline(context, middlewaresSnapshot, reducersSnapshot, stateComparerSnapshot);
lock (_lock)
{
_lastActionType = context.ActionType;
_lastDispatchRecord = new StoreDispatchRecord<TState>(
context.Action,
context.PreviousState,
context.NextState,
context.HasStateChanged,
context.DispatchedAt);
if (!context.HasStateChanged)
{
return;
}
ApplyCommittedStateChange(context.NextState, context.DispatchedAt, context.Action);
listenersSnapshot = CaptureListenersOrDeferNotification(context.NextState, out notificationState);
hasNotification = listenersSnapshot.Length > 0;
}
hasNotification = TryCommitDispatchResult(context, out listenersSnapshot, out notificationState);
}
finally
{
if (enteredDispatchScope)
{
lock (_lock)
{
_isDispatching = false;
}
ExitDispatchScope();
}
}
}
@ -831,6 +803,90 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
context.HasStateChanged = !stateComparer.Equals(context.PreviousState, nextState);
}
/// <summary>
/// 进入一次新的 dispatch 作用域,并在状态锁内抓取本次执行所需的上下文快照。
/// 该方法只做最短路径的共享状态访问,把 middleware/reducer 的实际执行留到锁外完成。
/// </summary>
/// <typeparam name="TAction">action 的具体类型。</typeparam>
/// <param name="action">本次分发的 action。</param>
/// <param name="middlewaresSnapshot">返回本次 dispatch 使用的中间件快照。</param>
/// <param name="reducersSnapshot">返回本次 dispatch 使用的 reducer 快照。</param>
/// <param name="stateComparerSnapshot">返回本次 dispatch 使用的状态比较器快照。</param>
/// <returns>已初始化的 dispatch 上下文。</returns>
private StoreDispatchContext<TState> EnterDispatchScope<TAction>(
TAction action,
out IStoreMiddleware<TState>[] middlewaresSnapshot,
out IStoreReducerAdapter[] reducersSnapshot,
out IEqualityComparer<TState> stateComparerSnapshot)
{
Debug.Assert(
Monitor.IsEntered(_dispatchGate),
"Caller must hold _dispatchGate before entering a dispatch scope.");
lock (_lock)
{
EnsureNotDispatching();
_isDispatching = true;
var context = new StoreDispatchContext<TState>(action!, _state);
stateComparerSnapshot = _stateComparer;
middlewaresSnapshot = CreateMiddlewareSnapshotCore();
reducersSnapshot = CreateReducerSnapshotCore(context.ActionType);
return context;
}
}
/// <summary>
/// 在 dispatch 管线执行完成后提交诊断信息和状态变更。
/// 状态与订阅集合的更新统一在该阶段完成,从而保证 dispatch 与 time-travel 共享同一提交流程。
/// </summary>
/// <param name="context">刚完成 middleware/reducer 管线的 dispatch 上下文。</param>
/// <param name="listenersSnapshot">若需要立即通知,则返回锁外回放的监听器快照。</param>
/// <param name="notificationState">若需要立即通知,则返回要通知的状态。</param>
/// <returns>本次 dispatch 是否需要在锁外执行监听器通知。</returns>
private bool TryCommitDispatchResult(
StoreDispatchContext<TState> context,
out Action<TState>[] listenersSnapshot,
out TState notificationState)
{
Debug.Assert(
Monitor.IsEntered(_dispatchGate),
"Caller must hold _dispatchGate before committing a dispatch result.");
lock (_lock)
{
_lastActionType = context.ActionType;
_lastDispatchRecord = new StoreDispatchRecord<TState>(
context.Action,
context.PreviousState,
context.NextState,
context.HasStateChanged,
context.DispatchedAt);
if (!context.HasStateChanged)
{
listenersSnapshot = Array.Empty<Action<TState>>();
notificationState = default!;
return false;
}
ApplyCommittedStateChange(context.NextState, context.DispatchedAt, context.Action);
listenersSnapshot = CaptureListenersOrDeferNotification(context.NextState, out notificationState);
return listenersSnapshot.Length > 0;
}
}
/// <summary>
/// 退出当前 dispatch 作用域,允许后续 dispatch 或历史控制继续进入。
/// </summary>
private void ExitDispatchScope()
{
lock (_lock)
{
_isDispatching = false;
}
}
/// <summary>
/// 确保当前 Store 没有发生重入分发或在 dispatch 中执行历史控制。
/// </summary>
@ -949,20 +1005,65 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
if (_actionMatchingMode == StoreActionMatchingMode.ExactTypeOnly)
{
if (!_reducers.TryGetValue(actionType, out var exactReducers) || exactReducers.Count == 0)
{
return Array.Empty<IStoreReducerAdapter>();
}
var exactSnapshot = new IStoreReducerAdapter[exactReducers.Count];
for (var i = 0; i < exactReducers.Count; i++)
{
exactSnapshot[i] = exactReducers[i].Adapter;
}
return exactSnapshot;
return CreateExactReducerSnapshot(actionType);
}
return CreateAssignableReducerSnapshot(actionType);
}
/// <summary>
/// 为精确类型匹配模式创建 reducer 快照。
/// </summary>
/// <param name="actionType">当前 action 的运行时类型。</param>
/// <returns>精确匹配到的 reducer 快照;若未注册则返回空数组。</returns>
private IStoreReducerAdapter[] CreateExactReducerSnapshot(Type actionType)
{
if (!_reducers.TryGetValue(actionType, out var exactReducers) || exactReducers.Count == 0)
{
return Array.Empty<IStoreReducerAdapter>();
}
var exactSnapshot = new IStoreReducerAdapter[exactReducers.Count];
for (var i = 0; i < exactReducers.Count; i++)
{
exactSnapshot[i] = exactReducers[i].Adapter;
}
return exactSnapshot;
}
/// <summary>
/// 为多态匹配模式创建 reducer 快照。
/// 该路径会收集所有可赋值的注册桶,并按“精确类型 -> 基类距离 -> 接口 -> 注册顺序”的稳定规则排序。
/// </summary>
/// <param name="actionType">当前 action 的运行时类型。</param>
/// <returns>多态模式下的 reducer 快照;若未注册则返回空数组。</returns>
private IStoreReducerAdapter[] CreateAssignableReducerSnapshot(Type actionType)
{
var matches = CollectReducerMatches(actionType);
if (matches is null || matches.Count == 0)
{
return Array.Empty<IStoreReducerAdapter>();
}
matches.Sort(CompareReducerMatch);
var snapshot = new IStoreReducerAdapter[matches.Count];
for (var i = 0; i < matches.Count; i++)
{
snapshot[i] = matches[i].Adapter;
}
return snapshot;
}
/// <summary>
/// 收集当前 action 类型可命中的 reducer 注册,并附带稳定排序所需的匹配元数据。
/// </summary>
/// <param name="actionType">当前 action 的运行时类型。</param>
/// <returns>匹配结果列表;若没有任何匹配则返回 <see langword="null"/>。</returns>
private List<ReducerMatch>? CollectReducerMatches(Type actionType)
{
List<ReducerMatch>? matches = null;
foreach (var reducerBucket in _reducers)
@ -984,35 +1085,30 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
}
}
if (matches is null || matches.Count == 0)
return matches;
}
/// <summary>
/// 比较两个 reducer 匹配结果的优先级,保证多态匹配下的执行顺序稳定可预测。
/// </summary>
/// <param name="left">左侧匹配结果。</param>
/// <param name="right">右侧匹配结果。</param>
/// <returns>排序比较结果。</returns>
private static int CompareReducerMatch(ReducerMatch left, ReducerMatch right)
{
var categoryComparison = left.MatchCategory.CompareTo(right.MatchCategory);
if (categoryComparison != 0)
{
return Array.Empty<IStoreReducerAdapter>();
return categoryComparison;
}
matches.Sort(static (left, right) =>
var distanceComparison = left.InheritanceDistance.CompareTo(right.InheritanceDistance);
if (distanceComparison != 0)
{
var categoryComparison = left.MatchCategory.CompareTo(right.MatchCategory);
if (categoryComparison != 0)
{
return categoryComparison;
}
var distanceComparison = left.InheritanceDistance.CompareTo(right.InheritanceDistance);
if (distanceComparison != 0)
{
return distanceComparison;
}
return left.Sequence.CompareTo(right.Sequence);
});
var snapshot = new IStoreReducerAdapter[matches.Count];
for (var i = 0; i < matches.Count; i++)
{
snapshot[i] = matches[i].Adapter;
return distanceComparison;
}
return snapshot;
return left.Sequence.CompareTo(right.Sequence);
}
/// <summary>
@ -1412,4 +1508,4 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
/// </summary>
public TState PendingState { get; set; } = default!;
}
}
}

View File

@ -7,22 +7,21 @@
## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-005`
- 当前阶段:`Phase 5`
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-006`
- 当前阶段:`Phase 6`
- 当前焦点:
- 已完成 `GFramework.Core/Pause/PauseStackManager.cs` 的 `MA0051` 收口:将 `DestroyAsync``Pop` 拆分为锁内状态迁移、
栈调整和锁外通知三个阶段,同时保持日志、事件与销毁补发语义不变
- 已为销毁路径补充 `PauseStackManagerTests.DestroyAsync_Should_NotifyResumedGroups` 回归测试,覆盖“销毁时向所有仍暂停组补发恢复通知”
- 下一轮若继续推进,优先`CoroutineScheduler``Store` 的剩余 `MA0051` 中只选一个切入点,不回到已完成的
`PauseStackManager`
- 已完成 `GFramework.Core/StateManagement/Store.cs` 的 `MA0051` 收口:将 `Dispatch` 拆分为“进入分发域 / 提交结果 / 退出分发域”
三个辅助阶段,并将 reducer 快照创建拆分为精确匹配与多态匹配两条路径
- 本轮保持 `Store` 的锁顺序、middleware 执行时机、batch 通知折叠和多态 reducer 排序规则不变,未改公共 API
- 下一轮若继续推进,优先只处理 `GFramework.Core/Coroutine/CoroutineScheduler.cs` 的剩余 `MA0051`,不回到已完成的
`Store` 或 `PauseStackManager`
## 当前状态摘要
- 已完成 `GFramework.Core``GFramework.Cqrs``GFramework.Godot` 与部分 source generator 的低风险 warning 清理
- 已完成多轮 CodeRabbit follow-up 修复,并用定向测试与项目/解决方案构建验证了关键回归风险
- 当前 `PauseStackManager` 的长方法 warning 已从 active 入口移除;主题内剩余 warning 主要集中在
`GFramework.Core/Coroutine/CoroutineScheduler.cs``GFramework.Core/StateManagement/Store.cs`、文件/类型命名冲突、
delegate 形状和少量公共集合抽象接口问题
- 当前 `PauseStackManager``Store` 的长方法 warning 已从 active 入口移除;主题内剩余 warning 主要集中在
`GFramework.Core/Coroutine/CoroutineScheduler.cs`、文件/类型命名冲突、delegate 形状和少量公共集合抽象接口问题
## 当前活跃事实
@ -32,6 +31,8 @@
- `RP-003` 已在不改生命周期契约的前提下完成 `ArchitectureLifecycle` 初始化主流程拆分,并通过定向 build/test 验证
- `RP-004` 已完成当前 PR review follow-up修复 `TryCreateGeneratedRegistry` 的可空 `out` 契约并清理 trace 文档重复标题
- `RP-005` 已在不改公共 API 的前提下完成 `PauseStackManager` 两个 `MA0051` 的结构拆分,并补充销毁通知回归测试
- `RP-006` 已在不改公共 API 的前提下完成 `Store` 两个 `MA0051` 的结构拆分,并通过定向 build/test 验证 dispatch、
多态 reducer 匹配与历史语义未回归
- 当前工作树分支 `fix/analyzer-warning-reduction-batch` 已在 `ai-plan/public/README.md` 建立 topic 映射
## 当前风险
@ -65,11 +66,16 @@
- 结果:`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`
- `RP-006` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo -clp:"Summary;WarningsOnly"`
- 结果:`25 Warning(s)``0 Error(s)``Store.cs` 已不再出现在 `MA0051` 列表中
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~StoreTests -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo`
- 结果:`30 Passed``0 Failed`
- active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史
## 下一步
1. 若要继续该主题,先读 active tracking再按需展开历史归档中的 warning 热点与验证记录
2. 优先在 `GFramework.Core/Coroutine/CoroutineScheduler.cs` `GFramework.Core/StateManagement/Store.cs`
`MA0051` 中只选一个继续,不要在同一轮同时扩多个风险面
2. 优先在 `GFramework.Core/Coroutine/CoroutineScheduler.cs` `Run``FinalizeCoroutine` 两个 `MA0051`
中继续,保持“单文件、单 warning family”的节奏
3. 若本主题确认暂缓,可保持当前归档状态,不需要再恢复 `local-plan/`

View File

@ -1,5 +1,35 @@
# Analyzer Warning Reduction 追踪
## 2026-04-21 — RP-006
### 阶段Store `MA0051` 收口RP-006
- 依据 active tracking 中“继续只选一个 `GFramework.Core` 结构性切入点”的约束,本轮选择
`GFramework.Core/StateManagement/Store.cs`,因为该文件的两个 `MA0051` 都集中在 dispatch / reducer snapshot 逻辑,
且已有 `StoreTests` 覆盖 dispatch、batch、history 和多态 reducer 匹配语义
- 在正式验证前先处理 WSL 环境噪音:当前 worktree 的 `GFramework.Core/obj/project.assets.json` 是 Windows 侧 restore
产物,`--no-restore` 构建会继续引用宿主 Windows fallback package folder本轮先执行一次 Linux 侧
`dotnet restore GFramework.Core/GFramework.Core.csproj -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> --ignore-failed-sources -nologo`
刷新资产文件,再继续 warnings-only build
- 将 `Dispatch` 拆分为:
- `EnterDispatchScope`
- `TryCommitDispatchResult`
- `ExitDispatchScope`
- 将 `CreateReducerSnapshotCore` 拆分为:
- `CreateExactReducerSnapshot`
- `CreateAssignableReducerSnapshot`
- `CollectReducerMatches`
- `CompareReducerMatch`
- 保持 `_dispatchGate -> _lock` 的锁顺序、middleware 锁外执行、批处理通知折叠以及“精确类型 -> 基类 -> 接口 ->
注册顺序”的 reducer 稳定排序语义不变,只收缩主方法长度并补齐辅助方法意图注释
- 验证通过:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo -clp:"Summary;WarningsOnly"`
- 结果:`25 Warning(s)``0 Error(s)``Store.cs` 已不再出现在 `MA0051` 列表
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~StoreTests -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo`
- 结果:`30 Passed``0 Failed`
- 下一步保持同一节奏:只在 `CoroutineScheduler.cs``Run` / `FinalizeCoroutine` 两个 `MA0051` 中继续,不与其他
warning 家族混做
## 2026-04-21 — RP-005
### 阶段PauseStackManager `MA0051` 收口RP-005