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.
133 lines
5.2 KiB
Swift
133 lines
5.2 KiB
Swift
extension PureYAML.Parsing.Scanner {
|
|
func scanIndentation(_ state: inout State) throws {
|
|
let start = state.reader.mark
|
|
let width = try scanIndentationWidth(&state)
|
|
guard shouldProcessIndentation(state) else {
|
|
return
|
|
}
|
|
|
|
// Mirror libyaml unrolling the block indent to the line's column before any
|
|
// token on the line is scanned, so a dedented line restores the enclosing
|
|
// collection's indent. A continuation of a plain scalar never reaches here
|
|
// (it is consumed inside the scalar scanner), so this only fires at genuine
|
|
// block-line starts.
|
|
unrollBlockIndent(toColumn: width, state: &state)
|
|
|
|
let end = state.reader.mark
|
|
let current = state.indentation.last ?? 0
|
|
|
|
if width > current {
|
|
applyIncreasedIndentation(width: width, start: start, end: end, state: &state)
|
|
} else if width < current {
|
|
try applyDedentation(width: width, start: start, end: end, state: &state)
|
|
}
|
|
|
|
pruneImplicitIndentation(&state)
|
|
}
|
|
|
|
func scanIndentationWidth(_ state: inout State) throws -> Int {
|
|
var width = 0
|
|
while state.reader.peek() == " " {
|
|
width += 1
|
|
state.reader.advance()
|
|
}
|
|
// Block indentation (s-indent) is spaces only: a tab is never indentation
|
|
// (scanner.c's scan_to_next_token eats only spaces unconditionally). The tab
|
|
// is left under the reader so the token scanner can decide whether it is legal
|
|
// in-line separation (before a scalar value or flow node) or an illegal tab
|
|
// that supplies the indentation of a nested block node; `rejectTabSeparation`
|
|
// and the blank-line handling in `scan` make that call. A whitespace-only line
|
|
// (spaces and/or tabs followed by a break) is a blank line and carries no
|
|
// indentation token.
|
|
return width
|
|
}
|
|
|
|
func shouldProcessIndentation(_ state: State) -> Bool {
|
|
guard !state.reader.isAtEnd, !isLineBreak(state.reader.peek()) else {
|
|
return false
|
|
}
|
|
// A leading '#' starts a comment line, which must not drive indent/dedent
|
|
// processing. Block scalar content (including lines that begin with '#') is
|
|
// consumed in full by the block scalar scanner before control returns here,
|
|
// so this check no longer needs to special-case block scalars.
|
|
if state.reader.peek() == "#" {
|
|
return false
|
|
}
|
|
return state.flowDepth == 0
|
|
}
|
|
|
|
func applyIncreasedIndentation(
|
|
width: Int,
|
|
start: PureYAML.Parsing.Mark,
|
|
end: PureYAML.Parsing.Mark,
|
|
state: inout State,
|
|
) {
|
|
state.indentation.append(width)
|
|
state.append(.indent(width: width), mark: start, endMark: end)
|
|
}
|
|
|
|
func applyDedentation(
|
|
width: Int,
|
|
start: PureYAML.Parsing.Mark,
|
|
end: PureYAML.Parsing.Mark,
|
|
state: inout State,
|
|
) throws {
|
|
while state.indentation.count > 1, width < (state.indentation.last ?? 0) {
|
|
state.indentation.removeLast()
|
|
state.append(.dedent(width: state.indentation.last ?? 0), mark: end, endMark: end)
|
|
}
|
|
if canUseImplicitIndentation(width: width, state: state) {
|
|
state.indentation.append(width)
|
|
state.append(.indent(width: width), mark: start, endMark: end)
|
|
}
|
|
guard width == state.indentation.last else {
|
|
throw PureYAML.Parsing.ParseError.unexpectedIndentation(line: end.line)
|
|
}
|
|
}
|
|
|
|
func canUseImplicitIndentation(
|
|
width: Int,
|
|
state: State,
|
|
) -> Bool {
|
|
width != state.indentation.last
|
|
&& width > (state.indentation.last ?? 0)
|
|
&& state.validImplicitIndentation.contains(width)
|
|
}
|
|
|
|
func pruneImplicitIndentation(_ state: inout State) {
|
|
let current = state.indentation.last ?? 0
|
|
state.validImplicitIndentation = state.validImplicitIndentation.filter { $0 <= current }
|
|
}
|
|
|
|
/// Mirrors libyaml's `yaml_parser_roll_indent`: when a block collection opens at
|
|
/// `column` (0-based) deeper than the current block indent, push the old indent
|
|
/// and adopt the new one. No-op in flow context, matching the C scanner.
|
|
func rollBlockIndent(toColumn column: Int, state: inout State) {
|
|
guard state.flowDepth == 0, state.blockIndent < column else {
|
|
return
|
|
}
|
|
state.blockIndents.append(state.blockIndent)
|
|
state.blockIndent = column
|
|
}
|
|
|
|
/// Mirrors libyaml's `yaml_parser_unroll_indent`: pop block indents until the
|
|
/// current block indent is at or below `column`. Called at each block-context
|
|
/// line start so the block indent tracks the line's actual nesting.
|
|
func unrollBlockIndent(toColumn column: Int, state: inout State) {
|
|
guard state.flowDepth == 0 else {
|
|
return
|
|
}
|
|
while state.blockIndent > column {
|
|
state.blockIndent = state.blockIndents.popLast() ?? -1
|
|
}
|
|
}
|
|
|
|
func closeIndentation(_ state: inout State) {
|
|
let mark = state.reader.mark
|
|
while state.indentation.count > 1 {
|
|
state.indentation.removeLast()
|
|
state.append(.dedent(width: state.indentation.last ?? 0), mark: mark, endMark: mark)
|
|
}
|
|
}
|
|
}
|