fix(pr269): 收口剩余评审修复

- 修复 SchemaConfigGenerator 的根类型标识符校验与 comparer XML 文档转义\n- 补强 LoggingConfiguration 与 CollectionExtensions 的公共 API 兼容断言\n- 重构 Cqrs 运行时类型反射查找 helper,并更新 analyzer-warning-reduction 跟踪与验证记录
This commit is contained in:
GeWuYou 2026-04-22 16:27:30 +08:00 committed by gewuyou
parent 12f15961af
commit df68cdfd82
8 changed files with 200 additions and 39 deletions

View File

@ -174,9 +174,19 @@ public class CollectionExtensionsTests
var method = typeof(GFramework.Core.Extensions.CollectionExtensions)
.GetMethods()
.Single(static method => method.Name == nameof(GFramework.Core.Extensions.CollectionExtensions.ToDictionarySafe));
var methodGenericArguments = method.GetGenericArguments();
var returnTypeGenericArguments = method.ReturnType.GetGenericArguments();
Assert.That(method.ReturnType.IsGenericType, Is.True);
Assert.That(method.ReturnType.GetGenericTypeDefinition(), Is.EqualTo(typeof(Dictionary<,>)));
Assert.Multiple(() =>
{
Assert.That(method.IsGenericMethodDefinition, Is.True);
Assert.That(method.ReturnType.IsGenericType, Is.True);
Assert.That(method.ReturnType.GetGenericTypeDefinition(), Is.EqualTo(typeof(Dictionary<,>)));
Assert.That(methodGenericArguments.Select(static argument => argument.Name), Is.EqualTo(new[] { "T", "TKey", "TValue" }));
Assert.That(returnTypeGenericArguments, Has.Length.EqualTo(2));
Assert.That(returnTypeGenericArguments[0], Is.SameAs(methodGenericArguments[1]));
Assert.That(returnTypeGenericArguments[1], Is.SameAs(methodGenericArguments[2]));
});
}
/// <summary>

View File

@ -65,7 +65,12 @@ public class LoggingConfigurationTests
var config = new LoggingConfiguration();
config.LoggerLevels["GFramework.Core"] = LogLevel.Info;
Assert.That(config.LoggerLevels.ContainsKey("gframework.core"), Is.False);
Assert.Multiple(() =>
{
Assert.That(config.LoggerLevels.ContainsKey("GFramework.Core"), Is.True);
Assert.That(config.LoggerLevels["GFramework.Core"], Is.EqualTo(LogLevel.Info));
Assert.That(config.LoggerLevels.ContainsKey("gframework.core"), Is.False);
});
}
[Test]

View File

