mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +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_012 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||||
GF_ConfigSchema_013 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
GF_ConfigSchema_013 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||||
GF_ConfigSchema_014 | 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) &&
|
return TryValidateStringFormatMetadataRecursively(filePath, "<root>", root, out diagnostic) &&
|
||||||
|
TryValidateUnsupportedCombinatorKeywordsRecursively(filePath, "<root>", root, out diagnostic) &&
|
||||||
TryValidateDependentRequiredMetadataRecursively(filePath, "<root>", root, out diagnostic) &&
|
TryValidateDependentRequiredMetadataRecursively(filePath, "<root>", root, out diagnostic) &&
|
||||||
TryValidateDependentSchemasMetadataRecursively(filePath, "<root>", root, out diagnostic) &&
|
TryValidateDependentSchemasMetadataRecursively(filePath, "<root>", root, out diagnostic) &&
|
||||||
TryValidateAllOfMetadataRecursively(filePath, "<root>", root, out diagnostic) &&
|
TryValidateAllOfMetadataRecursively(filePath, "<root>", root, out diagnostic) &&
|
||||||
@ -844,6 +845,80 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
out diagnostic);
|
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>
|
/// <summary>
|
||||||
/// 以统一顺序递归遍历 schema 树,并把每个节点交给调用方提供的校验逻辑。
|
/// 以统一顺序递归遍历 schema 树,并把每个节点交给调用方提供的校验逻辑。
|
||||||
/// 该遍历覆盖对象属性、<c>dependentSchemas</c> / <c>allOf</c> /
|
/// 该遍历覆盖对象属性、<c>dependentSchemas</c> / <c>allOf</c> /
|
||||||
|
|||||||
@ -162,4 +162,15 @@ public static class ConfigSchemaDiagnostics
|
|||||||
SourceGeneratorsConfigCategory,
|
SourceGeneratorsConfigCategory,
|
||||||
DiagnosticSeverity.Error,
|
DiagnosticSeverity.Error,
|
||||||
true);
|
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>
|
/// <summary>
|
||||||
/// 验证 allOf 条目只接受 object-typed schema。
|
/// 验证 allOf 条目只接受 object-typed schema。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -566,10 +620,12 @@ public sealed class YamlConfigLoaderAllOfTests
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="rewardPropertiesJson">奖励对象的 properties JSON 片段。</param>
|
/// <param name="rewardPropertiesJson">奖励对象的 properties JSON 片段。</param>
|
||||||
/// <param name="allOfJson">allOf 约束的 JSON 数组片段。</param>
|
/// <param name="allOfJson">allOf 约束的 JSON 数组片段。</param>
|
||||||
|
/// <param name="additionalRewardKeywordsJson">追加到奖励对象上的额外关键字 JSON 片段。</param>
|
||||||
/// <returns>完整的 schema JSON 文本。</returns>
|
/// <returns>完整的 schema JSON 文本。</returns>
|
||||||
private static string BuildMonsterSchema(
|
private static string BuildMonsterSchema(
|
||||||
string rewardPropertiesJson,
|
string rewardPropertiesJson,
|
||||||
string allOfJson)
|
string allOfJson,
|
||||||
|
string additionalRewardKeywordsJson = "")
|
||||||
{
|
{
|
||||||
return $$"""
|
return $$"""
|
||||||
{
|
{
|
||||||
@ -580,7 +636,7 @@ public sealed class YamlConfigLoaderAllOfTests
|
|||||||
"reward": {
|
"reward": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {{rewardPropertiesJson}},
|
"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,
|
JsonElement element,
|
||||||
bool isRoot = false)
|
bool isRoot = false)
|
||||||
{
|
{
|
||||||
|
ValidateUnsupportedCombinatorKeywords(tableName, schemaPath, propertyPath, element);
|
||||||
var typeName = ResolveNodeTypeName(tableName, schemaPath, propertyPath, element);
|
var typeName = ResolveNodeTypeName(tableName, schemaPath, propertyPath, element);
|
||||||
var referenceTableName = TryGetReferenceTableName(tableName, schemaPath, propertyPath, element);
|
var referenceTableName = TryGetReferenceTableName(tableName, schemaPath, propertyPath, element);
|
||||||
ValidateObjectOnlyKeywords(tableName, schemaPath, propertyPath, element, typeName);
|
ValidateObjectOnlyKeywords(tableName, schemaPath, propertyPath, element, typeName);
|
||||||
@ -336,6 +337,47 @@ internal static partial class YamlConfigSchemaValidator
|
|||||||
return parsedNode.WithNegatedSchemaNode(ParseNegatedSchemaNode(tableName, schemaPath, propertyPath, element));
|
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>
|
/// <summary>
|
||||||
/// 解析 schema 节点声明的类型名称,并在缺失或类型错误时立刻给出定位清晰的诊断。
|
/// 解析 schema 节点声明的类型名称,并在缺失或类型错误时立刻给出定位清晰的诊断。
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// 验证 <c>then</c> 子 schema 内的非法 <c>format</c> 也会在生成阶段直接给出诊断。
|
/// 验证 <c>then</c> 子 schema 内的非法 <c>format</c> 也会在生成阶段直接给出诊断。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -37,8 +37,8 @@
|
|||||||
|
|
||||||
- [x] 继续扩展最有价值的 JSON Schema 子集
|
- [x] 继续扩展最有价值的 JSON Schema 子集
|
||||||
- 原则:只做 Runtime / Generator / Tooling 三端都能稳定解释的关键字
|
- 原则:只做 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`
|
- 已补齐:`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,不做属性合并
|
- 当前产出:运行时拒绝相关约束违规值,VS Code 校验与表单 hint 对齐,生成代码 XML 文档同步暴露新关键字;对象 / 数组 `enum` 当前主要参与校验与文档输出,不额外扩展复杂表单控件;`allOf` 与 `if` / `then` / `else` 当前都收敛为 object-focused constraint block,不做属性合并;`oneOf` / `anyOf` 当前已统一定义为不支持并在三端显式拒绝
|
||||||
|
|
||||||
- [x] 评估可选只读索引能力
|
- [x] 评估可选只读索引能力
|
||||||
- 目标:为高频查询字段提供比 `All()` 线性扫描更强的读取体验
|
- 目标:为高频查询字段提供比 `All()` 线性扫描更强的读取体验
|
||||||
@ -63,7 +63,7 @@
|
|||||||
## 暂缓
|
## 暂缓
|
||||||
|
|
||||||
- [ ] 不追求完整 JSON Schema 全量支持
|
- [ ] 不追求完整 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
|
- 在当前稳定 `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/Config/YamlConfigSchemaValidator.cs`
|
||||||
- `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs`
|
- `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs`
|
||||||
@ -108,5 +109,5 @@
|
|||||||
- 结果:通过
|
- 结果:通过
|
||||||
- 下一步:
|
- 下一步:
|
||||||
1. 检查 `YamlConfigSchemaValidator.cs`、`SchemaConfigGenerator.cs`、`configValidation.js` 中当前已支持的关键字列表
|
1. 检查 `YamlConfigSchemaValidator.cs`、`SchemaConfigGenerator.cs`、`configValidation.js` 中当前已支持的关键字列表
|
||||||
2. 评估 `oneOf` / `anyOf` 是否存在可接受的 object-focused 子集
|
2. 跳过 `oneOf` / `anyOf`,选择下一批仍不改变生成类型形状的共享关键字
|
||||||
3. 若结论否定,选择下一批共享解释关键字而不是先回工具 UI
|
3. 优先找不需要属性合并、联合分支生成或额外 UI 形状解释的关键字,而不是先回工具 UI
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
- 当前阶段:`C# Runtime + Source Generator + Consumer DX`
|
- 当前阶段:`C# Runtime + Source Generator + Consumer DX`
|
||||||
- 当前焦点:
|
- 当前焦点:
|
||||||
- 已完成 object-focused `if` / `then` / `else`,继续评估下一批仍不改变生成类型形状的共享关键字
|
- 已完成 object-focused `if` / `then` / `else`,继续评估下一批仍不改变生成类型形状的共享关键字
|
||||||
|
- 已明确将 `oneOf` / `anyOf` 归类为当前不支持的组合关键字,并在 Runtime / Generator / Tooling 三端显式拒绝,避免静默接受导致形状漂移
|
||||||
- 已完成 PR #262 的 CodeRabbit follow-up,补齐 latest review body 中 folded `Nitpick comments` 的 skill 解析并按建议收口 Tooling / Tests
|
- 已完成 PR #262 的 CodeRabbit follow-up,补齐 latest review body 中 folded `Nitpick comments` 的 skill 解析并按建议收口 Tooling / Tests
|
||||||
- 先以 Runtime / Generator / Tooling 三端一致语义为前提筛选下一项,而不是盲目扩全量 JSON Schema
|
- 先以 Runtime / Generator / Tooling 三端一致语义为前提筛选下一项,而不是盲目扩全量 JSON Schema
|
||||||
- 继续把 VS Code 工具能力视为非阻塞项,不让复杂 UI 编辑器需求反过来拖慢 C# 主线
|
- 继续把 VS Code 工具能力视为非阻塞项,不让复杂 UI 编辑器需求反过来拖慢 C# 主线
|
||||||
@ -18,7 +19,7 @@
|
|||||||
### 已知风险
|
### 已知风险
|
||||||
|
|
||||||
- 组合关键字扩展风险:下一批候选关键字可能像标准 `oneOf` / `anyOf` 一样更容易引入生成类型形状漂移
|
- 组合关键字扩展风险:下一批候选关键字可能像标准 `oneOf` / `anyOf` 一样更容易引入生成类型形状漂移
|
||||||
- 缓解措施:延续 object-focused / focused matcher 约束,只接受三端都能稳定解释且不需要属性合并的子集
|
- 缓解措施:`oneOf` / `anyOf` 已改为三端显式拒绝;后续仅继续评估不会引入联合形状、属性合并或分支生成漂移的关键字子集
|
||||||
- 工具链验证风险:VS Code 与 CI / 发布管道验证覆盖不足
|
- 工具链验证风险:VS Code 与 CI / 发布管道验证覆盖不足
|
||||||
- 缓解措施:继续为新增共享关键字补齐三端测试覆盖,优先保证 C# Runtime 与 Generator 回归通过,并记录 JS 测试与构建验证
|
- 缓解措施:继续为新增共享关键字补齐三端测试覆盖,优先保证 C# Runtime 与 Generator 回归通过,并记录 JS 测试与构建验证
|
||||||
- PR review 信号漂移风险:CodeRabbit 可能把建议折叠在 latest review body,而不是 issue comments
|
- PR review 信号漂移风险:CodeRabbit 可能把建议折叠在 latest review body,而不是 issue comments
|
||||||
@ -35,8 +36,10 @@
|
|||||||
- 已补齐一批共享 JSON Schema 子集,包括:
|
- 已补齐一批共享 JSON Schema 子集,包括:
|
||||||
- `enum`、`const`、`not`、`pattern`
|
- `enum`、`const`、`not`、`pattern`
|
||||||
- `format` 稳定子集:`date`、`date-time`、`duration`、`email`、`time`、`uri`、`uuid`
|
- `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`
|
- `minProperties`、`maxProperties`、`dependentRequired`、`dependentSchemas`、`allOf`、object-focused `if` / `then` / `else`
|
||||||
|
- 已明确拒绝会改变生成类型形状的组合关键字:
|
||||||
|
- `oneOf`、`anyOf` 当前会在 Runtime / Generator / Tooling 三端直接报错,而不是静默忽略
|
||||||
- `if` / `then` / `else` 已按“不改变生成类型形状”的边界落地:
|
- `if` / `then` / `else` 已按“不改变生成类型形状”的边界落地:
|
||||||
- 只允许 object 节点上的 object-typed inline schema
|
- 只允许 object 节点上的 object-typed inline schema
|
||||||
- `if` 必填,且必须至少伴随 `then` 或 `else` 之一
|
- `if` 必填,且必须至少伴随 `then` 或 `else` 之一
|
||||||
@ -95,4 +98,4 @@
|
|||||||
|
|
||||||
1. 提交并推送当前 PR `#262` follow-up 修复后,重新抓取一次 PR review,确认 outside-diff comment 与 open thread 是否都已收口
|
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` 盘点下一批候选关键字
|
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 三端同时定义一致边界,再进入实现
|
2. 若继续扩共享关键字,先在 Runtime / Generator / Tooling 三端同时定义一致边界,再进入实现
|
||||||
3. 继续把 active 入口保持精简,只记录当前恢复点、验证与下一步
|
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 未声明的额外同级字段继续存在
|
- `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 语义把每个条目叠加到当前对象上,不做属性合并,也不改变生成类型形状
|
- `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 语义,允许对象保留未在条件块中声明的额外同级字段
|
- `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` 条目不会把新字段并回父对象。
|
`allOf` 的最小可工作示例如下。关键点是:字段形状先在父对象 `properties` 中声明,再用 `allOf` 叠加 `required` 或更细的字段约束;`allOf` 条目不会把新字段并回父对象。
|
||||||
|
|
||||||
|
|||||||
@ -1095,6 +1095,13 @@ function unquoteScalar(value) {
|
|||||||
*/
|
*/
|
||||||
function parseSchemaNode(rawNode, displayPath) {
|
function parseSchemaNode(rawNode, displayPath) {
|
||||||
const value = rawNode && typeof rawNode === "object" ? rawNode : {};
|
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 type = typeof value.type === "string" ? value.type : "object";
|
||||||
const patternMetadata = normalizeSchemaPattern(value.pattern, displayPath);
|
const patternMetadata = normalizeSchemaPattern(value.pattern, displayPath);
|
||||||
const stringFormat = normalizeSchemaStringFormat(value.format, type, displayPath);
|
const stringFormat = normalizeSchemaStringFormat(value.format, type, displayPath);
|
||||||
@ -1253,6 +1260,25 @@ function parseSchemaNode(rawNode, displayPath) {
|
|||||||
}, value.const, displayPath), value.enum, 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
|
* Parse one optional `not` sub-schema and keep path formatting aligned with
|
||||||
* the runtime/generator diagnostics.
|
* 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, "\"\"");
|
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", () => {
|
test("parseSchemaContent should build object const comparable keys with ordinal property ordering", () => {
|
||||||
const schema = parseSchemaContent(`
|
const schema = parseSchemaContent(`
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user