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
34 changes: 25 additions & 9 deletions GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -884,7 +884,9 @@ private static bool TryValidateUnsupportedCombinatorKeywordsRecursively(
/// <summary>
/// 递归拒绝当前共享子集尚未支持的开放对象关键字形状。
/// 当前对象字段集默认是闭合的,因此这里只接受显式重复该语义的
/// <c>additionalProperties: false</c>。
/// <c>additionalProperties: false</c>,并继续拒绝
/// <c>patternProperties</c>、<c>propertyNames</c> 与
/// <c>unevaluatedProperties</c> 这类会重新打开对象形状的关键字。
/// </summary>
/// <param name="filePath">Schema 文件路径。</param>
/// <param name="displayPath">逻辑字段路径。</param>
Expand Down Expand Up @@ -959,12 +961,7 @@ private static bool TryValidateUnsupportedOpenObjectKeywords(
out Diagnostic? diagnostic)
{
diagnostic = null;
if (!element.TryGetProperty("additionalProperties", out var additionalPropertiesElement))
{
return true;
}

if (additionalPropertiesElement.ValueKind == JsonValueKind.False)
if (TryGetUnsupportedOpenObjectKeywordName(element) is not { } keywordName)
{
return true;
}
Expand All @@ -974,8 +971,8 @@ private static bool TryValidateUnsupportedOpenObjectKeywords(
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
"additionalProperties",
"The current config schema subset only accepts 'additionalProperties: false' so object fields remain closed and strongly typed.");
keywordName,
"The current config schema subset only accepts 'additionalProperties: false' and rejects keywords that reopen object shapes so fields remain closed and strongly typed.");
return false;
}

Expand All @@ -991,6 +988,25 @@ private static bool TryValidateUnsupportedOpenObjectKeywords(
null;
}

/// <summary>
/// 返回当前节点声明的首个未支持开放对象关键字。
/// </summary>
/// <param name="element">当前 schema 节点。</param>
/// <returns>命中的关键字名称;未声明时返回空。</returns>
private static string? TryGetUnsupportedOpenObjectKeywordName(JsonElement element)
{
if (element.TryGetProperty("additionalProperties", out var additionalPropertiesElement) &&
additionalPropertiesElement.ValueKind != JsonValueKind.False)
{
return "additionalProperties";
}

return element.TryGetProperty("patternProperties", out _) ? "patternProperties" :
element.TryGetProperty("propertyNames", out _) ? "propertyNames" :
element.TryGetProperty("unevaluatedProperties", out _) ? "unevaluatedProperties" :
null;
}

/// <summary>
/// 以统一顺序递归遍历 schema 树,并把每个节点交给调用方提供的校验逻辑。
/// 该遍历覆盖对象属性、<c>dependentSchemas</c> / <c>allOf</c> /
Expand Down
3 changes: 3 additions & 0 deletions GFramework.Game.SourceGenerators/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ GameProject/
- `oneOf`
- `anyOf`
- 非 `false` 的 `additionalProperties`
- `patternProperties`
- `propertyNames`
- `unevaluatedProperties`
- 其他依赖开放对象形状、联合分支或属性合并的复杂组合约束

遇到这些情况时,建议先回到 [配置系统文档](../docs/zh-CN/game/config-system.md) 和原始 schema / YAML 设计本体,确认是否需要调整配置建模方式,而不是默认期待生成器直接支持完整 `JSON Schema` 语义。
Expand Down
49 changes: 49 additions & 0 deletions GFramework.Game.Tests/Config/YamlConfigLoaderAllOfTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,55 @@ public void LoadAsync_Should_Throw_When_Object_Schema_Declares_Unsupported_Addit
});
}

/// <summary>
/// 验证运行时会拒绝会重新打开对象形状的其他开放对象关键字。
/// </summary>
[TestCase("patternProperties", """
{
"^dynamic-": { "type": "integer" }
}
""")]
[TestCase("propertyNames", """
{
"pattern": "^[a-z]+$"
}
""")]
[TestCase("unevaluatedProperties", "false")]
public void LoadAsync_Should_Throw_When_Object_Schema_Declares_Unsupported_OpenObject_Keyword(
string keywordName,
string keywordValueJson)
{
CreateConfigFile(
"monster/slime.yaml",
BuildMonsterConfigYaml(
"""
itemCount: 3
"""));
CreateSchemaFile(
"schemas/monster.schema.json",
BuildMonsterSchema(
DefaultRewardPropertiesJson,
DefaultAllOfJson,
$$"""
"{{keywordName}}": {{keywordValueJson}}
"""));

var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();

var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));

Assert.Multiple(() =>
{
Assert.That(exception, Is.Not.Null);
Assert.That(exception!.Diagnostic.FailureKind, Is.EqualTo(ConfigLoadFailureKind.SchemaUnsupported));
Assert.That(exception.Diagnostic.DisplayPath, Is.EqualTo("reward"));
Assert.That(exception.Message, Does.Contain($"unsupported '{keywordName}' metadata"));
Assert.That(exception.Message, Does.Contain("rejects keywords that reopen object shapes"));
Assert.That(registry.Count, Is.EqualTo(0));
});
}

