├── .gitignore ├── .swiftlint.yml ├── Example └── main.swift ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── Graphviz │ ├── Graphviz.swift │ ├── Node.swift │ ├── Statement.swift │ ├── Subgraph.swift │ └── SwiftGraph+Extensions.swift ├── GriftKit │ ├── GraphBuilder.swift │ ├── GraphStructure.swift │ ├── Graphviz+Extensions.swift │ ├── Structure+Extensions.swift │ └── TypeNameExtractor.swift └── grift │ ├── DependenciesCommand.swift │ └── main.swift ├── Tests ├── GraphvizTests │ └── GraphvizTests.swift └── GriftKitTests │ └── GriftKitTests.swift └── roadmap.md /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/xcode,swift,macos 3 | 4 | ### macOS ### 5 | *.DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | 9 | # Icon must end with two \r 10 | Icon 11 | 12 | 13 | # Thumbnails 14 | ._* 15 | 16 | # Files that might appear in the root of a volume 17 | .DocumentRevisions-V100 18 | .fseventsd 19 | .Spotlight-V100 20 | .TemporaryItems 21 | .Trashes 22 | .VolumeIcon.icns 23 | .com.apple.timemachine.donotpresent 24 | 25 | # Directories potentially created on remote AFP share 26 | .AppleDB 27 | .AppleDesktop 28 | Network Trash Folder 29 | Temporary Items 30 | .apdisk 31 | 32 | ### Swift ### 33 | # Xcode 34 | # 35 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 36 | 37 | ## Build generated 38 | build/ 39 | DerivedData/ 40 | 41 | ## Various settings 42 | *.pbxuser 43 | !default.pbxuser 44 | *.mode1v3 45 | !default.mode1v3 46 | *.mode2v3 47 | !default.mode2v3 48 | *.perspectivev3 49 | !default.perspectivev3 50 | xcuserdata/ 51 | 52 | ## Other 53 | *.moved-aside 54 | *.xccheckout 55 | *.xcscmblueprint 56 | 57 | ## Obj-C/Swift specific 58 | *.hmap 59 | *.ipa 60 | *.dSYM.zip 61 | *.dSYM 62 | 63 | ## Playgrounds 64 | timeline.xctimeline 65 | playground.xcworkspace 66 | 67 | # Swift Package Manager 68 | # 69 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 70 | # Packages/ 71 | # Package.pins 72 | .build/ 73 | 74 | # CocoaPods 75 | # 76 | # We recommend against adding the Pods directory to your .gitignore. However 77 | # you should judge for yourself, the pros and cons are mentioned at: 78 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 79 | # 80 | # Pods/ 81 | 82 | # Carthage 83 | # 84 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 85 | # Carthage/Checkouts 86 | 87 | Carthage/Build 88 | 89 | # fastlane 90 | # 91 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 92 | # screenshots whenever they are needed. 93 | # For more information about the recommended setup visit: 94 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 95 | 96 | fastlane/report.xml 97 | fastlane/Preview.html 98 | fastlane/screenshots 99 | fastlane/test_output 100 | 101 | ### Xcode ### 102 | build 103 | *.xcodeproj/* 104 | #!*.xcodeproj/project.pbxproj 105 | #!*.xcworkspace/contents.xcworkspacedata 106 | /*.gcno 107 | 108 | # End of https://www.gitignore.io/api/xcode,swift,macos 109 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | included: 2 | - grift 3 | - griftTests 4 | - GriftKit 5 | - GriftKitTests 6 | excluded: 7 | - GriftKit/TestFile.swift 8 | opt_in_rules: 9 | - attributes 10 | - closure_end_indentation 11 | - closure_spacing 12 | - empty_count 13 | - explicit_init 14 | - first_where 15 | - number_separator 16 | - operator_usage_whitespace 17 | - overridden_super_call 18 | - private_outlet 19 | - prohibited_super_call 20 | - redundant_nil_coalescing 21 | - sorted_imports 22 | disabled_rules: 23 | - force_cast 24 | - force_try 25 | - todo 26 | - variable_name 27 | 28 | line_length: 240 29 | -------------------------------------------------------------------------------- /Example/main.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /* expected edges: 4 | FooClass -> 5 | String 6 | Array 7 | Int 8 | CustomStringConvertible 9 | 10 | BarStruct -> 11 | FooClass 12 | String 13 | */ 14 | open class FooClass { 15 | var a_variable: String 16 | let a_constant: [Int] 17 | 18 | init(variable: String) { 19 | self.a_variable = variable 20 | 21 | let const = Array() 22 | a_constant = const 23 | _ = thing() 24 | } 25 | 26 | public func thing() -> String { 27 | return a_variable 28 | } 29 | } 30 | 31 | extension FooClass: CustomStringConvertible { 32 | public var description: String { 33 | return a_variable 34 | } 35 | } 36 | 37 | struct BarStruct { 38 | let fooClass = FooClass(variable: "Hello") 39 | 40 | func blah() -> String { 41 | return fooClass.thing() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Kevin Lundberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Clang_C", 6 | "repositoryURL": "https://github.com/norio-nomura/Clang_C.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "90a9574276f0fd17f02f58979423c3fd4d73b59e", 10 | "version": "1.0.2" 11 | } 12 | }, 13 | { 14 | "package": "Commandant", 15 | "repositoryURL": "https://github.com/Carthage/Commandant.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "7f29606ec3a2054a601f0e72f562a104dbc1a11a", 19 | "version": "0.13.0" 20 | } 21 | }, 22 | { 23 | "package": "Nimble", 24 | "repositoryURL": "https://github.com/Quick/Nimble.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "22800b0954c89344bb8c87f8ab93378076716fb7", 28 | "version": "7.0.3" 29 | } 30 | }, 31 | { 32 | "package": "Quick", 33 | "repositoryURL": "https://github.com/Quick/Quick.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "0ff81f2c665b4381f526bd656f8708dd52a9ea2f", 37 | "version": "1.2.0" 38 | } 39 | }, 40 | { 41 | "package": "Result", 42 | "repositoryURL": "https://github.com/antitypical/Result.git", 43 | "state": { 44 | "branch": null, 45 | "revision": "7477584259bfce2560a19e06ad9f71db441fff11", 46 | "version": "3.2.4" 47 | } 48 | }, 49 | { 50 | "package": "SWXMLHash", 51 | "repositoryURL": "https://github.com/drmohundro/SWXMLHash.git", 52 | "state": { 53 | "branch": null, 54 | "revision": "4e7f5af6331735d39fc357b2c71c3bb15af97c8f", 55 | "version": "4.3.6" 56 | } 57 | }, 58 | { 59 | "package": "SourceKit", 60 | "repositoryURL": "https://github.com/norio-nomura/SourceKit.git", 61 | "state": { 62 | "branch": null, 63 | "revision": "18eaa67ca44443bbe39646916792b9f0c98dbaa1", 64 | "version": "1.0.1" 65 | } 66 | }, 67 | { 68 | "package": "SourceKitten", 69 | "repositoryURL": "https://github.com/jpsim/SourceKitten.git", 70 | "state": { 71 | "branch": null, 72 | "revision": "e06eb730499439ae32c5fbb6f72809ebec2371fd", 73 | "version": "0.19.1" 74 | } 75 | }, 76 | { 77 | "package": "SwiftGraph", 78 | "repositoryURL": "https://github.com/davecom/SwiftGraph.git", 79 | "state": { 80 | "branch": null, 81 | "revision": "c369391fe1d86ba3f6771cd0004ad996b9a9e076", 82 | "version": "1.5.1" 83 | } 84 | }, 85 | { 86 | "package": "Yams", 87 | "repositoryURL": "https://github.com/jpsim/Yams.git", 88 | "state": { 89 | "branch": null, 90 | "revision": "95f45caf07472ec78223ebada45255086a85b01a", 91 | "version": "0.5.0" 92 | } 93 | } 94 | ] 95 | }, 96 | "version": 1 97 | } 98 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "grift", 8 | products: [ 9 | .executable(name: "grift", targets: ["grift", "GriftKit"]), 10 | .library(name: "GriftKit", targets: ["GriftKit"]), 11 | ], 12 | dependencies: [ 13 | // Dependencies declare other packages that this package depends on. 14 | .package(url: "https://github.com/davecom/SwiftGraph.git", from: "1.5.1"), 15 | .package(url: "https://github.com/jpsim/SourceKitten.git", from: "0.19.1"), 16 | .package(url: "https://github.com/Carthage/Commandant.git", from: "0.13.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 21 | .target( 22 | name: "grift", 23 | dependencies: ["GriftKit", "Commandant"]), 24 | .target( 25 | name: "GriftKit", 26 | dependencies: ["Graphviz", "SwiftGraph", "SourceKittenFramework"]), 27 | .testTarget( 28 | name: "GriftKitTests", 29 | dependencies: ["GriftKit"]), 30 | .target( 31 | name: "Graphviz", 32 | dependencies: ["SwiftGraph"]), 33 | .testTarget( 34 | name: "GraphvizTests", 35 | dependencies: ["Graphviz", "SwiftGraph"]), 36 | ] 37 | ) 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grift 2 | A swift dependency graph visualizer tool. 3 | 4 | The intent of this is to use sourcekit to get type information for all elements of a set of swift files and create a dotfile that can be rendered into a graph via graphviz or some other tool. 5 | 6 | Grift is in its very early stages. There are many scenarios it does not account for properly, and there's lots of type information that it isn't able to easily detect yet. If you're curious what it does support so far, check out the uncommented unit tests [here](Tests/GriftKitTests/GriftKitTests.swift) for examples. 7 | 8 | To play with what's there, you can simply run `swift run` to run grift in its own directory, or `swift run grift dependencies --path {your-path-here}` to specify your own location. The output will be in the graphviz dot format to standard output, which you can generate an image with using graphviz 9 | 10 | For a rough idea of what this tool may become, check out the [roadmap](roadmap.md). 11 | -------------------------------------------------------------------------------- /Sources/Graphviz/Graphviz.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftGraph 3 | 4 | public struct Graphviz { 5 | public enum DirectionType: String { 6 | case undirected = "graph" 7 | case directed = "digraph" 8 | 9 | public var edgeOperator: String { 10 | switch self { 11 | case .undirected: return "--" 12 | case .directed: return "->" 13 | } 14 | } 15 | } 16 | 17 | var type: DirectionType 18 | var name: String 19 | var statements: [Statement] 20 | 21 | public init(type: DirectionType = .undirected, name: String = "", statements: [Statement] = []) { 22 | self.type = type 23 | self.name = name 24 | self.statements = statements 25 | } 26 | 27 | public func serialize() -> String { 28 | return "\(type.rawValue) \(name) { \(statements.serialize(with: self)) }".removingRepeatedWhitespace() 29 | } 30 | } 31 | 32 | extension Graphviz: CustomStringConvertible { 33 | public var description: String { 34 | return serialize() 35 | } 36 | } 37 | 38 | extension String { 39 | func removingRepeatedWhitespace() -> String { 40 | return self.replacingOccurrences(of: "\\s+", 41 | with: " ", 42 | options: .regularExpression, 43 | range: self.startIndex.. String { 13 | return "\"\(identifier)\"" 14 | } 15 | } 16 | 17 | extension Node: ExpressibleByStringLiteral { 18 | public init(stringLiteral value: String) { 19 | self.init(value) 20 | } 21 | 22 | public init(unicodeScalarLiteral value: UnicodeScalar) { 23 | self.init(String(value)) 24 | } 25 | 26 | public init(extendedGraphemeClusterLiteral value: Character) { 27 | self.init(String(value)) 28 | } 29 | } 30 | 31 | public func >> (lhs: Node, rhs: Node) -> Edge { 32 | return Edge(from: lhs, to: rhs) 33 | } 34 | 35 | public struct Edge { 36 | public var from: Node 37 | public var to: Node 38 | } 39 | 40 | extension Edge: Statement { 41 | public func serialize(with context: Graphviz) -> String { 42 | return "\"\(from.identifier)\" \(context.type.edgeOperator) \"\(to.identifier)\"" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/Graphviz/Statement.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol Statement { 4 | func serialize(with context: Graphviz) -> String 5 | } 6 | 7 | extension Sequence where Iterator.Element == Statement { 8 | public func serialize(with context: Graphviz) -> String { 9 | return self.map({ $0.serialize(with: context) }).joined(separator: "; ") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/Graphviz/Subgraph.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct Subgraph { 4 | public var identifier: String 5 | public var statements: [Statement] 6 | 7 | public init(_ identifier: String = "", isCluster: Bool = false, statements: [Statement] = []) { 8 | self.identifier = isCluster ? "cluster_\(identifier)" : identifier 9 | self.statements = statements 10 | } 11 | } 12 | 13 | extension Subgraph: Statement { 14 | public func serialize(with context: Graphviz) -> String { 15 | return "subgraph \(identifier) { \(statements.serialize(with: context)) }" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Graphviz/SwiftGraph+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftGraph 3 | 4 | extension UnweightedGraph { 5 | public func addEdgeIfNotPresent(from source: T, to destination: T, directed: Bool) { 6 | if !edgeExists(from: source, to: destination) { 7 | addEdge(from: source, to: destination, directed: directed) 8 | } 9 | } 10 | } 11 | 12 | extension Graph { 13 | @discardableResult 14 | public func addVertextIfNotPresent(_ vertex: V) -> Int { 15 | return indexOfVertex(vertex) ?? addVertex(vertex) 16 | } 17 | 18 | public func graphviz(name: String = "") -> Graphviz { 19 | var statements = [Statement]() 20 | 21 | for from in self { 22 | for to in neighborsForVertex(from)! { 23 | let fromNode = String(describing: from) 24 | let toNode = String(describing: to) 25 | statements.append(Node(fromNode) >> Node(toNode)) 26 | } 27 | } 28 | 29 | return Graphviz(type: .directed, name: name, statements: statements) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/GriftKit/GraphBuilder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SourceKittenFramework 3 | import SwiftGraph 4 | import Graphviz 5 | 6 | public typealias Vertex = String 7 | 8 | public enum GraphBuilder { 9 | 10 | public static func build(files: [File]) -> UnweightedGraph { 11 | let graph = UnweightedGraph() 12 | 13 | return graph 14 | } 15 | 16 | public static func build(structures: [Structure]) -> UnweightedGraph { 17 | let graph = UnweightedGraph() 18 | for structure in structures { 19 | populate(graph: graph, from: structure.dictionary) 20 | } 21 | return graph 22 | } 23 | 24 | public static func build(docs: [SwiftDocs]) -> UnweightedGraph { 25 | let graph = UnweightedGraph() 26 | for doc in docs { 27 | populate(graph: graph, from: doc.docsDictionary) 28 | } 29 | return graph 30 | } 31 | 32 | private static func populate(graph: UnweightedGraph, from dict: [String: SourceKitRepresentable], forVertexNamed name: String = "") { 33 | 34 | var name = name 35 | 36 | if let typeName = dict[.typeName] as? String, !name.isEmpty { 37 | for singleTypeName in normalize(typeWithName: typeName) { 38 | graph.addVertextIfNotPresent(singleTypeName) 39 | graph.addEdgeIfNotPresent(from: name, to: singleTypeName, directed: true) 40 | } 41 | } 42 | 43 | if let newName = dict[.name] as? String, kindIsEnclosingType(kind: dict[.kind]) { 44 | name = newName 45 | graph.addVertextIfNotPresent(name) 46 | } 47 | 48 | if let substructures = dict[.substructure] as? [SourceKitRepresentable] { 49 | for case let substructureDict as [String: SourceKitRepresentable] in substructures { 50 | populate(graph: graph, from: substructureDict, forVertexNamed: name) 51 | } 52 | } 53 | } 54 | 55 | private static func normalize(typeWithName name: String) -> [String] { 56 | let dictionarShorthandRegex = try! NSRegularExpression(pattern: "\\[\\s*(.+)\\s*:\\s*(.+)\\s*]", options: []) 57 | 58 | let dictionaryNormalizedTypeName = dictionarShorthandRegex.stringByReplacingMatches(in: name, 59 | options: [], 60 | range: NSRange(location: 0, length: (name as NSString).length), withTemplate: "Dictionary<$1,$2>") 61 | 62 | let arrayShorthandRegex = try! NSRegularExpression(pattern: "\\[(.+)\\]", options: []) 63 | 64 | let normalizedTypeName = arrayShorthandRegex.stringByReplacingMatches(in: dictionaryNormalizedTypeName, 65 | options: [], 66 | range: NSRange(location: 0, length: (dictionaryNormalizedTypeName as NSString).length), 67 | withTemplate: "Array<$1>") 68 | 69 | return splitSingleTypeNames(fromComposedTypeName: normalizedTypeName) 70 | } 71 | 72 | private static func splitSingleTypeNames(fromComposedTypeName name: String) -> [String] { 73 | let separatorCharacters = CharacterSet(charactersIn: "><,") 74 | 75 | return name.unicodeScalars.split(whereSeparator: { 76 | return separatorCharacters.contains($0) 77 | }).map({ String($0).trimmingCharacters(in: CharacterSet.whitespaces) }) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/GriftKit/GraphStructure.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SourceKittenFramework 3 | import Graphviz 4 | 5 | private func filesInDirectory(at path: String, using fileManager: FileManager = .default) throws -> [String] { 6 | 7 | guard let enumerator = fileManager.enumerator(at: URL(fileURLWithPath: path), includingPropertiesForKeys: nil) else { 8 | return [] // TODO: throw error 9 | } 10 | 11 | return enumerator.flatMap({ 12 | guard let url = $0 as? URL, 13 | !url.pathComponents.contains(".build"), 14 | !url.pathComponents.contains("Carthage"), 15 | url.pathExtension == "swift" else { 16 | return nil 17 | } 18 | 19 | return url.relativePath 20 | }) 21 | 22 | // let contents = try fileManager.contentsOfDirectory(atPath: path) 23 | // 24 | // return contents.flatMap({ (filename: String) -> String? in 25 | // guard filename.hasSuffix(".swift") else { 26 | // return nil 27 | // } 28 | // 29 | // return (path as NSString).appendingPathComponent(filename) 30 | // 31 | // }) 32 | } 33 | 34 | public func syntaxMaps(for code: String) throws -> [SyntaxMap] { 35 | return try [SyntaxMap(file: File(contents: code))] 36 | } 37 | 38 | public func docs(for code: String) -> [SwiftDocs] { 39 | return SwiftDocs(file: File(contents: code), arguments: []).map({ [$0] }) ?? [] 40 | } 41 | 42 | public func structures(at path: String, using fileManager: FileManager = .default) throws -> [Structure] { 43 | let filePaths = try filesInDirectory(at: path, using: fileManager) 44 | 45 | return try filePaths.flatMap({ try structure(forFile: $0) }) 46 | } 47 | 48 | func structures(for code: String) throws -> [Structure] { 49 | return try [Structure(file: File(contents: code))] 50 | } 51 | 52 | public func structure(forFile path: String) throws -> Structure? { 53 | guard let file = File(path: path) else { 54 | return nil 55 | } 56 | return try Structure(file: file) 57 | } 58 | 59 | extension Dictionary { 60 | subscript (keyEnum: SwiftDocKey) -> Value? { 61 | guard let key = keyEnum.rawValue as? Key else { 62 | return nil 63 | } 64 | return self[key] 65 | } 66 | } 67 | 68 | func kindIsEnclosingType(kind: SourceKitRepresentable?) -> Bool { 69 | guard let kind = kind as? String, 70 | let declarationKind = SwiftDeclarationKind(rawValue: kind) else { 71 | return false 72 | } 73 | 74 | switch declarationKind { 75 | case .`struct`, .`class`, .`enum`, .`protocol`: 76 | return true 77 | default: 78 | return false 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/GriftKit/Graphviz+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Graphviz 3 | import SourceKittenFramework 4 | 5 | extension Graphviz { 6 | init(structures: [Structure]) { 7 | self.init(type: .directed, statements: structures.flatMap({ Graphviz.createStatements(dict: $0.dictionary) })) 8 | } 9 | 10 | private static func createStatements(dict: [String: SourceKitRepresentable], name: String = "") -> [Statement] { 11 | 12 | var statements: [Statement] = [] 13 | var name = name 14 | 15 | if let typeName = dict[.typeName] as? String, !name.isEmpty { 16 | statements.append(Node(name) >> Node(typeName)) 17 | } 18 | 19 | if let newName = dict[.name] as? String, kindIsEnclosingType(kind: dict[.kind]) { 20 | name = newName 21 | } 22 | 23 | if let substructures = dict[.substructure] as? [SourceKitRepresentable] { 24 | for substructure in substructures { 25 | if let substructureDict = substructure as? [String: SourceKitRepresentable] { 26 | statements.append(contentsOf: createStatements(dict: substructureDict, name: name)) 27 | } 28 | } 29 | } 30 | 31 | return statements 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/GriftKit/Structure+Extensions.swift: -------------------------------------------------------------------------------- 1 | //import SourceKittenFramework 2 | // 3 | //extension Structure { 4 | // var subStructures: [SubStructure]? { 5 | // guard let substructures = dictionary[SwiftDocKey.substructure.rawValue] as? [SourceKitRepresentable] else { 6 | // return nil 7 | // } 8 | // return substructures.flatMap({ 9 | // guard let structure = $0 as? [String: SourceKitRepresentable] else { 10 | // return nil 11 | // } 12 | // return SubStructure(dictionary: structure) 13 | // }) 14 | // } 15 | //} 16 | // 17 | //struct SubStructure { 18 | // enum Kind: String { 19 | // case `class` = "source.lang.swift.decl.class" 20 | // case `struct` = "source.lang.swift.decl.struct" 21 | // case `enum` = "source.lang.swift.decl.enum" 22 | // } 23 | // 24 | // enum Accessibility: String { 25 | // case `public` = "source.lang.swift.accessibility.public" 26 | // case `internal` = "source.lang.swift.accessibility.internal" 27 | // case `private` = "source.lang.swift.accessibility.private" 28 | // } 29 | // 30 | // var kind: Kind 31 | // var accessibility: Accessibility 32 | // var inheritedTypeNames: [String] 33 | // var referencedTypes: [String] = [] 34 | // 35 | // init(dictionary: [String: SourceKitRepresentable]) { 36 | // self.kind = Kind(rawValue: dictionary[SwiftDocKey.kind.rawValue] as! String)! 37 | // self.accessibility = Accessibility(rawValue: dictionary["key.accessibility"] as! String)! 38 | // let types = dictionary[SwiftDocKey.inheritedtypes.rawValue] as! [SourceKitRepresentable] 39 | // self.inheritedTypeNames = types.map({ 40 | // $0 as! [String: SourceKitRepresentable] 41 | // }).map({ 42 | // $0[SwiftDocKey.name.rawValue] as! String 43 | // }) 44 | // } 45 | // 46 | //} 47 | 48 | -------------------------------------------------------------------------------- /Sources/GriftKit/TypeNameExtractor.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SourceKittenFramework 3 | 4 | func extractTypes(from file: File) throws -> [String] { 5 | let map = try SyntaxMap(file: file) 6 | var names: [String] = [] 7 | for token in map.tokens where token.type == SyntaxKind.typeidentifier.rawValue { 8 | let startIndex = file.contents.index(file.contents.startIndex, offsetBy: token.offset) 9 | let endIndex = file.contents.index(startIndex, offsetBy: token.length) 10 | 11 | names.append(String(file.contents[startIndex.. Result<(), GriftError> { 29 | do { 30 | let structures = try GriftKit.structures(at: options.path) 31 | let graph = GraphBuilder.build(structures: structures) 32 | let dot = graph.graphviz() 33 | print(dot.description) 34 | 35 | return .success(()) 36 | } catch { 37 | return .failure(GriftError(message: "\(error)")) 38 | } 39 | } 40 | } 41 | 42 | struct DependenciesOptions: OptionsProtocol { 43 | let path: String 44 | 45 | static func create(_ path: String) -> DependenciesOptions { 46 | return DependenciesOptions(path: path) 47 | } 48 | 49 | static func evaluate(_ m: CommandMode) -> Result> { 50 | return create 51 | <*> m <| Option(key: "path", defaultValue: ".", usage: "The path to generate a dependency graph from") 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/grift/main.swift: -------------------------------------------------------------------------------- 1 | import Commandant 2 | import Foundation 3 | import GriftKit 4 | 5 | let commands = CommandRegistry() 6 | let dependenciesCommand = DependenciesCommand() 7 | commands.register(dependenciesCommand) 8 | 9 | commands.register(HelpCommand(registry: commands)) 10 | 11 | commands.main(defaultVerb: dependenciesCommand.verb) { (error) in 12 | fputs(error.description + "\n", stderr) 13 | } 14 | -------------------------------------------------------------------------------- /Tests/GraphvizTests/GraphvizTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Graphviz 2 | import SwiftGraph 3 | import XCTest 4 | 5 | class GraphvizTests: XCTestCase { 6 | 7 | func testEmptyGraph() { 8 | let graph = Graphviz() 9 | 10 | XCTAssertEqual(graph.description, "graph { }") 11 | } 12 | 13 | func testEmptyDirectedGraph() { 14 | let graph = Graphviz(type: .directed) 15 | 16 | XCTAssertEqual(graph.description, "digraph { }") 17 | } 18 | 19 | func testEmptyGraphWithName() { 20 | let graph = Graphviz(name: "foo") 21 | 22 | XCTAssertEqual(graph.description, "graph foo { }") 23 | } 24 | 25 | func testGraphWithOneNode() { 26 | let graph = Graphviz(statements: [Node("A")]) 27 | 28 | XCTAssertEqual(graph.description, "graph { \"A\" }") 29 | } 30 | 31 | func testGraphWithTwoNodes() { 32 | let graph = Graphviz(statements: [Node("A"), Node("B")]) 33 | 34 | XCTAssertEqual(graph.description, "graph { \"A\"; \"B\" }") 35 | } 36 | 37 | func testUndirectedGraphWithOneEdge() { 38 | let graph = Graphviz(statements: ["A" >> "B"]) 39 | 40 | XCTAssertEqual(graph.description, "graph { \"A\" -- \"B\" }") 41 | } 42 | 43 | func testDirectedGraphWithOneEdge() { 44 | let graph = Graphviz(type: .directed, 45 | statements: ["A" >> "B"]) 46 | 47 | XCTAssertEqual(graph.description, "digraph { \"A\" -> \"B\" }") 48 | } 49 | 50 | func testUndirectedGraphWithTwoEdges() { 51 | let graph = Graphviz(statements: ["A" >> "B", "B" >> "C"]) 52 | 53 | XCTAssertEqual(graph.description, "graph { \"A\" -- \"B\"; \"B\" -- \"C\" }") 54 | } 55 | 56 | func testDirectedGraphWithTwoEdges() { 57 | let graph = Graphviz(type: .directed, 58 | statements: ["A" >> "B", "B" >> "C"]) 59 | 60 | XCTAssertEqual(graph.description, "digraph { \"A\" -> \"B\"; \"B\" -> \"C\" }") 61 | } 62 | 63 | func testUndirectedGraphWithSubgraph() { 64 | let graph = Graphviz(statements: [Subgraph("foo")]) 65 | 66 | XCTAssertEqual(graph.description, "graph { subgraph foo { } }") 67 | } 68 | 69 | func testComplexUndirectedGraphWithSubgraph() { 70 | let graph = Graphviz(statements: ["A" >> "B", 71 | Subgraph(statements: ["C" >> "D"])]) 72 | 73 | XCTAssertEqual(graph.description, "graph { \"A\" -- \"B\"; subgraph { \"C\" -- \"D\" } }") 74 | } 75 | 76 | func testGraphWithClusteredSubgraph() { 77 | let graph = Graphviz(statements: [Subgraph("blah", isCluster: true)]) 78 | 79 | XCTAssertEqual(graph.description, "graph { subgraph cluster_blah { } }") 80 | } 81 | 82 | func testSerializingBasicGraph() { 83 | let graph = UnweightedGraph() 84 | _ = graph.addVertex("A") 85 | _ = graph.addVertex("B") 86 | _ = graph.addVertex("C") 87 | 88 | graph.addEdge(from: "A", to: "B", directed: true) 89 | graph.addEdge(from: "B", to: "C", directed: true) 90 | graph.addEdge(from: "C", to: "A", directed: true) 91 | 92 | let gv = graph.graphviz(name: "Foo") 93 | XCTAssertEqual(gv.description, "digraph Foo { \"A\" -> \"B\"; \"B\" -> \"C\"; \"C\" -> \"A\" }") 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Tests/GriftKitTests/GriftKitTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @testable import GriftKit 3 | import SourceKittenFramework 4 | import SwiftGraph 5 | import XCTest 6 | 7 | class GriftKitTests: XCTestCase { 8 | 9 | // func testFolderGivesStructureArrayOfAllFilesInIt() { 10 | // 11 | // var path = "./example.swift.test" 12 | // let thing = try! structure(forFile: path) 13 | // print(thing) 14 | // 15 | // var args = ["-sdk", 16 | // "/Applications/Xcode-9.2.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk", 17 | // "-module-name", 18 | // "Blah", 19 | // "-c", 20 | // path] 21 | // 22 | // let cursorInfo = try! Request.cursorInfo(file: path, offset: 489, arguments: args).send() 23 | // print(cursorInfo) 24 | // 25 | //// { 26 | //// "key.kind" : "source.lang.swift.expr.call", 27 | //// "key.offset" : 489, 28 | //// "key.nameoffset" : 489, 29 | //// "key.namelength" : 5, 30 | //// "key.bodyoffset" : 495, 31 | //// "key.bodylength" : 0, 32 | //// "key.length" : 7, 33 | //// "key.name" : "thing" 34 | //// } 35 | // args = ["-sdk", 36 | // "/Applications/Xcode-8.0.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk", 37 | // "-module-name", 38 | // "Blah", 39 | // "-c", 40 | // path] 41 | //// args += files 42 | // 43 | // let index = Request.index( 44 | // file: path, 45 | // arguments:args) 46 | // try! print(toJSON(index.send())) 47 | // } 48 | 49 | private func buildGraph(for code: String) throws -> UnweightedGraph { 50 | return try GraphBuilder.build(structures: structures(for: code)) 51 | // return GraphBuilder.build(docs: docs(for: code)) 52 | // return GraphBuilder.build(syntax: syntaxMaps(for: code)) 53 | } 54 | 55 | func testSingleStructWithNoFieldsCreatesSingleVertexGraph() throws { 56 | let code = "struct Thing { }" 57 | 58 | let graph = try buildGraph(for: code) 59 | 60 | XCTAssertEqual(graph.vertexCount, 1) 61 | XCTAssertEqual(graph.edgeCount, 0) 62 | XCTAssertTrue(graph.vertexInGraph(vertex: "Thing")) 63 | } 64 | 65 | func testSingleStructSwiftCodeCreatesOneEdgeGraph() throws { 66 | let code = "struct Thing { var x: String }" 67 | 68 | let graph = try buildGraph(for: code) 69 | 70 | XCTAssertEqual(graph.vertexCount, 2) 71 | XCTAssertEqual(graph.edgeCount, 1) 72 | XCTAssertTrue(graph.edgeExists(from: "Thing", to: "String")) 73 | } 74 | 75 | func testTwoStructSwiftCodeCreatesTwoEdgeGraphGraph() throws { 76 | let code = "struct Thing { var x: String }; struct Foo { var bar: Int }" 77 | 78 | let graph = try buildGraph(for: code) 79 | 80 | XCTAssertEqual(graph.vertexCount, 4) 81 | XCTAssertEqual(graph.edgeCount, 2) 82 | XCTAssertTrue(graph.edgeExists(from: "Thing", to: "String")) 83 | XCTAssertTrue(graph.edgeExists(from: "Foo", to: "Int")) 84 | } 85 | 86 | func testNestedStructSwiftCodeCreatesExpectedGraph() throws { 87 | let code = "struct Thing { struct Foo {} var x: Foo }" 88 | 89 | let graph = try buildGraph(for: code) 90 | 91 | XCTAssertEqual(graph.vertexCount, 2) 92 | XCTAssertEqual(graph.edgeCount, 1) 93 | XCTAssertTrue(graph.edgeExists(from: "Thing", to: "Foo")) 94 | } 95 | 96 | func testMoreComplexNestedStructSwiftCodeCreatesExpectedGraph() throws { 97 | let code = "struct Thing { struct Foo { let s: Int } var x: Foo }" 98 | 99 | let graph = try buildGraph(for: code) 100 | 101 | XCTAssertEqual(graph.vertexCount, 3) 102 | XCTAssertEqual(graph.edgeCount, 2) 103 | XCTAssertTrue(graph.edgeExists(from: "Thing", to: "Foo")) 104 | XCTAssertTrue(graph.edgeExists(from: "Foo", to: "Int")) 105 | } 106 | 107 | func testStructWithFunctionParametersShowsParametersProperly() throws { 108 | let code = "struct Thing { func foo(d: Double) { } }" 109 | 110 | let graph = try buildGraph(for: code) 111 | 112 | XCTAssertEqual(graph.vertexCount, 2) 113 | XCTAssertEqual(graph.edgeCount, 1) 114 | XCTAssertTrue(graph.edgeExists(from: "Thing", to: "Double")) 115 | } 116 | 117 | func testClassWithFunctionParametersShowsParametersProperly() throws { 118 | let code = "class Thing { func foo(d: Double) { } }" 119 | 120 | let graph = try buildGraph(for: code) 121 | 122 | XCTAssertEqual(graph.vertexCount, 2) 123 | XCTAssertEqual(graph.edgeCount, 1) 124 | XCTAssertTrue(graph.edgeExists(from: "Thing", to: "Double")) 125 | } 126 | 127 | func testEnumWithFunctionParametersShowsParametersProperly() throws { 128 | let code = "enum Thing { func foo(d: Double) { } }" 129 | 130 | let graph = try buildGraph(for: code) 131 | 132 | XCTAssertEqual(graph.vertexCount, 2) 133 | XCTAssertEqual(graph.edgeCount, 1) 134 | XCTAssertTrue(graph.edgeExists(from: "Thing", to: "Double")) 135 | } 136 | 137 | func testProtocolWithFunctionParametersShowsParametersProperly() throws { 138 | let code = "protocol Thing { func foo(d: Double) }" 139 | 140 | let graph = try buildGraph(for: code) 141 | 142 | XCTAssertEqual(graph.vertexCount, 2) 143 | XCTAssertEqual(graph.edgeCount, 1) 144 | XCTAssertTrue(graph.edgeExists(from: "Thing", to: "Double")) 145 | } 146 | 147 | func testTwoReferencesToTheSameTypeOnlyYieldOneEdge() throws { 148 | let code = "struct Thing { var x: String; var y: String }" 149 | 150 | let graph = try buildGraph(for: code) 151 | 152 | XCTAssertEqual(graph.vertexCount, 2) 153 | XCTAssertEqual(graph.edgeCount, 1) 154 | XCTAssertTrue(graph.edgeExists(from: "Thing", to: "String")) 155 | } 156 | 157 | func testThatGenericTypesPointToBothTypeAndGenericTypeParameter() throws { 158 | let code = "struct Thing { var x: Array }" 159 | 160 | let graph = try buildGraph(for: code) 161 | 162 | XCTAssertEqual(graph.vertexCount, 3) 163 | XCTAssertEqual(graph.edgeCount, 2) 164 | 165 | XCTAssertTrue(graph.edgeExists(from: "Thing", to: "Array")) 166 | XCTAssertTrue(graph.edgeExists(from: "Thing", to: "String")) 167 | } 168 | 169 | func testArrayTypesAreNormalizedToNotHaveBrackets() throws { 170 | let code = "struct Thing { var x: [String] }" 171 | 172 | let graph = try buildGraph(for: code) 173 | 174 | XCTAssertEqual(graph.vertexCount, 3) 175 | XCTAssertEqual(graph.edgeCount, 2) 176 | 177 | XCTAssertTrue(graph.edgeExists(from: "Thing", to: "Array")) 178 | XCTAssertTrue(graph.edgeExists(from: "Thing", to: "String")) 179 | } 180 | 181 | func testThatGenericTypesWithMultipleGenericParamsPointToEachParameter() throws { 182 | let code = "struct Thing { var x: Dictionary }" 183 | 184 | let graph = try buildGraph(for: code) 185 | 186 | XCTAssertEqual(graph.vertexCount, 4) 187 | XCTAssertEqual(graph.edgeCount, 3) 188 | 189 | XCTAssertTrue(graph.edgeExists(from: "Thing", to: "Dictionary")) 190 | XCTAssertTrue(graph.edgeExists(from: "Thing", to: "String")) 191 | XCTAssertTrue(graph.edgeExists(from: "Thing", to: "Int")) 192 | } 193 | 194 | func testThatDictionaryTypesAreNormalizedToNotHaveBracketsOrColons() throws { 195 | let code = "open class Thing { var x: [String: Int] }" 196 | 197 | let graph = try buildGraph(for: code) 198 | 199 | XCTAssertEqual(graph.vertexCount, 4) 200 | XCTAssertEqual(graph.edgeCount, 3) 201 | 202 | XCTAssertTrue(graph.edgeExists(from: "Thing", to: "Dictionary")) 203 | XCTAssertTrue(graph.edgeExists(from: "Thing", to: "String")) 204 | XCTAssertTrue(graph.edgeExists(from: "Thing", to: "Int")) 205 | } 206 | 207 | // func testStructWithFunctionShowsFunctionReturnTypeProperly() throws { 208 | // let code = "struct Thing { func foo() -> Double { return 0 } }" 209 | // 210 | // let graph = try buildGraph(for: code) 211 | // 212 | // XCTAssertEqual(graph.vertexCount, 2) 213 | // XCTAssertEqual(graph.edgeCount, 1) 214 | // XCTAssertTrue(graph.edgeExists(from: "Thing", to: "Double")) 215 | // } 216 | } 217 | -------------------------------------------------------------------------------- /roadmap.md: -------------------------------------------------------------------------------- 1 | 2 | # Graphviz for type dependencies (or uml?) 3 | - Solid for class or struct, dotted for protocol 4 | - Composition vs inheritance difference in edges? (red for inheritance, black for normal reference) 5 | - Cluster nodes of same module together in subgraphs 6 | - submodules in sub-cluster 7 | - edges within a module defined in cluster, between modules outside of cluster 8 | 9 | # Complexity analysis 10 | - how many types have large numbers of dependencies 11 | - cyclic dependency detection? 12 | - islands based on some root set of Files or types (no incoming dependencies from a root), declare as safe to remove 13 | 14 | # Objc support (nice to have) 15 | - swift generated interface of bridging header, analyze types available there for edges coming into swift types 16 | - search .m/.mm files for -Swift.h imports for edges leaving swift types (references to types in generated header are not safe to delete) 17 | - ? objc files are all roots as static dep analysis is not easy or feasible there with sourcekit 18 | --------------------------------------------------------------------------------