PureXML/Sources/XPath/Compiler.swift
Mihaela Mihaljevic 380160cc2b refactor(validation): decompose XSD instance validation into composable rules
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.
2026-07-12 14:12:17 +02:00

209 lines
6.7 KiB
Swift

extension PureXML.XPath {
/// Compiles an XPath expression into an ``Expression`` tree. A recursive
/// descent parser whose grammatical position disambiguates the tokens XPath
/// overloads: `*` is a wildcard in a node test but multiplication after an
/// operand, and `div`/`mod`/`and`/`or` are operator names only where an
/// operator is expected. The path, primary, and token parsing live in a
/// companion file. Reimplemented from the XPath 1.0 grammar.
struct Compiler {
let chars: [Character]
var index = 0
private init(_ path: String) {
chars = Array(path)
}
static func compile(_ path: String) throws -> Expression {
var compiler = Compiler(path)
compiler.skipSpace()
guard !compiler.isAtEnd else { throw QueryError.empty }
let expression = try compiler.parseExpression()
compiler.skipSpace()
guard compiler.isAtEnd else {
throw QueryError.unexpectedToken(String(compiler.peek() ?? " "))
}
return expression
}
// MARK: Operator precedence ladder
mutating func parseExpression() throws -> Expression {
try parseOr()
}
private mutating func parseOr() throws -> Expression {
var left = try parseAnd()
while peekKeyword("or") {
advance(by: 2)
left = try .binary(.logicalOr, left, parseAnd())
}
return left
}
private mutating func parseAnd() throws -> Expression {
var left = try parseEquality()
while peekKeyword("and") {
advance(by: 3)
left = try .binary(.logicalAnd, left, parseEquality())
}
return left
}
private mutating func parseEquality() throws -> Expression {
var left = try parseRelational()
while true {
skipSpace()
if consume("!=") {
left = try .binary(.notEqual, left, parseRelational())
} else if peek() == "=" {
advance()
left = try .binary(.equal, left, parseRelational())
} else {
return left
}
}
}
private mutating func parseRelational() throws -> Expression {
var left = try parseAdditive()
while let look = relationalOperator() {
left = try .binary(look, left, parseAdditive())
}
return left
}
private mutating func relationalOperator() -> BinaryOperator? {
skipSpace()
if consume("<=") {
return .lessOrEqual
}
if consume(">=") {
return .greaterOrEqual
}
if peek() == "<" {
advance()
return .lessThan
}
if peek() == ">" {
advance()
return .greaterThan
}
return nil
}
private mutating func parseAdditive() throws -> Expression {
var left = try parseMultiplicative()
while true {
skipSpace()
if peek() == "+" {
advance()
left = try .binary(.add, left, parseMultiplicative())
} else if peek() == "-" {
advance()
left = try .binary(.subtract, left, parseMultiplicative())
} else {
return left
}
}
}
private mutating func parseMultiplicative() throws -> Expression {
var left = try parseUnary()
while let look = multiplicativeOperator() {
left = try .binary(look, left, parseUnary())
}
return left
}
private mutating func multiplicativeOperator() -> BinaryOperator? {
skipSpace()
if peek() == "*" {
advance()
return .multiply
}
if peekKeyword("div") {
advance(by: 3)
return .divide
}
if peekKeyword("mod") {
advance(by: 3)
return .modulo
}
return nil
}
private mutating func parseUnary() throws -> Expression {
skipSpace()
if peek() == "-" {
advance()
return try .negate(parseUnary())
}
return try parseUnion()
}
private mutating func parseUnion() throws -> Expression {
var left = try parsePath()
while true {
skipSpace()
guard peek() == "|" else { return left }
advance()
left = try .union(left, parsePath())
}
}
// MARK: Cursor
mutating func expect(_ character: Character) throws {
guard peek() == character else {
throw QueryError.unexpectedToken(String(peek() ?? " "))
}
advance()
}
var isAtEnd: Bool {
index >= chars.count
}
func peek(_ ahead: Int = 0) -> Character? {
let target = index + ahead
return target < chars.count ? chars[target] : nil
}
/// Whether `word` appears next as a whole token (not followed by a name
/// character), used to read operator keywords in operator position.
func peekKeyword(_ word: String) -> Bool {
guard matches(word) else { return false }
if let after = peek(word.count), after.isXMLNameContinuation {
return false
}
return true
}
func matches(_ literal: String) -> Bool {
let target = Array(literal)
guard index + target.count <= chars.count else { return false }
for (offset, character) in target.enumerated() where chars[index + offset] != character {
return false
}
return true
}
@discardableResult
mutating func consume(_ literal: String) -> Bool {
guard matches(literal) else { return false }
index += literal.count
return true
}
mutating func advance(by count: Int = 1) {
index = Swift.min(index + count, chars.count)
}
mutating func skipSpace() {
while let character = peek(), character == " " || character == "\t" || character == "\n" || character == "\r" {
advance()
}
}
}
}