PureYAML/Sources/Parsing/TokenEventParser.EmptyNode.swift
Mihaela Mihaljevic cdf5f082f8 fix(parsing): support empty (null) nodes in every position
Compose an empty scalar wherever a node is grammatically required but the next token is a structural indicator or line/stream end, mirroring libyaml's process_empty_scalar: after a tag or anchor property with no node, as an absent mapping value (block and flow), as an absent mapping key (both the plain ': v' form and the explicit '?' form), in flow empty key/value forms ({ : }, { , : v }), and for an indented block node following properties. Also fixes a pre-existing block-sequence bug that nested a sibling '-' as a child when only properties remained on the entry line.

Empty scalars resolve through the existing resolver against the empty string, so !!str yields the empty string, bare empty or !!null yields null, and an empty key yields the empty string.

Flips 20 official YAML test suite cases to passing (270/402). CT4Q no longer false-rejects but still value-mismatches on flow plain-scalar folding (a separate gap), so it is reclassified in the manifest rather than removed. Updates the gap manifest, fixtures, counts, and roadmap.
2026-06-23 20:40:40 +02:00

83 lines
3.6 KiB
Swift

extension PureYAML.Parsing.TokenEventParser {
/// Parses the key portion of a block-mapping pair. An explicit `?` key may
/// have no following node (an empty key), and a pair written as plain
/// `: value` has no `?` and no key node at all. Both compose to an empty
/// scalar key, mirroring libyaml's empty-scalar production for an absent key.
mutating func parseMappingPairKey() throws {
guard cursor.current?.kind.isMappingKey == true else {
let mark = cursor.current?.mark ?? cursor.previous?.endMark ?? .start
try emitEmptyScalar(properties: .none, mark: mark)
return
}
let keyIndicator = try expect("mapping key") { $0.isMappingKey }
let properties = try consumeProperties()
if properties != .none || canBeginNode(cursor.current) {
_ = try parseNode(properties: properties)
return
}
try emitEmptyScalar(properties: properties, mark: keyIndicator.endMark)
}
/// Emits an empty (null) plain scalar carrying any pending anchor or tag.
/// This is the single composition point for an empty node, so `!!null`,
/// `!!str`, an anchored empty scalar, and a bare absent node all resolve
/// through the normal scalar resolver against an empty value.
mutating func emitEmptyScalar(
properties: PureYAML.Parsing.NodeProperties,
mark: PureYAML.Parsing.Mark,
) throws {
try emit(.scalar(
value: "",
anchor: properties.anchor,
tag: properties.tag,
style: .plain,
mark: properties.mark ?? mark,
))
}
/// Emits an empty scalar when a node is grammatically required but the next
/// token cannot begin one. An anchor or tag makes the empty node explicit;
/// without either, the absence is a genuine error, matching libyaml's
/// "did not find expected node content".
mutating func emptyScalarOrThrow(
properties: PureYAML.Parsing.NodeProperties,
) throws -> PureYAML.Parsing.NodeResult {
let mark = properties.mark ?? cursor.current?.mark ?? cursor.previous?.endMark ?? .start
guard properties.anchor != nil || properties.tag != nil else {
throw PureYAML.Parsing.ParseError.expectedNode(line: mark.line, column: mark.column)
}
try emitEmptyScalar(properties: properties, mark: mark)
return PureYAML.Parsing.NodeResult(kind: .scalar, endMark: mark)
}
/// True when `token` can begin a YAML node (scalar, alias, or a collection
/// opener). Properties (anchor/tag) are consumed before this is asked.
func canBeginNode(_ token: PureYAML.Parsing.Token?) -> Bool {
guard let token else {
return false
}
switch token.kind {
case .scalar, .alias, .blockScalarHeader, .blockEntry,
.flowMappingStart, .flowSequenceStart, .indent:
return true
default:
return false
}
}
/// Parses a block node introduced by a leading indent token, carrying the
/// already-consumed node properties through to the indented content. Used
/// when an anchor or tag precedes an indented block collection on the next
/// line, e.g. `key: &anchor` then an indented `!!map` mapping.
mutating func parseIndentedNode(
properties: PureYAML.Parsing.NodeProperties,
) throws -> PureYAML.Parsing.NodeResult {
_ = try expect("indent") { $0.isIndent }
let result = try parseNode(properties: properties)
if cursor.current?.kind.isDedent == true {
_ = try expect("dedent") { $0.isDedent }
}
return result
}
}