fix(parsing): apply the YAML 1.2 flow ':' rule with JSON-key adjacency
libyaml makes every ':' in a flow collection a value indicator, so it mis-scans ':x', 'http://foo.com', '::vector', and 'omitted value:,', and drops the value after an adjacent quoted or collection key. Per the spec a ':' only ends a node when followed by whitespace, end of input, or a flow indicator, except an adjacent ':' after a JSON key (a quoted flow scalar or a flow collection used as the key). Gate the ':' value-indicator dispatch on that rule, tracking on the pending simple key whether it is a JSON node (set for quoted flow scalars and flow-collection starts). End the plain scalar at a ':' before a flow indicator instead of rejecting it as unexpected. Surpasses libyaml on 4ABK, 58MP, 5T43, DBG4, FRK4, HM87/00, with 9MMW (mixing adjacent and separated keys) still correct. Port score 292 -> 298.
This commit is contained in:
parent
c3a1e6aaa4
commit
ff199516ac
12
CHANGELOG.md
12
CHANGELOG.md
@ -22,6 +22,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- Apply the YAML 1.2 flow `:` rule instead of libyaml's "always a value
|
||||
indicator in flow". In a flow collection a `:` only ends a node when it is
|
||||
followed by whitespace, the end of input, or a flow indicator, or when it is
|
||||
adjacent to a JSON key (a quoted scalar or a flow collection used as the key).
|
||||
Otherwise the `:` is plain-scalar content. So `[:x]` and `{x: :x}` keep the
|
||||
scalar `:x`, `http://foo.com` and `::vector` stay single scalars, `"key":value`
|
||||
and `{JSON: like}:adjacent` bind the value across the adjacent `:`, and
|
||||
`omitted value:,` is a key with an empty value rather than a scan error. The
|
||||
pending simple key now records whether it is a JSON node, and the plain scalar
|
||||
scanner ends (rather than rejecting) at a `:` before a flow indicator. Surpasses
|
||||
libyaml on 4ABK, 58MP, 5T43, DBG4, FRK4, and HM87/00, with 9MMW (mixed
|
||||
adjacent/separate keys) still correct.
|
||||
- Treat `?` in a flow collection as plain-scalar content unless it is followed by
|
||||
whitespace, the end of input, or a flow indicator. libyaml makes every `?` in
|
||||
flow context a mapping-key indicator, dropping the `?` from `?foo`, but
|
||||
|
||||
@ -111,7 +111,12 @@ extension PureYAML.Parsing.LibYAML.Scanner {
|
||||
// `ns-plain-first` a `?` followed by a flow-safe character (anything but
|
||||
// whitespace, end, or a flow indicator) begins a plain scalar (`?foo`).
|
||||
fetchKey()
|
||||
case ":" where flowLevel > 0 || Classify.isBlankZ(reader.peek(1)):
|
||||
case ":" where Classify.isBlankZ(reader.peek(1))
|
||||
|| (flowLevel > 0 && (Classify.isFlowIndicator(reader.peek(1)) || flowKeyIsJSON)):
|
||||
// In flow context libyaml makes every `:` a value indicator. Per the
|
||||
// spec a `:` only ends a node when followed by whitespace, end of
|
||||
// input, or a flow indicator, except an adjacent `:` after a JSON
|
||||
// (quoted) key. Otherwise the `:` is plain-scalar content (`:x`).
|
||||
try fetchValue()
|
||||
default:
|
||||
// A plain scalar may begin with any non-blank character; a blank (a
|
||||
@ -128,6 +133,13 @@ extension PureYAML.Parsing.LibYAML.Scanner {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this flow level's pending simple key is a JSON (quoted) node, which
|
||||
/// lets an adjacent `:` open its value without a separating space.
|
||||
private var flowKeyIsJSON: Bool {
|
||||
let key = simpleKeys[simpleKeys.count - 1]
|
||||
return key.possible && key.isJSON
|
||||
}
|
||||
|
||||
/// Fetch a flow-collection indicator (`[` `]` `{` `}` `,`); returns false
|
||||
/// otherwise.
|
||||
private mutating func fetchFlowToken(_ character: Unicode.Scalar) throws -> Bool {
|
||||
@ -178,7 +190,13 @@ extension PureYAML.Parsing.LibYAML.Scanner {
|
||||
/// Port of `yaml_parser_fetch_flow_scalar`.
|
||||
private mutating func fetchFlowScalar(single: Bool) throws {
|
||||
mark = reader.mark
|
||||
let saved = simpleKeyAllowed
|
||||
try saveSimpleKey()
|
||||
// A quoted scalar that became this level's simple key is a JSON node, so a
|
||||
// later adjacent `:` (no space) still opens its value.
|
||||
if saved {
|
||||
simpleKeys[simpleKeys.count - 1].isJSON = true
|
||||
}
|
||||
simpleKeyAllowed = false
|
||||
let token = try scanFlowScalar(single: single)
|
||||
indentEngine.enqueue(token)
|
||||
@ -196,7 +214,13 @@ extension PureYAML.Parsing.LibYAML.Scanner {
|
||||
/// Port of `yaml_parser_fetch_flow_collection_start`.
|
||||
private mutating func fetchFlowCollectionStart(_ type: PureYAML.Parsing.LibYAML.TokenType) {
|
||||
// saveSimpleKey cannot throw in flow: a required key needs block context.
|
||||
let saved = simpleKeyAllowed
|
||||
try? saveSimpleKey()
|
||||
// A flow collection is a JSON node, so when it is this level's simple key
|
||||
// an adjacent `:` after its closing `]`/`}` still opens its value.
|
||||
if saved {
|
||||
simpleKeys[simpleKeys.count - 1].isJSON = true
|
||||
}
|
||||
increaseFlowLevel()
|
||||
simpleKeyAllowed = true
|
||||
let startMark = reader.mark
|
||||
|
||||
@ -52,13 +52,16 @@ extension PureYAML.Parsing.LibYAML.Scanner {
|
||||
|
||||
/// Consume the run of non-blank characters, folding any pending breaks or
|
||||
/// whitespace ahead of each one.
|
||||
private mutating func consumePlainContent(_ state: inout PlainState, startMark: PureYAML.Parsing.Mark) throws {
|
||||
private mutating func consumePlainContent(_ state: inout PlainState, startMark _: PureYAML.Parsing.Mark) throws {
|
||||
while let character = reader.peek(), !Classify.isBlankZ(character) {
|
||||
if character == ":", Classify.isBlankZ(reader.peek(1)) {
|
||||
break
|
||||
}
|
||||
if flowLevel > 0, character == ":", Classify.isFlowIndicator(reader.peek(1)) {
|
||||
throw scannerError("found unexpected ':'", at: startMark)
|
||||
// A `:` before a flow indicator ends the key; the value is empty.
|
||||
// libyaml rejects this as an unexpected ':', but it is a valid
|
||||
// value indicator in flow context.
|
||||
break
|
||||
}
|
||||
if flowLevel > 0, Classify.isFlowIndicator(character) {
|
||||
break
|
||||
|
||||
@ -11,6 +11,10 @@ extension PureYAML.Parsing.LibYAML {
|
||||
var tokenNumber: Int
|
||||
/// The position the potential key started at.
|
||||
var mark: PureYAML.Parsing.Mark
|
||||
/// Whether this key is a JSON-style node (a quoted flow scalar). A `:`
|
||||
/// adjacent to such a key is a value indicator even without a following
|
||||
/// space, per the YAML flow "adjacent value" rule.
|
||||
var isJSON = false
|
||||
|
||||
/// The empty entry pushed for a new flow level (`{ 0, 0, 0, {0,0,0} }`).
|
||||
static var empty: Self {
|
||||
|
||||
@ -12,7 +12,7 @@ struct CanonicalEventConformanceTests {
|
||||
/// Valid suite cases whose event stream PureYAML does NOT yet emit exactly.
|
||||
/// This is the authoritative gap set: a fix removes an id (and the test fails
|
||||
/// until it is removed), a regression adds one (and the test fails). Of these,
|
||||
/// 14 currently throw on valid input and 2 emit a wrong event stream.
|
||||
/// 8 currently throw on valid input and 2 emit a wrong event stream.
|
||||
static let knownEventGaps: Set<String> = [
|
||||
// Gap set for the ported libyaml engine. Verified against the libyaml
|
||||
// reference dumper: on all but four of these the port matches libyaml
|
||||
@ -21,9 +21,9 @@ struct CanonicalEventConformanceTests {
|
||||
// itself handles.
|
||||
//
|
||||
// false-rejects (parseEvents throws on valid input, as libyaml does)
|
||||
"4ABK", "58MP", "5T43", "6BCT", "6CA3",
|
||||
"A2M4", "CFD4", "DBG4", "DK95/00",
|
||||
"DK95/04", "FRK4", "HM87/00",
|
||||
"6BCT", "6CA3",
|
||||
"A2M4", "CFD4", "DK95/00",
|
||||
"DK95/04",
|
||||
"Q5MG",
|
||||
"Y79Y/010",
|
||||
// wrong event stream: the port matches libyaml but both deviate from the
|
||||
|
||||
Loading…
Reference in New Issue
Block a user