$ git cherry -v cc9d2fb52971 fbb59629db34 + 7362051ae01a0e35956c077c3be5505c70edd200 testdata: add more regression tests + a88ca0234e2c3732a53cd49514fb3877a5d9f1f5 properly record which methods implement interfaces + b762b0b27fa23ebbdfc31df1af4097cfd89a17f6 work properly with method wrapper functions + f59bb08c5c7429b88e0c6e2399b12e71e8d950db testdata: consistently use tabs + 46a5101c55d03117b263b4c5161e5d01353311c1 replace more callgraph code with plain SSA + 1679b9996abdc6431c2147a133e5223ebb86ea60 rewrite foo(bar()) code to work without callgraph + 71b5df77c291f8c5ead5f9ff0a69e0eebf3ae5b2 rewrite "called in return" code without callgraph + fc5b1c74f563d4f2f9ee7d91eda648fc7baebf67 check: replace last use of callgraph + 229ad68a599e2622cf9d3bb87a385b158efe736f fix a minor false negative in callExtract + fbb59629db34a0f69275bc336cc8c3a4dd9fbe5d fix up another false negative in testdata
		
			
				
	
	
		
			980 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			980 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
 | 
						|
// See LICENSE for licensing information
 | 
						|
 | 
						|
// Package check implements the unparam linter. Note that its API is not
 | 
						|
// stable.
 | 
						|
package check
 | 
						|
 | 
						|
import (
 | 
						|
	"bytes"
 | 
						|
	"fmt"
 | 
						|
	"go/ast"
 | 
						|
	"go/constant"
 | 
						|
	"go/parser"
 | 
						|
	"go/printer"
 | 
						|
	"go/token"
 | 
						|
	"go/types"
 | 
						|
	"io"
 | 
						|
	"os"
 | 
						|
	"path/filepath"
 | 
						|
	"regexp"
 | 
						|
	"sort"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	"golang.org/x/tools/go/packages"
 | 
						|
	"golang.org/x/tools/go/ssa"
 | 
						|
	"golang.org/x/tools/go/ssa/ssautil"
 | 
						|
)
 | 
						|
 | 
						|
// UnusedParams returns a list of human-readable issues that point out unused
 | 
						|
// function parameters.
 | 
						|
func UnusedParams(tests bool, exported, debug bool, args ...string) ([]string, error) {
 | 
						|
	wd, err := os.Getwd()
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	c := &Checker{
 | 
						|
		wd:       wd,
 | 
						|
		tests:    tests,
 | 
						|
		exported: exported,
 | 
						|
	}
 | 
						|
	if debug {
 | 
						|
		c.debugLog = os.Stderr
 | 
						|
	}
 | 
						|
	return c.lines(args...)
 | 
						|
}
 | 
						|
 | 
						|
// Checker finds unused parameterss in a program. You probably want to use
 | 
						|
// UnusedParams instead, unless you want to use a *loader.Program and
 | 
						|
// *ssa.Program directly.
 | 
						|
type Checker struct {
 | 
						|
	pkgs []*packages.Package
 | 
						|
	prog *ssa.Program
 | 
						|
 | 
						|
	wd string
 | 
						|
 | 
						|
	tests    bool
 | 
						|
	exported bool
 | 
						|
	debugLog io.Writer
 | 
						|
 | 
						|
	issues []Issue
 | 
						|
 | 
						|
	cachedDeclCounts map[string]map[string]int
 | 
						|
 | 
						|
	// callByPos maps from a call site position to its CallExpr.
 | 
						|
	callByPos map[token.Pos]*ast.CallExpr
 | 
						|
 | 
						|
	// funcBodyByPos maps from a function position to its body. We can't map
 | 
						|
	// to the declaration, as that could be either a FuncDecl or FuncLit.
 | 
						|
	funcBodyByPos map[token.Pos]*ast.BlockStmt
 | 
						|
 | 
						|
	// typesImplementing records the method names that each named type needs
 | 
						|
	// to typecheck properly, as they're required to implement interfaces.
 | 
						|
	typesImplementing map[*types.Named][]string
 | 
						|
 | 
						|
	// localCallSites is a very simple form of a callgraph, only recording
 | 
						|
	// direct function calls within a single package.
 | 
						|
	localCallSites map[*ssa.Function][]ssa.CallInstruction
 | 
						|
 | 
						|
	// These three maps record whether an entire func's signature cannot be
 | 
						|
	// changed, or only its list of parameters or results.
 | 
						|
 | 
						|
	signRequiredBy    map[*ssa.Function]string
 | 
						|
	paramsRequiredBy  map[*ssa.Function]string
 | 
						|
	resultsRequiredBy map[*ssa.Function]string
 | 
						|
}
 | 
						|
 | 
						|
var errorType = types.Universe.Lookup("error").Type()
 | 
						|
 | 
						|
// lines runs the checker and returns the list of readable issues.
 | 
						|