/// <summary>
/// 验证 allOf 条目只接受 object-typed schema。
/// </summary>
Expand Down
34 changes: 25 additions & 9 deletions GFramework.Game/Config/YamlConfigSchemaValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,9 @@ private static void ValidateUnsupportedCombinatorKeywords(
/// <summary>
/// 显式拒绝当前共享子集中尚未支持的开放对象关键字形状。
/// 当前配置系统默认采用闭合对象字段集;这里只接受显式重复该语义的
/// <c>additionalProperties: false</c>,继续拒绝会引入动态字段形状的其它形式。
/// <c>additionalProperties: false</c>,并继续拒绝
/// <c>patternProperties</c>、<c>propertyNames</c> 与
/// <c>unevaluatedProperties</c> 这类会重新打开对象形状的关键字。
/// </summary>
/// <param name="tableName">所属配置表名称。</param>
/// <param name="schemaPath">Schema 文件路径。</param>
Expand All @@ -385,21 +387,16 @@ private static void ValidateUnsupportedOpenObjectKeywords(
string propertyPath,
JsonElement element)
{
if (!element.TryGetProperty("additionalProperties", out var additionalPropertiesElement))
{
return;
}

if (additionalPropertiesElement.ValueKind == JsonValueKind.False)
if (TryGetUnsupportedOpenObjectKeywordName(element) is not { } keywordName)
{
return;
}

throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' uses unsupported 'additionalProperties' metadata. " +
"The current config schema subset only accepts 'additionalProperties: false' so object fields remain closed and strongly typed.",
$"Property '{propertyPath}' in schema file '{schemaPath}' uses unsupported '{keywordName}' metadata. " +
"The current config schema subset only accepts 'additionalProperties: false' and rejects keywords that reopen object shapes so fields remain closed and strongly typed.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
Expand All @@ -416,6 +413,25 @@ private static void ValidateUnsupportedOpenObjectKeywords(
null;
}

/// <summary>
/// 返回当前节点声明的首个未支持开放对象关键字。
/// </summary>
/// <param name="element">当前 schema 节点。</param>
/// <returns>命中的关键字名称;未声明时返回空。</returns>
private static string? TryGetUnsupportedOpenObjectKeywordName(JsonElement element)
{
if (element.TryGetProperty("additionalProperties", out var additionalPropertiesElement) &&
additionalPropertiesElement.ValueKind != JsonValueKind.False)
{
return "additionalProperties";
}

return element.TryGetProperty("patternProperties", out _) ? "patternProperties" :
element.TryGetProperty("propertyNames", out _) ? "propertyNames" :
element.TryGetProperty("unevaluatedProperties", out _) ? "unevaluatedProperties" :
null;
}

/// <summary>
/// 解析 schema 节点声明的类型名称,并在缺失或类型错误时立刻给出定位清晰的诊断。
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1965,6 +1965,58 @@ public void Run_Should_Report_Diagnostic_When_Object_Schema_Declares_Unsupported
});
}

/// <summary>
/// 验证生成器会拒绝会重新打开对象形状的其他开放对象关键字。
/// </summary>
[TestCase("patternProperties", """
{
"^dynamic-": { "type": "integer" }
}
""")]
[TestCase("propertyNames", """
{
"pattern": "^[a-z]+$"
}
""")]
[TestCase("unevaluatedProperties", "false")]
public void Run_Should_Report_Diagnostic_When_Object_Schema_Declares_Unsupported_OpenObject_Keyword(
string keywordName,
string keywordValueJson)
{
const string source = DummySource;
var schema = $$"""
{
"type": "object",
"required": ["id", "reward"],
"properties": {
"id": { "type": "integer" },
"reward": {
"type": "object",
"{{keywordName}}": {{keywordValueJson}},
"properties": {
"itemCount": { "type": "integer" }
}
}
}
}
""";

var result = SchemaGeneratorTestDriver.Run(
source,
("monster.schema.json", schema));

var diagnostic = result.Results.Single().Diagnostics.Single();

Assert.Multiple(() =>
{
Assert.That(diagnostic.Id, Is.EqualTo("GF_ConfigSchema_016"));
Assert.That(diagnostic.Severity, Is.EqualTo(DiagnosticSeverity.Error));
Assert.That(diagnostic.GetMessage(), Does.Contain("reward"));
Assert.That(diagnostic.GetMessage(), Does.Contain(keywordName));
Assert.That(diagnostic.GetMessage(), Does.Contain("rejects keywords that reopen object shapes"));
});
}

