PureYAML/Sources/Parsing/TokenEventParser.Flow.swift
Mihaela M c1610f1eec fix(parsing): treat colon-led flow sequence element as empty-key map
A flow sequence element that begins with `:` is a single-pair mapping with an
empty key, e.g. `[ : value ]` is `[{null: value}]`, but parseFlowSequence ran
the element through parseNode, which found the leading `:` and raised
"expected a YAML node".

Detect the leading mapping-value indicator and emit a single-pair flow mapping
with a null key and the following value (itself possibly empty), the sequence
counterpart of the scalar-key element `[ a: b ]` that already becomes a
mapping through its implicit-key marker. Plain elements and scalar-key mapping
elements are untouched, and the allowance is confined to flow sequence element
position.

Raises yaml-test-suite parse-level to 298/333 (89.5%) with the should-error
rejection rate unchanged. 426 tests pass.
2026-06-28 19:43:56 +02:00

163 lines
7.1 KiB
Swift

extension PureYAML.Parsing.TokenEventParser {
mutating func parseFlowSequence(
properties: PureYAML.Parsing.NodeProperties,
) throws -> PureYAML.Parsing.NodeResult {
let start = try expect("flow sequence start") { kind in
if case .flowSequenceStart = kind {
return true
}
return false
}
let startMark = properties.mark ?? start.mark
var endMark = start.endMark
try emit(.sequenceStart(
anchor: properties.anchor,
tag: properties.tag,
style: .flow,
mark: startMark,
))
var hasEntry = false
while cursor.current != nil {
if case .flowSequenceEnd = cursor.current?.kind {
let end = try expect("flow sequence end") { kind in
if case .flowSequenceEnd = kind {
return true
}
return false
}
endMark = end.endMark
try emit(.sequenceEnd(mark: endMark))
return PureYAML.Parsing.NodeResult(kind: .sequence, endMark: endMark)
}
if case .flowEntry = cursor.current?.kind {
// A comma separates entries, so one must precede it: a leading or repeated comma
// is an empty entry, which is not allowed. A single trailing comma before the
// closing bracket is fine, since the close is handled above regardless of `hasEntry`.
guard hasEntry else {
throw unexpectedToken(expected: "flow sequence entry or flow sequence end")
}
hasEntry = false
_ = try cursor.advance()
continue
}
// A flow sequence element that begins with `:` is a single-pair mapping with an empty
// key, e.g. `[ : value ]`; an element that begins with a node and is then followed by `:`
// already becomes a mapping through the scalar key's implicit-key marker (`[ a: b ]`).
if case .mappingValue = cursor.current?.kind {
endMark = try parseEmptyKeyFlowMappingElement().endMark
} else {
endMark = try parseNode().endMark
}
hasEntry = true
if cursor.current?.kind.isFlowTerminator == false {
throw unexpectedToken(expected: "flow entry or flow sequence end")
}
}
throw unexpectedToken(expected: "flow sequence end")
}
/// Emits a single-pair flow mapping with an empty (null) key for a flow sequence element that
/// begins with `:`. The value follows the `:` and may itself be empty before the next separator.
mutating func parseEmptyKeyFlowMappingElement() throws -> PureYAML.Parsing.NodeResult {
let mark = cursor.current?.mark ?? .start
try emit(.mappingStart(anchor: nil, tag: nil, style: .flow, mark: mark))
try emit(.scalar(value: "", anchor: nil, tag: nil, style: .plain, mark: mark))
let valueIndicator = try expect("mapping value") { kind in
if case .mappingValue = kind {
return true
}
return false
}
let properties = try consumeProperties()
let endMark: PureYAML.Parsing.Mark
if properties == .none, cursor.current?.kind.isFlowTerminator != false {
endMark = valueIndicator.endMark
try emit(.scalar(value: "", anchor: nil, tag: nil, style: .plain, mark: endMark))
} else {
endMark = try parseNode(properties: properties).endMark
}
try emit(.mappingEnd(mark: endMark))
return PureYAML.Parsing.NodeResult(kind: .mapping, endMark: endMark)
}
mutating func parseFlowMapping(
properties: PureYAML.Parsing.NodeProperties,
) throws -> PureYAML.Parsing.NodeResult {
let start = try expect("flow mapping start") { kind in
if case .flowMappingStart = kind {
return true
}
return false
}
let startMark = properties.mark ?? start.mark
var endMark = start.endMark
try emit(.mappingStart(
anchor: properties.anchor,
tag: properties.tag,
style: .flow,
mark: startMark,
))
var hasEntry = false
while cursor.current != nil {
if case .flowMappingEnd = cursor.current?.kind {
let end = try expect("flow mapping end") { kind in
if case .flowMappingEnd = kind {
return true
}
return false
}
endMark = end.endMark
try emit(.mappingEnd(mark: endMark))
return PureYAML.Parsing.NodeResult(kind: .mapping, endMark: endMark)
}
if case .flowEntry = cursor.current?.kind {
// As in a flow sequence, a comma separates entries and so requires one before it;
// a leading or repeated comma is an empty entry. A trailing comma is fine.
guard hasEntry else {
throw unexpectedToken(expected: "flow mapping entry or flow mapping end")
}
hasEntry = false
_ = try cursor.advance()
continue
}
endMark = try parseFlowMappingPair().endMark
hasEntry = true
if cursor.current?.kind.isFlowTerminator == false {
throw unexpectedToken(expected: "flow entry or flow mapping end")
}
}
throw unexpectedToken(expected: "flow mapping end")
}
mutating func parseFlowMappingPair() throws -> PureYAML.Parsing.NodeResult {
if cursor.current?.kind.isMappingKey == true {
return try parseMappingPair()
}
// A flow mapping key may be empty (`{ : v }`) or carry only properties (`{ !!str : v }`):
// both resolve to a null key, so emit one rather than requiring a node before the `:`. This is
// valid only in flow mapping key position, so the empty-node allowance stays local here.
let keyProperties = try consumeProperties()
if case .mappingValue = cursor.current?.kind {
let mark = keyProperties.mark ?? cursor.current?.mark ?? .start
try emit(.scalar(value: "", anchor: keyProperties.anchor, tag: keyProperties.tag, style: .plain, mark: mark))
} else {
_ = try parseNode(properties: keyProperties)
}
let valueIndicator = try expect("mapping value") { kind in
if case .mappingValue = kind {
return true
}
return false
}
let properties = try consumeProperties()
guard properties != .none || cursor.current?.kind.isFlowTerminator == false else {
let mark = valueIndicator.endMark
try emit(.scalar(value: "", anchor: nil, tag: nil, style: .plain, mark: mark))
return PureYAML.Parsing.NodeResult(kind: .scalar, endMark: mark)
}
return try parseNode(properties: properties)
}
}