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.
This commit is contained in:
Mihaela M 2026-06-26 22:28:13 +02:00
parent 82d5b57fd6
commit 58f30b4343
4 changed files with 56 additions and 5 deletions

View File

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

View File

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

View File

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

View File

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