On the cross-document skip path (a schema declares import/include/redefine but no external document was loaded), the reference check stood down whole, so a prefixed reference whose namespace is never imported was accepted even though it can resolve to no component under any loading (corpus schZ011's d:d, where namespaces a/b/c are imported but d is not). Add undeclaredNamespaceReferenceErrors, run only on that skip path, to flag a reference whose namespace is neither the target, the XSD or XML namespace, nor named by any xs:import. To stay sound when the import closure is unknown, it stands down entirely the moment any external carries a schemaLocation: that document may transitively import the referenced namespace (corpus schZ004 imports only a, yet a's document imports b, so b:b resolves). It applies only when every external is location-less and the closure is provably complete. XSTS invalid-schemas-accepted 43 -> 39 (schZ011_a-d); valid-schemas-rejected held at 0; no other bucket moved.
389 lines
18 KiB
Swift
389 lines
18 KiB
Swift
extension PureXML.Schema.XSDParser {
|
|
private struct ResolutionContext {
|
|
let check: ReferenceCheckContext
|
|
|
|
init(
|
|
types: Set<String>,
|
|
pools: [String: Set<String>],
|
|
bindings: [String: String],
|
|
targetNamespace: String?,
|
|
foreignPools: [String?: [String: Set<String>]],
|
|
chameleonNamespace: Bool,
|
|
) {
|
|
check = ReferenceCheckContext(
|
|
types: types,
|
|
pools: pools,
|
|
bindings: bindings,
|
|
targetNamespace: targetNamespace,
|
|
foreignPools: foreignPools,
|
|
chameleonNamespace: chameleonNamespace,
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Schema-validity findings for unresolvable references: every QName a schema
|
|
/// names (a `type`/`base`/`itemType`/`memberTypes` type, an `element`/
|
|
/// `attribute`/`group`/`attributeGroup` `ref`, an element `substitutionGroup`)
|
|
/// must resolve to a declared component or a built-in. The reference is matched
|
|
/// by local name, as the rest of the compiler resolves it.
|
|
///
|
|
/// Skipped when the document pulls in external definitions through
|
|
/// `import`/`include`/`redefine`: the default compile does not load them, so
|
|
/// the pools would be incomplete and a reference into them must not be flagged.
|
|
static func referenceErrors(
|
|
_ schema: XSDTree,
|
|
in context: PureXML.Schema.XSDContext,
|
|
elements: [String: PureXML.Schema.ElementType],
|
|
containers: [XSDTree],
|
|
) -> [String] {
|
|
collectReferenceErrors(schema, in: context, elements: elements, containers: containers)
|
|
}
|
|
|
|
static func referenceFindings(
|
|
_ schema: XSDTree,
|
|
in context: PureXML.Schema.XSDContext,
|
|
elements: [String: PureXML.Schema.ElementType],
|
|
containers: [XSDTree],
|
|
) -> [PureXML.Schema.SchemaLocatedFinding] {
|
|
PureXML.Schema.SchemaLocatedFinding.unlocated(
|
|
collectReferenceErrors(schema, in: context, elements: elements, containers: containers),
|
|
)
|
|
}
|
|
|
|
private static func collectReferenceErrors(
|
|
_ schema: XSDTree,
|
|
in context: PureXML.Schema.XSDContext,
|
|
elements: [String: PureXML.Schema.ElementType],
|
|
containers: [XSDTree],
|
|
) -> [String] {
|
|
let xsdErrors = xsdNamespaceReferenceErrors(schema)
|
|
let simpleContentErrors = simpleContentBaseErrors(schema, in: context)
|
|
if skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) {
|
|
return xsdErrors + simpleContentErrors
|
|
+ undeclaredNamespaceReferenceErrors(schema, containers: containers)
|
|
}
|
|
let types = referenceBuiltins.union(context.simpleTypes.keys).union(context.complexTypeNodes.keys)
|
|
let pools: [String: Set<String>] = [
|
|
"element": Set(elements.keys),
|
|
"attribute": Set(context.globalAttributes.keys),
|
|
"group": Set(context.groups.keys),
|
|
"attributeGroup": Set(context.attributeGroups.keys),
|
|
]
|
|
let targetNamespace = context.targetNamespace
|
|
let foreignPools = context.compositionLoaded
|
|
? foreignComponentPools(containers, mainTargetNamespace: targetNamespace)
|
|
: [:]
|
|
var errors: [String] = []
|
|
let referenceSources: [(XSDTree, String?)]
|
|
if context.compositionLoaded {
|
|
let namespaceMap = resolveContainerNamespaces(containers, mainTargetNamespace: targetNamespace)
|
|
referenceSources = containers.compactMap { container -> (XSDTree, String?)? in
|
|
guard PureXML.Schema.XSDNode.localName(container) == "schema" else { return nil }
|
|
let index = containers.firstIndex(where: { $0 === container }) ?? containers.count - 1
|
|
let effectiveNamespace = namespaceMap[index] ?? targetNamespace
|
|
return (container, effectiveNamespace)
|
|
}
|
|
} else {
|
|
referenceSources = [(schema, targetNamespace)]
|
|
}
|
|
for (source, effectiveNamespace) in referenceSources {
|
|
let sourceBindings = PureXML.Schema.XSDNode.namespaceBindings(of: source)
|
|
let chameleonNamespace = PureXML.Schema.XSDNode.attribute(source, "targetNamespace") == nil
|
|
let resolutionContext = ResolutionContext(
|
|
types: types,
|
|
pools: pools,
|
|
bindings: sourceBindings,
|
|
targetNamespace: effectiveNamespace,
|
|
foreignPools: foreignPools,
|
|
chameleonNamespace: chameleonNamespace,
|
|
)
|
|
collectReferenceErrors(source, in: resolutionContext, inheritedBindings: sourceBindings, into: &errors)
|
|
}
|
|
return xsdErrors + simpleContentErrors + errors + redefineComponentExistenceErrors(containers)
|
|
+ importNamespaceErrors(context, containers)
|
|
}
|
|
|
|
/// XSD 1.0 `src-redefine.6`/`7.2.1`/`7.2.2`: each component a `redefine` names
|
|
/// must have a same-kind component of that name in the redefined schema, and a
|
|
/// `redefine` may redefine a given component at most once. Runs only past the
|
|
/// cross-document skip guard above, so the redefined schema is loaded; a
|
|
/// redefinition whose name matches nothing in any non-`redefine` container (the
|
|
/// loaded targets and the root) names a component that does not exist, and a
|
|
/// name redefined twice in one `redefine` is a duplicate redefinition.
|
|
private static func redefineComponentExistenceErrors(_ containers: [XSDTree]) -> [String] {
|
|
let kinds = ["complexType", "simpleType", "group", "attributeGroup"]
|
|
var defined: [String: Set<String>] = [:]
|
|
for container in containers where PureXML.Schema.XSDNode.localName(container) != "redefine" {
|
|
for kind in kinds {
|
|
for node in PureXML.Schema.XSDNode.children(container, named: kind) {
|
|
if let name = PureXML.Schema.XSDNode.attribute(node, "name") {
|
|
defined[kind, default: []].insert(name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
var errors: [String] = []
|
|
for container in containers where PureXML.Schema.XSDNode.localName(container) == "redefine" {
|
|
var redefinedHere: [String: Set<String>] = [:]
|
|
for kind in kinds {
|
|
for node in PureXML.Schema.XSDNode.children(container, named: kind) {
|
|
guard let name = PureXML.Schema.XSDNode.attribute(node, "name") else { continue }
|
|
if !redefinedHere[kind, default: []].insert(name).inserted {
|
|
errors.append("a redefine redefines \(kind) '\(name)' more than once")
|
|
} else if defined[kind]?.contains(name) != true {
|
|
errors.append("a redefine of \(kind) '\(name)' has no '\(name)' to redefine in the redefined schema")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return errors
|
|
}
|
|
|
|
/// Whether cross-document schema-validity rules should stand down: an external
|
|
/// reference is declared but no external document was loaded through a
|
|
/// `schemaLoader`, so the merged component set is incomplete.
|
|
static func skipsCrossDocumentRules(_ schema: XSDTree, compositionLoaded: Bool) -> Bool {
|
|
hasExternalReference(schema) && !compositionLoaded
|
|
}
|
|
|
|
/// Whether the document declares an `import`, `include`, or `redefine`, so its
|
|
/// component pools may be completed by an external document the default compile
|
|
/// does not load.
|
|
static func hasExternalReference(_ schema: XSDTree) -> Bool {
|
|
var found = false
|
|
func walk(_ node: XSDTree) {
|
|
if isExternalDefinition(node) {
|
|
found = true
|
|
return
|
|
}
|
|
for child in PureXML.Schema.XSDNode.elementChildren(node) where !found {
|
|
walk(child)
|
|
}
|
|
}
|
|
walk(schema)
|
|
return found
|
|
}
|
|
|
|
/// Whether `node` is an `xs:import`/`xs:include`/`xs:redefine` (an XSD-namespace
|
|
/// element bringing in external definitions).
|
|
private static func isExternalDefinition(_ node: XSDTree) -> Bool {
|
|
guard node.name?.namespaceURI == xsdNamespace, let local = PureXML.Schema.XSDNode.localName(node) else {
|
|
return false
|
|
}
|
|
return local == "import" || local == "include" || local == "redefine"
|
|
}
|
|
|
|
private static func collectReferenceErrors(
|
|
_ node: XSDTree,
|
|
in resolutionContext: ResolutionContext,
|
|
inheritedBindings: [String: String],
|
|
into errors: inout [String],
|
|
) {
|
|
let local = PureXML.Schema.XSDNode.localName(node)
|
|
if local == "appinfo" || local == "documentation" { return }
|
|
let bindings = mergedNamespaceBindings(on: node, inherited: inheritedBindings)
|
|
let nodeContext = ResolutionContext(
|
|
types: resolutionContext.check.types,
|
|
pools: resolutionContext.check.pools,
|
|
bindings: bindings,
|
|
targetNamespace: resolutionContext.check.targetNamespace,
|
|
foreignPools: resolutionContext.check.foreignPools,
|
|
chameleonNamespace: resolutionContext.check.chameleonNamespace,
|
|
)
|
|
if node.name?.namespaceURI == xsdNamespace, let local {
|
|
errors += referenceErrors(at: node, local: local, in: nodeContext)
|
|
}
|
|
for child in PureXML.Schema.XSDNode.elementChildren(node) {
|
|
collectReferenceErrors(child, in: resolutionContext, inheritedBindings: bindings, into: &errors)
|
|
}
|
|
}
|
|
|
|
private static func referenceErrors(
|
|
at node: XSDTree,
|
|
local: String,
|
|
in resolutionContext: ResolutionContext,
|
|
) -> [String] {
|
|
var errors: [String] = []
|
|
let context = resolutionContext.check
|
|
for attribute in ["type", "base", "itemType"] {
|
|
guard let qname = PureXML.Schema.XSDNode.attribute(node, attribute) else { continue }
|
|
if isUndeclaredReferenceType(qname, in: context) {
|
|
errors.append("\(attribute) references undeclared type '\(qname)'")
|
|
}
|
|
}
|
|
if let members = PureXML.Schema.XSDNode.attribute(node, "memberTypes") {
|
|
for token in members.split(whereSeparator: \.isWhitespace) {
|
|
let qname = String(token)
|
|
if isUndeclaredReferenceType(qname, in: context) {
|
|
errors.append("memberTypes references undeclared type '\(qname)'")
|
|
}
|
|
}
|
|
}
|
|
if let head = PureXML.Schema.XSDNode.attribute(node, "substitutionGroup") {
|
|
if isUndeclaredReferenceRef(head, poolName: "element", in: context) {
|
|
errors.append("substitutionGroup references undeclared element '\(head)'")
|
|
}
|
|
}
|
|
if let reference = PureXML.Schema.XSDNode.attribute(node, "ref"), context.pools[local] != nil {
|
|
if isUndeclaredReferenceRef(reference, poolName: local, in: context) {
|
|
errors.append("\(local) ref references undeclared '\(reference)'")
|
|
}
|
|
}
|
|
return errors
|
|
}
|
|
|
|
static func simpleTypeBaseNotComplexErrors(_ schema: XSDTree, in context: PureXML.Schema.XSDContext) -> [String] {
|
|
var errors: [String] = []
|
|
let bindings = PureXML.Schema.XSDNode.namespaceBindings(of: schema)
|
|
let targetNamespace = context.targetNamespace
|
|
|
|
func isComplex(_ qname: String) -> Bool {
|
|
let trimmed = qname.trimmingXMLWhitespace()
|
|
let prefix = PureXML.Schema.XSDNode.prefix(trimmed)
|
|
let uri = prefix.flatMap { bindings[$0] } ?? bindings[""]
|
|
let local = PureXML.Schema.XSDNode.stripPrefix(trimmed)
|
|
if uri == xsdNamespace {
|
|
if targetNamespace == xsdNamespace {
|
|
return local == "anyType" || context.complexTypeNodes[local] != nil
|
|
} else {
|
|
return local == "anyType"
|
|
}
|
|
}
|
|
if uri == targetNamespace || uri == nil || uri == "" {
|
|
return context.complexTypeNodes[local] != nil
|
|
}
|
|
return false
|
|
}
|
|
|
|
func check(_ node: XSDTree) {
|
|
let local = PureXML.Schema.XSDNode.localName(node)
|
|
if local == "appinfo" || local == "documentation" { return }
|
|
|
|
if node.name?.namespaceURI == xsdNamespace {
|
|
switch local {
|
|
case "restriction":
|
|
Self.checkRestrictionUnderSimpleType(node, isComplex: isComplex, errors: &errors)
|
|
case "list":
|
|
Self.checkList(node, isComplex: isComplex, errors: &errors)
|
|
case "union":
|
|
Self.checkUnion(node, isComplex: isComplex, errors: &errors)
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
for child in PureXML.Schema.XSDNode.elementChildren(node) {
|
|
check(child)
|
|
}
|
|
}
|
|
check(schema)
|
|
return errors
|
|
}
|
|
|
|
private static func checkRestrictionUnderSimpleType(_ node: XSDTree, isComplex: (String) -> Bool, errors: inout [String]) {
|
|
guard let parent = node.parent, parent.name?.namespaceURI == xsdNamespace, PureXML.Schema.XSDNode.localName(parent) == "simpleType" else { return }
|
|
if let base = PureXML.Schema.XSDNode.attribute(node, "base"), isComplex(base) {
|
|
errors.append("base type '\(base)' of simpleType restriction must be a simple type, not a complex type")
|
|
}
|
|
}
|
|
|
|
private static func checkList(_ node: XSDTree, isComplex: (String) -> Bool, errors: inout [String]) {
|
|
if let itemType = PureXML.Schema.XSDNode.attribute(node, "itemType"), isComplex(itemType) {
|
|
errors.append("itemType '\(itemType)' of list must be a simple type, not a complex type")
|
|
}
|
|
}
|
|
|
|
private static func checkUnion(_ node: XSDTree, isComplex: (String) -> Bool, errors: inout [String]) {
|
|
if let members = PureXML.Schema.XSDNode.attribute(node, "memberTypes") {
|
|
for token in members.split(whereSeparator: \.isWhitespace) where isComplex(String(token)) {
|
|
errors.append("memberType '\(token)' of union must be a simple type, not a complex type")
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func xsdNamespaceReferenceErrors(_ schema: XSDTree) -> [String] {
|
|
let targetNamespace = PureXML.Schema.XSDNode.attribute(schema, "targetNamespace")
|
|
if targetNamespace == xsdNamespace {
|
|
return []
|
|
}
|
|
var errors: [String] = []
|
|
let bindings = PureXML.Schema.XSDNode.namespaceBindings(of: schema)
|
|
|
|
func isUndeclaredBuiltin(_ qname: String) -> Bool {
|
|
let prefix = PureXML.Schema.XSDNode.prefix(qname)
|
|
let uri = prefix.flatMap { bindings[$0] } ?? bindings[""]
|
|
if uri == xsdNamespace {
|
|
let localName = PureXML.Schema.XSDNode.stripPrefix(qname)
|
|
return !referenceBuiltins.contains(localName)
|
|
}
|
|
return false
|
|
}
|
|
|
|
func check(_ node: XSDTree) {
|
|
let local = PureXML.Schema.XSDNode.localName(node)
|
|
if local == "appinfo" || local == "documentation" { return }
|
|
|
|
if node.name?.namespaceURI == xsdNamespace {
|
|
for attribute in ["type", "base", "itemType"] {
|
|
if let qname = PureXML.Schema.XSDNode.attribute(node, attribute), isUndeclaredBuiltin(qname) {
|
|
errors.append("\(attribute) references undeclared type '\(qname)'")
|
|
}
|
|
}
|
|
if let members = PureXML.Schema.XSDNode.attribute(node, "memberTypes") {
|
|
for token in members.split(whereSeparator: \.isWhitespace) {
|
|
let qname = String(token)
|
|
if isUndeclaredBuiltin(qname) {
|
|
errors.append("memberTypes references undeclared type '\(qname)'")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for child in PureXML.Schema.XSDNode.elementChildren(node) {
|
|
check(child)
|
|
}
|
|
}
|
|
check(schema)
|
|
return errors
|
|
}
|
|
|
|
private static func simpleContentBaseErrors(_ schema: XSDTree, in context: PureXML.Schema.XSDContext) -> [String] {
|
|
var errors: [String] = []
|
|
let bindings = PureXML.Schema.XSDNode.namespaceBindings(of: schema)
|
|
let targetNamespace = context.targetNamespace
|
|
|
|
func check(_ node: XSDTree) {
|
|
let local = PureXML.Schema.XSDNode.localName(node)
|
|
if local == "appinfo" || local == "documentation" { return }
|
|
|
|
let isSimpleContentRestriction = node.name?.namespaceURI == xsdNamespace && local == "restriction"
|
|
&& node.parent?.name?.namespaceURI == xsdNamespace
|
|
&& node.parent.flatMap { PureXML.Schema.XSDNode.localName($0) } == "simpleContent"
|
|
if isSimpleContentRestriction {
|
|
if let base = PureXML.Schema.XSDNode.attribute(node, "base") {
|
|
let prefix = PureXML.Schema.XSDNode.prefix(base)
|
|
let uri = prefix.flatMap { bindings[$0] } ?? bindings[""]
|
|
let localPart = PureXML.Schema.XSDNode.stripPrefix(base)
|
|
if uri == targetNamespace || uri == nil || uri == "" {
|
|
if let baseNode = context.complexTypeNodes[localPart] {
|
|
let children = PureXML.Schema.XSDNode.elementChildren(baseNode)
|
|
let hasSimpleContent = children.contains { child in
|
|
child.name?.namespaceURI == xsdNamespace && PureXML.Schema.XSDNode.localName(child) == "simpleContent"
|
|
}
|
|
if !hasSimpleContent {
|
|
errors.append("base type '\(base)' of simpleContent restriction must be a simple type or complex type with simpleContent")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for child in PureXML.Schema.XSDNode.elementChildren(node) {
|
|
check(child)
|
|
}
|
|
}
|
|
check(schema)
|
|
return errors
|
|
}
|
|
}
|