diff --git a/CHANGELOG.md b/CHANGELOG.md index 5df8a5f..108ab8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Sources/Parsing/TokenEventParser.swift b/Sources/Parsing/TokenEventParser.swift index 68965dd..7b6c064 100644 --- a/Sources/Parsing/TokenEventParser.swift +++ b/Sources/Parsing/TokenEventParser.swift @@ -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) + } } diff --git a/Tests/ExplicitKeyTests.swift b/Tests/ExplicitKeyTests.swift index 8396d49..1f358b9 100644 --- a/Tests/ExplicitKeyTests.swift +++ b/Tests/ExplicitKeyTests.swift @@ -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), + ]))) + } }