diff --git a/CHANGELOG.md b/CHANGELOG.md index 43d172d..3d08fdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Sources/Parsing/ParseError.swift b/Sources/Parsing/ParseError.swift index 5a44aa7..9df5668 100644 --- a/Sources/Parsing/ParseError.swift +++ b/Sources/Parsing/ParseError.swift @@ -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 } } diff --git a/Sources/Parsing/TokenEventParser.Block.swift b/Sources/Parsing/TokenEventParser.Block.swift index be4fb1f..52616d1 100644 --- a/Sources/Parsing/TokenEventParser.Block.swift +++ b/Sources/Parsing/TokenEventParser.Block.swift @@ -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 diff --git a/Tests/BlockSequenceAfterPropertyTests.swift b/Tests/BlockSequenceAfterPropertyTests.swift new file mode 100644 index 0000000..b109536 --- /dev/null +++ b/Tests/BlockSequenceAfterPropertyTests.swift @@ -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) + } +}