PureYAML/Sources/Parsing/TokenEventParser.EmptyNode.swift
Mihaela Mihaljevic dfb498a89c fix(parsing): accept anchors, tags, and aliases as mapping keys
A mapping key is a full node: it may carry &anchor and !tag properties and may itself be an *alias. Fixes: (1) anchor/alias name scanning now stops at the first non-name character per yaml_parser_scan_anchor, so *a: reads as alias 'a' plus a mapping indicator instead of name 'a:'; (2) the block-mapping parser enters on a leading node property or alias, attaches same-line key properties to the key node, and emits alias keys directly; (3) an anchor or tag at value position with no same-line node composes an empty (null) value carrying the property, so 'a: &anchor' registers the anchor; (4) the composer registers an anchor's resolved model value (an empty anchored scalar registers null); (5) implicit keys with trailing spaces before the colon are recognized.

Flips 10 official YAML test suite cases to passing (280/402), including G9HC now correctly rejected. 2SXE no longer false-rejects but still value-mismatches on compact-mapping interpretation (a separate gap), so it is reclassified in the manifest rather than removed. Updates the manifest, fixtures, counts, and roadmap.
2026-06-23 21:07:47 +02:00

144 lines
6.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(
preconsumedProperties: PureYAML.Parsing.NodeProperties = .none,
) throws {
// A key node may carry leading anchor/tag properties, which the scanner
// emits before the implicit `mappingKey` indicator (for example
// `&anchor c: 3` scans as anchor, `?`, `c`). It may also itself be an
// alias (`*b : value`), which has no `mappingKey` indicator at all.
// `preconsumedProperties` are properties the caller already consumed
// because they sat on the same line as the first key of this mapping.
var leadingProperties = preconsumedProperties
try leadingProperties.mergeUnset(from: consumeProperties())
if leadingProperties != .none {
try parseKeyWithLeadingProperties(leadingProperties)
return
}
if cursor.current?.kind.isAlias == true {
try emitAliasKey()
return
}
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()
// An alias as an explicit `? *alias` key is the key node itself; emit it
// directly so the following `:` is read as this pair's value separator
// rather than re-routing the alias into a nested block mapping.
if properties == .none, cursor.current?.kind.isAlias == true {
try emitAliasKey()
return
}
if properties != .none || canBeginNode(cursor.current) {
_ = try parseNode(properties: properties)
return
}
try emitEmptyScalar(properties: properties, mark: keyIndicator.endMark)
}
/// Consumes an alias token that stands in mapping-key position and emits the
/// alias event for it. Done here rather than through ``parseNode`` because an
/// alias followed by `:` routes back into a block mapping in ``parseNode``;
/// the key parser owns the alias key directly so it resolves to its value.
mutating func emitAliasKey() throws {
let token = try expect("alias") { $0.isAlias }
guard case let .alias(name) = token.kind else {
throw unexpectedToken(expected: "alias")
}
try emit(.alias(anchor: name, mark: token.mark))
}
/// Parses a key node whose anchor/tag properties were already consumed.
/// The scanner emits properties before the implicit `mappingKey` indicator,
/// so the indicator (if present) is skipped here and the remaining content,
/// or an empty scalar, carries the properties.
mutating func parseKeyWithLeadingProperties(
_ properties: PureYAML.Parsing.NodeProperties,
) throws {
if cursor.current?.kind.isMappingKey == true {
let keyIndicator = try expect("mapping key") { $0.isMappingKey }
if canBeginNode(cursor.current), hasNodeOnSameLine(after: keyIndicator) {
_ = try parseNode(properties: properties)
return
}
try emitEmptyScalar(properties: properties, mark: keyIndicator.endMark)
return
}
if canBeginNode(cursor.current) {
_ = try parseNode(properties: properties)
return
}
let mark = properties.mark ?? cursor.current?.mark ?? cursor.previous?.endMark ?? .start
try emitEmptyScalar(properties: properties, mark: mark)
}
/// 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
}
}