fix(core-source-generators): 修复ContextAware继承成员命名冲突

- 修复 ContextAwareGenerator 的保留名收集逻辑,使 _gFrameworkContextAware* 字段分配覆盖完整基类链

- 新增 inherited collision 快照回归测试与快照文件,锁定基类占用字段名时的后缀回退行为

- 更新 analyzer-warning-reduction 跟踪与 trace,记录本轮 Greptile follow-up 与验证结果
This commit is contained in:
gewuyou 2026-04-23 09:31:31 +08:00
parent 2fc8442bd4
commit ba3a4d4a37
5 changed files with 310 additions and 139 deletions

View File

@ -353,11 +353,7 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
/// <returns>当前生成轮次应使用的上下文字段名集合。</returns>
private static GeneratedContextMemberNames CreateGeneratedContextMemberNames(INamedTypeSymbol symbol)
{
var reservedNames = new HashSet<string>(
symbol.GetMembers()
.Where(static member => !member.IsImplicitlyDeclared)
.Select(static member => member.Name),
StringComparer.Ordinal);
var reservedNames = CollectReservedContextMemberNames(symbol);
return new GeneratedContextMemberNames(
AllocateGeneratedMemberName(reservedNames, "_gFrameworkContextAwareContext"),
@ -365,6 +361,30 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
AllocateGeneratedMemberName(reservedNames, "_gFrameworkContextAwareSync"));
}
/// <summary>
/// 收集当前类型及其基类链上所有显式声明的成员名,确保生成字段不会意外隐藏继承成员。
/// </summary>
/// <param name="symbol">当前需要补充 ContextAware 实现的目标类型。</param>
/// <returns>已被当前类型层级占用的成员名集合。</returns>
private static HashSet<string> CollectReservedContextMemberNames(INamedTypeSymbol symbol)
{
var reservedNames = new HashSet<string>(StringComparer.Ordinal);
// Walk the full inheritance chain so numeric suffix allocation also covers members introduced by base types.
for (var currentType = symbol; currentType is not null; currentType = currentType.BaseType)
{
foreach (var member in currentType.GetMembers())
{
if (!member.IsImplicitlyDeclared)
{
reservedNames.Add(member.Name);
}
}
}
return reservedNames;
}
/// <summary>
/// 在固定前缀基础上按顺序追加数字后缀,直到找到可安全写入的成员名。
/// </summary>

View File

