Skip to content

Commit

Permalink
Merge pull request #19 from xing/dependant_targets
Browse files Browse the repository at this point in the history
Dependant targets
  • Loading branch information
Juantri94 authored Mar 7, 2023
2 parents ffcd5ff + 1ad9f7b commit 9713f0f
Show file tree
Hide file tree
Showing 7 changed files with 257 additions and 7 deletions.
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ A Swift CLI tool that generates complexity metrics information from a Cocoapods
- Number of dependant modules
- Compare stats between different branches
- Show stats along the git history
- Show dependant targets

You can read more information about dependency complexity in our Technical article ["How to control your dependencies"](https://tech.xing.com/how-to-control-your-ios-dependencies-7690cc7b1c40).

Expand Down Expand Up @@ -145,6 +146,32 @@ jungle modules --target jungle

```

### Get dependant modules

```shell
OVERVIEW: Outputs a sorted list of targets that depends on the specified one in target

USAGE: jungle dependant --target <target> [--show-only-tests] [<directory-path>]

ARGUMENTS:
<directory-path> Path to the directory where Podfile.lock or Package.swift is located (default: .)

OPTIONS:
--target <target> The target in your Podfile or Package.swift file to be used
--show-only-tests Show only Test targets
--version Show the version.
-h, --help Show help information.

```

Example:

```shell
jungle dependant --target SamplePackage $HOME/Desktop/SamplePackage

Library, LibraryTests, SamplePackageTests


### Visualize Complexity Graphs

```shell
Expand Down
10 changes: 8 additions & 2 deletions Sources/DependencyModule/Module.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@ import Foundation
public struct Module: Hashable {
public let name: String
public let dependencies: [String]

public init(name: String, dependencies: [String]) {
public let type: ModuleType
public init(name: String, dependencies: [String], type: ModuleType = .library) {
self.name = name
self.dependencies = dependencies
self.type = type
}

public enum ModuleType {
case library
case test
}
}
12 changes: 9 additions & 3 deletions Sources/PodExtractor/Module+Podfile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public func modulesFromJSONPodfile(_ contents: String) throws -> [Module] {
return targetsRaw.flatMap(\.asTarget)
}

public func extractModulesFromPodfileLock(_ contents: String, excludeExternals: Bool = true) throws -> [Module] {
public func extractModulesFromPodfileLock(_ contents: String, excludeExternals: Bool = true, excludeTests: Bool = true) throws -> [Module] {
// parse YAML to JSON
guard let yaml = try? Yams.load(yaml: contents) else {
throw PodError.yamlParsingFailed
Expand All @@ -108,7 +108,9 @@ public func extractModulesFromPodfileLock(_ contents: String, excludeExternals:
// Exclude Test and External Pods
let podsWithoutExternalsOrSubspecs = pods
.filter { !externals.contains($0.name) }
.filter { !$0.name.contains("/") } // SubSpecs like Tests and Externals Subspecs
.filter {
return excludeTests ? !$0.name.contains("/") : true
} // SubSpecs like Tests and Externals Subspecs

return podsWithoutExternalsOrSubspecs
}
Expand All @@ -121,9 +123,13 @@ private func extractPodFromJSON(_ json: Any) throws -> Module {
let name = container.keys.first,
let dependencies = container.values.first {

let podComponents = name.components(separatedBy: "/")
let podType: Module.ModuleType = podComponents.count > 1 && podComponents[1].contains("Tests") ? .test : .library

return try .init(
name: clean(name),
dependencies: dependencies.map(clean)
dependencies: dependencies.map(clean),
type: podType
)

} else {
Expand Down
55 changes: 55 additions & 0 deletions Sources/SPMExtractor/Module+Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public struct Package: Decodable {
let name: String
let targetDependencies: [String]?
let productDependencies: [String]?
let type: TargetType

var dependencies: [String] {
[targetDependencies, productDependencies].compactMap { $0 }.flatMap { $0 }
Expand All @@ -18,6 +19,13 @@ public struct Package: Decodable {
case name
case targetDependencies = "target_dependencies"
case productDependencies = "product_dependencies"
case type
}

public enum TargetType: String, Decodable {
case library
case test
case executable
}
}
}
Expand Down Expand Up @@ -60,6 +68,53 @@ public func extracPackageModules(from packageRaw: String, target: String) throws
return (dependencies + external, targetDependencies)
}

public func extractDependantTargets(from packageRaw: String, target: String) throws -> [Module] {
guard
let data = packageRaw.data(using: .utf8)
else {
throw PackageError.nonDecodable(raw: packageRaw)
}

let package = try JSONDecoder().decode(Package.self, from: data)

guard let target = package.targets.filter({ $0.name == target }).first else {
throw TargetError.targetNotFound(target: target)
}

let dependantTargets: [Module] = package.targets
.filter { $0.dependencies.contains(target.name) }
.compactMap { .init(name: $0.name, dependencies: $0.dependencies, type: $0.type == .library ? .library : .test ) }

guard !dependantTargets.isEmpty else {
return dependantTargets
}

var indirectTargets: [Module] = []

try dependantTargets.forEach { target in
indirectTargets += try extractDependantTargets(from: packageRaw, target: target.name)
}

return dependantTargets + indirectTargets
}

public func extractDependantTargets(from modules: [Module], for target: String) throws -> [Module] {

let dependantTargets: [Module] = modules
.filter { $0.dependencies.contains(target) || $0.name.components(separatedBy: "/").count == 2 && $0.name.components(separatedBy: "/").first == target }

guard !dependantTargets.isEmpty else {
return dependantTargets
}

var indirectTargets: [Module] = []

try dependantTargets.forEach {
indirectTargets += try extractDependantTargets(from: modules, for: $0.name)
}

return dependantTargets + indirectTargets
}

public func extractDependencies(from package: Package, on target: String) -> [Module] {
guard
Expand Down
56 changes: 56 additions & 0 deletions Sources/jungle/Commands/DependantCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import Foundation
import ArgumentParser
import Shell
import SPMExtractor
import DependencyModule
import PodExtractor

struct DependantCommand: ParsableCommand {
static var configuration = CommandConfiguration(
commandName: "dependant",
abstract: "Outputs a sorted list of targets that depends on the specified one in target"
)

@Option(help: "The target in your Podfile or Package.swift file to be used")
var target: String

@Flag(help: "Show only Test targets")
var showOnlyTests: Bool = false

@Argument(help: "Path to the directory where Podfile.lock or Package.swift is located")
var directoryPath: String = "."

func run() throws {
let directoryPath = (directoryPath as NSString).expandingTildeInPath
let directoryURL = URL(fileURLWithPath: directoryPath, isDirectory: true)

// Check when this contains a Package.swift or a Podfile
if FileManager.default.fileExists(atPath: directoryURL.appendingPathComponent("Package.swift").path) {
try processPackage(at: directoryURL)
} else {
try processPodfile(at: directoryURL)
}
}

private func processPackage(at directoryURL: URL) throws {
let packageRaw = try shell("swift package describe --type json", at: directoryURL)
let targets = try extractDependantTargets(from: packageRaw, target: target)
processOutput(for: targets)
}

private func processPodfile(at directoryURL: URL) throws {
let podfileLock = try shell("git show HEAD:Podfile.lock", at: directoryURL)
let allPodfileModules = try extractModulesFromPodfileLock(podfileLock, excludeTests: false)
let targets = try extractDependantTargets(from: allPodfileModules, for: target)
processOutput(for: targets)
}

private func processOutput(for modules: [Module]) {
let output = Array(Set(modules))
.filter { showOnlyTests ? $0.type == .test : true }
.map(\.name)
.sorted()
.joined(separator: ", ")
print(output)
}
}
4 changes: 2 additions & 2 deletions Sources/jungle/Commands/Main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ struct Jungle: AsyncParsableCommand {
static var configuration = CommandConfiguration(
commandName: "jungle",
abstract: "SwiftPM and Cocoapods based projects complexity analyzer.",
version: "2.1.1",
subcommands: [HistoryCommand.self, CompareCommand.self, GraphCommand.self, ModulesCommand.self],
version: "2.2.0",
subcommands: [HistoryCommand.self, CompareCommand.self, GraphCommand.self, ModulesCommand.self, DependantCommand.self],
defaultSubcommand: CompareCommand.self
)
}
100 changes: 100 additions & 0 deletions Tests/SPMExtractorTests/SPMExtractorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -340,4 +340,104 @@ final class SPMExtractorTests: XCTestCase {

XCTAssertThrowsError(try extracPackageModules(from: rawPackage, target: "NonExistentTarget"))
}

func testTargetDependantFromTarget() throws {
let rawPackage = """
{
"dependencies" : [
{
"identity" : "yams",
"requirement" : {
"range" : [
{
"lower_bound" : "5.0.1",
"upper_bound" : "6.0.0"
}
]
},
"type" : "sourceControl",
"url" : "https://github.com/jpsim/Yams.git"
}
],
"manifest_display_name" : "SamplePackage",
"name" : "SamplePackage",
"path" : "/Users/oswaldo.rubio/Desktop/SamplePackage",
"platforms" : [
],
"products" : [
{
"name" : "SamplePackage",
"targets" : [
"SamplePackage"
],
"type" : {
"library" : [
"automatic"
]
}
}
],
"targets" : [
{
"c99name" : "SamplePackageTests",
"module_type" : "SwiftTarget",
"name" : "SamplePackageTests",
"path" : "Tests/SamplePackageTests",
"sources" : [
"SamplePackageTests.swift"
],
"target_dependencies" : [
"SamplePackage"
],
"type" : "test"
},
{
"c99name" : "SamplePackage",
"module_type" : "SwiftTarget",
"name" : "SamplePackage",
"path" : "Sources/SamplePackage",
"product_memberships" : [
"SamplePackage"
],
"sources" : [
"SamplePackage.swift"
],
"type" : "library"
},
{
"c99name" : "LibraryTests",
"module_type" : "SwiftTarget",
"name" : "LibraryTests",
"path" : "Tests/LibraryTests",
"sources" : [
"File.swift"
],
"target_dependencies" : [
"Library"
],
"type" : "test"
},
{
"c99name" : "Library",
"module_type" : "SwiftTarget",
"name" : "Library",
"path" : "Sources/Library",
"sources" : [
"File.swift"
],
"target_dependencies" : [
"SamplePackage"
],
"type" : "library"
}
],
"tools_version" : "5.7"
}
"""


let dependant = try extractDependantTargets(from: rawPackage, target: "SamplePackage")
XCTAssertEqual(dependant.map(\.name).sorted(), ["Library", "LibraryTests", "SamplePackageTests"])
}
}

0 comments on commit 9713f0f

Please sign in to comment.