fix(parsing): reject a misindented multi-line quoted scalar continuation

A quoted scalar's continuation line must be indented past the parent
block, so '"a' / 'b"' (continuation in column 0) and '"bar' / '\tbaz'
(tab indentation) are malformed; libyaml accepts both.

Record the indentation error during the continuation scan but defer
raising it until the closing quote is found. An unterminated quote reaches
end of input first and still reports the missing quote (the root cause)
rather than the indentation symptom, preserving the multi-error diagnostic.

Surpasses libyaml on QB6E and DK95/01. Whole-suite conformance is now
402/402 with zero recorded gaps; canonical events stay 308/308.
This commit is contained in:
Mihaela Mihaljevic 2026-06-24 22:09:14 +02:00
parent 98ca76e7af
commit 5c34ebd8b6
3 changed files with 35 additions and 6 deletions

View File

@ -22,6 +22,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Reject an under-indented or tab-indented continuation in a multi-line quoted
scalar. A quoted scalar's continuation must be indented past the parent block, so
`"a`\n`b"` (continuation in column 0) and `"bar`\n`\tbaz"` (tab indentation) are
malformed. The error is deferred until the closing quote is found, so an
unterminated quote still reports the missing quote (its root cause) rather than
the indentation symptom. Surpasses libyaml on QB6E and DK95/01, bringing
whole-suite conformance to 402/402.
- Reject a tab used to indent flow-collection content. A tab is never indentation,
so flow content reached through a tab below the continuation indent (`- [`\n
`\tfoo`) is malformed, even though the tab inflates the column. A tab at or past

View File

@ -28,6 +28,11 @@ extension PureYAML.Parsing.LibYAML.Scanner {
reader.skip()
var state = FlowState()
let quote: Unicode.Scalar = single ? "'" : "\""
let indent = indentEngine.indent + 1
// A continuation indented below `indent` is an error, but it is recorded
// and only raised once the quote is known to close; an unterminated quote
// reaches end of input first and reports that instead.
var indentError: PureYAML.Parsing.LibYAML.ScannerError?
while true {
if reader.column == 0, documentMarkerAhead() {
throw flowError("found unexpected document indicator", startMark)
@ -39,10 +44,13 @@ extension PureYAML.Parsing.LibYAML.Scanner {
if reader.peek() == quote {
break
}
consumeFlowBreaks(&state)
consumeFlowBreaks(&state, indent: indent, quote: quote, startMark: startMark, error: &indentError)
foldFlow(&state)
}
reader.skip()
if let indentError {
throw indentError
}
return PureYAML.Parsing.LibYAML.Token(
type: .scalar(value: state.string, style: single ? .singleQuoted : .doubleQuoted),
startMark: startMark,
@ -139,9 +147,18 @@ extension PureYAML.Parsing.LibYAML.Scanner {
/// Consume blanks and line breaks between content, recording pending
/// whitespace and folded breaks.
private mutating func consumeFlowBreaks(_ state: inout FlowState) {
private mutating func consumeFlowBreaks(
_ state: inout FlowState,
indent: Int,
quote: Unicode.Scalar,
startMark: PureYAML.Parsing.Mark,
error: inout PureYAML.Parsing.LibYAML.ScannerError?,
) {
while Classify.isBlank(reader.peek()) || Classify.isBreak(reader.peek()) {
if Classify.isBlank(reader.peek()) {
if error == nil, state.leadingBlanks, reader.column < indent, Classify.isTab(reader.peek()), !blankLineAhead() {
error = flowError("found a tab character that violates indentation", startMark)
}
if !state.leadingBlanks, let blank = reader.peek() {
state.whitespaces.unicodeScalars.append(blank)
}
@ -156,6 +173,11 @@ extension PureYAML.Parsing.LibYAML.Scanner {
state.trailingBreaks += "\n"
}
}
// The continuation content must be indented at least `indent`; a less
// indented (non-closing, non-empty) line is malformed.
if error == nil, state.leadingBlanks, let next = reader.peek(), next != quote, !Classify.isBreakZ(next), reader.column < indent {
error = flowError("found wrong indentation in a multi-line quoted scalar", startMark)
}
}
/// Fold the pending breaks or whitespace into the value.

View File

@ -10,8 +10,8 @@ source:
# oracle: json if in.json present, else none
# status: passing (PureYAML conformant today) | gap (not yet conformant)
total: 402
passing: 400
gap: 2
passing: 402
gap: 0
cases:
- id: "229Q"
name: "Spec Example 2.4. Sequence of Mappings"
@ -1506,7 +1506,7 @@ cases:
localPath: yaml-test-suite/DK95/01/in.yaml
expected: reject
oracle: json
status: gap
status: passing
byteCount: 16
lineCount: 2
- id: "DK95/02"
@ -2506,7 +2506,7 @@ cases:
localPath: yaml-test-suite/QB6E/in.yaml
expected: reject
oracle: none
status: gap
status: passing
byteCount: 20
lineCount: 4
- id: "QF4Y"