fix(parsing): accept an alias node as an implicit mapping key
An alias may stand where a mapping key is expected (`*a : value`). The scanner emits the implicit-key indicator before a scalar key that a `:` follows, but not before 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, so only a space-separated `:` marks the alias as a key. 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.
This commit is contained in:
parent
c858339307
commit
b45b7a4b7d
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
34
Tests/AliasAsMappingKeyTests.swift
Normal file
34
Tests/AliasAsMappingKeyTests.swift
Normal file
@ -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"))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user