fix(warning-reduction): 清理配置与测试切片告警

- 修复 YamlConfigLoader 的超长方法、依赖比较与热重载同步原语告警

- 拆分 MicrosoftDiContainerTests 与 AbstractAsyncQueryTests 的辅助类型文件以消除 MA0048

- 更新 analyzer warning reduction 跟踪文档并记录 non-incremental 构建基线变化
This commit is contained in:
gewuyou 2026-04-27 11:57:49 +08:00
parent 86cfaa7122
commit a9904a35be
22 changed files with 539 additions and 437 deletions

View File

@ -0,0 +1,8 @@
namespace GFramework.Core.Tests.Ioc;
/// <summary>
/// 同时实现多个别名接口的测试服务。
/// </summary>
public sealed class AliasAwareService : IPrimaryAliasService, ISecondaryAliasService
{
}

View File

@ -0,0 +1,9 @@
namespace GFramework.Core.Tests.Ioc;
/// <summary>
/// 混合服务接口(用于测试优先级和非优先级混合)
/// </summary>
public interface IMixedService
{
string? Name { get; set; }
}

View File

@ -0,0 +1,6 @@
namespace GFramework.Core.Tests.Ioc;
/// <summary>
/// 主服务别名接口。
/// </summary>
public interface IPrimaryAliasService : ISharedAliasService;

View File

@ -0,0 +1,11 @@
using GFramework.Core.Abstractions.Bases;
namespace GFramework.Core.Tests.Ioc;
/// <summary>
/// 优先级服务接口
/// </summary>
public interface IPrioritizedService : IPrioritized
{
string? Name { get; set; }
}

View File

@ -0,0 +1,6 @@
namespace GFramework.Core.Tests.Ioc;
/// <summary>
/// 次级兼容别名接口。
/// </summary>
public interface ISecondaryAliasService : ISharedAliasService;

View File

@ -0,0 +1,6 @@
namespace GFramework.Core.Tests.Ioc;
/// <summary>
/// 服务接口定义
/// </summary>
public interface IService;

View File

@ -0,0 +1,6 @@
namespace GFramework.Core.Tests.Ioc;
/// <summary>
/// 用于验证未冻结查询路径中的服务别名去重行为。
/// </summary>
public interface ISharedAliasService;

View File

@ -1,5 +1,4 @@
using System.Reflection;
using GFramework.Core.Abstractions.Bases;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Ioc;
using GFramework.Core.Logging;
@ -734,74 +733,3 @@ public class MicrosoftDiContainerTests
Assert.That(((IPrioritizedService)services[1]).Priority, Is.EqualTo(30));
}
}
/// <summary>
/// 服务接口定义
/// </summary>
public interface IService;
/// <summary>
/// 测试服务类,实现 IService 接口
/// </summary>
public sealed class TestService : IService
{
/// <summary>
/// 获取或设置优先级
/// </summary>
public int Priority { get; set; }
}
/// <summary>
/// 优先级服务接口
/// </summary>
public interface IPrioritizedService : IPrioritized
{
string? Name { get; set; }
}
/// <summary>
/// 混合服务接口(用于测试优先级和非优先级混合)
/// </summary>
public interface IMixedService
{
string? Name { get; set; }
}
/// <summary>
/// 用于验证未冻结查询路径中的服务别名去重行为。
/// </summary>
public interface ISharedAliasService;
/// <summary>
/// 主服务别名接口。
/// </summary>
public interface IPrimaryAliasService : ISharedAliasService;
/// <summary>
/// 次级兼容别名接口。
/// </summary>
public interface ISecondaryAliasService : ISharedAliasService;
/// <summary>
/// 同时实现多个别名接口的测试服务。
/// </summary>
public sealed class AliasAwareService : IPrimaryAliasService, ISecondaryAliasService
{
}
/// <summary>
/// 实现优先级的服务
/// </summary>
public sealed class PrioritizedService : IPrioritizedService, IMixedService
{
public int Priority { get; set; }
public string? Name { get; set; }
}
/// <summary>
/// 不实现优先级的服务
/// </summary>
public sealed class NonPrioritizedService : IMixedService
{
public string? Name { get; set; }
}

View File

@ -0,0 +1,12 @@
namespace GFramework.Core.Tests.Ioc;
/// <summary>
/// 不实现优先级的服务
/// </summary>
public sealed class NonPrioritizedService : IMixedService
{
/// <summary>
/// 获取或设置服务名称
/// </summary>
public string? Name { get; set; }
}

View File

@ -0,0 +1,17 @@
namespace GFramework.Core.Tests.Ioc;
/// <summary>
/// 实现优先级的服务
/// </summary>
public sealed class PrioritizedService : IPrioritizedService, IMixedService
{
/// <summary>
/// 获取或设置优先级
/// </summary>
public int Priority { get; set; }
/// <summary>
/// 获取或设置服务名称
/// </summary>
public string? Name { get; set; }
}

View File

@ -0,0 +1,12 @@
namespace GFramework.Core.Tests.Ioc;
/// <summary>
/// 测试服务类,实现 IService 接口
/// </summary>
public sealed class TestService : IService
{
/// <summary>
/// 获取或设置优先级
/// </summary>
public int Priority { get; set; }
}

View File

