├── LICENSE ├── README.md ├── docs └── expanderr.svg ├── expanderr.el ├── expanderr.go ├── expanderr_test.go ├── internal └── srcimporter │ └── srcimporter.go ├── lisp └── go-expanderr.el ├── pos.go ├── screencast.gif ├── screencast.mp4 └── testdata ├── comment.got └── src │ └── comment │ └── comment.go ├── comment.want └── src │ └── comment │ └── comment.go ├── commentinline.got └── src │ └── commentinline │ └── commentinline.go ├── commentinline.want └── src │ └── commentinline │ └── commentinline.go ├── customtypes.got └── src │ └── customtypes │ └── customtypes.go ├── customtypes.want └── src │ └── customtypes │ └── customtypes.go ├── functionliteral.got └── src │ └── functionliteral │ └── functionliteral.go ├── functionliteral.want └── src │ └── functionliteral │ └── functionliteral.go ├── introduceerr.got └── src │ └── introduceerr │ └── introduceerr.go ├── introduceerr.want └── src │ └── introduceerr │ └── introduceerr.go ├── multipkg.got └── src │ ├── lib │ └── lib.go │ └── multipkg │ └── multipkg.go ├── multipkg.want └── src │ └── multipkg │ └── multipkg.go ├── multipkgvendor.got └── src │ └── multipkg │ ├── multipkg.go │ └── vendor │ └── lib │ └── lib.go ├── multipkgvendor.want └── src │ └── multipkg │ └── multipkg.go ├── nocalleereturn.got └── src │ └── nocalleereturn │ └── nocalleereturn.go ├── nocalleereturn.want └── src │ └── nocalleereturn │ └── nocalleereturn.go ├── noerrreturn.got └── src │ └── noerrreturn │ └── noerrreturn.go ├── noerrreturn.want └── src │ └── noerrreturn │ └── noerrreturn.go ├── nointroduce.got └── src │ └── nointroduce │ └── nointroduce.go ├── nointroduce.want └── src │ └── nointroduce │ └── nointroduce.go ├── noreturncaller.got └── src │ └── noreturncaller │ └── noreturncaller.go ├── noreturncaller.want └── src │ └── noreturncaller │ └── noreturncaller.go ├── pkg.got └── src │ └── pkg │ ├── pkg1.go │ └── pkg2.go ├── pkg.want └── src │ └── pkg │ └── pkg2.go ├── presentdouble.got └── src │ └── presentdouble │ └── presentdouble.go ├── presentdouble.want └── src │ └── presentdouble │ └── presentdouble.go ├── presentsingle.got └── src │ └── presentsingle │ └── presentsingle.go ├── presentsingle.want └── src │ └── presentsingle │ └── presentsingle.go ├── returnerrcall.got └── src │ └── returnerrcall │ └── returnerrcall.go ├── returnerrcall.want └── src │ └── returnerrcall │ └── returnerrcall.go ├── singleerror.got └── src │ └── singleerror │ └── singleerror.go ├── singleerror.want └── src │ └── singleerror │ └── singleerror.go ├── underscore.got └── src │ └── underscore │ └── underscore.go ├── underscore.want └── src │ └── underscore │ └── underscore.go ├── varanderror.got └── src │ └── varanderror │ └── varanderror.go └── varanderror.want └── src └── varanderror └── varanderror.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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # expanderr 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/stapelberg/expanderr)](https://goreportcard.com/report/github.com/stapelberg/expanderr) 4 | 5 | expanderr logo 6 | 7 | The expanderr (think “expander”, pronounced with a pirate accent) is a tool 8 | which expands the Go Call Expression under your cursor to check errors. As an 9 | example, assuming your cursor is positioned on this call expression: 10 | 11 | ```go 12 | os.Remove("/tmp/state.bin") 13 | ``` 14 | 15 | …invoking the expanderr will leave you with this If Statement instead: 16 | 17 | ```go 18 | if err := os.Remove("/tmp/state.bin"); err != nil { 19 | return err 20 | } 21 | ``` 22 | 23 | Of course, the return values match the enclosing function signature, functions 24 | returning more than one argument are supported, and the local scope is 25 | considered to ensure that your code still compiles. 26 | 27 | ![screencast](screencast.gif) 28 | 29 | ## Setup 30 | 31 | Start by running `go get -u github.com/stapelberg/expanderr`. Then, follow the 32 | section for the editor you use: 33 | 34 | ### Emacs 35 | 36 | Add `(load "~/go/src/github.com/stapelberg/expanderr/lisp/go-expanderr.el")` to your Emacs configuration. 37 | 38 | From now on, use `C-c C-e` to invoke the expanderr. 39 | 40 | ## Opportunities to contribute 41 | 42 | * [vim integration](https://github.com/stapelberg/expanderr/issues/1) 43 | * use log.Fatal if within main() 44 | * integration for your favorite editor 45 | * [investigate support for the errors package](https://github.com/stapelberg/expanderr/issues/8) 46 | 47 | ## How does this differ from goreturns? 48 | 49 | goreturns only inserts a return statement with zero values for the current 50 | function. 51 | 52 | expanderr understands the signature of the call expression under your cursor and 53 | inserts the appropriate error checking statement (including a return 54 | statement). In practice, this eliminates the need of combining goreturns with an 55 | editor snippet, with the additional bonus of working correctly in a larger 56 | number of situations. 57 | -------------------------------------------------------------------------------- /docs/expanderr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 13 | 57 | 58 | 91 | 112 | 129 | 130 | 133 | 142 | 143 | 144 | 145 | 154 | 168 | 181 | 199 | 208 | 222 | 235 | 253 | 254 | 255 | 272 | 273 | 291 | 306 | 308 | 310 | 313 | 317 | 321 | 325 | 327 | 330 | 335 | 339 | 343 | 344 | 351 | 352 | 356 | 357 | 358 | 360 | 361 | 365 | 391 | 434 | 435 | 436 | 461 | 467 | 472 | 475 | 478 | 492 | 493 | 494 | 506 | 507 | 508 | 513 | 517 | 524 | 527 | 528 | 529 | 538 | 539 | 540 | 547 | 548 | 549 | -------------------------------------------------------------------------------- /expanderr.el: -------------------------------------------------------------------------------- 1 | ;; this file provides backwards compatibilty with previously documented way of 2 | ;; loading this libray in the readme: 3 | ;; (load "~/go/src/github.com/stapelberg/expanderr/expanderr.el") 4 | (load-file (expand-file-name "lisp/go-expanderr.el" (file-name-directory load-file-name))) 5 | -------------------------------------------------------------------------------- /expanderr.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go Authors. All rights reserved. 2 | // Copyright 2014 The Go Authors. All rights reserved. 3 | // Copyright 2017 The Go Authors. All rights reserved. 4 | // Use of this source code is governed by a BSD-style 5 | // license that can be found in the LICENSE file. 6 | 7 | package main 8 | 9 | import ( 10 | "bytes" 11 | "encoding/json" 12 | "errors" 13 | "flag" 14 | "fmt" 15 | "go/ast" 16 | "go/build" 17 | "go/format" 18 | "go/importer" 19 | "go/parser" 20 | "go/token" 21 | "go/types" 22 | "io" 23 | "io/ioutil" 24 | "log" 25 | "os" 26 | "path/filepath" 27 | "runtime/pprof" 28 | "strings" 29 | "unicode" 30 | 31 | "github.com/stapelberg/expanderr/internal/srcimporter" 32 | 33 | "golang.org/x/tools/go/ast/astutil" 34 | ) 35 | 36 | var ( 37 | unsafeFastImporter = flag.Bool("unsafe_fast_importer", 38 | false, 39 | "import installed packages when possible (unsafe until golang.org/issues/19337 is fixed)") 40 | ) 41 | 42 | // parseQueryPos parses the source query position pos and returns the path 43 | // enclosing the specified interval. 44 | // (based on parseQueryPos from github.com/golang/tools/cmd/guru/guru.go) 45 | func parseQueryPos(fset *token.FileSet, root *ast.File, pos string, needExact bool) ([]ast.Node, error) { 46 | filename, startOffset, endOffset, err := parsePos(pos) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | // Find the named file among those in the loaded program. 52 | var file *token.File 53 | fset.Iterate(func(f *token.File) bool { 54 | if sameFile(filename, f.Name()) { 55 | file = f 56 | return false // done 57 | } 58 | return true // continue 59 | }) 60 | if file == nil { 61 | return nil, fmt.Errorf("file %s not found in loaded program", filename) 62 | } 63 | 64 | // decrement startOffset as long as it points to |")", so that PathEnclosingInterval returns an ast.CallExpr 65 | //log.Printf("filename = %q, startOffset = %d, endOffset = %d\n", filename, startOffset, endOffset) 66 | b, err := ioutil.ReadFile(filename) 67 | if err != nil { 68 | return nil, err 69 | } 70 | //log.Printf("before: %q (rune: %v)", string(b[startOffset-5:endOffset+5]), rune(b[startOffset-1])) 71 | for unicode.IsSpace(rune(b[startOffset-1])) || b[startOffset-1] == ')' { 72 | startOffset-- 73 | endOffset-- 74 | //log.Printf("decremented to startOffset = %d, endOffset = %d\n", startOffset, endOffset) 75 | } 76 | //log.Printf("after: %q", string(b[startOffset-5:endOffset+5])) 77 | 78 | start, end, err := fileOffsetToPos(file, startOffset, endOffset) 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | path, exact := astutil.PathEnclosingInterval(root, start, end) 84 | if path == nil { 85 | return nil, fmt.Errorf("no syntax here") 86 | } 87 | if needExact && !exact { 88 | return nil, fmt.Errorf("ambiguous selection within %s", astutil.NodeDescription(path[0])) 89 | } 90 | return path, nil 91 | } 92 | 93 | func unparen(e ast.Expr) ast.Expr { return astutil.Unparen(e) } 94 | 95 | var errUnknownSignature = errors.New("unknown signature") 96 | 97 | func signatureOf(info *types.Info, e *ast.CallExpr) (*types.Signature, error) { 98 | // Deal with obviously static calls before constructing SSA form. 99 | // Some static calls may yet require SSA construction, 100 | // e.g. f := func(){}; f(). 101 | switch funexpr := unparen(e.Fun).(type) { 102 | case *ast.Ident: 103 | switch obj := info.Uses[funexpr].(type) { 104 | case *types.Builtin: 105 | // Reject calls to built-ins. 106 | return nil, fmt.Errorf("this is a call to the built-in '%s' operator", obj.Name()) 107 | case *types.Func: 108 | // This is a static function call 109 | return obj.Type().(*types.Signature), nil 110 | case *types.Var: 111 | // This is a function literal call 112 | return obj.Type().(*types.Signature), nil 113 | default: 114 | // TODO: better error message: the function signature for could not be found 115 | //return nil, fmt.Errorf("unhandled: info.Uses[%v] = %T", funexpr, obj) 116 | return nil, errUnknownSignature 117 | } 118 | case *ast.SelectorExpr: 119 | sel := info.Selections[funexpr] 120 | if sel == nil { 121 | // qualified identifier. 122 | // May refer to top level function variable 123 | // or to top level function. 124 | switch callee := info.Uses[funexpr.Sel].(type) { 125 | case *types.Func: 126 | return callee.Type().(*types.Signature), nil 127 | default: 128 | // TODO: better error message (see above) 129 | return nil, errUnknownSignature 130 | } 131 | } else if sel.Kind() == types.MethodVal { 132 | // Inspect the receiver type of the selected method. 133 | // If it is concrete, the call is statically dispatched. 134 | // (Due to implicit field selections, it is not enough to look 135 | // at sel.Recv(), the type of the actual receiver expression.) 136 | method := sel.Obj().(*types.Func) 137 | return method.Type().(*types.Signature), nil 138 | } 139 | } 140 | return nil, fmt.Errorf("unhandled: signature of %T", unparen(e.Fun)) 141 | } 142 | 143 | func currentSignature(path []ast.Node) (*ast.FuncType, error) { 144 | for _, n := range path { 145 | switch n := n.(type) { 146 | case *ast.FuncDecl: 147 | return n.Type, nil 148 | case *ast.FuncLit: 149 | return n.Type, nil 150 | } 151 | } 152 | return nil, fmt.Errorf("no function definition found in path") 153 | } 154 | 155 | // newZeroValueNode returns an AST expr representing the zero value of 156 | // typ. If determining the zero value requires additional information 157 | // (e.g., type-checking output), it returns nil. 158 | // (from github.com/sqs/goreturns/returns/fix.go) 159 | func newZeroValueNode(typ ast.Expr) ast.Expr { 160 | switch v := typ.(type) { 161 | case *ast.Ident: 162 | switch v.Name { 163 | case "uint8", "uint16", "uint32", "uint64", "int8", "int16", "int32", "int64", "byte", "rune", "uint", "int", "uintptr": 164 | return &ast.BasicLit{Kind: token.INT, Value: "0"} 165 | case "float32", "float64": 166 | return &ast.BasicLit{Kind: token.FLOAT, Value: "0"} 167 | case "complex64", "complex128": 168 | return &ast.BasicLit{Kind: token.IMAG, Value: "0"} 169 | case "bool": 170 | return &ast.Ident{Name: "false"} 171 | case "string": 172 | return &ast.BasicLit{Kind: token.STRING, Value: `""`} 173 | case "error": 174 | return &ast.Ident{Name: "nil"} 175 | } 176 | case *ast.ArrayType: 177 | if v.Len == nil { 178 | // slice 179 | return &ast.Ident{Name: "nil"} 180 | } 181 | return &ast.CompositeLit{Type: v} 182 | case *ast.StarExpr: 183 | return &ast.Ident{Name: "nil"} 184 | } 185 | return nil 186 | } 187 | 188 | // Like newZeroValueNode, but with type information. 189 | // 190 | // TODO: can we safely get rid of newZeroValueNode or does it handle cases which 191 | // would otherwise go unhandled? 192 | func newZeroValueNodeTypeName(id *ast.Ident, name *types.TypeName) ast.Expr { 193 | switch t := name.Type().Underlying().(type) { 194 | case *types.Struct: 195 | return &ast.Ident{Name: id.Name + "{}"} 196 | 197 | case *types.Interface: 198 | return &ast.Ident{Name: "nil"} 199 | 200 | case *types.Basic: 201 | switch t.Kind() { 202 | case types.Int, types.Int8, types.Int16, types.Int32, types.Int64, 203 | types.Uint, types.Uint8, types.Uint16, types.Uint32, types.Uint64, types.Uintptr: 204 | return &ast.BasicLit{Kind: token.INT, Value: "0"} 205 | 206 | case types.Float32, types.Float64: 207 | return &ast.BasicLit{Kind: token.FLOAT, Value: "0"} 208 | 209 | case types.Complex64, types.Complex128: 210 | return &ast.BasicLit{Kind: token.IMAG, Value: "0"} 211 | 212 | case types.Bool: 213 | return &ast.Ident{Name: "false"} 214 | 215 | case types.String: 216 | return &ast.BasicLit{Kind: token.STRING, Value: `""`} 217 | } 218 | } 219 | return nil 220 | } 221 | 222 | // fallbackImporter tries to import using importer first, falling back to 223 | // srcImporter on any error. This allows us to load binary files (significantly 224 | // faster) where possible, but import from source where necessary. 225 | type fallbackImporter struct { 226 | importer types.ImporterFrom 227 | srcImporter types.ImporterFrom 228 | } 229 | 230 | func (fi *fallbackImporter) Import(path string) (*types.Package, error) { 231 | p, err := fi.importer.Import(path) 232 | if err != nil { 233 | return fi.srcImporter.Import(path) 234 | } 235 | return p, err 236 | } 237 | 238 | func (fi *fallbackImporter) ImportFrom(path, srcDir string, mode types.ImportMode) (*types.Package, error) { 239 | p, err := fi.importer.ImportFrom(path, srcDir, mode) 240 | if err != nil { 241 | return fi.srcImporter.ImportFrom(path, srcDir, mode) 242 | } 243 | return p, err 244 | } 245 | 246 | func callExprAtPath(path []ast.Node) *ast.CallExpr { 247 | var ce *ast.CallExpr 248 | // Return the outer-most *ast.CallExpr in path, if any. 249 | for _, p := range path { 250 | if e, ok := p.(*ast.CallExpr); ok { 251 | ce = e 252 | } 253 | } 254 | if ce != nil { 255 | return ce 256 | } 257 | 258 | // Look for an *ast.CallExpr within the *ast.BlockStmt, if path starts with 259 | // an *ast.BlockStmt. 260 | if first, ok := path[0].(*ast.BlockStmt); ok { 261 | ast.Inspect(first, func(n ast.Node) bool { 262 | if n, ok := n.(*ast.CallExpr); ok { 263 | ce = n 264 | return false // found, stop 265 | } 266 | 267 | return true // recurse 268 | }) 269 | if ce != nil { 270 | return ce 271 | } 272 | } 273 | 274 | return nil // no *ast.CallExpr found 275 | } 276 | 277 | func defaultImporter() types.Importer { 278 | // TODO(golang.org/issues/19337): default to fallbackImporter once packages 279 | // are augmented. 280 | if *unsafeFastImporter { 281 | return &fallbackImporter{ 282 | importer: importer.Default().(types.ImporterFrom), 283 | srcImporter: importer.For("source", nil).(types.ImporterFrom), 284 | } 285 | } 286 | i := importer.For("source", nil) 287 | if i == nil { 288 | // Fallback for Go <1.9 289 | i = srcimporter.New(&build.Default, token.NewFileSet(), make(map[string]*types.Package)) 290 | } 291 | return i 292 | } 293 | 294 | // expansion holds state during the error expansion. 295 | type expansion struct { 296 | fset *token.FileSet 297 | file *ast.File // the file under cursor 298 | ce *ast.CallExpr // the call expression under the cursor 299 | callee *types.Signature // the callee’s signature 300 | caller *ast.FuncType // the caller’s type (including signature) 301 | results []ast.Expr // return values for the new error check 302 | info *types.Info // type information of the type-checked package 303 | pkg *types.Package 304 | path []ast.Node // node under cursor and all its ancestors 305 | } 306 | 307 | func (e *expansion) getScope() *types.Scope { 308 | for _, p := range e.path { 309 | if funcDecl, ok := p.(*ast.FuncDecl); ok { 310 | if s, ok := e.info.Scopes[funcDecl.Type]; ok { 311 | return s 312 | } 313 | } 314 | if s, ok := e.info.Scopes[p]; ok { 315 | return s 316 | } 317 | } 318 | return nil 319 | } 320 | 321 | func errPresent(lhs []ast.Expr) bool { 322 | for _, expr := range lhs { 323 | if id, ok := expr.(*ast.Ident); ok && id.Name == "err" { 324 | return true 325 | } 326 | } 327 | return false 328 | } 329 | 330 | // parent returns the parent node of n within e.path. 331 | func (e *expansion) parent(n ast.Node) ast.Node { 332 | found := false 333 | for _, p := range e.path { 334 | if found { 335 | return p 336 | } 337 | found = (p == n) 338 | } 339 | return nil 340 | } 341 | 342 | func (e *expansion) typeCheck(pkgname string, files []*ast.File, addWarnFunc func(string)) error { 343 | e.info = &types.Info{ 344 | Uses: make(map[*ast.Ident]types.Object), 345 | Selections: make(map[*ast.SelectorExpr]*types.Selection), 346 | Scopes: make(map[ast.Node]*types.Scope), 347 | } 348 | 349 | conf := types.Config{ 350 | Importer: defaultImporter(), 351 | Error: func(err error) { 352 | addWarnFunc(fmt.Sprintf("ignoring type-checking error: %v", err)) 353 | }, // keep going on errors 354 | } 355 | pkg, _ := conf.Check(pkgname, e.fset, files, e.info) 356 | // Type checking errors are ignored so that we can write expressions like 357 | // “n := io.Write(p)” (two values assigned to one variable). 358 | 359 | e.pkg = pkg 360 | 361 | var err error 362 | e.caller, err = currentSignature(e.path) 363 | if err != nil { 364 | return err 365 | } 366 | 367 | e.ce = callExprAtPath(e.path) 368 | if e.ce == nil { 369 | return fmt.Errorf("no ast.CallExpr found") 370 | } 371 | e.callee, err = signatureOf(e.info, e.ce) 372 | if err != nil { 373 | return err 374 | } 375 | 376 | if e.caller.Results == nil { 377 | return nil 378 | } 379 | 380 | e.results = make([]ast.Expr, len(e.caller.Results.List)) 381 | for idx, res := range e.caller.Results.List { 382 | if id, ok := res.Type.(*ast.Ident); ok && id.Name == "error" { 383 | e.results[idx] = &ast.Ident{Name: "err"} 384 | } else { 385 | e.results[idx] = newZeroValueNode(res.Type) 386 | if e.results[idx] == nil { 387 | // We could not figure out from the AST what the type is, so 388 | // it’s not a builtin type, array type or pointer type. 389 | if id, ok := res.Type.(*ast.Ident); ok { 390 | if tn, ok := e.info.Uses[id].(*types.TypeName); ok { 391 | e.results[idx] = newZeroValueNodeTypeName(id, tn) 392 | } 393 | } 394 | } 395 | } 396 | } 397 | return nil 398 | } 399 | 400 | // this function either returns just the return values of the function or, if there are no errors 401 | // returned, will also add in the no-error-callback 402 | func (e *expansion) getFinalOutput(noReturnStr string, errName string) ([]ast.Stmt, error) { 403 | var normalReturn = &ast.ReturnStmt{Results: e.results} 404 | 405 | var returnsError = true 406 | if res := e.caller.Results; res == nil { 407 | returnsError = false 408 | } else if list := res.List; len(list) == 0 { 409 | returnsError = false 410 | } else if id, ok := list[len(list)-1].Type.(*ast.Ident); ok && id.Name != "error" { 411 | returnsError = false 412 | } 413 | 414 | if returnsError { 415 | return []ast.Stmt{normalReturn}, nil 416 | } 417 | 418 | if noReturnStr == "" { // default output when no error retuned 419 | return []ast.Stmt{&ast.ExprStmt{ 420 | X: &ast.CallExpr{ 421 | Fun: &ast.Ident{Name: "panic"}, 422 | Args: []ast.Expr{ 423 | &ast.Ident{Name: "err"}, 424 | }, 425 | }, 426 | }}, nil 427 | } 428 | 429 | noReturnExpr, err := parser.ParseExpr(noReturnStr) 430 | if err != nil { 431 | return nil, fmt.Errorf("parsing template result into ast: %v", err) 432 | } 433 | 434 | return []ast.Stmt{ 435 | &ast.ExprStmt{X: noReturnExpr}, 436 | normalReturn, 437 | }, nil 438 | } 439 | 440 | func logic(w io.Writer, buildctx *build.Context, posn, noReturnStr string) error { 441 | e := expansion{ 442 | fset: token.NewFileSet(), 443 | } 444 | 445 | // Short-cut: parse+type-check a single file before loading the entire 446 | // package. 447 | filename, _, _, err := parsePos(posn) 448 | if err != nil { 449 | return err 450 | } 451 | 452 | e.file, err = parser.ParseFile(e.fset, filename, nil, parser.ParseComments) 453 | if err != nil { 454 | return fmt.Errorf("parsing: %v", err) 455 | } 456 | 457 | e.path, err = parseQueryPos(e.fset, e.file, posn, false) 458 | if err != nil { 459 | return err 460 | } 461 | 462 | // TODO(golang.org/issues/21418): hack: importer.For always uses 463 | // build.Default, so we need to change build.Default 464 | build.Default = *buildctx 465 | 466 | var warnings []string 467 | var warnFunc = func(warning string) { warnings = append(warnings, warning) } 468 | if err := e.typeCheck("main", []*ast.File{e.file}, warnFunc); err != nil { 469 | if err != errUnknownSignature { 470 | return err 471 | } 472 | 473 | // Parse all files, type-check again. 474 | d, err := os.Open(filepath.Dir(filename)) 475 | if err != nil { 476 | return err 477 | } 478 | defer d.Close() 479 | names, err := d.Readdirnames(-1) 480 | if err != nil { 481 | return err 482 | } 483 | files := []*ast.File{e.file} 484 | // TODO: parallelize 485 | for _, n := range names { 486 | if n == filepath.Base(filename) { 487 | continue // already parsed 488 | } 489 | if strings.HasPrefix(n, "expanderr") { 490 | continue // skip expanderr temp file when working in /tmp 491 | } 492 | if !strings.HasSuffix(n, ".go") { 493 | continue 494 | } 495 | f, err := parser.ParseFile(e.fset, filepath.Join(filepath.Dir(filename), n), nil, parser.ParseComments) 496 | if err != nil { 497 | return fmt.Errorf("parsing: %v", err) 498 | } 499 | files = append(files, f) 500 | } 501 | if err := e.typeCheck(e.pkg.Name(), files, warnFunc); err != nil { 502 | return err 503 | } 504 | } 505 | 506 | var subject ast.Node // what will be replaced 507 | subject = e.ce 508 | var repl []ast.Node 509 | switch e.callee.Results().Len() { 510 | case 0: 511 | // nothing to replace, i.e. keep the original *ast.CallExpr 512 | repl = []ast.Node{e.ce} 513 | case 1: 514 | // TODO: check if this CallExpr is within an AssignStmt. if so, replace the AssignStmt instead 515 | if parent := e.parent(subject); parent != nil { 516 | if stmt, ok := parent.(*ast.AssignStmt); ok { 517 | subject = stmt 518 | } 519 | } 520 | 521 | outputStmt, err := e.getFinalOutput(noReturnStr, "err") 522 | if err != nil { 523 | return err 524 | } 525 | 526 | // e.g. os.Remove(…) → if err := os.Remove(…); err != nil { return 0, err } 527 | repl = []ast.Node{&ast.IfStmt{ 528 | Init: &ast.AssignStmt{ 529 | Lhs: []ast.Expr{&ast.Ident{Name: "err"}}, 530 | Tok: token.DEFINE, 531 | Rhs: []ast.Expr{e.ce}, 532 | }, 533 | Cond: &ast.BinaryExpr{ 534 | X: &ast.Ident{Name: "err"}, 535 | Op: token.NEQ, 536 | Y: &ast.Ident{Name: "nil"}, 537 | }, 538 | Body: &ast.BlockStmt{ 539 | List: outputStmt, 540 | }, 541 | }} 542 | default: 543 | // e.g. f := os.Create(…) → f, err := os.Create(…); if err != nil { return 0, err } 544 | 545 | // walk up to the *ast.AssignStmt and append an *ast.Ident to its .Lhs 546 | var as *ast.AssignStmt 547 | for _, p := range e.path { 548 | if p, ok := p.(*ast.AssignStmt); ok { 549 | as = p 550 | break 551 | } 552 | } 553 | if as == nil { 554 | return fmt.Errorf("no *ast.AssignStmt found in path") // TODO: better error msg 555 | } 556 | 557 | scope := e.getScope() 558 | if scope == nil { 559 | return fmt.Errorf("could not find scope") // TODO: better error msg. can this happen at all? 560 | } 561 | errInScope := scope.Lookup("err") != nil 562 | 563 | subject = as 564 | 565 | onlyUnderscore := true 566 | for _, lhs := range as.Lhs { 567 | switch lhs := lhs.(type) { 568 | case *ast.Ident: 569 | if lhs.Name != "_" { 570 | onlyUnderscore = false 571 | } 572 | default: 573 | onlyUnderscore = false 574 | } 575 | } 576 | 577 | // TODO: verify all other parameters are assigned 578 | 579 | if !errPresent(as.Lhs) { 580 | as.Lhs = append(as.Lhs, &ast.Ident{Name: "err"}) 581 | } 582 | 583 | outputStmt, err := e.getFinalOutput(noReturnStr, "err") 584 | if err != nil { 585 | return err 586 | } 587 | 588 | if !onlyUnderscore && as.Tok == token.DEFINE { 589 | // Insert a new *ast.IfStmt after the *ast.CallExpr. 590 | repl = []ast.Node{ 591 | subject, 592 | &ast.IfStmt{ 593 | Cond: &ast.BinaryExpr{ 594 | X: &ast.Ident{Name: "err"}, 595 | Op: token.NEQ, 596 | Y: &ast.Ident{Name: "nil"}, 597 | }, 598 | Body: &ast.BlockStmt{ 599 | List: outputStmt, 600 | }, 601 | }, 602 | } 603 | } else { 604 | tok := as.Tok 605 | if onlyUnderscore { 606 | tok = token.DEFINE 607 | } 608 | if !onlyUnderscore && !errInScope { 609 | // The “err” identifier is not yet in scope, so insert a “var 610 | // err error” declaration before the *ast.IfStmt. 611 | repl = append(repl, &ast.DeclStmt{ 612 | Decl: &ast.GenDecl{ 613 | Tok: token.VAR, 614 | Specs: []ast.Spec{ 615 | &ast.ValueSpec{ 616 | Names: []*ast.Ident{&ast.Ident{Name: "err"}}, 617 | Type: &ast.Ident{Name: "error"}, 618 | }, 619 | }, 620 | }, 621 | }) 622 | } 623 | // Embed the *ast.CallExpr in an *ast.IfStmt. 624 | repl = append(repl, &ast.IfStmt{ 625 | Init: &ast.AssignStmt{ 626 | Lhs: as.Lhs, 627 | Tok: tok, 628 | Rhs: []ast.Expr{e.ce}, 629 | }, 630 | Cond: &ast.BinaryExpr{ 631 | X: &ast.Ident{Name: "err"}, 632 | Op: token.NEQ, 633 | Y: &ast.Ident{Name: "nil"}, 634 | }, 635 | Body: &ast.BlockStmt{ 636 | List: []ast.Stmt{ 637 | &ast.ReturnStmt{Results: e.results}, 638 | }, 639 | }, 640 | }) 641 | } 642 | } 643 | 644 | // TODO(golang.org/issues/20744): switch from textual replacement to 645 | // formatting the AST once comments are represented in a more convenient 646 | // way. 647 | 648 | b, err := ioutil.ReadFile(filename) 649 | if err != nil { 650 | return err 651 | } 652 | var src bytes.Buffer 653 | // Copy everything before the subject as-is. 654 | if _, err := src.Write(b[:subject.Pos()-1]); err != nil { 655 | return err 656 | } 657 | var newEnd int 658 | // Print the replacement 659 | for _, node := range repl { 660 | // Hack: inline comments (e.g. os.Remove("/foo" /* path */)) get lost 661 | // when formatting node, so we string-replace the formatted CallExpr 662 | // with the original CallExpr. 663 | 664 | var stmtFmt, ceFmt bytes.Buffer 665 | if err := format.Node(&stmtFmt, token.NewFileSet(), node); err != nil { 666 | return fmt.Errorf("formatting replacement: %v", err) 667 | } 668 | if err := format.Node(&ceFmt, e.fset, e.ce); err != nil { 669 | return fmt.Errorf("formatting replacement: %v", err) 670 | } 671 | 672 | newEnd += len(strings.Split(stmtFmt.String(), "\n")) 673 | 674 | ceOrig := string(b[e.ce.Pos()-1 : e.ce.End()-1]) 675 | if _, err := src.Write([]byte(strings.Replace(stmtFmt.String(), ceFmt.String(), ceOrig, 1))); err != nil { 676 | return err 677 | } 678 | 679 | if _, err := src.Write([]byte(";")); err != nil { 680 | return err 681 | } 682 | } 683 | // Copy everything after the subject as-is. 684 | if _, err := src.Write(b[subject.End():]); err != nil { 685 | return err 686 | } 687 | 688 | // Format the entire source code — formatting the replacement is not 689 | // sufficient, as the replacement is not formatted in context. 690 | formatted, err := format.Source(src.Bytes()) 691 | if err != nil { 692 | return fmt.Errorf("formatting source: %v.\nsource:\n%s", err, src.String()) 693 | } 694 | 695 | if *formatFlag == "json" { 696 | start := e.fset.Position(subject.Pos()).Line 697 | end := e.fset.Position(subject.End()).Line 698 | var lines []string 699 | for idx, line := range strings.Split(string(formatted), "\n") { 700 | if idx >= start-1 && idx < start-1+newEnd { 701 | lines = append(lines, line) 702 | } 703 | } 704 | if err := json.NewEncoder(w).Encode(struct { 705 | Start int `json:"start"` 706 | End int `json:"end"` 707 | Lines []string `json:"lines"` 708 | Warnings []string `json:"warnings"` 709 | }{ 710 | Start: start, 711 | End: end, 712 | Lines: lines, 713 | Warnings: warnings, 714 | }); err != nil { 715 | return err 716 | } 717 | } else { 718 | for _, w := range warnings { 719 | log.Print(w) 720 | } 721 | if _, err := w.Write(formatted); err != nil { 722 | return err 723 | } 724 | } 725 | 726 | return nil 727 | } 728 | 729 | var ( 730 | wFlag = flag.String("w", "", "write") 731 | formatFlag = flag.String("format", "", "output format (source, json). defaults to 'source'") 732 | cpuprofile = flag.String("cpuprofile", "", "write cpu profile `file`") 733 | noErrReturnStr = flag.String("no-error-callback", "", "function call to be used if there is no error return value. ex: 'log.Fatalf(\"boom: %v\", err)'. defaults to 'panic(err)'") 734 | ) 735 | 736 | func main() { 737 | flag.Parse() 738 | 739 | if *cpuprofile != "" { 740 | f, err := os.Create(*cpuprofile) 741 | if err != nil { 742 | log.Fatal("could not create CPU profile: ", err) 743 | } 744 | if err := pprof.StartCPUProfile(f); err != nil { 745 | log.Fatal("could not start CPU profile: ", err) 746 | } 747 | defer pprof.StopCPUProfile() 748 | } 749 | 750 | args := flag.Args() 751 | if len(args) != 1 { 752 | flag.Usage() 753 | os.Exit(2) 754 | } 755 | posn := args[0] 756 | 757 | o := io.Writer(os.Stdout) 758 | if *wFlag != "" { 759 | f, err := os.Create(*wFlag) 760 | if err != nil { 761 | log.Fatal(err) 762 | } 763 | defer f.Close() 764 | o = f 765 | } 766 | 767 | if err := logic(o, &build.Default, posn, *noErrReturnStr); err != nil { 768 | if *formatFlag == "json" { 769 | jsonErr := json.NewEncoder(o).Encode(struct { 770 | Error string `json:"error"` 771 | }{ 772 | Error: err.Error(), 773 | }) 774 | if jsonErr != nil { 775 | log.Println(err) 776 | log.Fatal(jsonErr) 777 | } 778 | return 779 | } 780 | log.Fatal(err) 781 | } 782 | } 783 | -------------------------------------------------------------------------------- /expanderr_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "flag" 7 | "go/build" 8 | "io/ioutil" 9 | "path/filepath" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | func TestExpand(t *testing.T) { 15 | // Cannot be safely run in parallel as long as build.Default is overridden 16 | // t.Parallel() 17 | 18 | for _, entry := range []struct { 19 | name string 20 | fn string 21 | posn string 22 | errcallback string 23 | }{ 24 | {"SingleErrorAfter", "testdata/singleerror.got/src/singleerror/singleerror.go", ":#90", ""}, 25 | {"SingleErrorBefore", "testdata/singleerror.got/src/singleerror/singleerror.go", ":#69", ""}, 26 | {"SingleErrorMiddle", "testdata/singleerror.got/src/singleerror/singleerror.go", ":#81", ""}, 27 | {"NoReturn", "testdata/nocalleereturn.got/src/nocalleereturn/nocalleereturn.go", ":#75", ""}, 28 | {"VariableAndError", "testdata/varanderror.got/src/varanderror/varanderror.go", ":#148", ""}, 29 | {"Comment", "testdata/comment.got/src/comment/comment.go", ":#90", ""}, 30 | {"CommentInline", "testdata/commentinline.got/src/commentinline/commentinline.go", ":#109", ""}, 31 | {"NoReturnCaller", "testdata/noreturncaller.got/src/noreturncaller/noreturncaller.go", ":#77", ""}, 32 | {"NoErrReturn", "testdata/noerrreturn.got/src/noerrreturn/noerrreturn.go", ":#81", ""}, 33 | {"ReturnErrCall", "testdata/returnerrcall.got/src/returnerrcall/returnerrcall.go", ":#101", "log.Fatal(err.Error())"}, 34 | {"FunctionLiteral", "testdata/functionliteral.got/src/functionliteral/functionliteral.go", ":#87", ""}, 35 | // The following test spreads out one package over two files, exercising 36 | // the code path for loading multiple files. 37 | {"2Files1Pkg", "testdata/pkg.got/src/pkg/pkg2.go", ":#49", ""}, 38 | // MultiPkg calls a function in another not-compiled, non-stdlib package. 39 | {"MultiPkg", "testdata/multipkg.got/src/multipkg/multipkg.go", ":#79", ""}, 40 | {"MultiPkgVendor", "testdata/multipkgvendor.got/src/multipkg/multipkg.go", ":#79", ""}, 41 | {"Underscore", "testdata/underscore.got/src/underscore/underscore.go", ":#162", ""}, 42 | {"IntroduceErr", "testdata/introduceerr.got/src/introduceerr/introduceerr.go", ":#176", ""}, 43 | {"NoIntroduce", "testdata/nointroduce.got/src/nointroduce/nointroduce.go", ":#165", ""}, 44 | {"PresentSingle", "testdata/presentsingle.got/src/presentsingle/presentsingle.go", ":#90", ""}, 45 | {"PresentDouble", "testdata/presentdouble.got/src/presentdouble/presentdouble.go", ":#105", ""}, 46 | {"CustomTypes", "testdata/customtypes.got/src/customtypes/customtypes.go", ":#191", ""}, 47 | } { 48 | entry := entry // copy 49 | t.Run(entry.name, func(t *testing.T) { 50 | // Cannot be safely run in parallel as long as build.Default is overridden 51 | // t.Parallel() 52 | 53 | flag.Set("format", "source") 54 | 55 | wantContents, err := ioutil.ReadFile(strings.Replace(entry.fn, ".got", ".want", 1)) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | 60 | gopath, err := filepath.Abs(filepath.Join(strings.Split(entry.fn, "/")[:2]...)) 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | buildctx := build.Context{ 65 | GOARCH: build.Default.GOARCH, 66 | GOOS: build.Default.GOOS, 67 | GOROOT: build.Default.GOROOT, 68 | GOPATH: gopath, 69 | Compiler: build.Default.Compiler, 70 | } 71 | 72 | var buf bytes.Buffer 73 | if err := logic(&buf, &buildctx, entry.fn+entry.posn, entry.errcallback); err != nil { 74 | t.Fatal(err) 75 | } 76 | 77 | if got, want := buf.String(), string(wantContents); got != want { 78 | t.Fatalf("unexpected result: have:\n%s\nwant:\n%s", got, want) 79 | } 80 | 81 | // Test the JSON output format as well. 82 | 83 | buf.Reset() 84 | flag.Set("format", "json") 85 | 86 | gotContents, err := ioutil.ReadFile(entry.fn) 87 | if err != nil { 88 | t.Fatal(err) 89 | } 90 | 91 | if err := logic(&buf, &buildctx, entry.fn+entry.posn, entry.errcallback); err != nil { 92 | t.Fatal(err) 93 | } 94 | 95 | var change struct { 96 | Start int `json:"start"` 97 | End int `json:"end"` 98 | Lines []string `json:"lines"` 99 | Warnings []string `json:"warnings"` 100 | } 101 | if err := json.Unmarshal(buf.Bytes(), &change); err != nil { 102 | t.Fatal(err) 103 | } 104 | 105 | lines := strings.Split(string(gotContents), "\n") 106 | 107 | replaced := make([]string, len(lines[:change.Start-1])) 108 | copy(replaced, lines) 109 | replaced = append(replaced, change.Lines...) 110 | replaced = append(replaced, lines[change.End:]...) 111 | 112 | if got, want := strings.Join(replaced, "\n"), string(wantContents); got != want { 113 | t.Fatalf("unexpected result: have:\n%s\nwant:\n%s", got, want) 114 | } 115 | }) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /internal/srcimporter/srcimporter.go: -------------------------------------------------------------------------------- 1 | // Copied from golang/go/src/go/internal/srcimporter 2 | 3 | // Copyright 2017 The Go Authors. All rights reserved. 4 | // Use of this source code is governed by a BSD-style 5 | // license that can be found in the LICENSE file. 6 | 7 | // Package srcimporter implements importing directly 8 | // from source files rather than installed packages. 9 | package srcimporter 10 | 11 | import ( 12 | "fmt" 13 | "go/ast" 14 | "go/build" 15 | "go/parser" 16 | "go/token" 17 | "go/types" 18 | "path/filepath" 19 | "sync" 20 | ) 21 | 22 | // An Importer provides the context for importing packages from source code. 23 | type Importer struct { 24 | ctxt *build.Context 25 | fset *token.FileSet 26 | packages map[string]*types.Package 27 | } 28 | 29 | // NewImporter returns a new Importer for the given context, file set, and map 30 | // of packages. The context is used to resolve import paths to package paths, 31 | // and identifying the files belonging to the package. If the context provides 32 | // non-nil file system functions, they are used instead of the regular package 33 | // os functions. The file set is used to track position information of package 34 | // files; and imported packages are added to the packages map. 35 | func New(ctxt *build.Context, fset *token.FileSet, packages map[string]*types.Package) *Importer { 36 | return &Importer{ 37 | ctxt: ctxt, 38 | fset: fset, 39 | packages: packages, 40 | } 41 | } 42 | 43 | // Importing is a sentinel taking the place in Importer.packages 44 | // for a package that is in the process of being imported. 45 | var importing types.Package 46 | 47 | // Import(path) is a shortcut for ImportFrom(path, "", 0). 48 | func (p *Importer) Import(path string) (*types.Package, error) { 49 | return p.ImportFrom(path, "", 0) 50 | } 51 | 52 | // ImportFrom imports the package with the given import path resolved from the given srcDir, 53 | // adds the new package to the set of packages maintained by the importer, and returns the 54 | // package. Package path resolution and file system operations are controlled by the context 55 | // maintained with the importer. The import mode must be zero but is otherwise ignored. 56 | // Packages that are not comprised entirely of pure Go files may fail to import because the 57 | // type checker may not be able to determine all exported entities (e.g. due to cgo dependencies). 58 | func (p *Importer) ImportFrom(path, srcDir string, mode types.ImportMode) (*types.Package, error) { 59 | if mode != 0 { 60 | panic("non-zero import mode") 61 | } 62 | 63 | // determine package path (do vendor resolution) 64 | var bp *build.Package 65 | var err error 66 | switch { 67 | default: 68 | if abs, err := p.absPath(srcDir); err == nil { // see issue #14282 69 | srcDir = abs 70 | } 71 | bp, err = p.ctxt.Import(path, srcDir, build.FindOnly) 72 | 73 | case build.IsLocalImport(path): 74 | // "./x" -> "srcDir/x" 75 | bp, err = p.ctxt.ImportDir(filepath.Join(srcDir, path), build.FindOnly) 76 | 77 | case p.isAbsPath(path): 78 | return nil, fmt.Errorf("invalid absolute import path %q", path) 79 | } 80 | if err != nil { 81 | return nil, err // err may be *build.NoGoError - return as is 82 | } 83 | 84 | // package unsafe is known to the type checker 85 | if bp.ImportPath == "unsafe" { 86 | return types.Unsafe, nil 87 | } 88 | 89 | // no need to re-import if the package was imported completely before 90 | pkg := p.packages[bp.ImportPath] 91 | if pkg != nil { 92 | if pkg == &importing { 93 | return nil, fmt.Errorf("import cycle through package %q", bp.ImportPath) 94 | } 95 | if !pkg.Complete() { 96 | // Package exists but is not complete - we cannot handle this 97 | // at the moment since the source importer replaces the package 98 | // wholesale rather than augmenting it (see #19337 for details). 99 | // Return incomplete package with error (see #16088). 100 | return pkg, fmt.Errorf("reimported partially imported package %q", bp.ImportPath) 101 | } 102 | return pkg, nil 103 | } 104 | 105 | p.packages[bp.ImportPath] = &importing 106 | defer func() { 107 | // clean up in case of error 108 | // TODO(gri) Eventually we may want to leave a (possibly empty) 109 | // package in the map in all cases (and use that package to 110 | // identify cycles). See also issue 16088. 111 | if p.packages[bp.ImportPath] == &importing { 112 | p.packages[bp.ImportPath] = nil 113 | } 114 | }() 115 | 116 | // collect package files 117 | bp, err = p.ctxt.ImportDir(bp.Dir, 0) 118 | if err != nil { 119 | return nil, err // err may be *build.NoGoError - return as is 120 | } 121 | var filenames []string 122 | filenames = append(filenames, bp.GoFiles...) 123 | filenames = append(filenames, bp.CgoFiles...) 124 | 125 | files, err := p.parseFiles(bp.Dir, filenames) 126 | if err != nil { 127 | return nil, err 128 | } 129 | 130 | // type-check package files 131 | var firstHardErr error 132 | conf := types.Config{ 133 | IgnoreFuncBodies: true, 134 | FakeImportC: true, 135 | // continue type-checking after the first error 136 | Error: func(err error) { 137 | if firstHardErr == nil && !err.(types.Error).Soft { 138 | firstHardErr = err 139 | } 140 | }, 141 | Importer: p, 142 | } 143 | pkg, err = conf.Check(bp.ImportPath, p.fset, files, nil) 144 | if err != nil { 145 | // If there was a hard error it is possibly unsafe 146 | // to use the package as it may not be fully populated. 147 | // Do not return it (see also #20837, #20855). 148 | if firstHardErr != nil { 149 | pkg = nil 150 | err = firstHardErr // give preference to first hard error over any soft error 151 | } 152 | return pkg, fmt.Errorf("type-checking package %q failed (%v)", bp.ImportPath, err) 153 | } 154 | if firstHardErr != nil { 155 | // this can only happen if we have a bug in go/types 156 | panic("package is not safe yet no error was returned") 157 | } 158 | 159 | p.packages[bp.ImportPath] = pkg 160 | return pkg, nil 161 | } 162 | 163 | func (p *Importer) parseFiles(dir string, filenames []string) ([]*ast.File, error) { 164 | open := p.ctxt.OpenFile // possibly nil 165 | 166 | files := make([]*ast.File, len(filenames)) 167 | errors := make([]error, len(filenames)) 168 | 169 | var wg sync.WaitGroup 170 | wg.Add(len(filenames)) 171 | for i, filename := range filenames { 172 | go func(i int, filepath string) { 173 | defer wg.Done() 174 | if open != nil { 175 | src, err := open(filepath) 176 | if err != nil { 177 | errors[i] = fmt.Errorf("opening package file %s failed (%v)", filepath, err) 178 | return 179 | } 180 | files[i], errors[i] = parser.ParseFile(p.fset, filepath, src, 0) 181 | src.Close() // ignore Close error - parsing may have succeeded which is all we need 182 | } else { 183 | // Special-case when ctxt doesn't provide a custom OpenFile and use the 184 | // parser's file reading mechanism directly. This appears to be quite a 185 | // bit faster than opening the file and providing an io.ReaderCloser in 186 | // both cases. 187 | // TODO(gri) investigate performance difference (issue #19281) 188 | files[i], errors[i] = parser.ParseFile(p.fset, filepath, nil, 0) 189 | } 190 | }(i, p.joinPath(dir, filename)) 191 | } 192 | wg.Wait() 193 | 194 | // if there are errors, return the first one for deterministic results 195 | for _, err := range errors { 196 | if err != nil { 197 | return nil, err 198 | } 199 | } 200 | 201 | return files, nil 202 | } 203 | 204 | // context-controlled file system operations 205 | 206 | func (p *Importer) absPath(path string) (string, error) { 207 | // TODO(gri) This should be using p.ctxt.AbsPath which doesn't 208 | // exist but probably should. See also issue #14282. 209 | return filepath.Abs(path) 210 | } 211 | 212 | func (p *Importer) isAbsPath(path string) bool { 213 | if f := p.ctxt.IsAbsPath; f != nil { 214 | return f(path) 215 | } 216 | return filepath.IsAbs(path) 217 | } 218 | 219 | func (p *Importer) joinPath(elem ...string) string { 220 | if f := p.ctxt.JoinPath; f != nil { 221 | return f(elem...) 222 | } 223 | return filepath.Join(elem...) 224 | } 225 | -------------------------------------------------------------------------------- /lisp/go-expanderr.el: -------------------------------------------------------------------------------- 1 | (require 'go-mode) ; we use a few gofmt-- functions. 2 | 3 | (defcustom go-expanderr-command "expanderr" 4 | "The `expanderr' command; by default, $PATH is searched." 5 | :type 'string 6 | :group 'expanderr) 7 | 8 | (defcustom go-expanderr-show-error-buffer t 9 | "Whether to show a the error buffer when `go-expanderr' failed." 10 | :type 'boolean 11 | :group 'expanderr) 12 | 13 | (defun go-expanderr () 14 | "Expand the Call Expression before/under the cursor to check errors." 15 | (interactive) 16 | (let ((tmpfile (make-temp-file "expanderr" nil ".go")) 17 | (patchbuf (get-buffer-create "*Expanderr patch*")) 18 | (errbuf (if gofmt-show-errors (get-buffer-create "*Expanderr Errors*"))) 19 | (coding-system-for-read 'utf-8) 20 | (coding-system-for-write 'utf-8) 21 | our-gofmt-args) 22 | 23 | (unwind-protect 24 | (save-restriction 25 | (widen) 26 | (if errbuf 27 | (with-current-buffer errbuf 28 | (setq buffer-read-only nil) 29 | (erase-buffer))) 30 | (with-current-buffer patchbuf 31 | (erase-buffer)) 32 | 33 | (save-buffer) 34 | (setq expanderr-command go-expanderr-command) 35 | (setq our-expanderr-args (list "-w" tmpfile "-no-error-callback" "log.Fatal(err)" 36 | (concat 37 | (file-truename buffer-file-name) 38 | (format ":#%d" (position-bytes (point)))))) 39 | (message "Calling expanderr: %s %s" expanderr-command our-expanderr-args) 40 | ;; We're using errbuf for the mixed stdout and stderr output. This 41 | ;; is not an issue because expanderr -w does not produce any stdout 42 | ;; output in case of success. 43 | (if (zerop (apply #'call-process expanderr-command nil errbuf nil our-expanderr-args)) 44 | (progn 45 | (if (zerop (call-process-region (point-min) (point-max) "diff" nil patchbuf nil "-n" "-" tmpfile)) 46 | (message "Buffer is already expanded") 47 | (go--apply-rcs-patch patchbuf) 48 | (message "Applied expanderr")) 49 | (if errbuf (gofmt--kill-error-buffer errbuf))) 50 | (message "Could not apply expanderr") 51 | (when (and errbuf go-expanderr-show-error-buffer) 52 | (gofmt--process-errors (buffer-file-name) tmpfile errbuf)))) 53 | 54 | (kill-buffer patchbuf) 55 | (delete-file tmpfile)))) 56 | 57 | (define-key go-mode-map (kbd "C-c C-e") #'go-expanderr) 58 | 59 | (provide 'go-expanderr) 60 | -------------------------------------------------------------------------------- /pos.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 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 | // This file defines utilities for working with file positions. 8 | 9 | import ( 10 | "fmt" 11 | "go/token" 12 | "os" 13 | "path/filepath" 14 | "strconv" 15 | "strings" 16 | ) 17 | 18 | // parseOctothorpDecimal returns the numeric value if s matches "#%d", 19 | // otherwise -1. 20 | func parseOctothorpDecimal(s string) int { 21 | if s != "" && s[0] == '#' { 22 | if s, err := strconv.ParseInt(s[1:], 10, 32); err == nil { 23 | return int(s) 24 | } 25 | } 26 | return -1 27 | } 28 | 29 | // parsePos parses a string of the form "file:pos" or 30 | // file:start,end" where pos, start, end match #%d and represent byte 31 | // offsets, and returns its components. 32 | // 33 | // (Numbers without a '#' prefix are reserved for future use, 34 | // e.g. to indicate line/column positions.) 35 | // 36 | func parsePos(pos string) (filename string, startOffset, endOffset int, err error) { 37 | if pos == "" { 38 | err = fmt.Errorf("no source position specified") 39 | return 40 | } 41 | 42 | colon := strings.LastIndex(pos, ":") 43 | if colon < 0 { 44 | err = fmt.Errorf("bad position syntax %q", pos) 45 | return 46 | } 47 | filename, offset := pos[:colon], pos[colon+1:] 48 | startOffset = -1 49 | endOffset = -1 50 | if comma := strings.Index(offset, ","); comma < 0 { 51 | // e.g. "foo.go:#123" 52 | startOffset = parseOctothorpDecimal(offset) 53 | endOffset = startOffset 54 | } else { 55 | // e.g. "foo.go:#123,#456" 56 | startOffset = parseOctothorpDecimal(offset[:comma]) 57 | endOffset = parseOctothorpDecimal(offset[comma+1:]) 58 | } 59 | if startOffset < 1 || endOffset < startOffset { 60 | err = fmt.Errorf("invalid offset %q in query position", offset) 61 | return 62 | } 63 | return 64 | } 65 | 66 | // fileOffsetToPos translates the specified file-relative byte offsets 67 | // into token.Pos form. It returns an error if the file was not found 68 | // or the offsets were out of bounds. 69 | // 70 | func fileOffsetToPos(file *token.File, startOffset, endOffset int) (start, end token.Pos, err error) { 71 | // Range check [start..end], inclusive of both end-points. 72 | 73 | if 0 <= startOffset && startOffset <= file.Size() { 74 | start = file.Pos(int(startOffset)) 75 | } else { 76 | err = fmt.Errorf("start position is beyond end of file") 77 | return 78 | } 79 | 80 | if 0 <= endOffset && endOffset <= file.Size() { 81 | end = file.Pos(int(endOffset)) 82 | } else { 83 | err = fmt.Errorf("end position is beyond end of file") 84 | return 85 | } 86 | 87 | return 88 | } 89 | 90 | // sameFile returns true if x and y have the same basename and denote 91 | // the same file. 92 | // 93 | func sameFile(x, y string) bool { 94 | if filepath.Base(x) == filepath.Base(y) { // (optimisation) 95 | if xi, err := os.Stat(x); err == nil { 96 | if yi, err := os.Stat(y); err == nil { 97 | return os.SameFile(xi, yi) 98 | } 99 | } 100 | } 101 | return false 102 | } 103 | -------------------------------------------------------------------------------- /screencast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stapelberg/expanderr/792eb1fdcf7c23487ebef4034d4f122fadaec013/screencast.gif -------------------------------------------------------------------------------- /screencast.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stapelberg/expanderr/792eb1fdcf7c23487ebef4034d4f122fadaec013/screencast.mp4 -------------------------------------------------------------------------------- /testdata/comment.got/src/comment/comment.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | func logic() (int, error) { 9 | os.Remove("/tmp/foo") // delete 10 | return 0, nil 11 | } 12 | 13 | func main() { 14 | if _, err := logic(); err != nil { 15 | log.Fatal(err) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /testdata/comment.want/src/comment/comment.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | func logic() (int, error) { 9 | if err := os.Remove("/tmp/foo"); err != nil { 10 | return 0, err 11 | } // delete 12 | return 0, nil 13 | } 14 | 15 | func main() { 16 | if _, err := logic(); err != nil { 17 | log.Fatal(err) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /testdata/commentinline.got/src/commentinline/commentinline.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | func logic() (int, error) { 9 | os.Remove("/tmp/foo" /*path*/) // delete 10 | return 0, nil 11 | } 12 | 13 | func main() { 14 | if _, err := logic(); err != nil { 15 | log.Fatal(err) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /testdata/commentinline.want/src/commentinline/commentinline.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | func logic() (int, error) { 9 | if err := os.Remove("/tmp/foo" /*path*/); err != nil { 10 | return 0, err 11 | } // delete 12 | return 0, nil 13 | } 14 | 15 | func main() { 16 | if _, err := logic(); err != nil { 17 | log.Fatal(err) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /testdata/customtypes.got/src/customtypes/customtypes.go: -------------------------------------------------------------------------------- 1 | package err 2 | 3 | import "os" 4 | 5 | type customstr string 6 | 7 | type customstruct struct{} 8 | 9 | type custominterface interface{} 10 | 11 | func err() (customstr, customstruct, custominterface, error) { 12 | a := os.Getwd() 13 | return nil 14 | } 15 | -------------------------------------------------------------------------------- /testdata/customtypes.want/src/customtypes/customtypes.go: -------------------------------------------------------------------------------- 1 | package err 2 | 3 | import "os" 4 | 5 | type customstr string 6 | 7 | type customstruct struct{} 8 | 9 | type custominterface interface{} 10 | 11 | func err() (customstr, customstruct, custominterface, error) { 12 | a, err := os.Getwd() 13 | if err != nil { 14 | return "", customstruct{}, nil, err 15 | } 16 | return nil 17 | } 18 | -------------------------------------------------------------------------------- /testdata/functionliteral.got/src/functionliteral/functionliteral.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | ) 6 | 7 | var boom func() error 8 | 9 | func testing() error { 10 | boom() 11 | return nil 12 | } 13 | 14 | func main() { 15 | if err := testing(); err != nil { 16 | log.Fatal(err.Error()) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /testdata/functionliteral.want/src/functionliteral/functionliteral.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | ) 6 | 7 | var boom func() error 8 | 9 | func testing() error { 10 | if err := boom(); err != nil { 11 | return err 12 | } 13 | return nil 14 | } 15 | 16 | func main() { 17 | if err := testing(); err != nil { 18 | log.Fatal(err.Error()) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /testdata/introduceerr.got/src/introduceerr/introduceerr.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | func logic() (int, error) { 9 | f, ferr := os.Create("/tmp/a") 10 | if ferr != nil { 11 | return 0, ferr 12 | } 13 | var n int 14 | n = f.Write([]byte("foo")) 15 | return 0, nil 16 | } 17 | 18 | func main() { 19 | if _, err := logic(); err != nil { 20 | log.Fatal(err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /testdata/introduceerr.want/src/introduceerr/introduceerr.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | func logic() (int, error) { 9 | f, ferr := os.Create("/tmp/a") 10 | if ferr != nil { 11 | return 0, ferr 12 | } 13 | var n int 14 | var err error 15 | if n, err = f.Write([]byte("foo")); err != nil { 16 | return 0, err 17 | } 18 | return 0, nil 19 | } 20 | 21 | func main() { 22 | if _, err := logic(); err != nil { 23 | log.Fatal(err) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /testdata/multipkg.got/src/lib/lib.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | func Logic() (int, error) { 4 | return 23, nil 5 | } 6 | -------------------------------------------------------------------------------- /testdata/multipkg.got/src/multipkg/multipkg.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "lib" 5 | "log" 6 | ) 7 | 8 | func logic() error { 9 | i := lib.Logic() 10 | } 11 | 12 | func main() { 13 | if err := logic(); err != nil { 14 | log.Fatal(err) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /testdata/multipkg.want/src/multipkg/multipkg.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "lib" 5 | "log" 6 | ) 7 | 8 | func logic() error { 9 | i, err := lib.Logic() 10 | if err != nil { 11 | return err 12 | } 13 | } 14 | 15 | func main() { 16 | if err := logic(); err != nil { 17 | log.Fatal(err) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /testdata/multipkgvendor.got/src/multipkg/multipkg.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "lib" 5 | "log" 6 | ) 7 | 8 | func logic() error { 9 | i := lib.Logic() 10 | } 11 | 12 | func main() { 13 | if err := logic(); err != nil { 14 | log.Fatal(err) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /testdata/multipkgvendor.got/src/multipkg/vendor/lib/lib.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | func Logic() (int, error) { 4 | return 23, nil 5 | } 6 | -------------------------------------------------------------------------------- /testdata/multipkgvendor.want/src/multipkg/multipkg.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "lib" 5 | "log" 6 | ) 7 | 8 | func logic() error { 9 | i, err := lib.Logic() 10 | if err != nil { 11 | return err 12 | } 13 | } 14 | 15 | func main() { 16 | if err := logic(); err != nil { 17 | log.Fatal(err) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /testdata/nocalleereturn.got/src/nocalleereturn/nocalleereturn.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | func logic() error { 9 | os.Clearenv() 10 | } 11 | 12 | func main() { 13 | if err := logic(); err != nil { 14 | log.Fatal(err) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /testdata/nocalleereturn.want/src/nocalleereturn/nocalleereturn.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | func logic() error { 9 | os.Clearenv() 10 | } 11 | 12 | func main() { 13 | if err := logic(); err != nil { 14 | log.Fatal(err) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /testdata/noerrreturn.got/src/noerrreturn/noerrreturn.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | func logic() int { 9 | os.Remove("/tmp/foo") 10 | return 42 11 | } 12 | 13 | func main() { 14 | myInt := logic() 15 | log.Printf("ohai %d", myInt) 16 | } 17 | -------------------------------------------------------------------------------- /testdata/noerrreturn.want/src/noerrreturn/noerrreturn.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | func logic() int { 9 | if err := os.Remove("/tmp/foo"); err != nil { 10 | panic(err) 11 | } 12 | return 42 13 | } 14 | 15 | func main() { 16 | myInt := logic() 17 | log.Printf("ohai %d", myInt) 18 | } 19 | -------------------------------------------------------------------------------- /testdata/nointroduce.got/src/nointroduce/nointroduce.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | func logic() (int, error) { 9 | f, ferr := os.Create("/tmp/a") 10 | if ferr != nil { 11 | return 0, ferr 12 | } 13 | _ = f.Write([]byte("foo")) 14 | return 0, nil 15 | } 16 | 17 | func main() { 18 | if _, err := logic(); err != nil { 19 | log.Fatal(err) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /testdata/nointroduce.want/src/nointroduce/nointroduce.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | func logic() (int, error) { 9 | f, ferr := os.Create("/tmp/a") 10 | if ferr != nil { 11 | return 0, ferr 12 | } 13 | if _, err := f.Write([]byte("foo")); err != nil { 14 | return 0, err 15 | } 16 | return 0, nil 17 | } 18 | 19 | func main() { 20 | if _, err := logic(); err != nil { 21 | log.Fatal(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /testdata/noreturncaller.got/src/noreturncaller/noreturncaller.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | func logic() { 9 | os.Remove("/tmp/foo") 10 | } 11 | 12 | func main() { 13 | log.Printf("ohai") 14 | logic() 15 | } 16 | -------------------------------------------------------------------------------- /testdata/noreturncaller.want/src/noreturncaller/noreturncaller.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | func logic() { 9 | if err := os.Remove("/tmp/foo"); err != nil { 10 | panic(err) 11 | } 12 | } 13 | 14 | func main() { 15 | log.Printf("ohai") 16 | logic() 17 | } 18 | -------------------------------------------------------------------------------- /testdata/pkg.got/src/pkg/pkg1.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | func logic() (string, error) { 4 | return "", nil 5 | } 6 | -------------------------------------------------------------------------------- /testdata/pkg.got/src/pkg/pkg2.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | func caller() error { 4 | s := logic() 5 | } 6 | -------------------------------------------------------------------------------- /testdata/pkg.want/src/pkg/pkg2.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | func caller() error { 4 | s, err := logic() 5 | if err != nil { 6 | return err 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /testdata/presentdouble.got/src/presentdouble/presentdouble.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | ) 7 | 8 | func logic() (int, error) { 9 | n, err := ioutil.ReadAll(nil) 10 | return 0, nil 11 | } 12 | 13 | func main() { 14 | if _, err := logic(); err != nil { 15 | log.Fatal(err) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /testdata/presentdouble.want/src/presentdouble/presentdouble.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | ) 7 | 8 | func logic() (int, error) { 9 | n, err := ioutil.ReadAll(nil) 10 | if err != nil { 11 | return 0, err 12 | } 13 | return 0, nil 14 | } 15 | 16 | func main() { 17 | if _, err := logic(); err != nil { 18 | log.Fatal(err) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /testdata/presentsingle.got/src/presentsingle/presentsingle.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | func logic() (int, error) { 9 | err := os.Remove("a") 10 | return 0, nil 11 | } 12 | 13 | func main() { 14 | if _, err := logic(); err != nil { 15 | log.Fatal(err) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /testdata/presentsingle.want/src/presentsingle/presentsingle.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | func logic() (int, error) { 9 | if err := os.Remove("a"); err != nil { 10 | return 0, err 11 | } 12 | return 0, nil 13 | } 14 | 15 | func main() { 16 | if _, err := logic(); err != nil { 17 | log.Fatal(err) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /testdata/returnerrcall.got/src/returnerrcall/returnerrcall.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | ) 7 | 8 | func logic() (int, string) { 9 | b := ioutil.ReadAll(nil) 10 | return len(b), "hoi" 11 | } 12 | 13 | func main() { 14 | myInt, myStr := logic() 15 | fmt.Printf("%s, my number is %d", myStr, myInt) 16 | } 17 | -------------------------------------------------------------------------------- /testdata/returnerrcall.want/src/returnerrcall/returnerrcall.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | ) 7 | 8 | func logic() (int, string) { 9 | b, err := ioutil.ReadAll(nil) 10 | if err != nil { 11 | log.Fatal(err.Error()) 12 | return 0, "" 13 | } 14 | return len(b), "hoi" 15 | } 16 | 17 | func main() { 18 | myInt, myStr := logic() 19 | fmt.Printf("%s, my number is %d", myStr, myInt) 20 | } 21 | -------------------------------------------------------------------------------- /testdata/singleerror.got/src/singleerror/singleerror.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | func logic() (int, error) { 9 | os.Remove("/tmp/foo") 10 | return 0, nil 11 | } 12 | 13 | func main() { 14 | log.Printf("ohai") 15 | if _, err := logic(); err != nil { 16 | log.Fatal(err) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /testdata/singleerror.want/src/singleerror/singleerror.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | func logic() (int, error) { 9 | if err := os.Remove("/tmp/foo"); err != nil { 10 | return 0, err 11 | } 12 | return 0, nil 13 | } 14 | 15 | func main() { 16 | log.Printf("ohai") 17 | if _, err := logic(); err != nil { 18 | log.Fatal(err) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /testdata/underscore.got/src/underscore/underscore.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | func logic() (int, error) { 9 | f, err := os.Create("/tmp/a") 10 | if err != nil { 11 | return 0, err 12 | } 13 | _ = f.Write([]byte("foo")) 14 | return 0, nil 15 | } 16 | 17 | func main() { 18 | if _, err := logic(); err != nil { 19 | log.Fatal(err) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /testdata/underscore.want/src/underscore/underscore.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | func logic() (int, error) { 9 | f, err := os.Create("/tmp/a") 10 | if err != nil { 11 | return 0, err 12 | } 13 | if _, err := f.Write([]byte("foo")); err != nil { 14 | return 0, err 15 | } 16 | return 0, nil 17 | } 18 | 19 | func main() { 20 | if _, err := logic(); err != nil { 21 | log.Fatal(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /testdata/varanderror.got/src/varanderror/varanderror.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "log" 7 | "strings" 8 | ) 9 | 10 | func logic() (int, error) { 11 | n := io.Copy(ioutil.Discard, strings.NewReader("test")) 12 | return 0, nil 13 | } 14 | 15 | func main() { 16 | log.Printf("ohai") 17 | if _, err := logic(); err != nil { 18 | log.Fatal(err) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /testdata/varanderror.want/src/varanderror/varanderror.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "log" 7 | "strings" 8 | ) 9 | 10 | func logic() (int, error) { 11 | n, err := io.Copy(ioutil.Discard, strings.NewReader("test")) 12 | if err != nil { 13 | return 0, err 14 | } 15 | return 0, nil 16 | } 17 | 18 | func main() { 19 | log.Printf("ohai") 20 | if _, err := logic(); err != nil { 21 | log.Fatal(err) 22 | } 23 | } 24 | --------------------------------------------------------------------------------