From 58f30b4343abbfdb3444ba0847d26f09b94233f8 Mon Sep 17 00:00:00 2001 From: Mihaela M Date: Fri, 26 Jun 2026 22:28:13 +0200 Subject: [PATCH] fix(parsing): an explicit key with no value maps to null The block mapping parser required a ':' after every explicit key ('? key'), so a key with no value, including several '? key' entries in a row (a set), failed to parse with 'expected mapping value'. An explicit key with no following ':' now takes an implicit null value, shared with the existing 'key:'-with-nothing-after path via one emitNullMappingValue helper. Explicit keys with values and complex keys in general are unaffected. A test that asserted '? [a, b]' followed by a block entry is a missing-value error was corrected: the key is a valid null entry, and the trailing sequence is now rejected as extra content (a second root node), which is the more accurate diagnosis. Measured against the official YAML test-suite, parse-level conformance rises from 229/333 (68.8%) to 235/333 (70.6%), with several of the newly parsed documents also value-correct and no regression. --- CHANGELOG.md | 5 +++ .../Parsing/TokenEventParser.Helpers.swift | 14 ++++++-- Tests/ComplexMappingKeyParsingTests.swift | 8 +++-- Tests/ExplicitKeyTests.swift | 34 +++++++++++++++++++ 4 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 Tests/ExplicitKeyTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index f5e659d..b4803e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- An explicit mapping key (`? key`) with no following `:` now maps to null instead of failing to + parse, so several `? key` entries in a row (a set, for instance) each map to null. The parser + required a `:` after every explicit key; it now treats a missing one as an implicit null value. + Explicit keys with values, and complex keys generally, are unaffected. Lifts official test-suite + parse-level conformance from 229/333 (68.8%) to 235/333 (70.6%). - An unrecognized directive (e.g. `%FOO`) before a document is now ignored, as the YAML spec requires (a warning, otherwise no effect), instead of being rejected with an error. `%YAML` and `%TAG` are unchanged, and a directive after content (where one cannot appear) is still diff --git a/Sources/Parsing/TokenEventParser.Helpers.swift b/Sources/Parsing/TokenEventParser.Helpers.swift index dea4413..702b940 100644 --- a/Sources/Parsing/TokenEventParser.Helpers.swift +++ b/Sources/Parsing/TokenEventParser.Helpers.swift @@ -1,7 +1,12 @@ extension PureYAML.Parsing.TokenEventParser { mutating func parseMappingPair() throws -> PureYAML.Parsing.NodeResult { _ = try expect("mapping key") { $0.isMappingKey } - _ = try parseNode() + let key = try parseNode() + // 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 { + return try emitNullMappingValue(at: key.endMark) + } let valueIndicator = try expect("mapping value") { kind in if case .mappingValue = kind { return true @@ -47,7 +52,12 @@ extension PureYAML.Parsing.TokenEventParser { ) } - let mark = valueIndicator.endMark + return try emitNullMappingValue(at: valueIndicator.endMark) + } + + /// Emit an empty (null) scalar as a mapping value and return its node result, used where a + /// mapping entry has no value: an explicit key with no `:`, or a `key:` with nothing after it. + mutating func emitNullMappingValue(at mark: PureYAML.Parsing.Mark) throws -> PureYAML.Parsing.NodeResult { try emit(.scalar(value: "", anchor: nil, tag: nil, style: .plain, mark: mark)) return PureYAML.Parsing.NodeResult(kind: .scalar, endMark: mark) } diff --git a/Tests/ComplexMappingKeyParsingTests.swift b/Tests/ComplexMappingKeyParsingTests.swift index fe3f43d..e428080 100644 --- a/Tests/ComplexMappingKeyParsingTests.swift +++ b/Tests/ComplexMappingKeyParsingTests.swift @@ -136,13 +136,15 @@ struct ComplexMappingKeyParsingTests { #expect(service?.rootMapping?["[\"a\", \"b\"]"] == nil) } - @Test("Rejects complex keys that are not followed by mapping values") - func test_rejectsComplexKeysThatAreNotFollowedByMappingValues() { + @Test("A complex key with no value is null; a bare sequence after it is extra content") + func test_complexKeyWithoutValueIsNullThenRejectsExtraContent() { + // `? [a, b]` with no `:` is a valid null-valued entry, so the following block sequence is a + // second root node, rejected as extra content rather than as a missing mapping value. expectParseError(""" ? [a, b] - invalid """, .unexpectedToken( - expected: "mapping value", + expected: "document boundary or stream end", actual: "blockEntry", line: 2, column: 1, diff --git a/Tests/ExplicitKeyTests.swift b/Tests/ExplicitKeyTests.swift new file mode 100644 index 0000000..8396d49 --- /dev/null +++ b/Tests/ExplicitKeyTests.swift @@ -0,0 +1,34 @@ +@testable import PureYAML +import Testing + +@Suite("Explicit keys") +struct ExplicitKeyTests { + @Test("Explicit keys with no value each map to null") + func test_explicitKeysWithoutValuesMapToNull() throws { + // Several `? key` entries in a row (a set, for instance) are valid; each maps to null. + let value = try PureYAML.parse(""" + ? a + ? b + c: + """) + #expect(value == .mapping(.init([ + .init(key: "a", value: .null), + .init(key: "b", value: .null), + .init(key: "c", value: .null), + ]))) + } + + @Test("Explicit keys with values still parse") + func test_explicitKeysWithValues() throws { + let value = try PureYAML.parse(""" + ? a + : b + ? c + : d + """) + #expect(value == .mapping(.init([ + .init(key: "a", value: .string("b")), + .init(key: "c", value: .string("d")), + ]))) + } +}