@ -6,7 +6,6 @@ using GFramework.Core.Environment;
using GFramework.Core.Events;
using GFramework.Core.Ioc;
using GFramework.Core.Query;
using GFramework.Cqrs.Abstractions.Cqrs.Query;
namespace GFramework.Core.Tests.Query;
@ -236,179 +235,3 @@ public class AbstractAsyncQueryTests
Assert.That(result2, Is.EqualTo(40));
}
}
/// <summary>
/// 测试用异步查询输入类V2
/// </summary>
public sealed class TestAsyncQueryInputV2 : IQueryInput
{
/// <summary>
/// 获取或设置值
/// </summary>
public int Value { get; init; }
}
/// <summary>
/// 整数类型测试异步查询类V4继承AbstractAsyncQuery
/// </summary>
public sealed class TestAsyncQueryV4 : AbstractAsyncQuery<TestAsyncQueryInputV2, int>
{
/// <summary>
/// 初始化TestAsyncQueryV4的新实例
/// </summary>
/// <param name="input">查询输入参数</param>
public TestAsyncQueryV4(TestAsyncQueryInputV2 input) : base(input)
{
}
/// <summary>
/// 获取查询是否已执行
/// </summary>
public bool Executed { get; private set; }
/// <summary>
/// 执行异步查询操作的具体实现
/// </summary>
/// <param name="input">查询输入参数</param>
/// <returns>查询结果将输入值乘以2</returns>
protected override Task<int> OnDoAsync(TestAsyncQueryInputV2 input)
{
Executed = true;
return Task.FromResult(input.Value * 2);
}
}
/// <summary>
/// 字符串类型测试异步查询类V4继承AbstractAsyncQuery
/// </summary>
public sealed class TestAsyncStringQueryV4 : AbstractAsyncQuery<TestAsyncQueryInputV2, string>
{
/// <summary>
/// 初始化TestAsyncStringQueryV4的新实例
/// </summary>
/// <param name="input">查询输入参数</param>
public TestAsyncStringQueryV4(TestAsyncQueryInputV2 input) : base(input)
{
}
/// <summary>
/// 获取查询是否已执行
/// </summary>
public bool Executed { get; private set; }
/// <summary>
/// 执行异步查询操作的具体实现
/// </summary>
/// <param name="input">查询输入参数</param>
/// <returns>格式化的字符串结果</returns>
protected override Task<string> OnDoAsync(TestAsyncQueryInputV2 input)
{
Executed = true;
return Task.FromResult($"Value: {input.Value * 2}");
}
}
/// <summary>
/// 复杂对象类型测试异步查询类V4继承AbstractAsyncQuery
/// </summary>
public sealed class TestAsyncComplexQueryV4 : AbstractAsyncQuery<TestAsyncQueryInputV2, TestAsyncQueryResultV2>
{
/// <summary>
/// 初始化TestAsyncComplexQueryV4的新实例
/// </summary>
/// <param name="input">查询输入参数</param>
public TestAsyncComplexQueryV4(TestAsyncQueryInputV2 input) : base(input)
{
}
/// <summary>
/// 获取查询是否已执行
/// </summary>
public bool Executed { get; private set; }
/// <summary>
/// 执行异步查询操作的具体实现
/// </summary>
/// <param name="input">查询输入参数</param>
/// <returns>复杂对象查询结果</returns>
protected override Task<TestAsyncQueryResultV2> OnDoAsync(TestAsyncQueryInputV2 input)
{
Executed = true;
var result = new TestAsyncQueryResultV2
{
Value = input.Value * 2,
DoubleValue = input.Value * 3
};
return Task.FromResult(result);
}
}
/// <summary>
/// 测试用异步查询类(抛出异常)
/// </summary>
public sealed class TestAsyncQueryWithExceptionV4 : AbstractAsyncQuery<TestAsyncQueryInputV2, int>
{
/// <summary>
/// 初始化TestAsyncQueryWithExceptionV4的新实例
/// </summary>
/// <param name="input">查询输入参数</param>
public TestAsyncQueryWithExceptionV4(TestAsyncQueryInputV2 input) : base(input)
{
}
/// <summary>
/// 执行异步查询操作并抛出异常
/// </summary>
/// <param name="input">查询输入参数</param>
/// <exception cref="InvalidOperationException">总是抛出异常</exception>
protected override Task<int> OnDoAsync(TestAsyncQueryInputV2 input)
{
throw new InvalidOperationException("Test exception");
}
}
/// <summary>
/// 测试用异步查询子类V4继承AbstractAsyncQuery
/// </summary>
public sealed class TestAsyncQueryChildV4 : AbstractAsyncQuery<TestAsyncQueryInputV2, int>
{
/// <summary>
/// 初始化TestAsyncQueryChildV4的新实例
/// </summary>
/// <param name="input">查询输入参数</param>
public TestAsyncQueryChildV4(TestAsyncQueryInputV2 input) : base(input)
{
}
/// <summary>
/// 获取查询是否已执行
/// </summary>
public bool Executed { get; private set; }
/// <summary>
/// 执行异步查询操作的具体实现子类实现乘以3
/// </summary>
/// <param name="input">查询输入参数</param>
/// <returns>查询结果将输入值乘以3</returns>
protected override Task<int> OnDoAsync(TestAsyncQueryInputV2 input)
{
Executed = true;
return Task.FromResult(input.Value * 3);
}
}
/// <summary>
/// 测试用复杂查询结果类V2
/// </summary>
public sealed class TestAsyncQueryResultV2
{
/// <summary>
/// 获取或设置值
/// </summary>
public int Value { get; init; }
/// <summary>
/// 获取或设置双倍值
/// </summary>
public int DoubleValue { get; init; }
}

View File

