mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-13 22:25:37 +08:00
feat(config): 添加配置验证工具和代码分析规则
- 实现配置架构解析器和验证功能 - 添加YAML解析和注释提取功能 - 创建配置验证诊断规则表格 - 实现批量编辑器支持的字段提取 - 添加字符串格式验证(日期、邮箱、UUID等) - 创建示例配置YAML生成功能 - 实现表单更新应用到YAML的功能 - 添加常量和枚举值的元数据处理 - 实现精确小数倍数验证算法 - 添加配置模式规范化和比较功能
This commit is contained in:
parent
5185247c35
commit
01a815a518
@ -18,17 +18,6 @@
|
|||||||
GF_ContextRegistration_001 | GFramework.SourceGenerators.rule | Warning | ContextRegistrationDiagnostics
|
GF_ContextRegistration_001 | GFramework.SourceGenerators.rule | Warning | ContextRegistrationDiagnostics
|
||||||
GF_ContextRegistration_002 | GFramework.SourceGenerators.rule | Warning | ContextRegistrationDiagnostics
|
GF_ContextRegistration_002 | GFramework.SourceGenerators.rule | Warning | ContextRegistrationDiagnostics
|
||||||
GF_ContextRegistration_003 | GFramework.SourceGenerators.rule | Warning | ContextRegistrationDiagnostics
|
GF_ContextRegistration_003 | GFramework.SourceGenerators.rule | Warning | ContextRegistrationDiagnostics
|
||||||
GF_ConfigSchema_001 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
|
||||||
GF_ConfigSchema_002 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
|
||||||
GF_ConfigSchema_003 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
|
||||||
GF_ConfigSchema_004 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
|
||||||
GF_ConfigSchema_005 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
|
||||||
GF_ConfigSchema_006 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
|
||||||
GF_ConfigSchema_007 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
|
||||||
GF_ConfigSchema_008 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
|
||||||
GF_ConfigSchema_009 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
|
||||||
GF_ConfigSchema_010 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
|
||||||
GF_ConfigSchema_011 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
|
||||||
GF_AutoModule_001 | GFramework.SourceGenerators.Architecture | Error | AutoRegisterModuleDiagnostics
|
GF_AutoModule_001 | GFramework.SourceGenerators.Architecture | Error | AutoRegisterModuleDiagnostics
|
||||||
GF_AutoModule_002 | GFramework.SourceGenerators.Architecture | Error | AutoRegisterModuleDiagnostics
|
GF_AutoModule_002 | GFramework.SourceGenerators.Architecture | Error | AutoRegisterModuleDiagnostics
|
||||||
GF_AutoModule_003 | GFramework.SourceGenerators.Architecture | Error | AutoRegisterModuleDiagnostics
|
GF_AutoModule_003 | GFramework.SourceGenerators.Architecture | Error | AutoRegisterModuleDiagnostics
|
||||||
|
|||||||
@ -15,3 +15,4 @@
|
|||||||
GF_ConfigSchema_008 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
GF_ConfigSchema_008 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||||
GF_ConfigSchema_009 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
GF_ConfigSchema_009 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||||
GF_ConfigSchema_010 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
GF_ConfigSchema_010 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||||
|
GF_ConfigSchema_011 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||||
|
|||||||
@ -958,6 +958,44 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证当前 schema 节点是否以运行时支持的方式声明了 <c>dependentSchemas</c>。
|
||||||
|
/// 只有 object 节点允许挂载该关键字;一旦关键字出现,就继续复用对象节点的形状校验,
|
||||||
|
/// 保证发布到 XML 文档和运行时的约束解释范围保持一致。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filePath">Schema 文件路径。</param>
|
||||||
|
/// <param name="displayPath">逻辑字段路径。</param>
|
||||||
|
/// <param name="element">当前 schema 节点。</param>
|
||||||
|
/// <param name="schemaType">当前节点声明的 schema 类型。</param>
|
||||||
|
/// <param name="diagnostic">失败时返回的诊断。</param>
|
||||||
|
/// <returns>当前节点上的 dependentSchemas 声明是否有效。</returns>
|
||||||
|
private static bool TryValidateDependentSchemasDeclaration(
|
||||||
|
string filePath,
|
||||||
|
string displayPath,
|
||||||
|
JsonElement element,
|
||||||
|
string? schemaType,
|
||||||
|
out Diagnostic? diagnostic)
|
||||||
|
{
|
||||||
|
diagnostic = null;
|
||||||
|
if (!element.TryGetProperty("dependentSchemas", out _))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.Equals(schemaType, "object", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
diagnostic = Diagnostic.Create(
|
||||||
|
ConfigSchemaDiagnostics.InvalidDependentSchemasMetadata,
|
||||||
|
CreateFileLocation(filePath),
|
||||||
|
Path.GetFileName(filePath),
|
||||||
|
displayPath,
|
||||||
|
"Only object schemas can declare 'dependentSchemas'.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TryValidateDependentSchemasMetadata(filePath, displayPath, element, out diagnostic);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 递归验证 schema 树中的对象级 <c>dependentSchemas</c> 元数据。
|
/// 递归验证 schema 树中的对象级 <c>dependentSchemas</c> 元数据。
|
||||||
/// 该遍历会覆盖根节点、<c>not</c>、数组元素、<c>contains</c> 与嵌套 <c>dependentSchemas</c>,
|
/// 该遍历会覆盖根节点、<c>not</c>、数组元素、<c>contains</c> 与嵌套 <c>dependentSchemas</c>,
|
||||||
@ -980,15 +1018,11 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
element,
|
element,
|
||||||
static (currentFilePath, currentDisplayPath, currentElement, schemaType) =>
|
static (currentFilePath, currentDisplayPath, currentElement, schemaType) =>
|
||||||
{
|
{
|
||||||
if (!string.Equals(schemaType, "object", StringComparison.Ordinal))
|
return TryValidateDependentSchemasDeclaration(
|
||||||
{
|
|
||||||
return (true, (Diagnostic?)null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return TryValidateDependentSchemasMetadata(
|
|
||||||
currentFilePath,
|
currentFilePath,
|
||||||
currentDisplayPath,
|
currentDisplayPath,
|
||||||
currentElement,
|
currentElement,
|
||||||
|
schemaType,
|
||||||
out var currentDiagnostic)
|
out var currentDiagnostic)
|
||||||
? (true, (Diagnostic?)null)
|
? (true, (Diagnostic?)null)
|
||||||
: (false, currentDiagnostic);
|
: (false, currentDiagnostic);
|
||||||
@ -3447,6 +3481,9 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
/// 该摘要复用现有 enum / const / 约束文档构造器,避免 contains / not 与主属性文档逐渐漂移。
|
/// 该摘要复用现有 enum / const / 约束文档构造器,避免 contains / not 与主属性文档逐渐漂移。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="schemaElement">内联子 schema。</param>
|
/// <param name="schemaElement">内联子 schema。</param>
|
||||||
|
/// <param name="includeRequiredProperties">
|
||||||
|
/// 为对象摘要额外输出 <c>required</c> 信息时返回 <see langword="true" />。
|
||||||
|
/// </param>
|
||||||
/// <returns>格式化后的摘要字符串。</returns>
|
/// <returns>格式化后的摘要字符串。</returns>
|
||||||
private static string? TryBuildInlineSchemaSummary(
|
private static string? TryBuildInlineSchemaSummary(
|
||||||
JsonElement schemaElement,
|
JsonElement schemaElement,
|
||||||
|
|||||||
@ -10,7 +10,7 @@ namespace GFramework.Game.Tests.Config;
|
|||||||
[TestFixture]
|
[TestFixture]
|
||||||
public sealed class YamlConfigLoaderDependentSchemasTests
|
public sealed class YamlConfigLoaderDependentSchemasTests
|
||||||
{
|
{
|
||||||
private string _rootPath = null!;
|
private string? _rootPath;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 为每个用例创建隔离的临时目录,避免不同 dependentSchemas 场景互相污染。
|
/// 为每个用例创建隔离的临时目录,避免不同 dependentSchemas 场景互相污染。
|
||||||
@ -28,7 +28,8 @@ public sealed class YamlConfigLoaderDependentSchemasTests
|
|||||||
[TearDown]
|
[TearDown]
|
||||||
public void TearDown()
|
public void TearDown()
|
||||||
{
|
{
|
||||||
if (Directory.Exists(_rootPath))
|
if (!string.IsNullOrEmpty(_rootPath) &&
|
||||||
|
Directory.Exists(_rootPath))
|
||||||
{
|
{
|
||||||
Directory.Delete(_rootPath, true);
|
Directory.Delete(_rootPath, true);
|
||||||
}
|
}
|
||||||
@ -310,6 +311,8 @@ public sealed class YamlConfigLoaderDependentSchemasTests
|
|||||||
/// <param name="content">要写入的 YAML 或 schema 内容。</param>
|
/// <param name="content">要写入的 YAML 或 schema 内容。</param>
|
||||||
private void CreateConfigFile(string relativePath, string content)
|
private void CreateConfigFile(string relativePath, string content)
|
||||||
{
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(_rootPath);
|
||||||
|
|
||||||
var filePath = Path.Combine(_rootPath, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
var filePath = Path.Combine(_rootPath, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||||
var directoryPath = Path.GetDirectoryName(filePath);
|
var directoryPath = Path.GetDirectoryName(filePath);
|
||||||
if (!string.IsNullOrEmpty(directoryPath))
|
if (!string.IsNullOrEmpty(directoryPath))
|
||||||
@ -336,6 +339,8 @@ public sealed class YamlConfigLoaderDependentSchemasTests
|
|||||||
/// <returns>已注册测试表与 schema 路径的加载器。</returns>
|
/// <returns>已注册测试表与 schema 路径的加载器。</returns>
|
||||||
private YamlConfigLoader CreateMonsterRewardLoader()
|
private YamlConfigLoader CreateMonsterRewardLoader()
|
||||||
{
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(_rootPath);
|
||||||
|
|
||||||
return new YamlConfigLoader(_rootPath)
|
return new YamlConfigLoader(_rootPath)
|
||||||
.RegisterTable<int, MonsterDependentSchemasConfigStub>(
|
.RegisterTable<int, MonsterDependentSchemasConfigStub>(
|
||||||
"monster",
|
"monster",
|
||||||
|
|||||||
@ -9,7 +9,7 @@ namespace GFramework.Game.Tests.Config;
|
|||||||
[TestFixture]
|
[TestFixture]
|
||||||
public sealed class YamlConfigSchemaValidatorTests
|
public sealed class YamlConfigSchemaValidatorTests
|
||||||
{
|
{
|
||||||
private string _rootPath = null!;
|
private string? _rootPath;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 为每个测试准备独立临时目录。
|
/// 为每个测试准备独立临时目录。
|
||||||
@ -27,7 +27,8 @@ public sealed class YamlConfigSchemaValidatorTests
|
|||||||
[TearDown]
|
[TearDown]
|
||||||
public void TearDown()
|
public void TearDown()
|
||||||
{
|
{
|
||||||
if (Directory.Exists(_rootPath))
|
if (!string.IsNullOrEmpty(_rootPath) &&
|
||||||
|
Directory.Exists(_rootPath))
|
||||||
{
|
{
|
||||||
Directory.Delete(_rootPath, true);
|
Directory.Delete(_rootPath, true);
|
||||||
}
|
}
|
||||||
@ -70,6 +71,61 @@ public sealed class YamlConfigSchemaValidatorTests
|
|||||||
Assert.That(schema.ReferencedTableNames, Is.EqualTo(new[] { "ally", "item", "weapon" }));
|
Assert.That(schema.ReferencedTableNames, Is.EqualTo(new[] { "ally", "item", "weapon" }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证条件子 schema 复用同一条 ref-table 字段时,不会把同一引用重复写入结果。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void ValidateAndCollectReferences_Should_Not_Duplicate_Reference_Usages_From_DependentSchemas()
|
||||||
|
{
|
||||||
|
var schemaPath = CreateSchemaFile(
|
||||||
|
"schemas/monster.schema.json",
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"reward": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"itemId": {
|
||||||
|
"type": "string",
|
||||||
|
"x-gframework-ref-table": "item"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependentSchemas": {
|
||||||
|
"itemId": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"itemId": {
|
||||||
|
"type": "string",
|
||||||
|
"x-gframework-ref-table": "item"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
var schema = YamlConfigSchemaValidator.Load("monster", schemaPath);
|
||||||
|
|
||||||
|
var references = YamlConfigSchemaValidator.ValidateAndCollectReferences(
|
||||||
|
"monster",
|
||||||
|
schema,
|
||||||
|
"monster/slime.yaml",
|
||||||
|
"""
|
||||||
|
reward:
|
||||||
|
itemId: potion
|
||||||
|
""");
|
||||||
|
|
||||||
|
Assert.That(references, Has.Count.EqualTo(1));
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(references[0].DisplayPath, Is.EqualTo("reward.itemId"));
|
||||||
|
Assert.That(references[0].ReferencedTableName, Is.EqualTo("item"));
|
||||||
|
Assert.That(references[0].RawValue, Is.EqualTo("potion"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 在临时目录中创建 schema 文件。
|
/// 在临时目录中创建 schema 文件。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -80,6 +136,8 @@ public sealed class YamlConfigSchemaValidatorTests
|
|||||||
string relativePath,
|
string relativePath,
|
||||||
string content)
|
string content)
|
||||||
{
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(_rootPath);
|
||||||
|
|
||||||
var fullPath = Path.Combine(_rootPath, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
var fullPath = Path.Combine(_rootPath, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||||
var directoryPath = Path.GetDirectoryName(fullPath);
|
var directoryPath = Path.GetDirectoryName(fullPath);
|
||||||
if (!string.IsNullOrWhiteSpace(directoryPath))
|
if (!string.IsNullOrWhiteSpace(directoryPath))
|
||||||
|
|||||||
@ -813,15 +813,20 @@ internal static class YamlConfigSchemaValidator
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 校验对象节点声明的属性数量约束。
|
/// 校验对象节点声明的数量约束与条件对象约束。
|
||||||
|
/// 该阶段除了检查 <c>minProperties</c> / <c>maxProperties</c>,还会复用同一份 sibling 集合处理
|
||||||
|
/// <c>dependentRequired</c>,并在 <c>dependentSchemas</c> 命中时以 focused constraint block 语义
|
||||||
|
/// 对整个 <paramref name="mappingNode" /> 做额外试匹配。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="tableName">所属配置表名称。</param>
|
/// <param name="tableName">所属配置表名称。</param>
|
||||||
/// <param name="yamlPath">YAML 文件路径。</param>
|
/// <param name="yamlPath">YAML 文件路径。</param>
|
||||||
/// <param name="displayPath">对象字段路径;根对象时为空。</param>
|
/// <param name="displayPath">对象字段路径;根对象时为空。</param>
|
||||||
/// <param name="mappingNode">当前 YAML 对象节点。</param>
|
/// <param name="mappingNode">当前 YAML 对象节点;用于让条件子 schema 在完整对象视图上做匹配。</param>
|
||||||
/// <param name="seenProperties">当前对象已出现的属性集合。</param>
|
/// <param name="seenProperties">当前对象已出现的属性集合。</param>
|
||||||
/// <param name="schemaNode">对象 schema 节点。</param>
|
/// <param name="schemaNode">对象 schema 节点。</param>
|
||||||
/// <param name="references">可选的跨表引用收集器。</param>
|
/// <param name="references">
|
||||||
|
/// 可选的跨表引用收集器;当 <c>dependentSchemas</c> 命中且匹配成功时,只会回写该条件分支新增的引用。
|
||||||
|
/// </param>
|
||||||
private static void ValidateObjectConstraints(
|
private static void ValidateObjectConstraints(
|
||||||
string tableName,
|
string tableName,
|
||||||
string yamlPath,
|
string yamlPath,
|
||||||
@ -924,6 +929,9 @@ internal static class YamlConfigSchemaValidator
|
|||||||
// dependentSchemas acts as an additional conditional constraint block on the
|
// dependentSchemas acts as an additional conditional constraint block on the
|
||||||
// current object. Keep undeclared sibling fields outside the dependent sub-schema
|
// current object. Keep undeclared sibling fields outside the dependent sub-schema
|
||||||
// from blocking the match so schema authors can express focused follow-up rules.
|
// from blocking the match so schema authors can express focused follow-up rules.
|
||||||
|
// The trial matcher merges only new reference usages back into the outer collector,
|
||||||
|
// so re-checking the same scalar via a conditional sub-schema does not duplicate
|
||||||
|
// cross-table validation work later in the loader pipeline.
|
||||||
if (TryMatchSchemaNode(
|
if (TryMatchSchemaNode(
|
||||||
tableName,
|
tableName,
|
||||||
yamlPath,
|
yamlPath,
|
||||||
@ -3138,10 +3146,7 @@ internal static class YamlConfigSchemaValidator
|
|||||||
if (references is not null &&
|
if (references is not null &&
|
||||||
matchedReferences is not null)
|
matchedReferences is not null)
|
||||||
{
|
{
|
||||||
foreach (var referenceUsage in matchedReferences)
|
AddUniqueReferenceUsages(references, matchedReferences);
|
||||||
{
|
|
||||||
references.Add(referenceUsage);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -3153,6 +3158,50 @@ internal static class YamlConfigSchemaValidator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将试匹配分支采集到的引用回写到外层集合,并按结构化标识去重。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="references">外层引用集合。</param>
|
||||||
|
/// <param name="matchedReferences">当前成功匹配分支采集到的引用。</param>
|
||||||
|
private static void AddUniqueReferenceUsages(
|
||||||
|
ICollection<YamlConfigReferenceUsage> references,
|
||||||
|
IEnumerable<YamlConfigReferenceUsage> matchedReferences)
|
||||||
|
{
|
||||||
|
foreach (var referenceUsage in matchedReferences)
|
||||||
|
{
|
||||||
|
if (!ContainsReferenceUsage(references, referenceUsage))
|
||||||
|
{
|
||||||
|
references.Add(referenceUsage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断外层引用集合中是否已经存在同一条引用使用记录。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="references">要检查的引用集合。</param>
|
||||||
|
/// <param name="candidate">当前待合并的引用记录。</param>
|
||||||
|
/// <returns>当集合中已存在语义相同的记录时返回 <see langword="true" />。</returns>
|
||||||
|
private static bool ContainsReferenceUsage(
|
||||||
|
IEnumerable<YamlConfigReferenceUsage> references,
|
||||||
|
YamlConfigReferenceUsage candidate)
|
||||||
|
{
|
||||||
|
foreach (var referenceUsage in references)
|
||||||
|
{
|
||||||
|
if (string.Equals(referenceUsage.YamlPath, candidate.YamlPath, StringComparison.Ordinal) &&
|
||||||
|
string.Equals(referenceUsage.SchemaPath, candidate.SchemaPath, StringComparison.Ordinal) &&
|
||||||
|
string.Equals(referenceUsage.PropertyPath, candidate.PropertyPath, StringComparison.Ordinal) &&
|
||||||
|
string.Equals(referenceUsage.RawValue, candidate.RawValue, StringComparison.Ordinal) &&
|
||||||
|
string.Equals(referenceUsage.ReferencedTableName, candidate.ReferencedTableName, StringComparison.Ordinal) &&
|
||||||
|
referenceUsage.ValueType == candidate.ValueType)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 校验节点是否命中了 <c>not</c> 声明的禁用 schema。
|
/// 校验节点是否命中了 <c>not</c> 声明的禁用 schema。
|
||||||
/// 与 contains 不同,not 会沿用主校验链的严格对象语义,避免把“声明属性子集”误当成完整命中。
|
/// 与 contains 不同,not 会沿用主校验链的严格对象语义,避免把“声明属性子集”误当成完整命中。
|
||||||
|
|||||||
@ -584,6 +584,55 @@ public class SchemaConfigGeneratorTests
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证只有 object 节点允许声明 <c>dependentSchemas</c>。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void Run_Should_Report_Diagnostic_When_NonObject_Schema_Declares_DependentSchemas()
|
||||||
|
{
|
||||||
|
const string source = """
|
||||||
|
namespace TestApp
|
||||||
|
{
|
||||||
|
public sealed class Dummy
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
const string schema = """
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "tag"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "integer" },
|
||||||
|
"tag": {
|
||||||
|
"type": "string",
|
||||||
|
"dependentSchemas": {
|
||||||
|
"itemId": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
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_011"));
|
||||||
|
Assert.That(diagnostic.Severity, Is.EqualTo(DiagnosticSeverity.Error));
|
||||||
|
Assert.That(diagnostic.GetMessage(), Does.Contain("tag"));
|
||||||
|
Assert.That(diagnostic.GetMessage(), Does.Contain("Only object schemas can declare 'dependentSchemas'."));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证 <c>dependentSchemas</c> 子 schema 内的非法 <c>format</c> 也会在生成阶段直接给出诊断。
|
/// 验证 <c>dependentSchemas</c> 子 schema 内的非法 <c>format</c> 也会在生成阶段直接给出诊断。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -1742,9 +1742,8 @@ function validateObjectNode(schemaNode, yamlNode, displayPath, diagnostics, loca
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (schemaNode.dependentSchemas && typeof schemaNode.dependentSchemas === "object") {
|
if (schemaNode.dependentSchemas && typeof schemaNode.dependentSchemas === "object") {
|
||||||
for (const [triggerProperty, dependentSchema] of Object.entries(schemaNode.dependentSchemas)) {
|
for (const [triggerProperty, dependentSchema] of getTriggeredDependentSchemas(schemaNode, yamlNode)) {
|
||||||
if (!yamlNode.map.has(triggerProperty) ||
|
if (matchesSchemaNode(dependentSchema, yamlNode, true)) {
|
||||||
matchesSchemaNode(dependentSchema, yamlNode, true)) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1795,6 +1794,32 @@ function validateObjectNode(schemaNode, yamlNode, displayPath, diagnostics, loca
|
|||||||
validateNotSchemaMatch(schemaNode, yamlNode, displayPath, diagnostics, localizer);
|
validateNotSchemaMatch(schemaNode, yamlNode, displayPath, diagnostics, localizer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enumerate object-level `dependentSchemas` entries whose trigger property is
|
||||||
|
* present on the current YAML object.
|
||||||
|
*
|
||||||
|
* @param {SchemaNode} schemaNode Schema node.
|
||||||
|
* @param {YamlNode} yamlNode YAML node.
|
||||||
|
* @returns {Array<[string, SchemaNode]>} Triggered dependent schema entries.
|
||||||
|
*/
|
||||||
|
function getTriggeredDependentSchemas(schemaNode, yamlNode) {
|
||||||
|
if (!schemaNode.dependentSchemas ||
|
||||||
|
typeof schemaNode.dependentSchemas !== "object" ||
|
||||||
|
!yamlNode ||
|
||||||
|
yamlNode.kind !== "object") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const triggeredSchemas = [];
|
||||||
|
for (const [triggerProperty, dependentSchema] of Object.entries(schemaNode.dependentSchemas)) {
|
||||||
|
if (yamlNode.map.has(triggerProperty)) {
|
||||||
|
triggeredSchemas.push([triggerProperty, dependentSchema]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return triggeredSchemas;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test whether one YAML node satisfies one schema node without emitting user-facing diagnostics.
|
* Test whether one YAML node satisfies one schema node without emitting user-facing diagnostics.
|
||||||
* This is used by array `contains`, where object sub-schemas must behave like
|
* This is used by array `contains`, where object sub-schemas must behave like
|
||||||
@ -1869,14 +1894,11 @@ function matchesSchemaNodeInternal(schemaNode, yamlNode, allowUnknownObjectPrope
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (schemaNode.dependentSchemas && typeof schemaNode.dependentSchemas === "object") {
|
for (const [, dependentSchema] of getTriggeredDependentSchemas(schemaNode, yamlNode)) {
|
||||||
for (const [triggerProperty, dependentSchema] of Object.entries(schemaNode.dependentSchemas)) {
|
if (!matchesSchemaNodeInternal(dependentSchema, yamlNode, true)) {
|
||||||
if (yamlNode.map.has(triggerProperty) &&
|
|
||||||
!matchesSchemaNodeInternal(dependentSchema, yamlNode, true)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof schemaNode.minProperties === "number" &&
|
if (typeof schemaNode.minProperties === "number" &&
|
||||||
propertyCount < schemaNode.minProperties) {
|
propertyCount < schemaNode.minProperties) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user