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 }