From 3fc30725441f9cb11fcecd24febdd179e489d316 Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Wed, 24 Jun 2026 18:31:40 +0200 Subject: [PATCH] feat(parsing): parse flow mappings with empty keys A ':' value indicator with no preceding key is also valid inside a flow mapping, denoting an entry whose key is empty (null). libyaml hands the bare VALUE token to its node parser and fails with 'did not find expected node content'. The flow-mapping-key parser now emits the empty scalar key and lets the value state consume the indicator. Fixes '{ : }' and '{ key: value, : empty key }', completing the multi-document NKF9 case. Port score 282 -> 283. --- CHANGELOG.md | 5 +++++ Sources/Parsing/LibYAML.Parser.Flow.swift | 8 ++++++++ Tests/CanonicalEventConformanceTests.swift | 4 ++-- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71a531a..c6bcd71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 sequences (`- :`, `- ? : x`), and mixed empty-key/value entries. Surpasses libyaml on these seven test-suite cases (2JQS, 6M2F, M2N8/00, NHX8, S3PD, SM9W/01, UKK6/00). +- Parse flow mappings with empty (null) keys. The same `:`-without-a-key entry + is valid inside `{ ... }`, where libyaml hands the bare VALUE token to its node + parser and fails with "did not find expected node content". The flow-mapping-key + parser now emits the empty scalar key for a leading `:`, so `{ : }` and + `{ key: value, : empty key }` parse, completing the multi-document NKF9 case. - Emit no document for a stream whose only content is comments or whitespace. A comment-only or empty stream has zero documents in YAML 1.2 (matching libyaml and the test suite), but the event stream was emitting a spurious empty null diff --git a/Sources/Parsing/LibYAML.Parser.Flow.swift b/Sources/Parsing/LibYAML.Parser.Flow.swift index 466c997..28638b6 100644 --- a/Sources/Parsing/LibYAML.Parser.Flow.swift +++ b/Sources/Parsing/LibYAML.Parser.Flow.swift @@ -96,6 +96,14 @@ extension PureYAML.Parsing.LibYAML.Parser { if isKey(token) { return try flowMappingKeyEvent() } + // YAML 1.2 permits an empty (null) key in a flow mapping: a `:` with + // no preceding key denotes an entry whose key is empty. libyaml hands + // the VALUE token to `parse_node` and fails; instead emit the empty + // scalar key and let `parseFlowMappingValue` consume the indicator. + if isValue(token) { + state = .flowMappingValue + return processEmptyScalar(at: token.startMark) + } if !isFlowMappingEnd(token) { states.append(.flowMappingEmptyValue) return try parseNode(block: false, indentlessSequence: false) diff --git a/Tests/CanonicalEventConformanceTests.swift b/Tests/CanonicalEventConformanceTests.swift index 6dd3031..2d4b87b 100644 --- a/Tests/CanonicalEventConformanceTests.swift +++ b/Tests/CanonicalEventConformanceTests.swift @@ -12,7 +12,7 @@ struct CanonicalEventConformanceTests { /// Valid suite cases whose event stream PureYAML does NOT yet emit exactly. /// This is the authoritative gap set: a fix removes an id (and the test fails /// until it is removed), a regression adds one (and the test fails). Of these, - /// 23 currently throw on valid input and 3 emit a wrong event stream. + /// 22 currently throw on valid input and 3 emit a wrong event stream. static let knownEventGaps: Set = [ // Gap set for the ported libyaml engine. Verified against the libyaml // reference dumper: on all but four of these the port matches libyaml @@ -24,7 +24,7 @@ struct CanonicalEventConformanceTests { "4ABK", "58MP", "5T43", "6BCT", "6CA3", "96NN/00", "96NN/01", "A2M4", "CFD4", "DBG4", "DK3J", "DK95/00", "DK95/04", "FP8R", "FRK4", "HM87/00", "M7A3", - "NKF9", "Q5MG", "R4YG", "W4TN", + "Q5MG", "R4YG", "W4TN", "Y79Y/001", "Y79Y/010", // wrong event stream: the port matches libyaml but both deviate from the // canonical events (libyaml quirks to be surpassed).