PureYAML/Sources/Parsing/TokenCursor.swift
Mihaela M 735767ced9 fix(parsing): reject inline nested block mappings
Block style permits only one mapping pair per line, so a scalar followed
by ':' after a block-level value on the same line (a: b: c, a: 'b': c,
x: { y: z }in: valid, or trailing content after a quoted value) is not a
new key but a forbidden inline nested mapping. The scanner synthesized an
implicit key for any scalar followed by ':', regardless of position, so
these parsed as nested mappings instead of failing.

The scanner now records when a line already holds a block-level mapping
pair and suppresses a further implicit key on that line, in plain and
quoted scalars alike. The flag is set only for an implicit-key ':' (one
preceded by its key on the same line): a ':' that opens its line is an
explicit- or null-key value indicator, whose value may be a compact
mapping (? k then : a: 1), so that, flow mappings, next-line nesting, and
colons inside scalars stay valid.

Measured against the official YAML test-suite, parse-level conformance
rises from 256/333 (76.9%) to 260/333 (78.1%) with no regression.
2026-06-27 18:28:57 +02:00

207 lines
5.8 KiB
Swift

extension PureYAML.Parsing {
private final class TokenScannerStateBox {
var state: Scanner.State
init(state: Scanner.State) {
self.state = state
}
}
struct TokenCursor {
private var tokens: [Token]
private(set) var index: Int
private var previousToken: Token?
private var scanner: Scanner?
private var scannerBox: TokenScannerStateBox?
init(tokens: [Token]) {
self.tokens = tokens.filter { !$0.kind.isComment }
index = 0
previousToken = nil
scanner = nil
scannerBox = nil
}
init(yaml: String, scanner: Scanner = .init()) {
var state = Scanner.State(reader: Reader(yaml))
state.append(.streamStart)
tokens = state.tokens.filter { !$0.kind.isComment }
state.tokens.removeAll(keepingCapacity: true)
index = 0
previousToken = nil
self.scanner = scanner
scannerBox = TokenScannerStateBox(state: state)
}
var current: Token? {
guard tokens.indices.contains(index) else {
return nil
}
return tokens[index]
}
var previous: Token? {
previousToken
}
mutating func peek(offset: Int = 1) throws -> Token? {
try ensureAvailable(offset: offset)
let targetIndex = index + offset
guard tokens.indices.contains(targetIndex) else {
return nil
}
return tokens[targetIndex]
}
mutating func advance() throws -> Token? {
try ensureAvailable(offset: 0)
guard tokens.indices.contains(index) else {
return nil
}
let token = tokens[index]
previousToken = token
index += 1
compactConsumedTokens()
try ensureAvailable(offset: 0)
return token
}
private mutating func ensureAvailable(offset: Int) throws {
while tokens.count <= index + offset, scannerBox != nil {
try scanMoreTokens()
}
}
private mutating func scanMoreTokens() throws {
guard let scanner, let scannerBox else {
return
}
while scannerBox.state.tokens.isEmpty {
guard !scannerBox.state.reader.isAtEnd else {
scanner.closeIndentation(&scannerBox.state)
try scanner.enforceDirectiveHasDocument(scannerBox.state)
scannerBox.state.append(.streamEnd)
drainScannedTokens(from: &scannerBox.state)
self.scannerBox = nil
return
}
if scanner.isLineBreak(scannerBox.state.reader.peek()) {
scannerBox.state.reader.advance()
scanner.captureBlockScalarBlankLine(&scannerBox.state)
scannerBox.state.isAtLineStart = true
scannerBox.state.sawContentOnLine = false
scannerBox.state.sawMappingValueOnLine = false
continue
}
if scannerBox.state.isAtLineStart {
try scanner.scanIndentation(&scannerBox.state)
if scannerBox.state.reader.isAtEnd {
continue
}
if scanner.isLineBreak(scannerBox.state.reader.peek()) {
continue
}
if try scanner.scanDirectiveOrDocumentMarker(&scannerBox.state) {
if !scannerBox.state.tokens.isEmpty {
break
}
continue
}
scannerBox.state.isAtLineStart = false
}
try scanner.scanToken(&scannerBox.state)
}
drainScannedTokens(from: &scannerBox.state)
}
private mutating func drainScannedTokens(from state: inout Scanner.State) {
tokens.append(contentsOf: state.tokens.filter { !$0.kind.isComment })
state.tokens.removeAll(keepingCapacity: true)
}
private mutating func compactConsumedTokens() {
guard index > 4096 else {
return
}
tokens.removeFirst(index)
index = 0
}
}
}
extension PureYAML.Parsing.TokenKind {
var isBlockEntry: Bool {
if case .blockEntry = self {
return true
}
return false
}
var isComment: Bool {
if case .comment = self {
return true
}
return false
}
var isDedent: Bool {
if case .dedent = self {
return true
}
return false
}
var isDocumentEnd: Bool {
if case .documentEnd = self {
return true
}
return false
}
var isDocumentStart: Bool {
if case .documentStart = self {
return true
}
return false
}
var isFlowTerminator: Bool {
switch self {
case .flowEntry, .flowMappingEnd, .flowSequenceEnd:
true
default:
false
}
}
var isIndent: Bool {
if case .indent = self {
return true
}
return false
}
var isMappingKey: Bool {
if case .mappingKey = self {
return true
}
return false
}
var isStreamEnd: Bool {
if case .streamEnd = self {
return true
}
return false
}
var isTerminator: Bool {
isDedent || isDocumentEnd || isDocumentStart || isFlowTerminator || isStreamEnd
}
}