fix(parsing): allow a flow mapping entry with no value

A flow mapping entry may omit its `:` value indicator, in which case its key
maps to null: `{ a }` and `{ a, b }` are flow sets. parseFlowMappingPair
required a `:` after every key, so it rejected these entries, and rejected a
valueless key that spanned lines (`{ multi\n  line, a: b}`) with them.

Parse a value only when a `:` follows the key; otherwise emit a null value for
the key. A unit test that asserted `{ a, }` was an error is corrected to expect
the set `{a: null}`, the behaviour the yaml-test-suite cases 8KB6 and 9BXH
require.

Raises yaml-test-suite parse-level conformance to 304/333 (91.3%) and
value-level to 195/197, with the should-error rejection rate unchanged.
480 tests pass.
This commit is contained in:
Mihaela M 2026-06-29 01:42:02 +02:00
parent 5c99086a38
commit 31da488055
4 changed files with 62 additions and 6 deletions

View File

@ -84,6 +84,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- A flow mapping entry may now omit its `:` value indicator, mapping its key to null: `{ a }` and
`{ a, b }` are flow sets. The parser required a `:` after every flow mapping key and rejected these,
and a multi-line key with no value (`{ multi`/`line, a: b}`) with it. A value now parses only when a
`:` follows the key; otherwise the key maps to null. A test that asserted `{ a, }` was an error was
corrected, since it is a valid set. Raises yaml-test-suite parse-level conformance to 304/333
(91.3%) and value-level to 195/197, with the should-error rate unchanged.
- A plain scalar key in a flow collection may now span lines. A flow key, unlike a block implicit
key, is allowed to be multi-line, so `{ multi` / ` line: v }` is the key `multi line`. The scalar
continuation stopped at the `:` value indicator in every non-explicit case, dropping the second

View File

@ -138,11 +138,19 @@ extension PureYAML.Parsing.TokenEventParser {
// both resolve to a null key, so emit one rather than requiring a node before the `:`. This is
// valid only in flow mapping key position, so the empty-node allowance stays local here.
let keyProperties = try consumeProperties()
let keyEndMark: PureYAML.Parsing.Mark
if case .mappingValue = cursor.current?.kind {
let mark = keyProperties.mark ?? cursor.current?.mark ?? .start
try emit(.scalar(value: "", anchor: keyProperties.anchor, tag: keyProperties.tag, style: .plain, mark: mark))
keyEndMark = mark
} else {
_ = try parseNode(properties: keyProperties)
keyEndMark = try parseNode(properties: keyProperties).endMark
}
// A flow mapping entry may omit its `:` value indicator: `{ a }` and `{ a, b }` are sets whose
// keys map to null. A value parses only when a `:` follows the key.
guard case .mappingValue = cursor.current?.kind else {
try emit(.scalar(value: "", anchor: nil, tag: nil, style: .plain, mark: keyEndMark))
return PureYAML.Parsing.NodeResult(kind: .scalar, endMark: keyEndMark)
}
let valueIndicator = try expect("mapping value") { kind in
if case .mappingValue = kind {

View File

@ -42,10 +42,12 @@ struct FlowMappingEmptyKeyTests {
])))
}
@Test("A flow mapping entry with no colon at all is still an error")
func test_entryWithoutColonRejected() {
#expect(throws: PureYAML.Parsing.ParseError.self) {
try PureYAML.parse("{ a, }\n")
}
@Test("A flow mapping entry with no colon is a set-style key mapping to null")
func test_entryWithoutColonIsNullValue() throws {
// `{ a, }` is a flow set: the entry has no `:`, so its key maps to null. The trailing comma
// is allowed.
#expect(try PureYAML.parse("{ a, }\n") == .mapping(.init([
.init(key: "a", value: .null),
])))
}
}

View File

@ -0,0 +1,39 @@
@testable import PureYAML
import Testing
/// A flow mapping entry may omit its `:` value indicator, in which case the key maps to null: `{ a }`
/// and `{ a, b }` are flow sets. Entries with and without values mix freely, and a key without a value
/// may itself span lines (`{ multi`/`line, a: b}`).
@Suite("Flow mapping no value")
struct FlowMappingNoValueTests {
@Test("A single key with no value maps to null")
func test_singleKey() throws {
#expect(try PureYAML.parse("{ a }\n") == .mapping(.init([
.init(key: "a", value: .null),
])))
}
@Test("Several keys with no value form a set")
func test_set() throws {
#expect(try PureYAML.parse("{ a, b }\n") == .mapping(.init([
.init(key: "a", value: .null),
.init(key: "b", value: .null),
])))
}
@Test("Valued and valueless entries mix")
func test_mixed() throws {
#expect(try PureYAML.parse("{ a: 1, b }\n") == .mapping(.init([
.init(key: "a", value: .int(1)),
.init(key: "b", value: .null),
])))
}
@Test("A valueless key may span lines")
func test_multilineValuelessKey() throws {
#expect(try PureYAML.parse("{ multi\n line, a: b}\n") == .mapping(.init([
.init(key: "multi line", value: .null),
.init(key: "a", value: .string("b")),
])))
}
}