diff --git a/Sources/Yams/Parser.swift b/Sources/Yams/Parser.swift index 14ad9ef1..0caa4d55 100644 --- a/Sources/Yams/Parser.swift +++ b/Sources/Yams/Parser.swift @@ -356,15 +356,7 @@ private extension Parser { event = try parse() } let keys = pairs.map { $0.0 } - let duplicateKeys = Dictionary(grouping: keys, by: {$0}).filter { $1.count > 1 }.keys - if let duplicatedKey = duplicateKeys.first { - throw YamlError.parser( - context: YamlError.Context(text: "expected all keys in mapping to be unique", - mark: Mark(line: 1, column: 1)), - problem: "but found multiple instances of: \(duplicateKeys.compactMap { $0.string })", - duplicatedKey.mark!, - yaml: yaml) - } + try checkDuplicates(mappingKeys: keys) let node = Node.mapping(.init(pairs, tag(firstEvent.mappingTag), event.mappingStyle, firstEvent.startMark)) if let anchor = firstEvent.mappingAnchor { anchors[anchor] = node @@ -372,6 +364,23 @@ private extension Parser { return node } + private func checkDuplicates(mappingKeys: [Node]) throws { + var duplicates: [String: [Node]] = [:] + for key in mappingKeys { + if let keyString = key.string { + if duplicates.keys.contains(keyString) { + duplicates[keyString]?.append(key) + } else { + duplicates[keyString] = [key] + } + } + } + duplicates = duplicates.filter { $1.count > 1 } + guard duplicates.isEmpty else { + throw YamlError.duplicatedKeysInMapping(duplicates: duplicates, yaml: yaml) + } + } + func tag(_ string: String?) -> Tag { let tagName = string.map(Tag.Name.init(rawValue:)) ?? .implicit return Tag(tagName, resolver, constructor) diff --git a/Sources/Yams/YamlError.swift b/Sources/Yams/YamlError.swift index 7271589d..bff9c900 100644 --- a/Sources/Yams/YamlError.swift +++ b/Sources/Yams/YamlError.swift @@ -75,6 +75,11 @@ public enum YamlError: Error { /// - parameter encoding: The string encoding used to decode the string data. case dataCouldNotBeDecoded(encoding: String.Encoding) + /// Multiple uses of the same key detected in a mapping + /// + /// - parameter duplicates: A dictionary keyed by the duplicated node value, with all nodes that duplicate said value + case duplicatedKeysInMapping(duplicates: [String: [Node]], yaml: String) + /// The error context. public struct Context: CustomStringConvertible { /// Context text. @@ -175,6 +180,20 @@ extension YamlError: CustomStringConvertible { return problem case .dataCouldNotBeDecoded(encoding: let encoding): return "String could not be decoded from data using '\(encoding)' encoding" + case let .duplicatedKeysInMapping(duplicates, yaml): + return duplicates.duplicatedKeyErrorDescription(yaml: yaml) + } + } +} + +private extension Dictionary where Key == String, Value == [Node] { + func duplicatedKeyErrorDescription(yaml: String) -> String { + var error = "error: parser: expected all keys to be unique but found the following duplicated key(s):" + for key in self.keys.sorted() { + let duplicatedNodes = self[key]! + let marks = duplicatedNodes.compactMap { $0.mark } + error += "\n\(key) (\(marks)):\n\(marks.map { $0.snippet(from: yaml) }.joined(separator: "\n"))" } + return error } } diff --git a/Tests/YamsTests/YamlErrorTests.swift b/Tests/YamsTests/YamlErrorTests.swift index b5d475df..25014957 100644 --- a/Tests/YamsTests/YamlErrorTests.swift +++ b/Tests/YamsTests/YamlErrorTests.swift @@ -152,15 +152,45 @@ class YamlErrorTests: XCTestCase { func testDuplicateKeysCannotBeParsed() throws { let yamlString = """ - key: value - key: different_value + a: value + a: different_value """ XCTAssertThrowsError(try Parser(yaml: yamlString).singleRoot()) { error in XCTAssertTrue(error is YamlError) XCTAssertEqual("\(error)", """ - 1:5: error: parser: expected all keys in mapping to be unique in line 1, column 1 - but found multiple instances of: ["key"]: - key: value + error: parser: expected all keys to be unique but found the following duplicated key(s): + a ([1:5, 2:5]): + a: value + ^ + a: different_value + ^ + """) + } + } + + func testDuplicatedKeysCannotBeParsed_MultipleDuplicates() throws { + let yamlString = """ + a: value + a: different_value + b: value + b: different_value + b: different_different_value + """ + XCTAssertThrowsError(try Parser(yaml: yamlString).singleRoot()) { error in + XCTAssertTrue(error is YamlError) + XCTAssertEqual("\(error)", """ + error: parser: expected all keys to be unique but found the following duplicated key(s): + a ([1:5, 2:5]): + a: value + ^ + a: different_value + ^ + b ([3:5, 4:5, 5:5]): + b: value + ^ + b: different_value + ^ + b: different_different_value ^ """) }