From 990a2d2e428a2291d4d8d50269c855830439a6b6 Mon Sep 17 00:00:00 2001 From: Mihaela M Date: Mon, 29 Jun 2026 09:04:29 +0200 Subject: [PATCH] fix(parsing): parse an empty explicit key in a flow mapping An explicit key (`?`) may have no key node: `? : v`, a trailing `? }` / `? ]` / `? ,`, or `?` at the end of a flow mapping all denote a null key. The parser now emits an empty key in those positions instead of failing to find a node before the value indicator or terminator. Fixes suite case DFF7 (parse-level conformance 316 -> 317); adds tests. --- .../TokenEventParser.ExplicitKey.swift | 26 +++++++++++++++ .../Parsing/TokenEventParser.Helpers.swift | 2 +- Tests/FlowExplicitEmptyKeyTests.swift | 33 +++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 Sources/Parsing/TokenEventParser.ExplicitKey.swift create mode 100644 Tests/FlowExplicitEmptyKeyTests.swift diff --git a/Sources/Parsing/TokenEventParser.ExplicitKey.swift b/Sources/Parsing/TokenEventParser.ExplicitKey.swift new file mode 100644 index 0000000..dd436b0 --- /dev/null +++ b/Sources/Parsing/TokenEventParser.ExplicitKey.swift @@ -0,0 +1,26 @@ +extension PureYAML.Parsing.TokenEventParser { + /// The key node of an explicit (`?`) mapping entry. The key may be empty: `? : v`, a trailing + /// `? }` / `? ]` / `? ,`, or `?` at stream end all denote a null key. With no node and no + /// properties to anchor, emit a null scalar rather than failing to find a node. + mutating func parseExplicitMappingKey( + properties: PureYAML.Parsing.NodeProperties, + ) throws -> PureYAML.Parsing.NodeResult { + guard properties == .none, isAtEmptyExplicitKey else { + return try parseNode(properties: properties) + } + let mark = cursor.current?.mark ?? cursor.previous?.endMark ?? .start + try emit(.scalar(value: "", anchor: nil, tag: nil, style: .plain, mark: mark)) + return PureYAML.Parsing.NodeResult(kind: .scalar, endMark: mark) + } + + /// Whether an explicit key has no key node: the next token is the value indicator, a flow + /// terminator (`}`, `]`, `,`), or the stream has ended. + var isAtEmptyExplicitKey: Bool { + switch cursor.current?.kind { + case nil, .mappingValue, .flowMappingEnd, .flowSequenceEnd, .flowEntry: + true + default: + false + } + } +} diff --git a/Sources/Parsing/TokenEventParser.Helpers.swift b/Sources/Parsing/TokenEventParser.Helpers.swift index f92ab70..eb004cc 100644 --- a/Sources/Parsing/TokenEventParser.Helpers.swift +++ b/Sources/Parsing/TokenEventParser.Helpers.swift @@ -8,7 +8,7 @@ extension PureYAML.Parsing.TokenEventParser { var keyProperties = firstKeyProperties try keyProperties.mergeUnset(from: consumeProperties()) _ = try expect("mapping key") { $0.isMappingKey } - let key = try parseNode(properties: keyProperties) + let key = try parseExplicitMappingKey(properties: keyProperties) // An explicit key (`? key`) with no following `:` has an implicit null value, so several // `? key` entries in a row (a set, for instance) each map to null rather than being an error. guard case .mappingValue = cursor.current?.kind else { diff --git a/Tests/FlowExplicitEmptyKeyTests.swift b/Tests/FlowExplicitEmptyKeyTests.swift new file mode 100644 index 0000000..e3133e0 --- /dev/null +++ b/Tests/FlowExplicitEmptyKeyTests.swift @@ -0,0 +1,33 @@ +@testable import PureYAML +import Testing + +/// An explicit key (`?`) in a flow mapping may have no key node: `? : v`, a trailing `? }` or +/// `? ]` or `? ,`, all denote a null key. The parser emits an empty key rather than failing to +/// find a node before the value indicator or terminator. +@Suite("Flow explicit empty key") +struct FlowExplicitEmptyKeyTests { + @Test("An explicit key with a value but no key node is a null key") + func test_explicitEmptyKeyWithValue() throws { + #expect(try PureYAML.parse("{ ? : v }") == .mapping(.init([ + .init(keyNode: .string(""), value: .string("v")), + ]))) + } + + @Test("A trailing explicit key with no node maps a null key to null") + func test_trailingExplicitEmptyKey() throws { + #expect(try PureYAML.parse("{ a, ? }") == .mapping(.init([ + .init(key: "a", value: .null), + .init(keyNode: .string(""), value: .null), + ]))) + } + + @Test("Suite case DFF7: a flow mapping ending with an empty explicit key") + func test_dff7() throws { + let yaml = "{\n? explicit: entry,\nimplicit: entry,\n?\n}\n" + #expect(try PureYAML.parse(yaml) == .mapping(.init([ + .init(key: "explicit", value: .string("entry")), + .init(key: "implicit", value: .string("entry")), + .init(keyNode: .string(""), value: .null), + ]))) + } +}