From b0dc31922b91daa21432286fc300e6beaab8546c Mon Sep 17 00:00:00 2001 From: Mihaela M Date: Sat, 27 Jun 2026 18:35:58 +0200 Subject: [PATCH] fix(parsing): reject a block sequence inline as a mapping value A block sequence cannot be an implicit key's value on the same line as its ':' (key: - a); the value must begin on a following line, or use flow style. The scanner accepted the inline block entry and parsed it as the key's sequence value. The block-entry case now rejects a '-' that starts a sequence when the line already holds an implicit-key mapping value, reusing the same same-line-pair flag as the inline nested mapping rule. As there, an explicit- or null-key ':' opens its line and does not set the flag, so a compact sequence value after it stays valid, as do a sequence on the next line, a nested sequence, a flow sequence, and a negative number (key: -1). Measured against the official YAML test-suite, parse-level conformance rises from 260/333 (78.1%) to 261/333 (78.4%) with no regression. --- CHANGELOG.md | 6 ++++ Sources/Parsing/ParseError.swift | 11 ++++++-- Sources/Parsing/Scanner.swift | 7 +++++ Tests/InlineBlockSequenceValueTests.swift | 34 +++++++++++++++++++++++ 4 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 Tests/InlineBlockSequenceValueTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 016f5b2..bead216 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- A block sequence can no longer be an implicit key's value on the same line as its `:` + (`key: - a`); the value must begin on a following line, or use flow style (`key: [a]`). Like the + inline nested mapping rule, this is specific to an implicit key: an explicit- or null-key `:` + opens its line, so a compact sequence after it stays valid, as do a sequence on the next line, a + nested sequence (`- - a`), a flow sequence, and a negative number (`key: -1`). Lifts official + test-suite parse-level conformance from 260/333 (78.1%) to 261/333 (78.4%), with no regression. - An inline nested block mapping is now rejected. Block style allows only one mapping pair per line, so a scalar followed by `:` after a block-level value on the same line (`a: b: c`, `a: 'b': c`, `x: { y: z }in: valid`, or trailing content after a quoted value) no longer parses diff --git a/Sources/Parsing/ParseError.swift b/Sources/Parsing/ParseError.swift index 3d3bae1..a6c7a93 100644 --- a/Sources/Parsing/ParseError.swift +++ b/Sources/Parsing/ParseError.swift @@ -24,6 +24,7 @@ public extension PureYAML.Parsing { case duplicateDirective(name: String, line: Int) case directiveWithoutDocument(line: Int) case contentAfterDocumentEnd(line: Int) + case blockSequenceAsInlineMappingValue(line: Int) public var description: String { switch self { @@ -73,6 +74,8 @@ public extension PureYAML.Parsing { "a directive must be followed by a document at line \(line)" case let .contentAfterDocumentEnd(line): "no content may follow a document end marker at line \(line)" + case let .blockSequenceAsInlineMappingValue(line): + "a block sequence cannot start on the same line as a mapping value at line \(line)" } } @@ -124,6 +127,8 @@ public extension PureYAML.Parsing { "directiveWithoutDocument" case .contentAfterDocumentEnd: "contentAfterDocumentEnd" + case .blockSequenceAsInlineMappingValue: + "blockSequenceAsInlineMappingValue" } } @@ -142,7 +147,8 @@ public extension PureYAML.Parsing { let .malformedDirective(_, line), let .duplicateDirective(_, line), let .directiveWithoutDocument(line), - let .contentAfterDocumentEnd(line): + let .contentAfterDocumentEnd(line), + let .blockSequenceAsInlineMappingValue(line): line case let .expectedNode(line, _), let .expectedScalarKey(line, _), @@ -172,7 +178,8 @@ public extension PureYAML.Parsing { .malformedDirective, .duplicateDirective, .directiveWithoutDocument, - .contentAfterDocumentEnd: + .contentAfterDocumentEnd, + .blockSequenceAsInlineMappingValue: nil case let .expectedNode(_, column), let .expectedScalarKey(_, column), diff --git a/Sources/Parsing/Scanner.swift b/Sources/Parsing/Scanner.swift index 06897b9..43339dd 100644 --- a/Sources/Parsing/Scanner.swift +++ b/Sources/Parsing/Scanner.swift @@ -168,6 +168,13 @@ extension PureYAML.Parsing.Scanner { scanComment(&state) return true case "-" where isBlockEntry(state.reader): + // A block sequence cannot be an implicit key's value on the same line as its `:` + // (`key: - a`); the value must begin on a following line. As with an inline nested + // mapping, an explicit- or null-key `:` opens its line and does not set the flag, so a + // compact sequence value after it stays valid. + if state.sawMappingValueOnLine, state.flowDepth == 0 { + throw PureYAML.Parsing.ParseError.blockSequenceAsInlineMappingValue(line: state.reader.mark.line) + } scanFixedToken(.blockEntry, state: &state) return true case "?" where isIndicatorBoundary(state.reader.peek(offset: 1)): diff --git a/Tests/InlineBlockSequenceValueTests.swift b/Tests/InlineBlockSequenceValueTests.swift new file mode 100644 index 0000000..528e298 --- /dev/null +++ b/Tests/InlineBlockSequenceValueTests.swift @@ -0,0 +1,34 @@ +@testable import PureYAML +import Testing + +@Suite("Block sequence as an inline mapping value") +struct InlineBlockSequenceValueTests { + @Test("A block sequence on the same line as an implicit key's `:` is rejected") + func test_inlineBlockSequenceValueRejected() throws { + #expect(throws: PureYAML.Parsing.ParseError.self) { + try PureYAML.parse("key: - a\n - b\n") + } + } + + @Test("Block sequences positioned correctly, and look-alikes, stay valid", arguments: [ + "key:\n - a\n - b\n", // sequence indented on the next line + "key:\n- a\n- b\n", // sequence at the key's indentation + "- - a\n", // a nested block sequence + "- key: value\n", // a sequence item that is a mapping + "key: [a, b]\n", // a flow sequence value + "key: -1\n", // a negative number, not a block entry + "a: b - c\n", // a dash inside a plain scalar value + ]) + func test_validSequencesStillParse(_ yaml: String) throws { + _ = try PureYAML.parse(yaml) + } + + @Test("An explicit key's value on its own `:` line is not rejected as inline") + func test_explicitKeyCompactSequenceValueNotRejected() throws { + // The `:` opens the line, so it is an explicit-key value indicator and the compact sequence + // after it is allowed: the reject above is specific to an implicit key. The element folding + // of an explicit-key compact sequence is a separate, pre-existing concern, so this asserts + // only that the construct still parses rather than its exact value. + _ = try PureYAML.parse("? k\n: - a\n - b\n") + } +}