A mapping value's anchor or tag on the line below the key must be indented past the key. The parser consumed such a property regardless of its column, so 'seq:' then '&anchor' in column 1, anchoring a sequence at the document level, parsed instead of failing. When the value's properties are consumed before the indent check, a properly indented one would have been preceded by an indent token and consumed inside the indented value; reaching that point with a property on a later line therefore means it sits at the key's indentation, which is rejected. The indented-value branch and the new guard move to their own helpers so parseMappingPair stays within the complexity and length limits. An inline property, an indented property, and a block sequence at the key's indent are unaffected. Measured against the official YAML test-suite, parse-level conformance rises from 275/333 (82.6%) to 276/333 (82.9%) with no regression.
333 lines
12 KiB
Swift
333 lines
12 KiB
Swift
extension PureYAML.Parsing.TokenEventParser {
|
|
mutating func parseMappingPair() throws -> PureYAML.Parsing.NodeResult {
|
|
// A subsequent pair's key may carry an anchor or tag (`&a key: …`). The first pair's
|
|
// properties are consumed when the mapping is entered; consuming here covers the rest, and
|
|
// is a no-op (`.none`) when there are none.
|
|
let keyProperties = try consumeProperties()
|
|
_ = try expect("mapping key") { $0.isMappingKey }
|
|
let key = try parseNode(properties: keyProperties)
|
|
// An explicit key (`? key`) with no following `:` has an implicit null value, so several
|
|
// `? key` entries in a row (a set, for instance) each map to null rather than being an error.
|
|
guard case .mappingValue = cursor.current?.kind else {
|
|
return try emitNullMappingValue(at: key.endMark)
|
|
}
|
|
let valueIndicator = try expect("mapping value") { kind in
|
|
if case .mappingValue = kind {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
let properties = try consumeProperties()
|
|
try enforceValuePropertyIndented(properties, valueIndicator: valueIndicator)
|
|
if cursor.current?.kind.isIndent == true {
|
|
return try parseIndentedMappingValue(properties: properties)
|
|
}
|
|
if properties == .none, try isCommentInterruptedScalarValue(after: valueIndicator) {
|
|
return try rejectCommentInterruptedScalar()
|
|
}
|
|
if cursor.current?.kind.isMappingKey == true, hasNodeOnSameLine(after: valueIndicator) {
|
|
return try parseBlockMapping(
|
|
properties: properties,
|
|
includeIndentedContinuation: true,
|
|
minimumColumn: cursor.current?.mark.column,
|
|
)
|
|
}
|
|
if properties != .none || hasNodeOnSameLine(after: valueIndicator) {
|
|
return try parseNode(properties: properties)
|
|
}
|
|
if cursor.current?.kind.isBlockEntry == true {
|
|
return try parseBlockSequence(
|
|
properties: properties,
|
|
allowMappingKeyTerminator: true,
|
|
)
|
|
}
|
|
|
|
return try emitNullMappingValue(at: valueIndicator.endMark)
|
|
}
|
|
|
|
/// Parses a mapping value that begins on an indented line below the `:`. A leading block entry
|
|
/// is a block sequence; anything else is a node. The closing dedent is consumed unless the value
|
|
/// ended on a sibling mapping key, which the surrounding mapping continues from.
|
|
mutating func parseIndentedMappingValue(
|
|
properties: PureYAML.Parsing.NodeProperties,
|
|
) throws -> PureYAML.Parsing.NodeResult {
|
|
_ = try expect("indent") { $0.isIndent }
|
|
let result: PureYAML.Parsing.NodeResult = if cursor.current?.kind.isBlockEntry == true {
|
|
try parseBlockSequence(properties: properties, allowMappingKeyTerminator: true)
|
|
} else {
|
|
try parseNode(properties: properties)
|
|
}
|
|
if cursor.current?.kind.isDedent == true {
|
|
_ = try expect("dedent") { $0.isDedent }
|
|
} else if cursor.current?.kind.isMappingKey != true {
|
|
_ = try expect("dedent") { $0.isDedent }
|
|
}
|
|
return result
|
|
}
|
|
|
|
/// A value's anchor or tag on a line below the `:` must be indented past the key. Reaching the
|
|
/// value-property consumption with a property on a later line means no indent token preceded it
|
|
/// (a properly indented one is consumed inside the indented value), so it sits at the key's
|
|
/// level and is malformed (`seq:` then `&anchor` in column 1).
|
|
func enforceValuePropertyIndented(
|
|
_ properties: PureYAML.Parsing.NodeProperties,
|
|
valueIndicator: PureYAML.Parsing.Token,
|
|
) throws {
|
|
guard properties != .none, let propertyMark = properties.mark,
|
|
propertyMark.line != valueIndicator.mark.line
|
|
else {
|
|
return
|
|
}
|
|
throw PureYAML.Parsing.ParseError.unexpectedIndentation(line: propertyMark.line)
|
|
}
|
|
|
|
/// Emit an empty (null) scalar as a mapping value and return its node result, used where a
|
|
/// mapping entry has no value: an explicit key with no `:`, or a `key:` with nothing after it.
|
|
mutating func emitNullMappingValue(at mark: PureYAML.Parsing.Mark) throws -> PureYAML.Parsing.NodeResult {
|
|
try emit(.scalar(value: "", anchor: nil, tag: nil, style: .plain, mark: mark))
|
|
return PureYAML.Parsing.NodeResult(kind: .scalar, endMark: mark)
|
|
}
|
|
|
|
/// Rejects a plain scalar that the scanner split into a value-line scalar, an indent, and a
|
|
/// further scalar. The scanner folds a genuine multi-line plain scalar into a single token, so
|
|
/// this token shape arises only when a comment broke its continuation: a comment cannot
|
|
/// interrupt a multi-line plain scalar, so the construct is malformed.
|
|
mutating func rejectCommentInterruptedScalar() throws -> PureYAML.Parsing.NodeResult {
|
|
let first = try expect("plain scalar") { kind in
|
|
if case .scalar(_, .plain) = kind {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
throw PureYAML.Parsing.ParseError.commentInMultilineScalar(line: first.mark.line)
|
|
}
|
|
|
|
mutating func consumeProperties() throws -> PureYAML.Parsing.NodeProperties {
|
|
var properties = PureYAML.Parsing.NodeProperties.none
|
|
while let token = cursor.current {
|
|
switch token.kind {
|
|
case let .anchor(anchor):
|
|
properties.anchor = anchor
|
|
properties.mark = properties.mark ?? token.mark
|
|
_ = try cursor.advance()
|
|
case let .tag(tag):
|
|
properties.tag = tag
|
|
properties.mark = properties.mark ?? token.mark
|
|
_ = try cursor.advance()
|
|
default:
|
|
return properties
|
|
}
|
|
}
|
|
return properties
|
|
}
|
|
|
|
func decodeScalar(
|
|
_ value: String,
|
|
style: PureYAML.Parsing.ScalarStyle,
|
|
line: Int,
|
|
) throws -> String {
|
|
switch style {
|
|
case .doubleQuoted:
|
|
try scalarParser.parseDoubleQuoted("\"\(value)\"", line: line)
|
|
case .folded, .literal:
|
|
value
|
|
case .plain:
|
|
value
|
|
case .singleQuoted:
|
|
try scalarParser.parseSingleQuoted("'\(value)'", line: line)
|
|
}
|
|
}
|
|
|
|
func decodedScalar(from token: PureYAML.Parsing.Token) throws -> String {
|
|
guard case let .scalar(value, style) = token.kind else {
|
|
throw unexpectedToken(expected: "scalar")
|
|
}
|
|
return try decodeScalar(value, style: style, line: token.mark.line)
|
|
}
|
|
|
|
func hasNodeOnSameLine(after token: PureYAML.Parsing.Token) -> Bool {
|
|
guard let current = cursor.current, !current.kind.isTerminator else {
|
|
return false
|
|
}
|
|
return current.mark.line == token.mark.line
|
|
}
|
|
|
|
mutating func isCommentInterruptedScalarValue(after token: PureYAML.Parsing.Token) throws -> Bool {
|
|
guard case .scalar(_, .plain) = cursor.current?.kind else {
|
|
return false
|
|
}
|
|
guard cursor.current?.mark.line == token.mark.line else {
|
|
return false
|
|
}
|
|
guard try cursor.peek()?.kind.isIndent == true else {
|
|
return false
|
|
}
|
|
guard try isScalar(cursor.peek(offset: 2)) else {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
mutating func isCommentInterruptedScalar() throws -> Bool {
|
|
guard case .scalar(_, .plain) = cursor.current?.kind else {
|
|
return false
|
|
}
|
|
guard try cursor.peek()?.kind.isIndent == true else {
|
|
return false
|
|
}
|
|
guard try isScalar(cursor.peek(offset: 2)) else {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func isScalar(_ token: PureYAML.Parsing.Token?) -> Bool {
|
|
guard case .scalar = token?.kind else {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
mutating func consumeDedents(atOrAbove minimumColumn: Int) throws {
|
|
while let width = dedentWidth(cursor.current), width >= minimumColumn {
|
|
_ = try expect("dedent") { $0.isDedent }
|
|
}
|
|
}
|
|
|
|
mutating func consumeMappingSiblingBoundaryDedents(minimumColumn: Int) throws {
|
|
while try canConsumeMappingSiblingBoundaryDedent(minimumColumn: minimumColumn) {
|
|
_ = try expect("dedent") { $0.isDedent }
|
|
}
|
|
}
|
|
|
|
mutating func canConsumeMappingSiblingBoundaryDedent(minimumColumn: Int) throws -> Bool {
|
|
guard let width = dedentWidth(cursor.current),
|
|
let next = try cursor.peek(),
|
|
next.kind.isMappingKey,
|
|
next.mark.column >= minimumColumn
|
|
else {
|
|
return false
|
|
}
|
|
return width == next.mark.column - 1
|
|
}
|
|
|
|
mutating func consumeMappingSiblingBoundaryReindent(minimumColumn: Int) throws {
|
|
guard let dedent = dedentWidth(cursor.current),
|
|
dedent < minimumColumn,
|
|
try indentWidth(cursor.peek()) == minimumColumn,
|
|
let mappingKey = try cursor.peek(offset: 2),
|
|
mappingKey.kind.isMappingKey,
|
|
mappingKey.mark.column == minimumColumn + 1
|
|
else {
|
|
return
|
|
}
|
|
_ = try expect("dedent") { $0.isDedent }
|
|
_ = try expect("indent") { $0.isIndent }
|
|
}
|
|
|
|
func dedentWidth(_ token: PureYAML.Parsing.Token?) -> Int? {
|
|
guard case let .dedent(width) = token?.kind else {
|
|
return nil
|
|
}
|
|
return width
|
|
}
|
|
|
|
func indentWidth(_ token: PureYAML.Parsing.Token?) -> Int? {
|
|
guard case let .indent(width) = token?.kind else {
|
|
return nil
|
|
}
|
|
return width
|
|
}
|
|
|
|
func canContinueBlockMapping(minimumColumn: Int?) -> Bool {
|
|
guard let current = cursor.current, current.kind.isMappingKey || startsNodeProperties(current.kind) else {
|
|
return false
|
|
}
|
|
guard let minimumColumn else {
|
|
return true
|
|
}
|
|
return current.mark.column >= minimumColumn
|
|
}
|
|
|
|
/// Whether a token kind begins a node's properties (an anchor or a tag), which a mapping key may
|
|
/// carry, so the next pair's key is `&a key: …` rather than a bare key.
|
|
func startsNodeProperties(_ kind: PureYAML.Parsing.TokenKind) -> Bool {
|
|
switch kind {
|
|
case .anchor, .tag:
|
|
true
|
|
default:
|
|
false
|
|
}
|
|
}
|
|
|
|
func canEndBlockMapping(
|
|
at token: PureYAML.Parsing.Token?,
|
|
minimumColumn: Int? = nil,
|
|
) -> Bool {
|
|
guard let token else {
|
|
return true
|
|
}
|
|
return token.kind.isBlockEntry
|
|
|| token.kind.isDedent
|
|
|| token.kind.isDocumentEnd
|
|
|| token.kind.isDocumentStart
|
|
|| token.kind.isFlowTerminator
|
|
|| isLowerColumnMappingKey(token, minimumColumn: minimumColumn)
|
|
|| token.kind.isStreamEnd
|
|
}
|
|
|
|
func isLowerColumnMappingKey(
|
|
_ token: PureYAML.Parsing.Token?,
|
|
minimumColumn: Int?,
|
|
) -> Bool {
|
|
guard let token, token.kind.isMappingKey, let minimumColumn else {
|
|
return false
|
|
}
|
|
return token.mark.column < minimumColumn
|
|
}
|
|
|
|
mutating func expect(
|
|
_ expected: String,
|
|
matching: (PureYAML.Parsing.TokenKind) -> Bool,
|
|
) throws -> PureYAML.Parsing.Token {
|
|
guard let token = cursor.current else {
|
|
let mark = cursor.previous?.endMark ?? .start
|
|
throw PureYAML.Parsing.ParseError.unexpectedToken(
|
|
expected: expected,
|
|
actual: "end of tokens",
|
|
line: mark.line,
|
|
column: mark.column,
|
|
)
|
|
}
|
|
guard matching(token.kind) else {
|
|
throw PureYAML.Parsing.ParseError.unexpectedToken(
|
|
expected: expected,
|
|
actual: token.kind.description,
|
|
line: token.mark.line,
|
|
column: token.mark.column,
|
|
)
|
|
}
|
|
_ = try cursor.advance()
|
|
return token
|
|
}
|
|
|
|
func unexpectedToken(expected: String) -> PureYAML.Parsing.ParseError {
|
|
guard let token = cursor.current else {
|
|
let mark = cursor.previous?.endMark ?? .start
|
|
return PureYAML.Parsing.ParseError.unexpectedToken(
|
|
expected: expected,
|
|
actual: "end of tokens",
|
|
line: mark.line,
|
|
column: mark.column,
|
|
)
|
|
}
|
|
return PureYAML.Parsing.ParseError.unexpectedToken(
|
|
expected: expected,
|
|
actual: token.kind.description,
|
|
line: token.mark.line,
|
|
column: token.mark.column,
|
|
)
|
|
}
|
|
}
|