Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ All AI agents and contributors must follow these rules when writing, reviewing,
- Every completed task MUST pass at least one build validation before it is considered done.
- If the task changes multiple projects or shared abstractions, prefer a solution-level or affected-project
`dotnet build ... -c Release`; otherwise use the smallest build command that still proves the result compiles.
- When a task adds a feature or modifies code, contributors MUST run a Release build for every directly affected
module/project instead of relying on an unrelated project or solution slice that does not actually compile the touched
code.
- Warnings reported by those affected-module builds are part of the task scope. Contributors MUST resolve the touched
module's build warnings in the same change, or stop and explicitly report the exact warning IDs and blocker instead of
deferring them to a separate long-lived cleanup branch by default.
- If the required build passes and there are task-related staged or unstaged changes, contributors MUST create a Git
commit automatically instead of leaving the task uncommitted, unless the user explicitly says not to commit.
- Commit messages MUST use Conventional Commits format: `<type>(<scope>): <summary>`.
Expand Down
169 changes: 139 additions & 30 deletions GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ protected override string Generate(

var interfaceName = iContextAware.ToDisplayString(
SymbolDisplayFormat.FullyQualifiedFormat);
var memberNames = CreateGeneratedContextMemberNames(symbol);
sb.AppendLine("/// <summary>");
sb.AppendLine("/// 为当前规则类型补充自动生成的架构上下文访问实现。");
sb.AppendLine("/// </summary>");
Expand All @@ -107,15 +108,15 @@ protected override string Generate(
sb.AppendLine(
"/// 已缓存的实例上下文需要通过 <see cref=\"GFramework.Core.Abstractions.Rule.IContextAware.SetContext(GFramework.Core.Abstractions.Architectures.IArchitectureContext)\" /> 显式覆盖。");
sb.AppendLine(
"/// 与手动继承 <see cref=\"global::GFramework.Core.Rule.ContextAwareBase\" /> 的路径相比,生成实现会使用 <c>_contextSync</c> 协调惰性初始化、provider 切换和显式上下文注入;");
$"/// 与手动继承 <see cref=\"global::GFramework.Core.Rule.ContextAwareBase\" /> 的路径相比,生成实现会使用 <c>{memberNames.SyncFieldName}</c> 协调惰性初始化、provider 切换和显式上下文注入;");
sb.AppendLine(
"/// <see cref=\"global::GFramework.Core.Rule.ContextAwareBase\" /> 则保持无锁的实例级缓存语义,更适合已经由调用方线程模型保证串行访问的简单场景。");
sb.AppendLine("/// </remarks>");
sb.AppendLine($"partial class {symbol.Name} : {interfaceName}");
sb.AppendLine("{");

GenerateContextProperty(sb);
GenerateInterfaceImplementations(sb, iContextAware);
GenerateContextProperty(sb, memberNames);
GenerateInterfaceImplementations(sb, iContextAware, memberNames);

sb.AppendLine("}");
return sb.ToString().TrimEnd();
Expand All @@ -138,13 +139,40 @@ protected override string GetHintName(INamedTypeSymbol symbol)
/// 生成Context属性
/// </summary>
/// <param name="sb">字符串构建器</param>
private static void GenerateContextProperty(StringBuilder sb)
/// <param name="memberNames">当前目标类型应使用的上下文字段名。</param>
private static void GenerateContextProperty(
StringBuilder sb,
GeneratedContextMemberNames memberNames)
{
GenerateContextBackingFields(sb, memberNames);
GenerateContextGetter(sb, memberNames);
GenerateContextProviderConfiguration(sb, memberNames);
}

/// <summary>
/// 生成上下文缓存和同步所需的字段。
/// </summary>
/// <param name="sb">字符串构建器。</param>
private static void GenerateContextBackingFields(
StringBuilder sb,
GeneratedContextMemberNames memberNames)
{
sb.AppendLine(" private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _context;");
sb.AppendLine(
" private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? _contextProvider;");
sb.AppendLine(" private static readonly object _contextSync = new();");
$" private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? {memberNames.ContextFieldName};");
sb.AppendLine(
$" private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? {memberNames.ProviderFieldName};");
sb.AppendLine($" private static readonly object {memberNames.SyncFieldName} = new();");
sb.AppendLine();
}