@ -173,22 +173,11 @@ public sealed partial class CqrsHandlerRegistryGenerator
INamedTypeSymbol namedType,
out RuntimeTypeReferenceSpec? runtimeTypeReference)
{
if (SymbolEqualityComparer.Default.Equals(namedType.ContainingAssembly, compilation.Assembly))
{
runtimeTypeReference = RuntimeTypeReferenceSpec.FromReflectionLookup(GetReflectionTypeMetadataName(namedType));
return true;
}
if (namedType.ContainingAssembly is null)
{
runtimeTypeReference = null;
return false;
}
runtimeTypeReference = RuntimeTypeReferenceSpec.FromExternalReflectionLookup(
namedType.ContainingAssembly.Identity.ToString(),
GetReflectionTypeMetadataName(namedType));
return true;
return TryCreateReflectionLookupReference(
compilation,
namedType,
GetReflectionTypeMetadataName(namedType),
out runtimeTypeReference);
}
/// <summary>
@ -223,22 +212,42 @@ public sealed partial class CqrsHandlerRegistryGenerator
return true;
}
if (SymbolEqualityComparer.Default.Equals(genericTypeDefinition.ContainingAssembly, compilation.Assembly))
return TryCreateReflectionLookupReference(
compilation,
genericTypeDefinition,
GetReflectionTypeMetadataName(genericTypeDefinition),
out genericTypeDefinitionReference);
}
/// <summary>
/// 为当前程序集或外部程序集中的命名类型构造统一的运行时反射查找描述。
/// </summary>
/// <param name="compilation">当前生成轮次的编译上下文。</param>
/// <param name="namedType">需要在运行时解析的命名类型。</param>
/// <param name="metadataName">写入生成代码的反射元数据名称。</param>
/// <param name="runtimeTypeReference">成功时返回可直接写入注册器的运行时类型引用描述。</param>
/// <returns>当命名类型具备可稳定编码的程序集归属信息时返回 <see langword="true" />。</returns>
private static bool TryCreateReflectionLookupReference(
Compilation compilation,
INamedTypeSymbol namedType,
string metadataName,
out RuntimeTypeReferenceSpec? runtimeTypeReference)
{
if (SymbolEqualityComparer.Default.Equals(namedType.ContainingAssembly, compilation.Assembly))
{
genericTypeDefinitionReference = RuntimeTypeReferenceSpec.FromReflectionLookup(
GetReflectionTypeMetadataName(genericTypeDefinition));
runtimeTypeReference = RuntimeTypeReferenceSpec.FromReflectionLookup(metadataName);
return true;
}
if (genericTypeDefinition.ContainingAssembly is null)
if (namedType.ContainingAssembly is null)
{
genericTypeDefinitionReference = null;
runtimeTypeReference = null;
return false;
}
genericTypeDefinitionReference = RuntimeTypeReferenceSpec.FromExternalReflectionLookup(
genericTypeDefinition.ContainingAssembly.Identity.ToString(),
GetReflectionTypeMetadataName(genericTypeDefinition));
runtimeTypeReference = RuntimeTypeReferenceSpec.FromExternalReflectionLookup(
namedType.ContainingAssembly.Identity.ToString(),
metadataName);
return true;
}

View File

@ -176,8 +176,13 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
return SchemaParseResult.FromDiagnostic(diagnostic!);
}
var entityName = ToPascalCase(GetSchemaBaseName(filePath));
var rootObject = ParseObjectSpec(filePath, root, "<root>", $"{entityName}Config", isRoot: true);
var schemaBaseName = GetSchemaBaseName(filePath);
if (!TryBuildRootTypeIdentifiers(filePath, schemaBaseName, out var entityName, out var rootClassName, out diagnostic))
{
return SchemaParseResult.FromDiagnostic(diagnostic!);
}
var rootObject = ParseObjectSpec(filePath, root, "<root>", rootClassName, isRoot: true);
if (rootObject.Diagnostic is not null)
{
return SchemaParseResult.FromDiagnostic(rootObject.Diagnostic);
@ -190,7 +195,6 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
return SchemaParseResult.FromDiagnostic(diagnostic);
}
var schemaBaseName = GetSchemaBaseName(filePath);
var configRelativePath = ResolveConfigRelativePath(filePath, root, schemaBaseName);
if (configRelativePath.Diagnostic is not null)
{
@ -3110,9 +3114,11 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
for (var index = 0; index < schemas.Count; index++)
{
var schema = schemas[index];
var comparerTypeDocumentation = EscapeXmlDocumentation(
$"global::System.Collections.Generic.IEqualityComparer<{schema.KeyClrType}>?");
builder.AppendLine(" /// <summary>");
builder.AppendLine(
$" /// Gets or sets the optional key comparer forwarded to {schema.EntityName}ConfigBindings.Register{schema.EntityName}Table(global::GFramework.Game.Config.YamlConfigLoader, global::System.Collections.Generic.IEqualityComparer<{schema.KeyClrType}>?) when aggregate registration runs.");
$" /// Gets or sets the optional key comparer forwarded to {schema.EntityName}ConfigBindings.Register{schema.EntityName}Table using <c>{comparerTypeDocumentation}</c> when aggregate registration runs.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(
$" public global::System.Collections.Generic.IEqualityComparer<{schema.KeyClrType}>? {schema.EntityName}Comparer {{ get; init; }}");
@ -4045,6 +4051,41 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
return false;
}
/// <summary>
/// 将 schema 文件名派生的根实体名验证为生成代码可直接使用的根类型标识符。
/// 这里与属性名验证保持一致,避免文件名中的前导数字或其他非法字符把根配置类型/表类型生成为无效 C# 标识符。
/// </summary>
/// <param name="filePath">Schema 文件路径。</param>
/// <param name="schemaBaseName">去除扩展名后的 schema 基础名。</param>
/// <param name="entityName">验证后的实体名。</param>
/// <param name="rootClassName">验证后的根配置类型名。</param>
/// <param name="diagnostic">根类型名非法时生成的诊断。</param>
/// <returns>是否成功生成合法的根类型标识符。</returns>
private static bool TryBuildRootTypeIdentifiers(
string filePath,
string schemaBaseName,
out string entityName,
out string rootClassName,
out Diagnostic? diagnostic)
{
entityName = ToPascalCase(schemaBaseName);
rootClassName = $"{entityName}Config";
if (SyntaxFacts.IsValidIdentifier(rootClassName))
{
diagnostic = null;
return true;
}
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidGeneratedIdentifier,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
"<root>",
schemaBaseName,
rootClassName);
return false;
}
/// <summary>
/// 从 schema 文件路径提取实体基础名。
/// </summary>

