From d1d5b1a37cea6f49379b06ab90eb6c11bfdba684 Mon Sep 17 00:00:00 2001 From: Mihaela M Date: Mon, 29 Jun 2026 21:40:12 +0200 Subject: [PATCH] fix(parsing): parse a flow mapping value on the next line In a flow mapping the value is a flow node and may sit on any line; flow is line insensitive. parseMappingPair gated value parsing on hasNodeOnSameLine, a block-only rule, so a value after `:` on the next line was treated as null and left unconsumed, failing the whole flow mapping. Thread an inFlow flag and, in flow, parse the value as a flow node line-insensitively (only a flow terminator leaves it null), bypassing the block-only same-line and indentation branches. Pretty-printed JSON (a YAML 1.2 superset) relies on this. Against the JSON Schema Test Suite corpus this fixes all 27 files that previously failed to parse (432/432 now); the YAML test-suite (318/333, value-level 206/206) and the full suite stay green. --- Sources/Parsing/TokenEventParser.Flow.swift | 2 +- .../Parsing/TokenEventParser.Helpers.swift | 11 +++++ Tests/FlowMultilineValueTests.swift | 49 +++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 Tests/FlowMultilineValueTests.swift diff --git a/Sources/Parsing/TokenEventParser.Flow.swift b/Sources/Parsing/TokenEventParser.Flow.swift index 3d0ce49..e923bce 100644 --- a/Sources/Parsing/TokenEventParser.Flow.swift +++ b/Sources/Parsing/TokenEventParser.Flow.swift @@ -131,7 +131,7 @@ extension PureYAML.Parsing.TokenEventParser { mutating func parseFlowMappingPair() throws -> PureYAML.Parsing.NodeResult { if cursor.current?.kind.isMappingKey == true { - return try parseMappingPair() + return try parseMappingPair(inFlow: true) } // A flow mapping key may be empty (`{ : v }`) or carry only properties (`{ !!str : v }`): diff --git a/Sources/Parsing/TokenEventParser.Helpers.swift b/Sources/Parsing/TokenEventParser.Helpers.swift index ef8e110..669b8fa 100644 --- a/Sources/Parsing/TokenEventParser.Helpers.swift +++ b/Sources/Parsing/TokenEventParser.Helpers.swift @@ -1,6 +1,7 @@ extension PureYAML.Parsing.TokenEventParser { mutating func parseMappingPair( firstKeyProperties: PureYAML.Parsing.NodeProperties = .none, + inFlow: Bool = false, ) throws -> PureYAML.Parsing.NodeResult { // A subsequent pair's key may carry an anchor or tag (`&a key: …`). The first pair's inline // properties are consumed when the mapping is entered and passed back in here; consuming @@ -22,6 +23,16 @@ extension PureYAML.Parsing.TokenEventParser { } let properties = try consumeProperties() + if inFlow { + // In a flow mapping the value is a flow node and may sit on any line: flow is line + // insensitive, so the block-only same-line and indentation rules below do not apply. + // Only a flow terminator (`,`, `}`, `]`) or the stream end leaves the value empty (null), + // mirroring the empty-value rule in `parseFlowMappingPair` for the non-`?` key path. + guard properties != .none || cursor.current?.kind.isFlowTerminator == false else { + return try emitNullMappingValue(at: valueIndicator.endMark) + } + return try parseNode(properties: properties) + } try enforceValuePropertyIndented(properties, valueIndicator: valueIndicator) if cursor.current?.kind.isIndent == true { return try parseIndentedMappingValue(properties: properties) diff --git a/Tests/FlowMultilineValueTests.swift b/Tests/FlowMultilineValueTests.swift new file mode 100644 index 0000000..864d107 --- /dev/null +++ b/Tests/FlowMultilineValueTests.swift @@ -0,0 +1,49 @@ +@testable import PureYAML +import Testing + +/// In a flow mapping the value is a flow node and may sit on any line: flow is line insensitive, +/// unlike a block mapping whose value must be on the key's line or an indented block below it. +/// A value after `:` on the next line is the value, not a null. Pretty-printed JSON (a YAML 1.2 +/// superset) relies on this throughout, which is why it must hold. Only a flow terminator leaves +/// the value empty. +@Suite("Flow multi-line value") +struct FlowMultilineValueTests { + @Test("A flow mapping value on the line after the colon is the value") + func test_valueOnNextLine() throws { + #expect(try PureYAML.parse("{\"a\":\n\"b\"}") == .mapping(.init([ + .init(key: "a", value: .string("b")), + ]))) + } + + @Test("Several flow values, each on its own line, each bind to their key") + func test_severalNextLineValues() throws { + let yaml = "{\n\"a\":\n \"x\",\n\"b\":\n \"y\"\n}" + #expect(try PureYAML.parse(yaml) == .mapping(.init([ + .init(key: "a", value: .string("x")), + .init(key: "b", value: .string("y")), + ]))) + } + + @Test("A flow value that is itself a flow node spanning lines is parsed") + func test_nextLineValueIsFlowNode() throws { + #expect(try PureYAML.parse("{\"a\":\n[1, 2]}") == .mapping(.init([ + .init(key: "a", value: .sequence([.int(1), .int(2)])), + ]))) + } + + @Test("Near miss: a flow value omitted before a terminator is null, not the next entry") + func test_emptyFlowValueStaysNull() throws { + #expect(try PureYAML.parse("{\"a\": , \"b\": \"c\"}") == .mapping(.init([ + .init(key: "a", value: .null), + .init(key: "b", value: .string("c")), + ]))) + } + + @Test("Pretty-printed JSON (an array of objects with values on their own lines) parses") + func test_prettyPrintedJSON() throws { + let yaml = "[\n {\n \"k\":\n \"v\"\n }\n]" + #expect(try PureYAML.parse(yaml) == .sequence([ + .mapping(.init([.init(key: "k", value: .string("v"))])), + ])) + } +}