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.
This commit is contained in:
parent
8649f1722a
commit
779ebe596d
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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:
|
||||
|
||||
24
Tests/AliasPropertiesTests.swift
Normal file
24
Tests/AliasPropertiesTests.swift
Normal file
@ -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")])))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user