PureXML/Sources/Schema/ComplexValidator.swift
Mihaela Mihaljevic be8a39394d fix(schema): advance XSTS valid-instances-rejected to 4 (#146)
Consolidates the in-progress #146 work; valid-instances-rejected reaches 4
on the 2006-11-06 XSTS archive (buckets now 1/262/4/147).

Regex: match Unicode scalars rather than grapheme clusters; the empty
pattern matches only the empty string; pin the category repertoire to
Unicode 3.2, the version the corpus is consistent at, so code points added
in 3.2 (Tagalog, Cyrillic Komi) are word characters while later additions
(the Tamil digit zero) stay unassigned and match \D. New XSDCategory and a
3.2 assignment table derived from DerivedAge, replacing the old Unicode31
files.

Content model: assess each child against the particle the content automaton
matched, not a by-name lookup (#180). Two same-named particles keep their
own fixed values, and an element matching a skip wildcard is no longer
validated against its global declaration. Selection is made reproducible
when an ambiguous schema slips past the UPA check.

XSTS runner: compile and merge every schemaDocument in a schemaTest, not
just the first; this also surfaced one previously-masked invalid-instance
acceptance (recorded in the baselines).

Also folds in attribute-group anyAttribute inheritance, extension wildcard
union, namespace-qualified type tables for multi-document
xsi:schemaLocation, per-container elementFormDefault, strict and lax
attribute wildcard validation, built-in type resolution, and xs:redefine
simpleType overrides.
2026-06-16 22:22:05 +02:00

341 lines
18 KiB
Swift

/// File-scope aliases for the XSD complex-type validator, kept out of the
/// namespace to avoid nesting a type two levels deep.
typealias XSDFailure = PureXML.Validation.ValidationError
typealias XSDPath = [PureXML.Validation.PathKey]
private struct XSDChildValidationFrame {
let path: XSDPath
let namespaceBindings: [String: String]
}
private typealias XSDGroup = PureXML.Schema.Group
private typealias XSDTerm = PureXML.Schema.Term
private typealias XSDTermLabel = PureXML.Schema.TermLabel
private typealias XSDProcessContents = PureXML.Schema.ProcessContents
private typealias XSDElementType = PureXML.Schema.ElementType
private typealias XSDWildcard = PureXML.Schema.Wildcard
public extension PureXML.Schema {
/// Validates an element against a ``ComplexType``: its attribute uses, its
/// content model, and, recursively, each child element against the type
/// declared for its name. Every violation is located by the element's coding
/// path, and the results are ``PureXML/Validation/ValidationError`` values.
struct ComplexValidator {
/// The named type table that `ElementType.typeReference` resolves against.
let types: [String: ElementType]
/// Local names of elements declared `nillable="true"`.
private let nillableElements: Set<String>
/// The `default`/`fixed` value constraint declared on each element name.
private let elementConstraints: [String: ValueConstraint]
/// Local names of complex types declared `abstract="true"`.
let abstractTypes: Set<String>
/// Derivation methods each named type forbids through `xsi:type`.
let typeBlock: [String: Set<DerivationMethod>]
/// Derivation methods each element declaration forbids through `xsi:type`
/// (the `block` on the element, distinct from the block on its type).
let elementBlock: [String: Set<DerivationMethod>]
/// Each named complex type's base and derivation method.
let typeDerivation: [String: TypeDerivation]
/// Global attribute declarations for strict/lax `anyAttribute` validation.
let globalAttributes: [String: AttributeUse]
/// Document-scoped xs:ID/xs:IDREF accumulator, filled during the typed walk
/// and reported by `idErrors()` once the whole tree has been seen. See the
/// ID/IDREF extension for `idErrors()` and `recordIDs(_:value:at:)`.
let idTracker = IDTracker()
public init(
types: [String: ElementType] = [:],
globalAttributes: [String: AttributeUse] = [:],
nillableElements: Set<String> = [],
elementConstraints: [String: ValueConstraint] = [:],
abstractTypes: Set<String> = [],
typeBlock: [String: Set<DerivationMethod>] = [:],
elementBlock: [String: Set<DerivationMethod>] = [:],
typeDerivation: [String: TypeDerivation] = [:],
) {
self.types = types
self.globalAttributes = globalAttributes
self.nillableElements = nillableElements
self.elementConstraints = elementConstraints
self.abstractTypes = abstractTypes
self.typeBlock = typeBlock
self.elementBlock = elementBlock
self.typeDerivation = typeDerivation
}
/// Validates `element` against `type` at `path`, one error per violation.
public func validate(
_ element: PureXML.Model.Element,
against type: ComplexType,
at path: [PureXML.Validation.PathKey] = [],
namespaceBindings inheritedBindings: [String: String] = [:],
) -> [PureXML.Validation.ValidationError] {
let namespaceBindings = Self.namespaceBindings(for: element, inherited: inheritedBindings)
var errors: [XSDFailure] = []
validateAttributes(element, type, at: path, into: &errors)
// An xsi:nil element keeps its attribute obligations but must have no
// content; its content model is not otherwise checked.
if let nilErrors = nilErrors(element, at: path) {
return errors + nilErrors
}
validateContent(element, type.content, at: path, namespaceBindings: namespaceBindings, into: &errors)
errors += elementFixedErrors(element, at: path)
return errors
}
/// The errors from an `xsi:nil="true"` element: rejecting it when the
/// element is not nillable, or when it carries content. Returns nil when
/// the element is not nilled.
func nilErrors(_ element: PureXML.Model.Element, at path: XSDPath) -> [XSDFailure]? {
guard Self.isNil(element) else { return nil }
let name = element.name.localName
if !nillableElements.contains(name) {
return [XSDFailure(reason: "element '\(name)' is not nillable but has xsi:nil", at: path)]
}
if Self.hasContent(element) {
return [XSDFailure(reason: "nil element '\(name)' must be empty", at: path)]
}
return []
}
/// The error from a `fixed` element value constraint: the element's text
/// must equal the fixed value in the element type's value space when known.
func elementFixedErrors(
_ element: PureXML.Model.Element,
valueType: SimpleType? = nil,
particleFixed: String? = nil,
at path: XSDPath,
) -> [XSDFailure] {
guard let fixed = particleFixed ?? elementConstraints[element.name.localName]?.fixedValue else { return [] }
let text = Self.rawTextContent(element)
// An empty element takes the fixed value; only present content must equal it.
if text.isEmpty { return [] }
if let valueType, valueType.valueMatches(text, literal: fixed) { return [] }
return text == fixed ? [] : [XSDFailure(reason: "element '\(element.name.localName)' is fixed and must be '\(fixed)'", at: path)]
}
/// Validates `element` against any element type at `path`, resolving a
/// `typeReference` through the type table.
public func validate(
_ element: PureXML.Model.Element,
as type: ElementType,
at path: [PureXML.Validation.PathKey] = [],
) -> [PureXML.Validation.ValidationError] {
var errors: [XSDFailure] = []
let namespaceBindings = Self.namespaceBindings(for: element)
validateChild(element, against: type, at: path, namespaceBindings: namespaceBindings, into: &errors)
return errors
}
// MARK: Content
private func validateContent(
_ element: PureXML.Model.Element,
_ content: ContentType,
at path: XSDPath,
namespaceBindings: [String: String],
into errors: inout [XSDFailure],
) {
let children = element.children.compactMap(\.element)
switch content {
case .empty:
if !children.isEmpty { errors.append(XSDFailure(reason: "element must be empty", at: path)) }
rejectText(element, at: path, into: &errors)
case let .simpleContent(type):
if !children.isEmpty { errors.append(XSDFailure(reason: "element must not have children", at: path)) }
if let error = type.validate(Self.rawTextContent(element)) {
errors.append(XSDFailure(reason: "content: \(error)", at: path))
}
case let .elementOnly(particle):
rejectText(element, at: path, into: &errors)
validateParticle(particle, children: children, at: path, namespaceBindings: namespaceBindings, into: &errors)
case let .mixed(particle):
validateParticle(particle, children: children, at: path, namespaceBindings: namespaceBindings, into: &errors)
}
}
func rejectText(_ element: PureXML.Model.Element, at path: XSDPath, into errors: inout [XSDFailure]) {
guard !Self.textContent(element).isEmpty else { return }
errors.append(XSDFailure(reason: "element must not contain text", at: path))
}
// MARK: Particles
private func validateParticle(
_ particle: Particle,
children: [PureXML.Model.Element],
at path: XSDPath,
namespaceBindings: [String: String],
into errors: inout [XSDFailure],
) {
// Locate each content-model violation at the offending child (or the
// missing one), with a recovery hint, rather than one opaque failure,
// then assess every well-placed child against the exact particle the
// content model matched it to (#180), not a by-name lookup that loses
// which of two same-named particles, or a wildcard versus a named
// particle, actually matched.
let matched: [PureXML.Schema.MatchedParticle?]
if case let .group(group) = particle.term, group.compositor == .all {
allStructureErrors(group, children: children, at: path, into: &errors)
matched = allMatchedParticles(group, children: children)
} else {
sequenceStructureErrors(particle, children: children, at: path, into: &errors)
matched = PureXML.Schema.ContentNFABuilder.build(particle).matchedParticles(children.map(\.name))
}
validateChildren(
children,
matched: matched,
frame: XSDChildValidationFrame(path: path, namespaceBindings: namespaceBindings),
into: &errors,
)
}
private func validateChildren(
_ children: [PureXML.Model.Element],
matched: [PureXML.Schema.MatchedParticle?],
frame: XSDChildValidationFrame,
into errors: inout [XSDFailure],
) {
let steps = Self.childSteps(children)
for (index, child) in children.enumerated() {
let namespaceBindings = Self.namespaceBindings(for: child, inherited: frame.namespaceBindings)
let path = frame.path + [steps[index]]
switch matched[index] {
case let .element(type, valueConstraint):
// An element particle without an inlined type (a bare ref)
// resolves through its global declaration, as before.
guard let declared = type ?? globalElementDeclaration(for: child.name) else { continue }
validateChild(child, against: declared, at: path, namespaceBindings: namespaceBindings, particleConstraint: valueConstraint, into: &errors)
case let .wildcard(wildcard):
validateWildcardChild(child, processContents: wildcard.processContents, at: path, namespaceBindings: namespaceBindings, into: &errors)
case .none:
// The content model rejected this child (a structure error,
// already located above); the matched particle is undefined.
continue
}
}
}
/// Validates a wildcard-matched child by its `processContents`: skip does
/// nothing, lax validates against a global declaration when one exists, and
/// strict requires one.
private func globalElementDeclaration(for name: PureXML.Model.QualifiedName) -> PureXML.Schema.ElementType? {
if let declaration = types[PureXML.Schema.XSDParser.elementDeclarationKey(name)] {
return declaration
}
guard name.namespaceURI == nil else { return nil }
return types["element:\(name.localName)"]
}
private func validateWildcardChild(
_ child: PureXML.Model.Element,
processContents: ProcessContents,
at path: XSDPath,
namespaceBindings: [String: String],
into errors: inout [XSDFailure],
) {
switch processContents {
case .skip:
return
case .lax:
if let declaration = globalElementDeclaration(for: child.name) {
validateChild(child, against: declaration, at: path, namespaceBindings: namespaceBindings, into: &errors)
}
case .strict:
if let declaration = globalElementDeclaration(for: child.name) {
validateChild(child, against: declaration, at: path, namespaceBindings: namespaceBindings, into: &errors)
} else if let type = xsiDeclaredType(for: child, namespaceBindings: namespaceBindings) {
validateChild(child, against: type, at: path, namespaceBindings: namespaceBindings, into: &errors)
} else {
errors.append(XSDFailure(reason: "no declaration for wildcard-matched element '\(child.name.localName)'", at: path))
}
}
}
/// The type named by a wildcard-matched element's `xsi:type`, when present.
private func xsiDeclaredType(for child: PureXML.Model.Element, namespaceBindings: [String: String]) -> ElementType? {
guard Self.xsiTypeAttributeValue(child) != nil else { return nil }
guard let reference = Self.xsiTypeReference(child, namespaceBindings: namespaceBindings),
let type = Self.resolveNamedType(reference, in: types)
else { return nil }
return type
}
private func validateChild(
_ child: PureXML.Model.Element,
against declared: ElementType,
at path: XSDPath,
namespaceBindings: [String: String],
particleConstraint: ValueConstraint? = nil,
into errors: inout [XSDFailure],
) {
// An instance `xsi:type` overrides the declared type, provided the named
// type exists in the schema. Resolve to the type itself rather than a
// reference so re-entry does not re-read the same xsi:type. A `block` on
// the declared type can forbid the substitution; an abstract declared
// type requires one.
if let blocked = blockedSubstitutionError(declared: declared, child: child, at: path) {
errors.append(blocked)
return
}
if case let .typeReference(name) = declared, let missing = abstractTypeError(named: name, child: child, at: path) {
errors.append(missing)
return
}
guard let declared = overriddenType(declared, for: child, namespaceBindings: namespaceBindings, at: path, into: &errors) else { return }
switch declared {
case let .simple(simple):
// A simple type declares no attribute uses, so a simple-typed
// element (including one whose simple type comes from an xsi:type
// override, and including a nilled one) may carry only the
// schema-instance and namespace attributes; any other is a
// violation. Checked before the nil short-circuit, since an
// xsi:nil element is equally bound by it.
for attribute in child.attributes where !Self.isNamespaceDeclaration(attribute) && !Self.isSchemaInstanceAttribute(attribute) {
errors.append(XSDFailure(reason: "'\(child.name.localName)' has a simple type and must not carry attribute '\(attribute.name.localName)'", at: path))
}
if let nilErrors = nilErrors(child, at: path) {
errors += nilErrors
return
}
if !child.children.compactMap(\.element).isEmpty {
errors.append(XSDFailure(reason: "'\(child.name.localName)' must not have children", at: path))
}
// An empty element takes its `default`/`fixed` value; that value
// (not the empty string) is what must be valid against the type,
// including any xsi:type override already resolved into `simple`.
let text = Self.rawTextContent(child)
let constraint = particleConstraint ?? elementConstraints[child.name.localName]
let effective = text.isEmpty ? (constraint?.value ?? text) : text
if let error = simple.validate(effective) {
errors.append(XSDFailure(reason: "'\(child.name.localName)': \(error)", at: path))
}
recordIDs(simple, value: text, at: path)
errors += elementFixedErrors(child, valueType: simple, particleFixed: particleConstraint?.fixedValue, at: path)
case let .complex(complex):
errors.append(contentsOf: validate(child, against: complex, at: path, namespaceBindings: namespaceBindings))
case .typeReference:
validateResolvedReference(declared, child, at: path, namespaceBindings: namespaceBindings, into: &errors)
}
}
/// Validates a child against a `typeReference` through the shared resolver,
/// reporting an unknown name or a circular chain as a located error.
private func validateResolvedReference(
_ declared: ElementType,
_ child: PureXML.Model.Element,
at path: XSDPath,
namespaceBindings: [String: String],
into errors: inout [XSDFailure],
) {
switch resolveReference(declared) {
case let .unknown(name):
errors.append(XSDFailure(reason: "unknown type '\(name)'", at: path))
case let .circular(name):
errors.append(XSDFailure(reason: "circular type reference '\(name)'", at: path))
case let .resolved(resolved):
validateChild(child, against: resolved, at: path, namespaceBindings: namespaceBindings, into: &errors)
}
}
}
}