mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-12 22:03:30 +08:00
fix(game): 清理剩余配置 schema warning
- 重构 YamlConfigSchemaValidator 的长方法为语义化 helper,清理剩余 MA0051 warning - 修复 条件分支 helper 的字符串比较方式,避免新增 MA0006 warning - 更新 analyzer warning reduction 跟踪与 trace,记录仓库根 clean build 已归零
This commit is contained in:
parent
104ac25dc3
commit
7da985947c
@ -320,6 +320,30 @@ internal static partial class YamlConfigSchemaValidator
|
|||||||
string propertyPath,
|
string propertyPath,
|
||||||
JsonElement element,
|
JsonElement element,
|
||||||
bool isRoot = false)
|
bool isRoot = false)
|
||||||
|
{
|
||||||
|
var typeName = ResolveNodeTypeName(tableName, schemaPath, propertyPath, element);
|
||||||
|
var referenceTableName = TryGetReferenceTableName(tableName, schemaPath, propertyPath, element);
|
||||||
|
ValidateObjectOnlyKeywords(tableName, schemaPath, propertyPath, element, typeName);
|
||||||
|
|
||||||
|
var parsedNode = CreateParsedNodeForType(
|
||||||
|
tableName,
|
||||||
|
schemaPath,
|
||||||
|
propertyPath,
|
||||||
|
element,
|
||||||
|
typeName,
|
||||||
|
referenceTableName,
|
||||||
|
isRoot);
|
||||||
|
return parsedNode.WithNegatedSchemaNode(ParseNegatedSchemaNode(tableName, schemaPath, propertyPath, element));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解析 schema 节点声明的类型名称,并在缺失或类型错误时立刻给出定位清晰的诊断。
|
||||||
|
/// </summary>
|
||||||
|
private static string ResolveNodeTypeName(
|
||||||
|
string tableName,
|
||||||
|
string schemaPath,
|
||||||
|
string propertyPath,
|
||||||
|
JsonElement element)
|
||||||
{
|
{
|
||||||
if (!element.TryGetProperty("type", out var typeElement) ||
|
if (!element.TryGetProperty("type", out var typeElement) ||
|
||||||
typeElement.ValueKind != JsonValueKind.String)
|
typeElement.ValueKind != JsonValueKind.String)
|
||||||
@ -332,20 +356,46 @@ internal static partial class YamlConfigSchemaValidator
|
|||||||
displayPath: GetDiagnosticPath(propertyPath));
|
displayPath: GetDiagnosticPath(propertyPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
var typeName = typeElement.GetString() ?? string.Empty;
|
return typeElement.GetString() ?? string.Empty;
|
||||||
var referenceTableName = TryGetReferenceTableName(tableName, schemaPath, propertyPath, element);
|
}
|
||||||
if (!string.Equals(typeName, "object", StringComparison.Ordinal) &&
|
|
||||||
TryGetObjectOnlyKeywordName(element) is { } objectOnlyKeywordName)
|
/// <summary>
|
||||||
|
/// 限制只允许对象 schema 使用对象专属关键字,避免后续分支在运行时才发现语义不兼容。
|
||||||
|
/// </summary>
|
||||||
|
private static void ValidateObjectOnlyKeywords(
|
||||||
|
string tableName,
|
||||||
|
string schemaPath,
|
||||||
|
string propertyPath,
|
||||||
|
JsonElement element,
|
||||||
|
string typeName)
|
||||||
|
{
|
||||||
|
if (string.Equals(typeName, "object", StringComparison.Ordinal) ||
|
||||||
|
TryGetObjectOnlyKeywordName(element) is not { } objectOnlyKeywordName)
|
||||||
{
|
{
|
||||||
throw ConfigLoadExceptionFactory.Create(
|
return;
|
||||||
ConfigLoadFailureKind.SchemaUnsupported,
|
|
||||||
tableName,
|
|
||||||
$"Property '{propertyPath}' in schema file '{schemaPath}' can only declare '{objectOnlyKeywordName}' on object schemas.",
|
|
||||||
schemaPath: schemaPath,
|
|
||||||
displayPath: GetDiagnosticPath(propertyPath));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var parsedNode = typeName switch
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
|
ConfigLoadFailureKind.SchemaUnsupported,
|
||||||
|
tableName,
|
||||||
|
$"Property '{propertyPath}' in schema file '{schemaPath}' can only declare '{objectOnlyKeywordName}' on object schemas.",
|
||||||
|
schemaPath: schemaPath,
|
||||||
|
displayPath: GetDiagnosticPath(propertyPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据声明的 schema 类型分派到对应的节点解析器。
|
||||||
|
/// </summary>
|
||||||
|
private static YamlConfigSchemaNode CreateParsedNodeForType(
|
||||||
|
string tableName,
|
||||||
|
string schemaPath,
|
||||||
|
string propertyPath,
|
||||||
|
JsonElement element,
|
||||||
|
string typeName,
|
||||||
|
string? referenceTableName,
|
||||||
|
bool isRoot)
|
||||||
|
{
|
||||||
|
return typeName switch
|
||||||
{
|
{
|
||||||
"object" => ParseObjectSchemaNode(
|
"object" => ParseObjectSchemaNode(
|
||||||
tableName,
|
tableName,
|
||||||
@ -391,7 +441,6 @@ internal static partial class YamlConfigSchemaValidator
|
|||||||
displayPath: GetDiagnosticPath(propertyPath),
|
displayPath: GetDiagnosticPath(propertyPath),
|
||||||
rawValue: typeName)
|
rawValue: typeName)
|
||||||
};
|
};
|
||||||
return parsedNode.WithNegatedSchemaNode(ParseNegatedSchemaNode(tableName, schemaPath, propertyPath, element));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -727,18 +776,68 @@ internal static partial class YamlConfigSchemaValidator
|
|||||||
ICollection<YamlConfigReferenceUsage>? references,
|
ICollection<YamlConfigReferenceUsage>? references,
|
||||||
bool allowUnknownObjectProperties)
|
bool allowUnknownObjectProperties)
|
||||||
{
|
{
|
||||||
if (node is not YamlMappingNode mappingNode)
|
var mappingNode = GetObjectMappingNode(tableName, yamlPath, displayPath, node, schemaNode);
|
||||||
|
var seenProperties = ValidateObjectPropertyEntries(
|
||||||
|
tableName,
|
||||||
|
yamlPath,
|
||||||
|
displayPath,
|
||||||
|
mappingNode,
|
||||||
|
schemaNode,
|
||||||
|
references,
|
||||||
|
allowUnknownObjectProperties);
|
||||||
|
ValidateRequiredObjectProperties(tableName, yamlPath, displayPath, schemaNode, seenProperties);
|
||||||
|
|
||||||
|
ValidateObjectConstraints(
|
||||||
|
tableName,
|
||||||
|
yamlPath,
|
||||||
|
displayPath,
|
||||||
|
mappingNode,
|
||||||
|
seenProperties,
|
||||||
|
schemaNode,
|
||||||
|
references);
|
||||||
|
|
||||||
|
ValidateAllowedValues(tableName, yamlPath, displayPath, mappingNode, schemaNode);
|
||||||
|
ValidateConstantValue(tableName, yamlPath, displayPath, mappingNode, schemaNode);
|
||||||
|
ValidateNegatedSchemaConstraint(tableName, yamlPath, displayPath, mappingNode, schemaNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 确认当前 YAML 节点确实是对象节点,避免后续属性枚举阶段再做重复判空与类型判断。
|
||||||
|
/// </summary>
|
||||||
|
private static YamlMappingNode GetObjectMappingNode(
|
||||||
|
string tableName,
|
||||||
|
string yamlPath,
|
||||||
|
string displayPath,
|
||||||
|
YamlNode node,
|
||||||
|
YamlConfigSchemaNode schemaNode)
|
||||||
|
{
|
||||||
|
if (node is YamlMappingNode mappingNode)
|
||||||
{
|
{
|
||||||
var subject = displayPath.Length == 0 ? "Root object" : $"Property '{displayPath}'";
|
return mappingNode;
|
||||||
throw ConfigLoadExceptionFactory.Create(
|
|
||||||
ConfigLoadFailureKind.PropertyTypeMismatch,
|
|
||||||
tableName,
|
|
||||||
$"{subject} in config file '{yamlPath}' must be an object.",
|
|
||||||
yamlPath: yamlPath,
|
|
||||||
schemaPath: schemaNode.SchemaPathHint,
|
|
||||||
displayPath: GetDiagnosticPath(displayPath));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var subject = displayPath.Length == 0 ? "Root object" : $"Property '{displayPath}'";
|
||||||
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
|
ConfigLoadFailureKind.PropertyTypeMismatch,
|
||||||
|
tableName,
|
||||||
|
$"{subject} in config file '{yamlPath}' must be an object.",
|
||||||
|
yamlPath: yamlPath,
|
||||||
|
schemaPath: schemaNode.SchemaPathHint,
|
||||||
|
displayPath: GetDiagnosticPath(displayPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 遍历对象属性并递归校验每个已声明字段,同时记录用于后续 required 与 dependency 判断的 sibling 集合。
|
||||||
|
/// </summary>
|
||||||
|
private static HashSet<string> ValidateObjectPropertyEntries(
|
||||||
|
string tableName,
|
||||||
|
string yamlPath,
|
||||||
|
string displayPath,
|
||||||
|
YamlMappingNode mappingNode,
|
||||||
|
YamlConfigSchemaNode schemaNode,
|
||||||
|
ICollection<YamlConfigReferenceUsage>? references,
|
||||||
|
bool allowUnknownObjectProperties)
|
||||||
|
{
|
||||||
var seenProperties = new HashSet<string>(StringComparer.Ordinal);
|
var seenProperties = new HashSet<string>(StringComparer.Ordinal);
|
||||||
foreach (var entry in mappingNode.Children)
|
foreach (var entry in mappingNode.Children)
|
||||||
{
|
{
|
||||||
@ -795,6 +894,19 @@ internal static partial class YamlConfigSchemaValidator
|
|||||||
allowUnknownObjectProperties);
|
allowUnknownObjectProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return seenProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 在对象主体字段遍历结束后统一检查缺失的 required 字段,保证错误消息使用稳定的完整路径。
|
||||||
|
/// </summary>
|
||||||
|
private static void ValidateRequiredObjectProperties(
|
||||||
|
string tableName,
|
||||||
|
string yamlPath,
|
||||||
|
string displayPath,
|
||||||
|
YamlConfigSchemaNode schemaNode,
|
||||||
|
HashSet<string> seenProperties)
|
||||||
|
{
|
||||||
if (schemaNode.RequiredProperties is null)
|
if (schemaNode.RequiredProperties is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@ -816,19 +928,6 @@ internal static partial class YamlConfigSchemaValidator
|
|||||||
schemaPath: schemaNode.SchemaPathHint,
|
schemaPath: schemaNode.SchemaPathHint,
|
||||||
displayPath: requiredPath);
|
displayPath: requiredPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
ValidateObjectConstraints(
|
|
||||||
tableName,
|
|
||||||
yamlPath,
|
|
||||||
displayPath,
|
|
||||||
mappingNode,
|
|
||||||
seenProperties,
|
|
||||||
schemaNode,
|
|
||||||
references);
|
|
||||||
|
|
||||||
ValidateAllowedValues(tableName, yamlPath, displayPath, mappingNode, schemaNode);
|
|
||||||
ValidateConstantValue(tableName, yamlPath, displayPath, mappingNode, schemaNode);
|
|
||||||
ValidateNegatedSchemaConstraint(tableName, yamlPath, displayPath, mappingNode, schemaNode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -864,11 +963,76 @@ internal static partial class YamlConfigSchemaValidator
|
|||||||
}
|
}
|
||||||
|
|
||||||
var propertyCount = seenProperties.Count;
|
var propertyCount = seenProperties.Count;
|
||||||
var subject = string.IsNullOrWhiteSpace(displayPath)
|
var subject = GetObjectConstraintSubject(displayPath);
|
||||||
|
|
||||||
|
ValidateObjectPropertyCountConstraints(
|
||||||
|
tableName,
|
||||||
|
yamlPath,
|
||||||
|
displayPath,
|
||||||
|
schemaNode,
|
||||||
|
constraints,
|
||||||
|
subject,
|
||||||
|
propertyCount);
|
||||||
|
ValidateDependentRequiredConstraints(
|
||||||
|
tableName,
|
||||||
|
yamlPath,
|
||||||
|
displayPath,
|
||||||
|
schemaNode,
|
||||||
|
constraints,
|
||||||
|
seenProperties);
|
||||||
|
ValidateDependentSchemas(
|
||||||
|
tableName,
|
||||||
|
yamlPath,
|
||||||
|
displayPath,
|
||||||
|
mappingNode,
|
||||||
|
schemaNode,
|
||||||
|
constraints,
|
||||||
|
references,
|
||||||
|
seenProperties,
|
||||||
|
subject);
|
||||||
|
ValidateAllOfSchemas(
|
||||||
|
tableName,
|
||||||
|
yamlPath,
|
||||||
|
displayPath,
|
||||||
|
mappingNode,
|
||||||
|
schemaNode,
|
||||||
|
constraints,
|
||||||
|
references,
|
||||||
|
subject);
|
||||||
|
ValidateConditionalObjectSchemas(
|
||||||
|
tableName,
|
||||||
|
yamlPath,
|
||||||
|
displayPath,
|
||||||
|
mappingNode,
|
||||||
|
schemaNode,
|
||||||
|
constraints,
|
||||||
|
references,
|
||||||
|
subject);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为对象约束构造统一的诊断主语,保证根对象与嵌套对象的错误消息格式一致。
|
||||||
|
/// </summary>
|
||||||
|
private static string GetObjectConstraintSubject(string displayPath)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(displayPath)
|
||||||
? "Root object"
|
? "Root object"
|
||||||
: $"Property '{displayPath}'";
|
: $"Property '{displayPath}'";
|
||||||
var rawValue = propertyCount.ToString(CultureInfo.InvariantCulture);
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 校验对象属性数量上下限。
|
||||||
|
/// </summary>
|
||||||
|
private static void ValidateObjectPropertyCountConstraints(
|
||||||
|
string tableName,
|
||||||
|
string yamlPath,
|
||||||
|
string displayPath,
|
||||||
|
YamlConfigSchemaNode schemaNode,
|
||||||
|
YamlConfigObjectConstraints constraints,
|
||||||
|
string subject,
|
||||||
|
int propertyCount)
|
||||||
|
{
|
||||||
|
var rawValue = propertyCount.ToString(CultureInfo.InvariantCulture);
|
||||||
if (constraints.MinProperties.HasValue &&
|
if (constraints.MinProperties.HasValue &&
|
||||||
propertyCount < constraints.MinProperties.Value)
|
propertyCount < constraints.MinProperties.Value)
|
||||||
{
|
{
|
||||||
@ -898,116 +1062,177 @@ internal static partial class YamlConfigSchemaValidator
|
|||||||
detail:
|
detail:
|
||||||
$"Maximum property count: {constraints.MaxProperties.Value.ToString(CultureInfo.InvariantCulture)}.");
|
$"Maximum property count: {constraints.MaxProperties.Value.ToString(CultureInfo.InvariantCulture)}.");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (constraints.DependentRequired is not null &&
|
/// <summary>
|
||||||
constraints.DependentRequired.Count > 0)
|
/// 使用已见 sibling 集合校验 dependentRequired,确保对象主路径与试匹配路径共用同一判定语义。
|
||||||
|
/// </summary>
|
||||||
|
private static void ValidateDependentRequiredConstraints(
|
||||||
|
string tableName,
|
||||||
|
string yamlPath,
|
||||||
|
string displayPath,
|
||||||
|
YamlConfigSchemaNode schemaNode,
|
||||||
|
YamlConfigObjectConstraints constraints,
|
||||||
|
HashSet<string> seenProperties)
|
||||||
|
{
|
||||||
|
if (constraints.DependentRequired is null ||
|
||||||
|
constraints.DependentRequired.Count == 0)
|
||||||
{
|
{
|
||||||
// Reuse the collected sibling-name set so the main validation path and
|
return;
|
||||||
// the contains/not matcher both interpret object dependencies identically.
|
|
||||||
foreach (var dependency in constraints.DependentRequired)
|
|
||||||
{
|
|
||||||
if (!seenProperties.Contains(dependency.Key))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var triggerPath = CombineDisplayPath(displayPath, dependency.Key);
|
|
||||||
foreach (var dependentProperty in dependency.Value)
|
|
||||||
{
|
|
||||||
if (seenProperties.Contains(dependentProperty))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var requiredPath = CombineDisplayPath(displayPath, dependentProperty);
|
|
||||||
throw ConfigLoadExceptionFactory.Create(
|
|
||||||
ConfigLoadFailureKind.MissingRequiredProperty,
|
|
||||||
tableName,
|
|
||||||
$"Property '{requiredPath}' in config file '{yamlPath}' is required when sibling property '{triggerPath}' is present.",
|
|
||||||
yamlPath: yamlPath,
|
|
||||||
schemaPath: schemaNode.SchemaPathHint,
|
|
||||||
displayPath: requiredPath,
|
|
||||||
detail:
|
|
||||||
$"Dependent requirement: when '{triggerPath}' exists, '{requiredPath}' must also be declared.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (constraints.DependentSchemas is not null &&
|
// Reuse the collected sibling-name set so the main validation path and
|
||||||
constraints.DependentSchemas.Count > 0)
|
// the contains/not matcher both interpret object dependencies identically.
|
||||||
|
foreach (var dependency in constraints.DependentRequired)
|
||||||
{
|
{
|
||||||
foreach (var dependency in constraints.DependentSchemas)
|
if (!seenProperties.Contains(dependency.Key))
|
||||||
{
|
{
|
||||||
if (!seenProperties.Contains(dependency.Key))
|
continue;
|
||||||
{
|
}
|
||||||
continue;
|
|
||||||
}
|
var triggerPath = CombineDisplayPath(displayPath, dependency.Key);
|
||||||
|
foreach (var dependentProperty in dependency.Value)
|
||||||
var triggerPath = CombineDisplayPath(displayPath, dependency.Key);
|
{
|
||||||
// dependentSchemas acts as an additional conditional constraint block on the
|
if (seenProperties.Contains(dependentProperty))
|
||||||
// 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,
|
|
||||||
displayPath,
|
|
||||||
mappingNode,
|
|
||||||
dependency.Value,
|
|
||||||
references,
|
|
||||||
allowUnknownObjectProperties: true))
|
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var requiredPath = CombineDisplayPath(displayPath, dependentProperty);
|
||||||
throw ConfigLoadExceptionFactory.Create(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
ConfigLoadFailureKind.ConstraintViolation,
|
ConfigLoadFailureKind.MissingRequiredProperty,
|
||||||
tableName,
|
tableName,
|
||||||
$"{subject} in config file '{yamlPath}' must satisfy the 'dependentSchemas' schema triggered by sibling property '{triggerPath}'.",
|
$"Property '{requiredPath}' in config file '{yamlPath}' is required when sibling property '{triggerPath}' is present.",
|
||||||
yamlPath: yamlPath,
|
yamlPath: yamlPath,
|
||||||
schemaPath: schemaNode.SchemaPathHint,
|
schemaPath: schemaNode.SchemaPathHint,
|
||||||
displayPath: GetDiagnosticPath(displayPath),
|
displayPath: requiredPath,
|
||||||
detail:
|
detail:
|
||||||
$"Dependent schema: when '{triggerPath}' exists, the current object must satisfy the corresponding inline schema.");
|
$"Dependent requirement: when '{triggerPath}' exists, '{requiredPath}' must also be declared.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (constraints.AllOfSchemas is not null &&
|
/// <summary>
|
||||||
constraints.AllOfSchemas.Count > 0)
|
/// 在触发字段出现时,以 focused matcher 语义试跑 dependentSchemas。
|
||||||
|
/// </summary>
|
||||||
|
private static void ValidateDependentSchemas(
|
||||||
|
string tableName,
|
||||||
|
string yamlPath,
|
||||||
|
string displayPath,
|
||||||
|
YamlMappingNode mappingNode,
|
||||||
|
YamlConfigSchemaNode schemaNode,
|
||||||
|
YamlConfigObjectConstraints constraints,
|
||||||
|
ICollection<YamlConfigReferenceUsage>? references,
|
||||||
|
HashSet<string> seenProperties,
|
||||||
|
string subject)
|
||||||
|
{
|
||||||
|
if (constraints.DependentSchemas is null ||
|
||||||
|
constraints.DependentSchemas.Count == 0)
|
||||||
{
|
{
|
||||||
for (var index = 0; index < constraints.AllOfSchemas.Count; index++)
|
return;
|
||||||
{
|
|
||||||
var allOfSchema = constraints.AllOfSchemas[index];
|
|
||||||
// allOf follows the same focused constraint block semantics as dependentSchemas:
|
|
||||||
// the inline schema may validate a subset of the current object without forcing
|
|
||||||
// unrelated sibling fields to be restated.
|
|
||||||
if (TryMatchSchemaNode(
|
|
||||||
tableName,
|
|
||||||
yamlPath,
|
|
||||||
displayPath,
|
|
||||||
mappingNode,
|
|
||||||
allOfSchema,
|
|
||||||
references,
|
|
||||||
allowUnknownObjectProperties: true))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var allOfEntryNumber = index + 1;
|
|
||||||
throw ConfigLoadExceptionFactory.Create(
|
|
||||||
ConfigLoadFailureKind.ConstraintViolation,
|
|
||||||
tableName,
|
|
||||||
$"{subject} in config file '{yamlPath}' must satisfy all 'allOf' schemas, but entry #{allOfEntryNumber.ToString(CultureInfo.InvariantCulture)} did not match.",
|
|
||||||
yamlPath: yamlPath,
|
|
||||||
schemaPath: schemaNode.SchemaPathHint,
|
|
||||||
displayPath: GetDiagnosticPath(displayPath),
|
|
||||||
detail:
|
|
||||||
$"allOf entry #{allOfEntryNumber.ToString(CultureInfo.InvariantCulture)} must match the current object.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var dependency in constraints.DependentSchemas)
|
||||||
|
{
|
||||||
|
if (!seenProperties.Contains(dependency.Key))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var triggerPath = CombineDisplayPath(displayPath, dependency.Key);
|
||||||
|
// 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,
|
||||||
|
displayPath,
|
||||||
|
mappingNode,
|
||||||
|
dependency.Value,
|
||||||
|
references,
|
||||||
|
allowUnknownObjectProperties: true))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
|
ConfigLoadFailureKind.ConstraintViolation,
|
||||||
|
tableName,
|
||||||
|
$"{subject} in config file '{yamlPath}' must satisfy the 'dependentSchemas' schema triggered by sibling property '{triggerPath}'.",
|
||||||
|
yamlPath: yamlPath,
|
||||||
|
schemaPath: schemaNode.SchemaPathHint,
|
||||||
|
displayPath: GetDiagnosticPath(displayPath),
|
||||||
|
detail:
|
||||||
|
$"Dependent schema: when '{triggerPath}' exists, the current object must satisfy the corresponding inline schema.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 逐条校验 allOf 约束,保持与 dependentSchemas 相同的 focused object 匹配语义。
|
||||||
|
/// </summary>
|
||||||
|
private static void ValidateAllOfSchemas(
|
||||||
|
string tableName,
|
||||||
|
string yamlPath,
|
||||||
|
string displayPath,
|
||||||
|
YamlMappingNode mappingNode,
|
||||||
|
YamlConfigSchemaNode schemaNode,
|
||||||
|
YamlConfigObjectConstraints constraints,
|
||||||
|
ICollection<YamlConfigReferenceUsage>? references,
|
||||||
|
string subject)
|
||||||
|
{
|
||||||
|
if (constraints.AllOfSchemas is null ||
|
||||||
|
constraints.AllOfSchemas.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var index = 0; index < constraints.AllOfSchemas.Count; index++)
|
||||||
|
{
|
||||||
|
var allOfSchema = constraints.AllOfSchemas[index];
|
||||||
|
// allOf follows the same focused constraint block semantics as dependentSchemas:
|
||||||
|
// the inline schema may validate a subset of the current object without forcing
|
||||||
|
// unrelated sibling fields to be restated.
|
||||||
|
if (TryMatchSchemaNode(
|
||||||
|
tableName,
|
||||||
|
yamlPath,
|
||||||
|
displayPath,
|
||||||
|
mappingNode,
|
||||||
|
allOfSchema,
|
||||||
|
references,
|
||||||
|
allowUnknownObjectProperties: true))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var allOfEntryNumber = index + 1;
|
||||||
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
|
ConfigLoadFailureKind.ConstraintViolation,
|
||||||
|
tableName,
|
||||||
|
$"{subject} in config file '{yamlPath}' must satisfy all 'allOf' schemas, but entry #{allOfEntryNumber.ToString(CultureInfo.InvariantCulture)} did not match.",
|
||||||
|
yamlPath: yamlPath,
|
||||||
|
schemaPath: schemaNode.SchemaPathHint,
|
||||||
|
displayPath: GetDiagnosticPath(displayPath),
|
||||||
|
detail:
|
||||||
|
$"allOf entry #{allOfEntryNumber.ToString(CultureInfo.InvariantCulture)} must match the current object.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行对象级 if/then/else 约束,并沿用 focused matcher 允许条件 schema 只声明关注字段。
|
||||||
|
/// </summary>
|
||||||
|
private static void ValidateConditionalObjectSchemas(
|
||||||
|
string tableName,
|
||||||
|
string yamlPath,
|
||||||
|
string displayPath,
|
||||||
|
YamlMappingNode mappingNode,
|
||||||
|
YamlConfigSchemaNode schemaNode,
|
||||||
|
YamlConfigObjectConstraints constraints,
|
||||||
|
ICollection<YamlConfigReferenceUsage>? references,
|
||||||
|
string subject)
|
||||||
|
{
|
||||||
var conditionalSchemas = constraints.ConditionalSchemas;
|
var conditionalSchemas = constraints.ConditionalSchemas;
|
||||||
if (conditionalSchemas is null)
|
if (conditionalSchemas is null)
|
||||||
{
|
{
|
||||||
@ -1025,49 +1250,72 @@ internal static partial class YamlConfigSchemaValidator
|
|||||||
conditionalSchemas.IfSchema,
|
conditionalSchemas.IfSchema,
|
||||||
references,
|
references,
|
||||||
allowUnknownObjectProperties: true);
|
allowUnknownObjectProperties: true);
|
||||||
if (ifMatched &&
|
ValidateConditionalSchemaBranch(
|
||||||
conditionalSchemas.ThenSchema is not null &&
|
tableName,
|
||||||
!TryMatchSchemaNode(
|
yamlPath,
|
||||||
|
displayPath,
|
||||||
|
mappingNode,
|
||||||
|
schemaNode,
|
||||||
|
references,
|
||||||
|
subject,
|
||||||
|
ifMatched,
|
||||||
|
conditionalSchemas.ThenSchema,
|
||||||
|
branchName: "then",
|
||||||
|
failureDetail:
|
||||||
|
"Conditional schema: the current object matched the inline 'if' schema, so it must also satisfy the corresponding 'then' schema.");
|
||||||
|
ValidateConditionalSchemaBranch(
|
||||||
|
tableName,
|
||||||
|
yamlPath,
|
||||||
|
displayPath,
|
||||||
|
mappingNode,
|
||||||
|
schemaNode,
|
||||||
|
references,
|
||||||
|
subject,
|
||||||
|
!ifMatched,
|
||||||
|
conditionalSchemas.ElseSchema,
|
||||||
|
branchName: "else",
|
||||||
|
failureDetail:
|
||||||
|
"Conditional schema: the current object did not match the inline 'if' schema, so it must satisfy the corresponding 'else' schema.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 校验 if/then/else 的单个分支,并在条件命中但分支未通过时提供统一诊断。
|
||||||
|
/// </summary>
|
||||||
|
private static void ValidateConditionalSchemaBranch(
|
||||||
|
string tableName,
|
||||||
|
string yamlPath,
|
||||||
|
string displayPath,
|
||||||
|
YamlMappingNode mappingNode,
|
||||||
|
YamlConfigSchemaNode schemaNode,
|
||||||
|
ICollection<YamlConfigReferenceUsage>? references,
|
||||||
|
string subject,
|
||||||
|
bool shouldValidate,
|
||||||
|
YamlConfigSchemaNode? branchSchema,
|
||||||
|
string branchName,
|
||||||
|
string failureDetail)
|
||||||
|
{
|
||||||
|
if (!shouldValidate ||
|
||||||
|
branchSchema is null ||
|
||||||
|
TryMatchSchemaNode(
|
||||||
tableName,
|
tableName,
|
||||||
yamlPath,
|
yamlPath,
|
||||||
displayPath,
|
displayPath,
|
||||||
mappingNode,
|
mappingNode,
|
||||||
conditionalSchemas.ThenSchema,
|
branchSchema,
|
||||||
references,
|
references,
|
||||||
allowUnknownObjectProperties: true))
|
allowUnknownObjectProperties: true))
|
||||||
{
|
{
|
||||||
throw ConfigLoadExceptionFactory.Create(
|
return;
|
||||||
ConfigLoadFailureKind.ConstraintViolation,
|
|
||||||
tableName,
|
|
||||||
$"{subject} in config file '{yamlPath}' must satisfy the 'then' schema because the inline 'if' condition matched.",
|
|
||||||
yamlPath: yamlPath,
|
|
||||||
schemaPath: schemaNode.SchemaPathHint,
|
|
||||||
displayPath: GetDiagnosticPath(displayPath),
|
|
||||||
detail:
|
|
||||||
"Conditional schema: the current object matched the inline 'if' schema, so it must also satisfy the corresponding 'then' schema.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ifMatched &&
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
conditionalSchemas.ElseSchema is not null &&
|
ConfigLoadFailureKind.ConstraintViolation,
|
||||||
!TryMatchSchemaNode(
|
tableName,
|
||||||
tableName,
|
$"{subject} in config file '{yamlPath}' must satisfy the '{branchName}' schema because the inline 'if' condition {(string.Equals(branchName, "then", StringComparison.Ordinal) ? "matched" : "did not match")}.",
|
||||||
yamlPath,
|
yamlPath: yamlPath,
|
||||||
displayPath,
|
schemaPath: schemaNode.SchemaPathHint,
|
||||||
mappingNode,
|
displayPath: GetDiagnosticPath(displayPath),
|
||||||
conditionalSchemas.ElseSchema,
|
detail: failureDetail);
|
||||||
references,
|
|
||||||
allowUnknownObjectProperties: true))
|
|
||||||
{
|
|
||||||
throw ConfigLoadExceptionFactory.Create(
|
|
||||||
ConfigLoadFailureKind.ConstraintViolation,
|
|
||||||
tableName,
|
|
||||||
$"{subject} in config file '{yamlPath}' must satisfy the 'else' schema because the inline 'if' condition did not match.",
|
|
||||||
yamlPath: yamlPath,
|
|
||||||
schemaPath: schemaNode.SchemaPathHint,
|
|
||||||
displayPath: GetDiagnosticPath(displayPath),
|
|
||||||
detail:
|
|
||||||
"Conditional schema: the current object did not match the inline 'if' schema, so it must satisfy the corresponding 'else' schema.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -1154,29 +1402,81 @@ internal static partial class YamlConfigSchemaValidator
|
|||||||
YamlConfigSchemaNode schemaNode,
|
YamlConfigSchemaNode schemaNode,
|
||||||
ICollection<YamlConfigReferenceUsage>? references)
|
ICollection<YamlConfigReferenceUsage>? references)
|
||||||
{
|
{
|
||||||
if (node is not YamlScalarNode scalarNode)
|
var scalarNode = GetScalarNodeOrThrow(tableName, yamlPath, displayPath, node, schemaNode);
|
||||||
|
var value = GetScalarValueOrThrow(tableName, yamlPath, displayPath, scalarNode, schemaNode);
|
||||||
|
ValidateScalarTypeOrThrow(tableName, yamlPath, displayPath, scalarNode, schemaNode, value);
|
||||||
|
var normalizedValue = NormalizeScalarValue(schemaNode.NodeType, value);
|
||||||
|
ValidateAllowedValues(tableName, yamlPath, displayPath, scalarNode, schemaNode);
|
||||||
|
|
||||||
|
if (schemaNode.Constraints is not null)
|
||||||
{
|
{
|
||||||
throw ConfigLoadExceptionFactory.Create(
|
ValidateScalarConstraints(tableName, yamlPath, displayPath, value, normalizedValue, schemaNode);
|
||||||
ConfigLoadFailureKind.PropertyTypeMismatch,
|
|
||||||
tableName,
|
|
||||||
$"Property '{displayPath}' in config file '{yamlPath}' must be a scalar value of type '{GetTypeName(schemaNode.NodeType)}'.",
|
|
||||||
yamlPath: yamlPath,
|
|
||||||
schemaPath: schemaNode.SchemaPathHint,
|
|
||||||
displayPath: GetDiagnosticPath(displayPath));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var value = scalarNode.Value;
|
ValidateConstantValue(tableName, yamlPath, displayPath, scalarNode, schemaNode);
|
||||||
if (value is null)
|
ValidateNegatedSchemaConstraint(tableName, yamlPath, displayPath, scalarNode, schemaNode);
|
||||||
|
CollectScalarReferenceUsage(yamlPath, displayPath, schemaNode, references, normalizedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 确认 schema 期望的节点在 YAML 中仍然表现为标量。
|
||||||
|
/// </summary>
|
||||||
|
private static YamlScalarNode GetScalarNodeOrThrow(
|
||||||
|
string tableName,
|
||||||
|
string yamlPath,
|
||||||
|
string displayPath,
|
||||||
|
YamlNode node,
|
||||||
|
YamlConfigSchemaNode schemaNode)
|
||||||
|
{
|
||||||
|
if (node is YamlScalarNode scalarNode)
|
||||||
{
|
{
|
||||||
throw ConfigLoadExceptionFactory.Create(
|
return scalarNode;
|
||||||
ConfigLoadFailureKind.NullScalarValue,
|
|
||||||
tableName,
|
|
||||||
$"Property '{displayPath}' in config file '{yamlPath}' cannot be null when schema type is '{GetTypeName(schemaNode.NodeType)}'.",
|
|
||||||
yamlPath: yamlPath,
|
|
||||||
schemaPath: schemaNode.SchemaPathHint,
|
|
||||||
displayPath: GetDiagnosticPath(displayPath));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
|
ConfigLoadFailureKind.PropertyTypeMismatch,
|
||||||
|
tableName,
|
||||||
|
$"Property '{displayPath}' in config file '{yamlPath}' must be a scalar value of type '{GetTypeName(schemaNode.NodeType)}'.",
|
||||||
|
yamlPath: yamlPath,
|
||||||
|
schemaPath: schemaNode.SchemaPathHint,
|
||||||
|
displayPath: GetDiagnosticPath(displayPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 读取标量文本值,并把 YAML 显式 null 与普通空字符串区分开。
|
||||||
|
/// </summary>
|
||||||
|
private static string GetScalarValueOrThrow(
|
||||||
|
string tableName,
|
||||||
|
string yamlPath,
|
||||||
|
string displayPath,
|
||||||
|
YamlScalarNode scalarNode,
|
||||||
|
YamlConfigSchemaNode schemaNode)
|
||||||
|
{
|
||||||
|
if (scalarNode.Value is { } value)
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
|
ConfigLoadFailureKind.NullScalarValue,
|
||||||
|
tableName,
|
||||||
|
$"Property '{displayPath}' in config file '{yamlPath}' cannot be null when schema type is '{GetTypeName(schemaNode.NodeType)}'.",
|
||||||
|
yamlPath: yamlPath,
|
||||||
|
schemaPath: schemaNode.SchemaPathHint,
|
||||||
|
displayPath: GetDiagnosticPath(displayPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按 schema 声明的标量类型验证 YAML 文本值。
|
||||||
|
/// </summary>
|
||||||
|
private static void ValidateScalarTypeOrThrow(
|
||||||
|
string tableName,
|
||||||
|
string yamlPath,
|
||||||
|
string displayPath,
|
||||||
|
YamlScalarNode scalarNode,
|
||||||
|
YamlConfigSchemaNode schemaNode,
|
||||||
|
string value)
|
||||||
|
{
|
||||||
var tag = scalarNode.Tag.ToString();
|
var tag = scalarNode.Tag.ToString();
|
||||||
var isValid = schemaNode.NodeType switch
|
var isValid = schemaNode.NodeType switch
|
||||||
{
|
{
|
||||||
@ -1194,42 +1494,45 @@ internal static partial class YamlConfigSchemaValidator
|
|||||||
YamlConfigSchemaPropertyType.Boolean => bool.TryParse(value, out _),
|
YamlConfigSchemaPropertyType.Boolean => bool.TryParse(value, out _),
|
||||||
_ => false
|
_ => false
|
||||||
};
|
};
|
||||||
|
if (isValid)
|
||||||
if (!isValid)
|
|
||||||
{
|
{
|
||||||
throw ConfigLoadExceptionFactory.Create(
|
return;
|
||||||
ConfigLoadFailureKind.PropertyTypeMismatch,
|
|
||||||
tableName,
|
|
||||||
$"Property '{displayPath}' in config file '{yamlPath}' must be of type '{GetTypeName(schemaNode.NodeType)}', but the current YAML scalar value is '{value}'.",
|
|
||||||
yamlPath: yamlPath,
|
|
||||||
schemaPath: schemaNode.SchemaPathHint,
|
|
||||||
displayPath: GetDiagnosticPath(displayPath),
|
|
||||||
rawValue: value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var normalizedValue = NormalizeScalarValue(schemaNode.NodeType, value);
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
ValidateAllowedValues(tableName, yamlPath, displayPath, scalarNode, schemaNode);
|
ConfigLoadFailureKind.PropertyTypeMismatch,
|
||||||
|
tableName,
|
||||||
|
$"Property '{displayPath}' in config file '{yamlPath}' must be of type '{GetTypeName(schemaNode.NodeType)}', but the current YAML scalar value is '{value}'.",
|
||||||
|
yamlPath: yamlPath,
|
||||||
|
schemaPath: schemaNode.SchemaPathHint,
|
||||||
|
displayPath: GetDiagnosticPath(displayPath),
|
||||||
|
rawValue: value);
|
||||||
|
}
|
||||||
|
|
||||||
if (schemaNode.Constraints is not null)
|
/// <summary>
|
||||||
|
/// 在标量值成功通过本地校验后,再把引用信息回写给外层收集器。
|
||||||
|
/// </summary>
|
||||||
|
private static void CollectScalarReferenceUsage(
|
||||||
|
string yamlPath,
|
||||||
|
string displayPath,
|
||||||
|
YamlConfigSchemaNode schemaNode,
|
||||||
|
ICollection<YamlConfigReferenceUsage>? references,
|
||||||
|
string normalizedValue)
|
||||||
|
{
|
||||||
|
if (schemaNode.ReferenceTableName is null ||
|
||||||
|
references is null)
|
||||||
{
|
{
|
||||||
ValidateScalarConstraints(tableName, yamlPath, displayPath, value, normalizedValue, schemaNode);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ValidateConstantValue(tableName, yamlPath, displayPath, scalarNode, schemaNode);
|
references.Add(
|
||||||
ValidateNegatedSchemaConstraint(tableName, yamlPath, displayPath, scalarNode, schemaNode);
|
new YamlConfigReferenceUsage(
|
||||||
|
yamlPath,
|
||||||
if (schemaNode.ReferenceTableName != null &&
|
schemaNode.SchemaPathHint,
|
||||||
references is not null)
|
displayPath,
|
||||||
{
|
normalizedValue,
|
||||||
references.Add(
|
schemaNode.ReferenceTableName,
|
||||||
new YamlConfigReferenceUsage(
|
schemaNode.NodeType));
|
||||||
yamlPath,
|
|
||||||
schemaNode.SchemaPathHint,
|
|
||||||
displayPath,
|
|
||||||
normalizedValue,
|
|
||||||
schemaNode.ReferenceTableName,
|
|
||||||
schemaNode.NodeType));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -2392,6 +2695,45 @@ internal static partial class YamlConfigSchemaValidator
|
|||||||
rawValue,
|
rawValue,
|
||||||
normalizedValue,
|
normalizedValue,
|
||||||
schemaNode);
|
schemaNode);
|
||||||
|
ValidateNumericLowerBounds(
|
||||||
|
tableName,
|
||||||
|
yamlPath,
|
||||||
|
displayPath,
|
||||||
|
rawValue,
|
||||||
|
schemaNode,
|
||||||
|
constraints,
|
||||||
|
numericValue);
|
||||||
|
ValidateNumericUpperBounds(
|
||||||
|
tableName,
|
||||||
|
yamlPath,
|
||||||
|
displayPath,
|
||||||
|
rawValue,
|
||||||
|
schemaNode,
|
||||||
|
constraints,
|
||||||
|
numericValue);
|
||||||
|
ValidateNumericMultipleOfConstraint(
|
||||||
|
tableName,
|
||||||
|
yamlPath,
|
||||||
|
displayPath,
|
||||||
|
rawValue,
|
||||||
|
normalizedValue,
|
||||||
|
schemaNode,
|
||||||
|
constraints,
|
||||||
|
numericValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 校验数值的最小值与开区间下界。
|
||||||
|
/// </summary>
|
||||||
|
private static void ValidateNumericLowerBounds(
|
||||||
|
string tableName,
|
||||||
|
string yamlPath,
|
||||||
|
string displayPath,
|
||||||
|
string rawValue,
|
||||||
|
YamlConfigSchemaNode schemaNode,
|
||||||
|
YamlConfigNumericConstraints constraints,
|
||||||
|
double numericValue)
|
||||||
|
{
|
||||||
if (constraints.Minimum.HasValue && numericValue < constraints.Minimum.Value)
|
if (constraints.Minimum.HasValue && numericValue < constraints.Minimum.Value)
|
||||||
{
|
{
|
||||||
throw ConfigLoadExceptionFactory.Create(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
@ -2418,7 +2760,20 @@ internal static partial class YamlConfigSchemaValidator
|
|||||||
detail:
|
detail:
|
||||||
$"Exclusive minimum allowed value: {constraints.ExclusiveMinimum.Value.ToString(CultureInfo.InvariantCulture)}.");
|
$"Exclusive minimum allowed value: {constraints.ExclusiveMinimum.Value.ToString(CultureInfo.InvariantCulture)}.");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 校验数值的最大值与开区间上界。
|
||||||
|
/// </summary>
|
||||||
|
private static void ValidateNumericUpperBounds(
|
||||||
|
string tableName,
|
||||||
|
string yamlPath,
|
||||||
|
string displayPath,
|
||||||
|
string rawValue,
|
||||||
|
YamlConfigSchemaNode schemaNode,
|
||||||
|
YamlConfigNumericConstraints constraints,
|
||||||
|
double numericValue)
|
||||||
|
{
|
||||||
if (constraints.Maximum.HasValue && numericValue > constraints.Maximum.Value)
|
if (constraints.Maximum.HasValue && numericValue > constraints.Maximum.Value)
|
||||||
{
|
{
|
||||||
throw ConfigLoadExceptionFactory.Create(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
@ -2445,21 +2800,37 @@ internal static partial class YamlConfigSchemaValidator
|
|||||||
detail:
|
detail:
|
||||||
$"Exclusive maximum allowed value: {constraints.ExclusiveMaximum.Value.ToString(CultureInfo.InvariantCulture)}.");
|
$"Exclusive maximum allowed value: {constraints.ExclusiveMaximum.Value.ToString(CultureInfo.InvariantCulture)}.");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (constraints.MultipleOf.HasValue &&
|
/// <summary>
|
||||||
!IsMultipleOf(normalizedValue, numericValue, constraints.MultipleOf.Value))
|
/// 校验数值是否满足 multipleOf 步进约束。
|
||||||
|
/// </summary>
|
||||||
|
private static void ValidateNumericMultipleOfConstraint(
|
||||||
|
string tableName,
|
||||||
|
string yamlPath,
|
||||||
|
string displayPath,
|
||||||
|
string rawValue,
|
||||||
|
string normalizedValue,
|
||||||
|
YamlConfigSchemaNode schemaNode,
|
||||||
|
YamlConfigNumericConstraints constraints,
|
||||||
|
double numericValue)
|
||||||
|
{
|
||||||
|
if (!constraints.MultipleOf.HasValue ||
|
||||||
|
IsMultipleOf(normalizedValue, numericValue, constraints.MultipleOf.Value))
|
||||||
{
|
{
|
||||||
throw ConfigLoadExceptionFactory.Create(
|
return;
|
||||||
ConfigLoadFailureKind.ConstraintViolation,
|
|
||||||
tableName,
|
|
||||||
$"Property '{displayPath}' in config file '{yamlPath}' must be a multiple of {constraints.MultipleOf.Value.ToString(CultureInfo.InvariantCulture)}, but the current YAML scalar value is '{rawValue}'.",
|
|
||||||
yamlPath: yamlPath,
|
|
||||||
schemaPath: schemaNode.SchemaPathHint,
|
|
||||||
displayPath: GetDiagnosticPath(displayPath),
|
|
||||||
rawValue: rawValue,
|
|
||||||
detail:
|
|
||||||
$"Required numeric step: {constraints.MultipleOf.Value.ToString(CultureInfo.InvariantCulture)}.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
|
ConfigLoadFailureKind.ConstraintViolation,
|
||||||
|
tableName,
|
||||||
|
$"Property '{displayPath}' in config file '{yamlPath}' must be a multiple of {constraints.MultipleOf.Value.ToString(CultureInfo.InvariantCulture)}, but the current YAML scalar value is '{rawValue}'.",
|
||||||
|
yamlPath: yamlPath,
|
||||||
|
schemaPath: schemaNode.SchemaPathHint,
|
||||||
|
displayPath: GetDiagnosticPath(displayPath),
|
||||||
|
rawValue: rawValue,
|
||||||
|
detail:
|
||||||
|
$"Required numeric step: {constraints.MultipleOf.Value.ToString(CultureInfo.InvariantCulture)}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -6,48 +6,43 @@
|
|||||||
|
|
||||||
## 当前恢复点
|
## 当前恢复点
|
||||||
|
|
||||||
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-093`
|
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-094`
|
||||||
- 当前阶段:`Phase 93`
|
- 当前阶段:`Phase 94`
|
||||||
- 当前焦点:
|
- 当前焦点:
|
||||||
- `2026-04-29` 使用 `$gframework-batch-boot 50` 从 clean build warning 基线继续分批清理 analyzer warnings
|
- `2026-04-29` 继续按 `$gframework-batch-boot 50` 从仓库根 `dotnet clean` + `dotnet build` 的权威 warning 基线收尾 `YamlConfigSchemaValidator`
|
||||||
- 已接受三个 worker 的 `GFramework.Cqrs.Tests/Mediator/*` 独立切片,三个 Mediator 测试文件的 warning 已清零
|
- 本轮 clean build 只剩 `15` 条 warning,但实际只对应 `YamlConfigSchemaValidator.cs` 同一文件中的 `5` 个独立 `MA0051` 热点,因此不再并发派发 worker,避免同文件冲突
|
||||||
- 主线程补齐 `YamlConfigSchemaValidator` 运行时正则 timeout 与 ordinal 字符串比较,先收掉低风险 `MA0009` / `MA0006`
|
- 已将 `ParseNode`、`ValidateObjectNode`、`ValidateObjectConstraints`、`ValidateScalarNode`、`ValidateNumericScalarConstraints` 按语义拆成 helper,并补齐对象条件分支 helper
|
||||||
- 已收口两个 Game 追加切片:`YamlConfigSchemaValidator.ObjectKeywords.cs` 方法拆分与 schema model 类型拆文件
|
- 当前仓库根 clean build 已收敛到 `0` warnings、`0` errors;本轮停止原因从“接近文件阈值”切换为“当前 warning hotspot 已耗尽”
|
||||||
- 当前停止条件为相对 `origin/main` 接近 `50` 个变更文件;本轮按用户要求到此结束,不再继续开新切片
|
|
||||||
|
|
||||||
## 当前活跃事实
|
## 当前活跃事实
|
||||||
|
|
||||||
- 当前 `origin/main` 基线提交为 `0e32dab`(`2026-04-28T17:15:47+08:00`)。
|
- 当前 `origin/main` 基线提交为 `0e32dab`(`2026-04-28T17:15:47+08:00`)。
|
||||||
- 当前直接验证结果:
|
- 当前直接验证结果:
|
||||||
- `dotnet clean -p:RestoreFallbackFolders= -v:quiet`
|
- `dotnet clean`
|
||||||
- 最新结果:成功;标准 `dotnet clean` 仍会先命中当前 WSL 环境的 Windows NuGet fallback 目录,已按既有环境口径先执行 `dotnet restore GFramework.sln -p:RestoreFallbackFolders= --disable-parallel` 后清理
|
- 最新结果:成功;标准仓库根 clean 本轮可直接运行,未再命中需要额外绕开的环境噪音
|
||||||
- `dotnet build -p:RestoreFallbackFolders= -clp:WarningsOnly -v:minimal -m:1 -nodeReuse:false`
|
- `dotnet build`
|
||||||
- 最新结果:成功;`15` warnings、`0` errors;warning 从本轮基线 `236` 降到 `15`
|
- 最新结果:成功;`0 Warning(s)`、`0 Error(s)`;本轮开始时同一口径 clean build 的 `15` 条 warning 已全部清零
|
||||||
- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release -p:RestoreFallbackFolders= -m:1 -nodeReuse:false -clp:Summary`
|
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release -clp:Summary`
|
||||||
- 最新结果:成功;`0 Warning(s)`、`0 Error(s)`
|
- 最新结果:成功;`0 Warning(s)`、`0 Error(s)`
|
||||||
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-build -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~Mediator"`
|
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~YamlConfigLoaderTests|FullyQualifiedName~YamlConfigSchemaValidatorTests"`
|
||||||
- 最新结果:成功;`45` 通过、`0` 失败
|
|
||||||
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release -p:RestoreFallbackFolders= -m:1 -nodeReuse:false -clp:Summary`
|
|
||||||
- 最新结果:成功;`0 Warning(s)`、`0 Error(s)`
|
|
||||||
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~YamlConfigLoaderTests|FullyQualifiedName~YamlConfigSchemaValidatorTests"`
|
|
||||||
- 最新结果:成功;`80` 通过、`0` 失败
|
- 最新结果:成功;`80` 通过、`0` 失败
|
||||||
|
- `git diff --check`
|
||||||
|
- 最新结果:成功;无新增 whitespace / conflict-marker 问题
|
||||||
- 当前批次摘要:
|
- 当前批次摘要:
|
||||||
- 当前分支提交后预计相对 `origin/main...HEAD` 包含 `22` 个变更文件,低于 `50` 个文件阈值
|
- 当前分支提交后预计相对 `origin/main...HEAD` 包含 `22` 个变更文件,低于 `50` 个文件阈值
|
||||||
- 已完成 worker 切片:
|
- 已完成 worker 切片:
|
||||||
- `ed269d4`:`MediatorArchitectureIntegrationTests.cs`,清理 `MA0048` / `MA0004` / `MA0016`
|
- `ed269d4`:`MediatorArchitectureIntegrationTests.cs`,清理 `MA0048` / `MA0004` / `MA0016`
|
||||||
- `121df44`:`MediatorAdvancedFeaturesTests.cs`,清理 `MA0048` / `MA0004` / `MA0015`
|
- `121df44`:`MediatorAdvancedFeaturesTests.cs`,清理 `MA0048` / `MA0004` / `MA0015`
|
||||||
- `9109eec`:`MediatorComprehensiveTests.cs`,清理 `MA0048` / `MA0004` / `MA0016` / `MA0002` / `MA0015`
|
- `9109eec`:`MediatorComprehensiveTests.cs`,清理 `MA0048` / `MA0004` / `MA0016` / `MA0002` / `MA0015`
|
||||||
- 主线程切片:`YamlConfigSchemaValidator.cs` 正则 timeout 与 ordinal equality,清理 `MA0009` / `MA0006`
|
- 主线程切片:`YamlConfigSchemaValidator.cs` 方法拆分,清理剩余 `MA0051`,并修正新增 helper 里的 `MA0006`
|
||||||
- Game 追加切片:
|
- Game 追加切片:
|
||||||
- `1395b84`:`YamlConfigSchemaValidator.ObjectKeywords.cs`,清理该文件 `MA0051`
|
- `1395b84`:`YamlConfigSchemaValidator.ObjectKeywords.cs`,清理该文件 `MA0051`
|
||||||
- 待提交:将 `YamlConfigSchemaValidator.cs` 末尾 schema model 类型拆到独立同名文件,清理 `MA0048`
|
- 已完成:将 `YamlConfigSchemaValidator.cs` 末尾 schema model 类型拆到独立同名文件,清理 `MA0048`
|
||||||
|
|
||||||
## 当前风险
|
## 当前风险
|
||||||
|
|
||||||
- `GFramework.Game/Config/YamlConfigSchemaValidator.cs` 仍有 `5` 个 `MA0051` 方法长度 warning,跨 `net8.0` / `net9.0` / `net10.0` 重复为 `15` 条。
|
- 当前仓库根 clean build warning 已清零,本主题暂时没有剩余源码 warning 风险。
|
||||||
- 缓解措施:下一轮只做主 validator 方法拆分,不再混入拆文件或正则安全修复。
|
- 缓解措施:若后续继续 batch warning 清理,先重新执行同轮 `dotnet clean` + `dotnet build` 采样,再决定是否需要分派 subagent。
|
||||||
- 标准 `dotnet clean` 在当前 WSL 环境仍会读取失效的 Windows fallback package folder。
|
|
||||||
- 缓解措施:本主题验证继续沿用 `-p:RestoreFallbackFolders=`,必要时先执行 solution restore 刷新 Linux 侧资产。
|
|
||||||
|
|
||||||
## 活跃文档
|
## 活跃文档
|
||||||
|
|
||||||
@ -68,13 +63,13 @@
|
|||||||
## 验证说明
|
## 验证说明
|
||||||
|
|
||||||
- 权威验证结果统一维护在“当前活跃事实”。
|
- 权威验证结果统一维护在“当前活跃事实”。
|
||||||
- `GFramework.Cqrs.Tests` 的当前受影响项目 Release 构建已清零,并通过 Mediator 定向测试回归。
|
- `GFramework.Game` 当前 Release 构建已清零,并通过 config 定向测试;本轮标准仓库根 Debug clean build 也已清零。
|
||||||
- `GFramework.Game` 当前 Release 构建已清零,并通过 config 定向测试;仓库 Debug 构建剩余 warning 属于主 validator 方法复杂度拆分。
|
- 本轮标准仓库根 `dotnet clean` + `dotnet build` 已直接回到 `0 Warning(s)`、`0 Error(s)`,因此 warning reduction 真值已从模块级验证收口到仓库级 clean build。
|
||||||
- `git diff --check` 结果为空,说明本轮新增改动没有引入新的尾随空格或冲突标记。
|
- `git diff --check` 结果为空,说明本轮新增改动没有引入新的尾随空格或冲突标记。
|
||||||
- warning reduction 的仓库级真值以同轮 `dotnet build`、定向 `dotnet test` 与 `git diff --check` 为准,并与 trace 中的验证里程碑保持一致。
|
- warning reduction 的仓库级真值以同轮 `dotnet build`、定向 `dotnet test` 与 `git diff --check` 为准,并与 trace 中的验证里程碑保持一致。
|
||||||
|
|
||||||
## 下一步建议
|
## 下一步建议
|
||||||
|
|
||||||
1. 提交 schema model 拆文件与本轮 `ai-plan` 收口。
|
1. 提交 `YamlConfigSchemaValidator` 收尾重构与本轮 `ai-plan` 同步。
|
||||||
2. 下一轮只处理 `GFramework.Game/Config/YamlConfigSchemaValidator.cs` 剩余 `MA0051` 方法拆分。
|
2. 如需继续 warning reduction,先从新的仓库根 clean build 重新采样是否还有新增 warning hotspot。
|
||||||
3. 保持 `RestoreFallbackFolders=` 验证口径,避免当前 WSL fallback package folder 干扰。
|
3. 若未来 warning 再次分散到多个文件,再按 `$gframework-batch-boot 50` 规则切换回多 worker 并行模式。
|
||||||
|
|||||||
@ -1,5 +1,40 @@
|
|||||||
# Analyzer Warning Reduction 追踪
|
# Analyzer Warning Reduction 追踪
|
||||||
|
|
||||||
|
## 2026-04-29 — RP-094
|
||||||
|
|
||||||
|
### 阶段:收尾 `YamlConfigSchemaValidator` 剩余 `MA0051` 并将仓库根 clean build 归零
|
||||||
|
|
||||||
|
- 触发背景:
|
||||||
|
- 用户要求先拿构建 warning,再在 warning 很多时分批指派 subagent;本轮按 `$gframework-batch-boot 50` 继续执行
|
||||||
|
- 基线与停机判断:
|
||||||
|
- 当前 `origin/main` 仍为 `0e32dab`(`2026-04-28T17:15:47+08:00`)
|
||||||
|
- 本轮标准仓库根 `dotnet clean` + `dotnet build` 直接成功;warning 总数为 `15`,但全部集中在 `GFramework.Game/Config/YamlConfigSchemaValidator.cs`
|
||||||
|
- 由于 `15` 条 warning 实际只对应同一文件内 `5` 个独立 `MA0051` 方法,不满足“warning 非常多且可安全分派多个独立写边界”的条件,因此不再新增 worker
|
||||||
|
- 主线程实施:
|
||||||
|
- 将 `ParseNode` 拆成 `ResolveNodeTypeName`、`ValidateObjectOnlyKeywords`、`CreateParsedNodeForType`
|
||||||
|
- 将 `ValidateObjectNode` 拆成对象类型确认、属性遍历与 required 校验 helper
|
||||||
|
- 将 `ValidateObjectConstraints` 拆成 property count、`dependentRequired`、`dependentSchemas`、`allOf`、条件分支五个 helper
|
||||||
|
- 将 `ValidateScalarNode` 与 `ValidateNumericScalarConstraints` 分别拆成标量类型确认、引用回写、数值上下界和 `multipleOf` helper
|
||||||
|
- 追加 `ValidateConditionalSchemaBranch` 收口 if/then/else 分支;随后修正该 helper 引入的 `MA0006`
|
||||||
|
- 验证里程碑:
|
||||||
|
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release -clp:Summary`
|
||||||
|
- 第一次结果:成功;`3` warnings、`0` errors(均为新 helper 中 `branchName == "then"` 引入的 `MA0006`)
|
||||||
|
- 第二次结果:成功;`0 Warning(s)`、`0 Error(s)`
|
||||||
|
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~YamlConfigLoaderTests|FullyQualifiedName~YamlConfigSchemaValidatorTests"`
|
||||||
|
- 结果:成功;`80` 通过、`0` 失败
|
||||||
|
- `dotnet clean`
|
||||||
|
- 结果:成功
|
||||||
|
- `dotnet build`
|
||||||
|
- 结果:成功;`0 Warning(s)`、`0 Error(s)`
|
||||||
|
- `git diff --check`
|
||||||
|
- 结果:成功;无新增 whitespace / conflict-marker 问题
|
||||||
|
- 当前指标:
|
||||||
|
- 仓库根 clean build warning:`15` -> `0`
|
||||||
|
- 当前分支相对 `origin/main...HEAD` 仍为 `22` 个变更文件,低于 `$gframework-batch-boot 50` 的文件阈值
|
||||||
|
- 当前停止原因:warning hotspot 已耗尽,不再有可重复切片
|
||||||
|
- 下一步:
|
||||||
|
- 提交 `YamlConfigSchemaValidator` 收尾重构与本轮 `ai-plan` 真值更新
|
||||||
|
|
||||||
## 2026-04-29 — RP-093
|
## 2026-04-29 — RP-093
|
||||||
|
|
||||||
### 阶段:按 `$gframework-batch-boot 50` 从 clean build warning 基线分批清理
|
### 阶段:按 `$gframework-batch-boot 50` 从 clean build warning 基线分批清理
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user