fix(parsing): treat colon-led flow sequence element as empty-key map

A flow sequence element that begins with `:` is a single-pair mapping with an
empty key, e.g. `[ : value ]` is `[{null: value}]`, but parseFlowSequence ran
the element through parseNode, which found the leading `:` and raised
"expected a YAML node".

Detect the leading mapping-value indicator and emit a single-pair flow mapping
with a null key and the following value (itself possibly empty), the sequence
counterpart of the scalar-key element `[ a: b ]` that already becomes a
mapping through its implicit-key marker. Plain elements and scalar-key mapping
elements are untouched, and the allowance is confined to flow sequence element
position.

Raises yaml-test-suite parse-level to 298/333 (89.5%) with the should-error
rejection rate unchanged. 426 tests pass.
This commit is contained in:
Mihaela M 2026-06-28 19:43:56 +02:00
parent 956f42bbb7
commit c1610f1eec
3 changed files with 81 additions and 1 deletions

View File

@ -84,6 +84,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- A flow sequence element that begins with `:` is now parsed as a single-pair mapping with an empty
(null) key, e.g. `[ : value ]` is `[{null: value}]`. The parser raised "expected a YAML node" for
the leading `:`; it now emits the empty-key mapping, mirroring the way `[ a: b ]` (a scalar key
followed by `:`) already becomes a mapping element. Plain elements and scalar-key mapping elements
are unchanged. Raises yaml-test-suite parse-level conformance to 298/333 (89.5%), with the
should-error rejection rate unchanged.
- A flow mapping entry may now begin with its `:` indicator, giving an empty (null) key, e.g.
`{ : value }` or `{ a: b, : c }`, or carry only properties before the `:`, e.g. `{ !!str : value }`.
The parser required a node before every flow mapping `:` and raised "expected a YAML node"; it now

View File

@ -40,7 +40,14 @@ extension PureYAML.Parsing.TokenEventParser {
_ = try cursor.advance()
continue
}
endMark = try parseNode().endMark
// A flow sequence element that begins with `:` is a single-pair mapping with an empty
// key, e.g. `[ : value ]`; an element that begins with a node and is then followed by `:`
// already becomes a mapping through the scalar key's implicit-key marker (`[ a: b ]`).
if case .mappingValue = cursor.current?.kind {
endMark = try parseEmptyKeyFlowMappingElement().endMark
} else {
endMark = try parseNode().endMark
}
hasEntry = true
if cursor.current?.kind.isFlowTerminator == false {
throw unexpectedToken(expected: "flow entry or flow sequence end")
@ -49,6 +56,30 @@ extension PureYAML.Parsing.TokenEventParser {
throw unexpectedToken(expected: "flow sequence end")
}
/// Emits a single-pair flow mapping with an empty (null) key for a flow sequence element that
/// begins with `:`. The value follows the `:` and may itself be empty before the next separator.
mutating func parseEmptyKeyFlowMappingElement() throws -> PureYAML.Parsing.NodeResult {
let mark = cursor.current?.mark ?? .start
try emit(.mappingStart(anchor: nil, tag: nil, style: .flow, mark: mark))
try emit(.scalar(value: "", anchor: nil, tag: nil, style: .plain, mark: mark))
let valueIndicator = try expect("mapping value") { kind in
if case .mappingValue = kind {
return true
}
return false
}
let properties = try consumeProperties()
let endMark: PureYAML.Parsing.Mark
if properties == .none, cursor.current?.kind.isFlowTerminator != false {
endMark = valueIndicator.endMark
try emit(.scalar(value: "", anchor: nil, tag: nil, style: .plain, mark: endMark))
} else {
endMark = try parseNode(properties: properties).endMark
}
try emit(.mappingEnd(mark: endMark))
return PureYAML.Parsing.NodeResult(kind: .mapping, endMark: endMark)
}
mutating func parseFlowMapping(
properties: PureYAML.Parsing.NodeProperties,
) throws -> PureYAML.Parsing.NodeResult {

View File

@ -0,0 +1,42 @@
@testable import PureYAML
import Testing
/// A flow sequence element that begins with `:` is a single-pair mapping with an empty (null) key,
/// e.g. `[ : value ]` is `[{null: value}]`. An element that begins with a node followed by `:` already
/// becomes a mapping through the scalar key's implicit-key marker (`[ a: b ]`), and a plain element is
/// unaffected.
@Suite("Flow sequence empty key")
struct FlowSequenceEmptyKeyTests {
@Test("An element beginning with a colon is a single-pair mapping with an empty key")
func test_emptyKeyElement() throws {
#expect(try PureYAML.parse("[ : x ]\n") == .sequence([
.mapping(.init([.init(keyNode: .string(""), value: .string("x"))])),
]))
}
@Test("The empty key works with no space and an empty value")
func test_noSpaceAndEmptyValue() throws {
#expect(try PureYAML.parse("[: y]\n") == .sequence([
.mapping(.init([.init(keyNode: .string(""), value: .string("y"))])),
]))
#expect(try PureYAML.parse("[ : ]\n") == .sequence([
.mapping(.init([.init(keyNode: .string(""), value: .null)])),
]))
}
@Test("An empty-key element sits alongside ordinary mapping and scalar elements")
func test_mixedElements() throws {
#expect(try PureYAML.parse("[a: b, : c]\n") == .sequence([
.mapping(.init([.init(key: "a", value: .string("b"))])),
.mapping(.init([.init(keyNode: .string(""), value: .string("c"))])),
]))
}
@Test("Plain elements and scalar-key mapping elements are unchanged")
func test_unaffected() throws {
#expect(try PureYAML.parse("[a, b]\n") == .sequence([.string("a"), .string("b")]))
#expect(try PureYAML.parse("[a: b]\n") == .sequence([
.mapping(.init([.init(key: "a", value: .string("b"))])),
]))
}
}