fix(parsing): parse a property-only explicit mapping key as an empty key

An explicit key that is only a node property (`? &e` then `: &a`) parsed to an
`expectedNode` error. Unlike the implicit form (`&a : v`), the scanner emits no
synthesized empty scalar for an explicit key, so the block parser read the key
via parseNode, consumed the property, then hit the `:` value indicator with no
node and threw.

parseNode now treats a `:` in node position as an empty key carrying any
properties, reusing emitEmptyNodeOrThrow (the node it already produced for a
property at end of input) without consuming the `:`, so the mapping machinery
reads the `:` as the value indicator. To stay within the cyclomatic-complexity
limit, the scalar case is extracted to parseScalarNode.

yaml-test-suite PW8X now passes (parse-level 314/333 to 315/333). should-error
rejections and value-level matches are unchanged, and a value-level test pins
the produced tree for the anchored and tagged property-only explicit keys.
This commit is contained in:
Mihaela M 2026-06-29 08:56:58 +02:00
parent 8d245ae335
commit ae29059530
3 changed files with 52 additions and 14 deletions

View File

@ -84,6 +84,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- An explicit mapping key that is only a node property (`? &e` then `: &a`) now parses as an empty
key carrying that property, completing the empty-property-key work for the explicit-key form. The
block parser rejected the `:` after a property-only explicit key with an `expectedNode` error;
`parseNode` now treats a `:` in node position as an empty property-carrying key, the same node
`emitEmptyNodeOrThrow` already produced for a property at end of input. yaml-test-suite PW8X now
passes (parse-level 314/333 to 315/333); should-error rejections and value-level matches are
unchanged.
- A block mapping key consisting only of a node property (`&a : v`, `!!str : v`) is now parsed as an
empty implicit key carrying that property, the block analogue of the flow `{ !!str : v }` case. The
scanner synthesized an empty mapping key only for a bare `: v` line; a line whose sole content

View File

@ -269,22 +269,40 @@ extension PureYAML.Parsing.TokenEventParser {
includeIndentedContinuation: false,
)
case let .scalar(value, style):
if try isCommentInterruptedScalar() {
return try rejectCommentInterruptedScalar()
}
_ = try cursor.advance()
let decoded = try decodeScalar(value, style: style, line: token.mark.line)
let mark = properties.mark ?? token.mark
try emit(.scalar(
value: decoded,
anchor: properties.anchor,
tag: properties.tag,
style: style,
mark: mark,
))
return PureYAML.Parsing.NodeResult(kind: .scalar, endMark: token.endMark)
return try parseScalarNode(value: value, style: style, token: token, properties: properties)
case .mappingValue:
// An explicit key that is only properties before its `:` (`? &e` then `: v`) is an empty
// key carrying those properties. Emit the empty scalar without consuming the `:`, so the
// mapping machinery reads it as the value indicator. With no properties this is a misplaced
// `:`, which `emitEmptyNodeOrThrow` keeps as an error. (The implicit form `&e : v` never
// reaches here: the scanner synthesizes its empty key as an explicit scalar token.)
return try emitEmptyNodeOrThrow(properties: properties)
default:
throw PureYAML.Parsing.ParseError.expectedNode(line: token.mark.line, column: token.mark.column)
}
}
/// Emits a scalar node from a scalar token, applying any node properties. A plain scalar that the
/// scanner split around a comment (the comment-interrupted shape) is rejected rather than emitted.
mutating func parseScalarNode(
value: String,
style: PureYAML.Parsing.ScalarStyle,
token: PureYAML.Parsing.Token,
properties: PureYAML.Parsing.NodeProperties,
) throws -> PureYAML.Parsing.NodeResult {
if try isCommentInterruptedScalar() {
return try rejectCommentInterruptedScalar()
}
_ = try cursor.advance()
let decoded = try decodeScalar(value, style: style, line: token.mark.line)
let mark = properties.mark ?? token.mark
try emit(.scalar(
value: decoded,
anchor: properties.anchor,
tag: properties.tag,
style: style,
mark: mark,
))
return PureYAML.Parsing.NodeResult(kind: .scalar, endMark: token.endMark)
}
}

View File

@ -31,4 +31,17 @@ struct ExplicitKeyTests {
.init(key: "c", value: .string("d")),
])))
}
@Test("An explicit key that is only a property is an empty key carrying it")
func test_explicitPropertyOnlyKey() throws {
// `? &e` is an anchored empty key, `: &a` an anchored empty value: an empty plain scalar
// resolves to the empty string as a key and to null as a value, the property binding to each
// during construction. The key is no longer rejected for having no scalar before its `:`.
#expect(try PureYAML.parse("? &e\n: &a\n") == .mapping(.init([
.init(keyNode: .string(""), value: .null),
])))
#expect(try PureYAML.parse("? !!str\n: !!null\n") == .mapping(.init([
.init(keyNode: .string(""), value: .null),
])))
}
}