fix(parsing): parse an explicit key inside a flow collection

A flow `?` did not mark an explicit key, so the scalar after it synthesized
its own implicit key: `{? foo : bar}` parsed as `{{foo: bar}: null}` instead of
`{foo: bar}`, and `[? foo : bar]` the same way.

Track the explicit-key state per flow level on the flow context: set it at a
flow `?`, clear it at that level's `:` or a `,`, and discard it with the
context when the collection closes. The key scalar then is not turned into an
implicit key, the pair's `:` is its value indicator, and a nested collection's
`:` does not affect an outer key (`{? {a: b} : c}`). The continuation fold for
a multi-line explicit key now also consults the flow context, restoring
`[? foo\n bar : baz]` to the single key `foo bar`.

The block explicit-key flag is left untouched, since it is not cleared between
valueless `? a` / `? b` entries and folding it into this check regressed
block sets; only the flow path is changed here.

Raises yaml-test-suite value-level conformance to 192/194 (99.0%) with the
parse-level and should-error rejection rates unchanged. 472 tests pass.
This commit is contained in:
Mihaela M 2026-06-29 01:17:26 +02:00
parent 143624be08
commit 95c5ff3966
5 changed files with 95 additions and 4 deletions

View File

@ -84,6 +84,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- An explicit key (`?`) inside a flow collection is now parsed correctly. The flow `?` did not mark an
explicit key, so the following scalar synthesized its own implicit key: `{? foo : bar}` came out as
`{{foo: bar}: null}` rather than `{foo: bar}`, and `[? foo : bar]` likewise. The explicit-key state
is now tracked per flow level on the flow context, so the key scalar is not turned into an implicit
key, a `:` at that level is the pair's value indicator, a `,` ends the key, and a nested collection's
`:` does not affect an outer key (`{? {a: b} : c}`). The key may also span lines, folding to one key,
which restores the multi-line flow case `[? foo\n bar : baz]`. Raises yaml-test-suite value-level
conformance to 192/194 (99.0%), with the parse and should-error rates unchanged.
- An explicit key (`? foo`) whose plain scalar spans lines now folds a continuation line that ends at
the value `:`. `? foo` then `bar : baz` is the key `foo bar`, not an error: the `:` is the key's
value indicator, so the line stays key content and folds in, where before the scalar dropped it and

View File

@ -2,5 +2,10 @@ extension PureYAML.Parsing {
struct ScannerFlowContext {
var kind: ScannerFlowContextKind
var isExpectingKey: Bool
/// Whether an explicit key indicator (`?`) is open at this flow level, so the following scalar
/// is the explicit key's content and must not synthesize its own implicit key. Cleared by the
/// pair's `:` value indicator or a `,`, and discarded with the context when the collection
/// closes, so it tracks per-level and a nested collection's `:` does not affect an outer key.
var hasExplicitKey = false
}
}

View File

