Skip to content

Commit 24bef33

Browse files
committed
[PackageGraph] Diagnose duplicate products across package graph
<rdar://problem/42036789> [SR-8220]: duplicate command in 'commands' map
1 parent d430b4b commit 24bef33

File tree

4 files changed

+86
-4
lines changed

4 files changed

+86
-4
lines changed

Sources/Basic/CollectionAlgorithms.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,13 @@ extension Collection where Element: Hashable {
6060
return table.values.filter({ $0.count > 1 })
6161
}
6262
}
63+
64+
extension Sequence {
65+
public func findDuplicateElements<Key: Hashable>(
66+
by keyPath: KeyPath<Self.Element, Key>
67+
) -> [[Element]] {
68+
return Dictionary(grouping: self, by: { $0[keyPath: keyPath] })
69+
.values
70+
.filter({ $0.count > 1 })
71+
}
72+
}

Sources/PackageGraph/PackageGraphLoader.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ enum PackageGraphError: Swift.Error {
3737

3838
/// The product dependency was found but the package name did not match.
3939
case productDependencyIncorrectPackage(name: String, package: String)
40+
41+
/// A product was found in multiple packages.
42+
case duplicateProduct(product: String, packages: [String])
4043
}
4144

4245
extension PackageGraphError: CustomStringConvertible {
@@ -55,6 +58,9 @@ extension PackageGraphError: CustomStringConvertible {
5558

5659
case .productDependencyIncorrectPackage(let name, let package):
5760
return "product dependency '\(name)' in package '\(package)' not found"
61+
62+
case .duplicateProduct(let product, let packages):
63+
return "multiple products named '\(product)' in: \(packages.joined(separator: ", "))"
5864
}
5965
}
6066
}
@@ -242,6 +248,28 @@ private func createResolvedPackages(
242248
})
243249
}
244250

251+
// Find duplicate products in the package graph.
252+
let duplicateProducts = packageBuilders
253+
.flatMap({ $0.products })
254+
.map({ $0.product })
255+
.findDuplicateElements(by: \.name)
256+
.map({ $0[0].name })
257+
258+
// Emit diagnostics for duplicate products.
259+
for productName in duplicateProducts {
260+
let packages = packageBuilders
261+
.filter({ $0.products.contains(where: { $0.product.name == productName }) })
262+
.map({ $0.package.name })
263+
.sorted()
264+
265+
diagnostics.emit(PackageGraphError.duplicateProduct(product: productName, packages: packages))
266+
}
267+
268+
// Remove the duplicate products from the builders.
269+
for packageBuilder in packageBuilders {
270+
packageBuilder.products = packageBuilder.products.filter({ !duplicateProducts.contains($0.product.name) })
271+
}
272+
245273
// The set of all target names.
246274
var allTargetNames = Set<String>()
247275

Tests/PackageGraphTests/PackageGraphTests.swift

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -588,10 +588,6 @@ class PackageGraphTests: XCTestCase {
588588
dependencies: [
589589
PackageDependencyDescription(url: "/Dep1", requirement: .upToNextMajor(from: "1.0.0")),
590590
],
591-
products: [
592-
ProductDescription(name: "FooLibrary", targets: ["Foo"]),
593-
ProductDescription(name: "BarLibrary", targets: ["Bar"]),
594-
],
595591
targets: [
596592
TargetDescription(name: "Foo", dependencies: ["BazLibrary"]),
597593
TargetDescription(name: "Bar"),
@@ -628,4 +624,51 @@ class PackageGraphTests: XCTestCase {
628624
result.check(diagnostic: "multiple targets named 'Foo' in: Dep2, Start", behavior: .error)
629625
}
630626
}
627+
628+
func testDuplicateProducts() throws {
629+
let fs = InMemoryFileSystem(emptyFiles:
630+
"/Foo/Sources/Foo/foo.swift",
631+
"/Bar/Sources/Bar/bar.swift",
632+
"/Baz/Sources/Baz/baz.swift"
633+
)
634+
635+
let diagnostics = DiagnosticsEngine()
636+
_ = loadPackageGraph(root: "/Foo", fs: fs, diagnostics: diagnostics,
637+
manifests: [
638+
Manifest.createV4Manifest(
639+
name: "Foo",
640+
path: "/Foo",
641+
url: "/Foo",
642+
dependencies: [
643+
PackageDependencyDescription(url: "/Bar", requirement: .upToNextMajor(from: "1.0.0")),
644+
PackageDependencyDescription(url: "/Baz", requirement: .upToNextMajor(from: "1.0.0")),
645+
],
646+
targets: [
647+
TargetDescription(name: "Foo", dependencies: ["Bar"]),
648+
]),
649+
Manifest.createV4Manifest(
650+
name: "Bar",
651+
path: "/Bar",
652+
url: "/Bar",
653+
products: [
654+
ProductDescription(name: "Bar", targets: ["Bar"])
655+
],
656+
targets: [
657+
TargetDescription(name: "Bar"),
658+
]),
659+
Manifest.createV4Manifest(
660+
name: "Baz",
661+
path: "/Baz",
662+
url: "/Baz",
663+
products: [
664+
ProductDescription(name: "Bar", targets: ["Baz"])
665+
],
666+
targets: [
667+
TargetDescription(name: "Baz"),
668+
]),
669+
]
670+
)
671+
672+
XCTAssertTrue(diagnostics.diagnostics.contains(where: { $0.localizedDescription.contains("multiple products named 'Bar' in: Bar, Baz") }), "\(diagnostics.diagnostics)")
673+
}
631674
}

Tests/PackageGraphTests/XCTestManifests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ extension PackageGraphTests {
2626
("testCycle", testCycle),
2727
("testDuplicateInterPackageTargetNames", testDuplicateInterPackageTargetNames),
2828
("testDuplicateModules", testDuplicateModules),
29+
("testDuplicateProducts", testDuplicateProducts),
2930
("testEmptyDependency", testEmptyDependency),
3031
("testMultipleDuplicateModules", testMultipleDuplicateModules),
3132
("testNestedDuplicateModules", testNestedDuplicateModules),

0 commit comments

Comments
 (0)