fix(core): 清零低风险分析器警告

- 更新 Core 配置与集合扩展的集合抽象契约

- 修复 CoroutineScheduler 字符串字典 comparer 与 EasyEvents 重复注册异常类型

- 补充 Option<T> 相等性接口并更新 analyzer recovery 记录
This commit is contained in:
GeWuYou 2026-04-22 08:56:05 +08:00 committed by gewuyou
parent 3f95843d59
commit 97573be2e1
9 changed files with 76 additions and 31 deletions

View File

@ -99,14 +99,14 @@ public class EasyEventsTests
}
/// <summary>
/// 测试并发场景下AddEvent的行为
/// 测试 AddEvent 对重复事件类型给出状态冲突异常。
/// </summary>
[Test]
public void AddEvent_Should_Throw_When_Already_Registered()
{
_easyEvents.AddEvent<Event<int>>();
Assert.Throws<ArgumentException>(() => _easyEvents.AddEvent<Event<int>>());
Assert.Throws<InvalidOperationException>(() => _easyEvents.AddEvent<Event<int>>());
}
/// <summary>
@ -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

@ -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,12 @@ public class EasyEvents
/// 添加指定类型的事件到事件字典中
/// </summary>
/// <typeparam name="T">事件类型必须实现IEasyEvent接口且具有无参构造函数</typeparam>
/// <exception cref="ArgumentException">当事件类型已存在时抛出</exception>
/// <exception cref="InvalidOperationException">当事件类型已存在时抛出</exception>
public void AddEvent<T>() where T : IEvent, new()
{
if (!_mTypeEvents.TryAdd(typeof(T), new T()))
{
throw new ArgumentException($"Event type {typeof(T).Name} already registered.");
throw new InvalidOperationException($"Event type {typeof(T).Name} already registered.");
}
}
@ -81,4 +81,4 @@ public class EasyEvents
{
return (T)_mTypeEvents.GetOrAdd(typeof(T), _ => new T());
}
}
}

View File

@ -81,7 +81,7 @@ public static class CollectionExtensions
/// // dict["a"] == 3 (最后一个值)
/// </code>
/// </example>
public static Dictionary<TKey, TValue> ToDictionarySafe<T, TKey, TValue>(
public static IDictionary<TKey, TValue> ToDictionarySafe<T, TKey, TValue>(
this IEnumerable<T> source,
Func<T, TKey> keySelector,
Func<T, TValue> valueSelector) where TKey : notnull

View File

@ -17,7 +17,7 @@ namespace GFramework.Core.Functional;
/// 表示可能存在或不存在的值,用于替代 null 引用的函数式编程类型
/// </summary>
/// <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 +313,4 @@ public readonly struct Option<T>
_isSome ? $"Some({_value})" : "None";
#endregion
}
}

View File

@ -20,10 +20,10 @@ public sealed class FilterConfiguration
/// <summary>
/// 命名空间前缀列表(用于 Namespace 过滤器)。
/// </summary>
public List<string>? Namespaces { get; set; }
public IList<string>? Namespaces { get; set; }
/// <summary>
/// 子过滤器列表(用于 Composite 过滤器)。
/// </summary>
public List<FilterConfiguration>? Filters { get; set; }
public IList<FilterConfiguration>? Filters { get; set; }
}

View File

@ -15,10 +15,11 @@ public sealed class LoggingConfiguration
/// <summary>
/// Appender 配置列表
/// </summary>
public List<AppenderConfiguration> Appenders { get; set; } = new();
public IList<AppenderConfiguration> Appenders { get; set; } = new List<AppenderConfiguration>();
/// <summary>
/// 特定 Logger 的日志级别配置
/// </summary>
public Dictionary<string, LogLevel> LoggerLevels { get; set; } = new(StringComparer.Ordinal);
public IDictionary<string, LogLevel> LoggerLevels { get; set; } =
new Dictionary<string, LogLevel>(StringComparer.Ordinal);
}

View File

