// 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/astutil" "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 = stringSetFlag{ "context.WithCancel": true, "context.WithDeadline": true, "context.WithTimeout": true, "context.WithValue": true, "errors.New": true, "fmt.Errorf": true, "fmt.Sprint": true, "fmt.Sprintf": true, "slices.Clip": true, "slices.Compact": true, "slices.CompactFunc": true, "slices.Delete": true, "slices.DeleteFunc": true, "slices.Grow": true, "slices.Insert": true, "slices.Replace": true, "sort.Reverse": true, } 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 := astutil.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 }