/// <summary>
/// 生成实例上下文访问器,包含显式注入优先和 provider 惰性回退语义。
/// </summary>
/// <param name="sb">字符串构建器。</param>
private static void GenerateContextGetter(
StringBuilder sb,
GeneratedContextMemberNames memberNames)
{
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// 获取当前实例绑定的架构上下文。");
sb.AppendLine(" /// </summary>");
Expand All @@ -158,37 +186,43 @@ private static void GenerateContextProperty(StringBuilder sb)
sb.AppendLine(
" /// 或 <see cref=\"ResetContextProvider\" /> 不会自动清除此缓存;如需覆盖,请显式调用 <c>IContextAware.SetContext(...)</c>。");
sb.AppendLine(
" /// 当前实现还假设 <see cref=\"GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider.GetContext\" /> 可在持有 <c>_contextSync</c> 时安全执行;");
$" /// 当前实现还假设 <see cref=\"GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider.GetContext\" /> 可在持有 <c>{memberNames.SyncFieldName}</c> 时安全执行;");
sb.AppendLine(
" /// 自定义 provider 不应在该调用链内重新进入当前类型的 provider 配置 API,且应避免引入与外部全局锁相互等待的锁顺序。");
sb.AppendLine(" /// </remarks>");
sb.AppendLine(" protected global::GFramework.Core.Abstractions.Architectures.IArchitectureContext Context");
sb.AppendLine(" {");
sb.AppendLine(" get");
sb.AppendLine(" {");
sb.AppendLine(" var context = _context;");
sb.AppendLine(" if (context is not null)");
sb.AppendLine(" {");
sb.AppendLine(" return context;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" // 在同一个同步域内协调懒加载与 provider 切换,避免读取到被并发重置的空提供者。");
sb.AppendLine(
" // provider 的 GetContext() 会在持有 _contextSync 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。");
sb.AppendLine(" lock (_contextSync)");
$" // provider 的 GetContext() 会在持有 {memberNames.SyncFieldName} 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。");
sb.AppendLine($" lock ({memberNames.SyncFieldName})");
sb.AppendLine(" {");
sb.AppendLine(
" _contextProvider ??= new global::GFramework.Core.Architectures.GameContextProvider();");
sb.AppendLine(" _context ??= _contextProvider.GetContext();");
sb.AppendLine(" return _context;");
$" {memberNames.ProviderFieldName} ??= new global::GFramework.Core.Architectures.GameContextProvider();");
sb.AppendLine($" {memberNames.ContextFieldName} ??= {memberNames.ProviderFieldName}.GetContext();");
sb.AppendLine($" return {memberNames.ContextFieldName};");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine();
}

