mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
fix(game-config): 显式拒绝 oneOf 与 anyOf 组合关键字
- 修复 Runtime、Source Generator 与 Tooling 对 oneOf/anyOf 的静默接受,统一改为显式报错 - 补充 JS 与 Release 测试回归,覆盖生成器诊断和运行时拒绝路径 - 更新 ai-plan 跟踪与中文文档,明确后续默认跳过会改变生成类型形状的组合关键字
This commit is contained in:
parent
8d6fc74b3d
commit
d6a154726c
@ -19,3 +19,4 @@
|
||||
GF_ConfigSchema_012 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||
GF_ConfigSchema_013 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||
GF_ConfigSchema_014 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||
GF_ConfigSchema_015 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||
|
||||
@ -232,6 +232,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
||||
}
|
||||
|
||||
return TryValidateStringFormatMetadataRecursively(filePath, "<root>", root, out diagnostic) &&
|
||||
TryValidateUnsupportedCombinatorKeywordsRecursively(filePath, "<root>", root, out diagnostic) &&
|
||||
TryValidateDependentRequiredMetadataRecursively(filePath, "<root>", root, out diagnostic) &&
|
||||
TryValidateDependentSchemasMetadataRecursively(filePath, "<root>", root, out diagnostic) &&
|
||||
TryValidateAllOfMetadataRecursively(filePath, "<root>", root, out diagnostic) &&
|
||||
@ -844,6 +845,80 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
||||
out diagnostic);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 递归拒绝当前共享子集尚未支持的组合关键字。
|
||||
/// 这里显式拦截 <c>oneOf</c> / <c>anyOf</c>,避免生成器静默接受会改变生成类型形状的 schema。
|
||||
/// </summary>
|
||||
/// <param name="filePath">Schema 文件路径。</param>
|
||||
/// <param name="displayPath">逻辑字段路径。</param>
|
||||
/// <param name="element">当前 schema 节点。</param>
|
||||
/// <param name="diagnostic">失败时返回的诊断。</param>
|
||||
/// <returns>当前节点树是否未声明不支持的组合关键字。</returns>
|
||||
private static bool TryValidateUnsupportedCombinatorKeywordsRecursively(
|
||||
string filePath,
|
||||
string displayPath,
|
||||
JsonElement element,
|
||||
out Diagnostic? diagnostic)
|
||||
{
|
||||
return TryTraverseSchemaRecursively(
|
||||
filePath,
|
||||
displayPath,
|
||||
element,
|
||||
static (currentFilePath, currentDisplayPath, currentElement, _) =>
|
||||
{
|
||||
return TryValidateUnsupportedCombinatorKeywords(
|
||||
currentFilePath,
|
||||
currentDisplayPath,
|
||||
currentElement,
|
||||
out var currentDiagnostic)
|
||||
? (true, (Diagnostic?)null)
|
||||
: (false, currentDiagnostic);
|
||||
},
|
||||
out diagnostic);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当前节点是否声明了会改变生成类型形状的未支持组合关键字。
|
||||
/// </summary>
|
||||
/// <param name="filePath">Schema 文件路径。</param>
|
||||
/// <param name="displayPath">逻辑字段路径。</param>
|
||||
/// <param name="element">当前 schema 节点。</param>
|
||||
/// <param name="diagnostic">失败时返回的诊断。</param>
|
||||
/// <returns>未声明不支持关键字时返回 <see langword="true" />。</returns>
|
||||
private static bool TryValidateUnsupportedCombinatorKeywords(
|
||||
string filePath,
|
||||
string displayPath,
|
||||
JsonElement element,
|
||||
out Diagnostic? diagnostic)
|
||||
{
|
||||
diagnostic = null;
|
||||
if (TryGetUnsupportedCombinatorKeywordName(element) is not { } keywordName)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
diagnostic = Diagnostic.Create(
|
||||
ConfigSchemaDiagnostics.UnsupportedCombinatorKeyword,
|
||||
CreateFileLocation(filePath),
|
||||
Path.GetFileName(filePath),
|
||||
displayPath,
|
||||
keywordName,
|
||||
"The current config schema subset does not support combinators that can change generated type shape.");
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回当前节点声明的首个未支持组合关键字。
|
||||
/// </summary>
|
||||
/// <param name="element">当前 schema 节点。</param>
|
||||
/// <returns>命中的关键字名称;未声明时返回空。</returns>
|
||||
private static string? TryGetUnsupportedCombinatorKeywordName(JsonElement element)
|
||||
{
|
||||
return element.TryGetProperty("oneOf", out _) ? "oneOf" :
|
||||
element.TryGetProperty("anyOf", out _) ? "anyOf" :
|
||||
null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 以统一顺序递归遍历 schema 树,并把每个节点交给调用方提供的校验逻辑。
|
||||
/// 该遍历覆盖对象属性、<c>dependentSchemas</c> / <c>allOf</c> /
|
||||
|
||||
@ -162,4 +162,15 @@ public static class ConfigSchemaDiagnostics
|
||||
SourceGeneratorsConfigCategory,
|
||||
DiagnosticSeverity.Error,
|
||||
true);
|
||||
|
||||
/// <summary>
|
||||
/// schema 节点声明了当前共享子集尚未支持的组合关键字。
|
||||
/// </summary>
|
||||
public static readonly DiagnosticDescriptor UnsupportedCombinatorKeyword = new(
|
||||
"GF_ConfigSchema_015",
|
||||
"Config schema uses an unsupported combinator keyword",
|
||||
"Property '{1}' in schema file '{0}' uses unsupported combinator keyword '{2}': {3}",
|
||||
SourceGeneratorsConfigCategory,
|
||||
DiagnosticSeverity.Error,
|
||||
true);
|
||||
}
|
||||
|
||||
@ -270,6 +270,60 @@ public sealed class YamlConfigLoaderAllOfTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证运行时会显式拒绝当前共享子集尚未支持的 <c>oneOf</c>。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void LoadAsync_Should_Throw_When_Object_Schema_Declares_Unsupported_OneOf()
|
||||
{
|
||||
CreateConfigFile(
|
||||
"monster/slime.yaml",
|
||||
BuildMonsterConfigYaml(
|
||||
"""
|
||||
itemCount: 3
|
||||
"""));
|
||||
CreateSchemaFile(
|
||||
"schemas/monster.schema.json",
|
||||
BuildMonsterSchema(
|
||||
DefaultRewardPropertiesJson,
|
||||
"""
|
||||
[
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["itemCount"],
|
||||
"properties": {
|
||||
"itemCount": { "type": "integer" }
|
||||
}
|
||||
}
|
||||
]
|
||||
""",
|
||||
"""
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["bonus"],
|
||||
"properties": {
|
||||
"bonus": { "type": "integer" }
|
||||
}
|
||||
}
|
||||
]
|
||||
"""));
|
||||
|
||||
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 combinator keyword 'oneOf'"));
|
||||
Assert.That(registry.Count, Is.EqualTo(0));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 allOf 条目只接受 object-typed schema。
|
||||
/// </summary>
|
||||
@ -566,10 +620,12 @@ public sealed class YamlConfigLoaderAllOfTests
|
||||
/// </summary>
|
||||
/// <param name="rewardPropertiesJson">奖励对象的 properties JSON 片段。</param>
|
||||
/// <param name="allOfJson">allOf 约束的 JSON 数组片段。</param>
|
||||
/// <param name="additionalRewardKeywordsJson">追加到奖励对象上的额外关键字 JSON 片段。</param>
|
||||
/// <returns>完整的 schema JSON 文本。</returns>
|
||||
private static string BuildMonsterSchema(
|
||||
string rewardPropertiesJson,
|
||||
string allOfJson)
|
||||
string allOfJson,
|
||||
string additionalRewardKeywordsJson = "")
|
||||
{
|
||||
return $$"""
|
||||
{
|
||||
@ -580,7 +636,7 @@ public sealed class YamlConfigLoaderAllOfTests
|
||||
"reward": {
|
||||
"type": "object",
|
||||
"properties": {{rewardPropertiesJson}},
|
||||
"allOf": {{allOfJson}}
|
||||
"allOf": {{allOfJson}}{{(string.IsNullOrWhiteSpace(additionalRewardKeywordsJson) ? string.Empty : "," + Environment.NewLine + additionalRewardKeywordsJson.Trim())}}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -321,6 +321,7 @@ internal static partial class YamlConfigSchemaValidator
|
||||
JsonElement element,
|
||||
bool isRoot = false)
|
||||
{
|
||||
ValidateUnsupportedCombinatorKeywords(tableName, schemaPath, propertyPath, element);
|
||||
var typeName = ResolveNodeTypeName(tableName, schemaPath, propertyPath, element);
|
||||
var referenceTableName = TryGetReferenceTableName(tableName, schemaPath, propertyPath, element);
|
||||
ValidateObjectOnlyKeywords(tableName, schemaPath, propertyPath, element, typeName);
|
||||
@ -336,6 +337,47 @@ internal static partial class YamlConfigSchemaValidator
|
||||
return parsedNode.WithNegatedSchemaNode(ParseNegatedSchemaNode(tableName, schemaPath, propertyPath, element));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显式拒绝当前共享子集中尚未支持、且会改变生成类型形状的组合关键字。
|
||||
/// 这样 Runtime / Generator / Tooling 会对同一份 schema 给出一致失败,
|
||||
/// 而不是默默忽略 <c>oneOf</c> / <c>anyOf</c> 造成接受范围漂移。
|
||||
/// </summary>
|
||||
/// <param name="tableName">所属配置表名称。</param>
|
||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||
/// <param name="propertyPath">当前节点的逻辑属性路径。</param>
|
||||
/// <param name="element">当前 schema 节点。</param>
|
||||
private static void ValidateUnsupportedCombinatorKeywords(
|
||||
string tableName,
|
||||
string schemaPath,
|
||||
string propertyPath,
|
||||
JsonElement element)
|
||||
{
|
||||
if (TryGetUnsupportedCombinatorKeywordName(element) is not { } keywordName)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"Property '{propertyPath}' in schema file '{schemaPath}' declares unsupported combinator keyword '{keywordName}'. " +
|
||||
"The current config schema subset does not support combinators that can change generated type shape.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回当前节点声明的首个未支持组合关键字。
|
||||
/// </summary>
|
||||
/// <param name="element">当前 schema 节点。</param>
|
||||
/// <returns>命中的关键字名称;未声明时返回空。</returns>
|
||||
private static string? TryGetUnsupportedCombinatorKeywordName(JsonElement element)
|
||||
{
|
||||
return element.TryGetProperty("oneOf", out _) ? "oneOf" :
|
||||
element.TryGetProperty("anyOf", out _) ? "anyOf" :
|
||||
null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析 schema 节点声明的类型名称,并在缺失或类型错误时立刻给出定位清晰的诊断。
|
||||
/// </summary>
|
||||
|
||||
@ -1795,6 +1795,54 @@ public class SchemaConfigGeneratorTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证生成器会显式拒绝当前共享子集尚未支持的 <c>oneOf</c>。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Run_Should_Report_Diagnostic_When_Object_Schema_Declares_Unsupported_OneOf()
|
||||
{
|
||||
const string source = DummySource;
|
||||
const string schema = """
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "reward"],
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"reward": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"itemCount": { "type": "integer" }
|
||||
},
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["itemCount"],
|
||||
"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_015"));
|
||||
Assert.That(diagnostic.Severity, Is.EqualTo(DiagnosticSeverity.Error));
|
||||
Assert.That(diagnostic.GetMessage(), Does.Contain("reward"));
|
||||
Assert.That(diagnostic.GetMessage(), Does.Contain("oneOf"));
|
||||
Assert.That(diagnostic.GetMessage(), Does.Contain("does not support combinators that can change generated type shape"));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 <c>then</c> 子 schema 内的非法 <c>format</c> 也会在生成阶段直接给出诊断。
|
||||
/// </summary>
|
||||
|
||||
@ -37,8 +37,8 @@
|
||||
|
||||
- [x] 继续扩展最有价值的 JSON Schema 子集
|
||||
- 原则:只做 Runtime / Generator / Tooling 三端都能稳定解释的关键字
|
||||
- 已补齐:`enum`(当前覆盖标量、对象、数组节点,以及标量数组元素)、`const`、`not`、`pattern`、`format`(当前稳定子集:`date`、`date-time`、`duration`、`email`、`time`、`uri`、`uuid`)、`minItems`、`maxItems`、`exclusiveMinimum`、`exclusiveMaximum`、`multipleOf`、`uniqueItems`、`minProperties`、`maxProperties`、`dependentRequired`、`dependentSchemas`、`allOf`、object-focused `if` / `then` / `else`
|
||||
- 当前产出:运行时拒绝相关约束违规值,VS Code 校验与表单 hint 对齐,生成代码 XML 文档同步暴露新关键字;对象 / 数组 `enum` 当前主要参与校验与文档输出,不额外扩展复杂表单控件;`allOf` 与 `if` / `then` / `else` 当前都收敛为 object-focused constraint block,不做属性合并
|
||||
- 已补齐:`enum`(当前覆盖标量、对象、数组节点,以及标量数组元素)、`const`、`not`、`pattern`、`format`(当前稳定子集:`date`、`date-time`、`duration`、`email`、`time`、`uri`、`uuid`)、`minLength`、`maxLength`、`minItems`、`maxItems`、`contains`、`minContains`、`maxContains`、`exclusiveMinimum`、`exclusiveMaximum`、`multipleOf`、`uniqueItems`、`minProperties`、`maxProperties`、`dependentRequired`、`dependentSchemas`、`allOf`、object-focused `if` / `then` / `else`
|
||||
- 当前产出:运行时拒绝相关约束违规值,VS Code 校验与表单 hint 对齐,生成代码 XML 文档同步暴露新关键字;对象 / 数组 `enum` 当前主要参与校验与文档输出,不额外扩展复杂表单控件;`allOf` 与 `if` / `then` / `else` 当前都收敛为 object-focused constraint block,不做属性合并;`oneOf` / `anyOf` 当前已统一定义为不支持并在三端显式拒绝
|
||||
|
||||
- [x] 评估可选只读索引能力
|
||||
- 目标:为高频查询字段提供比 `All()` 线性扫描更强的读取体验
|
||||
@ -63,7 +63,7 @@
|
||||
## 暂缓
|
||||
|
||||
- [ ] 不追求完整 JSON Schema 全量支持
|
||||
- 原因:维护成本高,且容易造成 Runtime / Generator / Tooling 三端漂移
|
||||
- 原因:维护成本高,且容易造成 Runtime / Generator / Tooling 三端漂移;像 `oneOf` / `anyOf` 这类会改变生成类型形状的组合关键字当前已明确排除
|
||||
|
||||
- [ ] 不优先做运行时可写配置
|
||||
- 原因:当前系统定位仍然是静态内容只读查询
|
||||
@ -87,6 +87,7 @@
|
||||
## 下次恢复点
|
||||
|
||||
- 在当前稳定 `format` 子集(`date`、`date-time`、`duration`、`email`、`time`、`uri`、`uuid`)、object-focused `allOf` 与 object-focused `if` / `then` / `else` 之后,转到下一批仍不改变生成类型形状的关键字评估;仍然不要先回工具 UI
|
||||
- `oneOf` / `anyOf` 已明确跳过;恢复时不要再把它们当作默认候选
|
||||
- 恢复时优先检查:
|
||||
- `GFramework.Game/Config/YamlConfigSchemaValidator.cs`
|
||||
- `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs`
|
||||
@ -108,5 +109,5 @@
|
||||
- 结果:通过
|
||||
- 下一步:
|
||||
1. 检查 `YamlConfigSchemaValidator.cs`、`SchemaConfigGenerator.cs`、`configValidation.js` 中当前已支持的关键字列表
|
||||
2. 评估 `oneOf` / `anyOf` 是否存在可接受的 object-focused 子集
|
||||
3. 若结论否定,选择下一批共享解释关键字而不是先回工具 UI
|
||||
2. 跳过 `oneOf` / `anyOf`,选择下一批仍不改变生成类型形状的共享关键字
|
||||
3. 优先找不需要属性合并、联合分支生成或额外 UI 形状解释的关键字,而不是先回工具 UI
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
- 当前阶段:`C# Runtime + Source Generator + Consumer DX`
|
||||
- 当前焦点:
|
||||
- 已完成 object-focused `if` / `then` / `else`,继续评估下一批仍不改变生成类型形状的共享关键字
|
||||
- 已明确将 `oneOf` / `anyOf` 归类为当前不支持的组合关键字,并在 Runtime / Generator / Tooling 三端显式拒绝,避免静默接受导致形状漂移
|
||||
- 已完成 PR #262 的 CodeRabbit follow-up,补齐 latest review body 中 folded `Nitpick comments` 的 skill 解析并按建议收口 Tooling / Tests
|
||||
- 先以 Runtime / Generator / Tooling 三端一致语义为前提筛选下一项,而不是盲目扩全量 JSON Schema
|
||||
- 继续把 VS Code 工具能力视为非阻塞项,不让复杂 UI 编辑器需求反过来拖慢 C# 主线
|
||||
@ -18,7 +19,7 @@
|
||||
### 已知风险
|
||||
|
||||
- 组合关键字扩展风险:下一批候选关键字可能像标准 `oneOf` / `anyOf` 一样更容易引入生成类型形状漂移
|
||||
- 缓解措施:延续 object-focused / focused matcher 约束,只接受三端都能稳定解释且不需要属性合并的子集
|
||||
- 缓解措施:`oneOf` / `anyOf` 已改为三端显式拒绝;后续仅继续评估不会引入联合形状、属性合并或分支生成漂移的关键字子集
|
||||
- 工具链验证风险:VS Code 与 CI / 发布管道验证覆盖不足
|
||||
- 缓解措施:继续为新增共享关键字补齐三端测试覆盖,优先保证 C# Runtime 与 Generator 回归通过,并记录 JS 测试与构建验证
|
||||
- PR review 信号漂移风险:CodeRabbit 可能把建议折叠在 latest review body,而不是 issue comments
|
||||
@ -35,8 +36,10 @@
|
||||
- 已补齐一批共享 JSON Schema 子集,包括:
|
||||
- `enum`、`const`、`not`、`pattern`
|
||||
- `format` 稳定子集:`date`、`date-time`、`duration`、`email`、`time`、`uri`、`uuid`
|
||||
- `minItems`、`maxItems`、`exclusiveMinimum`、`exclusiveMaximum`、`multipleOf`、`uniqueItems`
|
||||
- `minLength`、`maxLength`、`minItems`、`maxItems`、`contains`、`minContains`、`maxContains`、`exclusiveMinimum`、`exclusiveMaximum`、`multipleOf`、`uniqueItems`
|
||||
- `minProperties`、`maxProperties`、`dependentRequired`、`dependentSchemas`、`allOf`、object-focused `if` / `then` / `else`
|
||||
- 已明确拒绝会改变生成类型形状的组合关键字:
|
||||
- `oneOf`、`anyOf` 当前会在 Runtime / Generator / Tooling 三端直接报错,而不是静默忽略
|
||||
- `if` / `then` / `else` 已按“不改变生成类型形状”的边界落地:
|
||||
- 只允许 object 节点上的 object-typed inline schema
|
||||
- `if` 必填,且必须至少伴随 `then` 或 `else` 之一
|
||||
@ -95,4 +98,4 @@
|
||||
|
||||
1. 提交并推送当前 PR `#262` follow-up 修复后,重新抓取一次 PR review,确认 outside-diff comment 与 open thread 是否都已收口
|
||||
2. 若 PR review 已收口,再回到 `GFramework.Game/Config/YamlConfigSchemaValidator.cs`、`GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs`、`tools/gframework-config-tool/src/configValidation.js` 盘点下一批候选关键字
|
||||
3. 优先判断 `oneOf` / `anyOf` 是否存在可接受的 object-focused 子集;若仍会引入生成类型形状漂移,就直接跳过
|
||||
3. 跳过 `oneOf` / `anyOf`,优先筛选下一个仍不改变生成类型形状、且不需要属性合并或联合分支生成的共享关键字
|
||||
|
||||
@ -106,6 +106,32 @@
|
||||
|
||||
### 下一步
|
||||
|
||||
1. 评估 `oneOf` / `anyOf` 是否值得继续沿用 object-focused 子集;若仍会造成生成形状漂移,就直接跳过
|
||||
1. 跳过 `oneOf` / `anyOf`,优先筛选下一个仍不改变生成类型形状、且不需要属性合并或联合分支生成的共享关键字
|
||||
2. 若继续扩共享关键字,先在 Runtime / Generator / Tooling 三端同时定义一致边界,再进入实现
|
||||
3. 继续把 active 入口保持精简,只记录当前恢复点、验证与下一步
|
||||
|
||||
## 2026-04-30
|
||||
|
||||
### 阶段:组合关键字边界收口(AI-FIRST-CONFIG-RP-003)
|
||||
|
||||
- 已在 Runtime、Source Generator 与 VS Code Tooling 三端显式拒绝 `oneOf` / `anyOf`
|
||||
- 本轮结论不是继续做 object-focused 子集,而是先收紧共享边界:
|
||||
- `oneOf` / `anyOf` 更容易引入联合分支、属性合并或生成类型形状漂移
|
||||
- 当前配置系统主线仍优先保证 `C# Runtime + Source Generator + Consumer DX` 的稳定契约
|
||||
- 因此三端统一改为在 schema 解析 / 生成阶段直接失败,避免静默忽略同一份 schema
|
||||
- active tracking 也已同步更新,不再把 `oneOf` / `anyOf` 作为下一批默认候选
|
||||
|
||||
### 验证
|
||||
|
||||
- 2026-04-30:`bun run test`(`tools/gframework-config-tool`)
|
||||
- 目标:验证工具端会拒绝 `oneOf`
|
||||
- 2026-04-30:`dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~SchemaConfigGeneratorTests"`
|
||||
- 目标:验证生成器新增 `GF_ConfigSchema_015`
|
||||
- 2026-04-30:`dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~YamlConfigLoaderAllOfTests"`
|
||||
- 目标:验证运行时会拒绝对象节点上的 `oneOf`
|
||||
|
||||
### 下一步
|
||||
|
||||
1. 若本轮定向验证通过,继续盘点下一批真正低风险、且不改变生成类型形状的共享关键字
|
||||
2. 不再重复评估 `oneOf` / `anyOf` 的 object-focused 子集,除非未来主线明确接受联合形状生成
|
||||
3. 若后续关键字需要新诊断编号或文档边界说明,继续保持 Runtime / Generator / Tooling 同步收口
|
||||
|
||||
@ -802,6 +802,7 @@ if (MonsterConfigBindings.References.TryGetByDisplayPath("dropItems", out var re
|
||||
- `dependentSchemas`:供运行时校验、VS Code 校验、对象 section 表单 hint 和生成代码 XML 文档复用;当前只接受“已声明 sibling 字段触发 object 子 schema”的形状,不改变生成类型形状,并按 focused constraint block 语义允许条件子 schema 未声明的额外同级字段继续存在
|
||||
- `allOf`:供运行时校验、VS Code 校验、对象 section 表单 hint 和生成代码 XML 文档复用;当前只接受 object 节点上的 object-typed inline schema 数组,并按 focused constraint block 语义把每个条目叠加到当前对象上,不做属性合并,也不改变生成类型形状
|
||||
- `if` / `then` / `else`:供运行时校验、VS Code 校验、对象 section 表单 hint 和生成代码 XML 文档复用;当前只接受 object 节点上的 object-typed inline schema,`if` 必填且必须至少配合 `then` 或 `else` 之一使用,分支只能约束父对象已声明的字段,不做属性合并,也不改变生成类型形状;条件匹配本身沿用 `dependentSchemas` / `allOf` 的 focused matcher 语义,允许对象保留未在条件块中声明的额外同级字段
|
||||
- `oneOf` / `anyOf`:当前不属于共享支持子集;Runtime / Generator / Tooling 会在解析或生成阶段直接拒绝,避免静默接受会改变生成类型形状的组合关键字
|
||||
|
||||
`allOf` 的最小可工作示例如下。关键点是:字段形状先在父对象 `properties` 中声明,再用 `allOf` 叠加 `required` 或更细的字段约束;`allOf` 条目不会把新字段并回父对象。
|
||||
|
||||
|
||||
@ -1095,6 +1095,13 @@ function unquoteScalar(value) {
|
||||
*/
|
||||
function parseSchemaNode(rawNode, displayPath) {
|
||||
const value = rawNode && typeof rawNode === "object" ? rawNode : {};
|
||||
const unsupportedCombinatorKeyword = getUnsupportedCombinatorKeywordName(value);
|
||||
if (unsupportedCombinatorKeyword) {
|
||||
throw new Error(
|
||||
`Schema property '${displayPath}' declares unsupported combinator keyword '${unsupportedCombinatorKeyword}'. ` +
|
||||
"The current config schema subset does not support combinators that can change generated type shape.");
|
||||
}
|
||||
|
||||
const type = typeof value.type === "string" ? value.type : "object";
|
||||
const patternMetadata = normalizeSchemaPattern(value.pattern, displayPath);
|
||||
const stringFormat = normalizeSchemaStringFormat(value.format, type, displayPath);
|
||||
@ -1253,6 +1260,25 @@ function parseSchemaNode(rawNode, displayPath) {
|
||||
}, value.const, displayPath), value.enum, displayPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the first combinator keyword that the current shared schema subset
|
||||
* intentionally rejects to keep Runtime / Generator / Tooling behavior aligned.
|
||||
*
|
||||
* @param {Record<string, unknown>} schemaNode Raw schema object.
|
||||
* @returns {string | undefined} Unsupported keyword name when present.
|
||||
*/
|
||||
function getUnsupportedCombinatorKeywordName(schemaNode) {
|
||||
if (Object.prototype.hasOwnProperty.call(schemaNode, "oneOf")) {
|
||||
return "oneOf";
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(schemaNode, "anyOf")) {
|
||||
return "anyOf";
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse one optional `not` sub-schema and keep path formatting aligned with
|
||||
* the runtime/generator diagnostics.
|
||||
|
||||
@ -178,6 +178,33 @@ test("parseSchemaContent should preserve empty-string const raw and display meta
|
||||
assert.equal(schema.properties.name.constDisplayValue, "\"\"");
|
||||
});
|
||||
|
||||
test("parseSchemaContent should reject unsupported oneOf combinators", () => {
|
||||
assert.throws(
|
||||
() => parseSchemaContent(`
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reward": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"itemCount": { "type": "integer" }
|
||||
},
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["itemCount"],
|
||||
"properties": {
|
||||
"itemCount": { "type": "integer" }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
`),
|
||||
/unsupported combinator keyword 'oneOf'/u);
|
||||
});
|
||||
|
||||
test("parseSchemaContent should build object const comparable keys with ordinal property ordering", () => {
|
||||
const schema = parseSchemaContent(`
|
||||
{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user