@ -0,0 +1,38 @@
using GFramework.Core.Query;
namespace GFramework.Core.Tests.Query;
/// <summary>
/// 复杂对象类型测试异步查询类V4继承AbstractAsyncQuery
/// </summary>
public sealed class TestAsyncComplexQueryV4 : AbstractAsyncQuery<TestAsyncQueryInputV2, TestAsyncQueryResultV2>
{
/// <summary>
/// 初始化TestAsyncComplexQueryV4的新实例
/// </summary>
/// <param name="input">查询输入参数</param>
public TestAsyncComplexQueryV4(TestAsyncQueryInputV2 input) : base(input)
{
}
/// <summary>
/// 获取查询是否已执行
/// </summary>
public bool Executed { get; private set; }
/// <summary>
/// 执行异步查询操作的具体实现
/// </summary>
/// <param name="input">查询输入参数</param>
/// <returns>复杂对象查询结果</returns>
protected override Task<TestAsyncQueryResultV2> OnDoAsync(TestAsyncQueryInputV2 input)
{
Executed = true;
var result = new TestAsyncQueryResultV2
{
Value = input.Value * 2,
DoubleValue = input.Value * 3
};
return Task.FromResult(result);
}
}

View File

@ -0,0 +1,33 @@
using GFramework.Core.Query;
namespace GFramework.Core.Tests.Query;
/// <summary>
/// 测试用异步查询子类V4继承AbstractAsyncQuery
/// </summary>
public sealed class TestAsyncQueryChildV4 : AbstractAsyncQuery<TestAsyncQueryInputV2, int>
{
/// <summary>
/// 初始化TestAsyncQueryChildV4的新实例
/// </summary>
/// <param name="input">查询输入参数</param>
public TestAsyncQueryChildV4(TestAsyncQueryInputV2 input) : base(input)
{
}
/// <summary>
/// 获取查询是否已执行
/// </summary>
public bool Executed { get; private set; }
/// <summary>
/// 执行异步查询操作的具体实现子类实现乘以3
/// </summary>
/// <param name="input">查询输入参数</param>
/// <returns>查询结果将输入值乘以3</returns>
protected override Task<int> OnDoAsync(TestAsyncQueryInputV2 input)
{
Executed = true;
return Task.FromResult(input.Value * 3);
}
}

View File

@ -0,0 +1,14 @@
using GFramework.Cqrs.Abstractions.Cqrs.Query;
namespace GFramework.Core.Tests.Query;
/// <summary>
/// 测试用异步查询输入类V2
/// </summary>
public sealed class TestAsyncQueryInputV2 : IQueryInput
{
/// <summary>
/// 获取或设置值
/// </summary>
public int Value { get; init; }
}

View File

@ -0,0 +1,17 @@
namespace GFramework.Core.Tests.Query;
/// <summary>
/// 测试用复杂查询结果类V2
/// </summary>
public sealed class TestAsyncQueryResultV2
{
/// <summary>
/// 获取或设置值
/// </summary>
public int Value { get; init; }
/// <summary>
/// 获取或设置双倍值
/// </summary>
public int DoubleValue { get; init; }
}

View File

@ -0,0 +1,33 @@
using GFramework.Core.Query;
namespace GFramework.Core.Tests.Query;
/// <summary>
/// 整数类型测试异步查询类V4继承AbstractAsyncQuery
/// </summary>
public sealed class TestAsyncQueryV4 : AbstractAsyncQuery<TestAsyncQueryInputV2, int>
{
/// <summary>
/// 初始化TestAsyncQueryV4的新实例
/// </summary>
/// <param name="input">查询输入参数</param>
public TestAsyncQueryV4(TestAsyncQueryInputV2 input) : base(input)
{
}
/// <summary>
/// 获取查询是否已执行
/// </summary>
public bool Executed { get; private set; }
/// <summary>
/// 执行异步查询操作的具体实现
/// </summary>
/// <param name="input">查询输入参数</param>
/// <returns>查询结果将输入值乘以2</returns>
protected override Task<int> OnDoAsync(TestAsyncQueryInputV2 input)
{
Executed = true;
return Task.FromResult(input.Value * 2);
}
}

View File

@ -0,0 +1,27 @@
using GFramework.Core.Query;
namespace GFramework.Core.Tests.Query;
/// <summary>
/// 测试用异步查询类(抛出异常)
/// </summary>
public sealed class TestAsyncQueryWithExceptionV4 : AbstractAsyncQuery<TestAsyncQueryInputV2, int>
{
/// <summary>
/// 初始化TestAsyncQueryWithExceptionV4的新实例
/// </summary>
/// <param name="input">查询输入参数</param>
public TestAsyncQueryWithExceptionV4(TestAsyncQueryInputV2 input) : base(input)
{
}
/// <summary>
/// 执行异步查询操作并抛出异常
/// </summary>
/// <param name="input">查询输入参数</param>
/// <exception cref="InvalidOperationException">总是抛出异常</exception>
protected override Task<int> OnDoAsync(TestAsyncQueryInputV2 input)
{
throw new InvalidOperationException("Test exception");
}
}

View File

