From 7da985947cbeec40b1812435f4a2ff6932936451 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 29 Apr 2026 08:55:03 +0800 Subject: [PATCH] =?UTF-8?q?fix(game):=20=E6=B8=85=E7=90=86=E5=89=A9?= =?UTF-8?q?=E4=BD=99=E9=85=8D=E7=BD=AE=20schema=20warning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构 YamlConfigSchemaValidator 的长方法为语义化 helper,清理剩余 MA0051 warning - 修复 条件分支 helper 的字符串比较方式,避免新增 MA0006 warning - 更新 analyzer warning reduction 跟踪与 trace,记录仓库根 clean build 已归零 --- .../Config/YamlConfigSchemaValidator.cs | 805 +++++++++++++----- .../analyzer-warning-reduction-tracking.md | 51 +- .../analyzer-warning-reduction-trace.md | 35 + 3 files changed, 646 insertions(+), 245 deletions(-) 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 基线分批清理