mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-10 19:56:45 +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_002 | 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_002 | 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_009 | 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;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// 递归验证 schema 树中的对象级 <c>dependentSchemas</c> 元数据。
|
||||
/// 该遍历会覆盖根节点、<c>not</c>、数组元素、<c>contains</c> 与嵌套 <c>dependentSchemas</c>,
|
||||
@ -980,15 +1018,11 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
||||
element,
|
||||
static (currentFilePath, currentDisplayPath, currentElement, schemaType) =>
|
||||
{
|
||||
if (!string.Equals(schemaType, "object", StringComparison.Ordinal))
|
||||
{
|
||||
return (true, (Diagnostic?)null);
|
||||
}
|
||||
|
||||
return TryValidateDependentSchemasMetadata(
|
||||
return TryValidateDependentSchemasDeclaration(
|
||||
currentFilePath,
|
||||
currentDisplayPath,
|
||||
currentElement,
|
||||
schemaType,
|
||||
out var currentDiagnostic)
|
||||
? (true, (Diagnostic?)null)
|
||||
: (false, currentDiagnostic);
|
||||
@ -3447,6 +3481,9 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
||||
/// 该摘要复用现有 enum / const / 约束文档构造器,避免 contains / not 与主属性文档逐渐漂移。
|
||||
/// </summary>
|
||||
/// <param name="schemaElement">内联子 schema。</param>
|
||||
/// <param name="includeRequiredProperties">
|
||||
/// 为对象摘要额外输出 <c>required</c> 信息时返回 <see langword="true" />。
|
||||
/// </param>
|
||||
/// <returns>格式化后的摘要字符串。</returns>
|
||||
private static string? TryBuildInlineSchemaSummary(
|
||||
JsonElement schemaElement,
|
||||
|
||||
@ -10,7 +10,7 @@ namespace GFramework.Game.Tests.Config;
|
||||
[TestFixture]
|
||||
public sealed class YamlConfigLoaderDependentSchemasTests
|
||||
{
|
||||
private string _rootPath = null!;
|
||||
private string? _rootPath;
|
||||
|
||||
/// <summary>
|
||||
/// 为每个用例创建隔离的临时目录,避免不同 dependentSchemas 场景互相污染。
|
||||
@ -28,7 +28,8 @@ public sealed class YamlConfigLoaderDependentSchemasTests
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
if (Directory.Exists(_rootPath))
|
||||
if (!string.IsNullOrEmpty(_rootPath) &&
|
||||
Directory.Exists(_rootPath))
|
||||
{
|
||||
Directory.Delete(_rootPath, true);
|
||||
}
|
||||
@ -310,6 +311,8 @@ public sealed class YamlConfigLoaderDependentSchemasTests
|
||||
/// <param name="content">要写入的 YAML 或 schema 内容。</param>
|
||||
private void CreateConfigFile(string relativePath, string content)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(_rootPath);
|
||||
|
||||
var filePath = Path.Combine(_rootPath, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||
var directoryPath = Path.GetDirectoryName(filePath);
|
||||
if (!string.IsNullOrEmpty(directoryPath))
|
||||
@ -336,6 +339,8 @@ public sealed class YamlConfigLoaderDependentSchemasTests
|
||||
/// <returns>已注册测试表与 schema 路径的加载器。</returns>
|
||||
private YamlConfigLoader CreateMonsterRewardLoader()
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(_rootPath);
|
||||
|
||||
return new YamlConfigLoader(_rootPath)
|
||||
.RegisterTable<int, MonsterDependentSchemasConfigStub>(
|
||||
"monster",
|
||||
|
||||
@ -9,7 +9,7 @@ namespace GFramework.Game.Tests.Config;
|
||||
[TestFixture]
|
||||
public sealed class YamlConfigSchemaValidatorTests
|
||||
{
|
||||
private string _rootPath = null!;
|
||||
private string? _rootPath;
|
||||
|
||||
/// <summary>
|
||||
/// 为每个测试准备独立临时目录。
|
||||
@ -27,7 +27,8 @@ public sealed class YamlConfigSchemaValidatorTests
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
if (Directory.Exists(_rootPath))
|
||||
if (!string.IsNullOrEmpty(_rootPath) &&
|
||||
Directory.Exists(_rootPath))
|
||||
{
|
||||
Directory.Delete(_rootPath, true);
|
||||
}
|
||||
@ -70,6 +71,61 @@ public sealed class YamlConfigSchemaValidatorTests
|
||||
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>
|
||||
/// 在临时目录中创建 schema 文件。
|
||||
/// </summary>
|
||||
@ -80,6 +136,8 @@ public sealed class YamlConfigSchemaValidatorTests
|
||||
string relativePath,
|
||||
string content)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(_rootPath);
|
||||
|
||||
var fullPath = Path.Combine(_rootPath, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||
var directoryPath = Path.GetDirectoryName(fullPath);
|
||||
if (!string.IsNullOrWhiteSpace(directoryPath))
|
||||
|
||||
@ -813,15 +813,20 @@ internal static class YamlConfigSchemaValidator
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验对象节点声明的属性数量约束。
|
||||
/// 校验对象节点声明的数量约束与条件对象约束。
|
||||
/// 该阶段除了检查 <c>minProperties</c> / <c>maxProperties</c>,还会复用同一份 sibling 集合处理
|
||||
/// <c>dependentRequired</c>,并在 <c>dependentSchemas</c> 命中时以 focused constraint block 语义
|
||||
/// 对整个 <paramref name="mappingNode" /> 做额外试匹配。
|
||||
/// </summary>
|
||||
/// <param name="tableName">所属配置表名称。</param>
|
||||
/// <param name="yamlPath">YAML 文件路径。</param>
|
||||
/// <param name="displayPath">对象字段路径;根对象时为空。</param>
|
||||
/// <param name="mappingNode">当前 YAML 对象节点。</param>
|
||||
/// <param name="mappingNode">当前 YAML 对象节点;用于让条件子 schema 在完整对象视图上做匹配。</param>
|
||||
/// <param name="seenProperties">当前对象已出现的属性集合。</param>
|
||||
/// <param name="schemaNode">对象 schema 节点。</param>
|
||||
/// <param name="references">可选的跨表引用收集器。</param>
|
||||
/// <param name="references">
|
||||
/// 可选的跨表引用收集器;当 <c>dependentSchemas</c> 命中且匹配成功时,只会回写该条件分支新增的引用。
|
||||
/// </param>
|
||||
private static void ValidateObjectConstraints(
|
||||
string tableName,
|
||||
string yamlPath,
|
||||
@ -924,6 +929,9 @@ internal static class YamlConfigSchemaValidator
|
||||
// dependentSchemas acts as an additional conditional constraint block on the
|
||||
// current object. Keep undeclared sibling fields outside the dependent sub-schema
|
||||
// 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(
|
||||
tableName,
|
||||
yamlPath,
|
||||
@ -3138,10 +3146,7 @@ internal static class YamlConfigSchemaValidator
|
||||
if (references is not null &&
|
||||
matchedReferences is not null)
|
||||
{
|
||||
foreach (var referenceUsage in matchedReferences)
|
||||
{
|
||||
references.Add(referenceUsage);
|
||||
}
|
||||
AddUniqueReferenceUsages(references, matchedReferences);
|
||||
}
|
||||
|
||||
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>
|
||||
/// 校验节点是否命中了 <c>not</c> 声明的禁用 schema。
|
||||
/// 与 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>
|
||||
/// 验证 <c>dependentSchemas</c> 子 schema 内的非法 <c>format</c> 也会在生成阶段直接给出诊断。
|
||||
/// </summary>
|
||||
|
||||
@ -1742,9 +1742,8 @@ function validateObjectNode(schemaNode, yamlNode, displayPath, diagnostics, loca
|
||||
}
|
||||
|
||||
if (schemaNode.dependentSchemas && typeof schemaNode.dependentSchemas === "object") {
|
||||
for (const [triggerProperty, dependentSchema] of Object.entries(schemaNode.dependentSchemas)) {
|
||||
if (!yamlNode.map.has(triggerProperty) ||
|
||||
matchesSchemaNode(dependentSchema, yamlNode, true)) {
|
||||
for (const [triggerProperty, dependentSchema] of getTriggeredDependentSchemas(schemaNode, yamlNode)) {
|
||||
if (matchesSchemaNode(dependentSchema, yamlNode, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -1795,6 +1794,32 @@ function validateObjectNode(schemaNode, yamlNode, displayPath, diagnostics, loca
|
||||
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.
|
||||
* This is used by array `contains`, where object sub-schemas must behave like
|
||||
@ -1869,12 +1894,9 @@ function matchesSchemaNodeInternal(schemaNode, yamlNode, allowUnknownObjectPrope
|
||||
}
|
||||
}
|
||||
|
||||
if (schemaNode.dependentSchemas && typeof schemaNode.dependentSchemas === "object") {
|
||||
for (const [triggerProperty, dependentSchema] of Object.entries(schemaNode.dependentSchemas)) {
|
||||
if (yamlNode.map.has(triggerProperty) &&
|
||||
!matchesSchemaNodeInternal(dependentSchema, yamlNode, true)) {
|
||||
return false;
|
||||
}
|
||||
for (const [, dependentSchema] of getTriggeredDependentSchemas(schemaNode, yamlNode)) {
|
||||
if (!matchesSchemaNodeInternal(dependentSchema, yamlNode, true)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user