From c1610f1eec7f9ca9f13a482f0df528109e1e2b8f Mon Sep 17 00:00:00 2001 From: Mihaela M Date: Sun, 28 Jun 2026 19:43:56 +0200 Subject: [PATCH] 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. --- CHANGELOG.md | 7 ++++ Sources/Parsing/TokenEventParser.Flow.swift | 33 +++++++++++++++- Tests/FlowSequenceEmptyKeyTests.swift | 42 +++++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 Tests/FlowSequenceEmptyKeyTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d78590..003d48b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Sources/Parsing/TokenEventParser.Flow.swift b/Sources/Parsing/TokenEventParser.Flow.swift index 9f70ab4..7fad0b2 100644 --- a/Sources/Parsing/TokenEventParser.Flow.swift +++ b/Sources/Parsing/TokenEventParser.Flow.swift @@ -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 { diff --git a/Tests/FlowSequenceEmptyKeyTests.swift b/Tests/FlowSequenceEmptyKeyTests.swift new file mode 100644 index 0000000..a2b48de --- /dev/null +++ b/Tests/FlowSequenceEmptyKeyTests.swift @@ -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"))])), + ])) + } +}