Skip to content

Commit 4f7a99f

Browse files
authored
Merge pull request #1399 from hartbit/unused-dependencies-check
[SR-6064] Warn on unused dependencies
2 parents 7918e4d + e2832e4 commit 4f7a99f

File tree

3 files changed

+100
-3
lines changed

3 files changed

+100
-3
lines changed

Sources/PackageGraph/PackageGraphLoader.swift

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,18 @@ import PackageLoading
1313
import PackageModel
1414
import Utility
1515

16+
struct UnusedDependencyDiagnostic: DiagnosticData {
17+
static let id = DiagnosticID(
18+
type: UnusedDependencyDiagnostic.self,
19+
name: "org.swift.diags.unused-dependency",
20+
defaultBehavior: .warning,
21+
description: {
22+
$0 <<< "dependency" <<< { "'\($0.dependencyName)'" } <<< "is not used by any target"
23+
})
24+
25+
public let dependencyName: String
26+
}
27+
1628
enum PackageGraphError: Swift.Error {
1729
/// Indicates a non-root package with no targets.
1830
case noModules(Package)
@@ -134,13 +146,46 @@ public struct PackageGraphLoader {
134146
diagnostics: diagnostics
135147
)
136148

149+
let rootPackages = resolvedPackages.filter({ rootManifestSet.contains($0.manifest) })
150+
151+
checkAllDependenciesAreUsed(rootPackages, diagnostics)
152+
137153
return PackageGraph(
138-
rootPackages: resolvedPackages.filter({ rootManifestSet.contains($0.manifest) }),
154+
rootPackages: rootPackages,
139155
rootDependencies: resolvedPackages.filter({ rootDependencies.contains($0.manifest) })
140156
)
141157
}
142158
}
143159

160+
private func checkAllDependenciesAreUsed(_ rootPackages: [ResolvedPackage], _ diagnostics: DiagnosticsEngine) {
161+
for package in rootPackages {
162+
// List all dependency products dependended on by the package targets.
163+
let productDependencies: Set<ResolvedProduct> = Set(package.targets.flatMap({ target in
164+
return target.dependencies.flatMap({ targetDependency in
165+
switch targetDependency {
166+
case .product(let product):
167+
return product
168+
case .target:
169+
return nil
170+
}
171+
})
172+
}))
173+
174+
for dependency in package.dependencies {
175+
// We continue if the dependency contains executable products to make sure we don't
176+
// warn on a valid use-case for a lone dependency: swift run dependency executables.
177+
guard !dependency.products.contains(where: { $0.type == .executable }) else {
178+
continue
179+
}
180+
181+
let dependencyIsUsed = dependency.products.contains(where: productDependencies.contains)
182+
if !dependencyIsUsed {
183+
diagnostics.emit(data: UnusedDependencyDiagnostic(dependencyName: dependency.name))
184+
}
185+
}
186+
}
187+
}
188+
144189
/// Create resolved packages from the loaded packages.
145190
private func createResolvedPackages(
146191
allManifests: [Manifest],

Tests/PackageGraphTests/PackageGraphTests.swift

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,64 @@ class PackageGraphTests: XCTestCase {
170170
}
171171
}
172172

173+
func testUnusedDependency() throws {
174+
let fs = InMemoryFileSystem(emptyFiles:
175+
"/Foo/Sources/Foo/foo.swift",
176+
"/Bar/Sources/Bar/bar.swift",
177+
"/Baz/Sources/Baz/baz.swift",
178+
"/Biz/Sources/Biz/main.swift"
179+
)
180+
181+
let diagnostics = DiagnosticsEngine()
182+
_ = loadMockPackageGraph4([
183+
"/Bar": Package(
184+
name: "Bar",
185+
products: [
186+
.library(name: "BarLibrary", targets: ["Bar"]),
187+
],
188+
targets: [
189+
.target(name: "Bar"),
190+
]),
191+
"/Baz": Package(
192+
name: "Baz",
193+
products: [
194+
.library(name: "BazLibrary", targets: ["Baz"]),
195+
],
196+
targets: [
197+
.target(name: "Baz"),
198+
]),
199+
"/Biz": Package(
200+
name: "Biz",
201+
products: [
202+
.executable(name: "biz", targets: ["Biz"]),
203+
],
204+
targets: [
205+
.target(name: "Biz"),
206+
]),
207+
"/Foo": .init(
208+
name: "Foo",
209+
dependencies: [
210+
.package(url: "/Bar", from: "1.0.0"),
211+
.package(url: "/Baz", from: "1.0.0"),
212+
.package(url: "/Biz", from: "1.0.0"),
213+
],
214+
targets: [
215+
.target(name: "Foo", dependencies: ["BarLibrary"]),
216+
]),
217+
], root: "/Foo", diagnostics: diagnostics, in: fs)
218+
219+
DiagnosticsEngineTester(diagnostics) { result in
220+
result.check(diagnostic: "dependency 'Baz' is not used by any target", behavior: .warning)
221+
}
222+
}
223+
173224
static var allTests = [
174225
("testBasic", testBasic),
175226
("testDuplicateModules", testDuplicateModules),
176227
("testCycle", testCycle),
177228
("testProductDependencies", testProductDependencies),
178229
("testTestTargetDeclInExternalPackage", testTestTargetDeclInExternalPackage),
179230
("testEmptyDependency", testEmptyDependency),
231+
("testUnusedDependency", testUnusedDependency),
180232
]
181233
}

Tests/WorkspaceTests/WorkspaceTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,7 +1230,7 @@ final class WorkspaceTests: XCTestCase {
12301230
TestPackage(
12311231
name: "Root",
12321232
targets: [
1233-
TestTarget(name: "Root", dependencies: ["Foo"]),
1233+
TestTarget(name: "Root", dependencies: ["Foo", "Bar"]),
12341234
],
12351235
products: [],
12361236
dependencies: [
@@ -1456,7 +1456,7 @@ final class WorkspaceTests: XCTestCase {
14561456
TestPackage(
14571457
name: "Root",
14581458
targets: [
1459-
TestTarget(name: "Root", dependencies: ["Foo"]),
1459+
TestTarget(name: "Root", dependencies: ["Foo", "Bar"]),
14601460
],
14611461
products: [],
14621462
dependencies: [

0 commit comments

Comments
 (0)