├── .gitignore ├── .travis.yml ├── README.md ├── main.go └── unused ├── finder.go ├── func.go ├── funcs.go ├── funcs_test.go ├── identkind14.go ├── identkind15.go ├── idents.go ├── idents_test.go ├── object.go └── testdata ├── mockmain.go ├── pkg1 ├── random_num.go └── random_num_test.go └── pkg2 └── kittens.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | 25 | .*.swp 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.5 5 | - 1.6 6 | - tip 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | codecoroner [![Build Status](https://travis-ci.org/3rf/codecoroner.svg)](https://travis-ci.org/3rf/codecoroner) 2 | =============== 3 | 4 | ## WARNING 5 | *Heads up that this project is no longer maintained.* 6 | 7 | Unfortunately I haven't had the luxury of writing Go for my job for a few years now. 8 | I hope if some Google search brought you here, you're able to find what you need. 9 | 10 | I realize the irony of a dead code project being dead. 11 | 12 | 13 | ###### Version 1.2 by Kyle Erf, MIT License 14 | 15 | 16 | Leaving dead code in a large codebase with multiple libraries is difficult to avoid. 17 | Things get moved around; functions get refactored, leaving helpers on their own; people miscommunicate. 18 | 19 | The easiest ways to detect dead code is through static analysis. 20 | Unfortunately, Go's current static analysis tools (`oracle`, `callgraph`, etc) do not make aggregation of unused functions as easy as it should be. 21 | This tool, codecoroner, uses the output of the Go `ast`, `ssa`, `callgraph/rta`, and `types` libraries to find unused functions/methods in your codebase. 22 | 23 | The existing [unused code detectors](https://github.com/remyoudompheng/go-misc/tree/master/deadcode) are quite useful, but only work on a small scale. 24 | Codecoroner was developed with large, multi-package, multi-main projects in mind. 25 | The tool will detect unused functions and variables across packages, allowing you to see if your internal packages have exported code sitting unused. 26 | At MongoDB, it helps us keep our repositories clean and even caught a couple bugs. 27 | So far, codecoronoer has found dead code in every large public Go project I point it at. 28 | 29 | 30 | ### Quick Start 31 | 32 | First, grab the `codecoroner` binary by either cloning this git repository and building main.go or by running 33 | ```bash 34 | go get github.com/3rf/codecoroner 35 | ``` 36 | which should install a `codecoroner` binary in `$GOPATH/bin` 37 | 38 | Codecoroner has two modes: `funcs` and `idents`, which detect dead code using callgraph and identifier analysis, respectively. 39 | Each has their own set of benefits. 40 | 41 | #### Funcs 42 | 43 | The `funcs` command builds a graph of function calls within your codebase and checks which functions are definied, but not reachable, from your `main` packages. 44 | This method takes advantage of the `callgraph/rta` implementation, which is geared toward applications like dead code analysis. 45 | 46 | To run a `funcs` analysis, you can do 47 | ```bash 48 | codecoroner funcs ./... 49 | ``` 50 | in the root of your project, similarly to how you would use `golint`. 51 | 52 | Your results will look something like 53 | ``` 54 | unused/testdata/mockmain.go:15:1: oldHelper 55 | unused/testdata/pkg1/random_num.go:31:1: toUint 56 | unused/testdata/pkg1/random_num.go:36:1: GenUInt 57 | unused/testdata/pkg1/random_num.go:42:1: GenSix 58 | unused/testdata/pkg2/kittens.go:13:1: Val 59 | unused/testdata/pkg2/kittens.go:25:1: GrayKittenLink 60 | ``` 61 | 62 | As a note: the `funcs` command only detects the usage of top-level functions and methods declared in the `func myFunc(a string){...}` form. 63 | It does not track usage of anonymous functions or functions declared as package variables in the `var myFunc = func(a string){...}`; however, the `idents` command can catch the latter case. 64 | 65 | 66 | #### Idents 67 | 68 | The `idents` command is a more simplistic and broad form of analysis. 69 | It looks at every declared non-local identifier (package variables, functions, parameters, struct fields, methods) and checks to see that those identifiers are used outside of their declaration. 70 | Identifier analysis will catch things like unused constants, struct fields, and methods--all across packages. 71 | 72 | To run an `idents` analysis, you can do 73 | ```bash 74 | codecoroner idents ./... 75 | ``` 76 | in the root of your project. 77 | 78 | Your results will look something like 79 | ``` 80 | github.com/3rf/codecoroner/unused/testdata/pkg1/random_num.go:10:7: Number 81 | github.com/3rf/codecoroner/unused/testdata/pkg1/random_num.go:13:5: AnotherNumber 82 | github.com/3rf/codecoroner/unused/testdata/pkg1/random_num.go:36:6: GenUInt 83 | github.com/3rf/codecoroner/unused/testdata/pkg1/random_num.go:42:6: GenSix 84 | github.com/3rf/codecoroner/unused/testdata/pkg2/kittens.go:11:25: field 85 | github.com/3rf/codecoroner/unused/testdata/pkg2/kittens.go:13:7: ut 86 | github.com/3rf/codecoroner/unused/testdata/pkg2/kittens.go:13:22: (unusedType).Val 87 | github.com/3rf/codecoroner/unused/testdata/pkg2/kittens.go:25:6: GrayKittenLink 88 | ``` 89 | 90 | The `idents` command has more false positives and negatives than `funcs`. 91 | One reason for this is that `idents` does not build an execution graph, and so will not acknowledge code that is accessed through an interface, or catch unused code that is used cyclically but unreachable by main (e.g. `FuncA()` and `FuncB()` can call each other but nothing externally calls either of them). 92 | 93 | 94 | ### Full Usage 95 | 96 | In addition to a command, the `codecoroner` executable requires a set of files as an argument. 97 | You can pass in individual files and folders, or pass the current directory and its contents with `./...` in the style of the `go` program. 98 | Codecoroner will automatically see what packages the files you give it belong to and include them in the dead code analysis. 99 | This API is designed to play nice with existing go tools and makes sense to me, but if you would prefer a different API, I'd be happy to hear you out. 100 | 101 | Note that both modes will only report dead code for the packages/files you've passed to the tool. 102 | Imports will be automatically detected so that callgraphs can be generated, but dead code inside those imports will not be reported. 103 | 104 | #### Flags 105 | 106 | ##### -v 107 | ``` 108 | codecoroner -v funcs ./... 109 | ``` 110 | 111 | The verbose flag, `-v`, will print log messages to help you follow and troubleshoot your dead code analysis. 112 | For example, running `codecoroner` in the root of its repository produces: 113 | ``` 114 | $ ./codecoroner -v funcs ./... 115 | Collecting declarations from source files 116 | Found pkg github.com/3rf/codecoroner 117 | Ignoring path 'unused/funcs_test.go' 118 | Ignoring path 'unused/idents_test.go' 119 | Found pkg github.com/3rf/codecoroner/unused/testdata 120 | Ignoring path 'unused/testdata/pkg1/random_num_test.go' 121 | Parsed 8 source files 122 | Running callgraph analysis on following packages: 123 | github.com/3rf/codecoroner 124 | github.com/3rf/codecoroner/unused/testdata 125 | Running loader 126 | Scanning callgraph for unused functions 127 | 128 | unused/testdata/mockmain.go:15:1: oldHelper 129 | unused/testdata/pkg1/random_num.go:31:1: toUint 130 | unused/testdata/pkg1/random_num.go:36:1: GenUInt 131 | unused/testdata/pkg1/random_num.go:42:1: GenSix 132 | unused/testdata/pkg2/kittens.go:13:1: Val 133 | unused/testdata/pkg2/kittens.go:25:1: GrayKittenLink 134 | ``` 135 | 136 | ##### -tests 137 | ``` 138 | codecoroner -tests funcs ./... 139 | ``` 140 | 141 | The `-tests` flag includes test files and packages in your analysis. 142 | Doing this allows you to test main-less libraries and detect dead test helper code. 143 | 144 | 145 | ##### -ignore 146 | ``` 147 | codecoroner -ignore vendor,testdata funcs ./... 148 | ``` 149 | 150 | The `-ignore` flag accepts a comma-separated list of strings. 151 | If any of the listed strings matches part of a filepath during scanning, that file will be ignored and excluded from the analysis. 152 | This flag is a simple way to ignore vendored code without complicating the codecoroner's file argument. 153 | 154 | ##### -tags 155 | ``` 156 | codecornor -tags debug funcs ./... 157 | ``` 158 | 159 | The `-tags` flag lets you pass build tags like you would during a regular `go build`. 160 | If your codebase uses flags, note that unbuilt files may show up as dead code. 161 | 162 | #### Troubleshooting 163 | 164 | Some notes that may help with troubleshooting: 165 | * Make sure your code can actually compile with `go build` before running codecoroner on it. 166 | * If you have a vendoring system involving multiple GOPATHs, codecoroner should still work. In general, if you can execute `go build` from your current directory, you can run `codecoroner ./...` sucessfully. 167 | 168 | When in doubt, file a GitHub issue and I'll be happy to help. 169 | 170 | 171 | #### The Future 172 | It would be great to drop the `idents` command and `funcs` commands altogether and do everything with SSA analysis. 173 | None of the callgraph packages make the usage of non-function identifiers accessible, so it'll require haking at an existing implementation or building another callgraph package from scratch. 174 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/3rf/codecoroner/unused" 7 | "go/build" 8 | "golang.org/x/tools/go/buildutil" 9 | "os" 10 | "sort" 11 | "strings" 12 | ) 13 | 14 | func main() { 15 | var ignoreList string 16 | ucf := unused.NewUnusedCodeFinder() 17 | flag.BoolVar(&(ucf.Verbose), "v", false, 18 | "prints extra information during execution to stderr") 19 | flag.BoolVar(&(ucf.IncludeTests), "tests", false, "include tests in the analysis") 20 | flag.StringVar(&(ignoreList), "ignore", "", 21 | "don't read files that contain the given comma-separated strings (use to avoid /testdata, etc) ") 22 | // hack for testing code with build flags 23 | flag.Var((*buildutil.TagsFlag)(&build.Default.BuildTags), "tags", "a list of build tags") 24 | flag.Parse() 25 | // handle ignore list 26 | ucf.Ignore = strings.Split(ignoreList, ",") 27 | if len(ucf.Ignore) > 0 && ucf.Ignore[0] == "" { 28 | ucf.Ignore = nil 29 | } 30 | 31 | if len(flag.Args()) == 0 { 32 | fmt.Println("Must specify either 'funcs' or 'idents' command. Run with -help for more info.") 33 | os.Exit(2) 34 | } 35 | command := flag.Arg(0) 36 | switch command { 37 | case "funcs", "functions": 38 | ucf.Idents = false 39 | case "idents", "identifiers": 40 | ucf.Idents = true 41 | default: 42 | fmt.Println("Must specify either 'funcs' or 'idents' command. Run with -help for more info.") 43 | os.Exit(2) 44 | } 45 | 46 | unusedObjects, err := ucf.Run(flag.Args()[1:]) 47 | if err != nil { 48 | fmt.Println("ERROR:", err) 49 | os.Exit(1) 50 | } 51 | ucf.Logf("") // ensure a newline before printing results if -v is on 52 | 53 | sort.Sort(unused.ByPosition(unusedObjects)) 54 | for _, o := range unusedObjects { 55 | fmt.Printf("%s\n", o) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /unused/finder.go: -------------------------------------------------------------------------------- 1 | // The "unused" package wraps the go's static anaylsis packages and provides 2 | // hooks for finding unused functions and identifiers in a codebase 3 | package unused 4 | 5 | import ( 6 | "fmt" 7 | "go/ast" 8 | "go/parser" 9 | "go/token" 10 | "io" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | ) 15 | 16 | type UnusedCodeFinder struct { 17 | // universal config options 18 | Idents bool 19 | Ignore []string 20 | Verbose bool 21 | LogWriter io.Writer 22 | 23 | IncludeTests bool 24 | 25 | filesByCaller map[string][]token.Position 26 | pkgs map[string]struct{} 27 | funcs []UnusedObject 28 | numFilesRead int 29 | } 30 | 31 | func NewUnusedCodeFinder() *UnusedCodeFinder { 32 | return &UnusedCodeFinder{ 33 | // init private storage 34 | pkgs: map[string]struct{}{}, 35 | filesByCaller: map[string][]token.Position{}, 36 | funcs: []UnusedObject{}, 37 | // default to stderr; this can be overwritten before Run() is called 38 | LogWriter: os.Stderr, 39 | } 40 | } 41 | 42 | // TODO: move this log stuff to the bottom 43 | // Logf is a one-off function for writing any verbose log output to 44 | // stderr. There might be a more idiomatic way to do this in go... 45 | func (ucf *UnusedCodeFinder) Logf(format string, v ...interface{}) { 46 | if ucf.Verbose { 47 | //ignore any errors in Fprintf for now 48 | fmt.Fprintf(ucf.LogWriter, format+"\n", v...) 49 | } 50 | } 51 | 52 | // Errorf is a one-off function for writing any error output to 53 | // stderr. There might be a more idiomatic way to do this in go... 54 | func (ucf *UnusedCodeFinder) Errorf(format string, v ...interface{}) { 55 | fmt.Fprintf(ucf.LogWriter, format+"\n", v...) 56 | } 57 | 58 | // AddPkg sets the package name as an entry in the package map, 59 | // here the map holds no values and functions as a hash set 60 | func (ucf *UnusedCodeFinder) AddPkg(pkgName string) { 61 | ucf.pkgs[pkgName] = struct{}{} 62 | ucf.Logf("Found pkg %v", pkgName) 63 | } 64 | 65 | func (ucf *UnusedCodeFinder) pkgsAsArray() []string { 66 | packages := make([]string, 0, len(ucf.pkgs)) 67 | for pkg, _ := range ucf.pkgs { 68 | packages = append(packages, pkg) 69 | } 70 | return packages 71 | } 72 | 73 | func (ucf *UnusedCodeFinder) readFuncsAndImportsFromFile(filename string) error { 74 | fset := token.NewFileSet() 75 | f, err := parser.ParseFile(fset, filename, nil, 0) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | // check if this is a main packages or 81 | // if we want to analyze everything 82 | if f.Name.Name == "main" || ucf.Idents || ucf.IncludeTests { 83 | pkgName, err := getFullPkgName(filename) 84 | if err != nil { 85 | return fmt.Errorf("error getting main package path: %v", err) 86 | } 87 | ucf.AddPkg(pkgName) 88 | } 89 | 90 | // iterate over the AST, tracking found functions 91 | ast.Inspect(f, func(n ast.Node) bool { 92 | var s string 93 | switch node := n.(type) { 94 | case *ast.FuncDecl: 95 | s = node.Name.String() 96 | } 97 | if s != "" { 98 | switch { 99 | //TODO make this a helper 100 | case strings.Contains(s, "Test"): 101 | case s == "main": 102 | case s == "init": 103 | case s == "test": 104 | default: 105 | ucf.funcs = append(ucf.funcs, UnusedObject{s, fset.Position(n.Pos())}) 106 | } 107 | } 108 | return true 109 | }) 110 | 111 | ucf.numFilesRead++ 112 | return nil 113 | } 114 | 115 | // helper for directory traversal 116 | func isDir(filename string) bool { 117 | fi, err := os.Stat(filename) 118 | return err == nil && fi.IsDir() 119 | } 120 | 121 | // helper for grabbing package name from its folder 122 | func getFullPkgName(filename string) (string, error) { 123 | abs, err := filepath.Abs(filename) 124 | if err != nil { 125 | return "", err 126 | } 127 | // strip the GOPATH. Error if this doesn't work. 128 | stripped := trimGopath(abs) 129 | if stripped != filename { 130 | return filepath.Dir(stripped), nil 131 | } 132 | // a check during initialization ensures that GOPATH != "" so this should be safe 133 | goPaths := filepath.SplitList(os.Getenv("GOPATH")) 134 | return "", fmt.Errorf("cd %q and try again", goPaths[len(goPaths)-1]) 135 | } 136 | 137 | // trimGopath removes the GOPATH from a filepath, for simplicity 138 | func trimGopath(filename string) string { 139 | goPaths := filepath.SplitList(os.Getenv("GOPATH")) 140 | for _, p := range goPaths { 141 | p = filepath.Join(p, "src") + string(filepath.Separator) 142 | if !strings.HasPrefix(filename, p) { 143 | continue 144 | } 145 | stripped := strings.TrimPrefix(filename, p) 146 | return stripped 147 | } 148 | return filename 149 | } 150 | 151 | func (ucf *UnusedCodeFinder) canReadSourceFile(filename string) bool { 152 | if ucf.shouldIgnorePath(filename) { 153 | ucf.Logf("Ignoring path '%v'", filename) 154 | return false 155 | } 156 | if !strings.HasSuffix(filename, ".go") { 157 | return false 158 | } 159 | return true 160 | } 161 | 162 | func (ucf *UnusedCodeFinder) shouldIgnorePath(path string) bool { 163 | for _, ignoreToken := range ucf.Ignore { 164 | if strings.Contains(path, ignoreToken) { 165 | return true 166 | } 167 | } 168 | // skip test pkgs if -tests=false 169 | if !ucf.IncludeTests && strings.HasSuffix(path, "_test.go") { 170 | return true 171 | } 172 | return false 173 | } 174 | 175 | func (ucf *UnusedCodeFinder) readDir(dirname string) error { 176 | err := filepath.Walk(dirname, func(path string, info os.FileInfo, err error) error { 177 | if err == nil && !info.IsDir() && ucf.canReadSourceFile(path) { 178 | err = ucf.readFuncsAndImportsFromFile(path) 179 | } 180 | return err 181 | }) 182 | return err 183 | } 184 | 185 | func (ucf *UnusedCodeFinder) Run(fileArgs []string) ([]UnusedObject, error) { 186 | 187 | // do some basic sanity checks on system configuration 188 | if len(fileArgs) == 0 { 189 | return nil, fmt.Errorf( 190 | "no files supplied as arguments; must supply at least one file or directory") 191 | } 192 | if os.Getenv("GOPATH") == "" { 193 | return nil, fmt.Errorf("GOPATH not set") 194 | } 195 | 196 | // first, get all the file names and package imports 197 | ucf.Logf("Collecting declarations from source files") 198 | for _, filename := range fileArgs { 199 | if strings.HasSuffix(filename, "/...") && isDir(filename[:len(filename)-4]) { 200 | // go tool ./... style 201 | if err := ucf.readDir(filename[:len(filename)-4]); err != nil { 202 | ucf.Errorf("Error reading '...': %v", err.Error()) 203 | ucf.Errorf("Continuing...") 204 | } 205 | } else if isDir(filename) { 206 | if err := ucf.readDir(filename); err != nil { 207 | ucf.Errorf("Error reading '%v' directory: %v", filename, err.Error()) 208 | ucf.Errorf("Continuing...") 209 | } 210 | } else { 211 | if ucf.canReadSourceFile(filename) { 212 | if err := ucf.readFuncsAndImportsFromFile(filename); err != nil { 213 | ucf.Errorf("Error reading '%v' file: %v", filename, err.Error()) 214 | ucf.Errorf("Continuing...") 215 | } 216 | } 217 | } 218 | } 219 | ucf.Logf("Parsed %v source files", ucf.numFilesRead) 220 | 221 | if ucf.Idents { 222 | return ucf.findUnusedIdents() 223 | } 224 | return ucf.findUnusedFuncs() 225 | } 226 | -------------------------------------------------------------------------------- /unused/func.go: -------------------------------------------------------------------------------- 1 | package unused 2 | 3 | // typeFunc is implemented by both go/types.Func 4 | // and golang.org/x/tools/go/types/Func 5 | type typeFunc interface { 6 | Name() string 7 | FullName() string 8 | } 9 | -------------------------------------------------------------------------------- /unused/funcs.go: -------------------------------------------------------------------------------- 1 | package unused 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "golang.org/x/tools/go/callgraph/rta" 8 | "golang.org/x/tools/go/loader" 9 | "golang.org/x/tools/go/ssa" 10 | "golang.org/x/tools/go/ssa/ssautil" 11 | ) 12 | 13 | // main method for running callgraph-based unused code analysis 14 | func (ucf *UnusedCodeFinder) findUnusedFuncs() ([]UnusedObject, error) { 15 | // get the callgraph if we are doing this the hard way 16 | ucf.Logf("Running callgraph analysis on following packages: \n\t%v", 17 | strings.Join(ucf.pkgsAsArray(), "\n\t")) 18 | if err := ucf.getCallgraph(); err != nil { 19 | ucf.Errorf("Error running callgraph analysis: %v", err.Error()) 20 | return nil, err 21 | } 22 | 23 | // finally, figure out which functions are not in the graph 24 | ucf.Logf("Scanning callgraph for unused functions") 25 | unusedFuncs := ucf.computeUnusedFuncs() 26 | return unusedFuncs, nil 27 | } 28 | 29 | func (ucf *UnusedCodeFinder) getCallgraph() error { 30 | var conf loader.Config 31 | _, err := conf.FromArgs(ucf.pkgsAsArray(), ucf.IncludeTests) 32 | if err != nil { 33 | return fmt.Errorf("error loading program data: %v", err) 34 | } 35 | conf.AllowErrors = true 36 | ucf.Logf("Running loader") 37 | p, err := conf.Load() 38 | if err != nil { 39 | return fmt.Errorf("error loading program data: %v", err) 40 | } 41 | var buildMode ssa.BuilderMode 42 | if ucf.Verbose { 43 | buildMode = ssa.GlobalDebug 44 | } 45 | ssaP := ssautil.CreateProgram(p, buildMode) 46 | ssaP.Build() 47 | roots, err := ucf.getRoots(ssaP) 48 | if err != nil { 49 | return fmt.Errorf("error finding roots for callgraph analysis: %v", err) 50 | } 51 | res := rta.Analyze(roots, true) 52 | 53 | // build a simplified callgraph map for name->filenames 54 | for node, _ := range res.Reachable { 55 | position := ssaP.Fset.Position(node.Pos()) 56 | ucf.filesByCaller[node.Name()] = append(ucf.filesByCaller[node.Name()], position) 57 | } 58 | return nil 59 | } 60 | 61 | func (ucf *UnusedCodeFinder) isInCG(f UnusedObject) bool { 62 | for _, pos := range ucf.filesByCaller[f.Name] { 63 | if strings.Contains(pos.Filename, f.Position.Filename) { 64 | return true 65 | } 66 | } 67 | return false 68 | } 69 | 70 | func (ucf *UnusedCodeFinder) computeUnusedFuncs() []UnusedObject { 71 | unused := []UnusedObject{} 72 | for _, f := range ucf.funcs { 73 | if !ucf.isInCG(f) { 74 | unused = append(unused, f) 75 | } 76 | } 77 | return unused 78 | } 79 | 80 | // grab the callgraph roots from the passed in files. This is based on adonovan's 81 | // code from https://github.com/golang/tools/blob/master/cmd/callgraph/main.go 82 | func (ucf *UnusedCodeFinder) getRoots(prog *ssa.Program) ([]*ssa.Function, error) { 83 | pkgs := prog.AllPackages() 84 | mains := []*ssa.Package{} 85 | 86 | // create a test main if the user requests it 87 | if ucf.IncludeTests { 88 | if len(pkgs) > 0 { 89 | ucf.Logf("Building a test main for analysis") 90 | for _, pkg := range pkgs { 91 | if main := prog.CreateTestMainPackage(pkg); main != nil { 92 | mains = append(mains, main) 93 | } else { 94 | ucf.Logf("WARNING: -tests flag specified, but no test files found for %s", pkg) 95 | } 96 | } 97 | } else { 98 | return nil, fmt.Errorf("no packages specified") 99 | } 100 | } 101 | 102 | // then find *all* main packages 103 | for _, pkg := range pkgs { 104 | if pkg.Pkg.Name() == "main" { 105 | if pkg.Func("main") == nil { 106 | return nil, fmt.Errorf("no func main() in main package") 107 | } 108 | mains = append(mains, pkg) 109 | } 110 | } 111 | if len(mains) == 0 { 112 | return nil, fmt.Errorf("no main packages found") 113 | } 114 | 115 | roots := []*ssa.Function{} 116 | for _, root := range mains { 117 | roots = append(roots, root.Func("init"), root.Func("main")) 118 | } 119 | 120 | return roots, nil 121 | } 122 | -------------------------------------------------------------------------------- /unused/funcs_test.go: -------------------------------------------------------------------------------- 1 | package unused 2 | 3 | import ( 4 | "fmt" 5 | . "github.com/smartystreets/goconvey/convey" 6 | "os" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func init() { 12 | // quick check to make sure we are in the right directory 13 | if _, err := os.Stat("testdata/mockmain.go"); err != nil { 14 | panic("unused tests must be run from the 'github.com/3rf/codecoroner/unused' dir") 15 | } 16 | } 17 | 18 | // helpers for convey 19 | func ShouldBeFoundIn(actual interface{}, expected ...interface{}) string { 20 | // this can panic, but I'm not adding type checking 21 | target := actual.(string) 22 | results := expected[0].([]UnusedObject) 23 | for _, thing := range results { 24 | if strings.HasSuffix(thing.Name, target) { 25 | return "" 26 | } 27 | } 28 | return fmt.Sprintf("nothing named '%v' found in results", target) 29 | } 30 | 31 | func ShouldNotBeFoundIn(actual interface{}, expected ...interface{}) string { 32 | // this can panic, but I'm not adding type checking 33 | target := actual.(string) 34 | results := expected[0].([]UnusedObject) 35 | for _, thing := range results { 36 | if strings.HasSuffix(thing.Name, target) { 37 | return fmt.Sprintf("found '%v' in results (it shouldn't be there)", target) 38 | } 39 | } 40 | return "" 41 | } 42 | 43 | func TestUnusedFuncsWithMain(t *testing.T) { 44 | Convey("with a test main package and a default UnusedCodeFinder", t, func() { 45 | ucf := NewUnusedCodeFinder() 46 | So(ucf, ShouldNotBeNil) 47 | 48 | Convey("running 'funcs'", func() { 49 | results, err := ucf.Run([]string{"testdata"}) 50 | So(err, ShouldBeNil) 51 | 52 | Convey("all functions in pkg1 and pkg2 that main does not use should be found", func() { 53 | So("oldHelper", ShouldBeFoundIn, results) 54 | So("GenSix", ShouldBeFoundIn, results) 55 | So("GenUInt", ShouldBeFoundIn, results) 56 | So("toUint", ShouldBeFoundIn, results) 57 | So("GrayKittenLink", ShouldBeFoundIn, results) 58 | So("GenInt", ShouldNotBeFoundIn, results) 59 | So("GenIntMod400", ShouldNotBeFoundIn, results) 60 | So("ColorKittenLink", ShouldNotBeFoundIn, results) 61 | So("init", ShouldNotBeFoundIn, results) 62 | }) 63 | }) 64 | }) 65 | } 66 | 67 | func TestUnusedFuncsWithTests(t *testing.T) { 68 | Convey("with a test main package and a UnusedCodeFinder with -tests", t, func() { 69 | ucf := NewUnusedCodeFinder() 70 | So(ucf, ShouldNotBeNil) 71 | ucf.IncludeTests = true 72 | 73 | Convey("running 'funcs'", func() { 74 | results, err := ucf.Run([]string{"testdata"}) 75 | So(err, ShouldBeNil) 76 | 77 | Convey("all functions that are unused by any pkg or test are found", func() { 78 | So("oldHelper", ShouldBeFoundIn, results) 79 | So("GenUInt", ShouldBeFoundIn, results) 80 | So("toUint", ShouldBeFoundIn, results) 81 | So("GrayKittenLink", ShouldBeFoundIn, results) 82 | So("testhelper", ShouldBeFoundIn, results) 83 | }) 84 | 85 | Convey("but GenSix should not be found, since it is used in a test", func() { 86 | So("GenSix", ShouldNotBeFoundIn, results) 87 | }) 88 | }) 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /unused/identkind14.go: -------------------------------------------------------------------------------- 1 | // +build !go1.5 2 | 3 | package unused 4 | 5 | import "golang.org/x/tools/go/types" 6 | 7 | func objToFunc(obj types.Object) (f typeFunc, ok bool) { 8 | f, ok = obj.(*types.Func) 9 | return f, ok 10 | } 11 | -------------------------------------------------------------------------------- /unused/identkind15.go: -------------------------------------------------------------------------------- 1 | // +build go1.5 2 | 3 | package unused 4 | 5 | import "go/types" 6 | 7 | func objToFunc(obj types.Object) (f typeFunc, ok bool) { 8 | f, ok = obj.(*types.Func) 9 | return f, ok 10 | } 11 | -------------------------------------------------------------------------------- /unused/idents.go: -------------------------------------------------------------------------------- 1 | package unused 2 | 3 | import ( 4 | "fmt" 5 | "go/token" 6 | "strings" 7 | 8 | "golang.org/x/tools/go/loader" 9 | ) 10 | 11 | // shorten the method name for nicer printing and say if its a method 12 | func handleMethodName(f typeFunc) string { 13 | name := f.Name() 14 | if strings.HasPrefix(f.FullName(), "(") { 15 | // it's a method! let's shorten the receiver! 16 | fullName := f.FullName() 17 | // second to last "." 18 | sepIdx := strings.LastIndex(fullName[:strings.LastIndex(fullName, ".")], ".") 19 | if sepIdx <= 0 { // rare special case 20 | return fullName 21 | } 22 | return fmt.Sprintf("(%s", fullName[sepIdx+1:]) 23 | } 24 | return name 25 | } 26 | 27 | type ident struct { 28 | Name string 29 | Pos token.Pos 30 | } 31 | 32 | func (ucf *UnusedCodeFinder) findUnusedIdents() ([]UnusedObject, error) { 33 | var conf loader.Config 34 | _, err := conf.FromArgs(ucf.pkgsAsArray(), ucf.IncludeTests) 35 | if err != nil { 36 | return nil, fmt.Errorf("error loading program data: %v", err) 37 | } 38 | conf.AllowErrors = true 39 | ucf.Logf("Running loader") 40 | p, err := conf.Load() 41 | if err != nil { 42 | return nil, fmt.Errorf("error loading program data: %v", err) 43 | } 44 | 45 | identToUsage := map[ident]int{} 46 | defined := map[ident]struct{}{} 47 | 48 | for key, info := range p.Imported { 49 | if strings.Contains(key, ".") { //TODO do we need this if? 50 | 51 | // find all *used* idents 52 | for _, kind := range info.Info.Uses { 53 | if kind.Pkg() != nil { 54 | name := kind.Name() 55 | if f, ok := objToFunc(kind); ok { 56 | //special case for methods 57 | name = handleMethodName(f) 58 | } 59 | id := ident{Name: name, Pos: kind.Pos()} 60 | identToUsage[id] = identToUsage[id] + 1 61 | } 62 | } 63 | 64 | // find all *declared* idents 65 | for _, kind := range info.Info.Defs { 66 | if kind == nil { 67 | continue 68 | } 69 | if kind.Pkg() != nil { 70 | name := kind.Name() 71 | if name == "_" || 72 | name == "main" || 73 | name == "init" || 74 | strings.HasPrefix(name, "Test") { 75 | continue 76 | } 77 | if f, ok := objToFunc(kind); ok { 78 | name = handleMethodName(f) 79 | } 80 | if name == "." { 81 | continue 82 | } 83 | id := ident{Name: name, Pos: kind.Pos()} 84 | defined[id] = struct{}{} 85 | } 86 | } 87 | } 88 | } 89 | unused := []UnusedObject{} 90 | // see which declared idents are not actually used 91 | for key, _ := range defined { 92 | if _, exists := identToUsage[key]; !exists { 93 | unused = append(unused, UnusedObject{ 94 | Name: key.Name, 95 | Position: p.Fset.Position(key.Pos), 96 | }) 97 | } 98 | } 99 | return unused, nil 100 | } 101 | -------------------------------------------------------------------------------- /unused/idents_test.go: -------------------------------------------------------------------------------- 1 | package unused 2 | 3 | import ( 4 | . "github.com/smartystreets/goconvey/convey" 5 | "testing" 6 | ) 7 | 8 | func TestUnusedIdentsWithMain(t *testing.T) { 9 | Convey("with a test main package and a default UnusedCodeFinder", t, func() { 10 | ucf := NewUnusedCodeFinder() 11 | So(ucf, ShouldNotBeNil) 12 | ucf.Idents = true 13 | 14 | Convey("running 'idents'", func() { 15 | results, err := ucf.Run([]string{"testdata"}) 16 | So(err, ShouldBeNil) 17 | 18 | Convey("all idents in pkg1, pkg2, and main should be found", func() { 19 | So("Number", ShouldBeFoundIn, results) 20 | So("AnotherNumber", ShouldBeFoundIn, results) 21 | So("GenSix", ShouldBeFoundIn, results) 22 | So("GenUInt", ShouldBeFoundIn, results) 23 | So("(unusedType).Val", ShouldBeFoundIn, results) 24 | So("field", ShouldBeFoundIn, results) 25 | So("GrayKittenLink", ShouldBeFoundIn, results) 26 | So("oldHelper", ShouldBeFoundIn, results) 27 | So("unusedParam", ShouldBeFoundIn, results) 28 | 29 | So("GenInt", ShouldNotBeFoundIn, results) 30 | So("GenIntMod400", ShouldNotBeFoundIn, results) 31 | So("ColorKittenLink", ShouldNotBeFoundIn, results) 32 | So("init", ShouldNotBeFoundIn, results) 33 | }) 34 | 35 | Convey("but funcs that are called in other unused funcs will not be found", func() { 36 | So("toUint", ShouldNotBeFoundIn, results) 37 | }) 38 | }) 39 | }) 40 | } 41 | 42 | func TestUnusedIdentsWithTests(t *testing.T) { 43 | Convey("with a test main package and a default UnusedCodeFinder", t, func() { 44 | ucf := NewUnusedCodeFinder() 45 | So(ucf, ShouldNotBeNil) 46 | ucf.Idents = true 47 | ucf.IncludeTests = true 48 | 49 | Convey("running 'idents' with -tests", func() { 50 | results, err := ucf.Run([]string{"testdata"}) 51 | So(err, ShouldBeNil) 52 | 53 | Convey("all idents in pkg1, pkg2, and main should be found", func() { 54 | So("Number", ShouldBeFoundIn, results) 55 | So("AnotherNumber", ShouldBeFoundIn, results) 56 | So("GenUInt", ShouldBeFoundIn, results) 57 | So("(unusedType).Val", ShouldBeFoundIn, results) 58 | So("field", ShouldBeFoundIn, results) 59 | So("GrayKittenLink", ShouldBeFoundIn, results) 60 | So("oldHelper", ShouldBeFoundIn, results) 61 | So("unusedParam", ShouldBeFoundIn, results) 62 | 63 | So("GenInt", ShouldNotBeFoundIn, results) 64 | So("GenIntMod400", ShouldNotBeFoundIn, results) 65 | So("ColorKittenLink", ShouldNotBeFoundIn, results) 66 | So("init", ShouldNotBeFoundIn, results) 67 | So("toUint", ShouldNotBeFoundIn, results) 68 | 69 | Convey("plus idents only found in tests", func() { 70 | So("testhelper", ShouldBeFoundIn, results) 71 | So("GenSix", ShouldNotBeFoundIn, results) 72 | }) 73 | }) 74 | }) 75 | }) 76 | } 77 | 78 | func TestUnusedIdentsWithIgnore(t *testing.T) { 79 | Convey("with a test main package and a default UnusedCodeFinder", t, func() { 80 | ucf := NewUnusedCodeFinder() 81 | So(ucf, ShouldNotBeNil) 82 | ucf.Idents = true 83 | ucf.Ignore = []string{"pkg1", "pkg2"} 84 | 85 | Convey("running 'idents' with -ignore to skip pkg1 and pk2", func() { 86 | results, err := ucf.Run([]string{"testdata"}) 87 | So(err, ShouldBeNil) 88 | 89 | Convey("only unused idents in main should be found", func() { 90 | So("oldHelper", ShouldBeFoundIn, results) 91 | So("unusedParam", ShouldBeFoundIn, results) 92 | 93 | Convey("and nothing else", func() { 94 | So("pkg1.Number", ShouldNotBeFoundIn, results) 95 | So("pkg1.AnotherNumber", ShouldNotBeFoundIn, results) 96 | So("pkg1.GenUInt", ShouldNotBeFoundIn, results) 97 | So("pkg2.GrayKittenLink", ShouldNotBeFoundIn, results) 98 | }) 99 | }) 100 | }) 101 | }) 102 | } 103 | -------------------------------------------------------------------------------- /unused/object.go: -------------------------------------------------------------------------------- 1 | package unused 2 | 3 | import ( 4 | "fmt" 5 | "go/token" 6 | ) 7 | 8 | // UnusedThing represents a found unused function or identifier 9 | type UnusedObject struct { 10 | Name string 11 | Position token.Position 12 | } 13 | 14 | // String prints the position and name of the unused object. 15 | func (ut UnusedObject) String() string { 16 | return fmt.Sprintf("%v:%v:%v: %v", 17 | trimGopath(ut.Position.Filename), ut.Position.Line, ut.Position.Column, ut.Name) 18 | } 19 | 20 | // ByPosition sorts unused objects by file/location. 21 | // This type is a close copy of a similar sorter from the golint tool. 22 | type ByPosition []UnusedObject 23 | 24 | // Len method for sorting 25 | func (p ByPosition) Len() int { return len(p) } 26 | 27 | // Swap method for sorting 28 | func (p ByPosition) Swap(i, j int) { p[i], p[j] = p[j], p[i] } 29 | 30 | // Less method for sorting on the Position 31 | func (p ByPosition) Less(i, j int) bool { 32 | oi, oj := p[i].Position, p[j].Position 33 | 34 | if oi.Filename != oj.Filename { 35 | return oi.Filename < oj.Filename 36 | } 37 | if oi.Line != oj.Line { 38 | return oi.Line < oj.Line 39 | } 40 | if oi.Column != oj.Column { 41 | return oi.Column < oj.Column 42 | } 43 | 44 | // it's a bug if this even needs to be used 45 | return p[i].Name < p[j].Name 46 | } 47 | -------------------------------------------------------------------------------- /unused/testdata/mockmain.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/3rf/codecoroner/unused/testdata/pkg1" 6 | "github.com/3rf/codecoroner/unused/testdata/pkg2" 7 | ) 8 | 9 | func init() { 10 | // do nothing 11 | } 12 | 13 | // this function should be found by both modes, 14 | // the "unusedParam" parameter should be found by [idents] 15 | func oldHelper(str string, unusedParam uintptr) int { 16 | return len(str) 17 | } 18 | 19 | func main() { 20 | fmt.Println("This program is just for testing codecoroner.") 21 | fmt.Println("You're welcome to just run it if you want,") 22 | fmt.Println("but it's not meant to actually be used...\n") 23 | 24 | fmt.Println("Here are some random numbers:", pkg1.GenInt(), pkg1.GenInt(), pkg1.GenInt()) 25 | fmt.Println("And here is a link to a picture of a cat:", pkg2.ColorKittenLink()) 26 | } 27 | -------------------------------------------------------------------------------- /unused/testdata/pkg1/random_num.go: -------------------------------------------------------------------------------- 1 | // A small test package that generates random numbers. This code 2 | // is test code for codecoroner analysis--do not actually use it. 3 | package pkg1 4 | 5 | import ( 6 | "math/rand" 7 | ) 8 | 9 | // This const should be found by [idents] 10 | const Number = 5 11 | 12 | // This var should be found by [idents] 13 | var AnotherNumber = 7 14 | 15 | // This should not be found by any mode. 16 | var Six = 6 17 | 18 | // This function is used, so it should not be found by any mode. 19 | func GenInt() int { 20 | return rand.Int() 21 | } 22 | 23 | // This function should only be found by [idents] and [funcs] if 24 | // pkg2 is left out (i.e. just pkg1 is analyzed). 25 | func GenIntMod400() int { 26 | return GenInt() % 400 27 | } 28 | 29 | // This function should be found by [funcs] but not [idents], since 30 | // it is called by GenUInt, which is a dead function. 31 | func toUint(i int) uint { 32 | return uint(i) 33 | } 34 | 35 | // This function isn't used by any package, so [funcs] should find it 36 | func GenUInt() uint { 37 | return toUint(GenInt()) 38 | } 39 | 40 | // This function is only used in testing, so it should only be found 41 | // when tests are not included. 42 | func GenSix() int { 43 | return Six 44 | } 45 | -------------------------------------------------------------------------------- /unused/testdata/pkg1/random_num_test.go: -------------------------------------------------------------------------------- 1 | package pkg1 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // This helper should be found by [idents] and [funcs] if 8 | // test analysis is enabled. 9 | func testhelper() int { 10 | return 7 11 | } 12 | 13 | // This should not be found 14 | func TestTheNumberSix(t *testing.T) { 15 | if GenSix() != 6 { 16 | t.Fatal("THIS HAS GONE POORLY") 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /unused/testdata/pkg2/kittens.go: -------------------------------------------------------------------------------- 1 | // A small test package that generates random kitten picture links. 2 | // This code is test code for codecoroner analysis--do not actually use it. 3 | package pkg2 4 | 5 | import ( 6 | "fmt" 7 | "github.com/3rf/codecoroner/unused/testdata/pkg1" 8 | ) 9 | 10 | // this type and its method should be found by [idents] 11 | type unusedType struct{ field int } 12 | 13 | func (ut unusedType) Val() int { 14 | return 2 15 | } 16 | 17 | // This function should not be found, as it is used. 18 | func ColorKittenLink() string { 19 | return fmt.Sprintf("http://placekitten.com/%v/%v", 20 | pkg1.GenIntMod400()+400, 21 | pkg1.GenIntMod400()+200) 22 | } 23 | 24 | // This function should be found by [idents] and [funcs] 25 | func GrayKittenLink() string { 26 | return fmt.Sprintf("http://placekitten.com/g/%v/%v", 27 | pkg1.GenIntMod400()+400, 28 | pkg1.GenIntMod400()+200) 29 | } 30 | --------------------------------------------------------------------------------