diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ffb47d..1ba67c7 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 entry can no longer follow a flow collection that closed earlier on the same + line (`- { y: z }- invalid`, `- [a] - b`); the entry would begin a sequence inline after a + complete node. The scanner now tracks a top-level flow close on the current line and rejects a + block entry after it, while a nested block entry (`- -`, which has no preceding flow close), a + flow collection on its own line, and nested flow collections are unaffected. Lifts official + test-suite parse-level conformance from 276/333 (82.9%) to 277/333 (83.2%), with no regression. - A mapping value's anchor or tag on the line below the key must now be indented past the key. A property at the key's own indentation (`seq:` then `&anchor` in column 1) is ambiguous with a sibling and is rejected, while an inline property, a property indented under the key, and a block diff --git a/Sources/Parsing/Scanner.swift b/Sources/Parsing/Scanner.swift index bb73818..aa036e4 100644 --- a/Sources/Parsing/Scanner.swift +++ b/Sources/Parsing/Scanner.swift @@ -13,6 +13,7 @@ extension PureYAML.Parsing { state.isAtLineStart = true state.sawContentOnLine = false state.sawMappingValueOnLine = false + state.sawFlowCloseOnLine = false continue } @@ -83,6 +84,10 @@ extension PureYAML.Parsing.Scanner { // (`a: b: c`). Reset at each line break. Flow `:` do not set it, since flow allows several // pairs on a line (`{a: b, c: d}`). var sawMappingValueOnLine = false + // Whether a flow collection has closed on the current line at the top level. A block + // sequence entry cannot follow a flow node on the same line (`{a: b}- x`), unlike a nested + // block entry (`- -`), which is not preceded by a flow close. Reset at each line break. + var sawFlowCloseOnLine = false var tagHandles = [ "!": "!", "!!": "tag:yaml.org,2002:", @@ -193,6 +198,16 @@ extension PureYAML.Parsing.Scanner { if state.sawMappingValueOnLine, state.flowDepth == 0 { throw PureYAML.Parsing.ParseError.blockSequenceAsInlineMappingValue(line: state.reader.mark.line) } + // A block sequence entry cannot follow a flow collection that closed earlier on the + // line (`{a: b}- x`); the entry would begin a sequence inline after a complete node. + if state.sawFlowCloseOnLine { + throw PureYAML.Parsing.ParseError.unexpectedToken( + expected: "a line break or flow indicator", + actual: "-", + line: state.reader.mark.line, + column: state.reader.mark.column, + ) + } scanFixedToken(.blockEntry, state: &state) return true case "?" where isIndicatorBoundary(state.reader.peek(offset: 1)): @@ -220,6 +235,9 @@ extension PureYAML.Parsing.Scanner { state.flowDepth = max(0, state.flowDepth - 1) scanFixedToken(.flowSequenceEnd, state: &state) _ = state.flowContexts.popLast() + if state.flowDepth == 0 { + state.sawFlowCloseOnLine = true + } return true case "{": state.flowDepth += 1 @@ -230,6 +248,9 @@ extension PureYAML.Parsing.Scanner { state.flowDepth = max(0, state.flowDepth - 1) scanFixedToken(.flowMappingEnd, state: &state) _ = state.flowContexts.popLast() + if state.flowDepth == 0 { + state.sawFlowCloseOnLine = true + } return true case ",": scanFixedToken(.flowEntry, state: &state) diff --git a/Sources/Parsing/TokenCursor.swift b/Sources/Parsing/TokenCursor.swift index dc7405f..c6e2f7d 100644 --- a/Sources/Parsing/TokenCursor.swift +++ b/Sources/Parsing/TokenCursor.swift @@ -93,6 +93,7 @@ extension PureYAML.Parsing { scannerBox.state.isAtLineStart = true scannerBox.state.sawContentOnLine = false scannerBox.state.sawMappingValueOnLine = false + scannerBox.state.sawFlowCloseOnLine = false continue } diff --git a/Tests/BlockEntryAfterFlowTests.swift b/Tests/BlockEntryAfterFlowTests.swift new file mode 100644 index 0000000..af57e0d --- /dev/null +++ b/Tests/BlockEntryAfterFlowTests.swift @@ -0,0 +1,25 @@ +@testable import PureYAML +import Testing + +@Suite("A block entry after a flow collection") +struct BlockEntryAfterFlowTests { + @Test("A block entry on the same line as a closed flow collection is rejected", arguments: [ + "---\n- { y: z }- invalid\n", // a block entry after a flow mapping + "- [a] - b\n", // a block entry after a flow sequence + ]) + func test_blockEntryAfterFlowRejected(_ yaml: String) throws { + #expect(throws: PureYAML.Parsing.ParseError.self) { + try PureYAML.parse(yaml) + } + } + + @Test("Nested entries and flow collections on their own line stay valid", arguments: [ + "- - a\n- - b\n", // a nested block sequence + "- [a]\n- [b]\n", // flow sequences, each on its own line + "- {a: b}\n- c\n", // a flow mapping item then a sibling entry + "[[a], [b]]\n", // nested flow sequences + ]) + func test_validBlockAndFlowEntries(_ yaml: String) throws { + _ = try PureYAML.parse(yaml) + } +}