gopkg.in/alecthomas/gometalinter.v3@v3.0.0/_linters/src/mvdan.cc/unparam/check/check.go (about)

     1  // Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
     2  // See LICENSE for licensing information
     3  
     4  // Package check implements the unparam linter. Note that its API is not
     5  // stable.
     6  package check // import "mvdan.cc/unparam/check"
     7  
     8  import (
     9  	"fmt"
    10  	"go/ast"
    11  	"go/constant"
    12  	"go/parser"
    13  	"go/token"
    14  	"go/types"
    15  	"io"
    16  	"os"
    17  	"path/filepath"
    18  	"regexp"
    19  	"sort"
    20  	"strings"
    21  
    22  	"golang.org/x/tools/go/callgraph"
    23  	"golang.org/x/tools/go/callgraph/cha"
    24  	"golang.org/x/tools/go/loader"
    25  	"golang.org/x/tools/go/ssa"
    26  	"golang.org/x/tools/go/ssa/ssautil"
    27  
    28  	"github.com/kisielk/gotool"
    29  	"mvdan.cc/lint"
    30  )
    31  
    32  func UnusedParams(tests, debug bool, args ...string) ([]string, error) {
    33  	wd, err := os.Getwd()
    34  	if err != nil {
    35  		return nil, err
    36  	}
    37  	c := &Checker{
    38  		wd:    wd,
    39  		tests: tests,
    40  	}
    41  	if debug {
    42  		c.debugLog = os.Stderr
    43  	}
    44  	return c.lines(args...)
    45  }
    46  
    47  type Checker struct {
    48  	lprog *loader.Program
    49  	prog  *ssa.Program
    50  
    51  	wd string
    52  
    53  	tests    bool
    54  	debugLog io.Writer
    55  
    56  	cachedDeclCounts map[string]map[string]int
    57  }
    58  
    59  var (
    60  	_ lint.Checker = (*Checker)(nil)
    61  	_ lint.WithSSA = (*Checker)(nil)
    62  
    63  	skipValue = new(ssa.Value)
    64  )
    65  
    66  func (c *Checker) lines(args ...string) ([]string, error) {
    67  	paths := gotool.ImportPaths(args)
    68  	var conf loader.Config
    69  	if _, err := conf.FromArgs(paths, c.tests); err != nil {
    70  		return nil, err
    71  	}
    72  	lprog, err := conf.Load()
    73  	if err != nil {
    74  		return nil, err
    75  	}
    76  	prog := ssautil.CreateProgram(lprog, 0)
    77  	prog.Build()
    78  	c.Program(lprog)
    79  	c.ProgramSSA(prog)
    80  	issues, err := c.Check()
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  	lines := make([]string, len(issues))
    85  	for i, issue := range issues {
    86  		fpos := prog.Fset.Position(issue.Pos()).String()
    87  		if strings.HasPrefix(fpos, c.wd) {
    88  			fpos = fpos[len(c.wd)+1:]
    89  		}
    90  		lines[i] = fmt.Sprintf("%s: %s", fpos, issue.Message())
    91  	}
    92  	return lines, nil
    93  }
    94  
    95  type Issue struct {
    96  	pos token.Pos
    97  	msg string
    98  }
    99  
   100  func (i Issue) Pos() token.Pos  { return i.pos }
   101  func (i Issue) Message() string { return i.msg }
   102  
   103  func (c *Checker) Program(lprog *loader.Program) {
   104  	c.lprog = lprog
   105  }
   106  
   107  func (c *Checker) ProgramSSA(prog *ssa.Program) {
   108  	c.prog = prog
   109  }
   110  
   111  func (c *Checker) debug(format string, a ...interface{}) {
   112  	if c.debugLog != nil {
   113  		fmt.Fprintf(c.debugLog, format, a...)
   114  	}
   115  }
   116  
   117  func (c *Checker) Check() ([]lint.Issue, error) {
   118  	c.cachedDeclCounts = make(map[string]map[string]int)
   119  	wantPkg := make(map[*types.Package]*loader.PackageInfo)
   120  	for _, info := range c.lprog.InitialPackages() {
   121  		wantPkg[info.Pkg] = info
   122  	}
   123  	cg := cha.CallGraph(c.prog)
   124  
   125  	var issues []lint.Issue
   126  funcLoop:
   127  	for fn := range ssautil.AllFunctions(c.prog) {
   128  		if fn.Pkg == nil { // builtin?
   129  			continue
   130  		}
   131  		if len(fn.Blocks) == 0 { // stub
   132  			continue
   133  		}
   134  		info := wantPkg[fn.Pkg.Pkg]
   135  		if info == nil { // not part of given pkgs
   136  			continue
   137  		}
   138  		c.debug("func %s\n", fn.String())
   139  		if dummyImpl(fn.Blocks[0]) { // panic implementation
   140  			c.debug("  skip - dummy implementation\n")
   141  			continue
   142  		}
   143  		for _, edge := range cg.Nodes[fn].In {
   144  			switch edge.Site.Common().Value.(type) {
   145  			case *ssa.Function:
   146  			default:
   147  				// called via a parameter or field, type
   148  				// is set in stone.
   149  				c.debug("  skip - type is required via call\n")
   150  				continue funcLoop
   151  			}
   152  		}
   153  		if c.multipleImpls(info, fn) {
   154  			c.debug("  skip - multiple implementations via build tags\n")
   155  			continue
   156  		}
   157  
   158  		callers := cg.Nodes[fn].In
   159  		results := fn.Signature.Results()
   160  		// skip exported funcs, as well as those that are
   161  		// entirely unused
   162  		if !ast.IsExported(fn.Name()) && len(callers) > 0 {
   163  		resLoop:
   164  			for i := 0; i < results.Len(); i++ {
   165  				for _, edge := range callers {
   166  					val := edge.Site.Value()
   167  					if val == nil { // e.g. go statement
   168  						continue
   169  					}
   170  					for _, instr := range *val.Referrers() {
   171  						extract, ok := instr.(*ssa.Extract)
   172  						if !ok {
   173  							continue resLoop // direct, real use
   174  						}
   175  						if extract.Index != i {
   176  							continue // not the same result param
   177  						}
   178  						if len(*extract.Referrers()) > 0 {
   179  							continue resLoop // real use after extraction
   180  						}
   181  					}
   182  				}
   183  				res := results.At(i)
   184  				name := paramDesc(i, res)
   185  				issues = append(issues, Issue{
   186  					pos: res.Pos(),
   187  					msg: fmt.Sprintf("result %s is never used", name),
   188  				})
   189  			}
   190  		}
   191  
   192  		seen := make([]constant.Value, results.Len())
   193  		numRets := 0
   194  		for _, block := range fn.Blocks {
   195  			last := block.Instrs[len(block.Instrs)-1]
   196  			ret, ok := last.(*ssa.Return)
   197  			if !ok {
   198  				continue
   199  			}
   200  			for i, val := range ret.Results {
   201  				cnst, ok := val.(*ssa.Const)
   202  				switch {
   203  				case !ok:
   204  					seen[i] = nil
   205  				case numRets == 0:
   206  					seen[i] = cnst.Value
   207  				case seen[i] == nil:
   208  				case !constant.Compare(seen[i], token.EQL, cnst.Value):
   209  					seen[i] = nil
   210  				}
   211  			}
   212  			numRets++
   213  		}
   214  		if numRets > 1 {
   215  			for i, val := range seen {
   216  				if val == nil {
   217  					continue
   218  				}
   219  				res := results.At(i)
   220  				name := paramDesc(i, res)
   221  				issues = append(issues, Issue{
   222  					pos: res.Pos(),
   223  					msg: fmt.Sprintf("result %s is always %s", name, val.String()),
   224  				})
   225  			}
   226  		}
   227  
   228  		for i, par := range fn.Params {
   229  			if i == 0 && fn.Signature.Recv() != nil { // receiver
   230  				continue
   231  			}
   232  			c.debug("%s\n", par.String())
   233  			switch par.Object().Name() {
   234  			case "", "_": // unnamed
   235  				c.debug("  skip - unnamed\n")
   236  				continue
   237  			}
   238  			reason := "is unused"
   239  			if cv := receivesSameValue(cg.Nodes[fn].In, par, i); cv != nil {
   240  				reason = fmt.Sprintf("always receives %v", cv)
   241  			} else if anyRealUse(par, i) {
   242  				c.debug("  skip - used somewhere in the func body\n")
   243  				continue
   244  			}
   245  			issues = append(issues, Issue{
   246  				pos: par.Pos(),
   247  				msg: fmt.Sprintf("%s %s", par.Name(), reason),
   248  			})
   249  		}
   250  
   251  	}
   252  	// TODO: replace by sort.Slice once we drop Go 1.7 support
   253  	sort.Sort(byNamePos{c.prog.Fset, issues})
   254  	return issues, nil
   255  }
   256  
   257  type byNamePos struct {
   258  	fset *token.FileSet
   259  	l    []lint.Issue
   260  }
   261  
   262  func (p byNamePos) Len() int      { return len(p.l) }
   263  func (p byNamePos) Swap(i, j int) { p.l[i], p.l[j] = p.l[j], p.l[i] }
   264  func (p byNamePos) Less(i, j int) bool {
   265  	p1 := p.fset.Position(p.l[i].Pos())
   266  	p2 := p.fset.Position(p.l[j].Pos())
   267  	if p1.Filename == p2.Filename {
   268  		return p1.Offset < p2.Offset
   269  	}
   270  	return p1.Filename < p2.Filename
   271  }
   272  
   273  func receivesSameValue(in []*callgraph.Edge, par *ssa.Parameter, pos int) constant.Value {
   274  	if ast.IsExported(par.Parent().Name()) {
   275  		// we might not have all call sites for an exported func
   276  		return nil
   277  	}
   278  	var seen constant.Value
   279  	for _, edge := range in {
   280  		call := edge.Site.Common()
   281  		cnst, ok := call.Args[pos].(*ssa.Const)
   282  		if !ok {
   283  			return nil // not a constant
   284  		}
   285  		if seen == nil {
   286  			seen = cnst.Value // first constant
   287  		} else if !constant.Compare(seen, token.EQL, cnst.Value) {
   288  			return nil // different constants
   289  		}
   290  	}
   291  	return seen
   292  }
   293  
   294  func anyRealUse(par *ssa.Parameter, pos int) bool {
   295  refLoop:
   296  	for _, ref := range *par.Referrers() {
   297  		switch x := ref.(type) {
   298  		case *ssa.Call:
   299  			if x.Call.Value != par.Parent() {
   300  				return true // not a recursive call
   301  			}
   302  			for i, arg := range x.Call.Args {
   303  				if arg != par {
   304  					continue
   305  				}
   306  				if i == pos {
   307  					// reused directly in a recursive call
   308  					continue refLoop
   309  				}
   310  			}
   311  			return true
   312  		case *ssa.Store:
   313  			if insertedStore(x) {
   314  				continue // inserted by go/ssa, not from the code
   315  			}
   316  			return true
   317  		default:
   318  			return true
   319  		}
   320  	}
   321  	return false
   322  }
   323  
   324  func insertedStore(instr ssa.Instruction) bool {
   325  	if instr.Pos() != token.NoPos {
   326  		return false
   327  	}
   328  	store, ok := instr.(*ssa.Store)
   329  	if !ok {
   330  		return false
   331  	}
   332  	alloc, ok := store.Addr.(*ssa.Alloc)
   333  	// we want exactly one use of this alloc value for it to be
   334  	// inserted by ssa and dummy - the alloc instruction itself.
   335  	return ok && len(*alloc.Referrers()) == 1
   336  }
   337  
   338  var rxHarmlessCall = regexp.MustCompile(`(?i)\b(log(ger)?|errors)\b|\bf?print`)
   339  
   340  // dummyImpl reports whether a block is a dummy implementation. This is
   341  // true if the block will almost immediately panic, throw or return
   342  // constants only.
   343  func dummyImpl(blk *ssa.BasicBlock) bool {
   344  	var ops [8]*ssa.Value
   345  	for _, instr := range blk.Instrs {
   346  		if insertedStore(instr) {
   347  			continue // inserted by go/ssa, not from the code
   348  		}
   349  		for _, val := range instr.Operands(ops[:0]) {
   350  			switch x := (*val).(type) {
   351  			case nil, *ssa.Const, *ssa.ChangeType, *ssa.Alloc,
   352  				*ssa.MakeInterface, *ssa.Function,
   353  				*ssa.Global, *ssa.IndexAddr, *ssa.Slice,
   354  				*ssa.UnOp:
   355  			case *ssa.Call:
   356  				if rxHarmlessCall.MatchString(x.Call.Value.String()) {
   357  					continue
   358  				}
   359  			default:
   360  				return false
   361  			}
   362  		}
   363  		switch x := instr.(type) {
   364  		case *ssa.Alloc, *ssa.Store, *ssa.UnOp, *ssa.BinOp,
   365  			*ssa.MakeInterface, *ssa.MakeMap, *ssa.Extract,
   366  			*ssa.IndexAddr, *ssa.FieldAddr, *ssa.Slice,
   367  			*ssa.Lookup, *ssa.ChangeType, *ssa.TypeAssert,
   368  			*ssa.Convert, *ssa.ChangeInterface:
   369  			// non-trivial expressions in panic/log/print
   370  			// calls
   371  		case *ssa.Return, *ssa.Panic:
   372  			return true
   373  		case *ssa.Call:
   374  			if rxHarmlessCall.MatchString(x.Call.Value.String()) {
   375  				continue
   376  			}
   377  			return x.Call.Value.Name() == "throw" // runtime's panic
   378  		default:
   379  			return false
   380  		}
   381  	}
   382  	return false
   383  }
   384  
   385  func (c *Checker) declCounts(pkgDir string, pkgName string) map[string]int {
   386  	if m := c.cachedDeclCounts[pkgDir]; m != nil {
   387  		return m
   388  	}
   389  	fset := token.NewFileSet()
   390  	pkgs, err := parser.ParseDir(fset, pkgDir, nil, 0)
   391  	if err != nil {
   392  		panic(err.Error())
   393  		return nil
   394  	}
   395  	pkg := pkgs[pkgName]
   396  	count := make(map[string]int)
   397  	for _, file := range pkg.Files {
   398  		for _, decl := range file.Decls {
   399  			fd, _ := decl.(*ast.FuncDecl)
   400  			if fd == nil {
   401  				continue
   402  			}
   403  			name := astPrefix(fd.Recv) + fd.Name.Name
   404  			count[name]++
   405  		}
   406  	}
   407  	c.cachedDeclCounts[pkgDir] = count
   408  	return count
   409  }
   410  
   411  func astPrefix(recv *ast.FieldList) string {
   412  	if recv == nil {
   413  		return ""
   414  	}
   415  	expr := recv.List[0].Type
   416  	for {
   417  		star, _ := expr.(*ast.StarExpr)
   418  		if star == nil {
   419  			break
   420  		}
   421  		expr = star.X
   422  	}
   423  	id := expr.(*ast.Ident)
   424  	return id.Name + "."
   425  }
   426  
   427  func (c *Checker) multipleImpls(info *loader.PackageInfo, fn *ssa.Function) bool {
   428  	if fn.Parent() != nil { // nested func
   429  		return false
   430  	}
   431  	path := c.prog.Fset.Position(fn.Pos()).Filename
   432  	if path == "" { // generated func, like init
   433  		return false
   434  	}
   435  	count := c.declCounts(filepath.Dir(path), info.Pkg.Name())
   436  	name := fn.Name()
   437  	if fn.Signature.Recv() != nil {
   438  		tp := fn.Params[0].Type()
   439  		for {
   440  			point, _ := tp.(*types.Pointer)
   441  			if point == nil {
   442  				break
   443  			}
   444  			tp = point.Elem()
   445  		}
   446  		named := tp.(*types.Named)
   447  		name = named.Obj().Name() + "." + name
   448  	}
   449  	return count[name] > 1
   450  }
   451  
   452  func paramDesc(i int, v *types.Var) string {
   453  	name := v.Name()
   454  	if name != "" {
   455  		return name
   456  	}
   457  	return fmt.Sprintf("%d (%s)", i, v.Type().String())
   458  }