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.
This commit is contained in:
Mihaela Mihaljevic 2026-06-21 14:14:48 +02:00
parent f5059de42e
commit 76ef2d5b83
4 changed files with 90 additions and 19 deletions

View File

@ -188,10 +188,8 @@ extension PureXML.Validation {
static var userTypeValueConstraintsValid: Validation<PureXML.Schema.SchemaCompileRoot, PureXML.Schema.SchemaCompileContext> {
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)
}
}

View File

@ -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

View File

@ -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)

View File

@ -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 = """
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="urn:t" xmlns:t="urn:t">
<xs:simpleType name="count"><xs:restriction base="xs:integer"/></xs:simpleType>
<xs:element name="a" type="t:count" default="notanumber"/>
</xs:schema>
"""
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 = """
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="a" default="x">
<xs:simpleType><xs:restriction base="xs:integer"/></xs:simpleType>
</xs:element>
</xs:schema>
"""
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 = """
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="a" type="xs:integer" default="notanumber"/>
</xs:schema>
"""
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 = """