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.
144 lines
6.6 KiB
Swift
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
|
|
}
|
|
}
|