From 76ef2d5b83858c6b1e1fbfc201bdd2067be8fd52 Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Sun, 21 Jun 2026 14:14:48 +0200 Subject: [PATCH] feat(schema): locate value-constraint type errors Convert the three value-constraint checkers (built-in, named user type, and inline type) to located findings so a default/fixed value that is invalid for its declared type, or an illegal combination of value constraints, is attached to its declaring element or attribute instead of the schema root. An editor can underline the offending declaration by line and column. Error predicates and reason strings are unchanged; only the location each finding carries differs. Adds located-diagnostics tests for the built-in, user-type, and inline-type paths. --- Sources/Schema/SchemaCompileValidation.swift | 6 +-- Sources/Schema/SchemaFacetApplicability.swift | 47 +++++++++++----- Sources/Schema/SchemaStructure.swift | 2 +- Tests/SchemaLocatedDiagnosticsTests.swift | 54 +++++++++++++++++++ 4 files changed, 90 insertions(+), 19 deletions(-) diff --git a/Sources/Schema/SchemaCompileValidation.swift b/Sources/Schema/SchemaCompileValidation.swift index 5dca035..985d310 100644 --- a/Sources/Schema/SchemaCompileValidation.swift +++ b/Sources/Schema/SchemaCompileValidation.swift @@ -188,10 +188,8 @@ extension PureXML.Validation { static var userTypeValueConstraintsValid: Validation { compileRule("Element value constraints are valid against their declared types") { document in guard let namedTypes = document.namedTypes else { return [] } - return PureXML.Schema.SchemaLocatedFinding.unlocated( - PureXML.Schema.XSDParser.userTypeValueConstraintErrors(document.schema, document.context, namedTypes) - + PureXML.Schema.XSDParser.inlineTypeValueConstraintErrors(document.schema, document.context), - ) + return PureXML.Schema.XSDParser.userTypeValueConstraintFindings(document.schema, document.context, namedTypes) + + PureXML.Schema.XSDParser.inlineTypeValueConstraintFindings(document.schema, document.context) } } diff --git a/Sources/Schema/SchemaFacetApplicability.swift b/Sources/Schema/SchemaFacetApplicability.swift index 26647c2..25cff9e 100644 --- a/Sources/Schema/SchemaFacetApplicability.swift +++ b/Sources/Schema/SchemaFacetApplicability.swift @@ -37,19 +37,25 @@ extension PureXML.Schema.XSDParser { /// `fixed="abc"` on an `xs:integer` was wrongly accepted. A named or inline user /// type, or a type in another namespace, is left alone, so no valid value /// constraint is rejected. - static func valueConstraintErrors(_ schema: XSDTree) -> [String] { + static func valueConstraintFindings(_ schema: XSDTree) -> [PureXML.Schema.SchemaLocatedFinding] { let bindings = namespaceBindings(schema) - var errors: [String] = [] + var findings: [PureXML.Schema.SchemaLocatedFinding] = [] forEachValueConstrained(schema) { node in // The value constraint attributes are the unprefixed, no-namespace // `default`/`fixed` on an XSD-namespace declaration; a foreign same-local // attribute is not the value constraint and is not structurally checked. if hasBothValueConstraints(node) { - errors.append("a '\(PureXML.Schema.XSDNode.localName(node) ?? "")' declaration may not have both a 'default' and a 'fixed' value constraint") + findings.append(PureXML.Schema.SchemaLocatedFinding( + reason: "a '\(PureXML.Schema.XSDNode.localName(node) ?? "")' declaration may not have both a 'default' and a 'fixed' value constraint", + node: node, + )) return } if hasDefaultWithNonOptionalUse(node) { - errors.append("an 'attribute' with a 'default' value constraint must have use='optional'") + findings.append(PureXML.Schema.SchemaLocatedFinding( + reason: "an 'attribute' with a 'default' value constraint must have use='optional'", + node: node, + )) return } guard let typeName = PureXML.Schema.XSDNode.attribute(node, "type") else { return } @@ -60,9 +66,12 @@ extension PureXML.Schema.XSDParser { guard uri == xsdNamespace, let builtin = PureXML.Schema.BuiltinType(rawValue: local), !PureXML.Schema.SimpleType(base: builtin).isValid(value) else { return } - errors.append("the \(fixed != nil ? "fixed" : "default") value '\(value)' is not valid for type '\(typeName)'") + findings.append(PureXML.Schema.SchemaLocatedFinding( + reason: "the \(fixed != nil ? "fixed" : "default") value '\(value)' is not valid for type '\(typeName)'", + node: node, + )) } - return errors + return findings } /// A `default`/`fixed` value on an `element` or `attribute` whose `type` names a @@ -79,9 +88,13 @@ extension PureXML.Schema.XSDParser { /// inline anonymous type is left alone, so no valid value constraint is rejected. /// The compiled `SimpleType` validator is the same one instance validation uses, /// so its whitespace and facet handling match. - static func userTypeValueConstraintErrors(_ schema: XSDTree, _ context: PureXML.Schema.XSDContext, _ types: [String: PureXML.Schema.ElementType]) -> [String] { + static func userTypeValueConstraintFindings( + _ schema: XSDTree, + _ context: PureXML.Schema.XSDContext, + _ types: [String: PureXML.Schema.ElementType], + ) -> [PureXML.Schema.SchemaLocatedFinding] { let bindings = context.namespaceBindings - var errors: [String] = [] + var findings: [PureXML.Schema.SchemaLocatedFinding] = [] forEachValueConstrained(schema) { node in guard node.name?.namespaceURI == xsdNamespace, let typeName = unprefixedValue(node, "type") @@ -93,9 +106,12 @@ extension PureXML.Schema.XSDParser { guard uri == context.targetNamespace, let resolved = types[local], let simple = simpleContentType(of: resolved), !simple.isValid(value) else { return } - errors.append("the \(fixed != nil ? "fixed" : "default") value '\(value)' is not valid for type '\(typeName)'") + findings.append(PureXML.Schema.SchemaLocatedFinding( + reason: "the \(fixed != nil ? "fixed" : "default") value '\(value)' is not valid for type '\(typeName)'", + node: node, + )) } - return errors + return findings } /// Like ``userTypeValueConstraintErrors`` but for an element or attribute whose @@ -104,8 +120,8 @@ extension PureXML.Schema.XSDParser { /// against that type's value space. Validated against the simpleContent base's /// value space (not the local restriction facets), so only a clear value-space /// mismatch (a non-numeric `default` on a decimal-based type) is flagged. - static func inlineTypeValueConstraintErrors(_ schema: XSDTree, _ context: PureXML.Schema.XSDContext) -> [String] { - var errors: [String] = [] + static func inlineTypeValueConstraintFindings(_ schema: XSDTree, _ context: PureXML.Schema.XSDContext) -> [PureXML.Schema.SchemaLocatedFinding] { + var findings: [PureXML.Schema.SchemaLocatedFinding] = [] forEachValueConstrained(schema) { node in guard node.name?.namespaceURI == xsdNamespace, unprefixedValue(node, "type") == nil @@ -114,9 +130,12 @@ extension PureXML.Schema.XSDParser { guard let value = fixed ?? unprefixedValue(node, "default"), let simple = inlineSimpleType(of: node, context), !simple.isValid(value) else { return } - errors.append("the \(fixed != nil ? "fixed" : "default") value '\(value)' is not valid for the declared inline type") + findings.append(PureXML.Schema.SchemaLocatedFinding( + reason: "the \(fixed != nil ? "fixed" : "default") value '\(value)' is not valid for the declared inline type", + node: node, + )) } - return errors + return findings } /// The simple value space of an element/attribute's INLINE type: an inline diff --git a/Sources/Schema/SchemaStructure.swift b/Sources/Schema/SchemaStructure.swift index 32a7d09..f1be940 100644 --- a/Sources/Schema/SchemaStructure.swift +++ b/Sources/Schema/SchemaStructure.swift @@ -62,7 +62,7 @@ extension PureXML.Schema.XSDParser { let bindings = PureXML.Schema.XSDNode.namespaceBindings(of: schema) collectStructure(schema, bindings: bindings, into: &findings) append(simpleTypeVarietyFacetErrors(schema), at: schema, into: &findings) - append(valueConstraintErrors(schema), at: schema, into: &findings) + findings += valueConstraintFindings(schema) append(topLevelDeclarationErrors(schema), at: schema, into: &findings) append(nestedNamedDefinitionErrors(schema), at: schema, into: &findings) append(anySimpleTypeRestrictionErrors(schema) + anySimpleTypeFacetErrors(schema), at: schema, into: &findings) diff --git a/Tests/SchemaLocatedDiagnosticsTests.swift b/Tests/SchemaLocatedDiagnosticsTests.swift index 4246df9..0bb0e07 100644 --- a/Tests/SchemaLocatedDiagnosticsTests.swift +++ b/Tests/SchemaLocatedDiagnosticsTests.swift @@ -72,6 +72,60 @@ struct SchemaLocatedDiagnosticsTests { #expect(range?.start.column == 3) } + @Test("A bad value constraint against a named user type is located on its declaring element") + func test_userTypeValueConstraintLocated() throws { + let xsd = """ + + + + + """ + let findings = try inconsistentFindings(in: xsd) + let located = try #require(findings.first { $0.reason.contains("is not valid for type 't:count'") }) + #expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:element"]) + + let (tree, _) = PureXML.readTree(xsd) + let range = tree.sourceRange(at: located.codingPath) + #expect(range?.start.line == 3) + #expect(range?.start.column == 3) + } + + @Test("A bad value constraint against an inline type is located on its declaring element") + func test_inlineTypeValueConstraintLocated() throws { + let xsd = """ + + + + + + """ + let findings = try inconsistentFindings(in: xsd) + let located = try #require(findings.first { $0.reason.contains("is not valid for the declared inline type") }) + #expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:element"]) + + let (tree, _) = PureXML.readTree(xsd) + let range = tree.sourceRange(at: located.codingPath) + #expect(range?.start.line == 2) + #expect(range?.start.column == 3) + } + + @Test("A bad value constraint against a built-in type is located on its declaring element") + func test_builtinValueConstraintLocated() throws { + let xsd = """ + + + + """ + let findings = try inconsistentFindings(in: xsd) + let located = try #require(findings.first { $0.reason.contains("is not valid for type 'xs:integer'") }) + #expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:element"]) + + let (tree, _) = PureXML.readTree(xsd) + let range = tree.sourceRange(at: located.codingPath) + #expect(range?.start.line == 2) + #expect(range?.start.column == 3) + } + @Test("An xs:include targetNamespace mismatch is located on the include directive") func test_includeMismatchLocated() throws { let included = """