fix(parsing): bind an inline first-key property to the key
A property written inline before a block mapping's first key (`&a key: v`) was attached to the whole mapping, so an anchor registered only when the mapping ended, too late for an alias inside one of the mapping's values to resolve. The YAML grammar requires a block collection's own properties to be followed by a line break (`s-l+block-collection`), so a property on the first key's line belongs to the key, not the collection. The mapping now keeps only a property written on its own line above the first key; an inline one is threaded back to the first pair, where the key parses it and an anchor registers with the key. Fixes yaml-test-suite 2SXE (`&a:` and `&a` are distinct anchors, since a name includes a trailing `:`) and HMQ5 (a key carrying both a tag and an anchor). Raises parse-level conformance to 308/333 (92.5%) and value-level to 201/201 (100.0%), with the should-error rate unchanged.
This commit is contained in:
parent
e3caa5be0d
commit
c858339307
10
CHANGELOG.md
10
CHANGELOG.md
@ -84,6 +84,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- A property written inline before a block mapping's first key (`&a key: value`) now binds to that
|
||||
key rather than to the whole mapping. The YAML grammar requires a block collection's own
|
||||
properties to be followed by a line break, so a property on the first key's line belongs to the
|
||||
key, not the collection. The parser had attached it to the mapping, where an anchor registered
|
||||
only at the mapping's end, too late for an alias inside one of the mapping's values to resolve. A
|
||||
property on its own line above the key still binds to the mapping. Fixes yaml-test-suite 2SXE
|
||||
(anchor names include a trailing `:`, so `&a:` and `&a` are distinct anchors, already handled by
|
||||
the scanner) and HMQ5 (a key carrying both a tag and an anchor); raises parse-level conformance to
|
||||
308/333 (92.5%) and value-level to 201/201 (100.0%), with the should-error rate unchanged.
|
||||
|
||||
- An anchor on an empty mapping value (`a: &anchor`) now registers that value's implicit null node,
|
||||
so a later alias resolves to null instead of failing as an undefined alias. Two bugs compounded:
|
||||
the parser dispatched the anchor-only value position to a nested block mapping, absorbing the
|
||||
|
||||
@ -125,9 +125,24 @@ extension PureYAML.Parsing.TokenEventParser {
|
||||
let contentColumn = min(startMark.column, tokenColumn)
|
||||
let boundaryColumn = minimumColumn ?? contentColumn
|
||||
var endMark = startMark
|
||||
// A property written inline before the first key (`&a key: v`) binds to that key, not to the
|
||||
// mapping: the spec requires a block collection's own properties to be followed by a line
|
||||
// break (`s-l+block-collection`), so a property on the key's line is the key's. This matters
|
||||
// for anchors, which must register with the key (early enough for an alias inside a value to
|
||||
// resolve) rather than with the mapping, which registers only at its end. A property on its
|
||||
// own line above the first key still binds to the mapping.
|
||||
let firstKeyProperties: PureYAML.Parsing.NodeProperties
|
||||
let mappingProperties: PureYAML.Parsing.NodeProperties
|
||||
if properties != .none, properties.mark?.line == cursor.current?.mark.line {
|
||||
firstKeyProperties = properties
|
||||
mappingProperties = .none
|
||||
} else {
|
||||
firstKeyProperties = .none
|
||||
mappingProperties = properties
|
||||
}
|
||||
try emit(.mappingStart(
|
||||
anchor: properties.anchor,
|
||||
tag: properties.tag,
|
||||
anchor: mappingProperties.anchor,
|
||||
tag: mappingProperties.tag,
|
||||
style: .block,
|
||||
mark: startMark,
|
||||
))
|
||||
@ -135,6 +150,7 @@ extension PureYAML.Parsing.TokenEventParser {
|
||||
try parseBlockMappingPairs(
|
||||
boundaryColumn: boundaryColumn,
|
||||
includeIndentedContinuation: includeIndentedContinuation,
|
||||
firstKeyProperties: firstKeyProperties,
|
||||
endMark: &endMark,
|
||||
)
|
||||
if let current = cursor.current, !canEndBlockMapping(at: current, minimumColumn: boundaryColumn) {
|
||||
@ -152,13 +168,17 @@ extension PureYAML.Parsing.TokenEventParser {
|
||||
mutating func parseBlockMappingPairs(
|
||||
boundaryColumn: Int,
|
||||
includeIndentedContinuation: Bool,
|
||||
firstKeyProperties: PureYAML.Parsing.NodeProperties = .none,
|
||||
endMark: inout PureYAML.Parsing.Mark,
|
||||
) throws {
|
||||
var pendingKeyProperties = firstKeyProperties
|
||||
while canContinueBlockMapping(minimumColumn: boundaryColumn) {
|
||||
endMark = try parseMappingPairAndBoundaries(
|
||||
boundaryColumn: boundaryColumn,
|
||||
includeIndentedContinuation: includeIndentedContinuation,
|
||||
firstKeyProperties: pendingKeyProperties,
|
||||
).endMark
|
||||
pendingKeyProperties = .none
|
||||
}
|
||||
while includeIndentedContinuation, cursor.current?.kind.isIndent == true {
|
||||
endMark = try parseIndentedBlockMappingPairs(boundaryColumn: boundaryColumn).endMark
|
||||
@ -188,8 +208,9 @@ extension PureYAML.Parsing.TokenEventParser {
|
||||
mutating func parseMappingPairAndBoundaries(
|
||||
boundaryColumn: Int,
|
||||
includeIndentedContinuation: Bool,
|
||||
firstKeyProperties: PureYAML.Parsing.NodeProperties = .none,
|
||||
) throws -> PureYAML.Parsing.NodeResult {
|
||||
let result = try parseMappingPair()
|
||||
let result = try parseMappingPair(firstKeyProperties: firstKeyProperties)
|
||||
try consumeMappingSiblingBoundaryDedents(minimumColumn: boundaryColumn)
|
||||
try consumeMappingSiblingBoundaryReindent(minimumColumn: boundaryColumn)
|
||||
if includeIndentedContinuation {
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
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()
|
||||
mutating func parseMappingPair(
|
||||
firstKeyProperties: PureYAML.Parsing.NodeProperties = .none,
|
||||
) throws -> PureYAML.Parsing.NodeResult {
|
||||
// A subsequent pair's key may carry an anchor or tag (`&a key: …`). The first pair's inline
|
||||
// properties are consumed when the mapping is entered and passed back in here; consuming
|
||||
// again covers any on a later key, and is a no-op (`.none`) when there are none.
|
||||
var keyProperties = firstKeyProperties
|
||||
try keyProperties.mergeUnset(from: consumeProperties())
|
||||
_ = try expect("mapping key") { $0.isMappingKey }
|
||||
let key = try parseNode(properties: keyProperties)
|
||||
// An explicit key (`? key`) with no following `:` has an implicit null value, so several
|
||||
|
||||
34
Tests/InlineKeyPropertyAnchorTests.swift
Normal file
34
Tests/InlineKeyPropertyAnchorTests.swift
Normal file
@ -0,0 +1,34 @@
|
||||
@testable import PureYAML
|
||||
import Testing
|
||||
|
||||
@Suite("An inline property before the first key binds to the key, not the mapping")
|
||||
struct InlineKeyPropertyAnchorTests {
|
||||
/// A property written inline before the first key (`&k key: v`) anchors that key, so the anchor
|
||||
/// registers with the key and resolves early, before the enclosing mapping ends. Binding it to
|
||||
/// the mapping would register only at the mapping's end, too late for an alias inside a value.
|
||||
@Test("An inline anchor on the first key resolves from a later value")
|
||||
func test_inlineFirstKeyAnchorResolves() throws {
|
||||
let mapping = try requireMapping(PureYAML.parse("&k key: v\nx: *k\n"))
|
||||
#expect(mapping?["key"] == .string("v"))
|
||||
#expect(mapping?["x"] == .string("key"))
|
||||
}
|
||||
|
||||
/// yaml-test-suite 2SXE: anchor names include `:` (a valid `ns-anchor-char`), so `&a:` and `&a`
|
||||
/// are distinct anchors. The alias `*a:` resolves to the `&a:`-anchored key `key`, not the
|
||||
/// `&a`-anchored value, and the inline anchor binds to the key rather than the whole mapping.
|
||||
@Test("Distinct anchor names differing only by a trailing colon resolve independently")
|
||||
func test_anchorNamesIncludingColon() throws {
|
||||
let mapping = try requireMapping(PureYAML.parse("&a: key: &a value\nfoo:\n *a:\n"))
|
||||
#expect(mapping?["key"] == .string("value"))
|
||||
#expect(mapping?["foo"] == .string("key"))
|
||||
}
|
||||
|
||||
/// yaml-test-suite HMQ5: a key may carry both a tag and an anchor (`!!str &a1 "foo"`); the anchor
|
||||
/// registers the resolved key value so a later `*a1` comes back as that value.
|
||||
@Test("A first key carrying both a tag and an anchor registers for its alias")
|
||||
func test_inlineFirstKeyTagAndAnchor() throws {
|
||||
let mapping = try requireMapping(PureYAML.parse("!!str &a1 \"foo\":\n !!str bar\n&a2 baz : *a1\n"))
|
||||
#expect(mapping?["foo"] == .string("bar"))
|
||||
#expect(mapping?["baz"] == .string("foo"))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user