@ -1,4 +1,6 @@
using System.IO;
using System;
using System.IO;
using System.Linq;
using GFramework.Core.SourceGenerators.Rule;
using GFramework.SourceGenerators.Tests.Core;
@ -11,6 +13,60 @@ namespace GFramework.SourceGenerators.Tests.Rule;
[TestFixture]
public class ContextAwareGeneratorSnapshotTests
{
private const string SharedContextAwareInfrastructure = """
using System;
namespace GFramework.Core.SourceGenerators.Abstractions.Rule
{
[AttributeUsage(AttributeTargets.Class)]
public sealed class ContextAwareAttribute : Attribute { }
}
namespace GFramework.Core.Abstractions.Rule
{
public interface IContextAware
{
void SetContext(
GFramework.Core.Abstractions.Architectures.IArchitectureContext context);
GFramework.Core.Abstractions.Architectures.IArchitectureContext GetContext();
}
}
namespace GFramework.Core.Abstractions.Architectures
{
public interface IArchitectureContext { }
public interface IArchitectureContextProvider
{
IArchitectureContext GetContext();
bool TryGetContext<T>(out T? context) where T : class, IArchitectureContext;
}
}
namespace GFramework.Core.Architectures
{
using GFramework.Core.Abstractions.Architectures;
public sealed class GameContextProvider : IArchitectureContextProvider
{
public IArchitectureContext GetContext() => null;
public bool TryGetContext<T>(out T? context) where T : class, IArchitectureContext
{
context = null;
return false;
}
}
""";
private const string GameContextHelperSource = """
public static class GameContext
{
public static IArchitectureContext GetFirstArchitectureContext() => null;
}
""";
/// <summary>
/// 测试ContextAwareGenerator源代码生成器的快照功能
/// 验证生成器对带有ContextAware特性的类的处理结果
@ -19,73 +75,16 @@ public class ContextAwareGeneratorSnapshotTests
[Test]
public async Task Snapshot_ContextAwareGenerator()
{
// 定义测试用的源代码包含ContextAware特性和相关接口定义
const string source = """
using System;
namespace GFramework.Core.SourceGenerators.Abstractions.Rule
{
[AttributeUsage(AttributeTargets.Class)]
public sealed class ContextAwareAttribute : Attribute { }
}
namespace GFramework.Core.Abstractions.Rule
{
public interface IContextAware
{
void SetContext(
GFramework.Core.Abstractions.Architectures.IArchitectureContext context);
GFramework.Core.Abstractions.Architectures.IArchitectureContext GetContext();
}
}
namespace GFramework.Core.Abstractions.Architectures
{
public interface IArchitectureContext { }
public interface IArchitectureContextProvider
{
IArchitectureContext GetContext();
bool TryGetContext<T>(out T? context) where T : class, IArchitectureContext;
}
}
namespace GFramework.Core.Architectures
{
using GFramework.Core.Abstractions.Architectures;
public sealed class GameContextProvider : IArchitectureContextProvider
{
public IArchitectureContext GetContext() => null;
public bool TryGetContext<T>(out T? context) where T : class, IArchitectureContext
{
context = null;
return false;
}
}
public static class GameContext
{
public static IArchitectureContext GetFirstArchitectureContext() => null;
}
}
namespace TestApp
{
using GFramework.Core.SourceGenerators.Abstractions.Rule;
using GFramework.Core.Abstractions.Rule;
[ContextAware]
public partial class MyRule : IContextAware
{
}
}
""";
// 执行生成器快照测试,将生成的代码与预期快照进行比较
await GeneratorSnapshotTest<ContextAwareGenerator>.RunAsync(
source,
CreateContextAwareTestSource(
"""
[ContextAware]
public partial class MyRule : IContextAware
{
}
""",
includeGameContextHelper: true),
GetSnapshotFolder());
}
@ -96,76 +95,99 @@ public class ContextAwareGeneratorSnapshotTests
[Test]
public async Task Snapshot_ContextAwareGenerator_With_User_Field_Name_Collisions()
{
const string source = """
using System;
namespace GFramework.Core.SourceGenerators.Abstractions.Rule
{
[AttributeUsage(AttributeTargets.Class)]
public sealed class ContextAwareAttribute : Attribute { }
}
namespace GFramework.Core.Abstractions.Rule
{
public interface IContextAware
{
void SetContext(
GFramework.Core.Abstractions.Architectures.IArchitectureContext context);
GFramework.Core.Abstractions.Architectures.IArchitectureContext GetContext();
}
}
namespace GFramework.Core.Abstractions.Architectures
{
public interface IArchitectureContext { }
public interface IArchitectureContextProvider
{
IArchitectureContext GetContext();
bool TryGetContext<T>(out T? context) where T : class, IArchitectureContext;
}
}
namespace GFramework.Core.Architectures
{
using GFramework.Core.Abstractions.Architectures;
public sealed class GameContextProvider : IArchitectureContextProvider
{
public IArchitectureContext GetContext() => null;
public bool TryGetContext<T>(out T? context) where T : class, IArchitectureContext
{
context = null;
return false;
}
}
}
namespace TestApp
{
using GFramework.Core.SourceGenerators.Abstractions.Rule;
using GFramework.Core.Abstractions.Rule;
using GFramework.Core.Abstractions.Architectures;
[ContextAware]
public partial class CollisionProneRule : IContextAware
{
private readonly string _context = "user-field";
private static readonly string _contextProvider = "user-provider";
private static readonly object _contextSync = new();
private IArchitectureContext? _gFrameworkContextAwareContext;
private static IArchitectureContextProvider? _gFrameworkContextAwareProvider;
private static readonly object _gFrameworkContextAwareSync = new();
}
}
""";
await GeneratorSnapshotTest<ContextAwareGenerator>.RunAsync(
source,
CreateContextAwareTestSource(
"""
using GFramework.Core.Abstractions.Architectures;
[ContextAware]
public partial class CollisionProneRule : IContextAware
{
private readonly string _context = "user-field";
private static readonly string _contextProvider = "user-provider";
private static readonly object _contextSync = new();
private IArchitectureContext? _gFrameworkContextAwareContext;
private static IArchitectureContextProvider? _gFrameworkContextAwareProvider;
private static readonly object _gFrameworkContextAwareSync = new();
}
"""),
GetSnapshotFolder());
}
/// <summary>
/// 验证生成器在基类已经占用自动生成字段名时,也会为派生规则类型分配带后缀的唯一成员名。
/// </summary>
/// <returns>异步任务,无返回值。</returns>
[Test]
public async Task Snapshot_ContextAwareGenerator_With_Inherited_Field_Name_Collisions()
{
await GeneratorSnapshotTest<ContextAwareGenerator>.RunAsync(
CreateContextAwareTestSource(
"""
using GFramework.Core.Abstractions.Architectures;
public abstract class ContextAwareRuleBase
{
protected IArchitectureContext? _gFrameworkContextAwareContext;
protected static IArchitectureContextProvider? _gFrameworkContextAwareProvider;
protected static readonly object _gFrameworkContextAwareSync = new();
}
[ContextAware]
public partial class InheritedCollisionRule : ContextAwareRuleBase, IContextAware
{
}
"""),
GetSnapshotFolder());
}
/// <summary>
/// 组装 ContextAwareGenerator 快照测试共用的最小宿主源码,避免每个用例都重复长块样板代码。
/// </summary>
/// <param name="testTypeDeclarations">放在 <c>TestApp</c> 命名空间内的测试类型声明。</param>
/// <param name="includeGameContextHelper">是否额外包含兼容旧快照输入的 <c>GameContext</c> 帮助类型。</param>
/// <returns>可直接交给生成器测试驱动的完整源码文本。</returns>
private static string CreateContextAwareTestSource(string testTypeDeclarations, bool includeGameContextHelper = false)
{
var gameContextHelper = includeGameContextHelper ? GameContextHelperSource : string.Empty;
var testAppDeclarations = IndentBlock(testTypeDeclarations, 4);
return string.Concat(
SharedContextAwareInfrastructure,
gameContextHelper,
"""
}
namespace TestApp
{
using GFramework.Core.SourceGenerators.Abstractions.Rule;
using GFramework.Core.Abstractions.Rule;
""",
testAppDeclarations,
"""
}
""");
}
/// <summary>
/// 为内嵌源码片段补齐缩进,使其能安全插入原始字符串模板中的命名空间块。
/// </summary>
/// <param name="text">要缩进的源码文本。</param>
/// <param name="spaces">每行前要补齐的空格数。</param>
/// <returns>已经补齐统一缩进的多行文本。</returns>
private static string IndentBlock(string text, int spaces)
{
var indentation = new string(' ', spaces);
return string.Join(
Environment.NewLine,
text.Replace("\r\n", "\n", StringComparison.Ordinal)
.Trim()
.Split('\n')
.Select(line => indentation + line));
}
/// <summary>
/// 将运行时测试目录映射回仓库内已提交的上下文感知生成器快照目录。
/// </summary>

View File

@ -0,0 +1,97 @@
// <auto-generated/>
#nullable enable
namespace TestApp;
/// <summary>
/// 为当前规则类型补充自动生成的架构上下文访问实现。
/// </summary>
/// <remarks>
/// 生成代码会在实例级缓存首次解析到的上下文,并在未显式配置提供者时回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
/// 同一生成类型的所有实例共享一个静态上下文提供者;切换或重置提供者只会影响尚未缓存上下文的新实例或未初始化实例,
/// 已缓存的实例上下文需要通过 <see cref="GFramework.Core.Abstractions.Rule.IContextAware.SetContext(GFramework.Core.Abstractions.Architectures.IArchitectureContext)" /> 显式覆盖。
/// 与手动继承 <see cref="global::GFramework.Core.Rule.ContextAwareBase" /> 的路径相比,生成实现会使用 <c>_gFrameworkContextAwareSync1</c> 协调惰性初始化、provider 切换和显式上下文注入;
/// <see cref="global::GFramework.Core.Rule.ContextAwareBase" /> 则保持无锁的实例级缓存语义,更适合已经由调用方线程模型保证串行访问的简单场景。
/// </remarks>
partial class InheritedCollisionRule : global::GFramework.Core.Abstractions.Rule.IContextAware
{
private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _gFrameworkContextAwareContext1;
private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? _gFrameworkContextAwareProvider1;
private static readonly object _gFrameworkContextAwareSync1 = new();
/// <summary>
/// 获取当前实例绑定的架构上下文。
/// </summary>
/// <remarks>
/// 该属性会先返回通过 <c>IContextAware.SetContext(...)</c> 显式注入的实例上下文;若尚未设置,则在同一个同步域内惰性初始化共享提供者。
/// 当静态提供者尚未配置时,生成代码会回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
/// 一旦某个实例成功缓存上下文,后续 <see cref="SetContextProvider(GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider)" />
/// 或 <see cref="ResetContextProvider" /> 不会自动清除此缓存;如需覆盖,请显式调用 <c>IContextAware.SetContext(...)</c>。
/// 当前实现还假设 <see cref="GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider.GetContext" /> 可在持有 <c>_gFrameworkContextAwareSync1</c> 时安全执行;
/// 自定义 provider 不应在该调用链内重新进入当前类型的 provider 配置 API且应避免引入与外部全局锁相互等待的锁顺序。
/// </remarks>
protected global::GFramework.Core.Abstractions.Architectures.IArchitectureContext Context
{
get
{
// 在同一个同步域内协调懒加载与 provider 切换,避免读取到被并发重置的空提供者。
// provider 的 GetContext() 会在持有 _gFrameworkContextAwareSync1 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。
lock (_gFrameworkContextAwareSync1)
{
_gFrameworkContextAwareProvider1 ??= new global::GFramework.Core.Architectures.GameContextProvider();
_gFrameworkContextAwareContext1 ??= _gFrameworkContextAwareProvider1.GetContext();
return _gFrameworkContextAwareContext1;
}
}
}
/// <summary>
/// 配置当前生成类型共享的上下文提供者。
/// </summary>
/// <param name="provider">后续懒加载上下文时要使用的提供者实例。</param>
/// <exception cref="global::System.ArgumentNullException">当 <paramref name="provider" /> 为 null 时抛出。</exception>
/// <remarks>
/// 该方法使用与 <see cref="Context" /> 相同的同步锁,避免提供者切换与惰性初始化交错。
/// 已经缓存上下文的实例不会因为提供者切换而自动失效;该变更仅影响尚未初始化上下文的新实例或未缓存实例。
/// 如需覆盖已有实例的上下文,请显式调用 <c>IContextAware.SetContext(...)</c>。
/// </remarks>
public static void SetContextProvider(global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider provider)
{
global::System.ArgumentNullException.ThrowIfNull(provider);
lock (_gFrameworkContextAwareSync1)
{
_gFrameworkContextAwareProvider1 = provider;
}
}
/// <summary>
/// 重置共享上下文提供者,使后续懒加载回退到默认提供者。
/// </summary>
/// <remarks>
/// 该方法主要用于测试清理或跨用例恢复默认行为。
/// 它不会清除已经缓存到实例字段中的上下文;只有后续尚未初始化上下文的实例会重新回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
/// 如需覆盖已有实例的上下文,请显式调用 <c>IContextAware.SetContext(...)</c>。
/// </remarks>
public static void ResetContextProvider()
{
lock (_gFrameworkContextAwareSync1)
{
_gFrameworkContextAwareProvider1 = null;
}
}
void global::GFramework.Core.Abstractions.Rule.IContextAware.SetContext(global::GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
{
// 与 Context getter 共享同一同步协议,避免显式注入被并发懒加载覆盖。
lock (_gFrameworkContextAwareSync1)
{
_gFrameworkContextAwareContext1 = context;
}
}
global::GFramework.Core.Abstractions.Architectures.IArchitectureContext global::GFramework.Core.Abstractions.Rule.IContextAware.GetContext()
{
return Context;
}
}

View File

@ -7,8 +7,8 @@
## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-026`
- 当前阶段:`Phase 26`
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-027`
- 当前阶段:`Phase 27`
- 当前焦点:
- 已完成 `GFramework.Core` 当前 `MA0016` / `MA0002` / `MA0015` / `MA0077` 低风险收口批次
- 已复核 `net10.0` 下的 `MA0158` 基线:`GFramework.Core` / `GFramework.Cqrs` 当前共有 `16` 个 object lock
@ -33,6 +33,8 @@
- 已完成当前 PR #269 failed-test follow-up修正 `SchemaConfigGeneratorTests`
`Run_Should_Assign_Globally_Unique_Reference_Metadata_Member_Names` 的测试输入,使其继续覆盖
reference metadata 成员名全局去冲突,但不再依赖现已被 `GF_ConfigSchema_014` 拦截的非法同层 schema key 冲突
- 已完成当前 PR #269 Greptile follow-up`ContextAwareGenerator` 现在会把基类链显式成员名也纳入
`_gFrameworkContextAware*` 字段分配冲突检测,并新增 inherited-field collision 快照回归测试
- 已更新 `AGENTS.md`:变更模块必须运行对应 `dotnet build -c Release`,并处理或显式报告模块构建 warning
不再默认留给长期 warning 清理分支
- `CoroutineScheduler` 的 tag/group 字典已显式使用 `StringComparer.Ordinal`,保持既有区分大小写语义
@ -75,6 +77,8 @@
`CqrsHandlerRegistryGenerator``dynamic` 类型引用风险,并同步更新 `AGENTS.md` 的模块 build / warning 治理规范
- 已完成当前 PR #269 的 failed-test follow-up将 reference metadata 成员名唯一性回归测试改为合法 schema 路径组合,
并重新通过定向 generator test
- 已完成当前 PR #269 的 Greptile follow-up修复 `ContextAwareGenerator` 未覆盖基类成员名冲突的问题,并补齐
inherited-collision 快照测试
- 已完成 `GFramework.Game.SourceGenerators``SchemaConfigGenerator` 的第一批 `MA0051` 收口warnings-only 基线剩余 `9`
`MA0051`
@ -151,6 +155,9 @@
warning本轮 focused test 已通过,但测试项目整包 warning 尚未进入本轮写集
- 缓解措施:本轮已在 failed-test follow-up 的定向 `dotnet test` 中再次确认这些 warning 仍为既有基线;后续若继续修改该测试项目,
应按新增 `AGENTS.md` 规则先明确 warning 收口范围,再决定是否进入专门清理切片
- ContextAware 基类命名隐藏风险:若生成器只看当前类型声明成员,派生规则会重新占用基类已声明的
`_gFrameworkContextAware*` 字段名,导致生成成员隐藏继承状态并让快照无法锁定后缀分配行为
- 缓解措施:本轮已改为遍历完整 base-type 链收集保留名,并用 inherited collision 快照用例锁定该行为
- Godot 资产文件环境风险:当前 worktree 的 `GFramework.Godot` restore/build 仍会命中 Windows fallback package folder
- 缓解措施:后续若继续触达 Godot 模块,先用 Linux 侧 restore 资产或 Windows-hosted 构建链刷新该项目,再补跑定向 build
- 并行实现风险:批量收敛时若 subagent 写入边界不清晰,容易引入命名冲突或重复重构

View File

@ -1,5 +1,30 @@
# Analyzer Warning Reduction 追踪
## 2026-04-23 — RP-027
### 阶段PR #269 Greptile inherited-member collision follow-upRP-027
- 启动复核:
- 根据用户补充,重新核对 `$gframework-pr-review` 抓下来的 `greptile-apps[bot]` unresolved 线程,确认仍有一条
`ContextAwareGenerator` 关于 inherited member names 未参与 collision detection 的 P1 评论
- 本地读取 `CreateGeneratedContextMemberNames(...)` 后确认当前实现只收集 `symbol.GetMembers()`,确实没有遍历基类链
- 决策:
- 保持现有 `_gFrameworkContextAware*` 前缀和数字后缀分配规则不变,只把保留名集合扩展为“当前类型 + 基类链显式成员”
- 沿用既有 `ContextAwareGeneratorSnapshotTests` 模式,新增 inherited-field collision 快照,而不是只写松散字符串断言
- 实施调整:
- 为 `ContextAwareGenerator` 新增 `CollectReservedContextMemberNames(...)` helper遍历完整 `BaseType` 链收集显式成员名
- 为 `ContextAwareGeneratorSnapshotTests` 增加 `InheritedCollisionRule` 场景,并抽出公共测试源码 helper避免重复样板
- 新增快照 `InheritedCollisionRule.ContextAware.g.cs`,锁定基类已声明 `_gFrameworkContextAware*` 时生成器会回退到 `...1` 后缀
- 验证结果:
- `dotnet build GFramework.Core.SourceGenerators/GFramework.Core.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -clp:"Summary;WarningsOnly" -nologo`
- 结果:`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~ContextAwareGeneratorSnapshotTests.Snapshot_ContextAwareGenerator_With_Inherited_Field_Name_Collisions|FullyQualifiedName~ContextAwareGeneratorSnapshotTests.Snapshot_ContextAwareGenerator_With_User_Field_Name_Collisions|FullyQualifiedName~ContextAwareGeneratorSnapshotTests.Snapshot_ContextAwareGenerator" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`3 Passed``0 Failed`
- 说明:`GFramework.SourceGenerators.Tests` 仍打印既有 `MA0048``MA0051``MA0004` warning本轮未扩大到测试项目 warning 清理
- 下一步建议:
- 若继续收口 PR #269,可再次抓取最新 unresolved threads确认 Greptile / CodeRabbit 当前是否只剩陈旧信号
- 若继续推进 analyzer 主线,可单独评估 `GFramework.SourceGenerators.Tests` 的 warning 清理是否值得开新切片
## 2026-04-23 — RP-026
### 阶段PR #269 failed-test follow-upRP-026