feat(parsing): accept full YAML 1.2 anchor and alias names

An anchor or alias name is ns-anchor-char+ (any printable character that
is not whitespace, a line break, the BOM, or a flow indicator), so Unicode
and punctuation names are valid. libyaml restricts the name to
alphanumerics and rejects them.

Canonical event conformance: 271 -> 275 of 308 (2SXE, 8XYN, W5VH, Y2GN).
379 tests pass.
This commit is contained in:
Mihaela Mihaljevic 2026-06-24 18:11:58 +02:00
parent df3aa0b5d4
commit 18101b8ddc
2 changed files with 21 additions and 13 deletions

View File

@ -1,20 +1,21 @@
extension PureYAML.Parsing.LibYAML.Scanner {
private typealias Classify = PureYAML.Parsing.LibYAML.CharacterClass
/// Port of `yaml_parser_scan_anchor`. Scans an anchor (`&name`) or alias
/// (`*name`): the name is a run of `IS_ALPHA` characters and must be non-empty
/// and followed by a blank, the end of input, or one of the indicators that may
/// terminate it.
/// Port of `yaml_parser_scan_anchor`, widened to YAML 1.2: an anchor (`&name`)
/// or alias (`*name`) name is `ns-anchor-char+`, i.e. any printable character
/// that is not whitespace, a line break, or a flow indicator (`,[]{}`). libyaml
/// restricts the name to alphanumerics, which rejects Unicode and punctuation
/// anchor names that YAML 1.2 allows.
mutating func scanAnchor(isAnchor: Bool) throws -> PureYAML.Parsing.LibYAML.Token {
let startMark = reader.mark
reader.skip()
var name = ""
while let character = reader.peek(), Classify.isAlpha(character) {
while let character = reader.peek(), isAnchorCharacter(character) {
name.unicodeScalars.append(character)
reader.skip()
}
let endMark = reader.mark
guard !name.isEmpty, Classify.isBlankZ(reader.peek()) || isAnchorTerminator(reader.peek()) else {
guard !name.isEmpty else {
throw PureYAML.Parsing.LibYAML.ScannerError(
context: isAnchor ? "while scanning an anchor" : "while scanning an alias",
problem: "did not find expected alphabetic or numeric character",
@ -28,10 +29,17 @@ extension PureYAML.Parsing.LibYAML.Scanner {
)
}
private func isAnchorTerminator(_ scalar: Unicode.Scalar?) -> Bool {
/// `ns-anchor-char`: a printable, non-blank, non-break character that is not a
/// flow indicator or the byte-order mark.
private func isAnchorCharacter(_ scalar: Unicode.Scalar) -> Bool {
guard Classify.isPrintable(scalar), !Classify.isBlankZ(scalar), scalar != "\u{FEFF}" else {
return false
}
switch scalar {
case "?", ":", ",", "]", "}", "%", "@", "`": true
default: false
case ",", "[", "]", "{", "}":
return false
default:
return true
}
}
}

View File

@ -21,14 +21,14 @@ struct CanonicalEventConformanceTests {
// itself handles.
//
// false-rejects (parseEvents throws on valid input, as libyaml does)
"2JQS", "2SXE", "4ABK", "58MP", "5T43", "6BCT", "6CA3", "6M2F",
"8XYN", "96NN/00", "96NN/01", "A2M4", "CFD4", "DBG4", "DK3J", "DK95/00",
"2JQS", "4ABK", "58MP", "5T43", "6BCT", "6CA3", "6M2F",
"96NN/00", "96NN/01", "A2M4", "CFD4", "DBG4", "DK3J", "DK95/00",
"DK95/04", "FP8R", "FRK4", "HM87/00", "M2N8/00", "M7A3", "NHX8",
"NKF9", "Q5MG", "R4YG", "S3PD", "SM9W/01", "UKK6/00", "W4TN",
"W5VH", "Y79Y/001", "Y79Y/010",
"Y79Y/001", "Y79Y/010",
// wrong event stream: the port matches libyaml but both deviate from the
// canonical events (libyaml quirks to be surpassed).
"652Z", "HM87/01", "JEF9/02", "Y2GN",
"652Z", "HM87/01", "JEF9/02",
]
@Test("Every valid case emits canonical events, except the recorded gaps")