/// <summary>
/// 生成静态 provider 配置 API,供测试和宿主在懒加载前替换默认上下文来源。
/// </summary>
/// <param name="sb">字符串构建器。</param>
private static void GenerateContextProviderConfiguration(
StringBuilder sb,
GeneratedContextMemberNames memberNames)
{
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// 配置当前生成类型共享的上下文提供者。");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" /// <param name=\"provider\">后续懒加载上下文时要使用的提供者实例。</param>");
sb.AppendLine(
" /// <exception cref=\"global::System.ArgumentNullException\">当 <paramref name=\"provider\" /> 为 null 时抛出。</exception>");
sb.AppendLine(" /// <remarks>");
sb.AppendLine(" /// 该方法使用与 <see cref=\"Context\" /> 相同的同步锁,避免提供者切换与惰性初始化交错。");
sb.AppendLine(
Expand All @@ -198,9 +232,10 @@ private static void GenerateContextProperty(StringBuilder sb)
sb.AppendLine(
" public static void SetContextProvider(global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider provider)");
sb.AppendLine(" {");
sb.AppendLine(" lock (_contextSync)");
sb.AppendLine(" global::System.ArgumentNullException.ThrowIfNull(provider);");
sb.AppendLine($" lock ({memberNames.SyncFieldName})");
sb.AppendLine(" {");
sb.AppendLine(" _contextProvider = provider;");
sb.AppendLine($" {memberNames.ProviderFieldName} = provider;");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine();
Expand All @@ -215,9 +250,9 @@ private static void GenerateContextProperty(StringBuilder sb)
sb.AppendLine(" /// </remarks>");
sb.AppendLine(" public static void ResetContextProvider()");
sb.AppendLine(" {");
sb.AppendLine(" lock (_contextSync)");
sb.AppendLine($" lock ({memberNames.SyncFieldName})");
sb.AppendLine(" {");
sb.AppendLine(" _contextProvider = null;");
sb.AppendLine($" {memberNames.ProviderFieldName} = null;");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine();
Expand All @@ -234,7 +269,8 @@ private static void GenerateContextProperty(StringBuilder sb)
/// <param name="interfaceSymbol">接口符号</param>
private static void GenerateInterfaceImplementations(
StringBuilder sb,
INamedTypeSymbol interfaceSymbol)
INamedTypeSymbol interfaceSymbol,
GeneratedContextMemberNames memberNames)
{
var interfaceName = interfaceSymbol.ToDisplayString(
SymbolDisplayFormat.FullyQualifiedFormat);
Expand All @@ -244,7 +280,7 @@ private static void GenerateInterfaceImplementations(
if (method.MethodKind != MethodKind.Ordinary)
continue;

GenerateMethod(sb, interfaceName, method);
GenerateMethod(sb, interfaceName, method, memberNames);
sb.AppendLine();
}
}
Expand All @@ -258,7 +294,8 @@ private static void GenerateInterfaceImplementations(
private static void GenerateMethod(
StringBuilder sb,
string interfaceName,
IMethodSymbol method)
IMethodSymbol method,
GeneratedContextMemberNames memberNames)
{
var returnType = method.ReturnType.ToDisplayString(
SymbolDisplayFormat.FullyQualifiedFormat);
Expand All @@ -271,7 +308,7 @@ private static void GenerateMethod(
$" {returnType} {interfaceName}.{method.Name}({parameters})");
sb.AppendLine(" {");

GenerateMethodBody(sb, method);
GenerateMethodBody(sb, method, memberNames);

sb.AppendLine(" }");
}
Expand All @@ -283,15 +320,16 @@ private static void GenerateMethod(
/// <param name="method">方法符号</param>
private static void GenerateMethodBody(
StringBuilder sb,
IMethodSymbol method)
IMethodSymbol method,
GeneratedContextMemberNames memberNames)
{
switch (method.Name)
{
case "SetContext":
sb.AppendLine(" // 与 Context getter 共享同一同步协议,避免显式注入被并发懒加载覆盖。");
sb.AppendLine(" lock (_contextSync)");
sb.AppendLine($" lock ({memberNames.SyncFieldName})");
sb.AppendLine(" {");
sb.AppendLine(" _context = context;");
sb.AppendLine($" {memberNames.ContextFieldName} = context;");
sb.AppendLine(" }");
break;

Expand All @@ -307,4 +345,75 @@ private static void GenerateMethodBody(
break;
}
}

/// <summary>
/// 为生成字段选择不会与目标类型现有成员冲突的稳定名称。
/// </summary>
/// <param name="symbol">当前需要补充 ContextAware 实现的目标类型。</param>
/// <returns>当前生成轮次应使用的上下文字段名集合。</returns>
private static GeneratedContextMemberNames CreateGeneratedContextMemberNames(INamedTypeSymbol symbol)
{
var reservedNames = CollectReservedContextMemberNames(symbol);

return new GeneratedContextMemberNames(
AllocateGeneratedMemberName(reservedNames, "_gFrameworkContextAwareContext"),
AllocateGeneratedMemberName(reservedNames, "_gFrameworkContextAwareProvider"),
AllocateGeneratedMemberName(reservedNames, "_gFrameworkContextAwareSync"));
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

/// <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>
/// <param name="reservedNames">当前类型已占用或已为其他生成字段保留的名称集合。</param>
/// <param name="baseName">优先尝试的基础名称。</param>
/// <returns>本轮生成可以使用的唯一成员名。</returns>
private static string AllocateGeneratedMemberName(
ISet<string> reservedNames,
string baseName)
{
if (reservedNames.Add(baseName))
return baseName;

for (var suffix = 1; ; suffix++)
{
var candidateName = $"{baseName}{suffix}";
if (reservedNames.Add(candidateName))
return candidateName;
}
}

/// <summary>
/// 描述一次 ContextAware 代码生成中选定的上下文字段名。
/// </summary>
/// <param name="ContextFieldName">实例上下文缓存字段名。</param>
/// <param name="ProviderFieldName">共享上下文提供者字段名。</param>
/// <param name="SyncFieldName">用于串行化访问的同步字段名。</param>
private readonly record struct GeneratedContextMemberNames(
string ContextFieldName,
string ProviderFieldName,
string SyncFieldName);
}
4 changes: 2 additions & 2 deletions GFramework.Core.Tests/Events/EasyEventsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ public void GetOrAdd_Should_Be_Thread_Safe()
}

/// <summary>
/// 测试并发场景下AddEvent的行为
/// 测试 AddEvent 对重复事件类型保持兼容的参数异常类型。
/// </summary>
[Test]
public void AddEvent_Should_Throw_When_Already_Registered()
Expand Down Expand Up @@ -167,4 +167,4 @@ public void Concurrent_Registration_Of_Different_Event_Types_Should_Work()
Assert.That(_easyEvents.GetEvent<Event<int, string>>(), Is.Not.Null);
Assert.That(_easyEvents.GetEvent<Event<double>>(), Is.Not.Null);
}
}
}
26 changes: 25 additions & 1 deletion GFramework.Core.Tests/Extensions/CollectionExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,30 @@ public void ToDictionarySafe_Should_Create_Dictionary()
Assert.That(result["c"], Is.EqualTo(3));
}

/// <summary>
/// 测试ToDictionarySafe保持具体Dictionary返回类型,避免公开API继续收窄。
/// </summary>
[Test]
public void ToDictionarySafe_Should_Preserve_Concrete_Return_Type()
{
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.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>
/// 测试ToDictionarySafe方法在存在重复键时覆盖前面的值
/// </summary>
Expand Down Expand Up @@ -224,4 +248,4 @@ public void ToDictionarySafe_Should_Throw_ArgumentNullException_When_ValueSelect
Assert.Throws<ArgumentNullException>(() =>
items.ToDictionarySafe<(string, int), string, int>(x => x.Item1, null!));
}
}
}
Loading
Loading