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
18 changes: 18 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,24 @@ All AI agents and contributors must follow these rules when writing, reviewing,
- After resolving the host Windows Git path, prefer an explicit session-local binding for subsequent commands so the
shell does not fall back to Linux `/usr/bin/git` later in the same WSL session.

## Git Workflow Rules

- 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.
- 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>`.
- The commit `summary` MUST use simplified Chinese and briefly describe the main change.
- The commit `body` MUST use unordered list items, and each item MUST start with a verb such as `新增`、`修复`、`优化`、
`更新`、`补充`、`重构`.
- Each commit body bullet MUST describe one independent change point; avoid repeated or redundant descriptions.
- Keep technical terms in English when they are established project terms, such as `API`、`Model`、`System`.
- If a new task starts while the current branch is `main`, contributors MUST first try to update local `main` from the
remote, then create and switch to a dedicated branch before making substantive changes.
- The branch naming rule for a new task branch is `<type>/<topic-or-scope>`, where `<type>` should match the intended
Conventional Commit category as closely as practical.

## Subagent Usage Rules

- Use subagents only when the task is complex, the context is likely to grow too large, or the work can be split into
Expand Down
19 changes: 13 additions & 6 deletions GFramework.Core.Abstractions/Resource/IResourceManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@ public interface IResourceManager : IUtility
T? Load<T>(string path) where T : class;

/// <summary>
/// 异步加载资源
/// 异步加载指定路径的资源,并在缓存中对并发加载进行去重。
/// </summary>
/// <typeparam name="T">资源类型</typeparam>
/// <param name="path">资源路径</param>
/// <returns>资源实例,如果加载失败返回 null</returns>
/// <param name="path">资源路径,不能为空或空白。</param>
/// <returns>加载成功返回资源实例;加载失败返回 <see langword="null"/>。</returns>
/// <exception cref="ArgumentException">当 <paramref name="path"/> 为空或空白时抛出。</exception>
/// <exception cref="InvalidOperationException">当未注册对应资源加载器时抛出。</exception>
/// <remarks>实现内部可能使用 <c>ConfigureAwait(false)</c>,异步延续不保证回到调用线程。</remarks>
Task<T?> LoadAsync<T>(string path) where T : class;

/// <summary>
Expand Down Expand Up @@ -70,10 +73,14 @@ public interface IResourceManager : IUtility
void UnregisterLoader<T>() where T : class;

/// <summary>
/// 预加载资源(加载但不返回)
/// 预加载资源到缓存中。
/// </summary>
/// <typeparam name="T">资源类型</typeparam>
/// <param name="path">资源路径</param>
/// <param name="path">资源路径,不能为空或空白。</param>
/// <returns>表示预加载流程完成的任务。</returns>
/// <exception cref="ArgumentException">当 <paramref name="path"/> 为空或空白时抛出。</exception>
/// <exception cref="InvalidOperationException">当未注册对应资源加载器时抛出。</exception>
/// <remarks>内部委托给 <see cref="LoadAsync{T}(string)"/>,同样不捕获同步上下文。</remarks>
Task PreloadAsync<T>(string path) where T : class;

/// <summary>
Expand All @@ -86,4 +93,4 @@ public interface IResourceManager : IUtility
/// </summary>
/// <param name="strategy">资源释放策略</param>
void SetReleaseStrategy(IResourceReleaseStrategy strategy);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -635,23 +635,23 @@ private static bool TryGetRegistration(
if (targetMethod.TypeArguments[0] is not INamedTypeSymbol namedType)
return false;

if (targetMethod.Name == "RegisterModel" &&
if (string.Equals(targetMethod.Name, "RegisterModel", StringComparison.Ordinal) &&
SymbolHelpers.IsAssignableTo(targetMethod.ContainingType, symbols.IArchitectureType))
{
componentKind = ComponentKind.Model;
registeredType = namedType;
return true;
}

if (targetMethod.Name == "RegisterSystem" &&
if (string.Equals(targetMethod.Name, "RegisterSystem", StringComparison.Ordinal) &&
SymbolHelpers.IsAssignableTo(targetMethod.ContainingType, symbols.IArchitectureType))
{
componentKind = ComponentKind.System;
registeredType = namedType;
return true;
}

if (targetMethod.Name == "RegisterUtility" &&
if (string.Equals(targetMethod.Name, "RegisterUtility", StringComparison.Ordinal) &&
SymbolHelpers.IsAssignableTo(targetMethod.ContainingType, symbols.IArchitectureType))
{
componentKind = ComponentKind.Utility;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ private static void AnalyzeInvocation(
var method = invocation.TargetMethod;

// 检查方法名是否为 GetAll
if (method.Name != "GetAll")
if (!string.Equals(method.Name, "GetAll", StringComparison.Ordinal))
return;

// 检查是否为泛型方法
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,7 @@ private static List<RegistrationSpec> CollectRegistrations(
{
var registrations = new List<RegistrationSpec>();

foreach (var attribute in typeSymbol.GetAttributes()
// Roslyn 会把 partial 类型上的属性合并到同一个集合中。
// 先按语法树标识排序,才能让每个文件内的 Span.Start 成为可比较的稳定顺序键。
.OrderBy(GetAttributeSyntaxTreeOrderKey, StringComparer.Ordinal)
.ThenBy(GetAttributeOrder)
.ThenBy(GetAttributeTypeOrderKey, StringComparer.Ordinal))
foreach (var attribute in GetOrderedRegistrationAttributes(typeSymbol))
{
if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, registerModelAttribute))
{
Expand Down Expand Up @@ -239,6 +234,16 @@ private static List<RegistrationSpec> CollectRegistrations(
return registrations;
}

private static IOrderedEnumerable<AttributeData> GetOrderedRegistrationAttributes(INamedTypeSymbol typeSymbol)
{
// Roslyn 会把 partial 类型上的属性合并到同一个集合中。
// 先按语法树标识排序,才能让每个文件内的 Span.Start 成为可比较的稳定顺序键。
return typeSymbol.GetAttributes()
.OrderBy(GetAttributeSyntaxTreeOrderKey, StringComparer.Ordinal)
.ThenBy(GetAttributeOrder)
.ThenBy(GetAttributeTypeOrderKey, StringComparer.Ordinal);
}

private static bool TryCreateRegistration(
SourceProductionContext context,
INamedTypeSymbol ownerType,
Expand Down Expand Up @@ -323,7 +328,8 @@ private static string GenerateSource(INamedTypeSymbol typeSymbol, IReadOnlyList<
RegistrationKind.Model => "RegisterModel",
RegistrationKind.System => "RegisterSystem",
RegistrationKind.Utility => "RegisterUtility",
_ => throw new ArgumentOutOfRangeException(nameof(registration.Kind))
_ => throw new InvalidOperationException(
$"Unsupported registration kind '{registration.Kind}'.")
});
builder.Append("(new ");
builder.Append(registration.ComponentTypeDisplayName);
Expand Down
33 changes: 22 additions & 11 deletions GFramework.Core.SourceGenerators/Enums/EnumExtensionsGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,7 @@ protected override string Generate(INamedTypeSymbol symbol, AttributeData attr)
? null
: symbol.ContainingNamespace.ToDisplayString();

var generateIsMethods = GetNamedBooleanArgument(
attr,
nameof(GenerateEnumExtensionsAttribute.GenerateIsMethods),
true);
var generateIsInMethod = GetNamedBooleanArgument(
attr,
nameof(GenerateEnumExtensionsAttribute.GenerateIsInMethod),
true);
var generationOptions = GetGenerationOptions(attr);
var enumName = symbol.Name;
var fullEnumName = symbol.ToDisplayString();
var members = symbol.GetMembers()
Expand All @@ -104,15 +97,15 @@ protected override string Generate(INamedTypeSymbol symbol, AttributeData attr)
// 两个生成开关是彼此独立的契约,需要分别控制输出,并保持空行布局稳定,便于快照精确回归。
var hasGeneratedMembers = false;

if (generateIsMethods)
if (generationOptions.GenerateIsMethods)
{
hasGeneratedMembers = AppendIsMethods(
sb,
members,
fullEnumName);
}

if (generateIsInMethod)
if (generationOptions.GenerateIsInMethod)
{
if (hasGeneratedMembers)
{
Expand All @@ -130,6 +123,24 @@ protected override string Generate(INamedTypeSymbol symbol, AttributeData attr)
return sb.ToString();
}

/// <summary>
/// 读取枚举扩展生成选项,并在属性未显式指定时回退到契约默认值。
/// </summary>
/// <param name="attribute">待分析的特性数据。</param>
/// <returns>包含各个生成开关的选项元组。</returns>
private static (bool GenerateIsMethods, bool GenerateIsInMethod) GetGenerationOptions(AttributeData attribute)
{
return (
GetNamedBooleanArgument(
attribute,
nameof(GenerateEnumExtensionsAttribute.GenerateIsMethods),
true),
GetNamedBooleanArgument(
attribute,
nameof(GenerateEnumExtensionsAttribute.GenerateIsInMethod),
true));
}

/// <summary>
/// 获取生成文件的提示名称
/// </summary>
Expand All @@ -151,7 +162,7 @@ private static bool GetNamedBooleanArgument(AttributeData attribute, string argu
{
foreach (var namedArgument in attribute.NamedArguments)
{
if (namedArgument.Key == argumentName &&
if (string.Equals(namedArgument.Key, argumentName, StringComparison.Ordinal) &&
namedArgument.Value.Value is bool value)
{
return value;
Expand Down
20 changes: 19 additions & 1 deletion GFramework.Core.Tests/Extensions/NumericExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,24 @@ public void Between_Should_Throw_ArgumentException_When_Min_Is_Greater_Than_Max(
Assert.Throws<ArgumentException>(() => value.Between(100, 0));
}

/// <summary>
/// 测试Between方法在引用类型参数为null时抛出ArgumentNullException
/// </summary>
[Test]
public void Between_Should_Throw_ArgumentNullException_When_Reference_Arguments_Are_Null()
{
string? value = "m";
string? min = "a";
string? max = "z";

Assert.Multiple(() =>
{
Assert.Throws<ArgumentNullException>(() => NumericExtensions.Between(value!, null!, max!));
Assert.Throws<ArgumentNullException>(() => NumericExtensions.Between(value!, min!, null!));
Assert.Throws<ArgumentNullException>(() => NumericExtensions.Between(null!, min!, max!));
});
}

/// <summary>
/// 测试Lerp方法在t为0时返回起始值
/// </summary>
Expand Down Expand Up @@ -205,4 +223,4 @@ public void InverseLerp_Should_Throw_DivideByZeroException_When_From_Equals_To()
// Arrange & Act & Assert
Assert.Throws<DivideByZeroException>(() => 50f.InverseLerp(100f, 100f));
}
}
}
9 changes: 7 additions & 2 deletions GFramework.Core.Tests/Extensions/ResultExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,12 @@ public void Ensure_Should_Create_ArgumentException_With_Message()
{
var result = Result<int>.Succeed(-1);
var ensured = result.Ensure(x => x > 0, "Value must be positive");
Assert.That(ensured.Exception.Message, Is.EqualTo("Value must be positive"));
Assert.Multiple(() =>
{
Assert.That(ensured.Exception.Message, Does.StartWith("Value must be positive"));
Assert.That(ensured.Exception, Is.TypeOf<ArgumentException>());
Assert.That(((ArgumentException)ensured.Exception).ParamName, Is.EqualTo("result"));
});
}

/// <summary>
Expand Down Expand Up @@ -635,4 +640,4 @@ public void Should_Support_Ensure_Chaining_With_Multiple_Conditions()

Assert.That(result.IsSuccess, Is.True);
}
}
}
18 changes: 17 additions & 1 deletion GFramework.Core.Tests/Functional/ResultTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -429,4 +429,20 @@ public void ToString_Should_Return_Fail_With_Message_When_Failed()
// Assert
Assert.That(str, Is.EqualTo("Fail(Test error)"));
}
}

/// <summary>
/// 测试default(Result)不会在相等性比较、哈希计算和字符串格式化中触发空引用异常
/// </summary>
[Test]
public void Default_Result_Should_Be_Safe_For_Equality_HashCode_And_ToString()
{
var result = default(Result);

Assert.Multiple(() =>
{
Assert.That(result.Equals(default(Result)), Is.True);
Assert.That(result.GetHashCode(), Is.EqualTo(0));
Assert.That(result.ToString(), Is.EqualTo("Fail(null)"));
});
}
}
Loading
Loading