View File

@ -72,6 +72,48 @@ public class SchemaConfigGeneratorTests
});
}
/// <summary>
/// 验证 schema 文件名若生成无效根类型标识符时,会在生成前产生命名明确的诊断。
/// </summary>
[Test]
public void Run_Should_Report_Diagnostic_When_Schema_File_Name_Generates_Invalid_Root_Type_Identifier()
{
const string source = """
namespace TestApp
{
public sealed class Dummy
{
}
}
""";
const string schema = """
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" }
}
}
""";
var result = SchemaGeneratorTestDriver.Run(
source,
("123-monster.schema.json", schema));
var diagnostic = result.Results.Single().Diagnostics.Single();
Assert.Multiple(() =>
{
Assert.That(diagnostic.Id, Is.EqualTo("GF_ConfigSchema_006"));
Assert.That(diagnostic.Severity, Is.EqualTo(DiagnosticSeverity.Error));
Assert.That(diagnostic.GetMessage(), Does.Contain("<root>"));
Assert.That(diagnostic.GetMessage(), Does.Contain("123-monster"));
Assert.That(diagnostic.GetMessage(), Does.Contain("123MonsterConfig"));
});
}
/// <summary>
/// 用于模拟 AdditionalFiles 读取阶段直接收到取消请求的测试桩。
/// </summary>
@ -2687,6 +2729,12 @@ public class SchemaConfigGeneratorTests
Assert.That(catalogSource,
Does.Contain(
"public global::System.Collections.Generic.IEqualityComparer<int>? MonsterComparer { get; init; }"));
Assert.That(catalogSource,
Does.Contain(
"using <c>global::System.Collections.Generic.IEqualityComparer&lt;string&gt;?</c> when aggregate registration runs."));
Assert.That(catalogSource,
Does.Contain(
"using <c>global::System.Collections.Generic.IEqualityComparer&lt;int&gt;?</c> when aggregate registration runs."));
Assert.That(catalogSource, Does.Contain("return RegisterAllGeneratedConfigTables(loader, options: null);"));
Assert.That(catalogSource, Does.Contain("GeneratedConfigRegistrationOptions? options"));
Assert.That(catalogSource, Does.Contain("loader.RegisterItemTable(effectiveOptions.ItemComparer);"));

View File

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

View File

