PureXML/Sources/Schema/SchemaUndeclaredNamespace.swift
Mihaela Mihaljevic 4dd1d330cb feat(schema): locate reference-resolution schema diagnostics
Close the last superficially-located checker (#169): referenceFindings was
dispatched as located but its body wrapped the whole aggregate in
.unlocated, so every reference-resolution error still collapsed onto the
schema root. Convert all six sub-collectors to located findings, each
attaching to the node in scope: the core type/base/itemType/memberTypes/ref/
substitutionGroup walk and the xs:-namespace builtin check locate on the
referencing element; the simpleContent base-kind check on the restriction;
the un-imported-namespace check on the referencing element; the
redefine-component-existence check on the redefine's child component; and
the import-namespace-agreement check on the import element.

Every predicate, aggregation order, and reason string is unchanged; only the
location each finding carries differs, verified conformance-neutral by the
full XSTS suite (14383 groups, pinned counts unchanged). Removes two dead
helpers exposed along the way: the unused referenceErrors [String] wrapper
and the unused locatedFindings helper in SchemaCompileValidation. The outer
aggregation is rewritten from a single + chain to incremental appends to keep
the type-checker within budget. Adds a located-diagnostics test per
node-locatable sub-collector; the loader-dependent redefine/import families
are exercised by the XSTS suite.
2026-06-21 18:09:20 +02:00

100 lines
5.3 KiB
Swift

extension PureXML.Schema.XSDParser {
/// XSD 1.0 `src-resolve`, reduced to the part that holds even when no external
/// document was loaded: a prefixed reference whose namespace is neither this
/// schema's target namespace, the XSD or XML namespace, nor declared by any
/// `xs:import` in the assembled set can resolve to no component, so it is
/// unresolvable regardless. This runs only on the cross-document skip path.
///
/// Soundness requires that the import closure be fully *known*. It is known only
/// when every `import`/`include`/`redefine` is location-less: there is nothing to
/// load, so no unseen document can transitively import a further namespace. The
/// moment any external carries a `schemaLocation` that did not load, the closure is
/// unknown (that document might import the very namespace a reference names, as in
/// corpus schZ004, where the main doc imports only `a` but `a`'s document imports
/// `b`, making `b:b` resolvable), so the whole check stands down to stay lenient.
static func undeclaredNamespaceReferenceFindings(_ schema: XSDTree, containers: [XSDTree]) -> [PureXML.Schema.SchemaLocatedFinding] {
let sources = containers.isEmpty ? [schema] : containers
if hasLocatedExternal(sources) { return [] }
var declared: Set<String> = [xsdNamespace]
declared.formUnion(unloadedReferenceNamespaces)
for container in sources where PureXML.Schema.XSDNode.localName(container) == "schema" {
if let target = PureXML.Schema.XSDNode.attribute(container, "targetNamespace"), !target.isEmpty {
declared.insert(target)
}
for node in descendants(container, named: "import") where node.name?.namespaceURI == xsdNamespace {
if let namespace = PureXML.Schema.XSDNode.attribute(node, "namespace"), !namespace.isEmpty {
declared.insert(namespace)
}
}
}
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
for container in sources where PureXML.Schema.XSDNode.localName(container) == "schema" {
collectUndeclaredNamespaceReferences(
container,
inheritedBindings: PureXML.Schema.XSDNode.namespaceBindings(of: container),
declared: declared,
into: &findings,
)
}
return findings
}
private static func collectUndeclaredNamespaceReferences(
_ node: XSDTree,
inheritedBindings: [String: String],
declared: Set<String>,
into findings: inout [PureXML.Schema.SchemaLocatedFinding],
) {
let local = PureXML.Schema.XSDNode.localName(node)
if local == "appinfo" || local == "documentation" { return }
let bindings = mergedNamespaceBindings(on: node, inherited: inheritedBindings)
if node.name?.namespaceURI == xsdNamespace {
for qname in foreignReferenceQNames(of: node) {
guard PureXML.Schema.XSDNode.prefix(qname.trimmingXMLWhitespace()) != nil,
let uri = referenceURI(for: qname, bindings: bindings), !uri.isEmpty,
!declared.contains(uri)
else { continue }
findings.append(PureXML.Schema.SchemaLocatedFinding(
reason: "reference to '\(qname)' names namespace '\(uri)', which is not the target namespace and is not imported",
node: node,
))
}
}
for child in PureXML.Schema.XSDNode.elementChildren(node) {
collectUndeclaredNamespaceReferences(child, inheritedBindings: bindings, declared: declared, into: &findings)
}
}
/// Whether any `import`/`include`/`redefine` carries a `schemaLocation`. On the
/// skip path nothing was loaded, so a located external is one that failed to load,
/// leaving the import closure (and thus resolvability) unknown.
private static func hasLocatedExternal(_ sources: [XSDTree]) -> Bool {
for container in sources where PureXML.Schema.XSDNode.localName(container) == "schema" {
for kind in ["import", "include", "redefine"] {
for node in descendants(container, named: kind) where node.name?.namespaceURI == xsdNamespace {
if let location = PureXML.Schema.XSDNode.attribute(node, "schemaLocation"), !location.isEmpty {
return true
}
}
}
}
return false
}
/// Every QName a node names through a resolving attribute: the single-QName
/// `type`/`base`/`itemType`/`ref`/`substitutionGroup`, plus each token of a
/// whitespace-separated `memberTypes`.
private static func foreignReferenceQNames(of node: XSDTree) -> [String] {
var references: [String] = []
for attribute in ["type", "base", "itemType", "ref", "substitutionGroup"] {
if let qname = PureXML.Schema.XSDNode.attribute(node, attribute) {
references.append(qname)
}
}
if let members = PureXML.Schema.XSDNode.attribute(node, "memberTypes") {
references.append(contentsOf: members.split(whereSeparator: \.isWhitespace).map(String.init))
}
return references
}
}