| // Copyright 2015 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Package unusedresult defines an analyzer that checks for unused |
| // results of calls to certain functions. |
| package unusedresult |
| |
| // It is tempting to make this analysis inductive: for each function |
| // that tail-calls one of the functions that we check, check those |
| // functions too. However, just because you must use the result of |
| // fmt.Sprintf doesn't mean you need to use the result of every |
| // function that returns a formatted string: it may have other results |
| // and effects. |
| |
| import ( |
| _ "embed" |
| "go/ast" |
| "go/token" |
| "go/types" |
| "sort" |
| "strings" |
| |
| "golang.org/x/tools/go/analysis" |
| "golang.org/x/tools/go/analysis/passes/inspect" |
| "golang.org/x/tools/go/analysis/passes/internal/analysisutil" |
| "golang.org/x/tools/go/ast/inspector" |
| "golang.org/x/tools/go/types/typeutil" |
| ) |
| |
| //go:embed doc.go |
| var doc string |
| |
| var Analyzer = &analysis.Analyzer{ |
| Name: "unusedresult", |
| Doc: analysisutil.MustExtractDoc(doc, "unusedresult"), |
| URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unusedresult", |
| Requires: []*analysis.Analyzer{inspect.Analyzer}, |
| Run: run, |
| } |
| |
| // flags |
| var funcs, stringMethods stringSetFlag |
| |
| func init() { |
| // TODO(adonovan): provide a comment or declaration syntax to |
| // allow users to add their functions to this set using facts. |
| // For example: |
| // |
| // func ignoringTheErrorWouldBeVeryBad() error { |
| // type mustUseResult struct{} // enables vet unusedresult check |
| // ... |
| // } |
| // |
| // ignoringTheErrorWouldBeVeryBad() // oops |
| // |
| |
| // List standard library functions here. |
| // The context.With{Cancel,Deadline,Timeout} entries are |
| // effectively redundant wrt the lostcancel analyzer. |
| funcs.Set("errors.New,fmt.Errorf,fmt.Sprintf,fmt.Sprint,sort.Reverse,context.WithValue,context.WithCancel,context.WithDeadline,context.WithTimeout") |
| Analyzer.Flags.Var(&funcs, "funcs", |
| "comma-separated list of functions whose results must be used") |
| |
| stringMethods.Set("Error,String") |
| Analyzer.Flags.Var(&stringMethods, "stringmethods", |
| "comma-separated list of names of methods of type func() string whose results must be used") |
| } |
| |
| func run(pass *analysis.Pass) (interface{}, error) { |
| inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) |
| |
| // Split functions into (pkg, name) pairs to save allocation later. |
| pkgFuncs := make(map[[2]string]bool, len(funcs)) |
| for s := range funcs { |
| if i := strings.LastIndexByte(s, '.'); i > 0 { |
| pkgFuncs[[2]string{s[:i], s[i+1:]}] = true |
| } |
| } |
| |
| nodeFilter := []ast.Node{ |
| (*ast.ExprStmt)(nil), |
| } |
| inspect.Preorder(nodeFilter, func(n ast.Node) { |
| call, ok := analysisutil.Unparen(n.(*ast.ExprStmt).X).(*ast.CallExpr) |
| if !ok { |
| return // not a call statement |
| } |
| |
| // Call to function or method? |
| fn, ok := typeutil.Callee(pass.TypesInfo, call).(*types.Func) |
| if !ok { |
| return // e.g. var or builtin |
| } |
| |
| if sig := fn.Type().(*types.Signature); sig.Recv() != nil { |
| // method (e.g. foo.String()) |
| if types.Identical(sig, sigNoArgsStringResult) { |
| if stringMethods[fn.Name()] { |
| pass.Reportf(call.Lparen, "result of (%s).%s call not used", |
| sig.Recv().Type(), fn.Name()) |
| } |
| } |
| } else { |
| // package-level function (e.g. fmt.Errorf) |
| if pkgFuncs[[2]string{fn.Pkg().Path(), fn.Name()}] { |
| pass.Reportf(call.Lparen, "result of %s.%s call not used", |
| fn.Pkg().Path(), fn.Name()) |
| } |
| } |
| }) |
| return nil, nil |
| } |
| |
| // func() string |
| var sigNoArgsStringResult = types.NewSignature(nil, nil, |
| types.NewTuple(types.NewVar(token.NoPos, nil, "", types.Typ[types.String])), |
| false) |
| |
| type stringSetFlag map[string]bool |
| |
| func (ss *stringSetFlag) String() string { |
| var items []string |
| for item := range *ss { |
| items = append(items, item) |
| } |
| sort.Strings(items) |
| return strings.Join(items, ",") |
| } |
| |
| func (ss *stringSetFlag) Set(s string) error { |
| m := make(map[string]bool) // clobber previous value |
| if s != "" { |
| for _, name := range strings.Split(s, ",") { |
| if name == "" { |
| continue // TODO: report error? proceed? |
| } |
| m[name] = true |
| } |
| } |
| *ss = m |
| return nil |
| } |