diff --git a/CHANGELOG.md b/CHANGELOG.md index 59b92b7..35790a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- An alias node may now serve as an implicit mapping key (`*a : value`). The scanner emits the + implicit-key indicator before a scalar key followed by a `:` but did not for an alias, so the + parser, which already accepts an alias node in key position, never entered the pair and rejected + the alias as an unexpected token. The scanner now emits the same indicator before an alias that a + `:` (after optional spaces) follows, in block context. A `:` written directly against the alias + stays part of its name, since `:` is a valid anchor-name character. Fixes yaml-test-suite E76Z and + 26DV; raises parse-level conformance to 310/333 (93.1%) and value-level to 203/203 (100.0%), with + the should-error rate unchanged. + - A property written inline before a block mapping's first key (`&a key: value`) now binds to that key rather than to the whole mapping. The YAML grammar requires a block collection's own properties to be followed by a line break, so a property on the first key's line belongs to the diff --git a/Sources/Parsing/Scanner.Scalars.swift b/Sources/Parsing/Scanner.Scalars.swift index 2ebf278..230a9fb 100644 --- a/Sources/Parsing/Scanner.Scalars.swift +++ b/Sources/Parsing/Scanner.Scalars.swift @@ -117,6 +117,7 @@ extension PureYAML.Parsing.Scanner { func scanNamedToken( kind: (String) -> PureYAML.Parsing.TokenKind, missingNameError: (Int) -> PureYAML.Parsing.ParseError, + canBeImplicitKey: Bool = false, state: inout State, ) throws { let start = state.reader.mark @@ -127,6 +128,15 @@ extension PureYAML.Parsing.Scanner { guard !name.isEmpty else { throw missingNameError(start.line) } + // An alias node may serve as an implicit mapping key (`*a : value`). As with a scalar key, a + // `:` after optional spaces marks it as the key, so emit the key indicator before the alias. + // An anchor is a node property, never a key, so it never asks for this. An alias name cannot + // span lines, so the single-line implicit-key rule needs no separate check here. + let isAliasImplicitKey = canBeImplicitKey && state.flowDepth == 0 + && isAtMappingValueIndicatorAfterSpaces(state: state) && canSynthesizeImplicitBlockKey(state) + if isAliasImplicitKey { + state.append(.mappingKey, mark: start, endMark: start) + } state.append(kind(name), mark: start, endMark: state.reader.mark) } diff --git a/Sources/Parsing/Scanner.swift b/Sources/Parsing/Scanner.swift index 7438cdc..8d8a79d 100644 --- a/Sources/Parsing/Scanner.swift +++ b/Sources/Parsing/Scanner.swift @@ -321,6 +321,7 @@ extension PureYAML.Parsing.Scanner { try scanNamedToken( kind: PureYAML.Parsing.TokenKind.alias, missingNameError: PureYAML.Parsing.ParseError.expectedAliasName, + canBeImplicitKey: true, state: &state, ) return true diff --git a/Tests/AliasAsMappingKeyTests.swift b/Tests/AliasAsMappingKeyTests.swift new file mode 100644 index 0000000..0413e3b --- /dev/null +++ b/Tests/AliasAsMappingKeyTests.swift @@ -0,0 +1,34 @@ +@testable import PureYAML +import Testing + +@Suite("An alias node can serve as an implicit mapping key") +struct AliasAsMappingKeyTests { + /// yaml-test-suite E76Z: an alias may stand where a mapping key is expected (`*b : value`). The + /// scanner now emits the implicit-key indicator before an alias followed by a `:`, exactly as it + /// does for a scalar key, and the alias resolves to the anchored node's value. + @Test("An alias followed by a value indicator is a mapping key") + func test_aliasIsImplicitKey() throws { + let mapping = try requireMapping(PureYAML.parse("&a a: &b b\n*b : *a\n")) + #expect(mapping?["a"] == .string("b")) + #expect(mapping?["b"] == .string("a")) + } + + /// yaml-test-suite 26DV exercises an alias key nested inside a block-mapping value, where the + /// alias resolves to an earlier anchored scalar and becomes that nested mapping's key. + @Test("An alias key resolves inside a nested block mapping value") + func test_aliasKeyInNestedMapping() throws { + let root = try requireMapping(PureYAML.parse("top1: &x scalar1\ntop3:\n *x : scalar3\n")) + #expect(root?["top1"] == .string("scalar1")) + let nested = try requireMapping(#require(root?["top3"])) + #expect(nested?["scalar1"] == .string("scalar3")) + } + + /// A `:` written directly against an alias with no separating space is part of the alias name, + /// not a value indicator (`:` is a valid anchor-name character), so the alias is not a key here. + @Test("An alias with an adjacent colon keeps the colon in its name") + func test_adjacentColonStaysInName() throws { + let mapping = try requireMapping(PureYAML.parse("key: &a: scalar\nref: *a:\n")) + #expect(mapping?["key"] == .string("scalar")) + #expect(mapping?["ref"] == .string("scalar")) + } +}