fix(game): 清理剩余配置 schema warning

- 重构 YamlConfigSchemaValidator 的长方法为语义化 helper,清理剩余 MA0051 warning
- 修复 条件分支 helper 的字符串比较方式,避免新增 MA0006 warning
- 更新 analyzer warning reduction 跟踪与 trace,记录仓库根 clean build 已归零
This commit is contained in:
gewuyou 2026-04-29 08:55:03 +08:00
parent 104ac25dc3
commit 7da985947c
3 changed files with 646 additions and 245 deletions

View File

@ -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));
}
/// <summary>
/// 解析 schema 节点声明的类型名称,并在缺失或类型错误时立刻给出定位清晰的诊断。
/// </summary>
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;
}
/// <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(
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));
}
/// <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(
tableName,
@ -391,7 +441,6 @@ internal static partial class YamlConfigSchemaValidator
displayPath: GetDiagnosticPath(propertyPath),
rawValue: typeName)
};
return parsedNode.WithNegatedSchemaNode(ParseNegatedSchemaNode(tableName, schemaPath, propertyPath, element));
}
/// <summary>
@ -727,18 +776,68 @@ internal static partial class YamlConfigSchemaValidator
ICollection<YamlConfigReferenceUsage>? 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);
}
/// <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}'";
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));
}
/// <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);
foreach (var entry in mappingNode.Children)
{
@ -795,6 +894,19 @@ internal static partial class YamlConfigSchemaValidator
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)
{
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);
}
/// <summary>
@ -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);
}
/// <summary>
/// 为对象约束构造统一的诊断主语,保证根对象与嵌套对象的错误消息格式一致。
/// </summary>
private static string GetObjectConstraintSubject(string displayPath)
{
return string.IsNullOrWhiteSpace(displayPath)
? "Root object"
: $"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 &&
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)
/// <summary>
/// 使用已见 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
// 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)
/// <summary>
/// 在触发字段出现时,以 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++)
{
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.");
}
}
/// <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;
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.");
}
/// <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,
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);
}
/// <summary>
@ -1154,29 +1402,81 @@ internal static partial class YamlConfigSchemaValidator
YamlConfigSchemaNode schemaNode,
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(
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);
}
/// <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(
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));
}
/// <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 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)
/// <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);
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));
}
/// <summary>
@ -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);
}
/// <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)
{
throw ConfigLoadExceptionFactory.Create(
@ -2418,7 +2760,20 @@ internal static partial class YamlConfigSchemaValidator
detail:
$"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)
{
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))
/// <summary>
/// 校验数值是否满足 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(
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)}.");
}
/// <summary>

View File

@ -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` errorswarning 从本轮基线 `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 并行模式

View File

@ -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 基线分批清理