From 31da48805588245467f1dbf55b2d2c4988bcb9d9 Mon Sep 17 00:00:00 2001 From: Mihaela M Date: Mon, 29 Jun 2026 01:42:02 +0200 Subject: [PATCH] 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. --- CHANGELOG.md | 7 ++++ Sources/Parsing/TokenEventParser.Flow.swift | 10 +++++- Tests/FlowMappingEmptyKeyTests.swift | 12 ++++--- Tests/FlowMappingNoValueTests.swift | 39 +++++++++++++++++++++ 4 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 Tests/FlowMappingNoValueTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 120cbf1..999a0cb 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 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 diff --git a/Sources/Parsing/TokenEventParser.Flow.swift b/Sources/Parsing/TokenEventParser.Flow.swift index 7fad0b2..c1da0da 100644 --- a/Sources/Parsing/TokenEventParser.Flow.swift +++ b/Sources/Parsing/TokenEventParser.Flow.swift @@ -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 { diff --git a/Tests/FlowMappingEmptyKeyTests.swift b/Tests/FlowMappingEmptyKeyTests.swift index 9bd336b..6a3147f 100644 --- a/Tests/FlowMappingEmptyKeyTests.swift +++ b/Tests/FlowMappingEmptyKeyTests.swift @@ -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), + ]))) } } diff --git a/Tests/FlowMappingNoValueTests.swift b/Tests/FlowMappingNoValueTests.swift new file mode 100644 index 0000000..983b825 --- /dev/null +++ b/Tests/FlowMappingNoValueTests.swift @@ -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")), + ]))) + } +}