fix(parsing): resolve an alias to an anchor on a null value

An anchor on an empty mapping value (`a: &anchor`) failed to register, so a
later `*anchor` errored as an undefined alias. Two bugs compounded:

  - The parser dispatched the anchor-only value position to a nested block
    mapping, absorbing the sibling key as the anchored node's child instead of
    emitting the empty anchored node. It now emits an empty anchored node when
    only a sibling mapping key follows, since the indented and inline-nested
    cases are already handled by earlier branches.
  - The stream composer registered an anchor by its raw scalar text, so an
    anchor on an empty scalar came back as "" and a typed scalar (`&a 42`) as
    text. It now registers the composed value, fixing a latent typed-anchor bug.

Fixes yaml-test-suite 6KGN; raises parse-level conformance to 306/333 (91.9%)
and value-level to 199/199 (100.0%), with the should-error rate unchanged.
This commit is contained in:
Mihaela M 2026-06-29 02:56:17 +02:00
parent 7879818ce5
commit e3caa5be0d
4 changed files with 47 additions and 1 deletions

View File

@ -84,6 +84,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- An anchor on an empty mapping value (`a: &anchor`) now registers that value's implicit null node,
so a later alias resolves to null instead of failing as an undefined alias. Two bugs compounded:
the parser dispatched the anchor-only value position to a nested block mapping, absorbing the
sibling key (`b:`) as the anchored node's child instead of emitting the empty anchored node; and
the stream composer registered an anchor by its raw scalar text rather than the resolved node, so
even a registered anchor on an empty scalar came back as `""` and a typed scalar (`&a 42`) came
back as text. The value position now emits an empty anchored node when only a sibling mapping key
follows, and an anchor registers the composed value. Fixes yaml-test-suite 6KGN and a latent
typed-scalar anchor bug; raises parse-level conformance to 306/333 (91.9%) and value-level to
199/199 (100.0%), with the should-error rate unchanged.
- 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

View File

@ -92,7 +92,9 @@ private extension PureYAML.Parsing.EventStreamComposer {
) throws {
let model = try scalarParser.composeScalarValue(value, tag: tag, style: style, mark: mark)
if let anchor {
anchors[anchor] = .string(value)
// Register the resolved node, not the raw text: an alias resolves to the anchored node's
// value, so an empty scalar anchors null and a typed scalar (`&a 42`) anchors that value.
anchors[anchor] = model
}
try complete(.init(
value: model,

View File

@ -33,6 +33,13 @@ extension PureYAML.Parsing.TokenEventParser {
minimumColumn: cursor.current?.mark.column,
)
}
// A value position holding only properties (`a: &anchor`) followed by a sibling mapping key
// has an empty (null) node carrying those properties, not a nested mapping: the indented
// nested case is consumed above by the indent branch, and an inline nested mapping is caught
// by the mapping-key-with-same-line-node branch, so a bare mapping key here is a sibling.
if properties != .none, cursor.current?.kind.isMappingKey == true {
return try emitEmptyNodeOrThrow(properties: properties)
}
if properties != .none || hasNodeOnSameLine(after: valueIndicator) {
return try parseNode(properties: properties)
}

View File

@ -0,0 +1,26 @@
@testable import PureYAML
import Testing
@Suite("Anchor on a null or typed value resolves through its alias")
struct AnchorOnNullValueTests {
/// An anchor on a `key:` with no value anchors that key's implicit null node, so a later alias
/// resolves to null rather than failing as undefined (yaml-test-suite 6KGN). The empty value is
/// followed by a sibling mapping key, which must not be absorbed as a nested mapping.
@Test("An anchor on an empty mapping value registers a null node for its alias")
func test_anchorOnEmptyValueResolvesToNull() throws {
let value = try PureYAML.parse("a: &anchor\nb: *anchor\n")
let mapping = try requireMapping(value)
#expect(mapping?["a"] == .null)
#expect(mapping?["b"] == .null)
}
/// The anchored node is resolved before registration, so a typed scalar anchors its typed value
/// (here an integer), not its raw text: the alias must come back as the same `.int(42)`.
@Test("An anchor on a typed scalar registers the resolved value, not its raw text")
func test_anchorOnTypedScalarResolvesToTypedValue() throws {
let value = try PureYAML.parse("a: &a 42\nb: *a\n")
let mapping = try requireMapping(value)
#expect(mapping?["a"] == .int(42))
#expect(mapping?["b"] == .int(42))
}
}