func (c *Checker) lines(args ...string) ([]string, error) {
 | 
						|
	cfg := &packages.Config{
 | 
						|
		Mode:  packages.LoadSyntax,
 | 
						|
		Tests: c.tests,
 | 
						|
	}
 | 
						|
	pkgs, err := packages.Load(cfg, args...)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	if packages.PrintErrors(pkgs) > 0 {
 | 
						|
		return nil, fmt.Errorf("encountered errors")
 | 
						|
	}
 | 
						|
 | 
						|
	prog, _ := ssautil.Packages(pkgs, 0)
 | 
						|
	prog.Build()
 | 
						|
	c.Packages(pkgs)
 | 
						|
	c.ProgramSSA(prog)
 | 
						|
	issues, err := c.Check()
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	lines := make([]string, 0, len(issues))
 | 
						|
	prevLine := ""
 | 
						|
	for _, issue := range issues {
 | 
						|
		fpos := prog.Fset.Position(issue.Pos()).String()
 | 
						|
		if strings.HasPrefix(fpos, c.wd) {
 | 
						|
			fpos = fpos[len(c.wd)+1:]
 | 
						|
		}
 | 
						|
		line := fmt.Sprintf("%s: %s", fpos, issue.Message())
 | 
						|
		if line == prevLine {
 | 
						|
			// Deduplicate lines, since we may look at the same
 | 
						|
			// package multiple times if tests are involved.
 | 
						|
			// TODO: is there a better way to handle this?
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		prevLine = line
 | 
						|
		lines = append(lines, fmt.Sprintf("%s: %s", fpos, issue.Message()))
 | 
						|
	}
 | 
						|
	return lines, nil
 | 
						|
}
 | 
						|
 | 
						|
// Issue identifies a found unused parameter.
 | 
						|
type Issue struct {
 | 
						|
	pos   token.Pos
 | 
						|
	fname string
 | 
						|
	msg   string
 | 
						|
}
 | 
						|
 | 
						|
func (i Issue) Pos() token.Pos  { return i.pos }
 | 
						|
func (i Issue) Message() string { return i.fname + " - " + i.msg }
 | 
						|
 | 
						|
// Program supplies Checker with the needed *loader.Program.
 | 
						|
func (c *Checker) Packages(pkgs []*packages.Package) {
 | 
						|
	c.pkgs = pkgs
 | 
						|
}
 | 
						|
 | 
						|
// ProgramSSA supplies Checker with the needed *ssa.Program.
 | 
						|
func (c *Checker) ProgramSSA(prog *ssa.Program) {
 | 
						|
	c.prog = prog
 | 
						|
}
 | 
						|
 | 
						|
// CheckExportedFuncs sets whether to inspect exported functions
 | 
						|
func (c *Checker) CheckExportedFuncs(exported bool) {
 | 
						|
	c.exported = exported
 | 
						|
}
 | 
						|
 | 
						|
func (c *Checker) debug(format string, a ...interface{}) {
 | 
						|
	if c.debugLog != nil {
 | 
						|
		fmt.Fprintf(c.debugLog, format, a...)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// generatedDoc reports whether a comment text describes its file as being code
 | 
						|
// generated.
 | 
						|
func generatedDoc(text string) bool {
 | 
						|
	return strings.Contains(text, "Code generated") ||
 | 
						|
		strings.Contains(text, "DO NOT EDIT")
 | 
						|
}
 | 
						|
 | 
						|
// eqlConsts reports whether two constant values, possibly nil, are equal.
 | 
						|
func eqlConsts(c1, c2 *ssa.Const) bool {
 | 
						|
	if c1 == nil || c2 == nil {
 | 
						|
		return c1 == c2
 | 
						|
	}
 | 
						|
	if c1.Type() != c2.Type() {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
	if c1.Value == nil || c2.Value == nil {
 | 
						|
		return c1.Value == c2.Value
 | 
						|
	}
 | 
						|
	return constant.Compare(c1.Value, token.EQL, c2.Value)
 | 
						|
}
 | 
						|
 | 
						|
var stdSizes = types.SizesFor("gc", "amd64")
 | 
						|
 | 
						|
// Check runs the unused parameter check and returns the list of found issues,
 | 
						|
// and any error encountered.
 | 
						|
func (c *Checker) Check() ([]Issue, error) {
 | 
						|
	c.cachedDeclCounts = make(map[string]map[string]int)
 | 
						|
	c.callByPos = make(map[token.Pos]*ast.CallExpr)
 | 
						|
	c.funcBodyByPos = make(map[token.Pos]*ast.BlockStmt)
 | 
						|
	c.typesImplementing = make(map[*types.Named][]string)
 | 
						|
 | 
						|
	wantPkg := make(map[*types.Package]*packages.Package)
 | 
						|
	genFiles := make(map[string]bool)
 | 
						|
	for _, pkg := range c.pkgs {
 | 
						|
		wantPkg[pkg.Types] = pkg
 | 
						|
		for _, f := range pkg.Syntax {
 | 
						|
			if len(f.Comments) > 0 && generatedDoc(f.Comments[0].Text()) {
 | 
						|
				fname := c.prog.Fset.Position(f.Pos()).Filename
 | 
						|
				genFiles[fname] = true
 | 
						|
			}
 | 
						|
			ast.Inspect(f, func(node ast.Node) bool {
 | 
						|
				switch node := node.(type) {
 | 
						|
				case *ast.ValueSpec:
 | 
						|
					if len(node.Values) == 0 || node.Type == nil ||
 | 
						|
						len(node.Names) != 1 || node.Names[0].Name != "_" {
 | 
						|
						break
 | 
						|
					}
 | 
						|
					iface, ok := pkg.TypesInfo.TypeOf(node.Type).Underlying().(*types.Interface)
 | 
						|
					if !ok {
 | 
						|
						break
 | 
						|
					}
 | 
						|
					// var _ someIface = named
 | 
						|
					valTyp := pkg.TypesInfo.Types[node.Values[0]].Type
 | 
						|
					c.addImplementing(findNamed(valTyp), iface)
 | 
						|
				case *ast.CallExpr:
 | 
						|
					c.callByPos[node.Lparen] = node
 | 
						|
				// ssa.Function.Pos returns the declaring
 | 
						|
				// FuncLit.Type.Func or the position of the
 | 
						|
				// FuncDecl.Name.
 | 
						|
				case *ast.FuncDecl:
 | 
						|
					c.funcBodyByPos[node.Name.Pos()] = node.Body
 | 
						|
				case *ast.FuncLit:
 | 
						|
					c.funcBodyByPos[node.Pos()] = node.Body
 | 
						|
				}
 | 
						|
				return true
 | 
						|
			})
 | 
						|
		}
 | 
						|
	}
 | 
						|
	allFuncs := ssautil.AllFunctions(c.prog)
 | 
						|
 | 
						|
	// map from *ssa.FreeVar to *ssa.Function, to find function literals
 | 
						|
	// behind closure vars in the simpler scenarios.
 | 
						|
	freeVars := map[*ssa.FreeVar]*ssa.Function{}
 | 
						|
	for curFunc := range allFuncs {
 | 
						|
		for _, b := range curFunc.Blocks {
 | 
						|
			for _, instr := range b.Instrs {
 | 
						|
				instr, ok := instr.(*ssa.MakeClosure)
 | 
						|
				if !ok {
 | 
						|
					continue
 | 
						|
				}
 | 
						|
				fn := instr.Fn.(*ssa.Function)
 | 
						|
				for i, fv := range fn.FreeVars {
 | 
						|
					binding := instr.Bindings[i]
 | 
						|
					alloc, ok := binding.(*ssa.Alloc)
 | 
						|
					if !ok {
 | 
						|
						continue
 | 
						|
					}
 | 
						|
					for _, ref := range *alloc.Referrers() {
 | 
						|
						store, ok := ref.(*ssa.Store)
 | 
						|
						if !ok {
 | 
						|
							continue
 | 
						|
						}
 | 
						|
						if fn, ok := store.Val.(*ssa.Function); ok {
 | 
						|
							freeVars[fv] = fn
 | 
						|
							break
 | 
						|
						}
 | 
						|
					}
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	c.signRequiredBy = make(map[*ssa.Function]string)
 | 
						|
	c.paramsRequiredBy = make(map[*ssa.Function]string)
 | 
						|
	c.resultsRequiredBy = make(map[*ssa.Function]string)
 | 
						|
	c.localCallSites = make(map[*ssa.Function][]ssa.CallInstruction)
 | 
						|
	for curFunc := range allFuncs {
 | 
						|
		if strings.HasPrefix(curFunc.Synthetic, "wrapper for func") {
 | 
						|
			// Synthetic func wrappers are uninteresting, and can
 | 
						|
			// lead to false negatives.
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		for _, b := range curFunc.Blocks {
 | 
						|
			for _, instr := range b.Instrs {
 | 
						|
				if instr, ok := instr.(ssa.CallInstruction); ok {
 | 
						|
					if fn := findFunction(freeVars, instr.Common().Value); fn != nil {
 | 
						|
						c.localCallSites[fn] = append(c.localCallSites[fn], instr)
 | 
						|
					}
 | 
						|
					fn := receivesExtractedArgs(freeVars, instr)
 | 
						|
					if fn != nil {
 | 
						|
						// fn(someFunc()) fixes params
 | 
						|
						c.paramsRequiredBy[fn] = "forwarded call"
 | 
						|
					}
 | 
						|
				}
 | 
						|
				switch instr := instr.(type) {
 | 
						|
				case *ssa.Call:
 | 
						|
					for _, arg := range instr.Call.Args {
 | 
						|
						if fn := findFunction(freeVars, arg); fn != nil {
 | 
						|
							// someFunc(fn)
 | 
						|
							c.signRequiredBy[fn] = "call"
 | 
						|
						}
 | 
						|
					}
 | 
						|
				case *ssa.Phi:
 | 
						|
					for _, val := range instr.Edges {
 | 
						|
						if fn := findFunction(freeVars, val); fn != nil {
 | 
						|
							// nonConstVar = fn
 | 
						|
							c.signRequiredBy[fn] = "phi"
 | 
						|
						}
 | 
						|
					}
 | 
						|
				case *ssa.Return:
 | 
						|
					for _, val := range instr.Results {
 | 
						|
						if fn := findFunction(freeVars, val); fn != nil {
 | 
						|
							// return fn
 | 
						|
							c.signRequiredBy[fn] = "result"
 | 
						|
						}
 | 
						|
					}
 | 
						|
					if call := callExtract(instr, instr.Results); call != nil {
 | 
						|
						if fn := findFunction(freeVars, call.Call.Value); fn != nil {
 | 
						|
							// return fn()
 | 
						|
							c.resultsRequiredBy[fn] = "return"
 | 
						|
						}
 | 
						|
					}
 | 
						|
				case *ssa.Store:
 | 
						|
					as := ""
 | 
						|
					switch instr.Addr.(type) {
 | 
						|
					case *ssa.FieldAddr:
 | 
						|
						// x.someField = fn
 | 
						|
						as = "field"
 | 
						|
					case *ssa.IndexAddr:
 | 
						|
						// x[someIndex] = fn
 | 
						|
						as = "element"
 | 
						|
					case *ssa.Global:
 | 
						|
						// someGlobal = fn
 | 
						|
						as = "global"
 | 
						|
					default:
 | 
						|
						continue
 | 
						|
					}
 | 
						|
					if fn := findFunction(freeVars, instr.Val); fn != nil {
 | 
						|
						c.signRequiredBy[fn] = as
 | 
						|
					}
 | 
						|
				case *ssa.MakeInterface:
 | 
						|
					// someIface(named)
 | 
						|
					iface := instr.Type().Underlying().(*types.Interface)
 | 
						|
					c.addImplementing(findNamed(instr.X.Type()), iface)
 | 
						|
 | 
						|
					if fn := findFunction(freeVars, instr.X); fn != nil {
 | 
						|
						// emptyIface = fn
 | 
						|
						c.signRequiredBy[fn] = "interface"
 | 
						|
					}
 | 
						|
				case *ssa.ChangeType:
 | 
						|
					if fn := findFunction(freeVars, instr.X); fn != nil {
 | 
						|
						// someType(fn)
 | 
						|
						c.signRequiredBy[fn] = "type conversion"
 | 
						|
					}
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	for fn := range allFuncs {
 | 
						|
		switch {
 | 
						|
		case fn.Pkg == nil: // builtin?
 | 
						|
			continue
 | 
						|
		case fn.Name() == "init":
 | 
						|
			continue
 | 
						|
		case len(fn.Blocks) == 0: // stub
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		pkg := wantPkg[fn.Pkg.Pkg]
 | 
						|
		if pkg == nil { // not part of given pkgs
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		if c.exported || fn.Pkg.Pkg.Name() == "main" {
 | 
						|
			// we want exported funcs, or this is a main package so
 | 
						|
			// nothing is exported
 | 
						|
		} else if strings.Contains(fn.Name(), "$") {
 | 
						|
			// anonymous function within a possibly exported func
 | 
						|
		} else if ast.IsExported(fn.Name()) {
 | 
						|
			continue // user doesn't want to change signatures here
 | 
						|
		}
 | 
						|
		fname := c.prog.Fset.Position(fn.Pos()).Filename
 | 
						|
		if genFiles[fname] {
 | 
						|
			continue // generated file
 | 
						|
		}
 | 
						|
 | 
						|
		c.checkFunc(fn, pkg)
 | 
						|
	}
 | 
						|
	sort.Slice(c.issues, func(i, j int) bool {
 | 
						|
		p1 := c.prog.Fset.Position(c.issues[i].Pos())
 | 
						|
		p2 := c.prog.Fset.Position(c.issues[j].Pos())
 | 
						|
		if p1.Filename == p2.Filename {
 | 
						|
			return p1.Offset < p2.Offset
 | 
						|
		}
 | 
						|
		return p1.Filename < p2.Filename
 | 
						|
	})
 | 
						|
	return c.issues, nil
 | 
						|
}
 | 
						|
 | 
						|
func stringsContains(list []string, elem string) bool {
 | 
						|
	for _, e := range list {
 | 
						|
		if e == elem {
 | 
						|
			return true
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return false
 | 
						|
}
 | 
						|
 | 
						|
func (c *Checker) addImplementing(named *types.Named, iface *types.Interface) {
 | 
						|
	if named == nil || iface == nil {
 | 
						|
		return
 | 
						|
	}
 | 
						|
	list := c.typesImplementing[named]
 | 
						|
	for i := 0; i < iface.NumMethods(); i++ {
 | 
						|
		name := iface.Method(i).Name()
 | 
						|
		if !stringsContains(list, name) {
 | 
						|
			list = append(list, name)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	c.typesImplementing[named] = list
 | 
						|
}
 | 
						|
 | 
						|
func findNamed(typ types.Type) *types.Named {
 | 
						|
	switch typ := typ.(type) {
 | 
						|
	case *types.Pointer:
 | 
						|
		return findNamed(typ.Elem())
 | 
						|
	case *types.Named:
 | 
						|
		return typ
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// findFunction returns the function that is behind a value, if any.
 | 
						|
func findFunction(freeVars map[*ssa.FreeVar]*ssa.Function, value ssa.Value) *ssa.Function {
 | 
						|
	switch value := value.(type) {
 | 
						|
	case *ssa.Function:
 | 
						|
		name := value.Name()
 | 
						|
		if strings.HasSuffix(name, "$thunk") || strings.HasSuffix(name, "$bound") {
 | 
						|
			// Method wrapper funcs contain a single block, which
 | 
						|
			// calls the function being wrapped, and returns. We
 | 
						|
			// want the function being wrapped.
 | 
						|
			for _, instr := range value.Blocks[0].Instrs {
 | 
						|
				call, ok := instr.(*ssa.Call)
 | 
						|
				if !ok {
 | 
						|
					continue
 | 
						|
				}
 | 
						|
				if callee := call.Call.StaticCallee(); callee != nil {
 | 
						|
					return callee
 | 
						|
				}
 | 
						|
			}
 | 
						|
			return nil // no static callee?
 | 
						|
		}
 | 
						|
		return value
 | 
						|
	case *ssa.MakeClosure:
 | 
						|
		// closure of a func
 | 
						|
		return findFunction(freeVars, value.Fn)
 | 
						|
	case *ssa.UnOp:
 | 
						|
		if value.Op != token.MUL {
 | 
						|
			break
 | 
						|
		}
 | 
						|
		if fv, ok := value.X.(*ssa.FreeVar); ok {
 | 
						|
			return freeVars[fv]
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// addIssue records a newly found unused parameter.
 | 
						|
func (c *Checker) addIssue(fn *ssa.Function, pos token.Pos, format string, args ...interface{}) {
 | 
						|
	c.issues = append(c.issues, Issue{
 | 
						|
		pos:   pos,
 | 
						|
		fname: fn.RelString(fn.Package().Pkg),
 | 
						|
		msg:   fmt.Sprintf(format, args...),
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
// constValueString is cnst.Value.String() without panicking on untyped nils.
 | 
						|
func constValueString(cnst *ssa.Const) string {
 | 
						|
	if cnst.Value == nil {
 | 
						|
		return "nil"
 | 
						|
	}
 | 
						|
	return cnst.Value.String()
 | 
						|
}
 | 
						|
 | 
						|
// checkFunc checks a single function for unused parameters.
 | 
						|
func (c *Checker) checkFunc(fn *ssa.Function, pkg *packages.Package) {
 | 
						|
	c.debug("func %s\n", fn.RelString(fn.Package().Pkg))
 | 
						|
	if dummyImpl(fn.Blocks[0]) { // panic implementation
 | 
						|
		c.debug("  skip - dummy implementation\n")
 | 
						|
		return
 | 
						|
	}
 | 
						|
	if by := c.signRequiredBy[fn]; by != "" {
 | 
						|
		c.debug("  skip - func signature required by %s\n", by)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	if recv := fn.Signature.Recv(); recv != nil {
 | 
						|
		named := findNamed(recv.Type())
 | 
						|
		if stringsContains(c.typesImplementing[named], fn.Name()) {
 | 
						|
			c.debug("  skip - method required to implement an interface\n")
 | 
						|
			return
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if c.multipleImpls(pkg, fn) {
 | 
						|
		c.debug("  skip - multiple implementations via build tags\n")
 | 
						|
		return
 | 
						|
	}
 | 
						|
	paramsBy := c.paramsRequiredBy[fn]
 | 
						|
	resultsBy := c.resultsRequiredBy[fn]
 | 
						|
	callSites := c.localCallSites[fn]
 | 
						|
 | 
						|
	results := fn.Signature.Results()
 | 
						|
	sameConsts := make([]*ssa.Const, results.Len())
 | 
						|
	numRets := 0
 | 
						|
	allRetsExtracting := true
 | 
						|
	for _, block := range fn.Blocks {
 | 
						|
		if resultsBy != "" {
 | 
						|
			continue // we can't change the returns
 | 
						|
		}
 | 
						|
		last := block.Instrs[len(block.Instrs)-1]
 | 
						|
		ret, ok := last.(*ssa.Return)
 | 
						|
		if !ok {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		for i, val := range ret.Results {
 | 
						|
			if _, ok := val.(*ssa.Extract); !ok {
 | 
						|
				allRetsExtracting = false
 | 
						|
			}
 | 
						|
			cnst := constValue(val)
 | 
						|
			if numRets == 0 {
 | 
						|
				sameConsts[i] = cnst
 | 
						|
			} else if !eqlConsts(sameConsts[i], cnst) {
 | 
						|
				sameConsts[i] = nil
 | 
						|
			}
 | 
						|
		}
 | 
						|
		numRets++
 | 
						|
	}
 | 
						|
	for i, cnst := range sameConsts {
 | 
						|
		if cnst == nil {
 | 
						|
			// no consistent returned constant
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		if cnst.Value != nil && numRets == 1 {
 | 
						|
			// just one return and it's not untyped nil (too many
 | 
						|
			// false positives)
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		res := results.At(i)
 | 
						|
		name := paramDesc(i, res)
 | 
						|
		c.addIssue(fn, res.Pos(), "result %s is always %s", name, constValueString(cnst))
 | 
						|
	}
 | 
						|
 | 
						|
resLoop:
 | 
						|
	for i := 0; i < results.Len(); i++ {
 | 
						|
		if resultsBy != "" {
 | 
						|
			continue // we can't change the returns
 | 
						|
		}
 | 
						|
		if allRetsExtracting {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		res := results.At(i)
 | 
						|
		if res.Type() == errorType {
 | 
						|
			// "error is never used" is less useful, and it's up to
 | 
						|
			// tools like errcheck anyway.
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		count := 0
 | 
						|
		for _, site := range callSites {
 | 
						|
			val := site.Value()
 | 
						|
			if val == nil { // e.g. go statement
 | 
						|
				count++
 | 
						|
				continue
 | 
						|
			}
 | 
						|
			for _, instr := range *val.Referrers() {
 | 
						|
				extract, ok := instr.(*ssa.Extract)
 | 
						|
				if !ok {
 | 
						|
					continue resLoop // direct, real use
 | 
						|
				}
 | 
						|
				if extract.Index != i {
 | 
						|
					continue // not the same result param
 | 
						|
				}
 | 
						|
				if len(*extract.Referrers()) > 0 {
 | 
						|
					continue resLoop // real use after extraction
 | 
						|
				}
 | 
						|
			}
 | 
						|
			count++
 | 
						|
		}
 | 
						|
		if count < 2 {
 | 
						|
			continue // require ignoring at least twice
 | 
						|
		}
 | 
						|
		name := paramDesc(i, res)
 | 
						|
		c.addIssue(fn, res.Pos(), "result %s is never used", name)
 | 
						|
	}
 | 
						|
 | 
						|
	for i, par := range fn.Params {
 | 
						|
		if paramsBy != "" {
 | 
						|
			continue // we can't change the params
 | 
						|
		}
 | 
						|
		if i == 0 && fn.Signature.Recv() != nil { // receiver
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		c.debug("%s\n", par.String())
 | 
						|
		switch par.Object().Name() {
 | 
						|
		case "", "_": // unnamed
 | 
						|
			c.debug("  skip - unnamed\n")
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		if stdSizes.Sizeof(par.Type()) == 0 {
 | 
						|
			c.debug("  skip - zero size\n")
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		reason := "is unused"
 | 
						|
		constStr := c.alwaysReceivedConst(callSites, par, i)
 | 
						|
		if constStr != "" {
 | 
						|
			reason = fmt.Sprintf("always receives %s", constStr)
 | 
						|
		} else if c.anyRealUse(par, i, pkg) {
 | 
						|
			c.debug("  skip - used somewhere in the func body\n")
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		c.addIssue(fn, par.Pos(), "%s %s", par.Name(), reason)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// nodeStr stringifies a syntax tree node. It is only meant for simple nodes,
 | 
						|
// such as short value expressions.
 | 
						|
func nodeStr(node ast.Node) string {
 | 
						|
	var buf bytes.Buffer
 | 
						|
	fset := token.NewFileSet()
 | 
						|
	if err := printer.Fprint(&buf, fset, node); err != nil {
 | 
						|
		panic(err)
 | 
						|
	}
 | 
						|
	return buf.String()
 | 
						|
}
 | 
						|
 | 
						|
// alwaysReceivedConst checks if a function parameter always receives the same
 | 
						|
// constant value, given a list of inbound calls. If it does, a description of
 | 
						|
// the value is returned. If not, an empty string is returned.
 | 
						|
//
 | 
						|
// This function is used to recommend that the parameter be replaced by a direct
 | 
						|
// use of the constant. To avoid false positives, the function will return false
 | 
						|
// if the number of inbound calls is too low.
 | 
						|
func (c *Checker) alwaysReceivedConst(callSites []ssa.CallInstruction, par *ssa.Parameter, pos int) string {
 | 
						|
	if len(callSites) < 4 {
 | 
						|
		// We can't possibly receive the same constant value enough
 | 
						|
		// times, hence a potential false positive.
 | 
						|
		return ""
 | 
						|
	}
 | 
						|
	if ast.IsExported(par.Parent().Name()) {
 | 
						|
		// we might not have all call sites for an exported func
 | 
						|
		return ""
 | 
						|
	}
 | 
						|
	var seen *ssa.Const
 | 
						|
	origPos := pos
 | 
						|
	if par.Parent().Signature.Recv() != nil {
 | 
						|
		// go/ast's CallExpr.Args does not include the receiver, but
 | 
						|
		// go/ssa's equivalent does.
 | 
						|
		origPos--
 | 
						|
	}
 | 
						|
	seenOrig := ""
 | 
						|
	for _, site := range callSites {
 | 
						|
		call := site.Common()
 | 
						|
		if pos >= len(call.Args) {
 | 
						|
			// TODO: investigate? Weird crash in
 | 
						|
			// internal/x/net/http2/hpack/hpack_test.go, where we
 | 
						|
			// roughly do: "at := d.mustAt; at(3)".
 | 
						|
			return ""
 | 
						|
		}
 | 
						|
		cnst := constValue(call.Args[pos])
 | 
						|
		if cnst == nil {
 | 
						|
			return "" // not a constant
 | 
						|
		}
 | 
						|
		origArg := ""
 | 
						|
		origCall := c.callByPos[call.Pos()]
 | 
						|
		if origPos >= len(origCall.Args) {
 | 
						|
			// variadic parameter that wasn't given
 | 
						|
		} else {
 | 
						|
			origArg = nodeStr(origCall.Args[origPos])
 | 
						|
		}
 | 
						|
		if seen == nil {
 | 
						|
			seen = cnst // first constant
 | 
						|
			seenOrig = origArg
 | 
						|
		} else if !eqlConsts(seen, cnst) {
 | 
						|
			return "" // different constants
 | 
						|
		} else if origArg != seenOrig {
 | 
						|
			seenOrig = ""
 | 
						|
		}
 | 
						|
	}
 | 
						|
	seenStr := constValueString(seen)
 | 
						|
	if seenOrig != "" && seenStr != seenOrig {
 | 
						|
		return fmt.Sprintf("%s (%s)", seenOrig, seenStr)
 | 
						|
	}
 | 
						|
	return seenStr
 | 
						|
}
 | 
						|
 | 
						|
func constValue(value ssa.Value) *ssa.Const {
 | 
						|
	switch x := value.(type) {
 | 
						|
	case *ssa.Const:
 | 
						|
		return x
 | 
						|
	case *ssa.MakeInterface:
 | 
						|
		return constValue(x.X)
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// anyRealUse reports whether a parameter has any relevant use within its
 | 
						|
// function body. Certain uses are ignored, such as recursive calls where the
 | 
						|
// parameter is re-used as itself.
 | 
						|
func (c *Checker) anyRealUse(par *ssa.Parameter, pos int, pkg *packages.Package) bool {
 | 
						|
	refs := *par.Referrers()
 | 
						|
	if len(refs) == 0 {
 | 
						|
		// Look for any uses like "_ = par", which are the developer's
 | 
						|
		// way to tell they want to keep the parameter. SSA does not
 | 
						|
		// keep that kind of statement around.
 | 
						|
		body := c.funcBodyByPos[par.Parent().Pos()]
 | 
						|
		any := false
 | 
						|
		ast.Inspect(body, func(node ast.Node) bool {
 | 
						|
			if any {
 | 
						|
				return false
 | 
						|
			}
 | 
						|
			asgn, ok := node.(*ast.AssignStmt)
 | 
						|
			if !ok || asgn.Tok != token.ASSIGN || len(asgn.Lhs) != 1 || len(asgn.Rhs) != 1 {
 | 
						|
				return true
 | 
						|
			}
 | 
						|
			if left, ok := asgn.Lhs[0].(*ast.Ident); !ok || left.Name != "_" {
 | 
						|
				return true
 | 
						|
			}
 | 
						|
			if right, ok := asgn.Rhs[0].(*ast.Ident); ok {
 | 
						|
				obj := pkg.TypesInfo.Uses[right]
 | 
						|
				if obj != nil && obj.Pos() == par.Pos() {
 | 
						|
					any = true
 | 
						|
				}
 | 
						|
			}
 | 
						|
			return true
 | 
						|
		})
 | 
						|
		return any
 | 
						|
	}
 | 
						|
refLoop:
 | 
						|
	for _, ref := range refs {
 | 
						|
		switch x := ref.(type) {
 | 
						|
		case *ssa.Call:
 | 
						|
			if x.Call.Value != par.Parent() {
 | 
						|
				return true // not a recursive call
 | 
						|
			}
 | 
						|
			for i, arg := range x.Call.Args {
 | 
						|
				if arg != par {
 | 
						|
					continue
 | 
						|
				}
 | 
						|
				if i == pos {
 | 
						|
					// reused directly in a recursive call
 | 
						|
					continue refLoop
 | 
						|
				}
 | 
						|
			}
 | 
						|
			return true
 | 
						|
		case *ssa.Store:
 | 
						|
			if insertedStore(x) {
 | 
						|
				continue // inserted by go/ssa, not from the code
 | 
						|
			}
 | 
						|
			return true
 | 
						|
		default:
 | 
						|
			return true
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return false
 | 
						|
}
 | 
						|
 | 
						|
// insertedStore reports whether a SSA instruction was inserted by the SSA
 | 
						|
// building algorithm. That is, the store was not directly translated from an
 | 
						|
// original Go statement.
 | 
						|
func insertedStore(instr ssa.Instruction) bool {
 | 
						|
	if instr.Pos() != token.NoPos {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
	store, ok := instr.(*ssa.Store)
 | 
						|
	if !ok {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
	alloc, ok := store.Addr.(*ssa.Alloc)
 | 
						|
	// we want exactly one use of this alloc value for it to be
 | 
						|
	// inserted by ssa and dummy - the alloc instruction itself.
 | 
						|
	return ok && len(*alloc.Referrers()) == 1
 | 
						|
}
 | 
						|
 | 
						|
// rxHarmlessCall matches all the function expression strings which are allowed
 | 
						|
// in a dummy implementation.
 | 
						|
var rxHarmlessCall = regexp.MustCompile(`(?i)\b(log(ger)?|errors)\b|\bf?print|errorf?$`)
 | 
						|
 | 
						|
// dummyImpl reports whether a block is a dummy implementation. This is
 | 
						|
// true if the block will almost immediately panic, throw or return
 | 
						|
// constants only.
 | 
						|
func dummyImpl(blk *ssa.BasicBlock) bool {
 | 
						|
	var ops [8]*ssa.Value
 | 
						|
	for _, instr := range blk.Instrs {
 | 
						|
		if insertedStore(instr) {
 | 
						|
			continue // inserted by go/ssa, not from the code
 | 
						|
		}
 | 
						|
		for _, val := range instr.Operands(ops[:0]) {
 | 
						|
			switch x := (*val).(type) {
 | 
						|
			case nil, *ssa.Const, *ssa.ChangeType, *ssa.Alloc,
 | 
						|
				*ssa.MakeInterface, *ssa.MakeMap,
 | 
						|
				*ssa.Function, *ssa.Global,
 | 
						|
				*ssa.IndexAddr, *ssa.Slice,
 | 
						|
				*ssa.UnOp, *ssa.Parameter:
 | 
						|
			case *ssa.Call:
 | 
						|
				if rxHarmlessCall.MatchString(x.Call.Value.String()) {
 | 
						|
					continue
 | 
						|
				}
 | 
						|
			default:
 | 
						|
				return false
 | 
						|
			}
 | 
						|
		}
 | 
						|
		switch x := instr.(type) {
 | 
						|
		case *ssa.Alloc, *ssa.Store, *ssa.UnOp, *ssa.BinOp,
 | 
						|
			*ssa.MakeInterface, *ssa.MakeMap, *ssa.Extract,
 | 
						|
			*ssa.IndexAddr, *ssa.FieldAddr, *ssa.Slice,
 | 
						|
			*ssa.Lookup, *ssa.ChangeType, *ssa.TypeAssert,
 | 
						|
			*ssa.Convert, *ssa.ChangeInterface:
 | 
						|
			// non-trivial expressions in panic/log/print calls
 | 
						|
		case *ssa.Return, *ssa.Panic:
 | 
						|
			return true
 | 
						|
		case *ssa.Call:
 | 
						|
			if rxHarmlessCall.MatchString(x.Call.Value.String()) {
 | 
						|
				continue
 | 
						|
			}
 | 
						|
			return x.Call.Value.Name() == "throw" // runtime's panic
 | 
						|
		default:
 | 
						|
			return false
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return false
 | 
						|
}
 | 
						|
 | 
						|
// declCounts reports how many times a package's functions are declared. This is
 | 
						|
// used, for example, to find if a function has many implementations.
 | 
						|
//
 | 
						|
// Since this function parses all of the package's Go source files on disk, its
 | 
						|
// results are cached.
 | 
						|
func (c *Checker) declCounts(pkgDir string, pkgName string) map[string]int {
 | 
						|
	key := pkgDir + ":" + pkgName
 | 
						|
	if m, ok := c.cachedDeclCounts[key]; ok {
 | 
						|
		return m
 | 
						|
	}
 | 
						|
	fset := token.NewFileSet()
 | 
						|
	pkgs, err := parser.ParseDir(fset, pkgDir, nil, 0)
 | 
						|
	if err != nil {
 | 
						|
		// Don't panic or error here. In some part of the go/* libraries
 | 
						|
		// stack, we sometimes end up with a package directory that is
 | 
						|
		// wrong. That's not our fault, and we can't simply break the
 | 
						|
		// tool until we fix the underlying issue.
 | 
						|
		println(err.Error())
 | 
						|
		c.cachedDeclCounts[pkgDir] = nil
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
	if len(pkgs) == 0 {
 | 
						|
		// TODO: investigate why this started happening after switching
 | 
						|
		// to go/packages
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
	pkg := pkgs[pkgName]
 | 
						|
	count := make(map[string]int)
 | 
						|
	for _, file := range pkg.Files {
 | 
						|
		for _, decl := range file.Decls {
 | 
						|
			fd, ok := decl.(*ast.FuncDecl)
 | 
						|
			if !ok {
 | 
						|
				continue
 | 
						|
			}
 | 
						|
			name := recvPrefix(fd.Recv) + fd.Name.Name
 | 
						|
			count[name]++
 | 
						|
		}
 | 
						|
	}
 | 
						|
	c.cachedDeclCounts[key] = count
 | 
						|
	return count
 | 
						|
}
 | 
						|
 | 
						|
// recvPrefix returns the string prefix for a receiver field list. Star
 | 
						|
// expressions are ignored, so as to conservatively assume that pointer and
 | 
						|
// non-pointer receivers may still implement the same function.
 | 
						|
//
 | 
						|
// For example, for "function (*Foo) Bar()", recvPrefix will return "Foo.".
 | 
						|
func recvPrefix(recv *ast.FieldList) string {
 | 
						|
	if recv == nil {
 | 
						|
		return ""
 | 
						|
	}
 | 
						|
	expr := recv.List[0].Type
 | 
						|
	for {
 | 
						|
		star, ok := expr.(*ast.StarExpr)
 | 
						|
		if !ok {
 | 
						|
			break
 | 
						|
		}
 | 
						|
		expr = star.X
 | 
						|
	}
 | 
						|
	id := expr.(*ast.Ident)
 | 
						|
	return id.Name + "."
 | 
						|
}
 | 
						|
 | 
						|
// multipleImpls reports whether a function has multiple implementations in the
 | 
						|
// source code. For example, if there are different function bodies depending on
 | 
						|
// the operating system or architecture. That tends to mean that an unused
 | 
						|
// parameter in one implementation may not be unused in another.
 | 
						|
func (c *Checker) multipleImpls(pkg *packages.Package, fn *ssa.Function) bool {
 | 
						|
	if fn.Parent() != nil { // nested func
 | 
						|
		return false
 | 
						|
	}
 | 
						|
	path := c.prog.Fset.Position(fn.Pos()).Filename
 | 
						|
	count := c.declCounts(filepath.Dir(path), pkg.Types.Name())
 | 
						|
	name := fn.Name()
 | 
						|
	if recv := fn.Signature.Recv(); recv != nil {
 | 
						|
		named := findNamed(recv.Type())
 | 
						|
		name = named.Obj().Name() + "." + name
 | 
						|
	}
 | 
						|
	return count[name] > 1
 | 
						|
}
 | 
						|
 | 
						|
// receivesExtractedArgs returns the statically called function, if its multiple
 | 
						|
// arguments were all received via another function call. That is, if a call to
 | 
						|
// function "foo" was of the form "foo(bar())". This often means that the
 | 
						|
// parameters in "foo" are difficult to remove, even if unused.
 | 
						|
func receivesExtractedArgs(freeVars map[*ssa.FreeVar]*ssa.Function, call ssa.CallInstruction) *ssa.Function {
 | 
						|
	comm := call.Common()
 | 
						|
	callee := findFunction(freeVars, comm.Value)
 | 
						|
	if callee == nil {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
	if callee.Signature.Params().Len() < 2 {
 | 
						|
		// there aren't multiple parameters
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
	args := comm.Args
 | 
						|
	if callee.Signature.Recv() != nil {
 | 
						|
		// skip the receiver argument
 | 
						|
		args = args[1:]
 | 
						|
	}
 | 
						|
	if c := callExtract(call, args); c != nil {
 | 
						|
		return callee
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// callExtract returns the call instruction fn(...) if it is used directly as
 | 
						|
// arguments to the parent instruction, such as fn2(fn(...)) or return fn(...).
 | 
						|
func callExtract(parent ssa.Instruction, values []ssa.Value) *ssa.Call {
 | 
						|
	if len(values) == 1 {
 | 
						|
		if call, ok := values[0].(*ssa.Call); ok {
 | 
						|
			return call
 | 
						|
		}
 | 
						|
	}
 | 
						|
	var prev *ssa.Call
 | 
						|
	for i, val := range values {
 | 
						|
		ext, ok := val.(*ssa.Extract)
 | 
						|
		if !ok {
 | 
						|
			return nil
 | 
						|
		}
 | 
						|
		if ext.Index != i {
 | 
						|
			return nil // not extracted in the same order
 | 
						|
		}
 | 
						|
		call, ok := ext.Tuple.(*ssa.Call)
 | 
						|
		if !ok {
 | 
						|
			return nil // not a call
 | 
						|
		}
 | 
						|
		if prev == nil {
 | 
						|
			prev = call
 | 
						|
		} else if prev != call {
 | 
						|
			return nil // not the same calls
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if prev == nil {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
	if prev.Call.Signature().Results().Len() != len(values) {
 | 
						|
		return nil // not extracting all the results
 | 
						|
	}
 | 
						|
	if prev.Pos() < parent.Pos() {
 | 
						|
		// Of the form:
 | 
						|
		//
 | 
						|
		//   a, b := fn()
 | 
						|
		//   fn2(a, b)
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
	return prev
 | 
						|
}
 | 
						|
 | 
						|
// paramDesc returns a string describing a parameter variable. If the parameter
 | 
						|
// had no name, the function will fall back to describing the parameter by its
 | 
						|
// position within the parameter list and its type.
 | 
						|
func paramDesc(i int, v *types.Var) string {
 | 
						|
	name := v.Name()
 | 
						|
	if name != "" && name != "_" {
 | 
						|
		return name
 | 
						|
	}
 | 
						|
	return fmt.Sprintf("%d (%s)", i, v.Type().String())
 | 
						|
}
 |