├── .gitignore ├── LICENSE ├── README.md ├── cmd └── discover │ └── main.go ├── go.mod ├── go.sum ├── parse.go └── trim.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | cmd/discover/discover 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 André Eriksson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | discover 2 | ======== 3 | 4 | Discover is a utility to aid in conceptualizing large Go code bases. 5 | It is based on the idea presented by Alan Shreve in his talk on 6 | conceptualizing large software systems, held at dotGo 2015 in Paris. 7 | [Watch the video](http://www.thedotpost.com/2015/11/alan-shreve-conceptualizing-large-software-systems) for more information. 8 | 9 | It does this by taking a code coverage profile generated by "go test" 10 | and using it to trim the source code down to the blocks that were 11 | actually being run. 12 | 13 | Installation 14 | ------------ 15 | 16 | Simply run `go get github.com/eandre/discover/...` 17 | 18 | Examples 19 | -------- 20 | 21 | #### Run tests and output to console 22 | `discover test` 23 | 24 | #### Run a single test and output to console 25 | `discover test TestMyTestName` 26 | 27 | #### Run all tests starting with "TestFoo" 28 | `discover test TestFoo` 29 | 30 | #### Run all tests and write the output to ./foo 31 | `discover -output=./foo test` 32 | 33 | #### Parse an existing cover profile and write the output to ./foo 34 | `discover -output=./foo parse my-cover-profile.cov` 35 | 36 | Tips 37 | ---- 38 | 39 | If you want to track changes between two tests, write the output to a directory, 40 | and then use `git` to track the changes: 41 | 42 | ``` 43 | # Run first test 44 | discover -output=/tmp/example test TestFirst 45 | cd /tmp/example 46 | git init && git add -A && git commit -m "First" 47 | cd - 48 | 49 | # Run second test 50 | discover -output=/tmp/example test TestSecond 51 | cd /tmp/example 52 | git diff 53 | ``` 54 | -------------------------------------------------------------------------------- /cmd/discover/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "go/ast" 8 | "go/format" 9 | "go/token" 10 | "io/ioutil" 11 | "os" 12 | "os/exec" 13 | "path/filepath" 14 | "strings" 15 | 16 | "github.com/eandre/discover" 17 | "golang.org/x/tools/cover" 18 | ) 19 | 20 | func usage() { 21 | fmt.Fprintf(os.Stderr, `Usage: \n\ndiscover [flags] command [...] 22 | 23 | The commands are: 24 | 25 | discover [-output=] test [] 26 | Runs "go test -run " to output a cover profile, 27 | and then parses it and outputs the result. 28 | 29 | discover [-output=] parse 30 | Parses the given cover profile and outputs the result. 31 | 32 | For both commands, the output flag specifies a directory to write files to, 33 | as opposed to printing to stdout. If any of the files exist already, they will 34 | be overwritten. 35 | `) 36 | } 37 | 38 | var output = flag.String("output", "", "Directory to write output files to (will overwrite existing files)") 39 | 40 | func main() { 41 | flag.Usage = usage 42 | flag.Parse() 43 | if flag.NArg() == 0 { 44 | usage() 45 | os.Exit(1) 46 | } 47 | 48 | switch flag.Arg(0) { 49 | case "test": 50 | // run tests 51 | if err := runTests(flag.Arg(1)); err != nil { 52 | fmt.Fprintln(os.Stderr, err.Error()) 53 | os.Exit(1) 54 | } 55 | 56 | case "parse": 57 | if flag.NArg() <= 1 { 58 | fmt.Fprintln(os.Stderr, "missing cover profile") 59 | os.Exit(1) 60 | } 61 | if err := parseProfile(flag.Arg(1)); err != nil { 62 | fmt.Fprintln(os.Stderr, err.Error()) 63 | os.Exit(1) 64 | } 65 | } 66 | } 67 | 68 | func runTests(testRegexp string) error { 69 | tmpDir, err := ioutil.TempDir("", "discover") 70 | if err != nil { 71 | return err 72 | } 73 | defer os.RemoveAll(tmpDir) 74 | 75 | profilePath := filepath.Join(tmpDir, "coverprofile.out") 76 | args := []string{"test", "-coverprofile", profilePath} 77 | if testRegexp != "" { 78 | args = append(args, "-run", testRegexp) 79 | } 80 | 81 | cmd := exec.Command("go", args...) 82 | cmd.Stdin = nil 83 | cmd.Stdout = os.Stderr 84 | cmd.Stderr = os.Stderr 85 | if err := cmd.Run(); err != nil { 86 | return err 87 | } 88 | 89 | if _, err := os.Stat(profilePath); os.IsNotExist(err) { 90 | return errors.New("No tests found? (no cover profile generated)") 91 | } else if err != nil { 92 | return err 93 | } 94 | 95 | fmt.Printf("\n") // newline between "go test" output and ours 96 | return parseProfile(profilePath) 97 | } 98 | 99 | func parseProfile(fileName string) error { 100 | profiles, err := cover.ParseProfiles(fileName) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | prof, err := discover.ParseProfile(profiles) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | for _, f := range prof.Files { 111 | prof.Trim(f) 112 | 113 | // If we filtered out all decls, don't print at all 114 | if len(f.Decls) == 0 { 115 | continue 116 | } 117 | 118 | fn := filepath.Base(prof.Fset.File(f.Pos()).Name()) 119 | importPath := prof.ImportPaths[f] 120 | if importPath == "" { 121 | return fmt.Errorf("No import path found for %q", fn) 122 | } 123 | 124 | if err := outputFile(importPath, fn, prof.Fset, f); err != nil { 125 | return err 126 | } 127 | } 128 | return nil 129 | } 130 | 131 | func outputFile(importPath, name string, fset *token.FileSet, file *ast.File) error { 132 | if *output != "" { 133 | // Write to file 134 | dir := filepath.Join(*output, importPath) 135 | if err := os.MkdirAll(dir, 0755); err != nil { 136 | return err 137 | } 138 | target := filepath.Join(dir, name) 139 | f, err := os.Create(target) 140 | if err != nil { 141 | return err 142 | } 143 | if err := format.Node(f, fset, file); err != nil { 144 | return err 145 | } 146 | return nil 147 | } 148 | 149 | // Print to stdout 150 | fmt.Printf("%s:\n%s\n", name, strings.Repeat("=", len(name))) 151 | format.Node(os.Stdout, fset, file) 152 | fmt.Printf("\n\n") 153 | return nil 154 | } 155 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/eandre/discover 2 | 3 | go 1.16 4 | 5 | require golang.org/x/tools v0.1.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 2 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 3 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 4 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 5 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 6 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 7 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 8 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 9 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 10 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 11 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 12 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 13 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 14 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 15 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 16 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 17 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 18 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 19 | golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= 20 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 21 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 22 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 23 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 24 | -------------------------------------------------------------------------------- /parse.go: -------------------------------------------------------------------------------- 1 | // Package discover implements trimming of ASTs based on test coverage, 2 | // to aid in conceptualizing large code bases. 3 | // 4 | // It is based on the idea presented by Alan Shreve in his talk on 5 | // conceptualizing large software systems, held at dotGo 2015 in Paris. 6 | package discover 7 | 8 | import ( 9 | "fmt" 10 | "go/ast" 11 | "go/build" 12 | "go/parser" 13 | "go/token" 14 | "path/filepath" 15 | 16 | "golang.org/x/tools/cover" 17 | ) 18 | 19 | // Profile contains a map of statements and funcs that were covered 20 | // by the cover profiles. It supports using the information to trim 21 | // an AST down to the nodes that were actually reached. 22 | type Profile struct { 23 | Stmts map[ast.Stmt]bool 24 | Funcs map[*ast.FuncDecl]bool 25 | ImportPaths map[*ast.File]string 26 | Files []*ast.File 27 | Fset *token.FileSet 28 | } 29 | 30 | // ParseProfile parses a set of coverage profiles to produce a *Profile. 31 | func ParseProfile(profs []*cover.Profile) (*Profile, error) { 32 | profile := &Profile{ 33 | Stmts: make(map[ast.Stmt]bool), 34 | Funcs: make(map[*ast.FuncDecl]bool), 35 | ImportPaths: make(map[*ast.File]string), 36 | Fset: token.NewFileSet(), 37 | } 38 | 39 | for _, prof := range profs { 40 | file, importPath, err := findFile(prof.FileName) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | f, funcs, stmts, err := findFuncs(profile.Fset, file) 46 | if err != nil { 47 | return nil, err 48 | } 49 | profile.Files = append(profile.Files, f) 50 | profile.ImportPaths[f] = importPath 51 | 52 | blocks := prof.Blocks 53 | for len(funcs) > 0 { 54 | f := funcs[0] 55 | for i, b := range blocks { 56 | if b.StartLine > f.endLine || (b.StartLine == f.endLine && b.StartCol >= f.endCol) { 57 | // Past the end of the func 58 | funcs = funcs[1:] 59 | blocks = blocks[i:] 60 | break 61 | } 62 | if b.EndLine < f.startLine || (b.EndLine == f.startLine && b.EndCol <= f.startCol) { 63 | // Before the beginning of the func 64 | continue 65 | } 66 | if b.Count > 0 { 67 | profile.Funcs[f.decl] = true 68 | } 69 | funcs = funcs[1:] 70 | break 71 | } 72 | } 73 | 74 | blocks = prof.Blocks // reset to all blocks 75 | for len(stmts) > 0 { 76 | s := stmts[0] 77 | for i, b := range blocks { 78 | if b.StartLine > s.endLine || (b.StartLine == s.endLine && b.StartCol >= s.endCol) { 79 | // Past the end of the statement 80 | stmts = stmts[1:] 81 | blocks = blocks[i:] 82 | break 83 | } 84 | if b.EndLine < s.startLine || (b.EndLine == s.startLine && b.EndCol <= s.startCol) { 85 | // Before the beginning of the statement 86 | continue 87 | } 88 | if b.Count > 0 { 89 | profile.Stmts[s.stmt] = true 90 | } 91 | stmts = stmts[1:] 92 | break 93 | } 94 | } 95 | } 96 | 97 | return profile, nil 98 | } 99 | 100 | // findFile tries to find the full path to a file, by looking in $GOROOT 101 | // and $GOPATH. 102 | func findFile(file string) (filename, pkgPath string, err error) { 103 | dir, file := filepath.Split(file) 104 | if dir != "" { 105 | dir = dir[:len(dir)-1] // drop trailing '/' 106 | } 107 | pkg, err := build.Import(dir, ".", build.FindOnly) 108 | if err != nil { 109 | return "", "", fmt.Errorf("can't find %q: %v", file, err) 110 | } 111 | return filepath.Join(pkg.Dir, file), pkg.ImportPath, nil 112 | } 113 | 114 | // findFuncs parses the file and returns a slice of FuncExtent descriptors. 115 | func findFuncs(fset *token.FileSet, name string) (*ast.File, []*funcExtent, []*stmtExtent, error) { 116 | parsedFile, err := parser.ParseFile(fset, name, nil, parser.ParseComments) 117 | if err != nil { 118 | return nil, nil, nil, err 119 | } 120 | visitor := &funcVisitor{fset: fset} 121 | ast.Walk(visitor, parsedFile) 122 | return parsedFile, visitor.funcs, visitor.stmts, nil 123 | } 124 | 125 | // funcExtent describes a function's extent in the source by file and position. 126 | type funcExtent struct { 127 | decl *ast.FuncDecl 128 | name string 129 | startLine int 130 | startCol int 131 | endLine int 132 | endCol int 133 | } 134 | 135 | // stmtExtent describes a statement's extent in the source by file and position. 136 | type stmtExtent struct { 137 | stmt ast.Stmt 138 | startLine int 139 | startCol int 140 | endLine int 141 | endCol int 142 | } 143 | 144 | // funcVisitor implements the visitor that builds the function position list for a file. 145 | type funcVisitor struct { 146 | fset *token.FileSet 147 | funcs []*funcExtent 148 | stmts []*stmtExtent 149 | } 150 | 151 | // Visit implements the ast.Visitor interface. 152 | func (v *funcVisitor) Visit(node ast.Node) ast.Visitor { 153 | if f, ok := node.(*ast.FuncDecl); ok { 154 | start := v.fset.Position(f.Pos()) 155 | end := v.fset.Position(f.End()) 156 | fe := &funcExtent{ 157 | decl: f, 158 | startLine: start.Line, 159 | startCol: start.Column, 160 | endLine: end.Line, 161 | endCol: end.Column, 162 | } 163 | v.funcs = append(v.funcs, fe) 164 | } else if s, ok := node.(ast.Stmt); ok { 165 | start, end := v.fset.Position(s.Pos()), v.fset.Position(s.End()) 166 | se := &stmtExtent{ 167 | stmt: s, 168 | startLine: start.Line, 169 | startCol: start.Column, 170 | endLine: end.Line, 171 | endCol: end.Column, 172 | } 173 | v.stmts = append(v.stmts, se) 174 | } 175 | return v 176 | } 177 | -------------------------------------------------------------------------------- /trim.go: -------------------------------------------------------------------------------- 1 | package discover 2 | 3 | import "go/ast" 4 | 5 | // Trim trims the AST rooted at node based on the coverage profile, 6 | // removing irrelevant and unreached parts of the program. 7 | // If the node is an *ast.File, comments are updated as well using 8 | // an ast.CommentMap. 9 | func (p *Profile) Trim(node ast.Node) { 10 | if f, ok := node.(*ast.File); ok { 11 | cmap := ast.NewCommentMap(p.Fset, f, f.Comments) 12 | ast.Walk(&trimVisitor{p}, f) 13 | f.Comments = cmap.Filter(f).Comments() 14 | } else { 15 | ast.Walk(&trimVisitor{p}, node) 16 | } 17 | } 18 | 19 | // trimVisitor is an ast.Visitor that trims nodes as it walks the tree. 20 | type trimVisitor struct { 21 | p *Profile 22 | } 23 | 24 | func (v *trimVisitor) Visit(node ast.Node) ast.Visitor { 25 | var list *[]ast.Stmt 26 | switch node := node.(type) { 27 | case *ast.File: 28 | var replaced []ast.Decl 29 | for _, decl := range node.Decls { 30 | // Remove non-func declarations and funcs that were not covered 31 | if f, ok := decl.(*ast.FuncDecl); ok && v.p.Funcs[f] { 32 | replaced = append(replaced, decl) 33 | } 34 | } 35 | node.Decls = replaced 36 | 37 | // Node types containing lists of statements 38 | case *ast.BlockStmt: 39 | list = &node.List 40 | case *ast.CommClause: 41 | list = &node.Body 42 | case *ast.CaseClause: 43 | list = &node.Body 44 | } 45 | 46 | if list != nil { 47 | var replaced []ast.Stmt 48 | for _, stmt := range *list { 49 | replaced = append(replaced, v.replaceStmt(stmt)...) 50 | } 51 | 52 | *list = replaced 53 | } 54 | return v 55 | } 56 | 57 | // replaceStmt returns the (possibly many) statements that should replace 58 | // stmt. Generally a stmt is untouched or removed, but in some cases a 59 | // single stmt can result in multiple statements. This is usually only the case 60 | // when removing a block that was not taken, but pulling out function calls 61 | // that were part of the initialization of the block. 62 | func (v *trimVisitor) replaceStmt(stmt ast.Stmt) []ast.Stmt { 63 | switch stmt := stmt.(type) { 64 | case nil: 65 | return nil 66 | 67 | default: 68 | // Keep original 69 | return []ast.Stmt{stmt} 70 | 71 | case *ast.RangeStmt: 72 | if v.visited(stmt.Body) { 73 | return []ast.Stmt{stmt} 74 | } 75 | 76 | call := v.findCall(stmt.X) 77 | if call != nil { 78 | return []ast.Stmt{&ast.ExprStmt{call}} 79 | } 80 | return nil 81 | 82 | case *ast.ForStmt: 83 | if v.visited(stmt.Body) { 84 | return []ast.Stmt{stmt} 85 | } 86 | 87 | nodes := []*ast.CallExpr{ 88 | v.findCall(stmt.Init), 89 | v.findCall(stmt.Cond), 90 | v.findCall(stmt.Post), 91 | } 92 | 93 | var result []ast.Stmt 94 | for _, call := range nodes { 95 | if call != nil { 96 | result = append(result, &ast.ExprStmt{call}) 97 | } 98 | } 99 | return result 100 | 101 | case *ast.IfStmt: 102 | vIf := v.visited(stmt.Body) 103 | vElse := v.visited(stmt.Else) 104 | 105 | if !vIf { 106 | var result []ast.Stmt 107 | // If we didn't reach the body, pull out any calls from 108 | // init and cond. 109 | nodes := []*ast.CallExpr{ 110 | v.findCall(stmt.Init), 111 | v.findCall(stmt.Cond), 112 | } 113 | for _, call := range nodes { 114 | if call != nil { 115 | result = append(result, &ast.ExprStmt{call}) 116 | } 117 | } 118 | 119 | if vElse { 120 | // We reached the else; add it 121 | if block, ok := stmt.Else.(*ast.BlockStmt); ok { 122 | // For a block statement, add the statements individually 123 | // so we don't end up with an unnecessary block 124 | for _, stmt := range block.List { 125 | result = append(result, v.replaceStmt(stmt)...) 126 | } 127 | } else { 128 | result = append(result, v.replaceStmt(stmt.Else)...) 129 | } 130 | } 131 | return result 132 | } else { 133 | // We did take the if body 134 | if !vElse { 135 | // But not the else: remove it 136 | stmt.Else = nil 137 | } 138 | 139 | return []ast.Stmt{stmt} 140 | } 141 | 142 | case *ast.SelectStmt: 143 | var list []ast.Stmt 144 | for _, stmt := range stmt.Body.List { 145 | if v.visited(stmt) { 146 | list = append(list, stmt) 147 | } 148 | } 149 | stmt.Body.List = list 150 | return []ast.Stmt{stmt} 151 | 152 | case *ast.SwitchStmt: 153 | var list []ast.Stmt 154 | for _, stmt := range stmt.Body.List { 155 | if v.visitedAndMatters(stmt) { 156 | list = append(list, stmt) 157 | } 158 | } 159 | 160 | // If we didn't visit any case clauses, don't add the select at all. 161 | if len(list) == 0 { 162 | return nil 163 | } else { 164 | stmt.Body.List = list 165 | return []ast.Stmt{stmt} 166 | } 167 | 168 | case *ast.TypeSwitchStmt: 169 | var list []ast.Stmt 170 | for _, stmt := range stmt.Body.List { 171 | if v.visitedAndMatters(stmt) { 172 | list = append(list, stmt) 173 | } 174 | } 175 | 176 | // If we didn't visit any case clauses, don't add the select at all. 177 | if len(list) == 0 { 178 | return nil 179 | } else { 180 | stmt.Body.List = list 181 | return []ast.Stmt{stmt} 182 | } 183 | } 184 | } 185 | 186 | // visited is a helper function to return whether or not a statement 187 | // was visited. If stmt is nil, visited returns false. 188 | func (v *trimVisitor) visited(stmt ast.Stmt) bool { 189 | if stmt == nil { // for convenience with e.g. IfStmt.Else 190 | return false 191 | } 192 | return v.p.Stmts[stmt] 193 | } 194 | 195 | // visitedAndMatters is like visited, but also checks that the statement 196 | // has any effect. For example, an empty block has no effect and thus 197 | // is considered to not matter, even though it may have been visited. 198 | func (v *trimVisitor) visitedAndMatters(stmt ast.Stmt) bool { 199 | if !v.visited(stmt) { 200 | return false 201 | } 202 | 203 | switch stmt := stmt.(type) { 204 | default: 205 | // By default, statements matter 206 | return true 207 | 208 | case *ast.EmptyStmt: 209 | // Empty statements do not matter 210 | return false 211 | 212 | case *ast.BlockStmt: 213 | // Blocks matter if and only if any of the containing statements 214 | // matter. 215 | for _, stmt := range stmt.List { 216 | if v.visitedAndMatters(stmt) { 217 | return true 218 | } 219 | } 220 | return false 221 | 222 | case *ast.CaseClause: 223 | for _, stmt := range stmt.Body { 224 | if v.visitedAndMatters(stmt) { 225 | return true 226 | } 227 | } 228 | return false 229 | } 230 | } 231 | 232 | // findCall returns the first *ast.CallExpr encountered within the tree 233 | // rooted at node, or nil if no CallExpr was found. This is useful for 234 | // "pulling out" calls out of a statement or expression. 235 | func (v *trimVisitor) findCall(node ast.Node) *ast.CallExpr { 236 | if node == nil { // for convenience 237 | return nil 238 | } 239 | 240 | var call *ast.CallExpr 241 | ast.Inspect(node, func(n ast.Node) bool { 242 | if call != nil { 243 | return false 244 | } 245 | c, ok := n.(*ast.CallExpr) 246 | if ok { 247 | call = c 248 | return false 249 | } 250 | return true 251 | }) 252 | return call 253 | } 254 | --------------------------------------------------------------------------------