From 779ebe596d7da1c01a3a62246e1a73ae8af38d26 Mon Sep 17 00:00:00 2001 From: Mihaela M Date: Sat, 27 Jun 2026 21:11:10 +0200 Subject: [PATCH] fix(parsing): reject an anchor or tag on an alias node An alias is a bare reference and cannot carry node properties, so '&b *a' (an anchor on an alias) and a tag on an alias are malformed. parseNode ignored any pending anchor or tag when it reached an alias token, dropping the property and accepting the document. The alias case now rejects a pending anchor or tag, and moves to its own parseAlias helper so parseNode stays within the complexity limit. A bare alias and an anchor or tag on a real node are unaffected. Measured against the official YAML test-suite, parse-level conformance rises from 266/333 (79.9%) to 267/333 (80.2%) with no regression. --- CHANGELOG.md | 5 +++++ Sources/Parsing/ParseError.swift | 11 +++++++++-- Sources/Parsing/TokenEventParser.swift | 20 ++++++++++++++++---- Tests/AliasPropertiesTests.swift | 24 ++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 Tests/AliasPropertiesTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index a633191..43d172d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- An alias node can no longer carry an anchor or tag. An alias is a bare reference, so `&b *a` + (an anchor on an alias) and a tag on an alias are now rejected; the parser previously dropped the + property and accepted the document. A bare alias and an anchor or tag on a real node are + unaffected. Lifts official test-suite parse-level conformance from 266/333 (79.9%) to 267/333 + (80.2%), with no regression. - A comment can no longer interrupt a multi-line plain scalar. A comment terminates a plain scalar, so content that tries to continue it on a following line (`key: word1` then a `#` line then ` word2`, or `a` then `b # ...` then `c`) is now rejected instead of being silently merged diff --git a/Sources/Parsing/ParseError.swift b/Sources/Parsing/ParseError.swift index 7c90d91..5a44aa7 100644 --- a/Sources/Parsing/ParseError.swift +++ b/Sources/Parsing/ParseError.swift @@ -27,6 +27,7 @@ public extension PureYAML.Parsing { case blockSequenceAsInlineMappingValue(line: Int) case multilineImplicitKey(line: Int) case commentInMultilineScalar(line: Int) + case aliasWithProperties(line: Int, column: Int) public var description: String { switch self { @@ -82,6 +83,8 @@ public extension PureYAML.Parsing { "an implicit mapping key must not span multiple lines at line \(line)" case let .commentInMultilineScalar(line): "a comment cannot interrupt a multi-line plain scalar at line \(line)" + case let .aliasWithProperties(line, column): + "an alias node cannot have an anchor or tag at line \(line), column \(column)" } } @@ -139,6 +142,8 @@ public extension PureYAML.Parsing { "multilineImplicitKey" case .commentInMultilineScalar: "commentInMultilineScalar" + case .aliasWithProperties: + "aliasWithProperties" } } @@ -170,7 +175,8 @@ public extension PureYAML.Parsing { let .unexpectedEvent(_, _, line, _), let .unexpectedToken(_, _, line, _), let .nestingTooDeep(line, _), - let .commentNotPrecededByWhitespace(line, _): + let .commentNotPrecededByWhitespace(line, _), + let .aliasWithProperties(line, _): line } } @@ -203,7 +209,8 @@ public extension PureYAML.Parsing { let .unexpectedEvent(_, _, _, column), let .unexpectedToken(_, _, _, column), let .nestingTooDeep(_, column), - let .commentNotPrecededByWhitespace(_, column): + let .commentNotPrecededByWhitespace(_, column), + let .aliasWithProperties(_, column): column } } diff --git a/Sources/Parsing/TokenEventParser.swift b/Sources/Parsing/TokenEventParser.swift index 98be6c1..47c2cad 100644 --- a/Sources/Parsing/TokenEventParser.swift +++ b/Sources/Parsing/TokenEventParser.swift @@ -207,6 +207,21 @@ extension PureYAML.Parsing.TokenEventParser { throw PureYAML.Parsing.ParseError.nestingTooDeep(line: mark.line, column: mark.column) } + /// An alias is a bare reference, so it cannot carry an anchor or tag of its own (`&b *a`). + mutating func parseAlias( + anchor: String, + token: PureYAML.Parsing.Token, + properties: PureYAML.Parsing.NodeProperties, + ) throws -> PureYAML.Parsing.NodeResult { + guard properties.anchor == nil, properties.tag == nil else { + let mark = properties.mark ?? token.mark + throw PureYAML.Parsing.ParseError.aliasWithProperties(line: mark.line, column: mark.column) + } + _ = try cursor.advance() + try emit(.alias(anchor: anchor, mark: token.mark)) + return PureYAML.Parsing.NodeResult(kind: .alias, endMark: token.endMark) + } + mutating func parseNode( properties initialProperties: PureYAML.Parsing.NodeProperties, ) throws -> PureYAML.Parsing.NodeResult { @@ -223,10 +238,7 @@ extension PureYAML.Parsing.TokenEventParser { switch token.kind { case let .alias(anchor): - _ = try cursor.advance() - let mark = properties.mark ?? token.mark - try emit(.alias(anchor: anchor, mark: mark)) - return PureYAML.Parsing.NodeResult(kind: .alias, endMark: token.endMark) + return try parseAlias(anchor: anchor, token: token, properties: properties) case .blockEntry: return try parseBlockSequence(properties: properties) case .blockScalarHeader: diff --git a/Tests/AliasPropertiesTests.swift b/Tests/AliasPropertiesTests.swift new file mode 100644 index 0000000..4f70403 --- /dev/null +++ b/Tests/AliasPropertiesTests.swift @@ -0,0 +1,24 @@ +@testable import PureYAML +import Testing + +@Suite("An alias node carries no properties") +struct AliasPropertiesTests { + @Test("An anchor or tag on an alias is rejected", arguments: [ + "key1: &a value\nkey2: &b *a\n", // an anchor on an alias + "key1: &a value\nkey2: !!str *a\n", // a tag on an alias + ]) + func test_aliasWithPropertiesRejected(_ yaml: String) throws { + #expect(throws: PureYAML.Parsing.ParseError.self) { + try PureYAML.parse(yaml) + } + } + + @Test("A bare alias, and an anchor on a real node, stay valid") + func test_validAliasesAndAnchors() throws { + #expect(try PureYAML.parse("key1: &a value\nkey2: *a\n") == .mapping(.init([ + .init(key: "key1", value: .string("value")), + .init(key: "key2", value: .string("value")), + ]))) + #expect(try PureYAML.parse("- &a x\n- *a\n") == .sequence(.init([.string("x"), .string("x")]))) + } +}