@ -7,17 +7,18 @@
## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-015`
- 当前阶段:`Phase 15`
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-016`
- 当前阶段:`Phase 16`
- 当前焦点:
- 当前分支 PR #267 的失败测试已通过 `$gframework-pr-review` 与本地整包测试完成复核
- 已确认并修复 `AsyncLogAppender.Flush()` 在“后台线程先清空队列”场景下可能超时返回 `false` 的竞态
- 已补上稳定回归测试,避免只在整包 `GFramework.Core.Tests` 里偶发暴露的刷新完成信号问题再次回归
- 下一轮默认恢复到 `MA0016``MA0002` 低风险批次;`MA0015``MA0077` 继续作为尾项顺手吸收
- 已完成 `GFramework.Core` 当前 `MA0016` / `MA0002` / `MA0015` / `MA0077` 低风险收口批次
- `LoggingConfiguration``FilterConfiguration``CollectionExtensions` 已改用集合抽象接口,并保留内部具体集合默认值
- `CoroutineScheduler` 的 tag/group 字典已显式使用 `StringComparer.Ordinal`,保持既有区分大小写语义
- `EasyEvents.AddEvent<T>()` 的重复注册路径已改为状态冲突异常,避免把泛型类型参数伪装成方法参数名
- `Option<T>` 已声明 `IEquatable<Option<T>>`,与已有强类型 `Equals(Option<T>)` 契约对齐
- 当前 `GFramework.Core` `net8.0` warnings-only 基线已降到 `0`
- `GFramework.Godot``Timing.cs` 已同步适配新事件签名,但当前 worktree 的 Godot restore 资产仍受 Windows fallback package folder 干扰,独立 build 需在修复资产后补跑
- 后续继续按 warning 类型和数量批处理,而不是回退到按单文件切片推进
- 当某一轮主类型数量不足时,允许顺手合并其他低冲突 warning 类型,`MA0015``MA0077`
只是当前最明显的低数量示例,不构成限定
- 下一轮默认评估跨 target 的 `MA0158` 锁替换风险,或单独处理 source generator 剩余 `MA0051`
- 单次 `boot` 的工作树改动上限控制在约 `100` 个文件以内,避免 recovery context 与 review 面同时失控
- 若任务边界互不冲突,允许使用不同模型的 subagent 并行处理不同 warning 类型或不同目录,但必须遵守显式 ownership
@ -34,8 +35,7 @@
`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`
## 当前活跃事实
@ -66,16 +66,20 @@
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 语义
- 当前工作树分支 `fix/analyzer-warning-reduction-batch` 已在 `ai-plan/public/README.md` 建立 topic 映射
## 当前风险
- 公共契约兼容风险:剩余 `MA0016` 若直接改公开集合类型,可能波及用户代码
- 缓解措施:优先选择不改公共 API 的低风险切法;若必须触达公共契约,先补齐 XML 契约说明与定向测试
- 公共契约兼容风险:本轮将部分配置与扩展方法返回值从具体集合类型改为集合抽象接口
- 缓解措施:保留具体集合默认值,并通过配置反序列化、工厂创建与集合扩展定向测试覆盖主要消费路径
- 测试宿主稳定性风险:部分 Godot 失败路径在当前 .NET 测试宿主下仍不稳定
- 缓解措施:继续优先使用稳定的 targeted test、项目构建和相邻 smoke test 组合验证
- 多目标框架 warning 解释风险:同一源位置会在多个 target framework 下重复计数
- 缓解措施:继续以唯一源位置和 warning 家族为主要决策依据,而不是只看原始 warning 总数
- net10 专属 warning 风险:`MA0158` 建议使用 `System.Threading.Lock`,但项目多 target 时需要确认兼容边界
- 缓解措施:下一轮先按 target framework 与 API 可用性评估,不直接批量替换共享源码中的 `object` lock
- Godot 资产文件环境风险:当前 worktree 的 `GFramework.Godot` restore/build 仍会命中 Windows fallback package folder
- 缓解措施:后续若继续触达 Godot 模块,先用 Linux 侧 restore 资产或 Windows-hosted 构建链刷新该项目,再补跑定向 build
- 并行实现风险:批量收敛时若 subagent 写入边界不清晰,容易引入命名冲突或重复重构
@ -158,12 +162,18 @@
- 结果:`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
- active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史
## 下一步
1. 若要继续该主题,先读 active tracking再按需展开历史归档中的 warning 热点与验证记录
2. 下一轮优先在 `MA0016``MA0002` 之间选择低风险批次继续推进,默认先看 `LoggingConfiguration` /
`FilterConfiguration``CollectionExtensions`
3. 若后续继续改动 `GFramework.Godot`,先修复该项目的 Linux 侧 restore 资产,再补跑独立 build
4. 若本主题确认暂缓,可保持当前归档状态,不需要再恢复 `local-plan/`
2. 下一轮优先评估 `net10.0` 下的 `MA0158` 是否能在不破坏多 target 兼容性的前提下安全推进
3. 若暂不推进 `MA0158`,可转入 `GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs` 的剩余 `MA0051`
结构拆分
4. 若后续继续改动 `GFramework.Godot`,先修复该项目的 Linux 侧 restore 资产,再补跑独立 build
5. 若本主题确认暂缓,可保持当前归档状态,不需要再恢复 `local-plan/`

View File

@ -1,5 +1,39 @@
# Analyzer Warning Reduction 追踪
## 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