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")]))) + } +}