Skip to content

Commit 35aec4e

Browse files
committed
Prune targets not connected to workspace
1 parent f007aed commit 35aec4e

File tree

4 files changed

+133
-2
lines changed

4 files changed

+133
-2
lines changed

Sources/DependencyCalculator/DependencyGraph.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ extension WorkspaceInfo {
153153
folders: folders,
154154
dependencyStructure: resultDependencies,
155155
candidateTestPlans: candidateTestPlans)
156+
let finalWorkspaceInfo: WorkspaceInfo
156157
if let config {
157158
let additionalBasePath: Path
158159
if path.extension == "xcworkspace" || path.extension == "xcodeproj" {
@@ -161,10 +162,14 @@ extension WorkspaceInfo {
161162
additionalBasePath = path
162163
}
163164
// Process additional config
164-
return processAdditional(config: config, workspaceInfo: workspaceInfo, basePath: additionalBasePath)
165+
finalWorkspaceInfo = processAdditional(config: config,
166+
workspaceInfo: workspaceInfo,
167+
basePath: additionalBasePath)
165168
} else {
166-
return workspaceInfo
169+
finalWorkspaceInfo = workspaceInfo
167170
}
171+
172+
return finalWorkspaceInfo.pruningDisconnectedTargets()
168173
}
169174

170175
static func processAdditional(config: WorkspaceInfo.AdditionalConfig,

Sources/Workspace/DependencyGraph.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,38 @@ public struct DependencyGraph {
5454

5555
return DependencyGraph(dependsOn: map)
5656
}
57+
58+
public func reachableTargets(startingFrom roots: Set<TargetIdentity>) -> Set<TargetIdentity> {
59+
guard !roots.isEmpty else { return [] }
60+
61+
var visited = Set<TargetIdentity>()
62+
var stack = Array(roots)
63+
64+
while let current = stack.popLast() {
65+
if visited.contains(current) {
66+
continue
67+
}
68+
visited.insert(current)
69+
70+
let dependencies = dependsOn[current] ?? Set<TargetIdentity>()
71+
for dependency in dependencies where !visited.contains(dependency) {
72+
stack.append(dependency)
73+
}
74+
}
75+
76+
return visited
77+
}
78+
79+
public func filteringTargets(_ allowed: Set<TargetIdentity>) -> DependencyGraph {
80+
guard !allowed.isEmpty else { return DependencyGraph(dependsOn: [:]) }
81+
82+
var filtered: [TargetIdentity: Set<TargetIdentity>] = [:]
83+
84+
for target in allowed {
85+
let dependencies = (dependsOn[target] ?? Set<TargetIdentity>()).intersection(allowed)
86+
filtered[target] = dependencies
87+
}
88+
89+
return DependencyGraph(dependsOn: filtered)
90+
}
5791
}

Sources/Workspace/WorkspaceInfo.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,32 @@ public extension WorkspaceInfo {
9797
public let dependencies: [String: [String]]
9898
}
9999
}
100+
101+
public extension WorkspaceInfo {
102+
func pruningDisconnectedTargets() -> WorkspaceInfo {
103+
let projectTargets = Set(files.keys.filter { $0.type == .project })
104+
guard !projectTargets.isEmpty else { return self }
105+
106+
var reachable = dependencyStructure.reachableTargets(startingFrom: projectTargets).union(projectTargets)
107+
guard !reachable.isEmpty else { return self }
108+
109+
let reachablePackageRoots = Set(reachable
110+
.filter { $0.type == .package }
111+
.map { $0.path })
112+
113+
if !reachablePackageRoots.isEmpty {
114+
for target in files.keys where target.type == .package && reachablePackageRoots.contains(target.path) {
115+
reachable.insert(target)
116+
}
117+
}
118+
119+
let filteredFiles = files.filter { reachable.contains($0.key) }
120+
let filteredFolders = folders.filter { reachable.contains($0.value) }
121+
let filteredDependencies = dependencyStructure.filteringTargets(reachable)
122+
123+
return WorkspaceInfo(files: filteredFiles,
124+
folders: filteredFolders,
125+
dependencyStructure: filteredDependencies,
126+
candidateTestPlans: candidateTestPlans)
127+
}
128+
}

Tests/DependencyCalculatorTests/DependencyCalculatorTests.swift

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,67 @@ struct DependencyCalculatorTests {
9292
// then
9393
#expect(affected == Set([module, moduleTests, mainApp, mainAppTests]))
9494
}
95+
96+
@Test
97+
func filtersUnreferencedPackagesWhenWorkspaceHasProjects() throws {
98+
let project = TargetIdentity.project(path: Path("/workspace/App.xcodeproj"),
99+
targetName: "App",
100+
testTarget: false)
101+
let usedPackage = TargetIdentity.package(path: Path("/workspace/Packages/Used"),
102+
targetName: "UsedTarget",
103+
testTarget: false)
104+
let unusedPackage = TargetIdentity.package(path: Path("/workspace/Packages/Unused"),
105+
targetName: "UnusedTarget",
106+
testTarget: false)
107+
108+
let files: [TargetIdentity: Set<Path>] = [
109+
project: [Path("/workspace/App/App.swift")],
110+
usedPackage: [Path("/workspace/Packages/Used/Source.swift")],
111+
unusedPackage: [Path("/workspace/Packages/Unused/Source.swift")]
112+
]
113+
114+
let dependencies = DependencyGraph(dependsOn: [
115+
project: Set([usedPackage]),
116+
usedPackage: Set()
117+
])
118+
119+
let info = WorkspaceInfo(files: files,
120+
folders: [:],
121+
dependencyStructure: dependencies,
122+
candidateTestPlan: nil)
123+
124+
let pruned = info.pruningDisconnectedTargets()
125+
126+
#expect(pruned.files.keys.contains(project))
127+
#expect(pruned.files.keys.contains(usedPackage))
128+
#expect(!pruned.files.keys.contains(unusedPackage))
129+
}
130+
131+
@Test
132+
func keepsPackagesWhenNoProjectsPresent() throws {
133+
let packageA = TargetIdentity.package(path: Path("/workspace/Packages/A"),
134+
targetName: "ATarget",
135+
testTarget: false)
136+
let packageB = TargetIdentity.package(path: Path("/workspace/Packages/B"),
137+
targetName: "BTarget",
138+
testTarget: false)
139+
140+
let files: [TargetIdentity: Set<Path>] = [
141+
packageA: [Path("/workspace/Packages/A/file.swift")],
142+
packageB: [Path("/workspace/Packages/B/file.swift")]
143+
]
144+
145+
let dependencies = DependencyGraph(dependsOn: [
146+
packageA: Set([packageB])
147+
])
148+
149+
let info = WorkspaceInfo(files: files,
150+
folders: [:],
151+
dependencyStructure: dependencies,
152+
candidateTestPlan: nil)
153+
154+
let pruned = info.pruningDisconnectedTargets()
155+
156+
#expect(pruned.files.keys == files.keys)
157+
}
95158
}

0 commit comments

Comments
 (0)