PureYAML/Sources/Parsing/ParseError.swift
Mihaela M fcba4e1f57 fix(parsing): reject content after a document end marker
A document-end marker takes no node on its own line: only a comment or
blank may follow. The scanner skipped the rest of the marker's line, so
content such as the 'invalid' in '... invalid' was silently dropped and
the document accepted.

scanDocumentMarker now distinguishes the two markers after skipping spaces:
a document start may carry the root node on its line and leaves it for the
main loop, while a document end rejects any real content (a comment or
blank still passes). A '...' not followed by an indicator boundary is not
a marker at all and remains an ordinary plain scalar, so '...junk' still
parses as content.

Measured against the official YAML test-suite, parse-level conformance
rises from 255/333 (76.6%) to 256/333 (76.9%) with no regression.
2026-06-27 17:56:37 +02:00

191 lines
8.7 KiB
Swift

public extension PureYAML.Parsing {
/// Parser error with source line information when available.
enum ParseError: Swift.Error, Equatable, Sendable, CustomStringConvertible {
case unexpectedIndentation(line: Int)
case mixedCollectionStyles(line: Int)
case expectedAnchorName(line: Int)
case expectedAliasName(line: Int)
case expectedNode(line: Int, column: Int)
case expectedScalarKey(line: Int, column: Int)
case incompatibleYAMLDirective(line: Int)
case invalidTaggedScalar(tag: String, value: String, line: Int, column: Int)
case invalidMergeValue(line: Int, column: Int)
case unterminatedTag(line: Int)
case unterminatedQuotedString(line: Int)
case unsupportedDirective(name: String, line: Int)
case unsupportedMultiDocumentStream(line: Int)
case undefinedAlias(anchor: String, line: Int, column: Int)
case unexpectedEvent(expected: String, actual: String, line: Int, column: Int)
case unexpectedToken(expected: String, actual: String, line: Int, column: Int)
case nestingTooDeep(line: Int, column: Int)
case commentNotPrecededByWhitespace(line: Int, column: Int)
case invalidEscape(sequence: String, line: Int)
case malformedDirective(name: String, line: Int)
case duplicateDirective(name: String, line: Int)
case directiveWithoutDocument(line: Int)
case contentAfterDocumentEnd(line: Int)
public var description: String {
switch self {
case let .unexpectedIndentation(line):
"unexpected indentation at line \(line)"
case let .mixedCollectionStyles(line):
"mapping and sequence entries are mixed at line \(line)"
case let .expectedAnchorName(line):
"expected an anchor name at line \(line)"
case let .expectedAliasName(line):
"expected an alias name at line \(line)"
case let .expectedNode(line, column):
"expected a YAML node at line \(line), column \(column)"
case let .expectedScalarKey(line, column):
"expected a scalar mapping key at line \(line), column \(column)"
case let .incompatibleYAMLDirective(line):
"incompatible YAML directive at line \(line)"
case let .invalidTaggedScalar(tag, value, line, column):
"invalid scalar '\(value)' for tag '\(tag)' at line \(line), column \(column)"
case let .invalidMergeValue(line, column):
"expected merge value to be a mapping or sequence of mappings at line \(line), column \(column)"
case let .unterminatedTag(line):
"unterminated tag at line \(line)"
case let .unterminatedQuotedString(line):
"unterminated quoted string at line \(line)"
case let .unsupportedDirective(name, line):
"unsupported directive '\(name)' at line \(line)"
case let .unsupportedMultiDocumentStream(line):
"multi-document streams are not supported at line \(line)"
case let .undefinedAlias(anchor, line, column):
"undefined alias '\(anchor)' at line \(line), column \(column)"
case let .unexpectedEvent(expected, actual, line, column):
"expected \(expected) at line \(line), column \(column), found \(actual)"
case let .unexpectedToken(expected, actual, line, column):
"expected \(expected) at line \(line), column \(column), found \(actual)"
case let .nestingTooDeep(line, column):
"nesting is too deep at line \(line), column \(column)"
case let .commentNotPrecededByWhitespace(line, column):
"a comment must be preceded by whitespace at line \(line), column \(column)"
case let .invalidEscape(sequence, line):
"invalid escape sequence '\(sequence)' at line \(line)"
case let .malformedDirective(name, line):
"malformed directive '\(name)' at line \(line)"
case let .duplicateDirective(name, line):
"duplicate directive '\(name)' at line \(line)"
case let .directiveWithoutDocument(line):
"a directive must be followed by a document at line \(line)"
case let .contentAfterDocumentEnd(line):
"no content may follow a document end marker at line \(line)"
}
}
public var diagnosticCode: String {
switch self {
case .unexpectedIndentation:
"unexpectedIndentation"
case .mixedCollectionStyles:
"mixedCollectionStyles"
case .expectedAnchorName:
"expectedAnchorName"
case .expectedAliasName:
"expectedAliasName"
case .expectedNode:
"expectedNode"
case .expectedScalarKey:
"expectedScalarKey"
case .incompatibleYAMLDirective:
"incompatibleYAMLDirective"
case .invalidTaggedScalar:
"invalidTaggedScalar"
case .invalidMergeValue:
"invalidMergeValue"
case .unterminatedTag:
"unterminatedTag"
case .unterminatedQuotedString:
"unterminatedQuotedString"
case .unsupportedDirective:
"unsupportedDirective"
case .unsupportedMultiDocumentStream:
"unsupportedMultiDocumentStream"
case .undefinedAlias:
"undefinedAlias"
case .unexpectedEvent:
"unexpectedEvent"
case .unexpectedToken:
"unexpectedToken"
case .nestingTooDeep:
"nestingTooDeep"
case .commentNotPrecededByWhitespace:
"commentNotPrecededByWhitespace"
case .invalidEscape:
"invalidEscape"
case .malformedDirective:
"malformedDirective"
case .duplicateDirective:
"duplicateDirective"
case .directiveWithoutDocument:
"directiveWithoutDocument"
case .contentAfterDocumentEnd:
"contentAfterDocumentEnd"
}
}
public var sourceLine: Int? {
switch self {
case let .unexpectedIndentation(line),
let .mixedCollectionStyles(line),
let .expectedAnchorName(line),
let .expectedAliasName(line),
let .incompatibleYAMLDirective(line),
let .unterminatedTag(line),
let .unterminatedQuotedString(line),
let .unsupportedDirective(_, line),
let .unsupportedMultiDocumentStream(line),
let .invalidEscape(_, line),
let .malformedDirective(_, line),
let .duplicateDirective(_, line),
let .directiveWithoutDocument(line),
let .contentAfterDocumentEnd(line):
line
case let .expectedNode(line, _),
let .expectedScalarKey(line, _),
let .invalidTaggedScalar(_, _, line, _),
let .invalidMergeValue(line, _),
let .undefinedAlias(_, line, _),
let .unexpectedEvent(_, _, line, _),
let .unexpectedToken(_, _, line, _),
let .nestingTooDeep(line, _),
let .commentNotPrecededByWhitespace(line, _):
line
}
}
public var sourceColumn: Int? {
switch self {
case .unexpectedIndentation,
.mixedCollectionStyles,
.expectedAnchorName,
.expectedAliasName,
.incompatibleYAMLDirective,
.unterminatedTag,
.unterminatedQuotedString,
.unsupportedDirective,
.unsupportedMultiDocumentStream,
.invalidEscape,
.malformedDirective,
.duplicateDirective,
.directiveWithoutDocument,
.contentAfterDocumentEnd:
nil
case let .expectedNode(_, column),
let .expectedScalarKey(_, column),
let .invalidTaggedScalar(_, _, _, column),
let .invalidMergeValue(_, column),
let .undefinedAlias(_, _, column),
let .unexpectedEvent(_, _, _, column),
let .unexpectedToken(_, _, _, column),
let .nestingTooDeep(_, column),
let .commentNotPrecededByWhitespace(_, column):
column
}
}
}
}