├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── bench_test.go ├── cmd └── depth │ ├── depth.go │ └── depth_test.go ├── depth.go ├── depth_test.go ├── go.mod ├── pkg.go └── pkg_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | 16 | bin/ 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | go: 4 | - 1.9.x 5 | before_install: 6 | - go get github.com/mattn/goveralls 7 | script: 8 | - $HOME/gopath/bin/goveralls -service=travis-ci 9 | #script: go test $(go list ./... | grep -v vendor/) 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Kyle Banks 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION = 1.2.1 2 | 3 | RELEASE_PKG = ./cmd/depth 4 | INSTALL_PKG = $(RELEASE_PKG) 5 | 6 | 7 | # Remote includes require 'mmake' 8 | # github.com/tj/mmake 9 | include github.com/KyleBanks/make/go/install 10 | include github.com/KyleBanks/make/go/sanity 11 | include github.com/KyleBanks/make/go/release 12 | include github.com/KyleBanks/make/go/bench 13 | include github.com/KyleBanks/make/git/precommit 14 | 15 | # Runs a number of depth commands as examples of what's possible. 16 | example: | install 17 | depth github.com/KyleBanks/depth/cmd/depth strings ./ 18 | 19 | depth -internal strings 20 | 21 | depth -json github.com/KyleBanks/depth/cmd/depth 22 | 23 | depth -test github.com/KyleBanks/depth/cmd/depth 24 | 25 | depth -test -internal strings 26 | 27 | depth -test -internal -max 3 strings 28 | 29 | depth . 30 | 31 | depth ./cmd/depth 32 | .PHONY: example 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # depth 2 | 3 | [![GoDoc](https://godoc.org/github.com/KyleBanks/depth?status.svg)](https://godoc.org/github.com/KyleBanks/depth)  4 | [![Build Status](https://travis-ci.org/KyleBanks/depth.svg?branch=master)](https://travis-ci.org/KyleBanks/depth)  5 | [![Go Report Card](https://goreportcard.com/badge/github.com/KyleBanks/depth)](https://goreportcard.com/report/github.com/KyleBanks/depth)  6 | [![Coverage Status](https://coveralls.io/repos/github/KyleBanks/depth/badge.svg?branch=master)](https://coveralls.io/github/KyleBanks/depth?branch=master) 7 | 8 | `depth` is tool to retrieve and visualize Go source code dependency trees. 9 | 10 | ## Install 11 | 12 | Download the appropriate binary for your platform from the [Releases](https://github.com/KyleBanks/depth/releases) page, or: 13 | 14 | ```sh 15 | go get github.com/KyleBanks/depth/cmd/depth 16 | ``` 17 | 18 | ## Usage 19 | 20 | `depth` can be used as a standalone command-line application, or as a package within your own project. 21 | 22 | ### Command-Line 23 | 24 | Simply execute `depth` with one or more package names to visualize. You can use the fully qualified import path of the package, like so: 25 | 26 | ```sh 27 | $ depth github.com/KyleBanks/depth/cmd/depth 28 | github.com/KyleBanks/depth/cmd/depth 29 | ├ encoding/json 30 | ├ flag 31 | ├ fmt 32 | ├ io 33 | ├ log 34 | ├ os 35 | ├ strings 36 | └ github.com/KyleBanks/depth 37 | ├ fmt 38 | ├ go/build 39 | ├ path 40 | ├ sort 41 | └ strings 42 | 12 dependencies (11 internal, 1 external, 0 testing). 43 | ``` 44 | 45 | Or you can use a relative path, for example: 46 | 47 | ```sh 48 | $ depth . 49 | $ depth ./cmd/depth 50 | $ depth ../ 51 | ``` 52 | 53 | You can also use `depth` on the Go standard library: 54 | 55 | ```sh 56 | $ depth strings 57 | strings 58 | ├ errors 59 | ├ internal/bytealg 60 | ├ io 61 | ├ sync 62 | ├ unicode 63 | ├ unicode/utf8 64 | └ unsafe 65 | 7 dependencies (7 internal, 0 external, 0 testing). 66 | ``` 67 | 68 | Visualizing multiple packages at a time is supported by simply naming the packages you'd like to visualize: 69 | 70 | ```sh 71 | $ depth strings github.com/KyleBanks/depth 72 | strings 73 | ├ errors 74 | ├ internal/bytealg 75 | ├ io 76 | ├ sync 77 | ├ unicode 78 | ├ unicode/utf8 79 | └ unsafe 80 | 7 dependencies (7 internal, 0 external, 0 testing). 81 | github.com/KyleBanks/depth 82 | ├ bytes 83 | ├ errors 84 | ├ go/build 85 | ├ os 86 | ├ path 87 | ├ sort 88 | └ strings 89 | 7 dependencies (7 internal, 0 external, 0 testing). 90 | ``` 91 | 92 | #### `-internal` 93 | 94 | By default, `depth` only resolves the top level of dependencies for standard library packages, however you can use the `-internal` flag to visualize all internal dependencies: 95 | 96 | ```sh 97 | $ depth -internal strings 98 | strings 99 | ├ errors 100 | │ └ internal/reflectlite 101 | │ ├ internal/unsafeheader 102 | │ │ └ unsafe 103 | │ ├ runtime 104 | │ │ ├ internal/abi 105 | │ │ │ └ unsafe 106 | │ │ ├ internal/bytealg 107 | │ │ │ ├ internal/cpu 108 | │ │ │ └ unsafe 109 | │ │ ├ internal/cpu 110 | │ │ ├ internal/goexperiment 111 | │ │ ├ runtime/internal/atomic 112 | │ │ │ └ unsafe 113 | │ │ ├ runtime/internal/math 114 | │ │ │ └ runtime/internal/sys 115 | │ │ ├ runtime/internal/sys 116 | │ │ └ unsafe 117 | │ └ unsafe 118 | ├ internal/bytealg 119 | ├ io 120 | │ ├ errors 121 | │ └ sync 122 | │ ├ internal/race 123 | │ │ └ unsafe 124 | │ ├ runtime 125 | │ ├ sync/atomic 126 | │ │ └ unsafe 127 | │ └ unsafe 128 | ├ sync 129 | ├ unicode 130 | ├ unicode/utf8 131 | └ unsafe 132 | 18 dependencies (18 internal, 0 external, 0 testing). 133 | ``` 134 | 135 | #### `-max` 136 | 137 | The `-max` flag limits the dependency tree to the maximum depth provided. For example, if you supply `-max 1` on the `depth` package, your output would look like so: 138 | 139 | ``` 140 | $ depth -max 1 github.com/KyleBanks/depth/cmd/depth 141 | github.com/KyleBanks/depth/cmd/depth 142 | ├ encoding/json 143 | ├ flag 144 | ├ fmt 145 | ├ io 146 | ├ log 147 | ├ os 148 | ├ strings 149 | └ github.com/KyleBanks/depth 150 | 7 dependencies (6 internal, 1 external, 0 testing). 151 | ``` 152 | 153 | The `-max` flag is particularly useful in conjunction with the `-internal` flag which can lead to very deep dependency trees. 154 | 155 | #### `-test` 156 | 157 | By default, `depth` ignores dependencies that are only required for testing. However, you can view test dependencies using the `-test` flag: 158 | 159 | ```sh 160 | $ depth -test strings 161 | strings 162 | ├ bytes 163 | ├ errors 164 | ├ fmt 165 | ├ internal/bytealg 166 | ├ internal/testenv 167 | ├ io 168 | ├ math/rand 169 | ├ reflect 170 | ├ strconv 171 | ├ sync 172 | ├ testing 173 | ├ unicode 174 | ├ unicode/utf8 175 | └ unsafe 176 | 14 dependencies (14 internal, 0 external, 7 testing). 177 | ``` 178 | 179 | #### `-explain target-package` 180 | 181 | The `-explain` flag instructs `depth` to print import chains in which the 182 | `target-package` is found: 183 | 184 | ```sh 185 | $ depth -explain strings github.com/KyleBanks/depth/cmd/depth 186 | github.com/KyleBanks/depth/cmd/depth -> strings 187 | github.com/KyleBanks/depth/cmd/depth -> github.com/KyleBanks/depth -> strings 188 | ``` 189 | 190 | #### `-json` 191 | 192 | The `-json` flag instructs `depth` to output dependencies in JSON format: 193 | 194 | ```sh 195 | $ depth -json github.com/KyleBanks/depth/cmd/depth 196 | { 197 | "name": "github.com/KyleBanks/depth/cmd/depth", 198 | "deps": [ 199 | { 200 | "name": "encoding/json", 201 | "internal": true, 202 | "deps": null 203 | }, 204 | ... 205 | { 206 | "name": "github.com/KyleBanks/depth", 207 | "internal": false, 208 | "deps": [ 209 | { 210 | "name": "go/build", 211 | "internal": true, 212 | "deps": null 213 | }, 214 | ... 215 | ] 216 | } 217 | ] 218 | } 219 | ``` 220 | 221 | ### Integrating With Your Project 222 | 223 | The `depth` package can easily be used to retrieve the dependency tree for a particular package in your own project. For example, here's how you would retrieve the dependency tree for the `strings` package: 224 | 225 | ```go 226 | import "github.com/KyleBanks/depth" 227 | 228 | var t depth.Tree 229 | err := t.Resolve("strings") 230 | if err != nil { 231 | log.Fatal(err) 232 | } 233 | 234 | // Output: "'strings' has 4 dependencies." 235 | log.Printf("'%v' has %v dependencies.", t.Root.Name, len(t.Root.Deps)) 236 | ``` 237 | 238 | For additional customization, simply set the appropriate flags on the `Tree` before resolving: 239 | 240 | ```go 241 | import "github.com/KyleBanks/depth" 242 | 243 | t := depth.Tree { 244 | ResolveInternal: true, 245 | ResolveTest: true, 246 | MaxDepth: 10, 247 | } 248 | 249 | 250 | err := t.Resolve("strings") 251 | ``` 252 | 253 | ## Author 254 | 255 | `depth` was developed by [Kyle Banks](https://twitter.com/kylewbanks). 256 | 257 | ## License 258 | 259 | `depth` is available under the [MIT](./LICENSE) license. 260 | -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | package depth 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func BenchmarkTree_ResolveStrings(b *testing.B) { 8 | benchmarkTreeResolveStrings(&Tree{}, b) 9 | } 10 | 11 | func BenchmarkTree_ResolveStringsInternal(b *testing.B) { 12 | benchmarkTreeResolveStrings(&Tree{ 13 | ResolveInternal: true, 14 | }, b) 15 | } 16 | 17 | func BenchmarkTree_ResolveStringsTest(b *testing.B) { 18 | benchmarkTreeResolveStrings(&Tree{ 19 | ResolveTest: true, 20 | }, b) 21 | } 22 | 23 | func BenchmarkTree_ResolveStringsInternalTest(b *testing.B) { 24 | benchmarkTreeResolveStrings(&Tree{ 25 | ResolveInternal: true, 26 | ResolveTest: true, 27 | }, b) 28 | } 29 | 30 | func benchmarkTreeResolveStrings(t *Tree, b *testing.B) { 31 | for i := 0; i < b.N; i++ { 32 | if err := t.Resolve("strings"); err != nil { 33 | b.Fatal(err) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /cmd/depth/depth.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "os" 9 | "strings" 10 | 11 | "github.com/KyleBanks/depth" 12 | ) 13 | 14 | const ( 15 | outputClosedPadding = " " 16 | outputOpenPadding = "│ " 17 | outputPrefix = "├ " 18 | outputPrefixLast = "└ " 19 | ) 20 | 21 | var outputJSON bool 22 | var explainPkg string 23 | 24 | type summary struct { 25 | numInternal int 26 | numExternal int 27 | numTesting int 28 | } 29 | 30 | func main() { 31 | t, pkgs := parse(os.Args[1:]) 32 | if err := handlePkgs(t, pkgs, outputJSON, explainPkg); err != nil { 33 | os.Exit(1) 34 | } 35 | } 36 | 37 | // parse constructs a depth.Tree from command-line arguments, and returns the 38 | // remaining user-supplied package names 39 | func parse(args []string) (*depth.Tree, []string) { 40 | f := flag.NewFlagSet(os.Args[0], flag.ExitOnError) 41 | 42 | var t depth.Tree 43 | f.BoolVar(&t.ResolveInternal, "internal", false, "If set, resolves dependencies of internal (stdlib) packages.") 44 | f.BoolVar(&t.ResolveTest, "test", false, "If set, resolves dependencies used for testing.") 45 | f.IntVar(&t.MaxDepth, "max", 0, "Sets the maximum depth of dependencies to resolve.") 46 | f.BoolVar(&outputJSON, "json", false, "If set, outputs the depencies in JSON format.") 47 | f.StringVar(&explainPkg, "explain", "", "If set, show which packages import the specified target") 48 | f.Parse(args) 49 | 50 | return &t, f.Args() 51 | } 52 | 53 | // handlePkgs takes a slice of package names, resolves a Tree on them, 54 | // and outputs each Tree to Stdout. 55 | func handlePkgs(t *depth.Tree, pkgs []string, outputJSON bool, explainPkg string) error { 56 | for _, pkg := range pkgs { 57 | 58 | err := t.Resolve(pkg) 59 | if err != nil { 60 | fmt.Printf("'%v': FATAL: %v\n", pkg, err) 61 | return err 62 | } 63 | 64 | if outputJSON { 65 | writePkgJSON(os.Stdout, *t.Root) 66 | continue 67 | } 68 | 69 | if explainPkg != "" { 70 | writeExplain(os.Stdout, *t.Root, []string{}, explainPkg) 71 | continue 72 | } 73 | 74 | writePkg(os.Stdout, *t.Root) 75 | writePkgSummary(os.Stdout, *t.Root) 76 | } 77 | return nil 78 | } 79 | 80 | // writePkgSummary writes a summary of all packages in a tree 81 | func writePkgSummary(w io.Writer, pkg depth.Pkg) { 82 | var sum summary 83 | set := make(map[string]struct{}) 84 | for _, p := range pkg.Deps { 85 | collectSummary(&sum, p, set) 86 | } 87 | fmt.Fprintf(w, "%d dependencies (%d internal, %d external, %d testing).\n", 88 | sum.numInternal+sum.numExternal, 89 | sum.numInternal, 90 | sum.numExternal, 91 | sum.numTesting) 92 | } 93 | 94 | func collectSummary(sum *summary, pkg depth.Pkg, nameSet map[string]struct{}) { 95 | if _, ok := nameSet[pkg.Name]; !ok { 96 | nameSet[pkg.Name] = struct{}{} 97 | if pkg.Internal { 98 | sum.numInternal++ 99 | } else { 100 | sum.numExternal++ 101 | } 102 | if pkg.Test { 103 | sum.numTesting++ 104 | } 105 | for _, p := range pkg.Deps { 106 | collectSummary(sum, p, nameSet) 107 | } 108 | } 109 | } 110 | 111 | // writePkgJSON writes the full Pkg as JSON to the provided Writer. 112 | func writePkgJSON(w io.Writer, p depth.Pkg) { 113 | e := json.NewEncoder(w) 114 | e.SetIndent("", " ") 115 | e.Encode(p) 116 | } 117 | 118 | func writePkg(w io.Writer, p depth.Pkg) { 119 | fmt.Fprintf(w, "%s\n", p.String()) 120 | 121 | for idx, d := range p.Deps { 122 | writePkgRec(w, d, []bool{true}, idx == len(p.Deps)-1) 123 | } 124 | } 125 | 126 | // writePkg recursively prints a Pkg and its dependencies to the Writer provided. 127 | func writePkgRec(w io.Writer, p depth.Pkg, closed []bool, isLast bool) { 128 | var prefix string 129 | 130 | for _, c := range closed { 131 | if c { 132 | prefix += outputClosedPadding 133 | continue 134 | } 135 | 136 | prefix += outputOpenPadding 137 | } 138 | 139 | closed = append(closed, false) 140 | if isLast { 141 | prefix += outputPrefixLast 142 | closed[len(closed)-1] = true 143 | } else { 144 | prefix += outputPrefix 145 | } 146 | 147 | fmt.Fprintf(w, "%v%v\n", prefix, p.String()) 148 | 149 | for idx, d := range p.Deps { 150 | writePkgRec(w, d, closed, idx == len(p.Deps)-1) 151 | } 152 | } 153 | 154 | // writeExplain shows possible paths for a given package. 155 | func writeExplain(w io.Writer, pkg depth.Pkg, stack []string, explain string) { 156 | stack = append(stack, pkg.Name) 157 | if pkg.Name == explain { 158 | fmt.Fprintln(w, strings.Join(stack, " -> ")) 159 | } 160 | for _, p := range pkg.Deps { 161 | writeExplain(w, p, stack, explain) 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /cmd/depth/depth_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/KyleBanks/depth" 8 | ) 9 | 10 | func Test_parse(t *testing.T) { 11 | tests := []struct { 12 | internal bool 13 | test bool 14 | depth int 15 | json bool 16 | explain string 17 | }{ 18 | {true, true, 0, true, ""}, 19 | {false, false, 10, false, ""}, 20 | {true, false, 10, false, ""}, 21 | {false, true, 5, true, ""}, 22 | {false, true, 5, true, "github.com/KyleBanks/depth"}, 23 | {false, true, 5, true, ""}, 24 | } 25 | 26 | for idx, tt := range tests { 27 | tr, _ := parse([]string{ 28 | fmt.Sprintf("-internal=%v", tt.internal), 29 | fmt.Sprintf("-test=%v", tt.test), 30 | fmt.Sprintf("-max=%v", tt.depth), 31 | fmt.Sprintf("-json=%v", tt.json), 32 | fmt.Sprintf("-explain=%v", tt.explain), 33 | }) 34 | 35 | if tr.ResolveInternal != tt.internal { 36 | t.Fatalf("[%v] Unexpected ResolveInternal, expected=%v, got=%v", idx, tt.internal, tr.ResolveInternal) 37 | } else if tr.ResolveTest != tt.test { 38 | t.Fatalf("[%v] Unexpected ResolveTest, expected=%v, got=%v", idx, tt.test, tr.ResolveTest) 39 | } else if tr.MaxDepth != tt.depth { 40 | t.Fatalf("[%v] Unexpected MaxDepth, expected=%v, got=%v", idx, tt.depth, tr.MaxDepth) 41 | } else if outputJSON != tt.json { 42 | t.Fatalf("[%v] Unexpected outputJSON, expected=%v, got=%v", idx, tt.json, outputJSON) 43 | } else if explainPkg != tt.explain { 44 | t.Fatalf("[%v] Unexpected explainPkg, expected=%v, got=%v", idx, tt.explain, explainPkg) 45 | } 46 | } 47 | } 48 | 49 | func Example_handlePkgsStrings() { 50 | var t depth.Tree 51 | 52 | handlePkgs(&t, []string{"strings"}, false, "") 53 | // Output: 54 | // strings 55 | // ├ errors 56 | // ├ internal/bytealg 57 | // ├ io 58 | // ├ sync 59 | // ├ unicode 60 | // ├ unicode/utf8 61 | // └ unsafe 62 | // 7 dependencies (7 internal, 0 external, 0 testing). 63 | } 64 | 65 | func Example_handlePkgsTestStrings() { 66 | var t depth.Tree 67 | t.ResolveTest = true 68 | 69 | handlePkgs(&t, []string{"strings"}, false, "") 70 | // Output: 71 | // strings 72 | // ├ bytes 73 | // ├ errors 74 | // ├ fmt 75 | // ├ internal/bytealg 76 | // ├ internal/testenv 77 | // ├ io 78 | // ├ math/rand 79 | // ├ reflect 80 | // ├ strconv 81 | // ├ sync 82 | // ├ testing 83 | // ├ unicode 84 | // ├ unicode/utf8 85 | // └ unsafe 86 | // 14 dependencies (14 internal, 0 external, 7 testing). 87 | } 88 | 89 | func Example_handlePkgsDepth() { 90 | var t depth.Tree 91 | 92 | handlePkgs(&t, []string{"github.com/KyleBanks/depth/cmd/depth"}, false, "") 93 | // Output: 94 | // github.com/KyleBanks/depth/cmd/depth 95 | // ├ encoding/json 96 | // ├ flag 97 | // ├ fmt 98 | // ├ io 99 | // ├ os 100 | // ├ strings 101 | // └ github.com/KyleBanks/depth 102 | // ├ bytes 103 | // ├ errors 104 | // ├ go/build 105 | // ├ os 106 | // ├ path 107 | // ├ sort 108 | // └ strings 109 | // 12 dependencies (11 internal, 1 external, 0 testing). 110 | } 111 | 112 | func Example_handlePkgsUnknown() { 113 | var t depth.Tree 114 | 115 | handlePkgs(&t, []string{"notreal"}, false, "") 116 | // Output: 117 | // 'notreal': FATAL: unable to resolve root package 118 | } 119 | 120 | func Example_handlePkgsJson() { 121 | var t depth.Tree 122 | handlePkgs(&t, []string{"strings"}, true, "") 123 | 124 | // Output: 125 | // { 126 | // "name": "strings", 127 | // "internal": true, 128 | // "resolved": true, 129 | // "deps": [ 130 | // { 131 | // "name": "errors", 132 | // "internal": true, 133 | // "resolved": true, 134 | // "deps": null 135 | // }, 136 | // { 137 | // "name": "internal/bytealg", 138 | // "internal": true, 139 | // "resolved": true, 140 | // "deps": null 141 | // }, 142 | // { 143 | // "name": "io", 144 | // "internal": true, 145 | // "resolved": true, 146 | // "deps": null 147 | // }, 148 | // { 149 | // "name": "sync", 150 | // "internal": true, 151 | // "resolved": true, 152 | // "deps": null 153 | // }, 154 | // { 155 | // "name": "unicode", 156 | // "internal": true, 157 | // "resolved": true, 158 | // "deps": null 159 | // }, 160 | // { 161 | // "name": "unicode/utf8", 162 | // "internal": true, 163 | // "resolved": true, 164 | // "deps": null 165 | // }, 166 | // { 167 | // "name": "unsafe", 168 | // "internal": true, 169 | // "resolved": true, 170 | // "deps": null 171 | // } 172 | // ] 173 | // } 174 | 175 | } 176 | 177 | func Example_handlePkgsExplain() { 178 | var t depth.Tree 179 | 180 | handlePkgs(&t, []string{"github.com/KyleBanks/depth/cmd/depth"}, false, "strings") 181 | // Output: 182 | // github.com/KyleBanks/depth/cmd/depth -> strings 183 | // github.com/KyleBanks/depth/cmd/depth -> github.com/KyleBanks/depth -> strings 184 | } 185 | -------------------------------------------------------------------------------- /depth.go: -------------------------------------------------------------------------------- 1 | // Package depth provides the ability to traverse and retrieve Go source code dependencies in the form of 2 | // internal and external packages. 3 | // 4 | // For example, the dependencies of the stdlib `strings` package can be resolved like so: 5 | // 6 | // import "github.com/KyleBanks/depth" 7 | // 8 | // var t depth.Tree 9 | // err := t.Resolve("strings") 10 | // if err != nil { 11 | // log.Fatal(err) 12 | // } 13 | // 14 | // // Output: "strings has 4 dependencies." 15 | // log.Printf("%v has %v dependencies.", t.Root.Name, len(t.Root.Deps)) 16 | // 17 | // For additional customization, simply set the appropriate flags on the `Tree` before resolving: 18 | // 19 | // import "github.com/KyleBanks/depth" 20 | // 21 | // t := depth.Tree { 22 | // ResolveInternal: true, 23 | // ResolveTest: true, 24 | // MaxDepth: 10, 25 | // } 26 | // err := t.Resolve("strings") 27 | package depth 28 | 29 | import ( 30 | "errors" 31 | "go/build" 32 | "os" 33 | ) 34 | 35 | // ErrRootPkgNotResolved is returned when the root Pkg of the Tree cannot be resolved, 36 | // typically because it does not exist. 37 | var ErrRootPkgNotResolved = errors.New("unable to resolve root package") 38 | 39 | // Importer defines a type that can import a package and return its details. 40 | type Importer interface { 41 | Import(name, srcDir string, im build.ImportMode) (*build.Package, error) 42 | } 43 | 44 | // Tree represents the top level of a Pkg and the configuration used to 45 | // initialize and represent its contents. 46 | type Tree struct { 47 | Root *Pkg 48 | 49 | ResolveInternal bool 50 | ResolveTest bool 51 | MaxDepth int 52 | 53 | Importer Importer 54 | 55 | importCache map[string]struct{} 56 | } 57 | 58 | // Resolve recursively finds all dependencies for the root Pkg name provided, 59 | // and the packages it depends on. 60 | func (t *Tree) Resolve(name string) error { 61 | pwd, err := os.Getwd() 62 | if err != nil { 63 | return err 64 | } 65 | 66 | t.Root = &Pkg{ 67 | Name: name, 68 | Tree: t, 69 | SrcDir: pwd, 70 | Test: false, 71 | } 72 | 73 | // Reset the import cache each time to ensure a reused Tree doesn't 74 | // reuse the same cache. 75 | t.importCache = nil 76 | 77 | // Allow custom importers, but use build.Default if none is provided. 78 | if t.Importer == nil { 79 | t.Importer = &build.Default 80 | } 81 | 82 | t.Root.Resolve(t.Importer) 83 | if !t.Root.Resolved { 84 | return ErrRootPkgNotResolved 85 | } 86 | 87 | return nil 88 | } 89 | 90 | // shouldResolveInternal determines if internal packages should be further resolved beyond the 91 | // current parent. 92 | // 93 | // For example, if the parent Pkg is `github.com/foo/bar` and true is returned, all the 94 | // internal dependencies it relies on will be resolved. If for example `strings` is one of those 95 | // dependencies, and it is passed as the parent here, false may be returned and its internal 96 | // dependencies will not be resolved. 97 | func (t *Tree) shouldResolveInternal(parent *Pkg) bool { 98 | if t.ResolveInternal { 99 | return true 100 | } 101 | 102 | return parent == t.Root 103 | } 104 | 105 | // isAtMaxDepth returns true when the depth of the Pkg provided is at or beyond the maximum 106 | // depth allowed by the tree. 107 | // 108 | // If the Tree has a MaxDepth of zero, true is never returned. 109 | func (t *Tree) isAtMaxDepth(p *Pkg) bool { 110 | if t.MaxDepth == 0 { 111 | return false 112 | } 113 | 114 | return p.depth() >= t.MaxDepth 115 | } 116 | 117 | // hasSeenImport returns true if the import name provided has already been seen within the tree. 118 | // This function only returns false for a name once. 119 | func (t *Tree) hasSeenImport(name string) bool { 120 | if t.importCache == nil { 121 | t.importCache = make(map[string]struct{}) 122 | } 123 | 124 | if _, ok := t.importCache[name]; ok { 125 | return true 126 | } 127 | t.importCache[name] = struct{}{} 128 | return false 129 | } 130 | -------------------------------------------------------------------------------- /depth_test.go: -------------------------------------------------------------------------------- 1 | package depth 2 | 3 | import ( 4 | "go/build" 5 | "testing" 6 | ) 7 | 8 | type MockImporter struct { 9 | ImportFn func(name, srcDir string, im build.ImportMode) (*build.Package, error) 10 | } 11 | 12 | func (m MockImporter) Import(name, srcDir string, im build.ImportMode) (*build.Package, error) { 13 | return m.ImportFn(name, srcDir, im) 14 | } 15 | 16 | func TestTree_Resolve(t *testing.T) { 17 | // Fail case, bad package name 18 | var tr Tree 19 | if err := tr.Resolve("name"); err != ErrRootPkgNotResolved { 20 | t.Fatalf("Unexpected error, expected=%v, got=%b", ErrRootPkgNotResolved, err) 21 | } 22 | 23 | // Positive case, expect deps 24 | if err := tr.Resolve("strings"); err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | if tr.Root == nil || tr.Root.Name != "strings" { 29 | t.Fatalf("Unexpected Root, expected=%v, got=%v", "strings", tr.Root) 30 | } else if len(tr.Root.Deps) == 0 { 31 | t.Fatal("Expected positive number of Deps") 32 | } else if len(tr.Root.SrcDir) == 0 { 33 | t.Fatal("Expected SrcDir to be populated") 34 | } else if tr.Root.Raw == nil { 35 | t.Fatal("Expected non-nil Raw") 36 | } 37 | 38 | // Reuse the same tree and the same package to ensure that the internal pkg cache 39 | // is reset and dependencies are still resolved. 40 | stringsDepCount := len(tr.Root.Deps) 41 | if err := tr.Resolve("strings"); err != nil { 42 | t.Fatal(err) 43 | } 44 | if len(tr.Root.Deps) != stringsDepCount { 45 | t.Fatalf("Unexpected number of Deps, expected=%v, got=%b", stringsDepCount, len(tr.Root.Deps)) 46 | } 47 | } 48 | 49 | func TestTree_shouldResolveInternal(t *testing.T) { 50 | var pt Tree 51 | pt.Root = &Pkg{} 52 | 53 | if pt.shouldResolveInternal(&Pkg{}) { 54 | t.Fatal("Unexpected shouldResolveInternal, should have been false for non-root pkg and default config") 55 | } 56 | 57 | pt.ResolveInternal = true 58 | if !pt.shouldResolveInternal(&Pkg{}) { 59 | t.Fatal("Unexpected shouldResolveInternal, should have been true when ResolveInternal = true") 60 | } 61 | pt.ResolveInternal = false 62 | 63 | if !pt.shouldResolveInternal(pt.Root) { 64 | t.Fatal("Unexpected shouldResolveInternal, should have been true for root pkg") 65 | } 66 | } 67 | 68 | func TestTree_isAtMaxDepth(t *testing.T) { 69 | tests := []struct { 70 | maxDepth int 71 | depth int 72 | expected bool 73 | }{ 74 | {0, 0, false}, 75 | {0, 10, false}, 76 | {1, 0, false}, 77 | {1, 1, true}, 78 | {1, 10, true}, 79 | } 80 | 81 | for idx, tt := range tests { 82 | tr := Tree{MaxDepth: tt.maxDepth} 83 | 84 | var last *Pkg 85 | for i := 0; i < tt.depth+1; i++ { 86 | p := Pkg{Parent: last} 87 | last = &p 88 | } 89 | 90 | maxDepth := tr.isAtMaxDepth(last) 91 | if maxDepth != tt.expected { 92 | t.Fatalf("[%v] Unexpected isAtMaxDepth, expected=%v, got=%v", idx, tt.expected, maxDepth) 93 | } 94 | } 95 | } 96 | 97 | func TestTree_hasSeenImport(t *testing.T) { 98 | var tr Tree 99 | 100 | if tr.hasSeenImport("name") { 101 | t.Fatalf("Expected false the first time an import name is provided, got=true") 102 | } 103 | 104 | if !tr.hasSeenImport("name") { 105 | t.Fatalf("Expected true to be returned after the import name has been seen, got=false") 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/KyleBanks/depth 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /pkg.go: -------------------------------------------------------------------------------- 1 | package depth 2 | 3 | import ( 4 | "bytes" 5 | "go/build" 6 | "path" 7 | "sort" 8 | "strings" 9 | ) 10 | 11 | // Pkg represents a Go source package, and its dependencies. 12 | type Pkg struct { 13 | Name string `json:"name"` 14 | SrcDir string `json:"-"` 15 | 16 | Internal bool `json:"internal"` 17 | Resolved bool `json:"resolved"` 18 | Test bool `json:"-"` 19 | 20 | Tree *Tree `json:"-"` 21 | Parent *Pkg `json:"-"` 22 | Deps []Pkg `json:"deps"` 23 | 24 | Raw *build.Package `json:"-"` 25 | } 26 | 27 | // Resolve recursively finds all dependencies for the Pkg and the packages it depends on. 28 | func (p *Pkg) Resolve(i Importer) { 29 | // Resolved is always true, regardless of if we skip the import, 30 | // it is only false if there is an error while importing. 31 | p.Resolved = true 32 | 33 | name := p.cleanName() 34 | if name == "" { 35 | return 36 | } 37 | 38 | // Stop resolving imports if we've reached max depth or found a duplicate. 39 | var importMode build.ImportMode 40 | if p.Tree.hasSeenImport(name) || p.Tree.isAtMaxDepth(p) { 41 | importMode = build.FindOnly 42 | } 43 | 44 | pkg, err := i.Import(name, p.SrcDir, importMode) 45 | if err != nil { 46 | // TODO: Check the error type? 47 | p.Resolved = false 48 | return 49 | } 50 | p.Raw = pkg 51 | 52 | // Update the name with the fully qualified import path. 53 | p.Name = pkg.ImportPath 54 | 55 | // If this is an internal dependency, we may need to skip it. 56 | if pkg.Goroot { 57 | p.Internal = true 58 | if !p.Tree.shouldResolveInternal(p) { 59 | return 60 | } 61 | } 62 | 63 | //first we set the regular dependencies, then we add the test dependencies 64 | //sharing the same set. This allows us to mark all test-only deps linearly 65 | unique := make(map[string]struct{}) 66 | p.setDeps(i, pkg.Imports, pkg.Dir, unique, false) 67 | if p.Tree.ResolveTest { 68 | p.setDeps(i, append(pkg.TestImports, pkg.XTestImports...), pkg.Dir, unique, true) 69 | } 70 | } 71 | 72 | // setDeps takes a slice of import paths and the source directory they are relative to, 73 | // and creates the Deps of the Pkg. Each dependency is also further resolved prior to being added 74 | // to the Pkg. 75 | func (p *Pkg) setDeps(i Importer, imports []string, srcDir string, unique map[string]struct{}, isTest bool) { 76 | for _, imp := range imports { 77 | // Mostly for testing files where cyclic imports are allowed. 78 | if imp == p.Name { 79 | continue 80 | } 81 | 82 | // Skip duplicates. 83 | if _, ok := unique[imp]; ok { 84 | continue 85 | } 86 | unique[imp] = struct{}{} 87 | 88 | p.addDep(i, imp, srcDir, isTest) 89 | } 90 | 91 | sort.Sort(byInternalAndName(p.Deps)) 92 | } 93 | 94 | // addDep creates a Pkg and it's dependencies from an imported package name. 95 | func (p *Pkg) addDep(i Importer, name string, srcDir string, isTest bool) { 96 | dep := Pkg{ 97 | Name: name, 98 | SrcDir: srcDir, 99 | Tree: p.Tree, 100 | Parent: p, 101 | Test: isTest, 102 | } 103 | dep.Resolve(i) 104 | 105 | p.Deps = append(p.Deps, dep) 106 | } 107 | 108 | // isParent goes recursively up the chain of Pkgs to determine if the name provided is ever a 109 | // parent of the current Pkg. 110 | func (p *Pkg) isParent(name string) bool { 111 | if p.Parent == nil { 112 | return false 113 | } 114 | 115 | if p.Parent.Name == name { 116 | return true 117 | } 118 | 119 | return p.Parent.isParent(name) 120 | } 121 | 122 | // depth returns the depth of the Pkg within the Tree. 123 | func (p *Pkg) depth() int { 124 | if p.Parent == nil { 125 | return 0 126 | } 127 | 128 | return p.Parent.depth() + 1 129 | } 130 | 131 | // cleanName returns a cleaned version of the Pkg name used for resolving dependencies. 132 | // 133 | // If an empty string is returned, dependencies should not be resolved. 134 | func (p *Pkg) cleanName() string { 135 | name := p.Name 136 | 137 | // C 'package' cannot be resolved. 138 | if name == "C" { 139 | return "" 140 | } 141 | 142 | // Internal golang_org/* packages must be prefixed with vendor/ 143 | // 144 | // Thanks to @davecheney for this: 145 | // https://github.com/davecheney/graphpkg/blob/master/main.go#L46 146 | if strings.HasPrefix(name, "golang_org") { 147 | name = path.Join("vendor", name) 148 | } 149 | 150 | return name 151 | } 152 | 153 | // String returns a string representation of the Pkg containing the Pkg name and status. 154 | func (p *Pkg) String() string { 155 | b := bytes.NewBufferString(p.Name) 156 | 157 | if !p.Resolved { 158 | b.Write([]byte(" (unresolved)")) 159 | } 160 | 161 | return b.String() 162 | } 163 | 164 | // byInternalAndName ensures a slice of Pkgs are sorted such that the internal stdlib 165 | // packages are always above external packages (ie. github.com/whatever). 166 | type byInternalAndName []Pkg 167 | 168 | func (b byInternalAndName) Len() int { 169 | return len(b) 170 | } 171 | 172 | func (b byInternalAndName) Swap(i, j int) { 173 | b[i], b[j] = b[j], b[i] 174 | } 175 | 176 | func (b byInternalAndName) Less(i, j int) bool { 177 | if b[i].Internal && !b[j].Internal { 178 | return true 179 | } else if !b[i].Internal && b[j].Internal { 180 | return false 181 | } 182 | 183 | return b[i].Name < b[j].Name 184 | } 185 | -------------------------------------------------------------------------------- /pkg_test.go: -------------------------------------------------------------------------------- 1 | package depth 2 | 3 | import ( 4 | "go/build" 5 | "sort" 6 | "testing" 7 | ) 8 | 9 | func TestPkg_CleanName(t *testing.T) { 10 | tests := []struct { 11 | input string 12 | expected string 13 | }{ 14 | {"strings", "strings"}, 15 | {"net/http", "net/http"}, 16 | {"github.com/KyleBanks/depth", "github.com/KyleBanks/depth"}, 17 | {"C", ""}, 18 | {"golang_org/x/anything", "vendor/golang_org/x/anything"}, 19 | } 20 | 21 | for _, tt := range tests { 22 | p := Pkg{Name: tt.input} 23 | 24 | out := p.cleanName() 25 | if out != tt.expected { 26 | t.Fatalf("Unexpected cleanName, expected=%v, got=%v", tt.expected, out) 27 | } 28 | } 29 | } 30 | 31 | func TestPkg_AddDepImportSeen(t *testing.T) { 32 | var m MockImporter 33 | var tr Tree 34 | tr.Importer = m 35 | 36 | testName := "test" 37 | testSrcDir := "src/testing" 38 | var expectedIm build.ImportMode 39 | 40 | p := Pkg{Tree: &tr} 41 | m.ImportFn = func(name, srcDir string, im build.ImportMode) (*build.Package, error) { 42 | if name != testName { 43 | t.Fatalf("Unexpected name provided, expected=%v, got=%v", testName, name) 44 | } 45 | if srcDir != testSrcDir { 46 | t.Fatalf("Unexpected srcDir provided, expected=%v, got=%v", testSrcDir, srcDir) 47 | } 48 | if im != expectedIm { 49 | t.Fatalf("Unexpected ImportMode provided, expected=%v, got=%v", expectedIm, im) 50 | } 51 | 52 | return &build.Package{}, nil 53 | } 54 | 55 | // Hasn't seen the import 56 | p.addDep(m, testName, testSrcDir, false) 57 | 58 | // Has seen the import 59 | expectedIm = build.FindOnly 60 | p.addDep(m, testName, testSrcDir, false) 61 | } 62 | 63 | func TestByInternalAndName(t *testing.T) { 64 | pkgs := []Pkg{ 65 | Pkg{Internal: true, Name: "net/http"}, 66 | Pkg{Internal: false, Name: "github.com/KyleBanks/depth"}, 67 | Pkg{Internal: true, Name: "strings"}, 68 | Pkg{Internal: false, Name: "github.com/KyleBanks/commuter"}, 69 | } 70 | expected := []string{"net/http", "strings", "github.com/KyleBanks/commuter", "github.com/KyleBanks/depth"} 71 | 72 | sort.Sort(byInternalAndName(pkgs)) 73 | 74 | for i, e := range expected { 75 | if pkgs[i].Name != e { 76 | t.Fatalf("Unexpected Pkg at index %v, expected=%v, got=%v", i, e, pkgs[i].Name) 77 | } 78 | } 79 | } 80 | --------------------------------------------------------------------------------