├── .gitignore ├── .goreleaser.yml ├── .travis.yml ├── LICENSE ├── README.md ├── demo └── depscheck.png ├── deps_test.go ├── main.go ├── package.go ├── package_test.go ├── pkgstats.go ├── result.go ├── selector.go ├── test ├── bar │ └── bar.go ├── const.go ├── exported.go ├── exported2.go ├── external.go ├── foo │ └── foo.go ├── interface.go ├── pkg_dot.go ├── pkg_renamed.go ├── recursion.go ├── sample │ └── sample.go └── var.go └── walker.go /.gitignore: -------------------------------------------------------------------------------- 1 | oldtest/ 2 | .*.swp 3 | depscheck 4 | depscheck.iml 5 | .idea 6 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | --- 2 | project_name: depscheck 3 | 4 | release: 5 | github: 6 | owner: divan 7 | name: depscheck 8 | 9 | builds: 10 | - binary: depscheck 11 | goos: 12 | - darwin 13 | - windows 14 | - linux 15 | goarch: 16 | - amd64 17 | - 386 18 | env: 19 | - CGO_ENABLED=0 20 | main: . 21 | 22 | archive: 23 | format: tar.gz 24 | wrap_in_directory: true 25 | format_overrides: 26 | - goos: windows 27 | format: zip 28 | name_template: '{{ .Binary }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' 29 | files: 30 | - LICENSE 31 | - README.md 32 | 33 | snapshot: 34 | name_template: SNAPSHOT-{{ .Commit }} 35 | 36 | checksum: 37 | name_template: '{{ .ProjectName }}-{{ .Version }}-checksums.txt' 38 | 39 | changelog: 40 | sort: asc 41 | filters: 42 | exclude: 43 | - '^docs:' 44 | - '^test:' 45 | - '^dev:' 46 | - 'README' 47 | - Merge pull request 48 | - Merge branch 49 | 50 | git: 51 | short_hash: true -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - tip 5 | - "1.11.x" 6 | - "1.10.x" 7 | - "1.9.x" 8 | - "1.8.x" 9 | - "1.7.x" 10 | - "1.6.x" 11 | deploy: 12 | - provider: script 13 | skip_cleanup: true 14 | script: curl -sL https://git.io/goreleaser | bash 15 | on: 16 | tags: true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ivan Daniluk 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DepsCheck 2 | 3 | [![Build Status](https://img.shields.io/travis/divan/depscheck.svg)](https://travis-ci.org/divan/depscheck) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/divan/depscheck)](https://goreportcard.com/report/github.com/divan/depscheck) 5 | [![Downloads](https://img.shields.io/github/downloads/divan/depscheck/latest/total.svg)](https://github.com/divan/depscheck/releases) 6 | [![Latest release](https://img.shields.io/github/release/divan/depscheck.svg)](https://github.com/divan/depscheck/releases) 7 | 8 | Dependency checker for Golang (Go) packages. Prints stats and suggests to remove small LeftPad-like imports if any. 9 | 10 | ## Introduction 11 | 12 | DepsCheck analyzes source code of your package and all its imports and attempts to find good candidates to be removed as a dependency. It only suggests to pay attention to those dependencies, nothing more. 13 | It also can shows detailed statistics for imported packages usage, including external functions, methods, variables and types used in your project. For functions and methods it calculates LOC (Lines Of Code), Cumulative LOC (sum of nested functions), number of calls, nesting depth and so on. 14 | 15 | DepsCheck demo 16 | 17 | This tool was inspired by famous [LeftPad incident](http://blog.npmjs.org/post/141577284765/kik-left-pad-and-npm) in NPM/Javascript community. Although Go community do not tend to create packages for every single function over there, the goal is to let programs guide us and help people to learn better practices. 18 | 19 | Also some inspiration came from one of the [Go Proverbs](http://go-proverbs.github.io): 20 | 21 | > A little copying is better than a little dependency. 22 | 23 | If you struggle to understand how it applies with DRY and why it's wise point, I suggest you to check out [this video](https://www.youtube.com/watch?v=PAAkCSZUG1c) on a subject. 24 | 25 | ## Installation 26 | 27 | Just run go get: 28 | 29 | ```bash 30 | go get github.com/divan/depscheck 31 | ``` 32 | 33 | To update: 34 | 35 | ```bash 36 | go get -u github.com/divan/depscheck 37 | ``` 38 | 39 | ## Usage 40 | 41 | The usage is straightforward - just pass the package path (*github.com/user/package*) you want to check. Path can be also the dot (.) - in this case depscheck will check package in current directory. Also, you may pass one or many *.go files: 42 | 43 | depscheck . 44 | depscheck github.com/divan/expvarmon 45 | depscheck main.go 46 | depscheck /tmp/test.go /tmp/test.go 47 | 48 | In default mode, *depscheck* only prints totals stats and suggestions for small dependencies. 49 | The `-v` flag will print more verbose info with detailed statistics: 50 | 51 | depscheck -v . 52 | depscheck -v github.com/Typeform/goblitline 53 | 54 | By default, only external packages are checked. Use `-internal` flag in case you want to see statistics on internal and vendored packages too. 55 | 56 | depscheck -v -internal golang.org/x/tools/go/loader 57 | 58 | With `-stdlib` flag, *depscheck* also can analyze stdlib packages and treat them as an external dependencies. Suggestion mode is disabled with stdlib flag (stdlib is smarter than this tool), so you will probably will want `-v` flag to see how your package uses stdlib. 59 | 60 | depscheck -stdlib -v net/http 61 | depscheck -stdlib -v github.com/divan/gofresh 62 | 63 | Sometimes you want only totals statistics - how many packages, calls and LOC in total used by your package. Use `-totalonly` flag to get single-line easily parseable output with totals. You can even run *depscheck* agains every stdlib package in a loop: 64 | 65 | depscheck -totalonly -stdlib encoding/json 66 | for i in $(go list std); do depscheck -stdlib -totalonly $i; done 67 | 68 | Don't forget `-help` flag for detailed usage information. 69 | 70 | ## Sample Output 71 | 72 | ```bash 73 | $ depscheck -v github.com/divan/expvarmon 74 | github.com/divan/expvarmon: 4 packages, 1022 LOC, 93 calls, 11 depth, 23 depth int. 75 | +--------+---------+---------------------+-----------+-------+-----+--------+-------+----------+ 76 | | PKG | RECV | NAME | TYPE | COUNT | LOC | LOCCUM | DEPTH | DEPTHINT | 77 | +--------+---------+---------------------+-----------+-------+-----+--------+-------+----------+ 78 | | byten | | Size | func | 1 | 14 | 19 | 0 | 2 | 79 | | jason | *Object | GetInt64 | method | 1 | 15 | 98 | 0 | 6 | 80 | | | *Object | GetStringArray | method | 1 | 28 | 128 | 0 | 6 | 81 | | | *Object | GetValue | method | 1 | 2 | 62 | 0 | 4 | 82 | | | *Value | Array | method | 1 | 25 | 25 | 0 | 0 | 83 | | | *Value | Boolean | method | 1 | 15 | 15 | 0 | 0 | 84 | | | *Value | Float64 | method | 2 | 8 | 23 | 0 | 1 | 85 | | | *Value | Int64 | method | 2 | 8 | 23 | 0 | 1 | 86 | | | *Value | String | method | 1 | 15 | 15 | 0 | 0 | 87 | | | | NewObjectFromReader | func | 1 | 2 | 46 | 0 | 2 | 88 | | | | Object | type | 2 | | | | | 89 | | | | Value | type | 2 | | | | | 90 | | ranges | | Parse | func | 1 | 29 | 29 | 0 | 0 | 91 | | termui | | AttrBold | const | 6 | | | | | 92 | | | | ColorBlue | const | 1 | | | | | 93 | | | | ColorCyan | const | 4 | | | | | 94 | | | | ColorGreen | const | 7 | | | | | 95 | | | | ColorRed | const | 1 | | | | | 96 | | | | ColorWhite | const | 4 | | | | | 97 | | | | ColorYellow | const | 1 | | | | | 98 | | | | EventKey | const | 1 | | | | | 99 | | | | EventResize | const | 1 | | | | | 100 | | | | Close | func | 2 | 2 | 30 | 1 | 0 | 101 | | | | EventCh | func | 1 | 4 | 4 | 0 | 0 | 102 | | | | Init | func | 2 | 11 | 109 | 1 | 0 | 103 | | | | NewList | func | 2 | 6 | 6 | 0 | 0 | 104 | | | | NewPar | func | 5 | 6 | 6 | 0 | 0 | 105 | | | | NewSparkline | func | 2 | 5 | 5 | 0 | 0 | 106 | | | | NewSparklines | func | 2 | 3 | 3 | 0 | 0 | 107 | | | | Render | func | 2 | 9 | 129 | 5 | 1 | 108 | | | | TermHeight | func | 2 | 4 | 120 | 2 | 0 | 109 | | | | TermWidth | func | 2 | 4 | 120 | 2 | 0 | 110 | | | | UseTheme | func | 2 | 7 | 7 | 0 | 0 | 111 | | | | Bufferer | interface | 2 | | | | | 112 | | | | Attribute | type | 1 | | | | | 113 | | | | List | type | 4 | | | | | 114 | | | | Par | type | 10 | | | | | 115 | | | | Sparkline | type | 4 | | | | | 116 | | | | Sparklines | type | 5 | | | | | 117 | +--------+---------+---------------------+-----------+-------+-----+--------+-------+----------+ 118 | +--------+---------------------------------+-------+-------+--------+-------+----------+ 119 | | PKG | PATH | COUNT | CALLS | LOCCUM | DEPTH | DEPTHINT | 120 | +--------+---------------------------------+-------+-------+--------+-------+----------+ 121 | | byten | github.com/pyk/byten | 1 | 1 | 19 | 0 | 2 | 122 | | jason | github.com/antonholmquist/jason | 11 | 15 | 435 | 0 | 20 | 123 | | ranges | github.com/bsiegert/ranges | 1 | 1 | 29 | 0 | 0 | 124 | | termui | gopkg.in/gizak/termui.v1 | 26 | 76 | 539 | 11 | 1 | 125 | +--------+---------------------------------+-------+-------+--------+-------+----------+ 126 | - Package byten (github.com/pyk/byten) is a good candidate for removing from dependencies. 127 | Only 19 LOC used, in 1 calls, with 2 level of nesting 128 | - Package ranges (github.com/bsiegert/ranges) is a good candidate for removing from dependencies. 129 | Only 29 LOC used, in 1 calls, with 0 level of nesting 130 | ``` 131 | 132 | You can see that depscheck suggested to take a look into two packages - `byten` and `ranges`. It makes sense and I'm going to follow its advice. Those packages are really small and only one small function is used from both of them. 133 | 134 | ## Notes 135 | 136 | - Suggestions made by this tool are totally optional and could be totally false alarms. The language used is "package X is a good candidate to be remove" to bring your attention to inspect this package and decide. 137 | - Terms 'Depth' and 'DepthInternal' in statistics mean a number of external/internal dependencies (functions/methods/vars). Function with one level of external nested calls that contain 3 of them will have Depth equal 3. If 'depth' sounds strange, I'd be glad to hear suggestions on better naming. Also, actual func depth is easy to calculate. 138 | - This tool is beta and may report incorrect info and contain bugs. Don't rely a lot on its results without double checking. 139 | - There are many situations where it's really hard to even define what is "correct" - for example Cumulative Lines Of Code for code that has recursive dependencies. Also, external function with 1 line may use global variable or channel that is used by 99% other package's funcs. It's hard to predict all possible cases. 140 | - If you're encountered a situation where tools is reporting incorrectly or panics - feel free to open an issue or (better) create Pull Request. 141 | - This tool require Go 1.6+ 142 | 143 | ## License 144 | 145 | MIT License 146 | -------------------------------------------------------------------------------- /demo/depscheck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/divan/depscheck/d54c5bee1b1181c8ab963144bd9b15199d8be8a9/demo/depscheck.png -------------------------------------------------------------------------------- /deps_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "golang.org/x/tools/go/loader" 5 | "testing" 6 | ) 7 | 8 | func TestExportedFuncs(t *testing.T) { 9 | var result *Result 10 | var src string 11 | 12 | src = "test/exported.go" 13 | result = getResult(t, false, "test", src) 14 | checkCount(src, t, result, 2) 15 | checkSelector(src, t, result, "xsample.var.Sample", 1, 0, 0, 0, 0) 16 | checkSelector(src, t, result, "xsample.func.SampleFunc", 1, 6, 14, 0, 2) 17 | 18 | src = "test/exported2.go" 19 | result = getResult(t, false, "test", src) 20 | checkCount(src, t, result, 3) 21 | checkSelector(src, t, result, "xsample.func.SampleFunc", 1, 6, 14, 0, 2) 22 | checkSelector(src, t, result, "xsample.(Foo).method.Bar", 1, 3, 3, 0, 0) 23 | checkSelector(src, t, result, "xsample.type.Foo", 1, 0, 0, 0, 0) 24 | 25 | src = "test/pkg_renamed.go" 26 | result = getResult(t, false, "test", src) 27 | checkCount(src, t, result, 3) 28 | checkSelector(src, t, result, "xsample.func.SampleFunc", 1, 6, 14, 0, 2) 29 | checkSelector(src, t, result, "xsample.(Foo).method.Bar", 1, 3, 3, 0, 0) 30 | checkSelector(src, t, result, "xsample.type.Foo", 1, 0, 0, 0, 0) 31 | 32 | src = "test/pkg_dot.go" 33 | result = getResult(t, false, "test", src) 34 | checkCount(src, t, result, 3) 35 | checkSelector(src, t, result, "xsample.func.SampleFunc", 1, 6, 14, 0, 2) 36 | checkSelector(src, t, result, "xsample.(Foo).method.Bar", 1, 3, 3, 0, 0) 37 | checkSelector(src, t, result, "xsample.type.Foo", 1, 0, 0, 0, 0) 38 | } 39 | 40 | func TestRecursion(t *testing.T) { 41 | var result *Result 42 | var src string 43 | 44 | src = "test/recursion.go" 45 | result = getResult(t, false, "test", src) 46 | checkCount(src, t, result, 2) 47 | checkSelector(src, t, result, "bar.func.Bar", 1, 4, 4, 0, 0) 48 | checkSelector(src, t, result, "foo.func.Foo", 1, 4, 8, 1, 0) 49 | } 50 | 51 | func TestConsts(t *testing.T) { 52 | var result *Result 53 | var src string 54 | 55 | src = "test/const.go" 56 | result = getResult(t, false, "test", src) 57 | checkCount(src, t, result, 1) 58 | checkSelector(src, t, result, "foo.const.FooConst", 1, 0, 0, 0, 0) 59 | } 60 | 61 | func TestVars(t *testing.T) { 62 | var result *Result 63 | var src string 64 | 65 | src = "test/var.go" 66 | result = getResult(t, false, "test", src) 67 | checkCount(src, t, result, 1) 68 | checkSelector(src, t, result, "foo.var.FooVar", 1, 0, 0, 0, 0) 69 | } 70 | 71 | func TestInterface(t *testing.T) { 72 | var result *Result 73 | var src string 74 | 75 | src = "test/interface.go" 76 | result = getResult(t, false, "test", src) 77 | checkCount(src, t, result, 2) 78 | checkSelector(src, t, result, "foo.(Fooer).method.Foo", 1, 0, 0, 0, 0) 79 | checkSelector(src, t, result, "foo.interface.Fooer", 1, 0, 0, 0, 0) 80 | } 81 | 82 | func TestInternal(t *testing.T) { 83 | var result *Result 84 | var src string 85 | 86 | src = "test/recursion.go" 87 | result = getResult(t, false, "github.com/divan/depscheck/test", src) 88 | checkCount(src, t, result, 0) 89 | result = getResult(t, true, "github.com/divan/depscheck/test", src) 90 | checkCount(src, t, result, 2) 91 | 92 | src = "test/pkg_renamed.go" 93 | result = getResult(t, false, "github.com/divan/depscheck/test", src) 94 | checkCount(src, t, result, 0) 95 | result = getResult(t, true, "github.com/divan/depscheck/test", src) 96 | checkCount(src, t, result, 3) 97 | 98 | src = "test/external.go" 99 | result = getResult(t, false, "github.com/divan/depscheck/test", src) 100 | checkCount(src, t, result, 1) 101 | result = getResult(t, true, "github.com/divan/depscheck/test", src) 102 | checkCount(src, t, result, 2) 103 | } 104 | 105 | func getResult(t *testing.T, isInternal bool, name string, sources ...string) *Result { 106 | var conf loader.Config 107 | conf.CreateFromFilenames(name, sources...) 108 | p, err := conf.Load() 109 | if err != nil { 110 | t.Fatal(err) 111 | } 112 | 113 | w := NewWalker(p, false, isInternal) 114 | return w.TopWalk() 115 | } 116 | 117 | func checkCount(src string, t *testing.T, r *Result, want int) { 118 | if have := len(r.Counter); have != want { 119 | t.Fatalf("%s: expected to have %d selectors, but have %d", src, want, have) 120 | } 121 | } 122 | 123 | func checkSelector(src string, t *testing.T, r *Result, fn string, count, loc, loccum, depth, depthint int) { 124 | sel, ok := r.Selectors[fn] 125 | if !ok { 126 | t.Fatalf("%s: expected to see func '%s' in result, but could not", src, fn) 127 | } 128 | if r.Counter[fn] != count { 129 | t.Fatalf("%s: expected to func '%s' to have Count %d , but got %d", src, fn, count, r.Counter[fn]) 130 | } 131 | if sel.LOC != loc { 132 | t.Fatalf("%s: expected to func '%s' to have %d LOC, but got %d", src, fn, loc, sel.LOC) 133 | } 134 | if sel.LOCCum() != loccum { 135 | t.Fatalf("%s: expected to func '%s' to have %d Cumulative LOC, but got %d", src, fn, loccum, sel.LOCCum()) 136 | } 137 | if sel.Depth() != depth { 138 | t.Fatalf("%s: expected to func '%s' to have Depth %d, but got %d", src, fn, depth, sel.Depth()) 139 | } 140 | if sel.DepthInternal() != depthint { 141 | t.Fatalf("%s: expected to func '%s' to have %d Depth Internal, but got %d", src, fn, depthint, sel.DepthInternal()) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | "golang.org/x/tools/go/loader" 9 | ) 10 | 11 | var ( 12 | stdlib = flag.Bool("stdlib", false, "Treat stdlib packages as external dependencies") 13 | tests = flag.Bool("tests", false, "Include tests for deps analysis") 14 | verbose = flag.Bool("v", false, "Be verbose and print whole deps info table") 15 | totals = flag.Bool("totalonly", false, "Print only totals stats") 16 | internal = flag.Bool("internal", false, "Include intertanl packages analysis") 17 | ) 18 | 19 | func main() { 20 | flag.Usage = Usage 21 | flag.Parse() 22 | 23 | var conf loader.Config 24 | 25 | conf.FromArgs(flag.Args(), *tests) 26 | p, err := conf.Load() 27 | if err != nil { 28 | fmt.Println(err) 29 | return 30 | } 31 | 32 | w := NewWalker(p, *stdlib, *internal) 33 | 34 | result := w.TopWalk() 35 | 36 | // Output results 37 | topPackage := p.InitialPackages()[0].Pkg.Path() 38 | fmt.Println(result.Totals(topPackage)) 39 | if *totals { 40 | return 41 | } 42 | if len(result.Counter) == 0 { 43 | fmt.Println("No external dependencies found in this package") 44 | return 45 | } 46 | if *verbose { 47 | result.PrintStats() 48 | result.PrintPackagesStats() 49 | } 50 | 51 | // Do not report suggestions in stdlib mode. 52 | // Stlib is smarter than this tool. 53 | if !*stdlib { 54 | result.Suggestions() 55 | } 56 | 57 | if !*verbose { 58 | fmt.Println("Run with -v option to see detailed stats for dependencies.") 59 | } 60 | } 61 | 62 | // Usage prints usage information for this program. 63 | func Usage() { 64 | fmt.Fprintf(os.Stderr, "Usage: %s [options] \n\n", os.Args[0]) 65 | flag.PrintDefaults() 66 | fmt.Fprintf(os.Stderr, "\n%s\n", loader.FromArgsUsage) 67 | } 68 | -------------------------------------------------------------------------------- /package.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "runtime" 7 | "strings" 8 | ) 9 | 10 | // Package represents package info, needed for this tool. 11 | type Package struct { 12 | Name string 13 | Path string 14 | } 15 | 16 | // NewPackage creates new Package. 17 | func NewPackage(name, path string) Package { 18 | return Package{ 19 | Name: name, 20 | Path: path, 21 | } 22 | } 23 | 24 | func init() { 25 | // Try to load list of std packages from goroot 26 | getStdPkgs() 27 | } 28 | 29 | // IsInternal returns true if subpkg is a subpackage of 30 | // pkg. 31 | func IsInternal(pkg, subpkg string) bool { 32 | // Skip if any is stdlib 33 | if IsStdlib(pkg) || IsStdlib(subpkg) { 34 | return false 35 | } 36 | 37 | // Or it is submodule 38 | if strings.HasPrefix(subpkg, pkg+"/") { 39 | return true 40 | } 41 | 42 | // Or it is on same repo nesting level (nesting > 2) 43 | // FIXME: this code assumes layout "server/user/repo", 44 | // for non-standard layouts ("gopkg.in/music.v0") it'll 45 | // report false negative. 46 | if i := strings.Count(pkg, "/"); i > 2 { 47 | if strings.HasPrefix(subpkg, pkg[0:i]) { 48 | return true 49 | } 50 | } 51 | 52 | return false 53 | } 54 | 55 | // IsStdlib attempts to check if package belongs to stdlib. 56 | func IsStdlib(path string) bool { 57 | for _, p := range stdPkgs { 58 | if p == path { 59 | return true 60 | } 61 | } 62 | return false 63 | } 64 | 65 | // getStdPkgs tries to get list of stdlib packages by reading GOROOT 66 | // 67 | // This approach is used by "go list std" tool 68 | // Based on go/cmd function matchPackages (https://golang.org/src/cmd/go/main.go#L553) 69 | // and listStdPkgs function from https://golang.org/src/go/build/deps_test.go#L420 70 | // 71 | // List of stdlib packages sets to stdPkgsDefault if something went wrong 72 | func getStdPkgs() { 73 | goroot := runtime.GOROOT() 74 | 75 | src := filepath.Join(goroot, "src") + string(filepath.Separator) 76 | walkFn := func(path string, fi os.FileInfo, err error) error { 77 | if err != nil || !fi.IsDir() || path == src { 78 | return nil 79 | } 80 | 81 | base := filepath.Base(path) 82 | if strings.HasPrefix(base, ".") || strings.HasPrefix(base, "_") || base == "testdata" { 83 | return filepath.SkipDir 84 | } 85 | 86 | name := filepath.ToSlash(path[len(src):]) 87 | if name == "builtin" || name == "cmd" || strings.Contains(name, ".") { 88 | return filepath.SkipDir 89 | } 90 | 91 | stdPkgs = append(stdPkgs, name) 92 | return nil 93 | } 94 | if err := filepath.Walk(src, walkFn); err != nil { 95 | stdPkgs = stdPkgsDefault 96 | } 97 | } 98 | 99 | var stdPkgs []string 100 | 101 | var stdPkgsDefault = []string{ 102 | "archive/tar", 103 | "archive/zip", 104 | "bufio", 105 | "bytes", 106 | "compress/bzip2", 107 | "compress/flate", 108 | "compress/gzip", 109 | "compress/lzw", 110 | "compress/zlib", 111 | "container/heap", 112 | "container/list", 113 | "container/ring", 114 | "crypto", 115 | "crypto/aes", 116 | "crypto/cipher", 117 | "crypto/des", 118 | "crypto/dsa", 119 | "crypto/ecdsa", 120 | "crypto/elliptic", 121 | "crypto/hmac", 122 | "crypto/md5", 123 | "crypto/rand", 124 | "crypto/rc4", 125 | "crypto/rsa", 126 | "crypto/sha1", 127 | "crypto/sha256", 128 | "crypto/sha512", 129 | "crypto/subtle", 130 | "crypto/tls", 131 | "crypto/x509", 132 | "crypto/x509/pkix", 133 | "database/sql", 134 | "database/sql/driver", 135 | "debug/dwarf", 136 | "debug/elf", 137 | "debug/gosym", 138 | "debug/macho", 139 | "debug/pe", 140 | "debug/plan9obj", 141 | "encoding", 142 | "encoding/ascii85", 143 | "encoding/asn1", 144 | "encoding/base32", 145 | "encoding/base64", 146 | "encoding/binary", 147 | "encoding/csv", 148 | "encoding/gob", 149 | "encoding/hex", 150 | "encoding/json", 151 | "encoding/pem", 152 | "encoding/xml", 153 | "errors", 154 | "expvar", 155 | "flag", 156 | "fmt", 157 | "go/ast", 158 | "go/build", 159 | "go/constant", 160 | "go/doc", 161 | "go/format", 162 | "go/importer", 163 | "go/internal/gccgoimporter", 164 | "go/internal/gcimporter", 165 | "go/parser", 166 | "go/printer", 167 | "go/scanner", 168 | "go/token", 169 | "go/types", 170 | "hash", 171 | "hash/adler32", 172 | "hash/crc32", 173 | "hash/crc64", 174 | "hash/fnv", 175 | "html", 176 | "html/template", 177 | "image", 178 | "image/color", 179 | "image/color/palette", 180 | "image/draw", 181 | "image/gif", 182 | "image/internal/imageutil", 183 | "image/jpeg", 184 | "image/png", 185 | "index/suffixarray", 186 | "internal/golang.org/x/net/http2/hpack", 187 | "internal/race", 188 | "internal/singleflight", 189 | "internal/testenv", 190 | "internal/trace", 191 | "io", 192 | "io/ioutil", 193 | "log", 194 | "log/syslog", 195 | "math", 196 | "math/big", 197 | "math/cmplx", 198 | "math/rand", 199 | "mime", 200 | "mime/multipart", 201 | "mime/quotedprintable", 202 | "net", 203 | "net/http", 204 | "net/http/cgi", 205 | "net/http/cookiejar", 206 | "net/http/fcgi", 207 | "net/http/httptest", 208 | "net/http/httputil", 209 | "net/http/internal", 210 | "net/http/pprof", 211 | "net/internal/socktest", 212 | "net/mail", 213 | "net/rpc", 214 | "net/rpc/jsonrpc", 215 | "net/smtp", 216 | "net/textproto", 217 | "net/url", 218 | "os", 219 | "os/exec", 220 | "os/signal", 221 | "os/user", 222 | "path", 223 | "path/filepath", 224 | "reflect", 225 | "regexp", 226 | "regexp/syntax", 227 | "runtime", 228 | "runtime/cgo", 229 | "runtime/debug", 230 | "runtime/internal/atomic", 231 | "runtime/internal/sys", 232 | "runtime/pprof", 233 | "runtime/race", 234 | "runtime/trace", 235 | "sort", 236 | "strconv", 237 | "strings", 238 | "sync", 239 | "sync/atomic", 240 | "syscall", 241 | "testing", 242 | "testing/iotest", 243 | "testing/quick", 244 | "text/scanner", 245 | "text/tabwriter", 246 | "text/template", 247 | "text/template/parse", 248 | "time", 249 | "unicode", 250 | "unicode/utf16", 251 | "unicode/utf8", 252 | "unsafe", 253 | } 254 | -------------------------------------------------------------------------------- /package_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestPackageChecks(t *testing.T) { 8 | var pkg, subpkg string 9 | 10 | checkResult := func(pkg, subpkg string, want bool) { 11 | got := IsInternal(pkg, subpkg) 12 | if got != want { 13 | t.Fatalf("Expecting IsInternal to return %v in this case: (%s, %s)", want, pkg, subpkg) 14 | } 15 | } 16 | 17 | pkg, subpkg = "github.com/divan/depscheck", "github.com/divan/depscheck/foo" 18 | checkResult(pkg, subpkg, true) 19 | pkg, subpkg = "github.com/divan/depscheck/bar", "github.com/divan/depscheck/foo" 20 | checkResult(pkg, subpkg, true) 21 | pkg, subpkg = "github.com/divan/package1", "github.com/divan/package2" 22 | checkResult(pkg, subpkg, false) 23 | } 24 | 25 | func BenchmarkIsStdlibTrue(b *testing.B) { 26 | for i := 0; i < b.N; i++ { 27 | IsStdlib("fmt") 28 | } 29 | } 30 | func BenchmarkIsStdlibFalse(b *testing.B) { 31 | for i := 0; i < b.N; i++ { 32 | IsStdlib("github.com/divan/package") 33 | } 34 | } 35 | 36 | func BenchmarkIsInternal(b *testing.B) { 37 | for i := 0; i < b.N; i++ { 38 | IsInternal("github.com/divan/package1", "github.com/divan/package2") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pkgstats.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | ) 7 | 8 | // PackageStat holds stats about dependencies in a given package. 9 | type PackageStat struct { 10 | *Package 11 | 12 | DepsCount int 13 | DepsCallsCount int 14 | 15 | LOCCum int 16 | Depth, DepthInternal int 17 | } 18 | 19 | // NewPackageStat creates new PackageStat. 20 | func NewPackageStat(pkg Package) *PackageStat { 21 | return &PackageStat{ 22 | Package: &pkg, 23 | } 24 | } 25 | 26 | // String implements Stringer for PackageStat. 27 | func (p *PackageStat) String() string { 28 | return fmt.Sprintf("%s: (%d, %d) [LOC: %d] Depth [%d, %d]\n", p.Path, p.DepsCount, p.DepsCallsCount, p.LOCCum, p.Depth, p.DepthInternal) 29 | } 30 | 31 | // PackagesStats returns stats by packages in all selectors. 32 | func (r *Result) PackagesStats() []*PackageStat { 33 | pkgs := make(map[Package]*PackageStat) 34 | for _, sel := range r.All() { 35 | if _, ok := pkgs[sel.Pkg]; !ok { 36 | pkgs[sel.Pkg] = NewPackageStat(sel.Pkg) 37 | } 38 | pkgs[sel.Pkg].DepsCount++ 39 | pkgs[sel.Pkg].DepsCallsCount += r.Counter[sel.ID()] 40 | pkgs[sel.Pkg].LOCCum += sel.LOCCum() 41 | pkgs[sel.Pkg].Depth += sel.Depth() 42 | pkgs[sel.Pkg].DepthInternal += sel.DepthInternal() 43 | 44 | } 45 | 46 | var ret []*PackageStat 47 | for _, stat := range pkgs { 48 | ret = append(ret, stat) 49 | } 50 | sort.Sort(ByPackageName(ret)) 51 | return ret 52 | } 53 | 54 | // CanBeAvoided attempts to classify if package usage is small enough 55 | // to suggest user to avoid this package as a dependency and 56 | // instead copy/embed it's code into own project (if license permits). 57 | func (p *PackageStat) CanBeAvoided() bool { 58 | // If this dependency is using another dependencies, 59 | // it's almost for sure - no. For internal dependency, let's 60 | // allow just two level of nesting. 61 | if p.Depth > 0 { 62 | return false 63 | } 64 | if p.DepthInternal > 2 { 65 | return false 66 | } 67 | 68 | if p.DepsCount > 3 { 69 | return false 70 | } 71 | 72 | // Because 42 73 | if p.LOCCum > 42 { 74 | return false 75 | } 76 | 77 | return true 78 | } 79 | 80 | // ByPackageName is a helper type for sorting PackageStats by Name. 81 | type ByPackageName []*PackageStat 82 | 83 | func (b ByPackageName) Len() int { return len(b) } 84 | func (b ByPackageName) Swap(i, j int) { b[i], b[j] = b[j], b[i] } 85 | func (b ByPackageName) Less(i, j int) bool { 86 | return b[i].Name < b[j].Name 87 | } 88 | -------------------------------------------------------------------------------- /result.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/olekukonko/tablewriter" 6 | "os" 7 | "sort" 8 | ) 9 | 10 | // Result holds final result of this tool. 11 | type Result struct { 12 | Selectors map[string]*Selector 13 | Counter map[string]int 14 | } 15 | 16 | // NewResult inits new Result. 17 | func NewResult() *Result { 18 | return &Result{ 19 | Selectors: make(map[string]*Selector), 20 | Counter: make(map[string]int), 21 | } 22 | } 23 | 24 | // Add adds new selector to the result. 25 | func (r *Result) Add(sel *Selector) { 26 | key := sel.ID() 27 | if _, ok := r.Selectors[key]; !ok { 28 | r.Selectors[key] = sel 29 | } 30 | r.Counter[key]++ 31 | } 32 | 33 | // PrintStats prints results to stdout in a pretty table form. 34 | func (r *Result) PrintStats() { 35 | if len(r.Counter) == 0 { 36 | return 37 | } 38 | selectors := r.All() 39 | sort.Sort(ByID(selectors)) 40 | 41 | table := tablewriter.NewWriter(os.Stdout) 42 | table.SetHeader([]string{"Pkg", "Recv", "Name", "Type", "Count", "LOC", "LOCCum", "Depth", "DepthInt"}) 43 | 44 | var results [][]string 45 | var lastPkg string 46 | for _, sel := range selectors { 47 | pkg := "" 48 | if lastPkg != sel.Pkg.Name { 49 | lastPkg = sel.Pkg.Name 50 | pkg = sel.Pkg.Name 51 | } 52 | var loc, locCum, depth, depthInt string 53 | if sel.Type == "func" || sel.Type == "method" { 54 | loc = fmt.Sprintf("%d", sel.LOC) 55 | locCum = fmt.Sprintf("%d", sel.LOCCum()) 56 | depth = fmt.Sprintf("%d", sel.Depth()) 57 | depthInt = fmt.Sprintf("%d", sel.DepthInternal()) 58 | } 59 | count := fmt.Sprintf("%d", r.Counter[sel.ID()]) 60 | results = append(results, []string{pkg, sel.Recv, sel.Name, sel.Type, count, loc, locCum, depth, depthInt}) 61 | } 62 | for _, v := range results { 63 | table.Append(v) 64 | } 65 | table.Render() // Send output 66 | } 67 | 68 | // PrintPackagesStats prints package stats to stdout in a pretty table form. 69 | func (r *Result) PrintPackagesStats() { 70 | stats := r.PackagesStats() 71 | if len(stats) == 0 { 72 | return 73 | } 74 | 75 | table := tablewriter.NewWriter(os.Stdout) 76 | table.SetHeader([]string{"Pkg", "Path", "Count", "Calls", "LOCCum", "Depth", "DepthInt"}) 77 | 78 | var results [][]string 79 | for _, stat := range stats { 80 | count := fmt.Sprintf("%d", stat.DepsCount) 81 | callsCount := fmt.Sprintf("%d", stat.DepsCallsCount) 82 | loc := fmt.Sprintf("%d", stat.LOCCum) 83 | depth := fmt.Sprintf("%d", stat.Depth) 84 | depthInt := fmt.Sprintf("%d", stat.DepthInternal) 85 | results = append(results, []string{stat.Name, stat.Path, count, callsCount, loc, depth, depthInt}) 86 | } 87 | for _, v := range results { 88 | table.Append(v) 89 | } 90 | table.Render() // Send output 91 | } 92 | 93 | // All returns all known selectors in result. 94 | func (r *Result) All() []*Selector { 95 | var ret []*Selector 96 | for _, sel := range r.Selectors { 97 | ret = append(ret, sel) 98 | } 99 | return ret 100 | } 101 | 102 | // PrintDeps recursively print deps for all selectors found. 103 | func (r *Result) PrintDeps() { 104 | for _, s := range r.All() { 105 | s.PrintDeps() 106 | } 107 | } 108 | 109 | // Suggestions analyzes results and print suggestions on deps. 110 | // 111 | // It attempts to suggest which dependencies could be 112 | // copied to your source because of its small size. 113 | func (r *Result) Suggestions() { 114 | if len(r.Counter) == 0 { 115 | return 116 | } 117 | 118 | var hasCandidates bool 119 | for _, p := range r.PackagesStats() { 120 | if p.CanBeAvoided() { 121 | fmt.Printf(" - Package %s (%s) is a good candidate for removing from dependencies.\n", p.Name, p.Path) 122 | fmt.Printf(" Only %d LOC used, in %d calls, with %d level of nesting\n", p.LOCCum, p.DepsCount, p.DepthInternal) 123 | hasCandidates = true 124 | } 125 | } 126 | 127 | if !hasCandidates { 128 | fmt.Println("Cool, looks like your dependencies are sane.") 129 | } 130 | } 131 | 132 | // Totals represnts total stats for all packages. 133 | type Totals struct { 134 | Package string 135 | 136 | Packages int 137 | LOC int 138 | Calls int 139 | Depth int 140 | DepthInternal int 141 | } 142 | 143 | // Totals computes Totals for Result. 144 | func (r *Result) Totals(pkg string) *Totals { 145 | t := &Totals{ 146 | Package: pkg, 147 | } 148 | for _, stat := range r.PackagesStats() { 149 | t.Packages++ 150 | t.LOC += stat.LOCCum 151 | t.Calls += stat.DepsCallsCount 152 | t.Depth += stat.Depth 153 | t.DepthInternal += stat.DepthInternal 154 | } 155 | return t 156 | } 157 | 158 | // String implements Stringer for Totals type. 159 | func (t Totals) String() string { 160 | return fmt.Sprintf("%s: %d packages, %d LOC, %d calls, %d depth, %d depth int.", 161 | t.Package, t.Packages, t.LOC, t.Calls, t.Depth, t.DepthInternal) 162 | } 163 | -------------------------------------------------------------------------------- /selector.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "go/types" 6 | "strings" 7 | ) 8 | 9 | // Selector represents Go language selector (x.f), 10 | // which may be: 11 | // - method of variable of external package 12 | // - function from the external package 13 | // - variable/const from ext. package 14 | type Selector struct { 15 | Pkg Package 16 | Name string 17 | Type string 18 | Recv string 19 | 20 | // Applies for functions 21 | LOC int // actual Lines Of Code 22 | 23 | Deps Deps 24 | } 25 | 26 | // String implements Stringer interface for Selector. 27 | func (s *Selector) String() string { 28 | var out string 29 | if s.Recv != "" { 30 | out = fmt.Sprintf("%s.(%s).%s.%s", s.Pkg.Name, s.Recv, s.Type, s.Name) 31 | } 32 | out = fmt.Sprintf("%s.%s.%s", s.Pkg.Name, s.Type, s.Name) 33 | 34 | if s.Type == "func" || s.Type == "method" { 35 | out = fmt.Sprintf("%s LOC: %d, %d, Depth: %d,%d", out, s.LOC, s.LOCCum(), s.Depth(), s.DepthInternal()) 36 | } 37 | 38 | return out 39 | } 40 | 41 | // ID generates uniqie string ID for this selector. 42 | func (s *Selector) ID() string { 43 | if s.Recv != "" { 44 | return fmt.Sprintf("%s.(%s).%s.%s", s.Pkg.Name, s.Recv, s.Type, s.Name) 45 | } 46 | return fmt.Sprintf("%s.%s.%s", s.Pkg.Name, s.Type, s.Name) 47 | } 48 | 49 | // NewSelector creates new Selector. 50 | func NewSelector(pkg *types.Package, name, recv, typ string, loc int) *Selector { 51 | return &Selector{ 52 | Pkg: Package{ 53 | Name: pkg.Name(), 54 | Path: pkg.Path(), 55 | }, 56 | Name: name, 57 | 58 | Recv: recv, 59 | Type: typ, 60 | 61 | LOC: loc, 62 | } 63 | } 64 | 65 | // LOCCum returns cumulative LOC count for Selector and all it's dependencies. 66 | func (s *Selector) LOCCum() int { 67 | if !s.IsFunc() { 68 | return 0 69 | } 70 | 71 | ret := s.LOC 72 | for _, dep := range s.Deps { 73 | ret += dep.LOCCum() 74 | } 75 | 76 | return ret 77 | } 78 | 79 | // Depth returns Depth for Selector and all it's external dependencies. 80 | func (s *Selector) Depth() int { 81 | if !s.IsFunc() { 82 | return 0 83 | } 84 | 85 | ret := 0 86 | for _, dep := range s.Deps { 87 | if dep.Pkg != s.Pkg { 88 | ret++ 89 | ret += dep.Depth() 90 | } 91 | } 92 | 93 | return ret 94 | } 95 | 96 | // DepthInternal returns Depth for Selector and all it's internal dependencies. 97 | func (s *Selector) DepthInternal() int { 98 | if !s.IsFunc() { 99 | return 0 100 | } 101 | 102 | ret := 0 103 | for _, dep := range s.Deps { 104 | if dep.Pkg == s.Pkg { 105 | ret++ 106 | ret += dep.DepthInternal() 107 | } 108 | } 109 | 110 | return ret 111 | } 112 | 113 | // IsFunc returns true if Selector is either a function or a method. 114 | func (s *Selector) IsFunc() bool { 115 | return s.Type == "func" || s.Type == "method" 116 | } 117 | 118 | // PrintDeps recursively prints deps for selector. 119 | func (s *Selector) PrintDeps() { 120 | s.printDeps(0) 121 | } 122 | 123 | func (s *Selector) printDeps(depth int) { 124 | fmt.Println(strings.Repeat(" ", depth), fmt.Sprintf("%s.%s", s.Pkg.Name, s.Name)) 125 | for _, dep := range s.Deps { 126 | dep.printDeps(depth + 1) 127 | } 128 | } 129 | 130 | // ByID is helper type for sorting selectors by ID. 131 | type ByID []*Selector 132 | 133 | func (b ByID) Len() int { return len(b) } 134 | func (b ByID) Swap(i, j int) { b[i], b[j] = b[j], b[i] } 135 | func (b ByID) Less(i, j int) bool { 136 | return b[i].ID() < b[j].ID() 137 | } 138 | 139 | // Deps is a shorthand for Dependencies - a slice of Selectors. 140 | type Deps []*Selector 141 | 142 | // Append adds new Selector to Deps. 143 | func (deps *Deps) Append(s *Selector) { 144 | for _, d := range *deps { 145 | if d.ID() == s.ID() { 146 | return 147 | } 148 | } 149 | *deps = append(*deps, s) 150 | } 151 | 152 | // HasRecursion attempts to find selector in nested dependencies 153 | // to avoid recursion. 154 | func (deps Deps) HasRecursion(s *Selector) bool { 155 | for _, dep := range deps { 156 | if dep.ID() == s.ID() { 157 | return true 158 | } 159 | 160 | if dep.Deps != nil { 161 | has := dep.Deps.HasRecursion(s) 162 | if has { 163 | return true 164 | } 165 | } 166 | } 167 | return false 168 | } 169 | -------------------------------------------------------------------------------- /test/bar/bar.go: -------------------------------------------------------------------------------- 1 | package bar 2 | 3 | func Bar(x int) { 4 | if x == 3 { 5 | Foo(2) 6 | } 7 | } 8 | 9 | func Foo(x int) { 10 | if x == 3 { 11 | Bar(4) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/const.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/divan/depscheck/test/foo" 6 | ) 7 | 8 | func main() { 9 | x := foo.FooConst 10 | fmt.Println(x) 11 | } 12 | -------------------------------------------------------------------------------- /test/exported.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/divan/depscheck/test/sample" 6 | "math" 7 | "strings" 8 | ) 9 | 10 | type Test struct { 11 | X string 12 | Y int 13 | Z bool 14 | } 15 | 16 | func Xtest() { 17 | t := &Test{ 18 | Y: xsample.Sample + xsample.SampleFunc(), 19 | } 20 | _ = math.Pi 21 | if strings.HasPrefix("test", "t") { 22 | fmt.Println("OK") 23 | } 24 | _ = t.X 25 | fmt.Println(math.Max(1, 2)) 26 | go func() { 27 | _ = math.Min(1, 2) 28 | }() 29 | } 30 | -------------------------------------------------------------------------------- /test/exported2.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/divan/depscheck/test/sample" 5 | ) 6 | 7 | func Xtest() { 8 | xsample.SampleFunc() 9 | var foo xsample.Foo 10 | foo.Bar() 11 | } 12 | -------------------------------------------------------------------------------- /test/external.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/divan/depscheck/test/foo" 6 | "golang.org/x/tools/go/loader" 7 | ) 8 | 9 | func main() { 10 | x := foo.FooConst 11 | var l loader.Config 12 | fmt.Println(x, l) 13 | } 14 | -------------------------------------------------------------------------------- /test/foo/foo.go: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | import "github.com/divan/depscheck/test/bar" 4 | 5 | const FooConst = 42 6 | 7 | var FooVar = "42" 8 | 9 | type Fooer interface { 10 | Foo(int) 11 | } 12 | 13 | func Foo(x int) { 14 | if x == 2 { 15 | bar.Bar(3) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/interface.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/divan/depscheck/test/foo" 4 | 5 | func Foo(foo foo.Fooer) { 6 | foo.Foo(42) 7 | } 8 | -------------------------------------------------------------------------------- /test/pkg_dot.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | . "github.com/divan/depscheck/test/sample" 5 | ) 6 | 7 | func Xtest() { 8 | SampleFunc() 9 | var foo Foo 10 | foo.Bar() 11 | } 12 | -------------------------------------------------------------------------------- /test/pkg_renamed.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | x "github.com/divan/depscheck/test/sample" 5 | ) 6 | 7 | func Xtest() { 8 | x.SampleFunc() 9 | var foo x.Foo 10 | foo.Bar() 11 | } 12 | -------------------------------------------------------------------------------- /test/recursion.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/divan/depscheck/test/bar" 5 | "github.com/divan/depscheck/test/foo" 6 | ) 7 | 8 | func Foo(x int) { 9 | if x == 2 { 10 | bar.Bar(3) 11 | } 12 | } 13 | 14 | func Bar(x int) { 15 | if x == 2 { 16 | foo.Foo(3) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/sample/sample.go: -------------------------------------------------------------------------------- 1 | package xsample 2 | 3 | type Foo struct{} 4 | 5 | var Sample = 123 6 | 7 | func SampleFunc() int { 8 | x := 12 9 | x++ 10 | y := 5 11 | Xfunc() 12 | return y + x 13 | } 14 | 15 | func Xfunc() { 16 | y := 2 17 | y += 22 18 | y++ 19 | YFunc() 20 | } 21 | 22 | func YFunc() { 23 | x := 12 24 | _ = x 25 | } 26 | 27 | func (s Foo) Bar() { 28 | x := 42 29 | _ = x 30 | } 31 | -------------------------------------------------------------------------------- /test/var.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/divan/depscheck/test/foo" 6 | ) 7 | 8 | func main() { 9 | x := foo.FooVar 10 | fmt.Println(x) 11 | } 12 | -------------------------------------------------------------------------------- /walker.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/types" 7 | "golang.org/x/tools/go/loader" 8 | ) 9 | 10 | // Walker holds all information needed during walking 11 | // and analyzing AST source tree. 12 | type Walker struct { 13 | P *loader.Program 14 | Packages map[string]Package 15 | CacheLOC map[*ast.FuncDecl]int 16 | CacheNodes map[*ast.Ident]*ast.FuncDecl 17 | 18 | Stdlib bool 19 | Internal bool 20 | 21 | Visited map[*ast.FuncDecl]*Selector 22 | } 23 | 24 | // NewWalker inits new AST walker. 25 | func NewWalker(p *loader.Program, stdlib, internal bool) *Walker { 26 | packages := make(map[string]Package) 27 | for _, pkg := range p.InitialPackages() { 28 | // prepare map of resolved imports 29 | for _, i := range pkg.Pkg.Imports() { 30 | 31 | if !stdlib && IsStdlib(i.Path()) { 32 | continue 33 | } 34 | if !internal && IsInternal(pkg.Pkg.Path(), i.Path()) { 35 | continue 36 | } 37 | packages[i.Name()] = NewPackage(i.Name(), i.Path()) 38 | } 39 | } 40 | return &Walker{ 41 | P: p, 42 | Packages: packages, 43 | CacheLOC: make(map[*ast.FuncDecl]int), 44 | CacheNodes: make(map[*ast.Ident]*ast.FuncDecl), 45 | 46 | Stdlib: stdlib, 47 | Internal: internal, 48 | 49 | Visited: make(map[*ast.FuncDecl]*Selector), 50 | } 51 | } 52 | 53 | // TopWalk walks the initial package, looking only for selectors from imported 54 | // packages. 55 | func (w *Walker) TopWalk() *Result { 56 | result := NewResult() 57 | for _, pkg := range w.P.InitialPackages() { 58 | w.WalkPackage(pkg, result) 59 | } 60 | return result 61 | } 62 | 63 | // WalkPackage looks for dependencies used in a given package and saves 64 | // selectors to result. 65 | // 66 | // It should be called for the top-level package only. 67 | // Only external dependencies are added to result. 68 | func (w *Walker) WalkPackage(pkg *loader.PackageInfo, result *Result) { 69 | for _, obj := range pkg.Uses { 70 | if obj.Pkg() == nil || obj.Pkg() == pkg.Pkg { 71 | continue 72 | } 73 | 74 | // Omit the internal modules 75 | if !w.Internal && IsInternal(pkg.Pkg.Path(), obj.Pkg().Path()) { 76 | continue 77 | } 78 | 79 | if !obj.Exported() { 80 | continue 81 | } 82 | 83 | depPkg := w.P.Package(obj.Pkg().Path()) 84 | 85 | if sel := w.WalkObject(depPkg, obj); sel != nil { 86 | result.Add(sel) 87 | } 88 | } 89 | } 90 | 91 | // WalkObject builds Selector from the given pkg and object. 92 | // 93 | // It recursively goes into nested functions/calls adding it as Deps. 94 | func (w *Walker) WalkObject(pkg *loader.PackageInfo, obj types.Object) *Selector { 95 | if obj == nil { 96 | return nil 97 | } 98 | 99 | if !w.Stdlib && IsStdlib(pkg.Pkg.Path()) { 100 | return nil 101 | } 102 | 103 | decl, def := w.FindDefDecl(pkg, obj) 104 | if def == nil || decl == nil { 105 | return nil 106 | } 107 | 108 | var typ, recv string 109 | 110 | switch d := def.(type) { 111 | case *types.Const: 112 | typ = "const" 113 | case *types.Var: 114 | if d.IsField() { 115 | return nil 116 | } 117 | typ = "var" 118 | case *types.Func: 119 | typ = "func" 120 | if r := d.Type().(*types.Signature).Recv(); r != nil { 121 | typ = "method" 122 | recv = printType(r.Type()) 123 | } 124 | case *types.TypeName: 125 | typ = "type" 126 | if _, ok := d.Type().Underlying().(*types.Interface); ok { 127 | typ = "interface" 128 | } 129 | } 130 | 131 | fnDecl := w.FnDecl(pkg, decl) 132 | if fnDecl == nil { 133 | return NewSelector(pkg.Pkg, obj.Name(), recv, typ, 0) 134 | } 135 | 136 | if sel, ok := w.Visited[fnDecl]; ok { 137 | return sel 138 | } 139 | 140 | loc := w.LOC(fnDecl) 141 | sel := NewSelector(pkg.Pkg, fnDecl.Name.Name, recv, typ, loc) 142 | 143 | w.Visited[fnDecl] = sel 144 | deps := w.WalkFuncBody(pkg, fnDecl) 145 | 146 | if !deps.HasRecursion(sel) { 147 | sel.Deps = append(sel.Deps, deps...) 148 | // update visited Selector with deps 149 | w.Visited[fnDecl] = sel 150 | } 151 | 152 | return sel 153 | } 154 | 155 | // WalkFuncBody searches for all internal or external selectors, used in a given 156 | // function. It recursively goes into it, building Deps slice. 157 | func (w *Walker) WalkFuncBody(pkg *loader.PackageInfo, node *ast.FuncDecl) Deps { 158 | var deps Deps 159 | ast.Inspect(node, func(n ast.Node) bool { 160 | switch expr := n.(type) { 161 | case *ast.CallExpr: 162 | switch expr := expr.Fun.(type) { 163 | case *ast.Ident: 164 | obj := w.LookupObject(pkg, expr) 165 | s := w.WalkObject(pkg, obj) 166 | if s != nil { 167 | deps.Append(s) 168 | } 169 | return false 170 | case *ast.SelectorExpr: 171 | obj, ok := pkg.Uses[expr.Sel] 172 | if !ok || obj.Pkg() == nil { 173 | return false 174 | } 175 | 176 | depPkg := w.P.Package(obj.Pkg().Path()) 177 | s := w.WalkObject(depPkg, obj) 178 | if s != nil { 179 | deps.Append(s) 180 | } 181 | return false 182 | } 183 | return false 184 | } 185 | return true 186 | }) 187 | return deps 188 | } 189 | 190 | // FindDefDecl searches for declaration and definition for the given object. 191 | func (w *Walker) FindDefDecl(pkg *loader.PackageInfo, obj types.Object) (*ast.Ident, types.Object) { 192 | for decl, def := range pkg.Defs { 193 | if def == nil || obj == nil { 194 | continue 195 | } 196 | if def == obj { 197 | return decl, def 198 | } 199 | } 200 | 201 | return nil, nil 202 | } 203 | 204 | // FnDecl searches for the FuncDecl based on ast.Ident node. 205 | func (w *Walker) FnDecl(pkg *loader.PackageInfo, decl *ast.Ident) *ast.FuncDecl { 206 | if fn, ok := w.CacheNodes[decl]; ok { 207 | return fn 208 | } 209 | for _, f := range pkg.Files { 210 | for _, d := range f.Decls { 211 | if fnDecl, ok := d.(*ast.FuncDecl); ok { 212 | if decl == fnDecl.Name { 213 | w.CacheNodes[decl] = fnDecl 214 | return fnDecl 215 | } 216 | } 217 | } 218 | } 219 | return nil 220 | } 221 | 222 | // LOC calculates readl Lines Of Code for the given function node. 223 | // node must be ast.FuncDecl, panics otherwise. 224 | func (w *Walker) LOC(node *ast.FuncDecl) int { 225 | if lines, ok := w.CacheLOC[node]; ok { 226 | return lines 227 | } 228 | 229 | body := node.Body 230 | if body == nil { 231 | w.CacheLOC[node] = 0 232 | return 0 233 | } 234 | 235 | start := w.P.Fset.Position(body.Lbrace) 236 | end := w.P.Fset.Position(body.Rbrace) 237 | lines := end.Line - start.Line 238 | 239 | // for cases line 'func foo() { bar() }' 240 | // TODO: figure out how to calculate it smarter 241 | if lines == 0 { 242 | lines = 1 243 | } 244 | 245 | w.CacheLOC[node] = lines 246 | 247 | return lines 248 | } 249 | 250 | // LookupObject searches for the object in current package by ast.Ident node. 251 | func (w *Walker) LookupObject(pkg *loader.PackageInfo, expr *ast.Ident) types.Object { 252 | for decl, def := range pkg.Defs { 253 | if decl.Obj != nil && decl.Obj == expr.Obj { 254 | return def 255 | } 256 | } 257 | 258 | return nil 259 | } 260 | 261 | func printType(t types.Type) string { 262 | switch t := t.(type) { 263 | case *types.Pointer: 264 | return fmt.Sprintf("*%s", printType(t.Elem())) 265 | case *types.Named: 266 | return t.Obj().Name() 267 | } 268 | return t.String() 269 | } 270 | --------------------------------------------------------------------------------