├── LICENSE ├── README.md ├── equal.go ├── examples.go ├── go.mod ├── main.go ├── stats.go └── try.go /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 The Go Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## tryhard command 2 | 3 | `tryhard` is a simple tool to list and rewrite `try` candidate statements. 4 | It operates on a file-by-file basis and does not type-check the code; 5 | potential statements are recognized and rewritten purely based on pattern 6 | matching. `tryhard` may report (a usually very small number of) false positives; 7 | the likelyhood for false positives may be lowered by providing the flag `-err="err"`. 8 | It also undercounts `try` candidates because `tryhard` does not recognize 9 | opportunities for shared, deferred handlers. 10 | Use caution when using the rewrite feature (`-r` flag) and have a backup as needed. 11 | 12 | The command accepts a list of files or directories which it processes in the order 13 | they are provided. Given a file, it operates on that file no matter the file path. 14 | Given a directory, it operates on all `.go` files in that directory, recursively. 15 | Files starting with a period are ignored. Files may be explicitly ignored with 16 | the `-ignore` flag. 17 | 18 | `tryhard` considers each top-level function with a last result of named type `error`, 19 | which may or may not be the predefined type `error`. Inside these functions `tryhard` 20 | considers assignments of the form 21 | 22 | ```Go 23 | v1, v2, ..., vn, = f() // can also be := instead of = 24 | ``` 25 | 26 | followed by an `if` statement of the form 27 | 28 | ```Go 29 | if != nil { 30 | return ..., 31 | } 32 | ``` 33 | 34 | or an `if` statement with an init expression matching the above assignment. The 35 | error variable may have any name, unless specified explicitly with the 36 | `-err` flag; the variable may or may not be of type `error` or correspond to the 37 | result error. The return statement must contain one or more return expressions, 38 | with all but the last one denoting a zero value of sorts (a zero number literal, 39 | an empty string, an empty composite literal, etc.). The last result must be the 40 | variable . 41 | 42 | Unless no files were found, `tryhard` reports various counts at the end of its run. 43 | 44 | **CAUTION**: If the rewrite flag (`-r`) is specified, the file is updated in place! 45 | Make sure you can revert to the original. 46 | 47 | Function literals (closures) are currently ignored. 48 | 49 | ### Usage: 50 | ``` 51 | tryhard [flags] [path ...] 52 | ``` 53 | 54 | The flags are: 55 | ``` 56 | -l 57 | list positions of potential `try` candidate statements 58 | -r 59 | rewrite potential `try` candidate statements to use `try` 60 | -err 61 | name of error variable; using "" permits any name (default `""`) 62 | -ignore string 63 | ignore files with paths matching this (regexp) pattern (default `"vendor"`) 64 | ``` 65 | -------------------------------------------------------------------------------- /equal.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import "go/ast" 8 | 9 | // TODO(gri) A more general version of this code should 10 | // probably be in go/ast at some point. 11 | 12 | // equal reports whether x and y are equal; 13 | // i.e., whether they describe the same Go expression. 14 | // equal is conservative and report false for cases 15 | // that it ignores. 16 | func equal(x, y ast.Expr) bool { 17 | // We cannot use reflect.DeepEqual because the go/ast also encodes position 18 | // and comment information which we need to ignore. 19 | // We could use printing and compare the strings but the code here is simple 20 | // and straight-forward; and this is much faster than printing. 21 | // (switch body framework generated with: https://play.golang.org/p/zMt7U03s0ys) 22 | switch x := x.(type) { 23 | case nil: 24 | if y == nil { 25 | return true 26 | } 27 | case *ast.Ident: 28 | if y, ok := y.(*ast.Ident); ok { 29 | return x.Name == y.Name 30 | } 31 | case *ast.Ellipsis: 32 | if y, ok := y.(*ast.Ellipsis); ok { 33 | return equal(x.Elt, y.Elt) 34 | } 35 | case *ast.BasicLit: 36 | if y, ok := y.(*ast.BasicLit); ok { 37 | return x.Kind == y.Kind && x.Value == y.Value // this is overly conservative 38 | } 39 | case *ast.FuncLit: 40 | // ignored for now (we don't have the code for comparing statements) 41 | case *ast.CompositeLit: 42 | if y, ok := y.(*ast.CompositeLit); ok { 43 | return !x.Incomplete && !y.Incomplete && equal(x.Type, y.Type) && equalLists(x.Elts, y.Elts) 44 | } 45 | case *ast.ParenExpr: 46 | if y, ok := y.(*ast.ParenExpr); ok { 47 | return equal(x.X, y.X) 48 | } 49 | case *ast.SelectorExpr: 50 | if y, ok := y.(*ast.SelectorExpr); ok { 51 | return equal(x.X, y.X) && equal(x.Sel, y.Sel) 52 | } 53 | case *ast.IndexExpr: 54 | if y, ok := y.(*ast.IndexExpr); ok { 55 | return equal(x.X, y.X) && equal(x.Index, y.Index) 56 | } 57 | case *ast.SliceExpr: 58 | if y, ok := y.(*ast.SliceExpr); ok { 59 | return equal(x.X, y.X) && 60 | equal(x.Low, y.Low) && equal(x.High, y.High) && equal(x.Max, y.Max) && 61 | x.Slice3 == y.Slice3 62 | } 63 | case *ast.TypeAssertExpr: 64 | if y, ok := y.(*ast.TypeAssertExpr); ok { 65 | return equal(x.X, y.X) && equal(x.Type, y.Type) 66 | } 67 | case *ast.CallExpr: 68 | if y, ok := y.(*ast.CallExpr); ok { 69 | return equal(x.Fun, y.Fun) && equalLists(x.Args, y.Args) && x.Ellipsis == y.Ellipsis 70 | } 71 | case *ast.StarExpr: 72 | if y, ok := y.(*ast.StarExpr); ok { 73 | return equal(x.X, y.X) 74 | } 75 | case *ast.UnaryExpr: 76 | if y, ok := y.(*ast.UnaryExpr); ok { 77 | return x.Op == y.Op && equal(x.X, y.X) 78 | } 79 | case *ast.BinaryExpr: 80 | if y, ok := y.(*ast.BinaryExpr); ok { 81 | return equal(x.X, y.X) && x.Op == y.Op && equal(x.Y, y.Y) 82 | } 83 | case *ast.KeyValueExpr: 84 | if y, ok := y.(*ast.KeyValueExpr); ok { 85 | return equal(x.Key, y.Key) && equal(x.Value, y.Value) 86 | } 87 | case *ast.ArrayType: 88 | if y, ok := y.(*ast.ArrayType); ok { 89 | return equal(x.Len, y.Len) && equal(x.Elt, y.Elt) 90 | } 91 | case *ast.StructType: 92 | if y, ok := y.(*ast.StructType); ok { 93 | return !x.Incomplete && !y.Incomplete && equalFields(x.Fields, y.Fields) 94 | } 95 | case *ast.FuncType: 96 | if y, ok := y.(*ast.FuncType); ok { 97 | return equalFields(x.Params, y.Params) && equalFields(x.Results, y.Results) 98 | } 99 | case *ast.InterfaceType: 100 | if y, ok := y.(*ast.InterfaceType); ok { 101 | return !x.Incomplete && !y.Incomplete && equalFields(x.Methods, y.Methods) 102 | } 103 | case *ast.MapType: 104 | if y, ok := y.(*ast.MapType); ok { 105 | return equal(x.Key, y.Key) && equal(x.Value, y.Value) 106 | } 107 | case *ast.ChanType: 108 | if y, ok := y.(*ast.ChanType); ok { 109 | return x.Dir == y.Dir && equal(x.Value, y.Value) 110 | } 111 | } 112 | return false 113 | } 114 | 115 | func equalLists(x, y []ast.Expr) bool { 116 | if len(x) != len(y) { 117 | return false 118 | } 119 | for i, x := range x { 120 | y := y[i] 121 | if !equal(x, y) { 122 | return false 123 | } 124 | } 125 | return true 126 | } 127 | 128 | func equalFields(x, y *ast.FieldList) bool { 129 | if x == nil || y == nil { 130 | return x == y 131 | } 132 | if len(x.List) != len(y.List) { 133 | return false 134 | } 135 | for i, x := range x.List { 136 | y := y.List[i] 137 | if !equalIdents(x.Names, y.Names) || !equal(x.Type, y.Type) || !equal(x.Tag, y.Tag) { 138 | return false 139 | } 140 | } 141 | return true 142 | } 143 | 144 | func equalIdents(x, y []*ast.Ident) bool { 145 | if len(x) != len(y) { 146 | return false 147 | } 148 | for i, x := range x { 149 | y := y[i] 150 | if !equal(x, y) { 151 | return false 152 | } 153 | } 154 | return true 155 | } 156 | -------------------------------------------------------------------------------- /examples.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build ignore 6 | 7 | // This file contains code patterns where `try` might 8 | // be effective. 9 | // TODO(gri) expand and use as actual tests for `try`. 10 | 11 | package p 12 | 13 | func f() error { 14 | return nil 15 | } 16 | 17 | type myError struct { 18 | error 19 | } 20 | 21 | func sharedReturnExpr() error { 22 | if err := f(); err != nil { 23 | return myError{err} 24 | } 25 | if err := f(); err != nil { 26 | return myError{err} 27 | } 28 | } 29 | 30 | func notSharedReturnExpr() error { 31 | if err := f(); err != nil { 32 | return myError{err} 33 | } 34 | if err := f(); err != nil { 35 | return myError{err} 36 | } 37 | if err := f(); err != nil { 38 | return err 39 | } 40 | } 41 | 42 | // f uses a function literal to annotate errors uniformly 43 | func f(arg int) error { 44 | report := func(err error) error { return fmt.Errorf("f failed for %v: %v", arg, err) } 45 | 46 | err := g(arg) 47 | if err != nil { 48 | return report(err) 49 | } 50 | 51 | err = h(arg) 52 | if err != nil { 53 | return report(err) 54 | } 55 | return nil 56 | } 57 | 58 | // g uses repeated code (e.g., copy/paste) to annotate errors uniformly 59 | func g(arg int) error { 60 | err := h(arg) 61 | if err != nil { 62 | return fmt.Errorf("g failed for %v: %v", arg, err) 63 | } 64 | 65 | err = f(arg) 66 | if err != nil { 67 | return fmt.Errorf("g failed for %v: %v", arg, err) 68 | } 69 | return nil 70 | } 71 | 72 | func h(arg int) error { 73 | return nil 74 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/griesemer/tryhard 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // `tryhard` is a simple tool to list and rewrite `try` candidate statements. 6 | // See README.md for details. 7 | package main 8 | 9 | import ( 10 | "bytes" 11 | "flag" 12 | "fmt" 13 | "go/format" 14 | "go/parser" 15 | "go/scanner" 16 | "go/token" 17 | "io/ioutil" 18 | "os" 19 | "path/filepath" 20 | "regexp" 21 | "runtime" 22 | "strings" 23 | ) 24 | 25 | var ( 26 | // main operation modes 27 | list = flag.Bool("l", false, "list positions of potential `try` candidate statements") 28 | rewrite = flag.Bool("r", false, "rewrite potential `try` candidate statements to use `try`") 29 | 30 | // customization 31 | varname = flag.String("err", "", `name of error variable; using "" permits any name`) 32 | filter = flag.String("ignore", "vendor", "ignore files with paths matching this (regexp) pattern") 33 | ) 34 | 35 | var ( 36 | fset = token.NewFileSet() 37 | exitCode int 38 | fileCount int 39 | filterRx *regexp.Regexp 40 | ) 41 | 42 | func report(err error) { 43 | scanner.PrintError(os.Stderr, err) 44 | exitCode = 2 45 | } 46 | 47 | func usage() { 48 | fmt.Fprintf(os.Stderr, "usage: tryhard [flags] [path ...]\n") 49 | flag.PrintDefaults() 50 | } 51 | 52 | func main() { 53 | flag.Usage = usage 54 | flag.Parse() 55 | 56 | if *filter != "" { 57 | rx, err := regexp.Compile(*filter) 58 | if err != nil { 59 | report(err) 60 | os.Exit(exitCode) 61 | } 62 | filterRx = rx 63 | } 64 | 65 | for i := 0; i < flag.NArg(); i++ { 66 | path := flag.Arg(i) 67 | switch dir, err := os.Stat(path); { 68 | case err != nil: 69 | report(err) 70 | case dir.IsDir(): 71 | filepath.Walk(path, visitFile) 72 | default: 73 | if err := processFile(path); err != nil { 74 | report(err) 75 | } 76 | } 77 | } 78 | 79 | if fileCount > 0 { 80 | if *list { 81 | reportPositions() 82 | } 83 | reportCounts() 84 | } 85 | 86 | os.Exit(exitCode) 87 | } 88 | 89 | func processFile(filename string) error { 90 | fileCount++ 91 | 92 | var perm os.FileMode = 0644 93 | f, err := os.Open(filename) 94 | if err != nil { 95 | return err 96 | } 97 | defer f.Close() 98 | fi, err := f.Stat() 99 | if err != nil { 100 | return err 101 | } 102 | perm = fi.Mode().Perm() 103 | 104 | src, err := ioutil.ReadAll(f) 105 | if err != nil { 106 | return err 107 | } 108 | 109 | file, err := parser.ParseFile(fset, filename, src, parser.ParseComments) 110 | if err != nil { 111 | return err 112 | } 113 | 114 | modified := false 115 | tryFile(file, &modified) 116 | if !modified || !*rewrite { 117 | return nil 118 | } 119 | 120 | var buf bytes.Buffer 121 | if err := format.Node(&buf, fset, file); err != nil { 122 | return err 123 | } 124 | res := buf.Bytes() 125 | 126 | // make a temporary backup before overwriting original 127 | bakname, err := backupFile(filename+".", src, perm) 128 | if err != nil { 129 | return err 130 | } 131 | err = ioutil.WriteFile(filename, res, perm) 132 | if err != nil { 133 | os.Rename(bakname, filename) 134 | return err 135 | } 136 | return os.Remove(bakname) 137 | } 138 | 139 | func visitFile(path string, f os.FileInfo, err error) error { 140 | if err == nil && !excluded(path) && isGoFile(f) { 141 | err = processFile(path) 142 | } 143 | // Don't complain if a file was deleted in the meantime (i.e. 144 | // the directory changed concurrently while running tryhard). 145 | if err != nil && !os.IsNotExist(err) { 146 | report(err) 147 | } 148 | return nil 149 | } 150 | 151 | func excluded(path string) bool { 152 | return filterRx != nil && filterRx.MatchString(path) 153 | } 154 | 155 | func isGoFile(f os.FileInfo) bool { 156 | // ignore non-Go files 157 | name := f.Name() 158 | return !f.IsDir() && !strings.HasPrefix(name, ".") && strings.HasSuffix(name, ".go") 159 | } 160 | 161 | const chmodSupported = runtime.GOOS != "windows" 162 | 163 | // backupFile writes data to a new file named filename with permissions perm, 164 | // with != nil statements", 40 | TryCand: "try candidates", 41 | NonErrName: ` name is different from "err"`, 42 | 43 | // non-try candidates 44 | ReturnExpr: "{ return ... zero values ..., expr }", 45 | SharedLast: "possibly try/defer candidate", 46 | SingleStmt: "single statement then branch", 47 | ComplexBlock: "complex then branch; cannot use try", 48 | HasElse: "non-empty else branch; cannot use try", 49 | } 50 | 51 | var counts [numKinds]int 52 | var posLists [numKinds][]token.Pos 53 | 54 | // count adds 1 to the number of nodes categorized as k. 55 | // Counts are reported with reportCounts. 56 | // If n != nil, count appends that node's position to the 57 | // list of positions categorized as k. Collected positions 58 | // are reported with reportPositions. 59 | func count(k Kind, n ast.Node) { 60 | counts[k]++ 61 | if n != nil { 62 | posLists[k] = append(posLists[k], n.Pos()) 63 | } 64 | } 65 | 66 | func reportCounts() { 67 | fmt.Println("--- stats ---") 68 | reportCount(Func, Func) 69 | reportCount(FuncError, Func) 70 | reportCount(Stmt, Stmt) 71 | reportCount(If, Stmt) 72 | reportCount(IfErr, If) 73 | reportCount(TryCand, IfErr) 74 | reportCount(NonErrName, IfErr) 75 | 76 | help := "" 77 | if !*list { 78 | help = " (-l flag lists file positions)" 79 | } 80 | fmt.Printf("--- non-try candidates%s ---\n", help) 81 | reportCount(ReturnExpr, IfErr) 82 | reportCount(SharedLast, ReturnExpr) 83 | reportCount(SingleStmt, IfErr) 84 | reportCount(ComplexBlock, IfErr) 85 | reportCount(HasElse, IfErr) 86 | } 87 | 88 | func reportCount(k, ofk Kind) { 89 | x := counts[k] 90 | total := counts[ofk] 91 | // don't crash if total == 0 92 | p := 100.0 // percentage 93 | if total != 0 { 94 | p = float64(x) * 100 / float64(total) 95 | } 96 | fmt.Printf("% 7d (%5.1f%% of % 7d) %s\n", x, p, total, kindDesc[k]) 97 | } 98 | 99 | func reportPositions() { 100 | for k, list := range posLists { 101 | if len(list) == 0 { 102 | continue 103 | } 104 | fmt.Printf("--- %s ---\n", kindDesc[k]) 105 | for i, pos := range list { 106 | p := fset.Position(pos) 107 | fmt.Printf("% 7d %s:%d\n", i+1, p.Filename, p.Line) 108 | } 109 | fmt.Println() 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /try.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "go/ast" 9 | "go/token" 10 | "strconv" 11 | ) 12 | 13 | type funcInfo struct { 14 | modified bool // indicates whether the function body was modified 15 | sharedLast []ast.Expr // if != nil { return ... zero ..., last } always the same; valid if != nil 16 | } 17 | 18 | // tryFile identifies statements in f that are potential candidates for `try`, 19 | // lists their positions (-l flag), or rewrites them in place using `try` (-r flag) 20 | // and sets *modified to true. 21 | func tryFile(f *ast.File, modified *bool) { 22 | for _, d := range f.Decls { 23 | if f, ok := d.(*ast.FuncDecl); ok { 24 | count(Func, nil) 25 | if hasErrorResult(f.Type) && f.Body != nil { 26 | count(FuncError, nil) 27 | 28 | fi := funcInfo{false, []ast.Expr{} /* mark as valid but empty */} 29 | fi.tryBlock(f.Body) 30 | if fi.modified { 31 | *modified = true 32 | } 33 | 34 | if len(fi.sharedLast) > 1 { 35 | // Return statements in `if != nil` statements for 36 | // `try` candidates share the same last expression. This 37 | // is an indicator that deferred handling of that expression 38 | // may be possible if there are no other error returns. 39 | for _, last := range fi.sharedLast { 40 | count(SharedLast, last) 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | 48 | // tryBlock is like tryFile but operates on a block b. 49 | func (fi *funcInfo) tryBlock(b *ast.BlockStmt) { 50 | dirty := false // if set, b.List contains nil entries 51 | var p ast.Stmt // previous statement 52 | for i, s := range b.List { 53 | count(Stmt, nil) 54 | switch s := s.(type) { 55 | case *ast.BlockStmt: 56 | fi.tryBlock(s) 57 | case *ast.ForStmt: 58 | fi.tryBlock(s.Body) 59 | case *ast.RangeStmt: 60 | fi.tryBlock(s.Body) 61 | case *ast.SelectStmt: 62 | fi.tryBlock(s.Body) 63 | case *ast.SwitchStmt: 64 | fi.tryBlock(s.Body) 65 | case *ast.TypeSwitchStmt: 66 | fi.tryBlock(s.Body) 67 | case *ast.IfStmt: 68 | count(If, nil) 69 | fi.tryBlock(s.Body) 70 | if s, ok := s.Else.(*ast.BlockStmt); ok { 71 | fi.tryBlock(s) 72 | } 73 | 74 | // condition must be of the form: != nil 75 | // where stands for the error variable name 76 | errname := *varname 77 | if !isErrTest(s.Cond, &errname) { 78 | break 79 | } 80 | count(IfErr, nil) 81 | 82 | if s.Init == nil && isErrAssign(p, errname) && fi.isTryHandler(s, errname) { 83 | // ..., := 84 | // if != nil { 85 | // return ... zeroes ..., 86 | // } 87 | count(TryCand, s) 88 | if errname != "err" { 89 | count(NonErrName, s.Cond) 90 | } 91 | if *rewrite { 92 | b.List[i-1] = rewriteAssign(p, s.End()) 93 | b.List[i] = nil // remove `if` 94 | dirty = true 95 | fi.modified = true 96 | } 97 | } else if isErrAssign(s.Init, errname) && fi.isTryHandler(s, errname) { 98 | // if ..., := ; != nil { 99 | // return ... zeroes ..., 100 | // } 101 | count(TryCand, s) 102 | if errname != "err" { 103 | count(NonErrName, s.Cond) 104 | } 105 | if *rewrite { 106 | b.List[i] = rewriteAssign(s.Init, s.End()) 107 | fi.modified = true 108 | } 109 | } 110 | } 111 | p = s 112 | } 113 | 114 | if dirty { 115 | i := 0 116 | for _, s := range b.List { 117 | if s != nil { 118 | b.List[i] = s 119 | i++ 120 | } 121 | } 122 | b.List = b.List[:i] 123 | } 124 | } 125 | 126 | func (fi *funcInfo) isTryHandler(s *ast.IfStmt, errname string) bool { 127 | ok := true 128 | 129 | // then block must be of the form: return ... zero values ..., last (or just: return) 130 | isRet, last := isReturn(s.Body) 131 | if !isRet { 132 | ok = false 133 | // distinguish between single-statement and more complex then blocks 134 | k := SingleStmt 135 | if len(s.Body.List) > 1 { 136 | k = ComplexBlock 137 | } 138 | count(k, s.Body) 139 | } 140 | 141 | // last must be , if present 142 | if last != nil && !isName(last, errname) { 143 | ok = false 144 | count(ReturnExpr, s.Body) 145 | if fi.sharedLast != nil { 146 | if len(fi.sharedLast) == 0 || equal(last, fi.sharedLast[0]) { 147 | fi.sharedLast = append(fi.sharedLast, last) 148 | } else { 149 | fi.sharedLast = nil // invalidate 150 | } 151 | } 152 | } else { 153 | fi.sharedLast = nil // invalidate 154 | } 155 | 156 | // else block must be absent 157 | if s.Else != nil { 158 | ok = false 159 | count(HasElse, s.Else) 160 | } 161 | 162 | return ok 163 | } 164 | 165 | // rewriteAssign assumes that s is an assignment that is a potential candidate 166 | // for `try` and rewrites it accordingly. It returns the new assignment (or the 167 | // assignment's rhs if there's no lhs anymore). 168 | func rewriteAssign(s ast.Stmt, end token.Pos) ast.Stmt { 169 | a := s.(*ast.AssignStmt) 170 | lhs := a.Lhs[:len(a.Lhs)-1] 171 | rhs := a.Rhs[0] 172 | pos := rhs.Pos() 173 | rhs0 := &ast.CallExpr{Fun: &ast.Ident{NamePos: pos, Name: "try"}, Lparen: pos, Args: []ast.Expr{a.Rhs[0]}, Rparen: end} 174 | if isBlanks(lhs) { 175 | // no lhs anymore - no need for assignment 176 | return &ast.ExprStmt{X: rhs0} 177 | } 178 | a.Lhs = lhs 179 | a.Rhs[0] = rhs0 180 | return a 181 | } 182 | 183 | // isBlanks reports whether list is empty or contains only blank identifiers. 184 | func isBlanks(list []ast.Expr) bool { 185 | for _, x := range list { 186 | if x, ok := x.(*ast.Ident); !ok || x.Name != "_" { 187 | return false 188 | } 189 | } 190 | return true 191 | } 192 | 193 | // asErrAssign reports whether s is an assignment statement of the form: 194 | // 195 | // v1, v2, ... vn, = f() 196 | // v1, v2, ... vn, := f() 197 | // 198 | // where the vi are arbitrary expressions or variables (n may also be 0), 199 | // is the variable errname, and f() stands for a function call. 200 | func isErrAssign(s ast.Stmt, errname string) bool { 201 | a, ok := s.(*ast.AssignStmt) 202 | if !ok || a.Tok != token.ASSIGN && a.Tok != token.DEFINE { 203 | return false 204 | } 205 | return len(a.Lhs) > 0 && isName(a.Lhs[len(a.Lhs)-1], errname) && len(a.Rhs) == 1 && isCall(a.Rhs[0]) 206 | } 207 | 208 | // isCall reports whether x is a (function) call. 209 | // (A conversion may appear as a false positive). 210 | func isCall(x ast.Expr) bool { 211 | _, ok := x.(*ast.CallExpr) 212 | return ok 213 | } 214 | 215 | // isErrTest reports whether x is a the binary operation " != nil", 216 | // where stands for the name of the error variable. If *errname is 217 | // the empty string, may have any name, and *errname is set to it. 218 | // Otherwise, must be *errname. 219 | func isErrTest(x ast.Expr, errname *string) bool { 220 | c, ok := x.(*ast.BinaryExpr) 221 | if ok && c.Op == token.NEQ && isName(c.Y, "nil") { 222 | if errv, ok := c.X.(*ast.Ident); ok { 223 | if *errname == "" { 224 | *errname = errv.Name 225 | return true 226 | } 227 | return errv.Name == *errname 228 | } 229 | } 230 | return false 231 | } 232 | 233 | // reports whether b contains a single return statement 234 | // that is either naked, or that returns all zero values 235 | // followed by a last expression (which is not further 236 | // restricted); last is nil for naked returns. 237 | func isReturn(b *ast.BlockStmt) (ok bool, last ast.Expr) { 238 | if len(b.List) != 1 { 239 | return 240 | } 241 | ret, _ := b.List[0].(*ast.ReturnStmt) 242 | if ret == nil { 243 | return 244 | } 245 | if n := len(ret.Results); n > 0 { 246 | for _, x := range ret.Results[:n-1] { 247 | if !isZero(x) { 248 | return 249 | } 250 | } 251 | return true, ret.Results[n-1] 252 | } 253 | return true, nil 254 | } 255 | 256 | // hasErrorResult reports whether sig has a final result with type name "error". 257 | func hasErrorResult(sig *ast.FuncType) bool { 258 | res := sig.Results 259 | if res == nil || len(res.List) == 0 { 260 | return false // no results 261 | } 262 | last := res.List[len(res.List)-1].Type 263 | return isName(last, "error") 264 | } 265 | 266 | // isZero reports whether x is a zero value. 267 | func isZero(x ast.Expr) bool { 268 | switch x := x.(type) { 269 | case *ast.Ident: 270 | return x.Name == "nil" 271 | case *ast.BasicLit: 272 | v := x.Value 273 | if len(v) == 0 { 274 | return false 275 | } 276 | switch x.Kind { 277 | case token.INT: 278 | z, err := strconv.ParseInt(v, 0, 64) 279 | return err == nil && z == 0 280 | case token.FLOAT: 281 | z, err := strconv.ParseFloat(v, 64) 282 | return err == nil && z == 0 283 | case token.IMAG: 284 | z, err := strconv.ParseFloat(v[:len(v)-1], 64) 285 | return err == nil && z == 0 286 | case token.CHAR: 287 | return v == "0" // there are more cases here 288 | case token.STRING: 289 | return v == `""` || v == "``" 290 | } 291 | case *ast.CompositeLit: 292 | return len(x.Elts) == 0 293 | } 294 | return false 295 | } 296 | 297 | // isName reports whether x is an identifier with the given name. 298 | func isName(x ast.Expr, name string) bool { 299 | id, ok := x.(*ast.Ident) 300 | return ok && id.Name == name 301 | } 302 | --------------------------------------------------------------------------------