fix(parsing): reject a comment interrupting a multi-line plain scalar
A comment terminates a plain scalar, so a multi-line plain scalar cannot
resume after one. The parser accepted 'key: word1' then a '#' line then
' word2', and 'a' then 'b # ...' then 'c', merging the trailing content
back into the value ('word1 word2', 'a b c').
The scanner folds a genuine multi-line plain scalar into a single token
and splits it into a value-line scalar, an indent, and a further scalar
only when a comment broke its continuation folding. That token shape
therefore always denotes a comment-interrupted scalar, so the parser now
rejects it rather than re-merging. The re-merge path and its helper are
removed as they only ever served this malformed shape. A real multi-line
plain scalar and a trailing comment after a value stay valid.
Measured against the official YAML test-suite, parse-level conformance
rises from 264/333 (79.3%) to 266/333 (79.9%) with no regression.
This commit is contained in:
parent
b964fea138
commit
8649f1722a
@ -23,6 +23,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- A comment can no longer interrupt a multi-line plain scalar. A comment terminates a plain
|
||||
scalar, so content that tries to continue it on a following line (`key: word1` then a `#` line
|
||||
then ` word2`, or `a` then `b # ...` then `c`) is now rejected instead of being silently merged
|
||||
back into the value. A genuine multi-line plain scalar and a trailing comment after a value are
|
||||
unaffected. The parser path that re-merged the scanner-split scalar is removed, since the scanner
|
||||
only splits a plain scalar this way when a comment broke its folding. Lifts official test-suite
|
||||
parse-level conformance from 264/333 (79.3%) to 266/333 (79.9%), with no regression.
|
||||
- A quoted scalar that spans a line break can no longer be a block implicit key (`"c⏎ d": 1`).
|
||||
An implicit key in block context must lie on a single line, so a quoted scalar whose open and
|
||||
close quotes sit on different lines is now rejected when used as such a key. The rule is
|
||||
|
||||
@ -26,6 +26,7 @@ public extension PureYAML.Parsing {
|
||||
case contentAfterDocumentEnd(line: Int)
|
||||
case blockSequenceAsInlineMappingValue(line: Int)
|
||||
case multilineImplicitKey(line: Int)
|
||||
case commentInMultilineScalar(line: Int)
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
@ -79,6 +80,8 @@ public extension PureYAML.Parsing {
|
||||
"a block sequence cannot start on the same line as a mapping value at line \(line)"
|
||||
case let .multilineImplicitKey(line):
|
||||
"an implicit mapping key must not span multiple lines at line \(line)"
|
||||
case let .commentInMultilineScalar(line):
|
||||
"a comment cannot interrupt a multi-line plain scalar at line \(line)"
|
||||
}
|
||||
}
|
||||
|
||||
@ -134,6 +137,8 @@ public extension PureYAML.Parsing {
|
||||
"blockSequenceAsInlineMappingValue"
|
||||
case .multilineImplicitKey:
|
||||
"multilineImplicitKey"
|
||||
case .commentInMultilineScalar:
|
||||
"commentInMultilineScalar"
|
||||
}
|
||||
}
|
||||
|
||||
@ -154,7 +159,8 @@ public extension PureYAML.Parsing {
|
||||
let .directiveWithoutDocument(line),
|
||||
let .contentAfterDocumentEnd(line),
|
||||
let .blockSequenceAsInlineMappingValue(line),
|
||||
let .multilineImplicitKey(line):
|
||||
let .multilineImplicitKey(line),
|
||||
let .commentInMultilineScalar(line):
|
||||
line
|
||||
case let .expectedNode(line, _),
|
||||
let .expectedScalarKey(line, _),
|
||||
@ -186,7 +192,8 @@ public extension PureYAML.Parsing {
|
||||
.directiveWithoutDocument,
|
||||
.contentAfterDocumentEnd,
|
||||
.blockSequenceAsInlineMappingValue,
|
||||
.multilineImplicitKey:
|
||||
.multilineImplicitKey,
|
||||
.commentInMultilineScalar:
|
||||
nil
|
||||
case let .expectedNode(_, column),
|
||||
let .expectedScalarKey(_, column),
|
||||
|
||||
@ -36,8 +36,8 @@ extension PureYAML.Parsing.TokenEventParser {
|
||||
}
|
||||
return result
|
||||
}
|
||||
if properties == .none, try canParsePlainScalarWithContinuation(after: valueIndicator) {
|
||||
return try parsePlainScalarWithContinuation(properties: properties)
|
||||
if properties == .none, try isCommentInterruptedScalarValue(after: valueIndicator) {
|
||||
return try rejectCommentInterruptedScalar()
|
||||
}
|
||||
if cursor.current?.kind.isMappingKey == true, hasNodeOnSameLine(after: valueIndicator) {
|
||||
return try parseBlockMapping(
|
||||
@ -66,36 +66,18 @@ extension PureYAML.Parsing.TokenEventParser {
|
||||
return PureYAML.Parsing.NodeResult(kind: .scalar, endMark: mark)
|
||||
}
|
||||
|
||||
mutating func parsePlainScalarWithContinuation(
|
||||
properties: PureYAML.Parsing.NodeProperties,
|
||||
) throws -> PureYAML.Parsing.NodeResult {
|
||||
/// Rejects a plain scalar that the scanner split into a value-line scalar, an indent, and a
|
||||
/// further scalar. The scanner folds a genuine multi-line plain scalar into a single token, so
|
||||
/// this token shape arises only when a comment broke its continuation: a comment cannot
|
||||
/// interrupt a multi-line plain scalar, so the construct is malformed.
|
||||
mutating func rejectCommentInterruptedScalar() throws -> PureYAML.Parsing.NodeResult {
|
||||
let first = try expect("plain scalar") { kind in
|
||||
if case .scalar(_, .plain) = kind {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
var value = try decodedScalar(from: first)
|
||||
var endMark = first.endMark
|
||||
|
||||
_ = try expect("indent") { $0.isIndent }
|
||||
while isScalar(cursor.current) {
|
||||
let token = try cursor.advance() ?? first
|
||||
let continuation = try plainContinuationText(from: token)
|
||||
value += " " + continuation
|
||||
endMark = token.endMark
|
||||
}
|
||||
_ = try expect("dedent") { $0.isDedent }
|
||||
|
||||
let mark = properties.mark ?? first.mark
|
||||
try emit(.scalar(
|
||||
value: value,
|
||||
anchor: properties.anchor,
|
||||
tag: properties.tag,
|
||||
style: .plain,
|
||||
mark: mark,
|
||||
))
|
||||
return PureYAML.Parsing.NodeResult(kind: .scalar, endMark: endMark)
|
||||
throw PureYAML.Parsing.ParseError.commentInMultilineScalar(line: first.mark.line)
|
||||
}
|
||||
|
||||
mutating func consumeProperties() throws -> PureYAML.Parsing.NodeProperties {
|
||||
@ -141,20 +123,6 @@ extension PureYAML.Parsing.TokenEventParser {
|
||||
return try decodeScalar(value, style: style, line: token.mark.line)
|
||||
}
|
||||
|
||||
func plainContinuationText(from token: PureYAML.Parsing.Token) throws -> String {
|
||||
guard case let .scalar(value, style) = token.kind else {
|
||||
throw unexpectedToken(expected: "scalar")
|
||||
}
|
||||
switch style {
|
||||
case .doubleQuoted:
|
||||
return "\"\(value)\""
|
||||
case .singleQuoted:
|
||||
return "'\(value)'"
|
||||
case .folded, .literal, .plain:
|
||||
return try decodeScalar(value, style: style, line: token.mark.line)
|
||||
}
|
||||
}
|
||||
|
||||
func hasNodeOnSameLine(after token: PureYAML.Parsing.Token) -> Bool {
|
||||
guard let current = cursor.current, !current.kind.isTerminator else {
|
||||
return false
|
||||
@ -162,7 +130,7 @@ extension PureYAML.Parsing.TokenEventParser {
|
||||
return current.mark.line == token.mark.line
|
||||
}
|
||||
|
||||
mutating func canParsePlainScalarWithContinuation(after token: PureYAML.Parsing.Token) throws -> Bool {
|
||||
mutating func isCommentInterruptedScalarValue(after token: PureYAML.Parsing.Token) throws -> Bool {
|
||||
guard case .scalar(_, .plain) = cursor.current?.kind else {
|
||||
return false
|
||||
}
|
||||
@ -178,7 +146,7 @@ extension PureYAML.Parsing.TokenEventParser {
|
||||
return true
|
||||
}
|
||||
|
||||
mutating func canParsePlainScalarContinuation() throws -> Bool {
|
||||
mutating func isCommentInterruptedScalar() throws -> Bool {
|
||||
guard case .scalar(_, .plain) = cursor.current?.kind else {
|
||||
return false
|
||||
}
|
||||
|
||||
@ -241,8 +241,8 @@ extension PureYAML.Parsing.TokenEventParser {
|
||||
includeIndentedContinuation: false,
|
||||
)
|
||||
case let .scalar(value, style):
|
||||
if try canParsePlainScalarContinuation() {
|
||||
return try parsePlainScalarWithContinuation(properties: properties)
|
||||
if try isCommentInterruptedScalar() {
|
||||
return try rejectCommentInterruptedScalar()
|
||||
}
|
||||
_ = try cursor.advance()
|
||||
let decoded = try decodeScalar(value, style: style, line: token.mark.line)
|
||||
|
||||
32
Tests/CommentInPlainScalarTests.swift
Normal file
32
Tests/CommentInPlainScalarTests.swift
Normal file
@ -0,0 +1,32 @@
|
||||
@testable import PureYAML
|
||||
import Testing
|
||||
|
||||
@Suite("Comments and multi-line plain scalars")
|
||||
struct CommentInPlainScalarTests {
|
||||
@Test("A comment interrupting a multi-line plain scalar is rejected", arguments: [
|
||||
"key: word1\n# xxx\n word2\n", // a comment line between the value and its continuation
|
||||
"plain: a\n b # end of scalar\n c\n", // a comment ends the scalar, then it tries to continue
|
||||
])
|
||||
func test_commentInterruptedScalarRejected(_ yaml: String) throws {
|
||||
#expect(throws: PureYAML.Parsing.ParseError.self) {
|
||||
try PureYAML.parse(yaml)
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Multi-line plain scalars and trailing comments stay valid")
|
||||
func test_validMultilineScalarsAndComments() throws {
|
||||
#expect(try PureYAML.parse("plain: a\n b\n c\n") == .mapping(.init([
|
||||
.init(key: "plain", value: .string("a b c")),
|
||||
])))
|
||||
#expect(try PureYAML.parse("key: a b # comment\n") == .mapping(.init([
|
||||
.init(key: "key", value: .string("a b")),
|
||||
])))
|
||||
#expect(try PureYAML.parse("key: a\n b # comment\n") == .mapping(.init([
|
||||
.init(key: "key", value: .string("a b")),
|
||||
])))
|
||||
#expect(try PureYAML.parse("a: 1\n# comment\nb: 2\n") == .mapping(.init([
|
||||
.init(key: "a", value: .int(1)),
|
||||
.init(key: "b", value: .int(2)),
|
||||
])))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user