@ -7,8 +7,8 @@
## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-022`
- 当前阶段:`Phase 22`
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-023`
- 当前阶段:`Phase 23`
- 当前焦点:
- 已完成 `GFramework.Core` 当前 `MA0016` / `MA0002` / `MA0015` / `MA0077` 低风险收口批次
- 已复核 `net10.0` 下的 `MA0158` 基线:`GFramework.Core` / `GFramework.Cqrs` 当前共有 `16` 个 object lock
@ -23,6 +23,8 @@
`ContextAwareGenerator` 已补上字段名去冲突与锁内读取修正,`Option<T>` 补齐 `<remarks>` 契约说明
- 已完成当前 PR #269 第二轮 follow-up恢复 `EasyEvents``CollectionExtensions``LoggingConfiguration`
`FilterConfiguration` 的公共 API 兼容形状,并将 analyzer 兼容性处理收敛到局部 pragma
- 已完成当前 PR #269 第三轮 follow-up继续收口 `SchemaConfigGenerator` 的根类型标识符校验与 XML 文档转义,
并补齐 `LoggingConfigurationTests``CollectionExtensionsTests``Cqrs` helper 抽取与 `ai-plan` 命令文本修正
- `CoroutineScheduler` 的 tag/group 字典已显式使用 `StringComparer.Ordinal`,保持既有区分大小写语义
- `EasyEvents.AddEvent<T>()` 的重复注册路径已恢复为 `ArgumentException`,以保持既有异常契约
- `Option<T>` 已声明 `IEquatable<Option<T>>`,与已有强类型 `Equals(Option<T>)` 契约对齐
@ -105,6 +107,9 @@
- `RP-022` 继续复核 PR #269 的 latest-head review threads 与 nitpick确认仍成立的项包括公共 API 兼容回退、
`ContextAwareGenerator` 字段名真正去冲突与锁内读取、`SchemaConfigGenerator` 取消传播、`Cqrs` 运行时类型 null 防御;
已补齐对应回归测试与 focused build/test 验证
- `RP-023` 继续复核 PR #269 剩余 nitpick/outside-diff 项,确认仍成立的项集中在 `SchemaConfigGenerator` 根类型名校验、
aggregate registration comparer XML 文档转义、logging / collection 反射测试补强,以及跟踪文档中的
`RestoreFallbackFolders=""` 可复制性问题
- 当前工作树分支 `fix/analyzer-warning-reduction-batch` 已在 `ai-plan/public/README.md` 建立 topic 映射
## 当前风险
@ -248,16 +253,26 @@
`CqrsHandlerRegistryGeneratorTests=14 Passed`
- 说明:失败来自测试宿主并行写入同一 build 输出,不是代码回归;串行重跑后快照新增的字段重名场景和 CQRS 快照均通过
- `RP-022` 的验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders=\"\" -v minimal`
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:通过;`EasyEvents``CollectionExtensions` 与 logging 配置模型的兼容性回退未引入新的 `net8.0` 构建错误
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders=\"\" -v minimal`
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -v minimal`
- 结果:通过;`ContainingAssembly` null 防御与发射 helper 精简未引入新的构建错误
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders=\"\" -v minimal`
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -v minimal`
- 结果:通过;仍保留既有 `9``SchemaConfigGenerator.cs` `MA0051` warning非本轮新增
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~SchemaConfigGeneratorTests|FullyQualifiedName~ContextAwareGeneratorSnapshotTests|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests" -m:1 -p:RestoreFallbackFolders=\"\" -v minimal`
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~SchemaConfigGeneratorTests|FullyQualifiedName~ContextAwareGeneratorSnapshotTests|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
- 结果:`63 Passed``0 Failed`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~EasyEventsTests|FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~LoggingConfigurationTests|FullyQualifiedName~ConfigurableLoggerFactoryTests" -m:1 -p:RestoreFallbackFolders=\"\" -v minimal`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~EasyEventsTests|FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~LoggingConfigurationTests|FullyQualifiedName~ConfigurableLoggerFactoryTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
- 结果:`38 Passed``0 Failed`
- `RP-023` 的验证结果:
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -v minimal`
- 结果:通过;仍保留既有 `9``SchemaConfigGenerator.cs` `MA0051` warning未新增新的 generator warning
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -v minimal`
- 结果:通过;并行构建时 `GFramework.SourceGenerators.Common.dll` 复制阶段出现一次 `MSB3026` 重试,随后成功完成
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~SchemaConfigGeneratorTests|FullyQualifiedName~SchemaConfigGeneratorSnapshotTests|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
- 结果:`63 Passed``0 Failed`
- 说明:测试项目构建仍会显示既有 `GFramework.SourceGenerators.Tests` analyzer warning不属于本轮写集
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~LoggingConfigurationTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
- 结果:`27 Passed``0 Failed`
- active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史
## 下一步

