fix(parsing): allow a repeated document-end marker

A `...` after a document is already closed (only comments or blank lines since
the last `...`) ends nothing: a repeated document-end marker is valid YAML, so
consume it as a no-op rather than rejecting the stream as an unsupported
multi-document case. A second documentEnd is not emitted, so no phantom null
document is composed, and real content after the marker stays an error.

Fixes suite case M7A3 (parse-level conformance 313 -> 315); adds focused tests.
This commit is contained in:
Mihaela M 2026-06-29 08:32:26 +02:00
parent 8d245ae335
commit 056095ca3d
2 changed files with 46 additions and 2 deletions

View File

@ -20,8 +20,22 @@ extension PureYAML.Parsing.Scanner {
return true
}
if startsWith("...", state.reader), isIndicatorBoundary(state.reader.peek(offset: 3)) {
guard !state.isDocumentClosed else {
throw PureYAML.Parsing.ParseError.unsupportedMultiDocumentStream(line: state.reader.mark.line)
// A `...` after a document is already closed (only comments or blank lines since the
// last `...`) ends nothing: it is a repeated document-end marker, which YAML allows. Skip
// it as a no-op rather than emitting a second documentEnd (which would compose a phantom
// null document) or rejecting a valid stream. Content after it is still an error.
if state.isDocumentClosed {
state.reader.advance()
state.reader.advance()
state.reader.advance()
while state.reader.peek() == " " {
state.reader.advance()
}
if let next = state.reader.peek(), !isLineBreak(next), next != "#" {
throw PureYAML.Parsing.ParseError.contentAfterDocumentEnd(line: state.reader.mark.line)
}
skipLine(&state)
return true
}
// A `...` ends a document, so a directive still awaiting its `---` never gets one.
try enforceDirectiveHasDocument(state)

View File

@ -0,0 +1,30 @@
@testable import PureYAML
import Testing
/// A document-end marker (`...`) may appear repeatedly: once a document is closed, a further `...`
/// (with only comments or blank lines since the last one) ends nothing and is a no-op, rather than
/// being rejected. Content other than a comment after such a marker is still an error.
@Suite("Repeated document end")
struct RepeatedDocumentEndTests {
@Test("A second `...` after a closed document, separated by a comment, is a no-op")
func test_repeatedMarkerIsNoOp() throws {
let documents = try PureYAML.parseStream("a\n...\n# comment\n...\nb\n").map(\.value)
#expect(documents == [.string("a"), .string("b")])
}
@Test("A comment-only region between two `...` markers contributes no document")
func test_commentOnlyBetweenMarkers() throws {
// Mirrors suite case M7A3: a bare document, then `...`, a comment, a repeated `...`, and a
// root block scalar as the next document.
let yaml = "Bare\ndocument\n...\n# No document\n...\n|\n%!PS-Adobe-2.0 # Not the first line\n"
let documents = try PureYAML.parseStream(yaml).map(\.value)
#expect(documents == [.string("Bare document"), .string("%!PS-Adobe-2.0 # Not the first line\n")])
}
@Test("Real content after a repeated `...` marker is still rejected")
func test_contentAfterRepeatedMarkerRejected() {
#expect(throws: PureYAML.Parsing.ParseError.self) {
_ = try PureYAML.parseStream("a\n...\n... x\n")
}
}
}