@ -0,0 +1,33 @@
using GFramework.Core.Query;
namespace GFramework.Core.Tests.Query;
/// <summary>
/// 字符串类型测试异步查询类V4继承AbstractAsyncQuery
/// </summary>
public sealed class TestAsyncStringQueryV4 : AbstractAsyncQuery<TestAsyncQueryInputV2, string>
{
/// <summary>
/// 初始化TestAsyncStringQueryV4的新实例
/// </summary>
/// <param name="input">查询输入参数</param>
public TestAsyncStringQueryV4(TestAsyncQueryInputV2 input) : base(input)
{
}
/// <summary>
/// 获取查询是否已执行
/// </summary>
public bool Executed { get; private set; }
/// <summary>
/// 执行异步查询操作的具体实现
/// </summary>
/// <param name="input">查询输入参数</param>
/// <returns>格式化的字符串结果</returns>
protected override Task<string> OnDoAsync(TestAsyncQueryInputV2 input)
{
Executed = true;
return Task.FromResult($"Value: {input.Value * 2}");
}
}

View File

@ -1,4 +1,6 @@
using System.Diagnostics;
using System.Globalization;
using System.Threading;
using GFramework.Core.Abstractions.Events;
using GFramework.Game.Abstractions.Config;
using YamlDotNet.Serialization;
@ -472,93 +474,159 @@ public sealed class YamlConfigLoader : IConfigLoader
IDeserializer deserializer,
CancellationToken cancellationToken)
{
var directoryPath = Path.Combine(rootPath, RelativePath);
if (!Directory.Exists(directoryPath))
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.ConfigDirectoryNotFound,
Name,
$"Config directory '{directoryPath}' was not found for table '{Name}'.",
configDirectoryPath: directoryPath);
}
YamlConfigSchema? schema = null;
IReadOnlyCollection<string> referencedTableNames = Array.Empty<string>();
if (!string.IsNullOrEmpty(SchemaRelativePath))
{
var schemaPath = Path.Combine(rootPath, SchemaRelativePath);
schema = await YamlConfigSchemaValidator.LoadAsync(Name, schemaPath, cancellationToken)
.ConfigureAwait(false);
referencedTableNames = schema.ReferencedTableNames;
}
var directoryPath = GetValidatedDirectoryPath(rootPath);
var schema = await LoadSchemaAsync(rootPath, cancellationToken).ConfigureAwait(false);
var referenceUsages = new List<YamlConfigReferenceUsage>();
var values = await LoadValuesAsync(
directoryPath,
deserializer,
schema,
referenceUsages,
cancellationToken)
.ConfigureAwait(false);
return BuildLoadResult(directoryPath, schema, values, referenceUsages);
}
private string GetValidatedDirectoryPath(string rootPath)
{
var directoryPath = Path.Combine(rootPath, RelativePath);
if (Directory.Exists(directoryPath))
{
return directoryPath;
}
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.ConfigDirectoryNotFound,
Name,
$"Config directory '{directoryPath}' was not found for table '{Name}'.",
configDirectoryPath: directoryPath);
}
private async Task<YamlConfigSchema?> LoadSchemaAsync(string rootPath, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(SchemaRelativePath))
{
return null;
}
var schemaPath = Path.Combine(rootPath, SchemaRelativePath);
return await YamlConfigSchemaValidator.LoadAsync(Name, schemaPath, cancellationToken).ConfigureAwait(false);
}
private async Task<List<TValue>> LoadValuesAsync(
string directoryPath,
IDeserializer deserializer,
YamlConfigSchema? schema,
List<YamlConfigReferenceUsage> referenceUsages,
CancellationToken cancellationToken)
{
var values = new List<TValue>();
var files = Directory
foreach (var file in GetYamlFiles(directoryPath))
{
cancellationToken.ThrowIfCancellationRequested();
var yaml = await ReadYamlAsync(directoryPath, file, schema, cancellationToken).ConfigureAwait(false);
CollectReferenceUsages(referenceUsages, schema, file, yaml);
values.Add(DeserializeValue(deserializer, directoryPath, file, schema, yaml));
}
return values;
}
private static string[] GetYamlFiles(string directoryPath)
{
return Directory
.EnumerateFiles(directoryPath, "*.*", SearchOption.TopDirectoryOnly)
.Where(static path =>
path.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase) ||
path.EndsWith(".yml", StringComparison.OrdinalIgnoreCase))
.OrderBy(static path => path, StringComparer.Ordinal)
.ToArray();
}
foreach (var file in files)
private async Task<string> ReadYamlAsync(
string directoryPath,
string file,
YamlConfigSchema? schema,
CancellationToken cancellationToken)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
return await File.ReadAllTextAsync(file, cancellationToken).ConfigureAwait(false);
}
catch (Exception exception)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.ConfigFileReadFailed,
Name,
$"Failed to read config file '{file}' for table '{Name}'.",
configDirectoryPath: directoryPath,
yamlPath: file,
schemaPath: schema?.SchemaPath,
innerException: exception);
}
}
string yaml;
try
{
yaml = await File.ReadAllTextAsync(file, cancellationToken).ConfigureAwait(false);
}
catch (Exception exception)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.ConfigFileReadFailed,
Name,
$"Failed to read config file '{file}' for table '{Name}'.",
configDirectoryPath: directoryPath,
yamlPath: file,
schemaPath: schema?.SchemaPath,
innerException: exception);
}
if (schema != null)
{
// 先按 schema 拒绝结构问题并提取跨表引用,避免被 IgnoreUnmatchedProperties 或默认值掩盖配置错误。
referenceUsages.AddRange(
YamlConfigSchemaValidator.ValidateAndCollectReferences(Name, schema, file, yaml));
}
try
{
var value = deserializer.Deserialize<TValue>(yaml);
if (value == null)
{
throw new InvalidOperationException("YAML content was deserialized to null.");
}
values.Add(value);
}
catch (Exception exception)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.DeserializationFailed,
Name,
$"Failed to deserialize config file '{file}' for table '{Name}' as '{typeof(TValue).Name}'.",
configDirectoryPath: directoryPath,
yamlPath: file,
schemaPath: schema?.SchemaPath,
detail: $"Target CLR type: {typeof(TValue).FullName}.",
innerException: exception);
}
private void CollectReferenceUsages(
List<YamlConfigReferenceUsage> referenceUsages,
YamlConfigSchema? schema,
string file,
string yaml)
{
if (schema == null)
{
return;
}
// 先按 schema 拒绝结构问题并提取跨表引用,避免被 IgnoreUnmatchedProperties 或默认值掩盖配置错误。
referenceUsages.AddRange(
YamlConfigSchemaValidator.ValidateAndCollectReferences(Name, schema, file, yaml));
}
private TValue DeserializeValue(
IDeserializer deserializer,
string directoryPath,
string file,
YamlConfigSchema? schema,
string yaml)
{
try
{
var value = deserializer.Deserialize<TValue>(yaml);
if (value != null)
{
return value;
}
throw new InvalidOperationException("YAML content was deserialized to null.");
}
catch (Exception exception)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.DeserializationFailed,
Name,
$"Failed to deserialize config file '{file}' for table '{Name}' as '{typeof(TValue).Name}'.",
configDirectoryPath: directoryPath,
yamlPath: file,
schemaPath: schema?.SchemaPath,
detail: $"Target CLR type: {typeof(TValue).FullName}.",
innerException: exception);
}
}
private YamlTableLoadResult BuildLoadResult(
string directoryPath,
YamlConfigSchema? schema,
List<TValue> values,
List<YamlConfigReferenceUsage> referenceUsages)
{
try
{
var table = new InMemoryConfigTable<TKey, TValue>(values, _keySelector, _comparer);
return new YamlTableLoadResult(Name, table, referencedTableNames, referenceUsages);
return new YamlTableLoadResult(
Name,
table,
schema?.ReferencedTableNames ?? Array.Empty<string>(),
referenceUsages);
}
catch (Exception exception)
{
@ -630,6 +698,12 @@ public sealed class YamlConfigLoader : IConfigLoader
/// </summary>
private static class CrossTableReferenceValidator
{
private delegate bool IntegerTryParseDelegate<T>(
string value,
NumberStyles style,
IFormatProvider? provider,
out T result);
/// <summary>
/// 使用本轮新加载结果与注册表中保留的旧表,一起验证跨表引用是否全部有效。
/// </summary>
@ -754,59 +828,15 @@ public sealed class YamlConfigLoader : IConfigLoader
convertedKey = null;
errorMessage = string.Empty;
if (targetKeyType == typeof(int) &&
int.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
if (TryConvertIntegerKey<int>(rawValue, targetKeyType, typeof(int), int.TryParse, out convertedKey) ||
TryConvertIntegerKey<long>(rawValue, targetKeyType, typeof(long), long.TryParse, out convertedKey) ||
TryConvertIntegerKey<short>(rawValue, targetKeyType, typeof(short), short.TryParse, out convertedKey) ||
TryConvertIntegerKey<byte>(rawValue, targetKeyType, typeof(byte), byte.TryParse, out convertedKey) ||
TryConvertIntegerKey<uint>(rawValue, targetKeyType, typeof(uint), uint.TryParse, out convertedKey) ||
TryConvertIntegerKey<ulong>(rawValue, targetKeyType, typeof(ulong), ulong.TryParse, out convertedKey) ||
TryConvertIntegerKey<ushort>(rawValue, targetKeyType, typeof(ushort), ushort.TryParse, out convertedKey) ||
TryConvertIntegerKey<sbyte>(rawValue, targetKeyType, typeof(sbyte), sbyte.TryParse, out convertedKey))
{
convertedKey = intValue;
return true;
}
if (targetKeyType == typeof(long) &&
long.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var longValue))
{
convertedKey = longValue;
return true;
}
if (targetKeyType == typeof(short) &&
short.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var shortValue))
{
convertedKey = shortValue;
return true;
}
if (targetKeyType == typeof(byte) &&
byte.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var byteValue))
{
convertedKey = byteValue;
return true;
}
if (targetKeyType == typeof(uint) &&
uint.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var uintValue))
{
convertedKey = uintValue;
return true;
}
if (targetKeyType == typeof(ulong) &&
ulong.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ulongValue))
{
convertedKey = ulongValue;
return true;
}
if (targetKeyType == typeof(ushort) &&
ushort.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ushortValue))
{
convertedKey = ushortValue;
return true;
}
if (targetKeyType == typeof(sbyte) &&
sbyte.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var sbyteValue))
{
convertedKey = sbyteValue;
return true;
}
@ -815,6 +845,25 @@ public sealed class YamlConfigLoader : IConfigLoader
return false;
}
private static bool TryConvertIntegerKey<T>(
string rawValue,
Type targetKeyType,
Type supportedType,
IntegerTryParseDelegate<T> tryParse,
out object? convertedKey)
where T : struct
{
convertedKey = null;
if (targetKeyType != supportedType ||
!tryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedValue))
{
return false;
}
convertedKey = parsedValue;
return true;
}
private static bool ContainsKey(IConfigTable table, object key)
{
var tableInterface = table.GetType()
@ -838,7 +887,13 @@ public sealed class YamlConfigLoader : IConfigLoader
new(StringComparer.Ordinal);
private readonly IDeserializer _deserializer;
#if NET9_0_OR_GREATER
// net9.0 及以上目标使用专用 Lock以满足分析器对专用同步原语的建议。
private readonly Lock _gate = new();
#else
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
private readonly object _gate = new();
#endif
private readonly Action<string>? _onTableReloaded;
private readonly Action<string, Exception>? _onTableReloadFailed;
private readonly Dictionary<string, IYamlTableRegistration> _registrations = new(StringComparer.Ordinal);
@ -1121,7 +1176,7 @@ public sealed class YamlConfigLoader : IConfigLoader
foreach (var dependency in _dependenciesByTable)
{
if (!dependency.Value.Contains(currentTableName))
if (!ContainsDependency(dependency.Value, currentTableName))
{
continue;
}
@ -1138,6 +1193,14 @@ public sealed class YamlConfigLoader : IConfigLoader
.ToArray();
}
private static bool ContainsDependency(
IReadOnlyCollection<string> dependencies,
string tableName)
{
return dependencies.Any(
dependency => string.Equals(dependency, tableName, StringComparison.Ordinal));
}
private void InvokeReloaded(string tableName)
{
if (_onTableReloaded == null)

View File

@ -6,54 +6,48 @@
## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-081`
- 当前阶段:`Phase 81`
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-083`
- 当前阶段:`Phase 83`
- 当前焦点:
- `2026-04-27` 已复核 PR `#295` 的 latest-head review确认 `ThrowShouldNotRetry``ParamName` open thread 属于 stale finding本地代码已经使用传入值而非 `nameof(parameterName)`
- 已清理 `AsyncExtensionsTests.WithRetry_Should_Respect_ShouldRetry_Predicate` 中的冗余 `Task.Delay(50)`,保留 `ParamName == nameof(taskFactory)` 断言锁定契约
- 已增强 `.agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py` 的 failed-test 表格解析,允许 `Name` / `Failure Message` 后出现尾随额外列
- 已新增 Python `unittest` 回归用例覆盖“尾随额外列不影响前两列提取”的场景
- 当前剩余 warning 热点仍集中在 `YamlConfigSchemaValidator*`、`YamlConfigLoader.cs` 与大批量 `MA0048` 文件名拆分;这些 slice 仍高于本轮 PR review follow-up 的低风险边界
- `2026-04-27` 主线程已修复 `GFramework.Game/Config/YamlConfigLoader.cs``MA0051``MA0002``MA0158`,当前非增量仓库根构建已不再报告该文件 warning
- 并行 worker 已将 `GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs` 末尾的 `10` 个测试辅助接口/类拆分到 `Ioc/` 同目录独立文件
- 已接受第二波 worker 的已落地结果:`GFramework.Core.Tests/Query/AbstractAsyncQueryTests.cs` 末尾辅助类型已拆分到 `Query/` 同目录独立文件
- 最新 non-incremental 仓库根基线已从 `397` 条 warning / `316` 个唯一位点降到 `353` 条 warning / `279` 个唯一位点
- 当前剩余 warning 热点仍集中在 `GFramework.Cqrs.Tests/Mediator/*` 的大体量 `MA0048`、以及 `YamlConfigSchemaValidator*` 等高耦合 slice
## 当前活跃事实
- 当前 `origin/main` 基线提交为 `617e0bf``2026-04-26T12:17:15+08:00`)。
- 当前 PR review 真值:
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output <current-pr-review-json>`
- 最新结果:成功;当前分支对应 PR 为 `#295`
- 当前测试报告输出已能显示 `Summary` 统计、失败测试名称,以及 `Name / Failure Message` 表格中的关键信息
- 当前 GitHub latest-head review 仍显示 `1` 条 open thread但该线程指向的 `nameof(parameterName)` 问题已不在本地代码中成立,属于 stale finding
- 当前 latest review 中仍有 `2` 条与本地工作树一致的 nitpick`AsyncExtensionsTests` 冗余等待,以及 failed-test 表格解析对尾随列不鲁棒
- 当前 `origin/main` 基线提交为 `b6a9fef``2026-04-27T10:53:34+08:00`)。
- 当前直接验证结果:
- `python3 .agents/skills/gframework-pr-review/scripts/test_fetch_current_pr_review.py`
- 最新结果:成功;`Ran 1 test in 0.000s`, `OK`
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --section tests --json-output /tmp/current-pr-review-postfix.json`
- 最新结果:成功;真实 PR 评论抓取仍能输出 `2` 份测试报告,失败用例详情保持可见
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~WithRetry_Should_Respect_ShouldRetry_Predicate"`
- 最新结果:成功;`Failed: 0, Passed: 1, Skipped: 0, Total: 1`
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~RegisterMigration_During_Cache_Rebuild_Should_Not_Leave_Stale_Type_Cache"`
- 最新结果:成功;`Failed: 0, Passed: 1, Skipped: 0, Total: 1`
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
- 最新结果:成功;`111 Warning(s)``0 Error(s)`,其中不再包含 `GFramework.Game/Config/YamlConfigLoader.cs` 的 warning
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
- 最新结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet clean`
- 最新结果:成功;为本轮最终 warning 基线刷新提供非增量起点
- `dotnet build`
- 最新结果:成功;`353 Warning(s)``0 Error(s)`,唯一 warning 位点 `279`
- 当前构建输出已不再包含 `GFramework.Game/Config/YamlConfigLoader.cs``GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs``GFramework.Core.Tests/Query/AbstractAsyncQueryTests.cs`
- 当前分支 stop-condition 指标:
- `git diff --name-only refs/remotes/origin/main...HEAD | wc -l`
- 最新结果:`35`
- `git diff --numstat refs/remotes/origin/main...HEAD`
- 最新结果:`642` changed lines
- 当前待提交工作树 footprint
- 最新结果:`22` changed files距离 `$gframework-batch-boot 50` 的停止线仍有余量
- 当前批次摘要:
- 三轮低风险 warning 清理已在此前验证中将仓库根 warning 从 `639` 降到 `397`
- 当前批次的已完成 slice 明细已迁移到归档active todo 仅保留恢复真值
- 本轮新增内容为 PR review nitpick 收口与脚本回归测试补齐,不扩展 warning reduction 的热点清理边界
- 本轮完成 `YamlConfigLoader.cs` 的单文件 warning 清理,并通过受影响模块 Release 构建验证
- 本轮完成 `MicrosoftDiContainerTests.cs` 的 ownership-bounded `MA0048` 拆分 slice新增 `10` 个同目录辅助类型文件并保持测试语义不变
- 本轮还完成 `AbstractAsyncQueryTests.cs``MA0048` 拆分 slice新增 `7` 个同目录辅助类型文件并保持测试语义不变
- 本轮 non-incremental 仓库根 warning 真值从 `397` 降到 `353`,减少 `44` 条;唯一位点从 `316` 降到 `279`,减少 `37`
- 已尝试为 `ArchitectureContextTests.cs` 启动下一波 subagent但在共享工作树落地前已停止不计入本轮已完成事实
- 当前建议保留到下一波次的候选:
- `GFramework.Game/Config/YamlConfigLoader.cs``MA0158`(单点可修,但文件本身同时承载其他高耦合 warning
- 测试项目中的 `MA0048` 文件名拆分波次(会显著增加 changed-file 数)
- `GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs``7``MA0048`
- `GFramework.Core.Tests/Query/AsyncQueryExecutorTests.cs``7``MA0048`
- `GFramework.Game/Config/YamlConfigSchemaValidator.cs``YamlConfigSchemaValidator.ObjectKeywords.cs` 的高耦合 warning 热点
## 当前风险
- `GFramework.Game/Config/YamlConfigSchemaValidator*.cs` 仍然聚集多类高耦合 warning。
- 缓解措施:本轮先避开该热点,只清理低风险且 ownership 清晰的文件集合。
- `MA0158` 迁移涉及 `net8.0` / `net9.0` / `net10.0` 多目标兼容。
- 缓解措施:复用 `StoreSelection.cs` 已存在的 `#if NET9_0_OR_GREATER` 专用锁模式,不在 `net8.0` 引入不兼容 API。
- 当前 PR open thread 与 CI 失败信号仍依赖新提交进入远端 PR head 才能复核。
- 缓解措施:本轮提交并推送后重新执行 `$gframework-pr-review`,确认 stale open thread 是否被 GitHub 收口,以及两条 nitpick 是否从 latest review 中消失。
- `GFramework.Cqrs.Tests/Mediator/*` 仍有 `47` / `44` / `34` 个唯一 warning 位点,属于高 changed-file 风险的 `MA0048` 大波次。
- 缓解措施:优先继续处理 `6-7` 个 warning 的小文件切片,避免一次性推高文件数。
- `YamlConfigSchemaValidator*` 仍然聚集多类高耦合 warning。
- 缓解措施:继续把它们留在独立波次,不与测试项目的低风险拆分混提。
## 活跃文档
@ -73,11 +67,11 @@
## 验证说明
- 权威验证结果统一维护在“当前活跃事实”。
- `GFramework.Core.Tests` 当前仍有既有 analyzer / nullable warning 基线,因此本轮验证只证明 PR review 修复未引入构建错误,未将该项目 warning 清零
- 后续若刷新构建或 PR review 真值,只更新上述权威区块,不在本节重复抄录
- `GFramework.Core.Tests` 项目级 Release 构建已在本轮清零,但仓库根 non-incremental 构建仍保留大量既有 warning
- warning reduction 的仓库级真值只以同轮 `dotnet clean` 后的 `dotnet build` 为准
## 下一步建议
1. 提交本轮 `AsyncExtensionsTests` / `$gframework-pr-review` nitpick 修复、Python 回归测试与 `ai-plan` 同步。
2. 推送后重新执行 `$gframework-pr-review`,确认 PR `#295` 的 stale open thread、nitpick 与测试报告是否已刷新为新 head 真值
3. 若后续继续推进 warning reduction建议另开下一波次处理 `YamlConfigLoader.cs` 热点或测试项目 `MA0048` 拆分波次
1. 提交本轮 `YamlConfigLoader.cs`、`MicrosoftDiContainerTests.cs``AbstractAsyncQueryTests.cs` 的 warning reduction 结果及 `ai-plan` 同步。
2. 下一波优先挑选 `ArchitectureContextTests.cs``AsyncQueryExecutorTests.cs` 这类 `7`-warning 的纯 `MA0048` 单文件切片
3. 继续将 `YamlConfigSchemaValidator*``GFramework.Cqrs.Tests/Mediator/*` 作为独立高风险波次处理

