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.
96 lines
4.9 KiB
Swift
96 lines
4.9 KiB
Swift
/// Located content-model diagnostics: pinpoint which child breaks the model and
|
|
/// what was expected there, so an editor shows placed errors with recovery hints
|
|
/// rather than one opaque "content does not match" per element.
|
|
extension PureXML.Schema.ComplexValidator {
|
|
/// Walks the children through the content automaton, flagging the first child
|
|
/// the follow-set rejects, or the missing content when the sequence ends early.
|
|
func sequenceStructureErrors(_ particle: PureXML.Schema.Particle, children: [PureXML.Model.Element], at path: XSDPath, into errors: inout [XSDFailure]) {
|
|
let nfa = PureXML.Schema.ContentNFABuilder.build(particle)
|
|
let steps = Self.childSteps(children)
|
|
// Advance one active state-set across the children rather than re-walking
|
|
// the prefix per child (which is quadratic over the content model, #129).
|
|
var current = nfa.startStates()
|
|
for (index, child) in children.enumerated() {
|
|
guard let next = nfa.step(current, over: child.name) else {
|
|
let allowed = nfa.admissible(from: current)
|
|
errors.append(XSDFailure(reason: "element '\(child.name.localName)' is not allowed here\(Self.expectation(allowed))", at: path + [steps[index]]))
|
|
return
|
|
}
|
|
current = next
|
|
}
|
|
if !nfa.isComplete(current) {
|
|
errors.append(XSDFailure(reason: "content is incomplete\(Self.expectation(nfa.admissible(from: current)))", at: path))
|
|
}
|
|
}
|
|
|
|
/// Locates `all`-group violations: each child that is not an in-bounds member,
|
|
/// recovering past it, then each required member that never appeared.
|
|
func allStructureErrors(_ group: PureXML.Schema.Group, children: [PureXML.Model.Element], at path: XSDPath, into errors: inout [XSDFailure]) {
|
|
var counts = [Int](repeating: 0, count: group.particles.count)
|
|
let steps = Self.childSteps(children)
|
|
for (index, child) in children.enumerated() {
|
|
guard let position = group.particles.indices.first(where: { slot in
|
|
let member = group.particles[slot]
|
|
let room = member.maxOccurs.map { counts[slot] < $0 } ?? true
|
|
return room && Self.memberMatches(member.term, child.name)
|
|
}) else {
|
|
errors.append(XSDFailure(reason: "element '\(child.name.localName)' is not allowed here", at: path + [steps[index]]))
|
|
continue
|
|
}
|
|
counts[position] += 1
|
|
}
|
|
for (index, member) in group.particles.enumerated() where counts[index] < member.minOccurs {
|
|
if case let .element(name, _, _, _) = member.term {
|
|
errors.append(XSDFailure(reason: "element '\(name.localName)' is required but missing", at: path))
|
|
}
|
|
}
|
|
}
|
|
|
|
static func memberMatches(_ term: PureXML.Schema.Term, _ name: PureXML.Model.QualifiedName) -> Bool {
|
|
switch term {
|
|
case let .element(declared, _, _, _): declared.localName == name.localName && declared.namespaceURI == name.namespaceURI
|
|
case let .wildcard(wildcard): wildcard.admits(name)
|
|
case .group: false
|
|
}
|
|
}
|
|
|
|
/// The particle each child of an `all` group matched, for per-child
|
|
/// assessment. `all` is matched by counting, not by the automaton, so it has
|
|
/// its own pass mirroring ``allStructureErrors``.
|
|
func allMatchedParticles(_ group: PureXML.Schema.Group, children: [PureXML.Model.Element]) -> [PureXML.Schema.MatchedParticle?] {
|
|
var counts = [Int](repeating: 0, count: group.particles.count)
|
|
var result: [PureXML.Schema.MatchedParticle?] = []
|
|
for child in children {
|
|
guard let slot = group.particles.indices.first(where: { index in
|
|
let member = group.particles[index]
|
|
let room = member.maxOccurs.map { counts[index] < $0 } ?? true
|
|
return room && Self.memberMatches(member.term, child.name)
|
|
}) else {
|
|
result.append(nil)
|
|
continue
|
|
}
|
|
counts[slot] += 1
|
|
result.append(Self.matchedParticle(of: group.particles[slot].term))
|
|
}
|
|
return result
|
|
}
|
|
|
|
static func matchedParticle(of term: PureXML.Schema.Term) -> PureXML.Schema.MatchedParticle? {
|
|
switch term {
|
|
case let .element(_, type, _, valueConstraint): .element(type: type, valueConstraint: valueConstraint)
|
|
case let .wildcard(wildcard): .wildcard(wildcard)
|
|
case .group: nil
|
|
}
|
|
}
|
|
|
|
/// A "; expected a, b" hint naming the elements the automaton accepts next.
|
|
static func expectation(_ labels: [PureXML.Schema.TermLabel]) -> String {
|
|
let names = labels.compactMap { label -> String? in
|
|
if case let .name(qualified) = label { return "<\(qualified.localName)>" }
|
|
return nil
|
|
}
|
|
let unique = Set(names).sorted()
|
|
return unique.isEmpty ? "" : "; expected \(unique.joined(separator: ", "))"
|
|
}
|
|
}
|