Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions Sources/DependencyCalculator/DependencyGraph.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ extension WorkspaceInfo {
folders: folders,
dependencyStructure: resultDependencies,
candidateTestPlans: candidateTestPlans)
let finalWorkspaceInfo: WorkspaceInfo
if let config {
let additionalBasePath: Path
if path.extension == "xcworkspace" || path.extension == "xcodeproj" {
Expand All @@ -161,10 +162,14 @@ extension WorkspaceInfo {
additionalBasePath = path
}
// Process additional config
return processAdditional(config: config, workspaceInfo: workspaceInfo, basePath: additionalBasePath)
finalWorkspaceInfo = processAdditional(config: config,
workspaceInfo: workspaceInfo,
basePath: additionalBasePath)
} else {
return workspaceInfo
finalWorkspaceInfo = workspaceInfo
}

return finalWorkspaceInfo.pruningDisconnectedTargets()
}

static func processAdditional(config: WorkspaceInfo.AdditionalConfig,
Expand Down
34 changes: 34 additions & 0 deletions Sources/Workspace/DependencyGraph.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,38 @@ public struct DependencyGraph {

return DependencyGraph(dependsOn: map)
}

public func reachableTargets(startingFrom roots: Set<TargetIdentity>) -> Set<TargetIdentity> {
guard !roots.isEmpty else { return [] }

var visited = Set<TargetIdentity>()
var stack = Array(roots)

while let current = stack.popLast() {
if visited.contains(current) {
continue
}
visited.insert(current)

let dependencies = dependsOn[current] ?? Set<TargetIdentity>()
for dependency in dependencies where !visited.contains(dependency) {
stack.append(dependency)
}
}

return visited
}

public func filteringTargets(_ allowed: Set<TargetIdentity>) -> DependencyGraph {
guard !allowed.isEmpty else { return DependencyGraph(dependsOn: [:]) }

var filtered: [TargetIdentity: Set<TargetIdentity>] = [:]

for target in allowed {
let dependencies = (dependsOn[target] ?? Set<TargetIdentity>()).intersection(allowed)
filtered[target] = dependencies
}

return DependencyGraph(dependsOn: filtered)
}
}
29 changes: 29 additions & 0 deletions Sources/Workspace/WorkspaceInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,32 @@ public extension WorkspaceInfo {
public let dependencies: [String: [String]]
}
}

public extension WorkspaceInfo {
func pruningDisconnectedTargets() -> WorkspaceInfo {
let projectTargets = Set(files.keys.filter { $0.type == .project })
guard !projectTargets.isEmpty else { return self }

var reachable = dependencyStructure.reachableTargets(startingFrom: projectTargets).union(projectTargets)
guard !reachable.isEmpty else { return self }

let reachablePackageRoots = Set(reachable
.filter { $0.type == .package }
.map { $0.path })

if !reachablePackageRoots.isEmpty {
for target in files.keys where target.type == .package && reachablePackageRoots.contains(target.path) {
reachable.insert(target)
}
}

let filteredFiles = files.filter { reachable.contains($0.key) }
let filteredFolders = folders.filter { reachable.contains($0.value) }
let filteredDependencies = dependencyStructure.filteringTargets(reachable)

return WorkspaceInfo(files: filteredFiles,
folders: filteredFolders,
dependencyStructure: filteredDependencies,
candidateTestPlans: candidateTestPlans)
}
}
63 changes: 63 additions & 0 deletions Tests/DependencyCalculatorTests/DependencyCalculatorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,67 @@ struct DependencyCalculatorTests {
// then
#expect(affected == Set([module, moduleTests, mainApp, mainAppTests]))
}

@Test
func filtersUnreferencedPackagesWhenWorkspaceHasProjects() throws {
let project = TargetIdentity.project(path: Path("/workspace/App.xcodeproj"),
targetName: "App",
testTarget: false)
let usedPackage = TargetIdentity.package(path: Path("/workspace/Packages/Used"),
targetName: "UsedTarget",
testTarget: false)
let unusedPackage = TargetIdentity.package(path: Path("/workspace/Packages/Unused"),
targetName: "UnusedTarget",
testTarget: false)

let files: [TargetIdentity: Set<Path>] = [
project: [Path("/workspace/App/App.swift")],
usedPackage: [Path("/workspace/Packages/Used/Source.swift")],
unusedPackage: [Path("/workspace/Packages/Unused/Source.swift")]
]

let dependencies = DependencyGraph(dependsOn: [
project: Set([usedPackage]),
usedPackage: Set()
])

let info = WorkspaceInfo(files: files,
folders: [:],
dependencyStructure: dependencies,
candidateTestPlan: nil)

let pruned = info.pruningDisconnectedTargets()

#expect(pruned.files.keys.contains(project))
#expect(pruned.files.keys.contains(usedPackage))
#expect(!pruned.files.keys.contains(unusedPackage))
}

@Test
func keepsPackagesWhenNoProjectsPresent() throws {
let packageA = TargetIdentity.package(path: Path("/workspace/Packages/A"),
targetName: "ATarget",
testTarget: false)
let packageB = TargetIdentity.package(path: Path("/workspace/Packages/B"),
targetName: "BTarget",
testTarget: false)

let files: [TargetIdentity: Set<Path>] = [
packageA: [Path("/workspace/Packages/A/file.swift")],
packageB: [Path("/workspace/Packages/B/file.swift")]
]

let dependencies = DependencyGraph(dependsOn: [
packageA: Set([packageB])
])

let info = WorkspaceInfo(files: files,
folders: [:],
dependencyStructure: dependencies,
candidateTestPlan: nil)

let pruned = info.pruningDisconnectedTargets()

#expect(pruned.files.keys == files.keys)
}
}