diff --git a/CHANGELOG.md b/CHANGELOG.md index 35790a3..1ad3aa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Sources/Parsing/TokenEventParser.Helpers.swift b/Sources/Parsing/TokenEventParser.Helpers.swift index 61fb680..5de6a5d 100644 --- a/Sources/Parsing/TokenEventParser.Helpers.swift +++ b/Sources/Parsing/TokenEventParser.Helpers.swift @@ -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 diff --git a/Tests/SequencePropertyIndentTests.swift b/Tests/SequencePropertyIndentTests.swift new file mode 100644 index 0000000..85a700b --- /dev/null +++ b/Tests/SequencePropertyIndentTests.swift @@ -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")) + } +}