/// <summary>
/// 验证 <c>then</c> 子 schema 内的非法 <c>format</c> 也会在生成阶段直接给出诊断。
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- 当前焦点:
- 已完成 object-focused `if` / `then` / `else`,继续评估下一批仍不改变生成类型形状的共享关键字
- 已明确将 `oneOf` / `anyOf` 归类为当前不支持的组合关键字,并在 Runtime / Generator / Tooling 三端显式拒绝,避免静默接受导致形状漂移
- 已把开放对象关键字边界收紧为只接受 `additionalProperties: false`,并在 Runtime / Generator / Tooling 三端显式拒绝 `patternProperties`、`propertyNames`、`unevaluatedProperties`
- 已完成 PR #262 的 CodeRabbit follow-up,补齐 latest review body 中 folded `Nitpick comments` 的 skill 解析并按建议收口 Tooling / Tests
- 先以 Runtime / Generator / Tooling 三端一致语义为前提筛选下一项,而不是盲目扩全量 JSON Schema
- Tooling / Docs 后续改为非阻塞并行 lane;active 入口只保留主线恢复点,把批处理细节下沉到 backlog 文件
Expand All @@ -20,6 +21,8 @@

- 组合关键字扩展风险:下一批候选关键字可能像标准 `oneOf` / `anyOf` 一样更容易引入生成类型形状漂移
- 缓解措施:`oneOf` / `anyOf` 已改为三端显式拒绝;后续仅继续评估不会引入联合形状、属性合并或分支生成漂移的关键字子集
- 开放对象形状风险:如果某一端静默接受 `patternProperties`、`propertyNames`、`unevaluatedProperties` 等关键字,会重新打开对象形状并造成契约漂移
- 缓解措施:当前三端已统一把开放对象边界收紧为只接受 `additionalProperties: false`,其余开放对象关键字直接报错
- 工具链验证风险:VS Code 与 CI / 发布管道验证覆盖不足
- 缓解措施:继续为新增共享关键字补齐三端测试覆盖,优先保证 C# Runtime 与 Generator 回归通过,并记录 JS 测试与构建验证
- PR review 信号漂移风险:CodeRabbit 可能把建议折叠在 latest review body,而不是 issue comments
Expand All @@ -40,6 +43,9 @@
- `minProperties`、`maxProperties`、`dependentRequired`、`dependentSchemas`、`allOf`、object-focused `if` / `then` / `else`
- 已明确拒绝会改变生成类型形状的组合关键字:
- `oneOf`、`anyOf` 当前会在 Runtime / Generator / Tooling 三端直接报错,而不是静默忽略
- 已明确拒绝会重新打开对象形状的开放对象关键字:
- 当前只接受 `additionalProperties: false`
- `patternProperties`、`propertyNames`、`unevaluatedProperties` 当前会在 Runtime / Generator / Tooling 三端直接报错,而不是静默忽略
- `if` / `then` / `else` 已按“不改变生成类型形状”的边界落地:
- 只允许 object 节点上的 object-typed inline schema
- `if` 必填,且必须至少伴随 `then` 或 `else` 之一
Expand Down Expand Up @@ -86,11 +92,13 @@
- `2026-04-17` 之前的详细实现记录与定向验证命令已归档到历史 tracking / trace
- active 跟踪文件只保留当前恢复点、当前状态和下一步,不再重复堆积已完成阶段的完整历史
- 最近验证摘要:`2026-04-30` 已完成 Tooling / Docs reader-facing 收口与工具 parser 边界收紧,详细命令、批次背景与验证结果保留在 trace 的 `2026-04-30` 分阶段记录中
- 最近验证摘要:`2026-05-06` 已完成开放对象关键字边界收口;Runtime / Generator / Tooling 现统一拒绝 `patternProperties`、`propertyNames`、`unevaluatedProperties`,并保留 `additionalProperties: false` 作为唯一共享闭合对象入口;详细命令与批次背景保留在 trace 的 `2026-05-06` 记录中
- 最近验证摘要:`2026-05-06` 已按 PR `#325` latest review follow-up 移除三端开放对象校验中的不可达 `additionalProperties: false` 放行分支,补齐 Tooling 正向回归,并同步拆分 reader-facing docs 对开放对象边界的表述;细节与验证命令保留在 trace 的 `2026-05-06` 追加记录中
- PR `#306` follow-up 摘要:已按 latest open review threads 补齐 Generator `anyOf` 对称回归、Tooling schema type 白名单、object-array 直系收集边界,以及 reader-facing docs 的显式 `additionalProperties: false` / adoption guidance 说明;细节和验证命令保留在 trace 的 `2026-04-30` 新增阶段记录中
- PR review 跟进指针:当前分支的 latest review follow-up 与后续本地核验结论以 `ai-first-config-system-trace.md` 为准,active tracking 不再重复展开逐条命令历史

## 下一步

1. 主线继续回到 `YamlConfigSchemaValidator.cs`、`SchemaConfigGenerator.cs` 与 `configValidation.js` 的共享关键字盘点,默认跳过 `oneOf` / `anyOf`
1. 主线继续回到 `YamlConfigSchemaValidator.cs`、`SchemaConfigGenerator.cs` 与 `configValidation.js` 的共享关键字盘点,默认跳过 `oneOf` / `anyOf` 以及开放对象关键字扩展
2. Tooling / Docs 若要并发推进,优先补 reader-facing 示例或采用路径,不再重复扩写能力边界说明
3. 保持 active tracking / trace 精简,只记录当前恢复点、最近验证和下一步恢复指针
Loading
Loading