fix(parsing): reject inline nested block mappings
Block style permits 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) is not a
new key but a forbidden inline nested mapping. The scanner synthesized an
implicit key for any scalar followed by ':', regardless of position, so
these parsed as nested mappings instead of failing.
The scanner now records when a line already holds a block-level mapping
pair and suppresses a further implicit key on that line, in plain and
quoted scalars alike. The flag is set only for an implicit-key ':' (one
preceded by its key on the same line): a ':' that opens its line is an
explicit- or null-key value indicator, whose value may be a compact
mapping (? k then : a: 1), so that, flow mappings, next-line nesting, and
colons inside scalars stay valid.
Measured against the official YAML test-suite, parse-level conformance
rises from 256/333 (76.9%) to 260/333 (78.1%) with no regression.
This commit is contained in:
parent
fcba4e1f57
commit
735767ced9
@ -23,6 +23,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- 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
|
||||
as a nested mapping. The scanner suppresses the implicit key it would otherwise synthesize once
|
||||
the line already holds a pair. A value indicator that opens its line is an explicit- or null-key
|
||||
indicator, whose value may legitimately be a compact mapping (`? k` then `: a: 1`), so that, flow
|
||||
mappings (`{a: b, c: d}`), next-line nesting, and colons inside scalars are unaffected. Lifts
|
||||
official test-suite parse-level conformance from 256/333 (76.9%) to 260/333 (78.1%), with no
|
||||
regression.
|
||||
- A `...` document-end marker no longer accepts content on its own line. Only a comment or blank
|
||||
may follow the marker, so `... invalid` is now rejected; the scanner previously skipped the rest
|
||||
of the line. A bare `...`, a trailing comment (`... # done`), and a `...` not followed by a
|
||||
|
||||
@ -311,10 +311,18 @@ extension PureYAML.Parsing.Scanner {
|
||||
start: PureYAML.Parsing.Mark,
|
||||
state: inout State,
|
||||
) -> Bool {
|
||||
guard isAtMappingValueIndicator(state: state) else {
|
||||
guard isAtMappingValueIndicator(state: state), canSynthesizeImplicitBlockKey(state) else {
|
||||
return false
|
||||
}
|
||||
state.append(.mappingKey, mark: start, endMark: start)
|
||||
return true
|
||||
}
|
||||
|
||||
/// 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.
|
||||
func canSynthesizeImplicitBlockKey(_ state: State) -> Bool {
|
||||
state.flowDepth > 0 || !state.sawMappingValueOnLine
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,7 +45,7 @@ extension PureYAML.Parsing.Scanner {
|
||||
// Unlike a plain scalar (which consumes the space before a `:` into itself), a quoted scalar
|
||||
// stops at its closing quote, so the reader sits at any spaces before a `:`. A space between
|
||||
// an implicit key and its mapping-value indicator is allowed, so look past them.
|
||||
if isAtMappingValueIndicatorAfterSpaces(state: state) {
|
||||
if isAtMappingValueIndicatorAfterSpaces(state: state), canSynthesizeImplicitBlockKey(state) {
|
||||
state.append(.mappingKey, mark: start, endMark: start)
|
||||
}
|
||||
state.append(.scalar(value: value, style: style), mark: start, endMark: state.reader.mark)
|
||||
|
||||
@ -12,6 +12,7 @@ extension PureYAML.Parsing {
|
||||
captureBlockScalarBlankLine(&state)
|
||||
state.isAtLineStart = true
|
||||
state.sawContentOnLine = false
|
||||
state.sawMappingValueOnLine = false
|
||||
continue
|
||||
}
|
||||
|
||||
@ -62,12 +63,18 @@ extension PureYAML.Parsing.Scanner {
|
||||
var pendingBlockScalarLeadingBlankLines = 0
|
||||
var blockScalarIndentation: Int?
|
||||
var blockScalarContentPrefix = ""
|
||||
// Whether a real token has been scanned on the current physical line. Reset when the scan
|
||||
// loop consumes a line break and set as each token is scanned, so the scanner can tell a
|
||||
// comment that opens a line (valid) from one glued to earlier content on it (invalid). A
|
||||
// simple flag, not a token-line comparison, because a blank line inside a block scalar
|
||||
// emits an empty scalar attributed to the next line, which would read as phantom content.
|
||||
/// Whether a real token has been scanned on the current physical line. Reset when the scan
|
||||
/// loop consumes a line break and set as each token is scanned, so the scanner can tell a
|
||||
/// comment that opens a line (valid) from one glued to earlier content on it (invalid). A
|
||||
/// simple flag, not a token-line comparison, because a blank line inside a block scalar
|
||||
/// emits an empty scalar attributed to the next line, which would read as phantom content.
|
||||
var sawContentOnLine = false
|
||||
// Whether a block-level mapping-value indicator (`:`) has been emitted on the current line.
|
||||
// Once one has, a later scalar followed by `:` is not a new implicit key: block style allows
|
||||
// only one mapping pair per line, so the document would be a forbidden inline nested mapping
|
||||
// (`a: b: c`). Reset at each line break. Flow `:` do not set it, since flow allows several
|
||||
// pairs on a line (`{a: b, c: d}`).
|
||||
var sawMappingValueOnLine = false
|
||||
var tagHandles = [
|
||||
"!": "!",
|
||||
"!!": "tag:yaml.org,2002:",
|
||||
@ -127,7 +134,16 @@ extension PureYAML.Parsing.Scanner {
|
||||
column: state.reader.mark.column,
|
||||
)
|
||||
}
|
||||
let hadContentOnLine = state.sawContentOnLine
|
||||
state.sawContentOnLine = true
|
||||
// An implicit-key mapping value (`:` preceded by its key on the same line) means the line
|
||||
// now holds a pair, so a later scalar followed by `:` cannot start another: that would be a
|
||||
// forbidden inline nested block mapping (`a: b: c`). A `:` that opens the line is an explicit-
|
||||
// or null-key value indicator, whose value may legitimately be a compact mapping, so it does
|
||||
// not set the flag.
|
||||
if character == ":", hadContentOnLine, state.flowDepth == 0, isAtMappingValueIndicator(state: state) {
|
||||
state.sawMappingValueOnLine = true
|
||||
}
|
||||
if try scanLineIndicatorToken(character, state: &state) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -92,6 +92,7 @@ extension PureYAML.Parsing {
|
||||
scanner.captureBlockScalarBlankLine(&scannerBox.state)
|
||||
scannerBox.state.isAtLineStart = true
|
||||
scannerBox.state.sawContentOnLine = false
|
||||
scannerBox.state.sawMappingValueOnLine = false
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
41
Tests/InlineNestedMappingTests.swift
Normal file
41
Tests/InlineNestedMappingTests.swift
Normal file
@ -0,0 +1,41 @@
|
||||
@testable import PureYAML
|
||||
import Testing
|
||||
|
||||
@Suite("Inline nested block mappings are rejected")
|
||||
struct InlineNestedMappingTests {
|
||||
@Test("A second mapping pair on a block line is rejected", arguments: [
|
||||
"a: b: c: d\n", // a plain value followed by another key
|
||||
"a: 'b': c\n", // a quoted value followed by another key
|
||||
"key2: \"q\" no key: nor value\n", // trailing content after a quoted value
|
||||
"x: { y: z }in: valid\n", // trailing content after a flow value
|
||||
])
|
||||
func test_inlineNestedMappingRejected(_ yaml: String) throws {
|
||||
#expect(throws: PureYAML.Parsing.ParseError.self) {
|
||||
try PureYAML.parse(yaml)
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Mappings spread across lines, and flow mappings, stay valid", arguments: [
|
||||
"a: b\nc: d\n", // two pairs on separate lines
|
||||
"a:\n b: c\n", // a nested mapping on the next line
|
||||
"- key: value\n", // a sequence item that is a mapping
|
||||
"{a: b, c: d}\n", // a flow mapping with several pairs on one line
|
||||
"a: http://x\n", // a colon inside a plain scalar value
|
||||
])
|
||||
func test_validMappingsStillParse(_ yaml: String) throws {
|
||||
_ = try PureYAML.parse(yaml)
|
||||
}
|
||||
|
||||
@Test("An explicit key's value may be a compact mapping on the value line")
|
||||
func test_explicitKeyCompactMappingValue() throws {
|
||||
// The `:` opens the line, so it is an explicit-key value indicator whose value may be a
|
||||
// compact block mapping, unlike an implicit key's same-line value.
|
||||
let value = try PureYAML.parse("? k\n: minLength: 14\n pattern: x\n")
|
||||
#expect(value == .mapping(.init([
|
||||
.init(keyNode: .string("k"), value: .mapping(.init([
|
||||
.init(key: "minLength", value: .int(14)),
|
||||
.init(key: "pattern", value: .string("x")),
|
||||
]))),
|
||||
])))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user