PureXML/Sources/Schema/SchemaReferenceResolution.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

117 lines
5.4 KiB
Swift

struct ReferenceCheckContext {
let types: Set<String>
let pools: [String: Set<String>]
let bindings: [String: String]
let targetNamespace: String?
let foreignPools: [String?: [String: Set<String>]]
/// A no-`targetNamespace` included schema whose globals adopt the includer's namespace.
let chameleonNamespace: Bool
}
extension PureXML.Schema.XSDParser {
/// The built-in type names a reference may name without a declaration: the
/// XSD Part 2 datatypes, the built-in list types, and the ur-types.
static let referenceBuiltins: Set<String> = {
var names = Set(PureXML.Schema.BuiltinType.allCases.map(\.rawValue))
names.formUnion(["anyType", "anySimpleType", "anyAtomicType", "NOTATION", "IDREFS", "ENTITIES", "NMTOKENS"])
return names
}()
/// Namespaces commonly referenced without a loadable schema document (`xml:`,
/// and similar). References into them stand down rather than being rejected.
static let unloadedReferenceNamespaces: Set<String> = [
"http://www.w3.org/XML/1998/namespace",
]
static func referenceURI(for qname: String, bindings: [String: String]) -> String? {
let prefix = PureXML.Schema.XSDNode.prefix(qname)
var uri = prefix.flatMap { bindings[$0] } ?? bindings[""]
if uri == nil, prefix == "xml" {
uri = "http://www.w3.org/XML/1998/namespace"
}
return uri
}
static func isUndeclaredReferenceType(_ qname: String, in context: ReferenceCheckContext) -> Bool {
let uri = referenceURI(for: qname, bindings: context.bindings)
let localPart = referenceLocalName(qname)
if uri == xsdNamespace {
if context.targetNamespace == xsdNamespace {
return !referenceBuiltins.contains(localPart) && !context.types.contains(localPart)
} else {
return !referenceBuiltins.contains(localPart)
}
}
if let uri, unloadedReferenceNamespaces.contains(uri) {
return false
}
let inTargetNamespace = uri == context.targetNamespace
|| ((uri == nil || uri == "") && (context.targetNamespace == nil || context.targetNamespace == ""))
if inTargetNamespace {
return !context.types.contains(localPart)
}
if let foreignTypes = context.foreignPools[uri]?["type"] {
return !foreignTypes.contains(localPart)
}
if uri == nil || uri == "" {
// A no-namespace (unprefixed, no default `xmlns`) reference resolves to
// the absent namespace. It matches a chameleon-included global adopting
// the includer's namespace; otherwise, when the schema has a non-empty
// target namespace, it cannot name a type that lives there, so it is
// undeclared (XSTS addB009 / test66059). This mirrors the same branch in
// `isUndeclaredReferenceRef`.
if context.chameleonNamespace, context.types.contains(localPart) {
return false
}
if !(context.targetNamespace == nil || context.targetNamespace == "") {
return true
}
return !context.types.contains(localPart)
}
return false
}
static func isUndeclaredReferenceRef(_ qname: String, poolName: String, in context: ReferenceCheckContext) -> Bool {
let uri = referenceURI(for: qname, bindings: context.bindings)
let localPart = referenceLocalName(qname)
let inTargetNamespace = uri == context.targetNamespace
|| ((uri == nil || uri == "") && (context.targetNamespace == nil || context.targetNamespace == ""))
if inTargetNamespace {
return context.pools[poolName]?.contains(localPart) != true
}
if let uri, unloadedReferenceNamespaces.contains(uri) {
return false
}
if let foreign = context.foreignPools[uri]?[poolName] {
return !foreign.contains(localPart)
}
if uri == xsdNamespace {
// The XSD namespace declares no referenceable user component (element,
// group, attribute, attribute group), so a reference resolving to it is
// undeclared. This is reached when an unprefixed reference picks up a
// default `xmlns` of the XSD namespace, so it is NOT in the target
// namespace (XSTS xsd013/xsd014); the schema that itself targets the XSD
// namespace is the `inTargetNamespace` case above, and a genuinely loaded
// XSD-namespace document is the `foreignPools` case just before.
return true
}
if uri == nil || uri == "" {
if context.chameleonNamespace, context.pools[poolName]?.contains(localPart) == true {
return false
}
if !(context.targetNamespace == nil || context.targetNamespace == "") {
return true
}
return context.pools[poolName]?.contains(localPart) != true
}
return false
}
/// The local part of a QName reference, after the whitespace normalization a
/// `whiteSpace="collapse"` QName attribute receives (a value may be written
/// with surrounding or, harmlessly, interior whitespace).
static func referenceLocalName(_ qname: String) -> String {
PureXML.Schema.XSDNode.stripPrefix(qname.trimmingXMLWhitespace())
}
}