View File

@ -1,39 +1,45 @@
# Analyzer Warning Reduction 追踪
## 2026-04-27 — RP-081
## 2026-04-27 — RP-083
### 阶段:核实 PR `#295` 的剩余 nitpick并补齐脚本解析回归测试
### 阶段:修复 `YamlConfigLoader` 单文件 warning并拆分 `MicrosoftDiContainerTests` 的辅助类型
- 触发背景:
- 用户再次执行 `$gframework-pr-review`,需要根据当前 PR `#295` 的 latest-head review 继续核实哪些反馈仍需在本地处理
- 远端 review 显示 `1` 条 open thread 与 `2` 条 nitpick需要区分 stale finding 与仍然成立的本地问题
- 用户执行 `$gframework-batch-boot 50`,要求先拿仓库根构建 warning再按 bounded slice 分派给不同 subagent 并持续推进
- 当前分支在本轮开始时与 `origin/main@b6a9fef` 零提交差异,适合从低风险 warning slice 起步
- 主线程实施:
- 复核 `/tmp/current-pr-review.json` 与本地 `AsyncExtensionsTests.cs`,确认 open thread 指向的 `nameof(parameterName)` 问题已在现有代码中修复,属于 stale finding
- 删除 `GFramework.Core.Tests/Extensions/AsyncExtensionsTests.cs``WithRetry_Should_Respect_ShouldRetry_Predicate` 的冗余 `Task.Delay(50)`,将测试改回同步断言路径
- 调整 `.agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py``parse_failed_test_details`,允许 failed-test HTML 表格在 `Name` / `Failure Message` 后追加额外列
- 新增 `.agents/skills/gframework-pr-review/scripts/test_fetch_current_pr_review.py`,以 `unittest` 覆盖“尾随额外列不影响前两列提取”的回归场景
- 先执行 non-incremental 仓库根基线:`dotnet clean` + `dotnet build`,得到 `397 Warning(s)` / `316` 个唯一位点
- 主线程修复 `GFramework.Game/Config/YamlConfigLoader.cs``MA0051``MA0002``MA0158`
- 接受一个 worker batch`GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs` 末尾的 `10` 个测试辅助接口/类拆分到 `Ioc/` 同目录独立文件
- 接受第二波 worker 的已落地结果:将 `GFramework.Core.Tests/Query/AbstractAsyncQueryTests.cs` 末尾的 `7` 个测试辅助类型拆分到 `Query/` 同目录独立文件
- 启动 `ArchitectureContextTests.cs` 候选 worker但在共享工作树落地前主动停止以避免本轮上下文与 review 面积继续膨胀
- 验证里程碑:
- `python3 .agents/skills/gframework-pr-review/scripts/test_fetch_current_pr_review.py`
- 结果:成功;`Ran 1 test in 0.000s`, `OK`
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --section tests --json-output /tmp/current-pr-review-postfix.json`
- 结果:成功;真实 PR 评论抓取仍显示 `2` 份测试报告,失败测试名与 failure message 摘要保持可见
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~WithRetry_Should_Respect_ShouldRetry_Predicate"`
- 结果:成功;`Failed: 0, Passed: 1, Skipped: 0, Total: 1`
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
- 结果:成功;`111 Warning(s)``0 Error(s)`
- 观察:构建输出未再报告 `GFramework.Game/Config/YamlConfigLoader.cs`
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet clean`
- 结果:成功;刷新最终 non-incremental 仓库根 warning 基线
- `dotnet build`
- 结果:成功;`353 Warning(s)``0 Error(s)`,唯一位点 `279`
- 观察:构建输出未再报告 `GFramework.Game/Config/YamlConfigLoader.cs``GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs``GFramework.Core.Tests/Query/AbstractAsyncQueryTests.cs`
- 当前结论:
- 本轮 latest-head review 中只有 `AsyncExtensionsTests` 冗余等待与 failed-test 表格尾随列容错性两个 nitpick 仍与本地代码一致,现已修复
- `ThrowShouldNotRetry``ParamName` open thread 属于 stale finding本地代码已经符合预期只需等待新提交进入远端后复核 thread 状态
- 本轮已完成一个主线程单文件 slice 和两个 worker 拆分 slice仓库根 non-incremental warning 从 `397` 降到 `353`
- 当前共享工作树 footprint 为 `22` 个 changed files仍低于 `$gframework-batch-boot 50` 的停止线
- 下一波更适合继续处理 `7``MA0048` 的小文件,而不是立即进入 `Mediator*``YamlConfigSchemaValidator*` 的高耦合热点
## 活跃风险
- PR 上的 latest-head review thread 与测试报告仍需要等新提交进入远端后再复核
- 缓解措施:提交并推送后重新执行 `$gframework-pr-review`,只以新的 latest-head 和 test report 为准
- `YamlConfigSchemaValidator*``YamlConfigLoader.cs``MA0048` 拆分仍是下一波次的高耦合候选
- 缓解措施:保持本轮边界只处理 PR review nitpick follow-up不顺手扩展 warning reduction 范围
- `GFramework.Cqrs.Tests/Mediator/*``MA0048` 位点密度很高,一次性拆分会迅速推高 changed-file 数
- 缓解措施:下一波优先继续拿 `7` warning 级别的小切片
- `YamlConfigSchemaValidator*` 仍然聚集多类高耦合 warning
- 缓解措施:继续维持为独立波次,不与测试项目拆分混提
## 下一步
1. 完成本轮提交。
2. 推送后重新执行 `$gframework-pr-review`,确认 PR `#295` 的 stale open thread 与 nitpick 是否已刷新
1. 完成本轮 `YamlConfigLoader.cs``MicrosoftDiContainerTests.cs``ai-plan`提交。
2. 下一波优先从 `ArchitectureContextTests.cs``AsyncQueryExecutorTests.cs` 继续拆分纯 `MA0048`
## 历史归档指针