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