PureYAML/Sources/Parsing/LibYAML.Parser.Document.swift
Mihaela Mihaljevic b5129d71ee fix(parsing): reject misplaced directives, refresh suite manifest
Directives apply to the document that follows, so the previous document
must be closed with an explicit '...' end marker before them. After an
implicit end, a '%YAML'/'%TAG'/reserved directive is now rejected instead
of being silently accepted. Also reject a '%YAML' version trailed directly
by '#' with no separating space ('%YAML 1.1#...'), since a comment needs
leading separation; libyaml accepts both.

This restores reject-side conformance the suite manifest recorded as
passing but the libyaml port had dropped (9HCY, EB22, MUS6/00, MUS6/01,
RHX7), and updates the two PureYAML gap-error fixtures whose rejection now
fires earlier and more precisely at the misplaced directive.

Refresh yaml-test-suite-manifest.yaml to version 3: whole-suite
conformance is 384/402 (up from the recorded 331); 53 cases still marked
as gaps now pass, none regress.
2026-06-24 19:51:39 +02:00

113 lines
4.3 KiB
Swift

extension PureYAML.Parsing.LibYAML.Parser {
/// Port of `yaml_parser_parse_stream_start`.
mutating func parseStreamStart() throws -> Event {
let token = peekToken()
guard case .streamStart = token.type else {
throw ParserError(context: nil, problem: "did not find expected <stream-start>", mark: token.startMark)
}
state = .implicitDocumentStart
skipToken()
return .streamStart(mark: token.startMark)
}
/// Port of `yaml_parser_parse_document_start`, extended for YAML 1.2: stray
/// `...` markers are skipped, then a bare document is opened when one is allowed
/// (the first document or after an explicit `...`), an explicit document is
/// opened after directives and a `---`, or STREAM-END is emitted. libyaml only
/// allows a bare document as the very first one.
mutating func parseDocumentStart() throws -> Event {
var token = peekToken()
while case .documentEnd = token.type {
skipToken()
token = peekToken()
}
// Directives apply to the document that follows, so the previous document
// must be closed with an explicit `...` end marker before them. After an
// implicit end (`allowBareDocument` is false) a directive is an error.
if isDirective(token), !allowBareDocument {
throw ParserError(
context: nil,
problem: "directives require a preceding document end marker",
mark: token.startMark,
)
}
if allowBareDocument, !isDirectiveOrBoundary(token) {
try processDirectives()
states.append(.documentEnd)
state = .blockNode
return .documentStart(explicit: false, mark: token.startMark)
}
if case .streamEnd = token.type {
state = .end
skipToken()
return .streamEnd(mark: token.startMark)
}
return try openExplicitDocument(startMark: token.startMark)
}
/// The explicit-document branch: directives then a required `---`.
private mutating func openExplicitDocument(startMark: Mark) throws -> Event {
try processDirectives()
let token = peekToken()
guard case .documentStart = token.type else {
throw ParserError(context: nil, problem: "did not find expected <document start>", mark: token.startMark)
}
states.append(.documentEnd)
state = .documentContent
skipToken()
return .documentStart(explicit: true, mark: startMark)
}
/// Port of `yaml_parser_parse_document_content`: an empty scalar stands in for
/// a document with no node, otherwise parse the root node.
mutating func parseDocumentContent() throws -> Event {
let token = peekToken()
if isDirectiveOrBoundary(token) || isDocumentEnd(token) {
state = states.removeLast()
return processEmptyScalar(at: token.startMark)
}
return try parseNode(block: true, indentlessSequence: false)
}
/// Port of `yaml_parser_parse_document_end`: reset the per-document tag
/// directives and emit DOCUMENT-END, explicit only when `...` was present.
mutating func parseDocumentEnd() throws -> Event {
let token = peekToken()
var explicit = false
if case .documentEnd = token.type {
skipToken()
explicit = true
}
tagDirectives.removeAll()
// A bare document may follow only an explicit `...` end marker.
allowBareDocument = explicit
state = .documentStart
return .documentEnd(explicit: explicit, mark: token.startMark)
}
private func isDirectiveOrBoundary(_ token: Token) -> Bool {
switch token.type {
case .versionDirective, .tagDirective, .reservedDirective, .documentStart, .streamEnd:
true
default:
false
}
}
private func isDirective(_ token: Token) -> Bool {
switch token.type {
case .versionDirective, .tagDirective, .reservedDirective:
true
default:
false
}
}
private func isDocumentEnd(_ token: Token) -> Bool {
if case .documentEnd = token.type {
return true
}
return false
}
}