fix(parsing): reject a block sequence inline after a node property

A block collection must begin on the line after its node properties, so a
block sequence whose first entry sits on the same line as its anchor or tag
('&a - x', '!!seq - x') is malformed. The parser accepted it, anchoring or
tagging the inline sequence.

parseBlockSequence now rejects a sequence whose first block entry shares a
line with the anchor or tag that introduced it, via a small helper to stay
within the complexity and brace-style limits. An anchor or tag on its own
line above the sequence, inside a sequence item ('- &a x'), before a flow
sequence ('&a [x]'), or on a scalar is unaffected; the mapping form is
left alone, since '&a key: value' is a valid anchored key.

Measured against the official YAML test-suite, parse-level conformance
rises from 267/333 (80.2%) to 268/333 (80.5%) with no regression.
This commit is contained in:
Mihaela M 2026-06-27 23:14:54 +02:00
parent 779ebe596d
commit 28f549311f
4 changed files with 58 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 sequence can no longer start on the same line as its anchor or tag (`&a - x`). A block
collection must begin on the line after its node properties, so an anchor or tag immediately
before the first block entry is now rejected. An anchor or tag on its own line above the
sequence, inside a sequence item (`- &a x`), before a flow sequence (`&a [x]`), or on a scalar
is unaffected. Lifts official test-suite parse-level conformance from 267/333 (80.2%) to 268/333
(80.5%), with no regression.
- An alias node can no longer carry an anchor or tag. An alias is a bare reference, so `&b *a`
(an anchor on an alias) and a tag on an alias are now rejected; the parser previously dropped the
property and accepted the document. A bare alias and an anchor or tag on a real node are

View File

@ -28,6 +28,7 @@ public extension PureYAML.Parsing {
case multilineImplicitKey(line: Int)
case commentInMultilineScalar(line: Int)
case aliasWithProperties(line: Int, column: Int)
case inlineBlockSequenceAfterProperty(line: Int, column: Int)
public var description: String {
switch self {
@ -85,6 +86,8 @@ public extension PureYAML.Parsing {
"a comment cannot interrupt a multi-line plain scalar at line \(line)"
case let .aliasWithProperties(line, column):
"an alias node cannot have an anchor or tag at line \(line), column \(column)"
case let .inlineBlockSequenceAfterProperty(line, column):
"a block sequence cannot start on the same line as its anchor or tag at line \(line), column \(column)"
}
}
@ -144,6 +147,8 @@ public extension PureYAML.Parsing {
"commentInMultilineScalar"
case .aliasWithProperties:
"aliasWithProperties"
case .inlineBlockSequenceAfterProperty:
"inlineBlockSequenceAfterProperty"
}
}
@ -176,7 +181,8 @@ public extension PureYAML.Parsing {
let .unexpectedToken(_, _, line, _),
let .nestingTooDeep(line, _),
let .commentNotPrecededByWhitespace(line, _),
let .aliasWithProperties(line, _):
let .aliasWithProperties(line, _),
let .inlineBlockSequenceAfterProperty(line, _):
line
}
}
@ -210,7 +216,8 @@ public extension PureYAML.Parsing {
let .unexpectedToken(_, _, _, column),
let .nestingTooDeep(_, column),
let .commentNotPrecededByWhitespace(_, column),
let .aliasWithProperties(_, column):
let .aliasWithProperties(_, column),
let .inlineBlockSequenceAfterProperty(_, column):
column
}
}

View File

@ -4,6 +4,7 @@ extension PureYAML.Parsing.TokenEventParser {
allowMappingKeyTerminator: Bool = false,
) throws -> PureYAML.Parsing.NodeResult {
let start = try expect("block sequence entry") { $0.isBlockEntry }
try enforceBlockSequenceNotInlineAfterProperty(properties, firstEntry: start)
let startMark = properties.mark ?? start.mark
var endMark = start.endMark
try emit(.sequenceStart(
@ -38,6 +39,22 @@ extension PureYAML.Parsing.TokenEventParser {
return PureYAML.Parsing.NodeResult(kind: .sequence, endMark: endMark)
}
/// A block collection must begin on the line after its node properties, so an anchor or tag on
/// the same line as the sequence's first entry (`&a - x`) is malformed.
func enforceBlockSequenceNotInlineAfterProperty(
_ properties: PureYAML.Parsing.NodeProperties,
firstEntry: PureYAML.Parsing.Token,
) throws {
guard properties.anchor != nil || properties.tag != nil,
let propertyMark = properties.mark, propertyMark.line == firstEntry.mark.line
else {
return
}
throw PureYAML.Parsing.ParseError.inlineBlockSequenceAfterProperty(
line: propertyMark.line, column: propertyMark.column,
)
}
mutating func consumeSequenceItemBoundaryDedent(sequenceIndentWidth: Int) throws {
guard dedentWidth(cursor.current) == sequenceIndentWidth,
try cursor.peek()?.kind.isBlockEntry == true

View File

@ -0,0 +1,26 @@
@testable import PureYAML
import Testing
@Suite("A block sequence after a node property")
struct BlockSequenceAfterPropertyTests {
@Test("A block sequence on the same line as its anchor or tag is rejected", arguments: [
"&anchor - sequence entry\n", // an anchor immediately before a block entry
"!!seq - x\n", // a tag immediately before a block entry
])
func test_inlineBlockSequenceAfterPropertyRejected(_ yaml: String) throws {
#expect(throws: PureYAML.Parsing.ParseError.self) {
try PureYAML.parse(yaml)
}
}
@Test("Properties positioned correctly around a sequence stay valid", arguments: [
"&anchor\n- a\n- b\n", // the anchor on its own line, sequence below
"- &anchor x\n- y\n", // the anchor inside a sequence item
"&anchor [a, b]\n", // a flow sequence may follow an anchor inline
"key: &anchor\n - a\n - b\n", // the anchor then an indented sequence
"&anchor value\n", // a scalar may follow an anchor inline
])
func test_validSequencePropertyPlacement(_ yaml: String) throws {
_ = try PureYAML.parse(yaml)
}
}