diff --git a/CHANGELOG.md b/CHANGELOG.md index 53e04c7..2ffb47d 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 +- A mapping value's anchor or tag on the line below the key must now be indented past the key. A + property at the key's own indentation (`seq:` then `&anchor` in column 1) is ambiguous with a + sibling and is rejected, while an inline property, a property indented under the key, and a block + sequence at the key's indent (which carries no property) stay valid. Lifts official test-suite + parse-level conformance from 275/333 (82.6%) to 276/333 (82.9%), with no regression. - A `-` immediately before a flow indicator is no longer treated as a scalar. In a flow collection a `-` that begins a plain scalar must be followed by a non-indicator character, so `[-]`, `[-, -]`, and `{a: -}` are now rejected. A dash followed by ordinary content is unaffected: diff --git a/Sources/Parsing/TokenEventParser.Helpers.swift b/Sources/Parsing/TokenEventParser.Helpers.swift index 6063af5..1e75203 100644 --- a/Sources/Parsing/TokenEventParser.Helpers.swift +++ b/Sources/Parsing/TokenEventParser.Helpers.swift @@ -19,22 +19,9 @@ extension PureYAML.Parsing.TokenEventParser { } let properties = try consumeProperties() + try enforceValuePropertyIndented(properties, valueIndicator: valueIndicator) if cursor.current?.kind.isIndent == true { - _ = try expect("indent") { $0.isIndent } - let result: PureYAML.Parsing.NodeResult = if cursor.current?.kind.isBlockEntry == true { - try parseBlockSequence( - properties: properties, - allowMappingKeyTerminator: true, - ) - } else { - try parseNode(properties: properties) - } - if cursor.current?.kind.isDedent == true { - _ = try expect("dedent") { $0.isDedent } - } else if cursor.current?.kind.isMappingKey != true { - _ = try expect("dedent") { $0.isDedent } - } - return result + return try parseIndentedMappingValue(properties: properties) } if properties == .none, try isCommentInterruptedScalarValue(after: valueIndicator) { return try rejectCommentInterruptedScalar() @@ -59,6 +46,42 @@ extension PureYAML.Parsing.TokenEventParser { return try emitNullMappingValue(at: valueIndicator.endMark) } + /// Parses a mapping value that begins on an indented line below the `:`. A leading block entry + /// is a block sequence; anything else is a node. The closing dedent is consumed unless the value + /// ended on a sibling mapping key, which the surrounding mapping continues from. + mutating func parseIndentedMappingValue( + properties: PureYAML.Parsing.NodeProperties, + ) throws -> PureYAML.Parsing.NodeResult { + _ = try expect("indent") { $0.isIndent } + let result: PureYAML.Parsing.NodeResult = if cursor.current?.kind.isBlockEntry == true { + try parseBlockSequence(properties: properties, allowMappingKeyTerminator: true) + } else { + try parseNode(properties: properties) + } + if cursor.current?.kind.isDedent == true { + _ = try expect("dedent") { $0.isDedent } + } else if cursor.current?.kind.isMappingKey != true { + _ = try expect("dedent") { $0.isDedent } + } + return result + } + + /// A value's anchor or tag on a line below the `:` must be indented past the key. Reaching the + /// value-property consumption with a property on a later line means no indent token preceded it + /// (a properly indented one is consumed inside the indented value), so it sits at the key's + /// level and is malformed (`seq:` then `&anchor` in column 1). + func enforceValuePropertyIndented( + _ properties: PureYAML.Parsing.NodeProperties, + valueIndicator: PureYAML.Parsing.Token, + ) throws { + guard properties != .none, let propertyMark = properties.mark, + propertyMark.line != valueIndicator.mark.line + else { + return + } + throw PureYAML.Parsing.ParseError.unexpectedIndentation(line: propertyMark.line) + } + /// Emit an empty (null) scalar as a mapping value and return its node result, used where a /// mapping entry has no value: an explicit key with no `:`, or a `key:` with nothing after it. mutating func emitNullMappingValue(at mark: PureYAML.Parsing.Mark) throws -> PureYAML.Parsing.NodeResult { diff --git a/Tests/ValuePropertyIndentationTests.swift b/Tests/ValuePropertyIndentationTests.swift new file mode 100644 index 0000000..a4d7cad --- /dev/null +++ b/Tests/ValuePropertyIndentationTests.swift @@ -0,0 +1,22 @@ +@testable import PureYAML +import Testing + +@Suite("A mapping value's property indentation") +struct ValuePropertyIndentationTests { + @Test("A value's anchor on a line below the key, at the key's indent, is rejected") + func test_underIndentedValuePropertyRejected() throws { + #expect(throws: PureYAML.Parsing.ParseError.self) { + try PureYAML.parse("---\nseq:\n&anchor\n- a\n- b\n") + } + } + + @Test("Properties indented past the key, or inline, stay valid", arguments: [ + "seq:\n &a\n - x\n", // an anchor indented under the key + "key: &a value\n", // an inline anchor on the value + "seq:\n- a\n- b\n", // a block sequence at the key's indent (no property) + "key:\n &a sub: v\n", // an indented anchored key + ]) + func test_validValuePropertyPlacement(_ yaml: String) throws { + _ = try PureYAML.parse(yaml) + } +}