View File

@ -1,5 +1,38 @@
# Analyzer Warning Reduction 追踪
## 2026-04-22 — RP-023
### 阶段PR #269 第三轮 review follow-up 收口RP-023
- 启动复核:
- 延续 `$gframework-pr-review` 对 PR #269 的 latest-head unresolved threads、outside-diff comment 与 nitpick comment
- 本地核实后确认剩余仍成立的项集中在 `SchemaConfigGenerator` 根类型名校验、aggregate registration comparer XML 文档转义、
`LoggingConfigurationTests` / `CollectionExtensionsTests` 断言补强,以及 `ai-plan` 命令文本可复制性
- 决策:
- `SchemaConfigGenerator` 沿用现有 `InvalidGeneratedIdentifier` 诊断,不新增诊断 ID将根类型名校验收敛到独立 helper
让顶层 schema 文件名与属性名共享同一类安全边界
- aggregate registration comparer 文档直接复用现有 `EscapeXmlDocumentation(...)`,避免在 `///` 注释里再次写入原始泛型尖括号
- `CqrsHandlerRegistryGenerator` 的重复反射查找分支采用小 helper 抽取,不改变 fallback 语义和快照输出
- 实施调整:
- 为 `SchemaConfigGenerator` 新增 `TryBuildRootTypeIdentifiers(...)`,在进入 `ParseObjectSpec(...)` 前拦截非法根类型名
- 调整 aggregate registration comparer 属性的 XML 文档,使用 `<c>...</c>` 包裹并转义泛型类型文本
- 为 `SchemaConfigGeneratorTests` 增加非法 schema 文件名诊断回归,并补强 generated catalog 中 comparer 文档断言
- 为 `LoggingConfigurationTests` 增加正向键存在和值断言,为 `CollectionExtensionsTests` 补齐返回类型泛型参数绑定断言
- 为 `CqrsHandlerRegistryGenerator.RuntimeTypeReferences` 抽取共享反射查找 helper并修正 active tracking 中的转义引号
- 验证结果:
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -v minimal`
- 结果:通过;仍保留既有 `9``SchemaConfigGenerator.cs` `MA0051`
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -v minimal`
- 结果:通过;并行构建时出现一次 `MSB3026` 文件占用重试,自动恢复后完成
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~SchemaConfigGeneratorTests|FullyQualifiedName~SchemaConfigGeneratorSnapshotTests|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
- 结果:`63 Passed``0 Failed`
- 说明:测试项目构建仍打印既有 source-generator-tests analyzer warning不属于本轮写集
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~LoggingConfigurationTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
- 结果:`27 Passed``0 Failed`
- 下一步建议:
- 若本轮验证通过,继续回到 `SchemaConfigGenerator.cs` 剩余 `MA0051`
- 若 PR #269 仍有未关闭 review thread再按“先本地复核、再最小修复”的节奏收口
## 2026-04-22 — RP-022
### 阶段PR #269 第二轮 review follow-up 收口RP-022