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.
This commit is contained in:
Mihaela M 2026-06-29 09:04:29 +02:00
parent 8112954f4c
commit 990a2d2e42
3 changed files with 60 additions and 1 deletions

View File

@ -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
}
}
}

View File

@ -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 {

View File

@ -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),
])))
}
}