fix(parsing): parse a leading ':' flow-sequence entry as an empty-key pair

A flow sequence entry that begins with ':' (e.g. '[ : v ]') is a
single-pair mapping with an empty key, which YAML 1.2 permits but libyaml
rejects by handing the leading VALUE token to its node parser.

Open the implicit single-pair mapping on a leading ':' in the
flow-sequence-entry parser, and leave the VALUE token for the mapping
value state to consume so the value is parsed rather than dropped.
Surpasses libyaml on CFD4. Port score 298 -> 299.
This commit is contained in:
Mihaela Mihaljevic 2026-06-24 19:07:04 +02:00
parent ff199516ac
commit 8092897612
3 changed files with 16 additions and 3 deletions

View File

@ -22,6 +22,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Parse a flow sequence entry that begins with `:` as a single-pair mapping with
an empty key (`[ : v ]`). libyaml hands the leading VALUE token to its node
parser and fails. The flow-sequence-entry parser now opens the implicit mapping
on a leading `:` and leaves the indicator for the value state, so the value is
parsed rather than dropped. Surpasses libyaml on CFD4.
- 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

View File

@ -24,6 +24,12 @@ extension PureYAML.Parsing.LibYAML.Parser {
skipToken()
return .mappingStart(anchor: nil, tag: nil, style: .flow, mark: token.startMark)
}
if isValue(token) {
// A leading `:` opens a single-pair mapping with an empty key
// (`[ : v ]`). Keep the VALUE token for the mapping-key state.
state = .flowSequenceEntryMappingKey
return .mappingStart(anchor: nil, tag: nil, style: .flow, mark: token.startMark)
}
if !isFlowSequenceEnd(token) {
states.append(.flowSequenceEntry)
return try parseNode(block: false, indentlessSequence: false)
@ -42,7 +48,9 @@ extension PureYAML.Parsing.LibYAML.Parser {
states.append(.flowSequenceEntryMappingValue)
return try parseNode(block: false, indentlessSequence: false)
}
if isFlowSequenceEnd(token) {
// An empty key: leave a VALUE token for the value state to consume (so the
// value is parsed); a closing `]` ends the pair with an empty value too.
if isValue(token) || isFlowSequenceEnd(token) {
state = .flowSequenceEntryMappingValue
return processEmptyScalar(at: token.startMark)
}

View File

@ -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,
/// 8 currently throw on valid input and 2 emit a wrong event stream.
/// 7 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
@ -22,7 +22,7 @@ struct CanonicalEventConformanceTests {
//
// false-rejects (parseEvents throws on valid input, as libyaml does)
"6BCT", "6CA3",
"A2M4", "CFD4", "DK95/00",
"A2M4", "DK95/00",
"DK95/04",
"Q5MG",
"Y79Y/010",