├── .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 | [](https://travis-ci.org/divan/depscheck)
4 | [](https://goreportcard.com/report/github.com/divan/depscheck)
5 | [](https://github.com/divan/depscheck/releases)
6 | [](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 |
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 |
--------------------------------------------------------------------------------