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.
This commit is contained in:
parent
941c3f948a
commit
8d245ae335
@ -84,6 +84,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- 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
|
||||
before the `:` was an anchor or tag emitted no key marker, so the parser rejected the `:` with an
|
||||
`expectedNode` error. The scanner now tracks non-property content per line and synthesizes the empty
|
||||
key after a property, including directly after a block entry (`- !!str : v`). This makes
|
||||
yaml-test-suite FH7J pass; PW8X still needs the empty-anchored explicit-value form (`: &a`).
|
||||
Parse-level conformance rises from 313/333 to 314/333; should-error rejections and value-level
|
||||
matches are unchanged.
|
||||
- A line beginning with `%` inside a block scalar is now kept as literal content rather than being
|
||||
misread as a directive. A zero-indent literal or folded scalar sits at column 0, where the
|
||||
directive scan runs, so a content line such as `%!PS-Adobe-2.0` was rejected as an unsupported
|
||||
|
||||
@ -370,4 +370,24 @@ extension PureYAML.Parsing.Scanner {
|
||||
}
|
||||
return !state.sawMappingValueOnLine
|
||||
}
|
||||
|
||||
/// A `:` with no node before it on the line is an implicit empty mapping key, unless it is the
|
||||
/// value indicator of a pending explicit `?` key. This covers both a bare `: value` and a line
|
||||
/// whose only prior content is a node property (`&a : value`, `!!str : value`): the property
|
||||
/// decorates the empty key, which the preceding property tokens bind to. Synthesizes the empty
|
||||
/// key and its node so the block mapping machinery treats the pair uniformly.
|
||||
func synthesizeNullMappingKeyIfNeeded(
|
||||
_ character: Character,
|
||||
hadNonPropertyContentOnLine: Bool,
|
||||
state: inout State,
|
||||
) {
|
||||
guard character == ":", !hadNonPropertyContentOnLine, state.flowDepth == 0,
|
||||
!state.pendingExplicitKey, isAtMappingValueIndicator(state: state)
|
||||
else {
|
||||
return
|
||||
}
|
||||
let mark = state.reader.mark
|
||||
state.append(.mappingKey, mark: mark, endMark: mark)
|
||||
state.append(.scalar(value: "", style: .plain), mark: mark, endMark: mark)
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ extension PureYAML.Parsing {
|
||||
captureBlockScalarBlankLine(&state)
|
||||
state.isAtLineStart = true
|
||||
state.sawContentOnLine = false
|
||||
state.sawNonPropertyContentOnLine = false
|
||||
state.sawMappingValueOnLine = false
|
||||
state.sawFlowCloseOnLine = false
|
||||
continue
|
||||
@ -92,6 +93,11 @@ extension PureYAML.Parsing.Scanner {
|
||||
/// simple flag, not a token-line comparison, because a blank line inside a block scalar
|
||||
/// emits an empty scalar attributed to the next line, which would read as phantom content.
|
||||
var sawContentOnLine = false
|
||||
/// Like ``sawContentOnLine`` but not set by a node property (an anchor or tag). A `:` whose
|
||||
/// only preceding same-line content is a property (`&a :`, `!!str :`) is an empty implicit
|
||||
/// key carrying that property, so a null mapping key is synthesized for it exactly as for a
|
||||
/// bare `: value`. Reset at each line break.
|
||||
var sawNonPropertyContentOnLine = false
|
||||
// Whether a block-level mapping-value indicator (`:`) has been emitted on the current line.
|
||||
// Once one has, a later scalar followed by `:` is not a new implicit key: block style allows
|
||||
// only one mapping pair per line, so the document would be a forbidden inline nested mapping
|
||||
@ -166,7 +172,14 @@ extension PureYAML.Parsing.Scanner {
|
||||
)
|
||||
}
|
||||
let hadContentOnLine = state.sawContentOnLine
|
||||
let hadNonPropertyContentOnLine = state.sawNonPropertyContentOnLine
|
||||
state.sawContentOnLine = true
|
||||
// A leading `&` or `!` begins a node property (anchor or tag), which is not itself a key or
|
||||
// value; every other token character is real content. A property-only line followed by `:`
|
||||
// is thus an empty implicit key (`&a :`), synthesized below like a bare `: value`.
|
||||
if character != "&", character != "!" {
|
||||
state.sawNonPropertyContentOnLine = true
|
||||
}
|
||||
// An implicit-key mapping value (`:` preceded by its key on the same line) means the line
|
||||
// now holds a pair, so a later scalar followed by `:` cannot start another: that would be a
|
||||
// forbidden inline nested block mapping (`a: b: c`). A `:` that opens the line is an explicit-
|
||||
@ -175,7 +188,7 @@ extension PureYAML.Parsing.Scanner {
|
||||
if character == ":", hadContentOnLine, state.flowDepth == 0, isAtMappingValueIndicator(state: state) {
|
||||
state.sawMappingValueOnLine = true
|
||||
}
|
||||
synthesizeNullMappingKeyIfNeeded(character, hadContentOnLine: hadContentOnLine, state: &state)
|
||||
synthesizeNullMappingKeyIfNeeded(character, hadNonPropertyContentOnLine: hadNonPropertyContentOnLine, state: &state)
|
||||
if try scanLineIndicatorToken(character, state: &state) {
|
||||
return
|
||||
}
|
||||
@ -201,24 +214,6 @@ extension PureYAML.Parsing.Scanner {
|
||||
scanPlainScalar(&state)
|
||||
}
|
||||
|
||||
/// A `:` that opens a line with no key before it is an implicit null mapping key (`: value`),
|
||||
/// unless it is the value indicator of a pending explicit `?` key. Synthesizes the empty key and
|
||||
/// its node so the block mapping machinery treats the pair uniformly.
|
||||
func synthesizeNullMappingKeyIfNeeded(
|
||||
_ character: Character,
|
||||
hadContentOnLine: Bool,
|
||||
state: inout State,
|
||||
) {
|
||||
guard character == ":", !hadContentOnLine, state.flowDepth == 0, !state.pendingExplicitKey,
|
||||
isAtMappingValueIndicator(state: state)
|
||||
else {
|
||||
return
|
||||
}
|
||||
let mark = state.reader.mark
|
||||
state.append(.mappingKey, mark: mark, endMark: mark)
|
||||
state.append(.scalar(value: "", style: .plain), mark: mark, endMark: mark)
|
||||
}
|
||||
|
||||
func scanLineIndicatorToken(
|
||||
_ character: Character,
|
||||
state: inout State,
|
||||
@ -372,6 +367,9 @@ extension PureYAML.Parsing.Scanner {
|
||||
state.append(kind, mark: start, endMark: state.reader.mark)
|
||||
if case .blockEntry = kind {
|
||||
recordBlockEntryImplicitIndentation(start: start, state: &state)
|
||||
// A block entry opens a fresh node position, so a property-only key after it on the same
|
||||
// line (`- !!str : v`) is still an empty implicit key: the `-` is not content of the key.
|
||||
state.sawNonPropertyContentOnLine = false
|
||||
}
|
||||
if case .mappingValue = kind, state.flowContexts.last?.kind == .mapping {
|
||||
state.flowContexts[state.flowContexts.count - 1].isExpectingKey = false
|
||||
|
||||
@ -92,6 +92,7 @@ extension PureYAML.Parsing {
|
||||
scanner.captureBlockScalarBlankLine(&scannerBox.state)
|
||||
scannerBox.state.isAtLineStart = true
|
||||
scannerBox.state.sawContentOnLine = false
|
||||
scannerBox.state.sawNonPropertyContentOnLine = false
|
||||
scannerBox.state.sawMappingValueOnLine = false
|
||||
scannerBox.state.sawFlowCloseOnLine = false
|
||||
continue
|
||||
|
||||
@ -20,6 +20,28 @@ struct ImplicitNullKeyTests {
|
||||
])))
|
||||
}
|
||||
|
||||
@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([
|
||||
|
||||
Loading…
Reference in New Issue
Block a user