fix(parsing): parse a bare document after a document-end marker
Content after a '...' document-end marker begins a new bare document, but the scanner rejected it as an unsupported multi-document stream and the parser expected the document to end at the marker. A stream like '--- scalar / ... / key: value' or 'a / ... / b' is two documents. The scanner now reopens the stream when content follows '...', and parseDocument returns at a '...' so the outer stream loop parses the next document, bare or marked. A trailing '...' with nothing after it still leaves a single document. The single-document composer still rejects a multi-document stream, so 'parse' is unchanged. Two tests that asserted content after '...' is an error are corrected, since the construct is valid per the grammar and the official test-suite. Measured against the official YAML test-suite, parse-level conformance rises from 288/333 (86.5%) to 289/333 (86.8%) with no regression.
This commit is contained in:
parent
3291a3701f
commit
2501cacd19
@ -23,6 +23,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- Content after a document-end marker (`...`) now begins a new bare document. A stream such as
|
||||
`--- scalar / ... / key: value` or `a / ... / b` is two documents, but the scanner rejected the
|
||||
content and the parser expected the document to end at the marker. The scanner now reopens the
|
||||
stream after `...`, and the document loop returns at a `...` so the outer loop parses the next
|
||||
document. A trailing `...` with nothing after it still leaves a single document. Lifts official
|
||||
test-suite parse-level conformance from 288/333 (86.5%) to 289/333 (86.8%), with no regression.
|
||||
- An anchor or tag with no node after it now denotes an empty node. A property always attaches to a
|
||||
node and an empty node is allowed, so `&anchor` alone, `key: &a`, or a `!!str` with nothing after
|
||||
it (`x: !!str` is the empty string, `x: !!null` is null) is the property applied to a null node
|
||||
|
||||
@ -25,8 +25,10 @@ extension PureYAML.Parsing.Scanner {
|
||||
resetForDocumentEnd(&state)
|
||||
return true
|
||||
}
|
||||
guard !state.isDocumentClosed else {
|
||||
throw PureYAML.Parsing.ParseError.unsupportedMultiDocumentStream(line: state.reader.mark.line)
|
||||
// Content after a document-end marker (`...`) begins a new bare document, so reopen the
|
||||
// stream rather than rejecting it.
|
||||
if state.isDocumentClosed {
|
||||
state.isDocumentClosed = false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@ -149,8 +149,11 @@ extension PureYAML.Parsing.TokenEventParser {
|
||||
endMark = dedent.endMark
|
||||
}
|
||||
if cursor.current?.kind.isDocumentEnd == true {
|
||||
// A `...` ends this document. Content after it begins a new bare document, parsed by the
|
||||
// outer stream loop, so emit the end and return rather than expecting a boundary here.
|
||||
let end = try expect("document end") { $0.isDocumentEnd }
|
||||
endMark = end.endMark
|
||||
try emit(.documentEnd(mark: end.endMark))
|
||||
return
|
||||
}
|
||||
if let current = cursor.current, !canEndDocument(at: current) {
|
||||
throw PureYAML.Parsing.ParseError.unexpectedToken(
|
||||
|
||||
21
Tests/BareDocumentAfterEndTests.swift
Normal file
21
Tests/BareDocumentAfterEndTests.swift
Normal file
@ -0,0 +1,21 @@
|
||||
@testable import PureYAML
|
||||
import Testing
|
||||
|
||||
@Suite("Bare documents after a document-end marker")
|
||||
struct BareDocumentAfterEndTests {
|
||||
@Test("Content after `...` is a new bare document")
|
||||
func test_bareDocumentAfterEnd() throws {
|
||||
// A document started with `---`, ended with `...`, then a bare document follows.
|
||||
let documents = try PureYAML.parseStream("---\nscalar1\n...\nkey: value\n")
|
||||
#expect(documents.count == 2)
|
||||
#expect(documents[0].value == .string("scalar1"))
|
||||
#expect(documents[1].value == .mapping(.init([.init(key: "key", value: .string("value"))])))
|
||||
}
|
||||
|
||||
@Test("A `...` with nothing after it leaves a single document")
|
||||
func test_trailingEndMarkerLeavesOneDocument() throws {
|
||||
let documents = try PureYAML.parseStream("a\n...\n")
|
||||
#expect(documents.count == 1)
|
||||
#expect(documents[0].value == .string("a"))
|
||||
}
|
||||
}
|
||||
@ -269,7 +269,6 @@ struct ScannerTests {
|
||||
// directive cannot appear) is still rejected.
|
||||
expectScannerError("value: yes\n%FOO bar", .unsupportedDirective(name: "%FOO", line: 2))
|
||||
expectScannerError("---\n%TAG !e! tag:example.com,2026:", .unsupportedDirective(name: "%TAG", line: 2))
|
||||
expectScannerError("value: one\n...\nvalue: two", .unsupportedMultiDocumentStream(line: 3))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -140,17 +140,18 @@ struct StreamParsingTests {
|
||||
)
|
||||
}
|
||||
|
||||
@Test("Stream parser rejects content after explicit document end without next start")
|
||||
func test_streamParserRejectsContentAfterExplicitDocumentEndWithoutNextStart() {
|
||||
expectStreamParseError(
|
||||
@Test("Content after an explicit document end begins a new bare document")
|
||||
func test_contentAfterExplicitDocumentEndBeginsBareDocument() throws {
|
||||
let documents = try PureYAML.parseStream(
|
||||
"""
|
||||
name: First
|
||||
...
|
||||
name: Second
|
||||
""",
|
||||
.unsupportedMultiDocumentStream(line: 3),
|
||||
description: "multi-document streams are not supported at line 3",
|
||||
)
|
||||
#expect(documents.count == 2)
|
||||
#expect(documents[0].value == .mapping(.init([.init(key: "name", value: .string("First"))])))
|
||||
#expect(documents[1].value == .mapping(.init([.init(key: "name", value: .string("Second"))])))
|
||||
}
|
||||
|
||||
@Test("Stream parser rejects extra root content without a document start")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user