fix(parsing): bind an indented property to a zero-indent sequence

A node property written on its own indented line can sit above a block sequence
whose entries return to the mapping key's own indentation (`seq:` then
` &anchor` then `- a` at the key's column). The grammar allows a block-in-block
sequence at its parent's indent n while the property sits at n+1, so the
indent's matching dedent falls between the property and the entries.

The parser consumed the property as an empty anchored value and then rejected
the entries as an unexpected block entry. It now recognises the property-then-
dedent-then-entry shape, consumes that dedent, and binds the property to the
block sequence that follows.

Fixes yaml-test-suite SKE5; raises parse-level conformance to 311/333 (93.4%)
and value-level to 204/204 (100.0%), with the should-error rate unchanged.
This commit is contained in:
Mihaela M 2026-06-29 03:44:53 +02:00
parent b45b7a4b7d
commit 5528ce9aa4
3 changed files with 66 additions and 1 deletions

View File

@ -84,6 +84,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- A node property written on its own indented line above a zero-indent block sequence (`seq:` then
` &anchor` then `- a` back at the key's column) now binds to that sequence. The grammar lets a
block-in-block sequence sit at its parent's indentation while its property sits one level deeper,
so the indent's matching dedent falls between the property and the entries. The parser had read
the property as an empty anchored value and then rejected the entries as unexpected; it now
consumes that dedent and attaches the property to the sequence. Fixes yaml-test-suite SKE5; raises
parse-level conformance to 311/333 (93.4%) and value-level to 204/204 (100.0%), with the
should-error rate unchanged.
- An alias node may now serve as an implicit mapping key (`*a : value`). The scanner emits the
implicit-key indicator before a scalar key followed by a `:` but did not for an alias, so the
parser, which already accepts an alias node in key position, never entered the pair and rejected

View File

@ -63,17 +63,40 @@ extension PureYAML.Parsing.TokenEventParser {
properties: PureYAML.Parsing.NodeProperties,
) throws -> PureYAML.Parsing.NodeResult {
_ = try expect("indent") { $0.isIndent }
// A node property may sit alone on its own indented line above a block sequence whose entries
// return to the mapping key's indentation (`seq:` then ` &anchor` then `- a` back at the key's
// column). The grammar allows a block-in-block sequence at the parent indent n while the
// property is at n+1, so the indent's matching dedent falls between the property and the
// entries. Consume that dedent and bind the property to the sequence rather than emitting an
// empty anchored value and orphaning the entries.
if let kind = cursor.current?.kind, startsNodeProperties(kind) {
var carried = properties
try carried.mergeUnset(from: consumeProperties())
if cursor.current?.kind.isDedent == true, try cursor.peek(offset: 1)?.kind.isBlockEntry == true {
_ = try expect("dedent") { $0.isDedent }
return try parseBlockSequence(properties: carried, allowMappingKeyTerminator: true)
}
let node = try parseNode(properties: carried)
try consumeIndentedMappingValueClose()
return node
}
let result: PureYAML.Parsing.NodeResult = if cursor.current?.kind.isBlockEntry == true {
try parseBlockSequence(properties: properties, allowMappingKeyTerminator: true)
} else {
try parseNode(properties: properties)
}
try consumeIndentedMappingValueClose()
return result
}
/// Consume the dedent that closes an indented mapping value, unless the value ended on a sibling
/// mapping key, which the surrounding mapping continues from at the same indentation.
mutating func consumeIndentedMappingValueClose() throws {
if cursor.current?.kind.isDedent == true {
_ = try expect("dedent") { $0.isDedent }
} else if cursor.current?.kind.isMappingKey != true {
_ = try expect("dedent") { $0.isDedent }
}
return result
}
/// A value's anchor or tag on a line below the `:` must be indented past the key. A properly

View File

@ -0,0 +1,33 @@
@testable import PureYAML
import Testing
@Suite("A node property may sit indented above a zero-indent block sequence")
struct SequencePropertyIndentTests {
/// yaml-test-suite SKE5: a block sequence may sit at its parent mapping key's indentation while a
/// property written above it is more indented (`seq:` / ` &anchor` / `- a`). The property binds to
/// the sequence, not to an empty anchored value, so the entries are not orphaned.
@Test("An anchor indented above a zero-indent block sequence binds to the sequence")
func test_anchorAboveZeroIndentSequence() throws {
let mapping = try requireMapping(PureYAML.parse("---\nseq:\n &anchor\n- a\n- b\n"))
#expect(mapping?["seq"] == .sequence([.string("a"), .string("b")]))
}
/// The plain zero-indent block sequence (no property) keeps working: a sequence whose entries are
/// at the mapping key's own column is the value of that key.
@Test("A zero-indent block sequence without a property is unaffected")
func test_zeroIndentSequenceWithoutProperty() throws {
let mapping = try requireMapping(PureYAML.parse("seq:\n- a\n- b\n"))
#expect(mapping?["seq"] == .sequence([.string("a"), .string("b")]))
}
/// A property on its own indented line above an indented nested mapping still binds to that
/// mapping, so an alias resolves to the whole mapping rather than the sequence path above.
@Test("An anchor above an indented nested mapping still binds to the mapping")
func test_anchorAboveNestedMapping() throws {
let mapping = try requireMapping(PureYAML.parse("key:\n &a\n nested: v\nx: *a\n"))
let key = try requireMapping(#require(mapping?["key"]))
let alias = try requireMapping(#require(mapping?["x"]))
#expect(key?["nested"] == .string("v"))
#expect(alias?["nested"] == .string("v"))
}
}