fix(parsing): reject empty entries in flow collections

In a flow sequence or mapping a comma separates entries, so one must
precede it. The parser advanced past a comma without checking that an
entry came before, accepting a leading comma ([ , a ]) or a repeated comma
([ a, , b ]) as empty entries. Each flow loop now tracks whether an entry
precedes the comma and rejects it otherwise. A single trailing comma
([ a, b, ]) and empty collections ([], {}) remain valid, since the closing
bracket is handled before that check.

Measured against the official YAML test-suite, parse-level conformance
rises from 249/333 (74.8%) to 251/333 (75.4%) with no regression.
This commit is contained in:
Mihaela M 2026-06-27 16:26:46 +02:00
parent b7bcfe1a01
commit 97584dc57c
3 changed files with 48 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 flow collection no longer accepts an empty entry. A comma separates entries, so one must
precede it; a leading comma (`[ , a ]`), a repeated comma (`[ a, , b ]`), or the same in a flow
mapping is now rejected, while a single trailing comma (`[ a, b, ]`) and empty collections
(`[]`, `{}`) stay valid. The parser previously skipped a comma without checking that an entry
came before it. Lifts official test-suite parse-level conformance from 249/333 (74.8%) to
251/333 (75.4%), with no regression.
- Double-quoted escape sequences now follow the full YAML 1.2 escape table. Previously the
decoder recognized only a subset (`\" \\ \/ \b \f \n \r \t \u`) and silently passed any other
character through, so invalid escapes such as `\.` and `\'` were accepted while valid ones such

View File

@ -16,6 +16,7 @@ extension PureYAML.Parsing.TokenEventParser {
style: .flow,
mark: startMark,
))
var hasEntry = false
while cursor.current != nil {
if case .flowSequenceEnd = cursor.current?.kind {
let end = try expect("flow sequence end") { kind in
@ -29,10 +30,18 @@ extension PureYAML.Parsing.TokenEventParser {
return PureYAML.Parsing.NodeResult(kind: .sequence, endMark: endMark)
}
if case .flowEntry = cursor.current?.kind {
// A comma separates entries, so one must precede it: a leading or repeated comma
// is an empty entry, which is not allowed. A single trailing comma before the
// closing bracket is fine, since the close is handled above regardless of `hasEntry`.
guard hasEntry else {
throw unexpectedToken(expected: "flow sequence entry or flow sequence end")
}
hasEntry = false
_ = try cursor.advance()
continue
}
endMark = try parseNode().endMark
hasEntry = true
if cursor.current?.kind.isFlowTerminator == false {
throw unexpectedToken(expected: "flow entry or flow sequence end")
}
@ -57,6 +66,7 @@ extension PureYAML.Parsing.TokenEventParser {
style: .flow,
mark: startMark,
))
var hasEntry = false
while cursor.current != nil {
if case .flowMappingEnd = cursor.current?.kind {
let end = try expect("flow mapping end") { kind in
@ -70,10 +80,17 @@ extension PureYAML.Parsing.TokenEventParser {
return PureYAML.Parsing.NodeResult(kind: .mapping, endMark: endMark)
}
if case .flowEntry = cursor.current?.kind {
// As in a flow sequence, a comma separates entries and so requires one before it;
// a leading or repeated comma is an empty entry. A trailing comma is fine.
guard hasEntry else {
throw unexpectedToken(expected: "flow mapping entry or flow mapping end")
}
hasEntry = false
_ = try cursor.advance()
continue
}
endMark = try parseFlowMappingPair().endMark
hasEntry = true
if cursor.current?.kind.isFlowTerminator == false {
throw unexpectedToken(expected: "flow entry or flow mapping end")
}

View File

@ -0,0 +1,25 @@
@testable import PureYAML
import Testing
@Suite("Flow collections reject empty entries")
struct FlowEmptyEntryTests {
@Test("A leading or repeated comma is an empty entry and is rejected", arguments: [
"[ , a, b, c ]\n", // leading comma in a sequence
"[ a, b, c, , ]\n", // repeated comma in a sequence
"{ , a: 1 }\n", // leading comma in a mapping
"{ a: 1, , b: 2 }\n", // repeated comma in a mapping
])
func test_emptyEntryIsRejected(_ yaml: String) throws {
#expect(throws: PureYAML.Parsing.ParseError.self) {
try PureYAML.parse(yaml)
}
}
@Test("A single trailing comma and empty collections stay valid")
func test_trailingCommaAndEmptyCollectionsAccepted() throws {
#expect(try PureYAML.parse("[ a, b, ]\n") == .sequence(.init([.string("a"), .string("b")])))
#expect(try PureYAML.parse("[]\n") == .sequence(.init([])))
#expect(try PureYAML.parse("{ a: 1, }\n") == .mapping(.init([.init(key: "a", value: .int(1))])))
#expect(try PureYAML.parse("{}\n") == .mapping(.init([])))
}
}