PureXML/Sources/Schema/SchemaStructure.swift
Mihaela Mihaljevic 6147666e49 feat(schema): structural validity against the schema-for-schemas content model
The general mechanism for the structural tail of the schema-validity gap, in
place of hand-coding a rule per component. A data-driven table encodes the XSD
1.0 schema-for-schemas child content model; at compile time it rejects any
component child the model does not admit, more than one annotation (or an
annotation that is not first, except on schema/redefine, which allow several),
and an identity constraint missing its selector or field.

The walk visits only XSD-namespace elements and skips appinfo/documentation
foreign content, so foreign children are never flagged; restriction/extension
use the permissive union of their simple/complex/simpleContent contexts, so a
child valid in some context is never rejected. One table covers every component.

XSTS: invalid schemas accepted 1688 to 1313 (-375); valid schemas rejected held
at 72 (no over-rejection); instance buckets unchanged.
2026-06-13 22:48:20 +02:00

94 lines
5.5 KiB
Swift

extension PureXML.Schema.XSDParser {
/// The XML Schema namespace; only elements in it are schema vocabulary, so
/// foreign elements (and annotation content) are not structurally checked.
private static let xsdNamespace = "http://www.w3.org/2001/XMLSchema"
/// The schema-for-schemas child content model (XSD 1.0 Structures): the
/// XSD-namespace element local names each component admits as children. The
/// `restriction`/`extension` entries are the permissive union across their
/// simple/complex/simpleContent contexts, so this never rejects a child that
/// is valid in some context; order beyond the leading annotation is not
/// modelled here. Leaf components and facets admit only `annotation`.
private static let allowedChildren: [String: Set<String>] = {
let facets: Set = [
"minExclusive", "minInclusive", "maxExclusive", "maxInclusive", "totalDigits",
"fractionDigits", "length", "minLength", "maxLength", "enumeration", "whiteSpace", "pattern",
]
var table: [String: Set<String>] = [
"schema": ["include", "import", "redefine", "annotation", "simpleType", "complexType", "group", "attributeGroup", "element", "attribute", "notation"],
"complexType": ["annotation", "simpleContent", "complexContent", "group", "all", "choice", "sequence", "attribute", "attributeGroup", "anyAttribute"],
"simpleContent": ["annotation", "restriction", "extension"],
"complexContent": ["annotation", "restriction", "extension"],
"simpleType": ["annotation", "restriction", "list", "union"],
"element": ["annotation", "simpleType", "complexType", "unique", "key", "keyref"],
"attribute": ["annotation", "simpleType"],
"attributeGroup": ["annotation", "attribute", "attributeGroup", "anyAttribute"],
"group": ["annotation", "all", "choice", "sequence"],
"all": ["annotation", "element"],
"choice": ["annotation", "element", "group", "choice", "sequence", "any"],
"sequence": ["annotation", "element", "group", "choice", "sequence", "any"],
"restriction": Set(["annotation", "simpleType", "group", "all", "choice", "sequence", "attribute", "attributeGroup", "anyAttribute"]).union(facets),
"extension": ["annotation", "group", "all", "choice", "sequence", "attribute", "attributeGroup", "anyAttribute"],
"unique": ["annotation", "selector", "field"],
"key": ["annotation", "selector", "field"],
"keyref": ["annotation", "selector", "field"],
"annotation": ["appinfo", "documentation"],
"list": ["annotation", "simpleType"],
"union": ["annotation", "simpleType"],
"redefine": ["annotation", "simpleType", "complexType", "group", "attributeGroup"],
]
for leaf in ["selector", "field", "any", "anyAttribute", "notation", "import", "include"] + Array(facets) {
table[leaf] = ["annotation"]
}
return table
}()
/// Components whose annotation may be repeated and interspersed (XSD 1.0:
/// `schema` and `redefine`); everywhere else `annotation` is `annotation?`
/// and must be the first child.
private static let multipleAnnotation: Set<String> = ["schema", "redefine"]
/// Schema-validity findings for the structural content of a schema document:
/// each XSD-namespace element's XSD-namespace children must be admitted by the
/// schema-for-schemas content model, the single `annotation` (where allowed
/// once) must be first, and an identity constraint needs a selector and field.
/// Walks the one document rooted at `schema`, skipping foreign content.
static func structureErrors(_ schema: XSDTree) -> [String] {
var errors: [String] = []
collectStructure(schema, into: &errors)
return errors
}
private static func collectStructure(_ node: XSDTree, into errors: inout [String]) {
let local = PureXML.Schema.XSDNode.localName(node)
if local == "appinfo" || local == "documentation" { return }
let children = PureXML.Schema.XSDNode.elementChildren(node)
if node.name?.namespaceURI == xsdNamespace, let local, let allowed = allowedChildren[local] {
let names = children.filter { $0.name?.namespaceURI == xsdNamespace }.compactMap(PureXML.Schema.XSDNode.localName)
errors += childErrors(local: local, children: names, allowed: allowed)
}
for child in children {
collectStructure(child, into: &errors)
}
}
private static func childErrors(local: String, children: [String], allowed: Set<String>) -> [String] {
var errors: [String] = []
for name in children where !allowed.contains(name) {
errors.append("element '\(name)' is not allowed in '\(local)'")
}
if !multipleAnnotation.contains(local) {
let annotations = children.count(where: { $0 == "annotation" })
if annotations > 1 {
errors.append("'\(local)' may have at most one annotation")
} else if annotations == 1, children.first != "annotation" {
errors.append("the annotation in '\(local)' must be the first child")
}
}
if ["unique", "key", "keyref"].contains(local), !(children.contains("selector") && children.contains("field")) {
errors.append("identity constraint '\(local)' requires a selector and at least one field")
}
return errors
}
}