fix(parsing): allow a directive after a document-end marker

A directive following a document-end marker ('...') begins the next
document's directive section, but the scanner rejected it: the
isDocumentClosed flag set by '...' tripped the directive guard, so a
multi-document stream where each document carries its own directives
('%TAG ...' / '--- doc' / '...' / '%TAG ...' / '--- doc') failed.

A directive now reopens the stream when it follows '...', clearing the
closed flag, while a directive after document content within the same
document stays rejected via the content guard. The directive still
requires its own '---', so a directive after '...' with no document start
is still an error. Multi-document streams already composed through the
stream mode; this removes the scanner-side block on per-document
directives.

Measured against the official YAML test-suite, parse-level conformance
rises from 278/333 (83.5%) to 283/333 (85.0%) with no regression.
This commit is contained in:
Mihaela M 2026-06-28 02:09:01 +02:00
parent 25b1388dbb
commit 7e89ba1877
3 changed files with 41 additions and 1 deletions

View File

@ -23,6 +23,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- A directive after a document-end marker (`...`) now begins the next document instead of being
rejected. A multi-document stream where each document carries its own directives
(`%TAG ...` / `--- doc` / `...` / `%TAG ...` / `--- doc`) is now accepted, since a directive
following `...` opens the next document's directive section. A directive after document content
within the same document is still rejected, and a directive after `...` still requires its own
`---`. Lifts official test-suite parse-level conformance from 278/333 (83.5%) to 283/333
(85.0%), with no regression.
- 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

View File

@ -88,7 +88,12 @@ extension PureYAML.Parsing.Scanner {
guard let name = parts.first else {
return
}
guard !state.hasDocumentContent, !state.hasDocumentStartMarker, !state.isDocumentClosed else {
// A directive after a document-end marker (`...`) begins the next document's directive
// section, so it reopens the stream for a new document rather than being rejected.
if state.isDocumentClosed {
state.isDocumentClosed = false
}
guard !state.hasDocumentContent, !state.hasDocumentStartMarker else {
throw PureYAML.Parsing.ParseError.unsupportedDirective(name: name, line: start.line)
}
// Every directive must be followed by an explicit `---`; record that one is now awaiting it.

View File

@ -0,0 +1,28 @@
@testable import PureYAML
import Testing
@Suite("Directives after a document-end marker")
struct DirectiveAfterDocumentEndTests {
@Test("A directive after `...` begins the next document")
func test_directiveAfterDocumentEndStartsNextDocument() throws {
let docs = try PureYAML.parseStream("a\n...\n%YAML 1.2\n---\nb\n")
#expect(docs.count == 2)
#expect(docs[0].value == .string("a"))
#expect(docs[1].value == .string("b"))
}
@Test("A %TAG directive may be repeated for each document after `...`")
func test_repeatedTagDirectivePerDocument() throws {
let docs = try PureYAML.parseStream(
"%TAG !m! !my-\n--- !m!light fluorescent\n...\n%TAG !m! !my-\n--- !m!light green\n",
)
#expect(docs.count == 2)
}
@Test("A directive after `...` still requires its own document start")
func test_directiveAfterDocumentEndStillRequiresDocument() throws {
#expect(throws: PureYAML.Parsing.ParseError.self) {
try PureYAML.parseStream("a\n...\n%YAML 1.2\n")
}
}
}