@ -174,8 +174,12 @@ extension PureYAML.Parsing.Scanner {
// A continuation line ending at `:` is normally a new implicit key, so the scalar
// stops before it. Under an explicit key (`? foo` then `bar :`), the `:` is the key's
// value indicator instead, so the line is still key content: fold it in and stop, and
// the `:` is scanned next as the value indicator.
guard state.pendingExplicitKey else {
// the `:` is scanned next as the value indicator. The explicit key is tracked on the
// block flag or, in flow, on the current flow context.
let inExplicitKey = state.flowDepth > 0
? state.flowContexts.last?.hasExplicitKey == true
: state.pendingExplicitKey
guard inExplicitKey else {
return value
}
state = continuationState
@ -345,8 +349,12 @@ extension PureYAML.Parsing.Scanner {
/// Whether a scalar followed by `:` may start an implicit mapping key here. In block style a
/// line holds at most one mapping pair, so once a block-level `:` has appeared on the line a
/// further key would form a forbidden inline nested mapping; in flow style several pairs per
/// line are allowed, so the restriction does not apply.
/// line are allowed, so that restriction does not apply. In flow a scalar that is the content of
/// an open explicit key (`?`) is the key itself, not a new implicit key.
func canSynthesizeImplicitBlockKey(_ state: State) -> Bool {
state.flowDepth > 0 || !state.sawMappingValueOnLine
if state.flowDepth > 0 {
return state.flowContexts.last?.hasExplicitKey != true
}
return !state.sawMappingValueOnLine
}
}

View File

@ -293,6 +293,9 @@ extension PureYAML.Parsing.Scanner {
if state.flowContexts.last?.kind == .mapping {
state.flowContexts[state.flowContexts.count - 1].isExpectingKey = true
}
if !state.flowContexts.isEmpty {
state.flowContexts[state.flowContexts.count - 1].hasExplicitKey = false
}
return true
default:
return false
@ -378,5 +381,13 @@ extension PureYAML.Parsing.Scanner {
if state.flowDepth == 0, case .mappingValue = kind {
state.pendingExplicitKey = false
}
// In flow the same role is tracked on the current flow context, so it is per-level and a
// nested collection's `:` does not affect an outer explicit key (`{? {a: b} : c}`).
if state.flowDepth > 0, !state.flowContexts.isEmpty, case .mappingKey = kind {
state.flowContexts[state.flowContexts.count - 1].hasExplicitKey = true
}
if state.flowDepth > 0, !state.flowContexts.isEmpty, case .mappingValue = kind {
state.flowContexts[state.flowContexts.count - 1].hasExplicitKey = false
}
}
}

View File

@ -0,0 +1,58 @@
@testable import PureYAML
import Testing
/// An explicit key (`?`) inside a flow collection makes the following scalar the key itself, not a new
/// implicit key, and lets that key span lines. The explicit-key state is tracked per flow level, so a
/// nested collection's `:` does not affect an outer key, a `,` ends it, and the closing bracket
/// discards it. Ordinary flow mappings, where each `a: b` is an implicit pair, are unaffected.
@Suite("Flow explicit key")
struct FlowExplicitKeyTests {
@Test("A flow mapping explicit key takes its value")
func test_flowMappingExplicitKey() throws {
#expect(try PureYAML.parse("{? foo : bar}\n") == .mapping(.init([
.init(key: "foo", value: .string("bar")),
])))
}
@Test("A flow sequence explicit-key entry is a single-pair mapping")
func test_flowSequenceExplicitKey() throws {
#expect(try PureYAML.parse("[? foo : bar]\n") == .sequence([
.mapping(.init([.init(key: "foo", value: .string("bar"))])),
]))
}
@Test("An explicit key spans lines, folding to a single key")
func test_multilineExplicitKey() throws {
#expect(try PureYAML.parse("[? foo\n bar : baz]\n") == .sequence([
.mapping(.init([.init(key: "foo bar", value: .string("baz"))])),
]))
}
@Test("Explicit and implicit pairs mix in one flow mapping")
func test_mixedPairs() throws {
#expect(try PureYAML.parse("{a: 1, ? b : 2}\n") == .mapping(.init([
.init(key: "a", value: .int(1)),
.init(key: "b", value: .int(2)),
])))
#expect(try PureYAML.parse("{? a, ? b, c: 1}\n") == .mapping(.init([
.init(key: "a", value: .null),
.init(key: "b", value: .null),
.init(key: "c", value: .int(1)),
])))
}
@Test("A nested collection key is unaffected by the outer explicit key")
func test_nestedCollectionKey() throws {
#expect(try PureYAML.parse("{? {a: b} : c}\n") == .mapping(.init([
.init(keyNode: .mapping(.init([.init(key: "a", value: .string("b"))])), value: .string("c")),
])))
}
@Test("An ordinary flow mapping is unchanged")
func test_ordinaryFlowMapping() throws {
#expect(try PureYAML.parse("{a: 1, b: 2}\n") == .mapping(.init([
.init(key: "a", value: .int(1)),
.init(key: "b", value: .int(2)),
])))
}
}