blob: cb487a217756ae37a08c64ed72e0211003803a44 [file] [log] [blame]
// 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
}