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.
320 lines
18 KiB
Swift
320 lines
18 KiB
Swift
private typealias ContentDerivNode = PureXML.Schema.XSDNode
|
|
|
|
extension PureXML.Schema.XSDParser {
|
|
/// XSD 1.0 `cos-ct-extends.1.4.2.2`: a `complexContent` extension that adds
|
|
/// element content (a model group) produces an element-only/mixed content type,
|
|
/// so its base must have complex or empty content. A `simpleContent` base has a
|
|
/// simple content type and cannot gain element content this way. An extension
|
|
/// that adds only attributes keeps the base's simple content and is valid (it
|
|
/// is the `complexContent`-around-`simpleContent` idiom), so the model group is
|
|
/// required for the error. Resolved in this schema's own target namespace.
|
|
static func simpleContentExtensionBaseFindings(
|
|
_ schema: XSDTree,
|
|
_ context: PureXML.Schema.XSDContext,
|
|
_ types: [String: PureXML.Schema.ElementType],
|
|
) -> [PureXML.Schema.SchemaLocatedFinding] {
|
|
guard !skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) else { return [] }
|
|
let bindings = context.namespaceBindings
|
|
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
|
for content in descendants(schema, named: "complexContent") {
|
|
guard let ext = ContentDerivNode.firstChild(content, named: "extension"),
|
|
extensionAddsElementContent(ext),
|
|
let base = ContentDerivNode.attribute(ext, "base"),
|
|
ContentDerivNode.referenceNamespace(base, bindings) == context.targetNamespace,
|
|
case let .complex(complex)? = types[ContentDerivNode.stripPrefix(base)],
|
|
case .simpleContent = complex.content
|
|
else { continue }
|
|
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
|
reason: "a complexContent extension that adds element content may not extend '\(ContentDerivNode.stripPrefix(base))', which has simple content",
|
|
node: ext,
|
|
))
|
|
}
|
|
return findings
|
|
}
|
|
|
|
/// XSD 1.0 `src-ct.2` / Derivation Valid (Restriction): the base of a
|
|
/// `simpleContent` RESTRICTION must be a complex type (whose content is simple).
|
|
/// A built-in (`xs:integer`, `xs:anySimpleType`) or a user `simpleType` base is
|
|
/// invalid: a complex type with simple content is built by EXTENDING a simple
|
|
/// type, never by restricting one through `simpleContent`. Self-contained schema
|
|
/// only; a foreign or unresolved base is left alone.
|
|
static func simpleContentRestrictionBaseFindings(
|
|
_ schema: XSDTree,
|
|
_ context: PureXML.Schema.XSDContext,
|
|
_ types: [String: PureXML.Schema.ElementType],
|
|
) -> [PureXML.Schema.SchemaLocatedFinding] {
|
|
guard !skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) else { return [] }
|
|
let bindings = context.namespaceBindings
|
|
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
|
for content in descendants(schema, named: "simpleContent") {
|
|
guard let restriction = ContentDerivNode.firstChild(content, named: "restriction"),
|
|
let base = ContentDerivNode.attribute(restriction, "base")
|
|
else { continue }
|
|
let namespace = ContentDerivNode.referenceNamespace(base, bindings)
|
|
let baseName = ContentDerivNode.stripPrefix(base)
|
|
if namespace == xsdNamespace {
|
|
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
|
reason: "a simpleContent restriction's base must be a complex type, not the built-in simple type '\(baseName)'",
|
|
node: restriction,
|
|
))
|
|
} else if namespace == context.targetNamespace, case .simple? = types[baseName] {
|
|
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
|
reason: "a simpleContent restriction's base must be a complex type, not the simple type '\(baseName)'",
|
|
node: restriction,
|
|
))
|
|
}
|
|
}
|
|
return findings
|
|
}
|
|
|
|
/// XSD 1.0 Derivation Valid (Restriction, Simple): when a `simpleContent`
|
|
/// restriction supplies an inline `simpleType`, that inline type must itself be
|
|
/// a valid restriction of the base complex type's simple content type. A list
|
|
/// of `xs:int`, for example, is not a restriction of `xs:decimal` because it
|
|
/// changes the variety from atomic to list (W3C msData particlesZ018).
|
|
///
|
|
/// Only local, resolved complex bases with simple content are checked. Built-in,
|
|
/// foreign, unresolved, and composed bases are left to the surrounding base-kind
|
|
/// and cross-document rules so this cannot reject a schema whose base type is
|
|
/// unknown to this compile pass.
|
|
static func simpleContentRestrictionTypeFindings(
|
|
_ schema: XSDTree,
|
|
_ context: PureXML.Schema.XSDContext,
|
|
_ types: [String: PureXML.Schema.ElementType],
|
|
) -> [PureXML.Schema.SchemaLocatedFinding] {
|
|
guard !skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) else { return [] }
|
|
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
|
for content in descendants(schema, named: "simpleContent") {
|
|
guard let restriction = ContentDerivNode.firstChild(content, named: "restriction"),
|
|
let inline = ContentDerivNode.firstChild(restriction, named: "simpleType"),
|
|
let base = ContentDerivNode.attribute(restriction, "base")
|
|
else { continue }
|
|
let bindings = namespaceBindingsInScope(of: restriction, defaultBindings: context.namespaceBindings)
|
|
let namespace = ContentDerivNode.referenceNamespace(base, bindings)
|
|
let baseName = ContentDerivNode.stripPrefix(base)
|
|
guard namespace == context.targetNamespace,
|
|
case let .complex(complex)? = types[baseName],
|
|
case let .simpleContent(baseSimpleType) = complex.content
|
|
else { continue }
|
|
let derivedSimpleType = scopedSimpleType(inline, context)
|
|
if !isSimpleTypeRestrictionOK(derived: derivedSimpleType, base: baseSimpleType) {
|
|
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
|
reason: "the inline simpleType in a simpleContent restriction is not a valid restriction of base type '\(baseName)'",
|
|
node: restriction,
|
|
))
|
|
}
|
|
}
|
|
return findings
|
|
}
|
|
|
|
/// XSD 1.0 `src-ct.1`: the base of a `complexContent` derivation (restriction or
|
|
/// extension) must be a COMPLEX type. The ur-type `xs:anyType` is complex and is
|
|
/// the legitimate base of most complex types; every other XSD built-in, and a
|
|
/// user `simpleType`, is a simple type and may not be a complexContent base. A
|
|
/// LOCALLY-declared type takes precedence over the built-in reading: a schema may
|
|
/// target the XSD namespace and define its own components there (the
|
|
/// schema-for-schemas extends its own `xs:openAttrs`), so resolve `types` first
|
|
/// and only fall back to the built-in table when there is no local declaration.
|
|
/// Self-contained schema only; a foreign or unresolved base is left alone.
|
|
static func complexContentBaseKindFindings(
|
|
_ schema: XSDTree,
|
|
_ context: PureXML.Schema.XSDContext,
|
|
_ types: [String: PureXML.Schema.ElementType],
|
|
) -> [PureXML.Schema.SchemaLocatedFinding] {
|
|
guard !skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) else { return [] }
|
|
let bindings = context.namespaceBindings
|
|
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
|
for content in descendants(schema, named: "complexContent") {
|
|
let isRestriction = ContentDerivNode.firstChild(content, named: "restriction") != nil
|
|
guard let derivation = ContentDerivNode.firstChild(content, named: "extension")
|
|
?? ContentDerivNode.firstChild(content, named: "restriction"),
|
|
let base = ContentDerivNode.attribute(derivation, "base")
|
|
else { continue }
|
|
let namespace = ContentDerivNode.referenceNamespace(base, bindings)
|
|
let baseName = ContentDerivNode.stripPrefix(base)
|
|
if namespace == context.targetNamespace, let local = types[baseName] {
|
|
if case .simple = local {
|
|
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
|
reason: "a complexContent base must be a complex type, not the simple type '\(baseName)'",
|
|
node: derivation,
|
|
))
|
|
} else if isRestriction, case let .complex(complex) = local, case .simpleContent = complex.content {
|
|
// cos-ct-restricts 5.2: a complexContent restriction's {content type}
|
|
// is element-only/mixed/empty (from its model group), which can never
|
|
// validly restrict a SIMPLE content type. So a complexContent
|
|
// restriction of a complex type that has simple content is invalid.
|
|
// (The extension direction has a valid attribute-only idiom and is
|
|
// handled by simpleContentExtensionBaseFindings, so it is not flagged
|
|
// here.) Catches particlesZ039.
|
|
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
|
reason: "a complexContent restriction's base '\(baseName)' has simple content, which a complexContent restriction cannot restrict",
|
|
node: derivation,
|
|
))
|
|
}
|
|
} else if namespace == xsdNamespace, types[baseName] == nil, baseName != "anyType" {
|
|
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
|
reason: "a complexContent base must be a complex type, not the built-in simple type '\(baseName)'",
|
|
node: derivation,
|
|
))
|
|
}
|
|
}
|
|
return findings
|
|
}
|
|
|
|
/// XSD 1.0 `src-ct.2` (extension): the base of a `simpleContent` EXTENSION must be
|
|
/// a simple type or a complex type whose content type is simple. A complex type
|
|
/// with element-only, mixed, or empty content (including the ur-type
|
|
/// `xs:anyType`) may not be a simpleContent extension base, since the result must
|
|
/// have simple content. A LOCALLY-declared type takes precedence over the
|
|
/// built-in reading (a schema may target the XSD namespace), so resolve `types`
|
|
/// first. A simple-type base (built-in or user) is valid; only `xs:anyType` as a
|
|
/// built-in, or a local complex type without simple content, is an error.
|
|
/// Self-contained schema only; a foreign or unresolved base is left alone.
|
|
static func simpleContentExtensionBaseKindFindings(
|
|
_ schema: XSDTree,
|
|
_ context: PureXML.Schema.XSDContext,
|
|
_ types: [String: PureXML.Schema.ElementType],
|
|
) -> [PureXML.Schema.SchemaLocatedFinding] {
|
|
guard !skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) else { return [] }
|
|
let bindings = context.namespaceBindings
|
|
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
|
for content in descendants(schema, named: "simpleContent") {
|
|
guard let ext = ContentDerivNode.firstChild(content, named: "extension"),
|
|
let base = ContentDerivNode.attribute(ext, "base")
|
|
else { continue }
|
|
let namespace = ContentDerivNode.referenceNamespace(base, bindings)
|
|
let baseName = ContentDerivNode.stripPrefix(base)
|
|
if namespace == context.targetNamespace, case let .complex(complex)? = types[baseName] {
|
|
switch complex.content {
|
|
case .simpleContent: break
|
|
case .mixed, .elementOnly, .empty:
|
|
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
|
reason: "a simpleContent extension's base complex type '\(baseName)' must have simple content",
|
|
node: ext,
|
|
))
|
|
}
|
|
} else if namespace == xsdNamespace, types[baseName] == nil, baseName == "anyType" {
|
|
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
|
reason: "a simpleContent extension's base must be a simple type or a complex type with simple content, not '\(baseName)'",
|
|
node: ext,
|
|
))
|
|
}
|
|
}
|
|
return findings
|
|
}
|
|
|
|
/// XSD 1.0 `cos-valid-default` / `e-props-correct`: an element with a `default`
|
|
/// or `fixed` value constraint must have a content type that admits character
|
|
/// data, i.e. a simple type or mixed content. An element whose type is (or whose
|
|
/// inline complex type has) element-only or empty content may not carry a value
|
|
/// constraint. Conservative by construction: it flags only a content type that is
|
|
/// CERTAINLY element-only or empty. A simple type, mixed content, the ur-type
|
|
/// `xs:anyType` (mixed), an untyped element (also `anyType`), a complexContent
|
|
/// derivation (mixedness may be inherited), and a foreign or unresolved type are
|
|
/// all left alone, so this cannot raise a false positive. Self-contained schema.
|
|
static func elementValueConstraintContentFindings(
|
|
_ schema: XSDTree,
|
|
_ context: PureXML.Schema.XSDContext,
|
|
_ types: [String: PureXML.Schema.ElementType],
|
|
) -> [PureXML.Schema.SchemaLocatedFinding] {
|
|
guard !skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) else { return [] }
|
|
let bindings = context.namespaceBindings
|
|
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
|
for element in descendants(schema, named: "element") where element.name?.namespaceURI == xsdNamespace {
|
|
guard ContentDerivNode.attribute(element, "fixed") != nil || ContentDerivNode.attribute(element, "default") != nil,
|
|
valueConstraintForbiddenByContent(element, bindings, context, types)
|
|
else { continue }
|
|
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
|
reason: "an element with a 'default' or 'fixed' value must have simple or mixed content, not element-only or empty content",
|
|
node: element,
|
|
))
|
|
}
|
|
return findings
|
|
}
|
|
|
|
/// Whether `element`'s content type is certainly element-only or empty, so a
|
|
/// value constraint on it is invalid. Returns false (no error) for anything that
|
|
/// admits character data or that cannot be resolved with certainty.
|
|
private static func valueConstraintForbiddenByContent(
|
|
_ element: XSDTree,
|
|
_ bindings: [String: String],
|
|
_ context: PureXML.Schema.XSDContext,
|
|
_ types: [String: PureXML.Schema.ElementType],
|
|
) -> Bool {
|
|
if let inline = ContentDerivNode.firstChild(element, named: "complexType") {
|
|
if ContentDerivNode.firstChild(inline, named: "simpleContent") != nil {
|
|
return false
|
|
}
|
|
if ["true", "1"].contains(ContentDerivNode.attribute(inline, "mixed")) {
|
|
return false
|
|
}
|
|
if ContentDerivNode.firstChild(inline, named: "complexContent") != nil {
|
|
return false
|
|
}
|
|
return ["all", "sequence", "choice", "group"].contains { ContentDerivNode.firstChild(inline, named: $0) != nil }
|
|
}
|
|
guard let typeName = ContentDerivNode.attribute(element, "type") else { return false }
|
|
let namespace = ContentDerivNode.referenceNamespace(typeName, bindings)
|
|
let local = ContentDerivNode.stripPrefix(typeName)
|
|
if namespace == xsdNamespace {
|
|
return false
|
|
}
|
|
guard namespace == context.targetNamespace, case let .complex(complex)? = types[local] else { return false }
|
|
switch complex.content {
|
|
case .elementOnly, .empty: return true
|
|
case .simpleContent, .mixed: return false
|
|
}
|
|
}
|
|
|
|
/// XSD 1.0 `cos-ct-extends.1.4.3.2.2.1`: an explicit `mixed` on a
|
|
/// `complexContent` extension must match the base. If `mixed` is omitted, an
|
|
/// extension with no element particle inherits the base content unchanged
|
|
/// (`ctZ012b`), while an extension that adds element content uses the schema
|
|
/// default `false` (`ctZ010d`). Checked for a self-contained schema, and only
|
|
/// when the base content is definitely element-only or mixed (an empty or
|
|
/// simple-content base is governed by other rules and is left alone).
|
|
static func extensionMixedAgreementFindings(
|
|
_ schema: XSDTree,
|
|
_ context: PureXML.Schema.XSDContext,
|
|
_ types: [String: PureXML.Schema.ElementType],
|
|
) -> [PureXML.Schema.SchemaLocatedFinding] {
|
|
guard !skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) else { return [] }
|
|
let bindings = context.namespaceBindings
|
|
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
|
for content in descendants(schema, named: "complexContent") {
|
|
guard let ext = ContentDerivNode.firstChild(content, named: "extension"),
|
|
let base = ContentDerivNode.attribute(ext, "base"),
|
|
ContentDerivNode.referenceNamespace(base, bindings) == context.targetNamespace,
|
|
case let .complex(complex)? = types[ContentDerivNode.stripPrefix(base)]
|
|
else { continue }
|
|
let baseIsMixed: Bool
|
|
switch complex.content {
|
|
case .mixed: baseIsMixed = true
|
|
case .elementOnly: baseIsMixed = false
|
|
case .empty, .simpleContent: continue
|
|
}
|
|
guard let derivedMixed = declaredMixed(content, addsElementContent: extensionAddsElementContent(ext)) else { continue }
|
|
if baseIsMixed != derivedMixed {
|
|
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
|
reason: "a complexContent extension of '\(ContentDerivNode.stripPrefix(base))' must have the same mixed setting as its base",
|
|
node: ext,
|
|
))
|
|
}
|
|
}
|
|
return findings
|
|
}
|
|
|
|
/// The declared `mixed` value of a `complexContent` derivation. An omitted
|
|
/// value matters only when the extension adds element content; otherwise the
|
|
/// base content is inherited unchanged and there is no mixedness conflict.
|
|
private static func declaredMixed(_ complexContent: XSDTree, addsElementContent: Bool) -> Bool? {
|
|
if let own = ContentDerivNode.attribute(complexContent, "mixed") {
|
|
return own == "true" || own == "1"
|
|
}
|
|
if let parent = complexContent.parent, let outer = ContentDerivNode.attribute(parent, "mixed") {
|
|
return outer == "true" || outer == "1"
|
|
}
|
|
return addsElementContent ? false : nil
|
|
}
|
|
}
|