From 95c5ff39662a243219da34c5e9d53e09dd37495b Mon Sep 17 00:00:00 2001 From: Mihaela M Date: Mon, 29 Jun 2026 01:17:26 +0200 Subject: [PATCH] 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. --- CHANGELOG.md | 9 ++++ Sources/Parsing/Scanner.FlowContext.swift | 5 ++ Sources/Parsing/Scanner.Helpers.swift | 16 +++++-- Sources/Parsing/Scanner.swift | 11 +++++ Tests/FlowExplicitKeyTests.swift | 58 +++++++++++++++++++++++ 5 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 Tests/FlowExplicitKeyTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 91a8a81..6f093da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Sources/Parsing/Scanner.FlowContext.swift b/Sources/Parsing/Scanner.FlowContext.swift index 4752df3..c8b5ef5 100644 --- a/Sources/Parsing/Scanner.FlowContext.swift +++ b/Sources/Parsing/Scanner.FlowContext.swift @@ -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 } } diff --git a/Sources/Parsing/Scanner.Helpers.swift b/Sources/Parsing/Scanner.Helpers.swift index ace2812..5e4acc2 100644 --- a/Sources/Parsing/Scanner.Helpers.swift +++ b/Sources/Parsing/Scanner.Helpers.swift @@ -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 } } diff --git a/Sources/Parsing/Scanner.swift b/Sources/Parsing/Scanner.swift index 5c87eaa..55766bb 100644 --- a/Sources/Parsing/Scanner.swift +++ b/Sources/Parsing/Scanner.swift @@ -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 + } } } diff --git a/Tests/FlowExplicitKeyTests.swift b/Tests/FlowExplicitKeyTests.swift new file mode 100644 index 0000000..ac5828d --- /dev/null +++ b/Tests/FlowExplicitKeyTests.swift @@ -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)), + ]))) + } +}