PureXML/Sources/Schema/XSDAttributeParser.swift
Mihaela Mihaljevic 380160cc2b refactor(validation): decompose XSD instance validation into composable rules
The two monolithic XSD content/identity rules only satisfied the OpenAPIKit
validation idiom at the wrong granularity: no per-constraint removability,
no isolable failing/near-miss tests. Split them into 32 named rules, each
finding tagged at its origin and cached per XSDContext so the walk still
runs once per document (the #101 decision against duplicating XSD's
recursive type resolution still holds). Wired the streaming path's ID/IDREF
and identity checks through the same framework instead of calling the
validators imperatively, and added the unwrapAndLookup combinator the rule
already named as required.

Also includes the mandated repo-wide swiftformat/swiftlint pass.
2026-07-12 14:12:17 +02:00

165 lines
8.9 KiB
Swift

extension PureXML.Schema.XSDParser {
/// The attribute uses declared directly under `node` and through any
/// `attributeGroup` references it nests, flattened. The `visited` set guards
/// against attribute-group reference cycles.
static func attributeUses(under node: XSDTree, _ context: PureXML.Schema.XSDContext, visited: Set<String> = []) -> [PureXML.Schema.AttributeUse] {
// An `<xs:attributeGroup ref>` chain is flat in the document, so it escapes the
// parser's element-nesting `maxDepth`; flattening it recurses once per link, which
// overflows the stack on a long chain and is super-linear before that. `visited`
// counts the chain depth, so bound it the same way the content-model expansion is
// bounded, rejecting a pathological chain with a located error rather than crashing.
guard visited.count <= maximumContentModelExpansionDepth else {
context.diagnostics.report(
"attribute group reference nesting exceeds the maximum depth of \(maximumContentModelExpansionDepth)",
at: node,
)
return []
}
var uses: [PureXML.Schema.AttributeUse] = []
for child in PureXML.Schema.XSDNode.elementChildren(node) {
switch PureXML.Schema.XSDNode.localName(child) {
case "attribute":
if let use = attributeUse(child, context) {
uses.append(use)
}
case "attributeGroup":
guard let ref = PureXML.Schema.XSDNode.attribute(child, "ref") else { break }
let name = PureXML.Schema.XSDNode.stripPrefix(ref)
if visited.contains(name) {
if context.redefinedAttributeGroups.contains(name), let base = context.baseAttributeGroups[name] {
let scoped = context.scoped(for: PureXML.Schema.XSDNode.schemaOwner(base))
uses += attributeUses(under: base, scoped, visited: visited)
}
break
}
guard let group = context.attributeGroups[name] else { break }
let scoped = context.scoped(for: PureXML.Schema.XSDNode.schemaOwner(group))
uses += attributeUses(under: group, scoped, visited: visited.union([name]))
default:
break
}
}
return uses
}
/// Prefix bindings in effect on `node`: the enclosing schema's `xmlns` declarations
/// merged along the ancestor path (used for `ref` QNames inside included documents).
static func namespaceBindingsInScope(of node: XSDTree, defaultBindings: [String: String]) -> [String: String] {
var path: [XSDTree] = []
var current: XSDTree? = node
while let currentNode = current {
path.append(currentNode)
current = currentNode.parent
}
path.reverse()
let schemaRoot = path.first { PureXML.Schema.XSDNode.localName($0) == "schema" }
var bindings = schemaRoot.map { PureXML.Schema.XSDNode.namespaceBindings(of: $0) } ?? defaultBindings
for ancestor in path {
bindings = mergedNamespaceBindings(on: ancestor, inherited: bindings)
}
return bindings
}
static func attributeUse(_ node: XSDTree, _ context: PureXML.Schema.XSDContext) -> PureXML.Schema.AttributeUse? {
if let ref = PureXML.Schema.XSDNode.attribute(node, "ref") {
return referencedAttributeUse(node, ref: ref, context)
}
guard let name = PureXML.Schema.XSDNode.attribute(node, "name") else { return nil }
let use = PureXML.Schema.XSDNode.attribute(node, "use")
let required = use == "required"
let type: PureXML.Schema.SimpleType = if let typeName = PureXML.Schema.XSDNode.attribute(node, "type") {
PureXML.Schema.XSDSimpleParser.simpleTypeReference(typeName, context)
} else if let inline = PureXML.Schema.XSDNode.firstChild(node, named: "simpleType") {
PureXML.Schema.XSDSimpleParser.simpleType(inline, context)
} else {
PureXML.Schema.SimpleType(base: .string, isAnySimpleType: true)
}
let attributeFormQualified = attributeFormQualified(for: node, default: context.attributeFormQualified)
let qualified = PureXML.Schema.XSDNode.attribute(node, "form") == "qualified"
|| (PureXML.Schema.XSDNode.attribute(node, "form") == nil && attributeFormQualified)
let targetNamespace = owningTargetNamespace(of: node, fallback: context.targetNamespace, context)
let owner = PureXML.Schema.XSDNode.schemaOwner(node)
return PureXML.Schema.AttributeUse(
name: PureXML.Model.QualifiedName(localName: name, namespaceURI: qualified ? targetNamespace : nil),
type: type,
required: required,
valueConstraint: valueConstraint(of: node),
chameleonUnprefixed: qualified && isChameleonSchema(owner),
prohibited: use == "prohibited",
)
}
/// An `<attribute ref="...">` use: it takes its name (always target-namespace
/// qualified) and type from the referenced global declaration, but its
/// `use`/`default`/`fixed` from this reference site.
private static func referencedAttributeUse(_ node: XSDTree, ref: String, _ context: PureXML.Schema.XSDContext) -> PureXML.Schema.AttributeUse? {
let refName = PureXML.Schema.XSDNode.stripPrefix(ref)
let bindings = namespaceBindingsInScope(of: node, defaultBindings: context.namespaceBindings)
let namespace = PureXML.Schema.XSDNode.referenceNamespace(ref, bindings)
if namespace == "http://www.w3.org/XML/1998/namespace", refName == "base" {
return PureXML.Schema.AttributeUse(
name: PureXML.Model.QualifiedName(localName: refName, namespaceURI: namespace),
type: PureXML.Schema.SimpleType(base: .anyURI),
required: PureXML.Schema.XSDNode.attribute(node, "use") == "required",
valueConstraint: valueConstraint(of: node),
)
}
guard let declaration = context.globalAttributes[refName],
var use = attributeUse(declaration, context.scoped(for: PureXML.Schema.XSDNode.schemaOwner(declaration)))
else { return nil }
let declContext = context.scoped(for: PureXML.Schema.XSDNode.schemaOwner(declaration))
if let target = declContext.targetNamespace, !target.isEmpty {
use.name = PureXML.Model.QualifiedName(localName: refName, namespaceURI: target)
use.chameleonUnprefixed = isChameleonSchema(PureXML.Schema.XSDNode.schemaOwner(declaration))
} else {
use.name = PureXML.Model.QualifiedName(localName: refName, namespaceURI: namespace)
}
if PureXML.Schema.XSDNode.attribute(node, "use") == "required" {
use.required = true
}
if PureXML.Schema.XSDNode.attribute(node, "use") == "prohibited" {
use.prohibited = true
}
if let constraint = valueConstraint(of: node) {
use.valueConstraint = constraint
}
return use
}
/// A schema document with no explicit `targetNamespace` (a chameleon include).
private static func isChameleonSchema(_ schema: XSDTree) -> Bool {
guard PureXML.Schema.XSDNode.localName(schema) == "schema" else { return false }
guard let target = PureXML.Schema.XSDNode.attribute(schema, "targetNamespace") else { return true }
return target.isEmpty
}
/// Whether attributes declared under `node` default to qualified form in their
/// owning schema document, not the including schema's `attributeFormDefault`.
private static func attributeFormQualified(for node: XSDTree, default defaultQualified: Bool) -> Bool {
var current: XSDTree? = node
while let currentNode = current {
if PureXML.Schema.XSDNode.localName(currentNode) == "schema" {
return PureXML.Schema.XSDNode.attribute(currentNode, "attributeFormDefault") == "qualified"
}
current = currentNode.parent
}
return defaultQualified
}
/// The `targetNamespace` of the schema document that declares `node`, including
/// chameleon includes that inherit the including schema's namespace.
private static func owningTargetNamespace(of node: XSDTree, fallback: String?, _ context: PureXML.Schema.XSDContext) -> String? {
var current: XSDTree? = node
while let currentNode = current {
if PureXML.Schema.XSDNode.localName(currentNode) == "schema" {
if let target = PureXML.Schema.XSDNode.attribute(currentNode, "targetNamespace"), !target.isEmpty {
return target
}
return context.chameleonTargetNamespaces[ObjectIdentifier(currentNode)] ?? fallback
}
current = currentNode.parent
}
return fallback
}
}