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.
This commit is contained in:
Mihaela M 2026-06-27 18:35:58 +02:00
parent 735767ced9
commit b0dc31922b
4 changed files with 56 additions and 2 deletions

View File

@ -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

View File

@ -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),

View File

@ -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)):

View File

@ -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")
}
}