fix(parsing): accept a higher-minor %YAML version

The %YAML version check accepted only 1.1 and 1.2, rejecting anything else,
so `%YAML 1.3` errored. The spec calls for processing a document whose minor
version is higher than the processor's, with a warning, rather than refusing
it: only a different major version is incompatible.

Accept any well-formed `1.minor` version and reject only a different major
(`2.0`) or a malformed version. Three tests that asserted `%YAML 1.3` was an
error are corrected to use `2.0`, the genuinely incompatible case, and a test
covers that 1.3 now parses.

Raises yaml-test-suite parse-level conformance to 305/333 (91.6%) with the
value-level (100%) and should-error rejection rates unchanged. 486 tests pass.
This commit is contained in:
Mihaela M 2026-06-29 02:34:55 +02:00
parent 4b06f6e5fc
commit 7879818ce5
5 changed files with 29 additions and 4 deletions

View File

@ -84,6 +84,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- A `%YAML` directive with a higher minor version than 1.2 is now accepted rather than rejected. The
spec calls for processing a document with a higher minor version, with a warning, so `%YAML 1.3` is
read as a YAML 1.x document instead of erroring. The version check now accepts any well-formed
`1.minor` version and rejects only a different major version (`2.0`) or a malformed one. Three tests
that asserted `%YAML 1.3` was an error were corrected to use `2.0`. Raises yaml-test-suite parse-level
conformance to 305/333 (91.6%), with the should-error rate unchanged.
- A plain scalar that is an inline block sequence item value (`: - b`) now stops folding at a later
block entry that returns to the sequence's own indent, so the dashes form the rest of the sequence
rather than folding into the first item. A block entry only recorded its inline node's indent as a

View File

@ -102,7 +102,10 @@ extension PureYAML.Parsing.Scanner {
state.pendingDirectiveLine = start.line
switch name {
case "%YAML":
guard parts.indices.contains(1), parts[1] == "1.1" || parts[1] == "1.2" else {
// Any version with major 1 is processable: a higher minor than 1.2 (`%YAML 1.3`) is
// accepted, since the spec calls for processing it, with a warning, rather than rejecting
// it. Only a different major version, or a malformed one, is incompatible.
guard parts.indices.contains(1), isProcessableYAMLVersion(parts[1]) else {
throw PureYAML.Parsing.ParseError.incompatibleYAMLDirective(line: start.line)
}
// `%YAML` takes exactly one parameter, the version; only a comment may follow it.
@ -126,4 +129,14 @@ extension PureYAML.Parsing.Scanner {
return
}
}
/// Whether a `%YAML` version is one this processor can attempt: any `major.minor` with major 1,
/// so 1.0 through 1.2 and a higher minor like 1.3 are all accepted; a different major (2.0) or a
/// non-numeric version is not.
func isProcessableYAMLVersion(_ version: String) -> Bool {
let components = version.split(separator: ".", omittingEmptySubsequences: false)
return components.count == 2
&& components[0] == "1"
&& components.allSatisfy { !$0.isEmpty && $0.allSatisfy(\.isNumber) }
}
}

View File

@ -16,7 +16,7 @@ struct ParsingErrorTests {
expectParseError("name: \"open", .unterminatedQuotedString(line: 1))
expectParseError("name: 'open", .unterminatedQuotedString(line: 1))
expectParseError("second: *missing", .undefinedAlias(anchor: "missing", line: 1, column: 9))
expectParseError("%YAML 1.3\n---\nvalue: yes", .incompatibleYAMLDirective(line: 1))
expectParseError("%YAML 2.0\n---\nvalue: yes", .incompatibleYAMLDirective(line: 1))
expectParseError("value: !!int nope", .invalidTaggedScalar(
tag: "tag:yaml.org,2002:int",
value: "nope",

View File

@ -264,7 +264,7 @@ struct ScannerTests {
expectScannerError("anchor: &", .expectedAnchorName(line: 1))
expectScannerError("alias: *", .expectedAliasName(line: 1))
expectScannerError("tag: !<tag:example.com,2026:thing", .unterminatedTag(line: 1))
expectScannerError("%YAML 1.3\n---\nvalue: yes", .incompatibleYAMLDirective(line: 1))
expectScannerError("%YAML 2.0\n---\nvalue: yes", .incompatibleYAMLDirective(line: 1))
// An unrecognized directive before the document is ignored, but one after content (where a
// directive cannot appear) is still rejected.
expectScannerError("value: yes\n%FOO bar", .unsupportedDirective(name: "%FOO", line: 2))

View File

@ -6,7 +6,7 @@ struct YAMLDirectiveTests {
@Test("A malformed or repeated %YAML directive is rejected", arguments: [
"%YAML 1.2 foo\n---\nx\n", // an extra parameter after the version
"%YAML 1.2\n%YAML 1.2\n---\nx\n", // a second %YAML directive
"%YAML 1.3\n---\nx\n", // an unsupported version
"%YAML 2.0\n---\nx\n", // an incompatible major version
])
func test_invalidYAMLDirectiveIsRejected(_ yaml: String) throws {
#expect(throws: PureYAML.Parsing.ParseError.self) {
@ -14,6 +14,11 @@ struct YAMLDirectiveTests {
}
}
@Test("A higher minor version is accepted, since the spec calls for processing it with a warning")
func test_higherMinorVersionAccepted() throws {
#expect(try PureYAML.parse("%YAML 1.3\n---\nx\n") == .string("x"))
}
@Test("A well-formed %YAML directive, with an optional trailing comment, is accepted")
func test_validYAMLDirectiveAccepted() throws {
let expected = PureYAML.Model.Value.string("x")