The gap-only manifest stopped guarding a case once it was fixed, so later work silently regressed previously-fixed cases (caught only by an external full-suite run, not the gated sweep or full corpus). Replace it with a full manifest of all 402 official cases (Tests/Fixtures/yaml-test-suite-manifest.yaml), each carrying its conformant expectation and a passing/gap status, with every case's files vendored. The gated sweep now asserts every passing case still passes (the regression guard) and every gap still reproduces, so any regression fails the build. The honest gate-enforced count is 329/402 passing (the earlier 348 came from a more lenient one-time oracle). Repairs three regressions the new gate revealed: the bucket-6 tab rule over-rejected tabs in block-scalar content and as separation before a scalar value (root cause: the batch scanner and the incremental TokenCursor had divergent line-start loops; unified onto a single scanStep, fixing 96NN/00, 96NN/01, R4YG, Y79Y/001, DK95/00, DK95/03, DK95/07, 6CA3, Q5MG, J3BT); an anchor on a prior line before an alias key wrongly rejected 26DV; and a tag-only flow key mis-parsed WZ62. Genuine tab-indentation errors (4EJS, Y79Y/000) still reject.
86 lines
3.5 KiB
Swift
86 lines
3.5 KiB
Swift
extension PureYAML.Parsing.Scanner {
|
|
/// Result of one scanning iteration over the source. Both the batch ``scan``
|
|
/// loop and the incremental ``PureYAML/Parsing/TokenCursor`` driver share the
|
|
/// same step so the two paths cannot diverge (a divergence is what once let a
|
|
/// tab-indentation error slip past the streaming parser while the batch scanner
|
|
/// caught it).
|
|
enum Step {
|
|
/// Nothing remains; the caller appends stream end and stops.
|
|
case endOfInput
|
|
/// A line break, blank line, or comment was consumed; restart the loop.
|
|
case advanced
|
|
/// A directive or document marker was scanned; restart the loop.
|
|
case scannedMarker
|
|
/// An ordinary token was scanned.
|
|
case scannedToken
|
|
}
|
|
|
|
/// Performs one scanning iteration: consumes a leading line break or blank line,
|
|
/// scans indentation at a line start, then scans either a directive / document
|
|
/// marker or one ordinary token. Resets `simpleKeyAllowed` at every block-context
|
|
/// line break so a tab at the next line start is recognized as (illegal)
|
|
/// indentation rather than (legal) in-line separation.
|
|
func scanStep(_ state: inout State) throws -> Step {
|
|
if state.reader.isAtEnd {
|
|
return .endOfInput
|
|
}
|
|
if isLineBreak(state.reader.peek()) {
|
|
state.reader.advance()
|
|
state.isAtLineStart = true
|
|
if state.flowDepth == 0 {
|
|
state.simpleKeyAllowed = true
|
|
}
|
|
return .advanced
|
|
}
|
|
|
|
if state.isAtLineStart {
|
|
// A line whose entire content is blanks (spaces and/or tabs) before the
|
|
// break is a blank line: it carries no indentation and no token, matching
|
|
// scanner.c's scan_to_next_token, which eats the blanks and the break
|
|
// before any indent processing. Consuming it here keeps a whitespace-only
|
|
// line (e.g. `· \t`) from emitting a spurious indent token or being
|
|
// mis-scanned as content.
|
|
if consumeBlankLineRemainder(&state) {
|
|
return .advanced
|
|
}
|
|
try scanIndentation(&state)
|
|
if state.reader.isAtEnd || isLineBreak(state.reader.peek()) {
|
|
return .advanced
|
|
}
|
|
if try scanDirectiveOrDocumentMarker(&state) {
|
|
return .scannedMarker
|
|
}
|
|
state.isAtLineStart = false
|
|
}
|
|
|
|
try scanToken(&state)
|
|
return .scannedToken
|
|
}
|
|
|
|
/// When the reader sits on a run of blanks (spaces and/or tabs) that ends the
|
|
/// line (a line break or end of input follows), consumes that run plus the line
|
|
/// break and reports true: the line was blank. Otherwise leaves the reader where
|
|
/// it was and reports false. Used so a whitespace-only line (e.g. `· \t`) is
|
|
/// skipped rather than mis-scanned as plain-scalar content.
|
|
func consumeBlankLineRemainder(_ state: inout State) -> Bool {
|
|
var offset = 0
|
|
while let character = state.reader.peek(offset: offset), character == " " || character == "\t" {
|
|
offset += 1
|
|
}
|
|
let next = state.reader.peek(offset: offset)
|
|
guard next == nil || isLineBreak(next) else {
|
|
return false
|
|
}
|
|
for _ in 0 ..< offset {
|
|
state.reader.advance()
|
|
}
|
|
if isLineBreak(state.reader.peek()) {
|
|
state.reader.advance()
|
|
if state.flowDepth == 0 {
|
|
state.simpleKeyAllowed = true
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
}
|