PureXML/Sources/Schema/RelaxNGRestrictionOverlap.swift
Mihaela Mihaljevic 3415ff3d9a feat(relaxng): schema-correctness validation, spec suite complete
A RELAX NG schema document is validated before pattern interpretation
in three layers: the section-3 grammar check (vocabulary, attributes,
arity, text, QNames, datatypeLibrary URIs, hrefs, 4.16 excepts, xmlns
prohibitions), compiler checks (4.17 combine rules, unresolved refs,
parentRef scope, startless grammars, include/externalRef cycles and
overrides, grammar-checked loaded documents, unknown datatypes,
built-in-library params), and the 4.19/7.1-7.4 restrictions over the
compiled algebra after 4.20/4.21 normalization (prohibited paths,
content types, duplicate attributes via representative-name overlap,
infinite-class attributes outside oneOrMore, interleave constraints),
with recursion detection deliberately before normalization. Incorrect
schemas: 213 compiling down to 7, all the pre-Fifth-Edition name-class
class (count asserted). Two leniency tests updated to spec behavior.

Closes #131
2026-06-10 11:38:45 +02:00

132 lines
6.3 KiB
Swift

/// The attribute and interleave restrictions (7.3, 7.4) with the
/// representative-name overlap test for name classes.
extension RelaxNGRestrictionChecker {
// MARK: 7.3 duplicate attributes
func attributeSets(_ pattern: Pattern, visiting: Set<String>) -> (classes: [NameClass], problem: String?) {
switch pattern {
case let .attribute(nameClass, _):
([nameClass], nil)
case let .choice(lhs, rhs):
combinedAttributeSets(lhs, rhs, crossCheck: false, visiting: visiting)
case let .group(lhs, rhs), let .interleave(lhs, rhs):
combinedAttributeSets(lhs, rhs, crossCheck: true, visiting: visiting)
case let .oneOrMore(inner), let .list(inner):
attributeSets(inner, visiting: visiting)
case let .ref(name):
visiting.contains(name)
? ([], nil)
: attributeSets(resolve(name), visiting: visiting.union([name]))
default:
([], nil)
}
}
/// Merges two operands' attribute classes; group and interleave require
/// the operands' classes to be pairwise disjoint (7.3).
private func combinedAttributeSets(_ lhs: Pattern, _ rhs: Pattern, crossCheck: Bool, visiting: Set<String>) -> (classes: [NameClass], problem: String?) {
let left = attributeSets(lhs, visiting: visiting)
if left.problem != nil { return left }
let right = attributeSets(rhs, visiting: visiting)
if right.problem != nil { return right }
if crossCheck {
for first in left.classes {
for second in right.classes where nameClassesOverlap(first, second) {
return ([], "the same attribute may occur twice in one element (7.3)")
}
}
}
return (left.classes + right.classes, nil)
}
// MARK: 7.4 interleave
func interleaveProblem(_ pattern: Pattern, visiting: Set<String>) -> String? {
switch pattern {
case let .interleave(lhs, rhs):
if hasText(lhs, visiting: visiting), hasText(rhs, visiting: visiting) {
return "text may appear in only one operand of an interleave (7.4)"
}
for first in elementClasses(lhs, visiting: visiting) {
for second in elementClasses(rhs, visiting: visiting) where nameClassesOverlap(first, second) {
return "interleave operands share an element name (7.4)"
}
}
return interleaveProblem(lhs, visiting: visiting) ?? interleaveProblem(rhs, visiting: visiting)
case let .choice(lhs, rhs), let .group(lhs, rhs):
return interleaveProblem(lhs, visiting: visiting) ?? interleaveProblem(rhs, visiting: visiting)
case let .oneOrMore(inner), let .list(inner), let .attribute(_, inner), let .dataExcept(_, inner):
return interleaveProblem(inner, visiting: visiting)
case let .ref(name):
guard !visiting.contains(name) else { return nil }
return interleaveProblem(resolve(name), visiting: visiting.union([name]))
default:
return nil
}
}
private func hasText(_ pattern: Pattern, visiting: Set<String>) -> Bool {
switch pattern {
case .text:
return true
case let .choice(lhs, rhs), let .group(lhs, rhs), let .interleave(lhs, rhs):
return hasText(lhs, visiting: visiting) || hasText(rhs, visiting: visiting)
case let .oneOrMore(inner):
return hasText(inner, visiting: visiting)
case let .ref(name):
guard !visiting.contains(name) else { return false }
return hasText(resolve(name), visiting: visiting.union([name]))
default:
return false
}
}
private func elementClasses(_ pattern: Pattern, visiting: Set<String>) -> [NameClass] {
switch pattern {
case let .element(nameClass, _):
return [nameClass]
case let .choice(lhs, rhs), let .group(lhs, rhs), let .interleave(lhs, rhs):
return elementClasses(lhs, visiting: visiting) + elementClasses(rhs, visiting: visiting)
case let .oneOrMore(inner):
return elementClasses(inner, visiting: visiting)
case let .ref(name):
guard !visiting.contains(name) else { return [] }
return elementClasses(resolve(name), visiting: visiting.union([name]))
default:
return []
}
}
// MARK: Name-class overlap
/// Overlap by representative names: every concrete name either class
/// mentions, plus an illustrative name per nsName namespace and one in
/// a namespace neither could mention, tested against both classes.
private func namesClassRepresentatives(_ nameClass: NameClass, into set: inout [PureXML.Model.QualifiedName]) {
switch nameClass {
case let .name(namespace, localName):
set.append(PureXML.Model.QualifiedName(prefix: nil, localName: localName, namespaceURI: namespace.isEmpty ? nil : namespace))
case let .nsName(namespace):
set.append(PureXML.Model.QualifiedName(prefix: nil, localName: "\u{1}illustrative", namespaceURI: namespace.isEmpty ? nil : namespace))
case let .nsNameExcept(namespace, except):
set.append(PureXML.Model.QualifiedName(prefix: nil, localName: "\u{1}illustrative", namespaceURI: namespace.isEmpty ? nil : namespace))
namesClassRepresentatives(except, into: &set)
case .anyName:
set.append(PureXML.Model.QualifiedName(prefix: nil, localName: "\u{1}illustrative", namespaceURI: "\u{1}nowhere"))
case let .anyNameExcept(except):
set.append(PureXML.Model.QualifiedName(prefix: nil, localName: "\u{1}illustrative", namespaceURI: "\u{1}nowhere"))
namesClassRepresentatives(except, into: &set)
case let .choice(lhs, rhs):
namesClassRepresentatives(lhs, into: &set)
namesClassRepresentatives(rhs, into: &set)
}
}
func nameClassesOverlap(_ first: NameClass, _ second: NameClass) -> Bool {
var candidates: [PureXML.Model.QualifiedName] = []
namesClassRepresentatives(first, into: &candidates)
namesClassRepresentatives(second, into: &candidates)
return candidates.contains { first.contains($0) && second.contains($0) }
}
}