PureYAML/Tests/ImplicitNullKeyTests.swift
Mihaela M 8d245ae335 fix(parsing): parse a property-only block mapping key as an empty key
A block key that is only a node property (`&a : v`, `!!str : v`) parsed to an
`expectedNode` error. The scanner synthesized an empty mapping key for a bare
`: v` line but not when the line's sole content before the `:` was an anchor or
tag, so no key marker was emitted and the parser rejected the `:`.

Track non-property content per line (a leading `&`/`!` does not count, and a
block entry opens a fresh node position) and synthesize the empty key whenever
only properties precede the `:`. The property tokens already bind to the
synthesized empty scalar through the existing mapping machinery, so no parser
change is needed. Relocate the synthesis helper to Scanner.Helpers to keep
Scanner under the file-length limit.

yaml-test-suite FH7J now passes (parse-level 313/333 to 314/333); PW8X still
needs the empty-anchored explicit-value form. should-error rejections and
value-level matches are unchanged, and a value-level regression test pins the
produced tree for each property-only-key shape.
2026-06-29 08:26:50 +02:00

56 lines
2.3 KiB
Swift

@testable import PureYAML
import Testing
@Suite("Implicit null mapping keys")
struct ImplicitNullKeyTests {
@Test("A `:` opening a line is an implicit null key")
func test_implicitNullKey() throws {
#expect(try PureYAML.parse(": value\n") == .mapping(.init([
.init(keyNode: .string(""), value: .string("value")),
])))
}
@Test("Several null-keyed entries and a bare `:` parse")
func test_multipleNullKeys() throws {
_ = try PureYAML.parse(": a\n: b\n")
_ = try PureYAML.parse(":\n")
#expect(try PureYAML.parse("a: 1\n: b\n") == .mapping(.init([
.init(key: "a", value: .int(1)),
.init(keyNode: .string(""), value: .string("b")),
])))
}
@Test("A property-only key before `:` is an empty key carrying that property")
func test_propertyOnlyKey() throws {
// An anchor or tag with no scalar before the `:` decorates an empty implicit key: the block
// analogue of the flow `{ !!str : v }` case. The resolved key is the empty scalar; the
// property binds to it during construction.
#expect(try PureYAML.parse("&a : b\n") == .mapping(.init([
.init(keyNode: .string(""), value: .string("b")),
])))
#expect(try PureYAML.parse("!!str : a\n") == .mapping(.init([
.init(keyNode: .string(""), value: .string("a")),
])))
}
@Test("A property-only key after a block entry is an empty key")
func test_propertyOnlyKeyAfterBlockEntry() throws {
// The `-` opens a fresh node position, so `!!str :` after it is still an empty implicit key
// rather than a malformed node.
#expect(try PureYAML.parse("- !!str : a\n") == .sequence([
.mapping(.init([.init(keyNode: .string(""), value: .string("a"))])),
]))
}
@Test("Explicit keys, including a complex one, are not mistaken for null keys")
func test_explicitKeysUnaffected() throws {
#expect(try PureYAML.parse("? a\n: b\n") == .mapping(.init([
.init(key: "a", value: .string("b")),
])))
// The `:` after a flow complex key is the key's value, not a null key.
#expect(try PureYAML.parse("? {a: b}\n: c\n") == .mapping(.init([
.init(keyNode: .mapping(.init([.init(key: "a", value: .string("b"))])), value: .string("c")),
])))
}
}