fix(parsing): reject a block entry after a flow collection on a line

A block sequence entry cannot follow a flow collection that closed earlier
on the same line ('- { y: z }- invalid', '- [a] - b'): the entry would
begin a sequence inline after a complete node. The scanner accepted it,
producing extra sequence items.

The scanner now records when a flow collection closes at the top level on
the current line and rejects a following block entry. The flag resets at
each line break, so a flow collection on its own line is unaffected, and a
nested block entry ('- -') has no preceding flow close, so it stays valid.

Measured against the official YAML test-suite, parse-level conformance
rises from 276/333 (82.9%) to 277/333 (83.2%) with no regression.
This commit is contained in:
Mihaela M 2026-06-28 01:34:55 +02:00
parent ecf634fdf7
commit d79df8c4bc
4 changed files with 53 additions and 0 deletions

View File

@ -23,6 +23,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- A block sequence entry can no longer follow a flow collection that closed earlier on the same
line (`- { y: z }- invalid`, `- [a] - b`); the entry would begin a sequence inline after a
complete node. The scanner now tracks a top-level flow close on the current line and rejects a
block entry after it, while a nested block entry (`- -`, which has no preceding flow close), a
flow collection on its own line, and nested flow collections are unaffected. Lifts official
test-suite parse-level conformance from 276/333 (82.9%) to 277/333 (83.2%), with no regression.
- A mapping value's anchor or tag on the line below the key must now be indented past the key. A
property at the key's own indentation (`seq:` then `&anchor` in column 1) is ambiguous with a
sibling and is rejected, while an inline property, a property indented under the key, and a block

View File

@ -13,6 +13,7 @@ extension PureYAML.Parsing {
state.isAtLineStart = true
state.sawContentOnLine = false
state.sawMappingValueOnLine = false
state.sawFlowCloseOnLine = false
continue
}
@ -83,6 +84,10 @@ extension PureYAML.Parsing.Scanner {
// (`a: b: c`). Reset at each line break. Flow `:` do not set it, since flow allows several
// pairs on a line (`{a: b, c: d}`).
var sawMappingValueOnLine = false
// Whether a flow collection has closed on the current line at the top level. A block
// sequence entry cannot follow a flow node on the same line (`{a: b}- x`), unlike a nested
// block entry (`- -`), which is not preceded by a flow close. Reset at each line break.
var sawFlowCloseOnLine = false
var tagHandles = [
"!": "!",
"!!": "tag:yaml.org,2002:",
@ -193,6 +198,16 @@ extension PureYAML.Parsing.Scanner {
if state.sawMappingValueOnLine, state.flowDepth == 0 {
throw PureYAML.Parsing.ParseError.blockSequenceAsInlineMappingValue(line: state.reader.mark.line)
}
// A block sequence entry cannot follow a flow collection that closed earlier on the
// line (`{a: b}- x`); the entry would begin a sequence inline after a complete node.
if state.sawFlowCloseOnLine {
throw PureYAML.Parsing.ParseError.unexpectedToken(
expected: "a line break or flow indicator",
actual: "-",
line: state.reader.mark.line,
column: state.reader.mark.column,
)
}
scanFixedToken(.blockEntry, state: &state)
return true
case "?" where isIndicatorBoundary(state.reader.peek(offset: 1)):
@ -220,6 +235,9 @@ extension PureYAML.Parsing.Scanner {
state.flowDepth = max(0, state.flowDepth - 1)
scanFixedToken(.flowSequenceEnd, state: &state)
_ = state.flowContexts.popLast()
if state.flowDepth == 0 {
state.sawFlowCloseOnLine = true
}
return true
case "{":
state.flowDepth += 1
@ -230,6 +248,9 @@ extension PureYAML.Parsing.Scanner {
state.flowDepth = max(0, state.flowDepth - 1)
scanFixedToken(.flowMappingEnd, state: &state)
_ = state.flowContexts.popLast()
if state.flowDepth == 0 {
state.sawFlowCloseOnLine = true
}
return true
case ",":
scanFixedToken(.flowEntry, state: &state)

View File

@ -93,6 +93,7 @@ extension PureYAML.Parsing {
scannerBox.state.isAtLineStart = true
scannerBox.state.sawContentOnLine = false
scannerBox.state.sawMappingValueOnLine = false
scannerBox.state.sawFlowCloseOnLine = false
continue
}

View File

@ -0,0 +1,25 @@
@testable import PureYAML
import Testing
@Suite("A block entry after a flow collection")
struct BlockEntryAfterFlowTests {
@Test("A block entry on the same line as a closed flow collection is rejected", arguments: [
"---\n- { y: z }- invalid\n", // a block entry after a flow mapping
"- [a] - b\n", // a block entry after a flow sequence
])
func test_blockEntryAfterFlowRejected(_ yaml: String) throws {
#expect(throws: PureYAML.Parsing.ParseError.self) {
try PureYAML.parse(yaml)
}
}
@Test("Nested entries and flow collections on their own line stay valid", arguments: [
"- - a\n- - b\n", // a nested block sequence
"- [a]\n- [b]\n", // flow sequences, each on its own line
"- {a: b}\n- c\n", // a flow mapping item then a sibling entry
"[[a], [b]]\n", // nested flow sequences
])
func test_validBlockAndFlowEntries(_ yaml: String) throws {
_ = try PureYAML.parse(yaml)
}
}