mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
fix(game-config): 显式声明闭合对象字段边界
- 修复 Runtime 与 Source Generator 对 additionalProperties 的隐式闭合对象语义,统一接受 additionalProperties:false 并拒绝其它开放对象形状 - 补充 Release 回归测试,覆盖生成器诊断与运行时 additionalProperties 边界 - 更新配置工具元数据与 README 说明,使命令、设置和当前能力描述保持一致
This commit is contained in:
parent
eddce21383
commit
13b77eb3fe
@ -20,3 +20,4 @@
|
||||
GF_ConfigSchema_013 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||
GF_ConfigSchema_014 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||
GF_ConfigSchema_015 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||
GF_ConfigSchema_016 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||
|
||||
@ -233,6 +233,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
||||
|
||||
return TryValidateStringFormatMetadataRecursively(filePath, "<root>", root, out diagnostic) &&
|
||||
TryValidateUnsupportedCombinatorKeywordsRecursively(filePath, "<root>", root, out diagnostic) &&
|
||||
TryValidateUnsupportedOpenObjectKeywordsRecursively(filePath, "<root>", root, out diagnostic) &&
|
||||
TryValidateDependentRequiredMetadataRecursively(filePath, "<root>", root, out diagnostic) &&
|
||||
TryValidateDependentSchemasMetadataRecursively(filePath, "<root>", root, out diagnostic) &&
|
||||
TryValidateAllOfMetadataRecursively(filePath, "<root>", root, out diagnostic) &&
|
||||
@ -877,6 +878,39 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
||||
out diagnostic);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 递归拒绝当前共享子集尚未支持的开放对象关键字形状。
|
||||
/// 当前对象字段集默认是闭合的,因此这里只接受显式重复该语义的
|
||||
/// <c>additionalProperties: false</c>。
|
||||
/// </summary>
|
||||
/// <param name="filePath">Schema 文件路径。</param>
|
||||
/// <param name="displayPath">逻辑字段路径。</param>
|
||||
/// <param name="element">当前 schema 节点。</param>
|
||||
/// <param name="diagnostic">失败时返回的诊断。</param>
|
||||
/// <returns>当前节点树是否未声明不支持的开放对象关键字形状。</returns>
|
||||
private static bool TryValidateUnsupportedOpenObjectKeywordsRecursively(
|
||||
string filePath,
|
||||
string displayPath,
|
||||
JsonElement element,
|
||||
out Diagnostic? diagnostic)
|
||||
{
|
||||
return TryTraverseSchemaRecursively(
|
||||
filePath,
|
||||
displayPath,
|
||||
element,
|
||||
static (currentFilePath, currentDisplayPath, currentElement, _) =>
|
||||
{
|
||||
return TryValidateUnsupportedOpenObjectKeywords(
|
||||
currentFilePath,
|
||||
currentDisplayPath,
|
||||
currentElement,
|
||||
out var currentDiagnostic)
|
||||
? (true, (Diagnostic?)null)
|
||||
: (false, currentDiagnostic);
|
||||
},
|
||||
out diagnostic);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当前节点是否声明了会改变生成类型形状的未支持组合关键字。
|
||||
/// </summary>
|
||||
@ -907,6 +941,41 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <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 TryValidateUnsupportedOpenObjectKeywords(
|
||||
string filePath,
|
||||
string displayPath,
|
||||
JsonElement element,
|
||||
out Diagnostic? diagnostic)
|
||||
{
|
||||
diagnostic = null;
|
||||
if (!element.TryGetProperty("additionalProperties", out var additionalPropertiesElement))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (additionalPropertiesElement.ValueKind == JsonValueKind.False)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
diagnostic = Diagnostic.Create(
|
||||
ConfigSchemaDiagnostics.UnsupportedOpenObjectKeyword,
|
||||
CreateFileLocation(filePath),
|
||||
Path.GetFileName(filePath),
|
||||
displayPath,
|
||||
"additionalProperties",
|
||||
"The current config schema subset only accepts 'additionalProperties: false' so object fields remain closed and strongly typed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回当前节点声明的首个未支持组合关键字。
|
||||
/// </summary>
|
||||
|
||||
@ -173,4 +173,15 @@ public static class ConfigSchemaDiagnostics
|
||||
SourceGeneratorsConfigCategory,
|
||||
DiagnosticSeverity.Error,
|
||||
true);
|
||||
|
||||
/// <summary>
|
||||
/// schema 节点声明了当前共享子集尚未支持的开放对象关键字形状。
|
||||
/// </summary>
|
||||
public static readonly DiagnosticDescriptor UnsupportedOpenObjectKeyword = new(
|
||||
"GF_ConfigSchema_016",
|
||||
"Config schema uses an unsupported open-object keyword",
|
||||
"Property '{1}' in schema file '{0}' uses unsupported open-object keyword '{2}': {3}",
|
||||
SourceGeneratorsConfigCategory,
|
||||
DiagnosticSeverity.Error,
|
||||
true);
|
||||
}
|
||||
|
||||
@ -324,6 +324,73 @@ public sealed class YamlConfigLoaderAllOfTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证运行时接受显式声明的 <c>additionalProperties: false</c>,
|
||||
/// 因为这与当前闭合对象字段集语义保持一致。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task LoadAsync_Should_Accept_When_Object_Schema_Declares_AdditionalProperties_False()
|
||||
{
|
||||
CreateConfigFile(
|
||||
"monster/slime.yaml",
|
||||
BuildMonsterConfigYaml(
|
||||
"""
|
||||
itemCount: 3
|
||||
"""));
|
||||
CreateSchemaFile(
|
||||
"schemas/monster.schema.json",
|
||||
BuildMonsterSchema(
|
||||
DefaultRewardPropertiesJson,
|
||||
DefaultAllOfJson,
|
||||
"""
|
||||
"additionalProperties": false
|
||||
"""));
|
||||
|
||||
var loader = CreateMonsterRewardLoader();
|
||||
var registry = CreateRegistry();
|
||||
|
||||
await loader.LoadAsync(registry).ConfigureAwait(false);
|
||||
|
||||
var table = registry.GetTable<int, MonsterAllOfConfigStub>("monster");
|
||||
Assert.That(table.Count, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证运行时会拒绝会打开动态字段形状的 <c>additionalProperties</c>。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void LoadAsync_Should_Throw_When_Object_Schema_Declares_Unsupported_AdditionalProperties()
|
||||
{
|
||||
CreateConfigFile(
|
||||
"monster/slime.yaml",
|
||||
BuildMonsterConfigYaml(
|
||||
"""
|
||||
itemCount: 3
|
||||
"""));
|
||||
CreateSchemaFile(
|
||||
"schemas/monster.schema.json",
|
||||
BuildMonsterSchema(
|
||||
DefaultRewardPropertiesJson,
|
||||
DefaultAllOfJson,
|
||||
"""
|
||||
"additionalProperties": true
|
||||
"""));
|
||||
|
||||
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 'additionalProperties' metadata"));
|
||||
Assert.That(registry.Count, Is.EqualTo(0));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 allOf 条目只接受 object-typed schema。
|
||||
/// </summary>
|
||||
|
||||
@ -322,6 +322,7 @@ internal static partial class YamlConfigSchemaValidator
|
||||
bool isRoot = false)
|
||||
{
|
||||
ValidateUnsupportedCombinatorKeywords(tableName, schemaPath, propertyPath, element);
|
||||
ValidateUnsupportedOpenObjectKeywords(tableName, schemaPath, propertyPath, element);
|
||||
var typeName = ResolveNodeTypeName(tableName, schemaPath, propertyPath, element);
|
||||
var referenceTableName = TryGetReferenceTableName(tableName, schemaPath, propertyPath, element);
|
||||
ValidateObjectOnlyKeywords(tableName, schemaPath, propertyPath, element, typeName);
|
||||
@ -366,6 +367,40 @@ internal static partial class YamlConfigSchemaValidator
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显式拒绝当前共享子集中尚未支持的开放对象关键字形状。
|
||||
/// 当前配置系统默认采用闭合对象字段集;这里只接受显式重复该语义的
|
||||
/// <c>additionalProperties: false</c>,继续拒绝会引入动态字段形状的其它形式。
|
||||
/// </summary>
|
||||
/// <param name="tableName">所属配置表名称。</param>
|
||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||
/// <param name="propertyPath">当前节点的逻辑属性路径。</param>
|
||||
/// <param name="element">当前 schema 节点。</param>
|
||||
private static void ValidateUnsupportedOpenObjectKeywords(
|
||||
string tableName,
|
||||
string schemaPath,
|
||||
string propertyPath,
|
||||
JsonElement element)
|
||||
{
|
||||
if (!element.TryGetProperty("additionalProperties", out var additionalPropertiesElement))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (additionalPropertiesElement.ValueKind == JsonValueKind.False)
|
||||
{
|
||||
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.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回当前节点声明的首个未支持组合关键字。
|
||||
/// </summary>
|
||||
|
||||
@ -1843,6 +1843,77 @@ public class SchemaConfigGeneratorTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证生成器接受显式声明的 <c>additionalProperties: false</c>。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Run_Should_Accept_When_Object_Schema_Declares_AdditionalProperties_False()
|
||||
{
|
||||
const string source = DummySource;
|
||||
const string schema = """
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "reward"],
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"reward": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"itemCount": { "type": "integer" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var result = SchemaGeneratorTestDriver.Run(
|
||||
source,
|
||||
("monster.schema.json", schema));
|
||||
|
||||
Assert.That(result.Results.Single().Diagnostics, Is.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证生成器会拒绝会打开动态字段形状的 <c>additionalProperties</c>。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Run_Should_Report_Diagnostic_When_Object_Schema_Declares_Unsupported_AdditionalProperties()
|
||||
{
|
||||
const string source = DummySource;
|
||||
const string schema = """
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "reward"],
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"reward": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"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("additionalProperties"));
|
||||
Assert.That(diagnostic.GetMessage(), Does.Contain("only accepts 'additionalProperties: false'"));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 <c>then</c> 子 schema 内的非法 <c>format</c> 也会在生成阶段直接给出诊断。
|
||||
/// </summary>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# GFramework Config Tool
|
||||
|
||||
VS Code extension for the GFramework AI-First config workflow.
|
||||
VS Code extension for browsing, validating, and lightweight editing in the GFramework AI-First config workflow.
|
||||
|
||||
## Purpose
|
||||
|
||||
@ -34,7 +34,7 @@ GameProject/
|
||||
|
||||
### Explorer View
|
||||
|
||||
- Browse config files from the workspace `config/` directory
|
||||
- Browse config files from the first workspace folder's `config/` directory
|
||||
- Group files by config domain
|
||||
- Open matching schema files from `schemas/`
|
||||
|
||||
@ -43,11 +43,12 @@ GameProject/
|
||||
- Open raw YAML
|
||||
- Open the matching schema
|
||||
- Open a lightweight form preview
|
||||
- Revalidate saved config files automatically when they change
|
||||
|
||||
### Domain-Level Actions
|
||||
|
||||
- Batch edit one config domain across multiple files for top-level scalar and scalar-array fields
|
||||
- Run validation across the current workspace config surface
|
||||
- Validate all discovered config files from the explorer view
|
||||
|
||||
### Form / Validation Support
|
||||
|
||||
@ -56,6 +57,8 @@ GameProject/
|
||||
- Jump from reference fields to the referenced schema, config domain, or direct config file when a reference value is
|
||||
present
|
||||
- Initialize empty config files from schema-derived example YAML
|
||||
- Edit nested object fields recursively inside the form preview
|
||||
- Edit arrays of objects in the form preview, including nested object fields inside each item
|
||||
- Surface schema metadata such as `title`, `description`, `default`, `enum`, and `x-gframework-ref-table` in the
|
||||
lightweight editors
|
||||
|
||||
@ -69,6 +72,12 @@ The extension currently validates the repository's current schema subset:
|
||||
- scalar arrays with scalar item type checks
|
||||
- arrays of objects whose items use the same supported subset recursively
|
||||
- scalar `enum` constraints and scalar-array item `enum` constraints
|
||||
- scalar `const` constraints
|
||||
- numeric range constraints such as `minimum`, `exclusiveMinimum`, `maximum`, `exclusiveMaximum`, and `multipleOf`
|
||||
- string constraints such as `minLength`, `maxLength`, and `pattern`
|
||||
- array constraints such as `minItems`, `maxItems`, `contains`, `minContains`, `maxContains`, and `uniqueItems`
|
||||
- object constraints such as `minProperties`, `maxProperties`, `dependentRequired`, `dependentSchemas`, `allOf`, and
|
||||
object-focused `if` / `then` / `else`
|
||||
|
||||
## Workspace Settings
|
||||
|
||||
@ -83,7 +92,7 @@ The extension currently validates the repository's current schema subset:
|
||||
|
||||
1. Install the extension in VS Code and open the workspace that contains your `config/` and `schemas/` directories.
|
||||
2. Keep the default workspace layout, or set `gframeworkConfig.configPath` and `gframeworkConfig.schemasPath` to your
|
||||
project-specific paths.
|
||||
project-specific paths relative to the first workspace folder.
|
||||
3. Open the `GFramework Config` explorer view and select a config file or domain.
|
||||
4. Run validation first to confirm the current YAML files still match the supported schema subset.
|
||||
5. Open the lightweight form preview or domain batch editing actions, then fall back to raw YAML for deeper nested edits
|
||||
@ -98,7 +107,8 @@ The extension currently validates the repository's current schema subset:
|
||||
|
||||
- Multi-root workspaces use the first workspace folder
|
||||
- Validation only covers the repository's current schema subset
|
||||
- Form preview supports object-array editing, but nested object arrays inside array items still fall back to raw YAML
|
||||
- Form preview supports nested objects and object-array editing, but deeper nested object arrays inside array items still
|
||||
fall back to raw YAML
|
||||
- Batch editing remains limited to top-level scalar fields and top-level scalar arrays
|
||||
|
||||
## Local Testing
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
{
|
||||
"extension.displayName": "GFramework Config Tool",
|
||||
"extension.description": "VS Code tooling for browsing, validating, and editing AI-First config files in GFramework projects.",
|
||||
"extension.description": "VS Code tooling for browsing, validating, form-preview editing, and domain batch updates for AI-First config files in GFramework projects.",
|
||||
"view.gframeworkConfig.name": "GFramework Config",
|
||||
"command.refresh.title": "GFramework Config: Refresh",
|
||||
"command.openRaw.title": "GFramework Config: Open Raw File",
|
||||
"command.openRaw.title": "GFramework Config: Open Raw YAML",
|
||||
"command.openSchema.title": "GFramework Config: Open Schema",
|
||||
"command.openFormPreview.title": "GFramework Config: Open Form Preview",
|
||||
"command.batchEditDomain.title": "GFramework Config: Batch Edit Domain",
|
||||
"command.validateAll.title": "GFramework Config: Validate All",
|
||||
"configuration.title": "GFramework Config",
|
||||
"configuration.configPath.description": "Relative path from the workspace root to the config directory.",
|
||||
"configuration.schemasPath.description": "Relative path from the workspace root to the schema directory."
|
||||
"configuration.configPath.description": "Relative path from the first workspace folder to the config directory.",
|
||||
"configuration.schemasPath.description": "Relative path from the first workspace folder to the schema directory."
|
||||
}
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
{
|
||||
"extension.displayName": "GFramework 配置工具",
|
||||
"extension.description": "为 GFramework 项目中的 AI-First 配置文件提供浏览、校验和编辑能力的 VS Code 扩展。",
|
||||
"extension.description": "为 GFramework 项目中的 AI-First 配置文件提供浏览、校验、表单预览编辑和配置域批量更新能力的 VS Code 扩展。",
|
||||
"view.gframeworkConfig.name": "GFramework 配置",
|
||||
"command.refresh.title": "GFramework 配置:刷新",
|
||||
"command.openRaw.title": "GFramework 配置:打开原始文件",
|
||||
"command.openRaw.title": "GFramework 配置:打开原始 YAML",
|
||||
"command.openSchema.title": "GFramework 配置:打开 Schema",
|
||||
"command.openFormPreview.title": "GFramework 配置:打开表单预览",
|
||||
"command.batchEditDomain.title": "GFramework 配置:批量编辑配置域",
|
||||
"command.validateAll.title": "GFramework 配置:校验全部",
|
||||
"configuration.title": "GFramework 配置",
|
||||
"configuration.configPath.description": "从工作区根目录到配置目录的相对路径。",
|
||||
"configuration.schemasPath.description": "从工作区根目录到 Schema 目录的相对路径。"
|
||||
"configuration.configPath.description": "从第一个工作区目录到配置目录的相对路径。",
|
||||
"configuration.schemasPath.description": "从第一个工作区目录到 Schema 目录的相对路径。"
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user