fix(parsing): allow an anchor or tag on a non-first mapping key

A block mapping pair after the first could not carry an anchor or tag on
its key (k: 1 then &b key2: 2): the first pair's key properties are
consumed when the mapping is entered, but subsequent pairs were not, so an
anchored key past the first failed with 'expected mapping key, found
anchor'. The pair parser now consumes leading key properties for every
pair, attaching them to the key node, and a block mapping continues across
an anchor or tag token that begins a key.

Normal mappings, anchored values, sequences, and explicit keys are
unaffected. Measured against the official YAML test-suite, parse-level
conformance rises from 235/333 (70.6%) to 238/333 (71.5%), with no
regression.
This commit is contained in:
Mihaela M 2026-06-27 00:21:38 +02:00
parent 58f30b4343
commit 0ae88855da
3 changed files with 42 additions and 2 deletions

View File

@ -23,6 +23,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- A block mapping pair after the first may now carry an anchor or tag on its key
(`k: 1` / `&b key2: 2`). The first pair's key properties were consumed when the mapping was
entered, but later pairs were not, so an anchored key past the first failed with
"expected mapping key". The pair parser now consumes leading key properties for every pair,
and the mapping continues across an anchor or tag that begins a key. Lifts official test-suite
parse-level conformance from 235/333 (70.6%) to 238/333 (71.5%).
- 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.

View File

@ -1,7 +1,11 @@
extension PureYAML.Parsing.TokenEventParser {
mutating func parseMappingPair() throws -> PureYAML.Parsing.NodeResult {
// A subsequent pair's key may carry an anchor or tag (`&a key: …`). The first pair's
// properties are consumed when the mapping is entered; consuming here covers the rest, and
// is a no-op (`.none`) when there are none.
let keyProperties = try consumeProperties()
_ = try expect("mapping key") { $0.isMappingKey }
let key = try parseNode()
let key = try parseNode(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 {
@ -246,7 +250,7 @@ extension PureYAML.Parsing.TokenEventParser {
}
func canContinueBlockMapping(minimumColumn: Int?) -> Bool {
guard let current = cursor.current, current.kind.isMappingKey else {
guard let current = cursor.current, current.kind.isMappingKey || startsNodeProperties(current.kind) else {
return false
}
guard let minimumColumn else {
@ -255,6 +259,17 @@ extension PureYAML.Parsing.TokenEventParser {
return current.mark.column >= minimumColumn
}
/// Whether a token kind begins a node's properties (an anchor or a tag), which a mapping key may
/// carry, so the next pair's key is `&a key: …` rather than a bare key.
func startsNodeProperties(_ kind: PureYAML.Parsing.TokenKind) -> Bool {
switch kind {
case .anchor, .tag:
true
default:
false
}
}
func canEndBlockMapping(
at token: PureYAML.Parsing.Token?,
minimumColumn: Int? = nil,

View File

@ -0,0 +1,19 @@
@testable import PureYAML
import Testing
@Suite("Anchored mapping keys")
struct AnchoredMappingKeyTests {
@Test("A subsequent pair's key may carry an anchor")
func test_anchoredKeyOnSubsequentPair() throws {
let value = try PureYAML.parse("k: 1\n&b key2: 2\n")
#expect(value == .mapping(.init([
.init(key: "k", value: .int(1)),
.init(key: "key2", value: .int(2)),
])))
}
@Test("An anchored key on the first pair still parses")
func test_anchoredKeyOnFirstPair() throws {
#expect(try PureYAML.parse("&a c: 3\n") == .mapping(.init([.init(key: "c", value: .int(3))])))
}
}