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.
384 lines
18 KiB
Swift
384 lines
18 KiB
Swift
extension PureXML.Schema.XSDParser {
|
|
/// Schema-validity findings for component-name uniqueness. Within a schema,
|
|
/// global type names (simpleType and complexType share one symbol space),
|
|
/// global element names, global attribute names, named model-group names,
|
|
/// named attribute-group names, and notation names must each be unique;
|
|
/// identity-constraint names
|
|
/// (unique/key/keyref) must be unique across the whole schema. A duplicate was
|
|
/// accepted, with a later definition silently overwriting the earlier one.
|
|
///
|
|
/// Only the document's own globals are examined (`xs:redefine` children and
|
|
/// included documents are not direct globals here), so a redefinition is not
|
|
/// mistaken for a clash.
|
|
static func componentNameFindings(_ schema: XSDTree, _ containers: [XSDTree], _ context: PureXML.Schema.XSDContext) -> [PureXML.Schema.SchemaLocatedFinding] {
|
|
let namespaceMap = resolveContainerNamespaces(containers, mainTargetNamespace: context.targetNamespace)
|
|
let uniqueIndices = findUniqueIndices(
|
|
in: containers,
|
|
containerLocations: context.containerLocations,
|
|
namespaceMap: namespaceMap,
|
|
mainTargetNamespace: context.targetNamespace,
|
|
)
|
|
|
|
var findings = checkDuplicateGlobalFindings(
|
|
in: containers,
|
|
indices: uniqueIndices,
|
|
namespaceMap: namespaceMap,
|
|
mainTargetNamespace: context.targetNamespace,
|
|
)
|
|
findings += identityConstraintNameFindings(
|
|
containers,
|
|
uniqueIndices,
|
|
namespaceMap: namespaceMap,
|
|
mainTargetNamespace: context.targetNamespace,
|
|
)
|
|
findings += keyrefReferFindings(schema, containers, context)
|
|
return findings
|
|
}
|
|
|
|
private static func findUniqueIndices(
|
|
in containers: [XSDTree],
|
|
containerLocations: [ObjectIdentifier: String?],
|
|
namespaceMap: [Int: String?],
|
|
mainTargetNamespace: String?,
|
|
) -> [Int] {
|
|
var uniqueIndices: [Int] = []
|
|
for index in containers.indices {
|
|
let container = containers[index]
|
|
let namespaceURI = namespaceMap[index] ?? mainTargetNamespace
|
|
let location = containerLocations[ObjectIdentifier(container)] ?? nil
|
|
let alreadyExists = uniqueIndices.contains { prevIndex in
|
|
let prevNS = namespaceMap[prevIndex] ?? mainTargetNamespace
|
|
let prevLocation = containerLocations[ObjectIdentifier(containers[prevIndex])] ?? nil
|
|
guard prevNS == namespaceURI,
|
|
isStructurallyEqual(containers[prevIndex], container)
|
|
else {
|
|
return false
|
|
}
|
|
if prevLocation == location {
|
|
return true
|
|
}
|
|
// A circular import reloads the root schema under a schemaLocation while
|
|
// the compile root is appended last; treat that as one document, not a clash.
|
|
let rootIndex = containers.count - 1
|
|
return index == rootIndex || prevIndex == rootIndex
|
|
}
|
|
if !alreadyExists {
|
|
uniqueIndices.append(index)
|
|
}
|
|
}
|
|
return uniqueIndices
|
|
}
|
|
|
|
private struct ComponentKey: Hashable {
|
|
let kind: String
|
|
let targetNamespace: String?
|
|
let name: String
|
|
}
|
|
|
|
private static func checkDuplicateGlobalFindings(
|
|
in containers: [XSDTree],
|
|
indices: [Int],
|
|
namespaceMap: [Int: String?],
|
|
mainTargetNamespace: String?,
|
|
) -> [PureXML.Schema.SchemaLocatedFinding] {
|
|
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
|
var seenComponents: Set<ComponentKey> = []
|
|
for index in indices {
|
|
let container = containers[index]
|
|
guard PureXML.Schema.XSDNode.localName(container) != "redefine" else { continue }
|
|
let namespaceURI = namespaceMap[index] ?? mainTargetNamespace
|
|
let globals = PureXML.Schema.XSDNode.elementChildren(container).filter {
|
|
$0.name?.namespaceURI == xsdNamespace
|
|
}
|
|
for global in globals {
|
|
let localName = PureXML.Schema.XSDNode.localName(global) ?? ""
|
|
guard let kind = componentKind(for: localName),
|
|
let name = PureXML.Schema.XSDNode.attribute(global, "name")?.trimmingXMLWhitespace()
|
|
else {
|
|
continue
|
|
}
|
|
let key = ComponentKey(kind: kind, targetNamespace: namespaceURI, name: name)
|
|
if !seenComponents.insert(key).inserted {
|
|
let nsStr = namespaceURI.map { " in namespace '\($0)'" } ?? " in no namespace"
|
|
let kindLabel = kind == "group" ? "model group" : (kind == "attributeGroup" ? "attribute group" : kind)
|
|
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
|
reason: "duplicate \(kindLabel) name '\(name)'\(nsStr)",
|
|
node: global,
|
|
))
|
|
}
|
|
}
|
|
}
|
|
return findings
|
|
}
|
|
|
|
private static func componentKind(for localName: String) -> String? {
|
|
switch localName {
|
|
case "simpleType", "complexType": "type"
|
|
case "element": "element"
|
|
case "attribute": "attribute"
|
|
case "group": "group"
|
|
case "attributeGroup": "attributeGroup"
|
|
case "notation": "notation"
|
|
default: nil
|
|
}
|
|
}
|
|
|
|
private struct KeyrefInfo {
|
|
let name: String
|
|
let refer: String
|
|
let arity: Int
|
|
let node: XSDTree
|
|
}
|
|
|
|
/// Findings for a `keyref` whose `refer` does not name a `key` or `unique` in
|
|
/// the document, or whose field arity does not match the referenced key/unique's.
|
|
/// Skipped when the document pulls in external definitions
|
|
/// (`import`/`include`/`redefine`), which the default compile does not load,
|
|
/// so a `refer` into them is never flagged.
|
|
private static func keyrefReferFindings(_ schema: XSDTree, _ containers: [XSDTree], _ context: PureXML.Schema.XSDContext) -> [PureXML.Schema.SchemaLocatedFinding] {
|
|
if skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) {
|
|
return []
|
|
}
|
|
var keyArities: [String: Int] = [:]
|
|
var keyrefs: [KeyrefInfo] = []
|
|
let sources = context.compositionLoaded ? containers : [schema]
|
|
for source in sources where PureXML.Schema.XSDNode.localName(source) != "redefine" {
|
|
collectKeysAndRefers(source, keyArities: &keyArities, keyrefs: &keyrefs)
|
|
}
|
|
|
|
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
|
for keyref in keyrefs {
|
|
if let keyArity = keyArities[keyref.refer] {
|
|
if keyArity != keyref.arity {
|
|
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
|
reason: "keyref '\(keyref.name)' and its referenced key/unique '\(keyref.refer)' must have the same number of fields",
|
|
node: keyref.node,
|
|
))
|
|
}
|
|
} else {
|
|
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
|
reason: "keyref refers to undeclared key or unique '\(keyref.refer)'",
|
|
node: keyref.node,
|
|
))
|
|
}
|
|
}
|
|
return findings
|
|
}
|
|
|
|
private static func collectKeysAndRefers(
|
|
_ node: XSDTree,
|
|
keyArities: inout [String: Int],
|
|
keyrefs: inout [KeyrefInfo],
|
|
) {
|
|
if let local = PureXML.Schema.XSDNode.localName(node), local == "appinfo" || local == "documentation" {
|
|
return
|
|
}
|
|
if node.name?.namespaceURI == xsdNamespace, let local = PureXML.Schema.XSDNode.localName(node) {
|
|
if local == "key" || local == "unique", let name = PureXML.Schema.XSDNode.attribute(node, "name")?.trimmingXMLWhitespace() {
|
|
let arity = PureXML.Schema.XSDNode.elementChildren(node)
|
|
.count(where: { $0.name?.namespaceURI == xsdNamespace && PureXML.Schema.XSDNode.localName($0) == "field" })
|
|
|
|
keyArities[name] = arity
|
|
}
|
|
if local == "keyref" {
|
|
let name = PureXML.Schema.XSDNode.attribute(node, "name")?.trimmingXMLWhitespace()
|
|
let refer = PureXML.Schema.XSDNode.attribute(node, "refer")?.trimmingXMLWhitespace()
|
|
if let name, let refer {
|
|
let arity = PureXML.Schema.XSDNode.elementChildren(node)
|
|
.count(where: { $0.name?.namespaceURI == xsdNamespace && PureXML.Schema.XSDNode.localName($0) == "field" })
|
|
keyrefs.append(KeyrefInfo(name: name, refer: PureXML.Schema.XSDNode.stripPrefix(refer), arity: arity, node: node))
|
|
}
|
|
}
|
|
}
|
|
for child in PureXML.Schema.XSDNode.elementChildren(node) {
|
|
collectKeysAndRefers(child, keyArities: &keyArities, keyrefs: &keyrefs)
|
|
}
|
|
}
|
|
|
|
private static func identityConstraintNameFindings(
|
|
_ containers: [XSDTree],
|
|
_ uniqueIndices: [Int],
|
|
namespaceMap: [Int: String?],
|
|
mainTargetNamespace: String?,
|
|
) -> [PureXML.Schema.SchemaLocatedFinding] {
|
|
struct KeyrefNameKey: Hashable {
|
|
let targetNamespace: String?
|
|
let name: String
|
|
}
|
|
var seen: Set<KeyrefNameKey> = []
|
|
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
|
func walk(_ node: XSDTree, namespaceURI: String?) {
|
|
let local = PureXML.Schema.XSDNode.localName(node)
|
|
if local == "appinfo" || local == "documentation" {
|
|
return
|
|
}
|
|
if let name = identityConstraintName(node) {
|
|
let key = KeyrefNameKey(targetNamespace: namespaceURI, name: name)
|
|
if !seen.insert(key).inserted {
|
|
let nsStr = namespaceURI.map { " in namespace '\($0)'" } ?? " in no namespace"
|
|
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
|
reason: "duplicate identity constraint name '\(name)'\(nsStr)",
|
|
node: node,
|
|
))
|
|
}
|
|
}
|
|
for child in PureXML.Schema.XSDNode.elementChildren(node) {
|
|
walk(child, namespaceURI: namespaceURI)
|
|
}
|
|
}
|
|
for index in uniqueIndices {
|
|
let container = containers[index]
|
|
guard PureXML.Schema.XSDNode.localName(container) != "redefine" else { continue }
|
|
let namespaceURI = namespaceMap[index] ?? mainTargetNamespace
|
|
walk(container, namespaceURI: namespaceURI)
|
|
}
|
|
return findings
|
|
}
|
|
|
|
/// The `name` of `node` when it is an identity constraint (`unique`/`key`/
|
|
/// `keyref`) in the XSD namespace, or nil.
|
|
private static func identityConstraintName(_ node: XSDTree) -> String? {
|
|
guard node.name?.namespaceURI == xsdNamespace,
|
|
let local = PureXML.Schema.XSDNode.localName(node),
|
|
["unique", "key", "keyref"].contains(local)
|
|
else { return nil }
|
|
return PureXML.Schema.XSDNode.attribute(node, "name")?.trimmingXMLWhitespace()
|
|
}
|
|
|
|
private static func isStructurallyEqual(_ lhs: XSDTree, _ rhs: XSDTree) -> Bool {
|
|
if lhs.name != rhs.name {
|
|
return false
|
|
}
|
|
if lhs.attributes.count != rhs.attributes.count {
|
|
return false
|
|
}
|
|
for attr in lhs.attributes {
|
|
guard let rhsVal = rhs.attributes.first(where: { $0.name == attr.name })?.value,
|
|
rhsVal == attr.value
|
|
else { return false }
|
|
}
|
|
let lhsChildren = PureXML.Schema.XSDNode.elementChildren(lhs)
|
|
let rhsChildren = PureXML.Schema.XSDNode.elementChildren(rhs)
|
|
if lhsChildren.count != rhsChildren.count {
|
|
return false
|
|
}
|
|
guard !zip(lhsChildren, rhsChildren).contains(where: { !isStructurallyEqual($0, $1) }) else { return false }
|
|
return true
|
|
}
|
|
|
|
static func resolveContainerNamespaces(_ containers: [XSDTree], mainTargetNamespace: String?) -> [Int: String?] {
|
|
var resolved: [Int: String?] = [:]
|
|
var resolvedIndices = Set<Int>()
|
|
guard !containers.isEmpty else { return resolved }
|
|
let mainIndex = containers.count - 1
|
|
resolved[mainIndex] = mainTargetNamespace
|
|
resolvedIndices.insert(mainIndex)
|
|
for parentIndex in containers.indices.reversed() {
|
|
guard resolvedIndices.contains(parentIndex) else { continue }
|
|
let parentNamespace = resolvedNamespace(at: parentIndex, in: resolved, fallback: mainTargetNamespace)
|
|
let parent = containers[parentIndex]
|
|
for child in PureXML.Schema.XSDNode.elementChildren(parent) {
|
|
let kind = PureXML.Schema.XSDNode.localName(child)
|
|
guard kind == "include" || kind == "import" || kind == "redefine",
|
|
let childKind = kind
|
|
else {
|
|
continue
|
|
}
|
|
let targetNamespaceURI: String? = if childKind == "import" {
|
|
PureXML.Schema.XSDNode.attribute(child, "namespace")
|
|
} else {
|
|
parentNamespace
|
|
}
|
|
let expectedAttrNamespace: String? = childKind == "import" ? PureXML.Schema.XSDNode.attribute(child, "namespace") : parentNamespace
|
|
let foundIndex = findMatchingContainerIndex(
|
|
in: containers,
|
|
before: parentIndex,
|
|
kind: childKind,
|
|
namespaces: (parent: parentNamespace, expected: expectedAttrNamespace),
|
|
resolvedIndices: resolvedIndices,
|
|
)
|
|
if let foundIndex {
|
|
resolved[foundIndex] = targetNamespaceURI
|
|
resolvedIndices.insert(foundIndex)
|
|
}
|
|
}
|
|
}
|
|
for index in containers.indices where !resolvedIndices.contains(index) {
|
|
let container = containers[index]
|
|
if let attrNS = PureXML.Schema.XSDNode.attribute(container, "targetNamespace") {
|
|
resolved[index] = attrNS
|
|
} else {
|
|
resolved[index] = mainTargetNamespace
|
|
}
|
|
resolvedIndices.insert(index)
|
|
}
|
|
return resolved
|
|
}
|
|
|
|
/// Schema-validity findings for `xs:include` when external schemas load: the
|
|
/// included document must be chameleon (no `targetNamespace`) or declare the
|
|
/// same target namespace as the includer (XSD Structures §4.2.3).
|
|
static func includeCompositionFindings(
|
|
_ containers: [XSDTree],
|
|
mainTargetNamespace: String?,
|
|
compositionLoaded: Bool,
|
|
containerLocations: [ObjectIdentifier: String?],
|
|
) -> [PureXML.Schema.SchemaLocatedFinding] {
|
|
guard compositionLoaded else { return [] }
|
|
let namespaceMap = resolveContainerNamespaces(containers, mainTargetNamespace: mainTargetNamespace)
|
|
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
|
for (parentIndex, parent) in containers.enumerated() {
|
|
guard PureXML.Schema.XSDNode.localName(parent) == "schema" else { continue }
|
|
let parentNamespace = resolvedNamespace(at: parentIndex, in: namespaceMap, fallback: mainTargetNamespace)
|
|
for child in PureXML.Schema.XSDNode.elementChildren(parent) {
|
|
let kind = PureXML.Schema.XSDNode.localName(child)
|
|
guard kind == "include" || kind == "redefine",
|
|
let location = PureXML.Schema.XSDNode.attribute(child, "schemaLocation")
|
|
else { continue }
|
|
guard let includedIndex = containers.firstIndex(where: {
|
|
containerLocations[ObjectIdentifier($0)] == location && PureXML.Schema.XSDNode.localName($0) == "schema"
|
|
}) else { continue }
|
|
let includedNamespace = PureXML.Schema.XSDNode.attribute(containers[includedIndex], "targetNamespace")
|
|
if let includedNamespace, includedNamespace != parentNamespace {
|
|
let parentLabel = parentNamespace ?? "no namespace"
|
|
let verb = kind == "redefine" ? "redefined" : "included"
|
|
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
|
reason: "\(verb) schema targetNamespace '\(includedNamespace)' must match the '\(parentLabel)' targetNamespace or be chameleon (no targetNamespace)",
|
|
node: child,
|
|
))
|
|
}
|
|
}
|
|
}
|
|
return findings
|
|
}
|
|
|
|
private static func resolvedNamespace(at index: Int, in map: [Int: String?], fallback: String?) -> String? {
|
|
switch map[index] {
|
|
case nil: fallback
|
|
case let .some(namespace): namespace
|
|
}
|
|
}
|
|
|
|
private static func findMatchingContainerIndex(
|
|
in containers: [XSDTree],
|
|
before parentIndex: Int,
|
|
kind: String,
|
|
namespaces: (parent: String?, expected: String?),
|
|
resolvedIndices: Set<Int>,
|
|
) -> Int? {
|
|
for index in (0 ..< parentIndex).reversed() where !resolvedIndices.contains(index) {
|
|
let container = containers[index]
|
|
let attrNS = PureXML.Schema.XSDNode.attribute(container, "targetNamespace")
|
|
let isMatch: Bool = if kind == "import" {
|
|
attrNS == namespaces.expected
|
|
} else {
|
|
attrNS == nil || attrNS == namespaces.parent
|
|
}
|
|
if isMatch {
|
|
return index
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
}
|