PureXML/Sources/Schema/SchemaContentDerivation.swift
Mihaela Mihaljevic 2b465e9c9c feat(schema): locate content-derivation-pass schema diagnostics
Convert the seven checkers the complexExtensionBaseValid compile rule
dispatches (the cos-ct-extends/src-ct content-derivation family) to located
findings so each error attaches to its derivation node instead of the schema
root (#169): a complexContent extension adding element content over a
simpleContent base, a simpleContent restriction whose base is a built-in or
user simple type, an unfaithful inline type in a simpleContent restriction, a
complexContent base that is a simple type, a simpleContent extension whose
base is xs:anyType, and a mismatched mixed setting on a complexContent
extension each underline their extension/restriction node; an element
carrying a default/fixed value over element-only or empty content underlines
the element.

Each predicate, iteration order, and reason string is unchanged; only the
location each finding carries differs. The seven checkers had exactly one
caller each (the dispatch), so each is renamed in place and the .unlocated
wrap dropped. To keep the file under the 400-line limit, the seven functions
and their two private helpers move to a new SchemaContentDerivation.swift;
the shared extensionAddsElementContent helper, still used by the all-group
checker that stays behind, becomes module-internal. Adds a located-
diagnostics test per checker.
2026-06-21 17:29:41 +02:00

295 lines
17 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") {
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 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
}
}