Skip to content

Commit 2cd58cc

Browse files
committed
[PackageLoading] Diagnose duplicate dependency declaration
<rdar://problem/42625896> [SR-8379]: SwiftPM? Crash when compiling LinkerKitIRCBot
1 parent 41e1fb3 commit 2cd58cc

File tree

5 files changed

+104
-1
lines changed

5 files changed

+104
-1
lines changed

Sources/Basic/CollectionAlgorithms.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,15 @@ public extension Sequence where Iterator.Element: Hashable {
4848
return duplicate
4949
}
5050
}
51+
52+
extension Collection where Element: Hashable {
53+
54+
/// Finds duplicates in given collection of Hashables.
55+
public func findDuplicateElements() -> [[Element]] {
56+
var table: [Element: [Element]] = [:]
57+
for element in self {
58+
table[element, default: []].append(element)
59+
}
60+
return table.values.filter({ $0.count > 1 })
61+
}
62+
}

Sources/PackageLoading/ManifestLoader.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ public enum ManifestParseError: Swift.Error {
2121

2222
/// The manifest was successfully loaded by swift interpreter but there were runtime issues.
2323
case runtimeManifestErrors([String])
24+
25+
case duplicateDependencyDecl([[PackageDependencyDescription]])
2426
}
2527

2628
/// Resources required for manifest loading.
@@ -229,9 +231,19 @@ public final class ManifestLoader: ManifestLoaderProtocol {
229231
targets: manifestBuilder.targets
230232
)
231233

234+
try validate(manifest)
235+
232236
return manifest
233237
}
234238

239+
/// Validate the provided manifest.
240+
private func validate(_ manifest: Manifest) throws {
241+
let duplicateDecls = manifest.dependencies.map({ KeyedPair($0, key: PackageReference.computeIdentity(packageURL: $0.url)) }).findDuplicateElements()
242+
if !duplicateDecls.isEmpty {
243+
throw ManifestParseError.duplicateDependencyDecl(duplicateDecls.map({ $0.map({ $0.item }) }))
244+
}
245+
}
246+
235247
/// Load the JSON string for the given manifest.
236248
func loadJSONString(
237249
path inputPath: AbsolutePath,

Sources/PackageModel/Manifest.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ public enum SystemPackageProviderDescription: Equatable {
283283
public struct PackageDependencyDescription: Equatable, Codable {
284284

285285
/// The dependency requirement.
286-
public enum Requirement: Equatable {
286+
public enum Requirement: Equatable, CustomStringConvertible {
287287
case exact(Version)
288288
case range(Range<Version>)
289289
case revision(String)
@@ -297,6 +297,21 @@ public struct PackageDependencyDescription: Equatable, Codable {
297297
public static func upToNextMinor(from version: Utility.Version) -> Requirement {
298298
return .range(version..<Version(version.major, version.minor + 1, 0))
299299
}
300+
301+
public var description: String {
302+
switch self {
303+
case .exact(let version):
304+
return version.description
305+
case .range(let range):
306+
return range.description
307+
case .revision(let revision):
308+
return "revision[\(revision)]"
309+
case .branch(let branch):
310+
return "branch[\(branch)]"
311+
case .localPackage:
312+
return "local"
313+
}
314+
}
300315
}
301316

302317
/// The url of the dependency.

Sources/Workspace/Diagnostics.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,45 @@ public struct ManifestParseDiagnostic: DiagnosticData {
3030
}
3131
}
3232

33+
public struct ManifestDuplicateDeclDiagnostic: DiagnosticData {
34+
public static let id = DiagnosticID(
35+
type: ManifestParseDiagnostic.self,
36+
name: "org.swift.diags.manifest-dup-dep-decl",
37+
description: {
38+
$0 <<< .substitution({
39+
let `self` = $0 as! ManifestDuplicateDeclDiagnostic
40+
let stream = BufferedOutputByteStream()
41+
42+
stream <<< "manifest parse error(s): duplicate dependency declaration\n"
43+
let indent = Format.asRepeating(string: " ", count: 4)
44+
45+
for duplicateDecls in self.duplicates {
46+
for duplicate in duplicateDecls {
47+
stream <<< indent <<< duplicate.url <<< " @ " <<< "\(duplicate.requirement)" <<< "\n"
48+
}
49+
stream <<< "\n"
50+
}
51+
52+
return stream.bytes.asString!
53+
}, preference: .default)
54+
}
55+
)
56+
57+
public let duplicates: [[PackageDependencyDescription]]
58+
public init(_ duplicates: [[PackageDependencyDescription]]) {
59+
self.duplicates = duplicates
60+
}
61+
}
62+
3363
extension ManifestParseError: DiagnosticDataConvertible {
3464
public var diagnosticData: DiagnosticData {
3565
switch self {
3666
case .invalidManifestFormat(let error):
3767
return ManifestParseDiagnostic([error])
3868
case .runtimeManifestErrors(let errors):
3969
return ManifestParseDiagnostic(errors)
70+
case .duplicateDependencyDecl(let duplicates):
71+
return ManifestDuplicateDeclDiagnostic(duplicates)
4072
}
4173
}
4274
}

Tests/PackageLoadingTests/PD4_2LoadingTests.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,38 @@ class PackageDescription4_2LoadingTests: XCTestCase {
309309
}
310310
}
311311

312+
func testDuplicateDependencyDecl() throws {
313+
let stream = BufferedOutputByteStream()
314+
stream <<< """
315+
import PackageDescription
316+
let package = Package(
317+
name: "Trivial",
318+
dependencies: [
319+
.package(path: "../foo1"),
320+
.package(url: "/foo1.git", from: "1.0.1"),
321+
.package(url: "path/to/foo1", from: "3.0.0"),
322+
.package(url: "/foo2.git", from: "1.0.1"),
323+
.package(url: "/foo2.git", from: "1.1.1"),
324+
.package(url: "/foo3.git", from: "1.0.1"),
325+
],
326+
targets: [
327+
.target(
328+
name: "foo",
329+
dependencies: ["dep1", .target(name: "target")]),
330+
]
331+
)
332+
"""
333+
334+
do {
335+
try loadManifestThrowing(stream.bytes) { _ in }
336+
XCTFail("Unexpected success")
337+
} catch ManifestParseError.duplicateDependencyDecl(let duplicates) {
338+
XCTAssertEqual(duplicates.count, 2)
339+
let urls = duplicates.flatMap({$0}).map({ $0.url }).sorted()
340+
XCTAssertEqual(urls, ["/foo1", "/foo1.git", "/foo2.git", "/foo2.git", "/path/to/foo1"])
341+
}
342+
}
343+
312344
func testCaching() {
313345
mktmpdir { path in
314346
let fs = localFileSystem

0 commit comments

Comments
 (0)