From 5c99086a386c9e8a7052e6136b54862d9ac4641b Mon Sep 17 00:00:00 2001 From: Mihaela M Date: Mon, 29 Jun 2026 01:38:33 +0200 Subject: [PATCH] fix(parsing): let a flow plain key span lines A plain scalar continuation stops before a line ending at `:`, since a block implicit key may not span lines and the next line is a new key. A flow key may span lines, so the same rule wrongly split `{ multi\n line: v }`: the scalar returned just `multi` and the parser mis-read `line: v` as a separate entry. Fold across the `:` in flow as well, leaving the `:` to be scanned as the value indicator, so the key folds to `multi line`. A block implicit key keeps the previous behaviour and still stops before the `:`. Raises yaml-test-suite parse-level conformance to 301/333 (90.4%) and value-level to 193/195, with the should-error rejection rate unchanged. 476 tests pass. --- CHANGELOG.md | 7 +++++ Sources/Parsing/Scanner.Helpers.swift | 14 +++++----- Tests/MultilineFlowKeyTests.swift | 37 +++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 Tests/MultilineFlowKeyTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f093da..120cbf1 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 +- A plain scalar key in a flow collection may now span lines. A flow key, unlike a block implicit + key, is allowed to be multi-line, so `{ multi` / ` line: v }` is the key `multi line`. The scalar + continuation stopped at the `:` value indicator in every non-explicit case, dropping the second + line, so the parser mis-split the entry. Folding across the `:` now also applies in flow, while a + block implicit key still stops before such a `:`. Raises yaml-test-suite parse-level conformance to + 301/333 (90.4%) and value-level to 193/195, with the should-error rate unchanged. + - 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 diff --git a/Sources/Parsing/Scanner.Helpers.swift b/Sources/Parsing/Scanner.Helpers.swift index 5e4acc2..671a47d 100644 --- a/Sources/Parsing/Scanner.Helpers.swift +++ b/Sources/Parsing/Scanner.Helpers.swift @@ -172,14 +172,12 @@ extension PureYAML.Parsing.Scanner { let continuation = scanPlainScalarContinuationLine(&continuationState) if isAtMappingValueIndicator(state: continuationState) { // 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. 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 { + // stops before it. Two cases keep folding instead, with the `:` left to be scanned as + // the value indicator: a block explicit key (`? foo` then `bar :`), where the line is + // still the key's content, and any flow scalar, since a flow key may span lines + // (`{ multi`/`line: v }`) where a block implicit key may not. + let foldsAcrossMappingValue = state.flowDepth > 0 || state.pendingExplicitKey + guard foldsAcrossMappingValue else { return value } state = continuationState diff --git a/Tests/MultilineFlowKeyTests.swift b/Tests/MultilineFlowKeyTests.swift new file mode 100644 index 0000000..231e3f8 --- /dev/null +++ b/Tests/MultilineFlowKeyTests.swift @@ -0,0 +1,37 @@ +@testable import PureYAML +import Testing + +/// A plain scalar key in a flow collection may span lines: the continuation folds across the line +/// break even when the line ends at the `:` value indicator, since a flow key, unlike a block implicit +/// key, is allowed to be multi-line. A block implicit key still stops before such a `:`, treating the +/// next line as a new key. +@Suite("Multiline flow key") +struct MultilineFlowKeyTests { + @Test("A flow mapping key folds across a line break") + func test_flowMappingKey() throws { + #expect(try PureYAML.parse("{ multi\n line: v }\n") == .mapping(.init([ + .init(key: "multi line", value: .string("v")), + ]))) + } + + @Test("A flow sequence plain element still folds, unchanged") + func test_flowSequenceElement() throws { + #expect(try PureYAML.parse("[ multi\n line ]\n") == .sequence([.string("multi line")])) + } + + @Test("An ordinary single-line flow mapping is unchanged") + func test_singleLineFlowMapping() throws { + #expect(try PureYAML.parse("{a: 1, b: 2}\n") == .mapping(.init([ + .init(key: "a", value: .int(1)), + .init(key: "b", value: .int(2)), + ]))) + } + + @Test("A block implicit key does not span lines") + func test_blockImplicitKeyDoesNotSpan() { + // `foo` then `bar: baz` is a value and a new key, not the key `foo bar`, so this is an error. + #expect(throws: PureYAML.Parsing.ParseError.self) { + try PureYAML.parse("foo\n bar: baz\n") + } + } +}