├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── LICENSE ├── README.md ├── check └── check.go ├── go.mod ├── go.sum ├── main.go ├── main_test.go └── testdata └── script ├── build_tags.txtar ├── closure.txtar ├── cmds.txtar ├── crashers.txtar ├── extractparams.txtar ├── funclit.txtar ├── ignoredrets.txtar ├── impl.txtar ├── include_test.txtar ├── main.txtar ├── methods.txtar ├── names.txtar ├── paramuses.txtar ├── parenthesized_expression.txtar ├── phi.txtar ├── reused.txtar ├── samerets.txtar ├── simple.txtar ├── stubs.txtar ├── typealias.txtar ├── typeparams.txtar ├── usedas.txtar └── values.txtar /.gitattributes: -------------------------------------------------------------------------------- 1 | # To prevent CRLF breakages on Windows for fragile files, like testdata. 2 | * -text 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: mvdan 2 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Test 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: [1.23.x, 1.24.x] 8 | os: [ubuntu-latest, macos-latest, windows-latest] 9 | runs-on: ${{ matrix.os }} 10 | steps: 11 | - uses: actions/setup-go@v5 12 | with: 13 | go-version: ${{ matrix.go-version }} 14 | - uses: actions/checkout@v4 15 | - run: go test ./... 16 | - run: go test -race ./... 17 | 18 | # Static checks from this point forward. Only run on one Go version and on 19 | # Linux, since it's the fastest platform, and the tools behave the same. 20 | - if: matrix.os == 'ubuntu-latest' && matrix.go-version == '1.24.x' 21 | run: diff <(echo -n) <(gofmt -s -d .) 22 | - if: matrix.os == 'ubuntu-latest' && matrix.go-version == '1.24.x' 23 | run: go vet ./... 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Daniel Martí. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of the copyright holder nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # unparam 2 | 3 | go install mvdan.cc/unparam@latest 4 | 5 | Reports unused function parameters and results in your code. 6 | 7 | To minimise false positives, it ignores certain cases such as: 8 | 9 | * Exported functions (by default, see `-exported`) 10 | * Unnamed and underscore parameters (like `_` and `_foo`) 11 | * Funcs that may satisfy an interface 12 | * Funcs that may satisfy a function signature 13 | * Funcs that are stubs (empty, only error, immediately return, etc) 14 | * Funcs that have multiple implementations via build tags 15 | 16 | It also reports results that always return the same value, parameters 17 | that always receive the same value, and results that are never used. In 18 | the last two cases, a minimum number of calls is required to ensure that 19 | the warnings are useful. 20 | 21 | False positives can still occur by design. The aim of the tool is to be 22 | as precise as possible - if you find any mistakes, file a bug. 23 | -------------------------------------------------------------------------------- /check/check.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, Daniel Martí 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 7 | 8 | import ( 9 | "bytes" 10 | "fmt" 11 | "go/ast" 12 | "go/constant" 13 | "go/parser" 14 | "go/printer" 15 | "go/token" 16 | "go/types" 17 | "io" 18 | "os" 19 | "path/filepath" 20 | "regexp" 21 | "sort" 22 | "strings" 23 | 24 | "golang.org/x/tools/go/packages" 25 | "golang.org/x/tools/go/ssa" 26 | "golang.org/x/tools/go/ssa/ssautil" 27 | ) 28 | 29 | // UnusedParams returns a list of human-readable issues that point out unused 30 | // function parameters. 31 | func UnusedParams(tests, exported, debug bool, args ...string) ([]string, error) { 32 | wd, err := os.Getwd() 33 | if err != nil { 34 | return nil, err 35 | } 36 | c := &Checker{ 37 | wd: wd, 38 | tests: tests, 39 | exported: exported, 40 | } 41 | if debug { 42 | c.debugLog = os.Stderr 43 | } 44 | return c.lines(args...) 45 | } 46 | 47 | // Checker finds unused parameters in a program. You probably want to use 48 | // UnusedParams instead, unless you want to use a *loader.Program and 49 | // *ssa.Program directly. 50 | type Checker struct { 51 | pkgs []*packages.Package 52 | prog *ssa.Program 53 | 54 | wd string 55 | 56 | tests bool 57 | exported bool 58 | debugLog io.Writer 59 | 60 | issues []Issue 61 | 62 | cachedDeclCounts map[string]map[string]int 63 | 64 | // callByPos maps from a call site position to its CallExpr. 65 | callByPos map[token.Pos]*ast.CallExpr 66 | 67 | // funcBodyByPos maps from a function position to its body. We can't map 68 | // to the declaration, as that could be either a FuncDecl or FuncLit. 69 | funcBodyByPos map[token.Pos]*ast.BlockStmt 70 | 71 | // typesImplementing records the method names that each named type needs 72 | // to typecheck properly, as they're required to implement interfaces. 73 | typesImplementing map[*types.Named][]string 74 | 75 | // localCallSites is a very simple form of a callgraph, only recording 76 | // direct function calls within a single package. 77 | localCallSites map[*ssa.Function][]ssa.CallInstruction 78 | 79 | // These three maps record whether an entire func's signature cannot be 80 | // changed, or only its list of parameters or results. 81 | 82 | signRequiredBy map[*ssa.Function]string 83 | paramsRequiredBy map[*ssa.Function]string 84 | resultsRequiredBy map[*ssa.Function]string 85 | } 86 | 87 | var errorType = types.Universe.Lookup("error").Type() 88 | 89 | // lines runs the checker and returns the list of readable issues. 90 | func (c *Checker) lines(args ...string) ([]string, error) { 91 | cfg := &packages.Config{ 92 | Mode: packages.LoadSyntax, 93 | Tests: c.tests, 94 | } 95 | pkgs, err := packages.Load(cfg, args...) 96 | if err != nil { 97 | return nil, err 98 | } 99 | if packages.PrintErrors(pkgs) > 0 { 100 | return nil, fmt.Errorf("encountered errors") 101 | } 102 | 103 | prog, _ := ssautil.Packages(pkgs, 0) 104 | prog.Build() 105 | c.Packages(pkgs) 106 | c.ProgramSSA(prog) 107 | issues, err := c.Check() 108 | if err != nil { 109 | return nil, err 110 | } 111 | lines := make([]string, 0, len(issues)) 112 | prevLine := "" 113 | for _, issue := range issues { 114 | fpos := prog.Fset.Position(issue.Pos()).String() 115 | if strings.HasPrefix(fpos, c.wd) { 116 | fpos = fpos[len(c.wd)+1:] 117 | } 118 | line := fmt.Sprintf("%s: %s", fpos, issue.Message()) 119 | if line == prevLine { 120 | // Deduplicate lines, since we may look at the same 121 | // package multiple times if tests are involved. 122 | // TODO: is there a better way to handle this? 123 | continue 124 | } 125 | prevLine = line 126 | lines = append(lines, fmt.Sprintf("%s: %s", fpos, issue.Message())) 127 | } 128 | return lines, nil 129 | } 130 | 131 | // Issue identifies a found unused parameter. 132 | type Issue struct { 133 | pos token.Pos 134 | fname string 135 | msg string 136 | } 137 | 138 | func (i Issue) Pos() token.Pos { return i.pos } 139 | func (i Issue) Message() string { return i.fname + " - " + i.msg } 140 | 141 | // Program supplies Checker with the needed *loader.Program. 142 | func (c *Checker) Packages(pkgs []*packages.Package) { 143 | c.pkgs = pkgs 144 | } 145 | 146 | // ProgramSSA supplies Checker with the needed *ssa.Program. 147 | func (c *Checker) ProgramSSA(prog *ssa.Program) { 148 | c.prog = prog 149 | } 150 | 151 | // CheckExportedFuncs sets whether to inspect exported functions 152 | func (c *Checker) CheckExportedFuncs(exported bool) { 153 | c.exported = exported 154 | } 155 | 156 | func (c *Checker) debug(format string, a ...interface{}) { 157 | if c.debugLog != nil { 158 | fmt.Fprintf(c.debugLog, format, a...) 159 | } 160 | } 161 | 162 | // generatedDoc reports whether a comment text describes its file as being code 163 | // generated. 164 | func generatedDoc(text string) bool { 165 | return strings.Contains(text, "Code generated") || 166 | strings.Contains(text, "DO NOT EDIT") 167 | } 168 | 169 | // eqlConsts reports whether two constant values, possibly nil, are equal. 170 | func eqlConsts(c1, c2 *ssa.Const) bool { 171 | if c1 == nil || c2 == nil { 172 | return c1 == c2 173 | } 174 | if c1.Type() != c2.Type() { 175 | return false 176 | } 177 | if c1.Value == nil || c2.Value == nil { 178 | return c1.Value == c2.Value 179 | } 180 | return constant.Compare(c1.Value, token.EQL, c2.Value) 181 | } 182 | 183 | var stdSizes = types.SizesFor("gc", "amd64") 184 | 185 | // Check runs the unused parameter check and returns the list of found issues, 186 | // and any error encountered. 187 | func (c *Checker) Check() ([]Issue, error) { 188 | c.cachedDeclCounts = make(map[string]map[string]int) 189 | c.callByPos = make(map[token.Pos]*ast.CallExpr) 190 | c.funcBodyByPos = make(map[token.Pos]*ast.BlockStmt) 191 | c.typesImplementing = make(map[*types.Named][]string) 192 | 193 | wantPkg := make(map[*types.Package]*packages.Package) 194 | genFiles := make(map[string]bool) 195 | for _, pkg := range c.pkgs { 196 | wantPkg[pkg.Types] = pkg 197 | for _, f := range pkg.Syntax { 198 | if len(f.Comments) > 0 && generatedDoc(f.Comments[0].Text()) { 199 | fname := c.prog.Fset.Position(f.Pos()).Filename 200 | genFiles[fname] = true 201 | } 202 | ast.Inspect(f, func(node ast.Node) bool { 203 | switch node := node.(type) { 204 | case *ast.ValueSpec: 205 | if len(node.Values) == 0 || node.Type == nil || 206 | len(node.Names) != 1 || node.Names[0].Name != "_" { 207 | break 208 | } 209 | iface, ok := pkg.TypesInfo.TypeOf(node.Type).Underlying().(*types.Interface) 210 | if !ok { 211 | break 212 | } 213 | // var _ someIface = named 214 | valTyp := pkg.TypesInfo.Types[node.Values[0]].Type 215 | c.addImplementing(findNamed(valTyp), iface) 216 | case *ast.CallExpr: 217 | c.callByPos[node.Lparen] = node 218 | // ssa.Function.Pos returns the declaring 219 | // FuncLit.Type.Func or the position of the 220 | // FuncDecl.Name. 221 | case *ast.FuncDecl: 222 | c.funcBodyByPos[node.Name.Pos()] = node.Body 223 | case *ast.FuncLit: 224 | c.funcBodyByPos[node.Pos()] = node.Body 225 | } 226 | return true 227 | }) 228 | } 229 | } 230 | allFuncs := ssautil.AllFunctions(c.prog) 231 | 232 | // map from *ssa.FreeVar to *ssa.Function, to find function literals 233 | // behind closure vars in the simpler scenarios. 234 | freeVars := map[*ssa.FreeVar]*ssa.Function{} 235 | for curFunc := range allFuncs { 236 | for _, b := range curFunc.Blocks { 237 | for _, instr := range b.Instrs { 238 | instr, ok := instr.(*ssa.MakeClosure) 239 | if !ok { 240 | continue 241 | } 242 | fn := instr.Fn.(*ssa.Function) 243 | for i, fv := range fn.FreeVars { 244 | binding := instr.Bindings[i] 245 | alloc, ok := binding.(*ssa.Alloc) 246 | if !ok { 247 | continue 248 | } 249 | for _, ref := range *alloc.Referrers() { 250 | store, ok := ref.(*ssa.Store) 251 | if !ok { 252 | continue 253 | } 254 | if fn, ok := store.Val.(*ssa.Function); ok { 255 | freeVars[fv] = fn 256 | break 257 | } 258 | } 259 | } 260 | } 261 | } 262 | } 263 | 264 | c.signRequiredBy = make(map[*ssa.Function]string) 265 | c.paramsRequiredBy = make(map[*ssa.Function]string) 266 | c.resultsRequiredBy = make(map[*ssa.Function]string) 267 | c.localCallSites = make(map[*ssa.Function][]ssa.CallInstruction) 268 | for curFunc := range allFuncs { 269 | if strings.HasPrefix(curFunc.Synthetic, "wrapper for func") { 270 | // Synthetic func wrappers are uninteresting, and can 271 | // lead to false negatives. 272 | continue 273 | } 274 | for _, b := range curFunc.Blocks { 275 | for _, instr := range b.Instrs { 276 | if instr, ok := instr.(ssa.CallInstruction); ok { 277 | if fn := findFunction(freeVars, instr.Common().Value); fn != nil { 278 | c.localCallSites[fn] = append(c.localCallSites[fn], instr) 279 | } 280 | fn := receivesExtractedArgs(freeVars, instr) 281 | if fn != nil { 282 | // fn(someFunc()) fixes params 283 | c.paramsRequiredBy[fn] = "forwarded call" 284 | } 285 | } 286 | switch instr := instr.(type) { 287 | case *ssa.Call: 288 | for _, arg := range instr.Call.Args { 289 | if fn := findFunction(freeVars, arg); fn != nil { 290 | // someFunc(fn) 291 | c.signRequiredBy[fn] = "call" 292 | } 293 | } 294 | case *ssa.Phi: 295 | for _, val := range instr.Edges { 296 | if fn := findFunction(freeVars, val); fn != nil { 297 | // nonConstVar = fn 298 | c.signRequiredBy[fn] = "phi" 299 | } 300 | } 301 | case *ssa.Return: 302 | for _, val := range instr.Results { 303 | if fn := findFunction(freeVars, val); fn != nil { 304 | // return fn 305 | c.signRequiredBy[fn] = "result" 306 | } 307 | } 308 | if call := callExtract(instr, instr.Results); call != nil { 309 | if fn := findFunction(freeVars, call.Call.Value); fn != nil { 310 | // return fn() 311 | c.resultsRequiredBy[fn] = "return" 312 | } 313 | } 314 | case *ssa.Store: 315 | as := "" 316 | switch instr.Addr.(type) { 317 | case *ssa.FieldAddr: 318 | // x.someField = fn 319 | as = "field" 320 | case *ssa.IndexAddr: 321 | // x[someIndex] = fn 322 | as = "element" 323 | case *ssa.Global: 324 | // someGlobal = fn 325 | as = "global" 326 | default: 327 | continue 328 | } 329 | if fn := findFunction(freeVars, instr.Val); fn != nil { 330 | c.signRequiredBy[fn] = as 331 | } 332 | case *ssa.MakeInterface: 333 | // someIface(named) 334 | iface := instr.Type().Underlying().(*types.Interface) 335 | c.addImplementing(findNamed(instr.X.Type()), iface) 336 | 337 | if fn := findFunction(freeVars, instr.X); fn != nil { 338 | // emptyIface = fn 339 | c.signRequiredBy[fn] = "interface" 340 | } 341 | case *ssa.ChangeType: 342 | if fn := findFunction(freeVars, instr.X); fn != nil { 343 | // someType(fn) 344 | c.signRequiredBy[fn] = "type conversion" 345 | } 346 | } 347 | } 348 | } 349 | } 350 | 351 | for fn := range allFuncs { 352 | switch { 353 | case fn.Pkg == nil: // builtin? 354 | continue 355 | case fn.Name() == "init": 356 | continue 357 | case len(fn.Blocks) == 0: // stub 358 | continue 359 | } 360 | pkg := wantPkg[fn.Pkg.Pkg] 361 | if pkg == nil { // not part of given pkgs 362 | continue 363 | } 364 | if c.exported || fn.Pkg.Pkg.Name() == "main" { 365 | // we want exported funcs, or this is a main package so 366 | // nothing is exported 367 | } else if strings.Contains(fn.Name(), "$") { 368 | // anonymous function within a possibly exported func 369 | } else if ast.IsExported(fn.Name()) { 370 | continue // user doesn't want to change signatures here 371 | } 372 | fname := c.prog.Fset.Position(fn.Pos()).Filename 373 | if genFiles[fname] { 374 | continue // generated file 375 | } 376 | 377 | c.checkFunc(fn, pkg) 378 | } 379 | sort.Slice(c.issues, func(i, j int) bool { 380 | p1 := c.prog.Fset.Position(c.issues[i].Pos()) 381 | p2 := c.prog.Fset.Position(c.issues[j].Pos()) 382 | if p1.Filename == p2.Filename { 383 | return p1.Offset < p2.Offset 384 | } 385 | return p1.Filename < p2.Filename 386 | }) 387 | return c.issues, nil 388 | } 389 | 390 | func stringsContains(list []string, elem string) bool { 391 | for _, e := range list { 392 | if e == elem { 393 | return true 394 | } 395 | } 396 | return false 397 | } 398 | 399 | func (c *Checker) addImplementing(named *types.Named, iface *types.Interface) { 400 | if named == nil || iface == nil { 401 | return 402 | } 403 | list := c.typesImplementing[named] 404 | for i := 0; i < iface.NumMethods(); i++ { 405 | name := iface.Method(i).Name() 406 | if !stringsContains(list, name) { 407 | list = append(list, name) 408 | } 409 | } 410 | c.typesImplementing[named] = list 411 | } 412 | 413 | func findNamed(typ types.Type) *types.Named { 414 | switch typ := types.Unalias(typ).(type) { 415 | case *types.Pointer: 416 | return findNamed(typ.Elem()) 417 | case *types.Named: 418 | return typ 419 | } 420 | return nil 421 | } 422 | 423 | // findFunction returns the function that is behind a value, if any. 424 | func findFunction(freeVars map[*ssa.FreeVar]*ssa.Function, value ssa.Value) *ssa.Function { 425 | switch value := value.(type) { 426 | case *ssa.Function: 427 | name := value.Name() 428 | if strings.HasSuffix(name, "$thunk") || strings.HasSuffix(name, "$bound") { 429 | // Method wrapper funcs contain a single block, which 430 | // calls the function being wrapped, and returns. We 431 | // want the function being wrapped. 432 | for _, instr := range value.Blocks[0].Instrs { 433 | call, ok := instr.(*ssa.Call) 434 | if !ok { 435 | continue 436 | } 437 | if callee := call.Call.StaticCallee(); callee != nil { 438 | return callee 439 | } 440 | } 441 | return nil // no static callee? 442 | } 443 | return value 444 | case *ssa.MakeClosure: 445 | // closure of a func 446 | return findFunction(freeVars, value.Fn) 447 | case *ssa.UnOp: 448 | if value.Op != token.MUL { 449 | break 450 | } 451 | if fv, ok := value.X.(*ssa.FreeVar); ok { 452 | return freeVars[fv] 453 | } 454 | } 455 | return nil 456 | } 457 | 458 | // addIssue records a newly found unused parameter. 459 | func (c *Checker) addIssue(fn *ssa.Function, pos token.Pos, format string, args ...interface{}) { 460 | c.issues = append(c.issues, Issue{ 461 | pos: pos, 462 | fname: fn.RelString(fn.Package().Pkg), 463 | msg: fmt.Sprintf(format, args...), 464 | }) 465 | } 466 | 467 | // constValueString is cnst.Value.String() without panicking on untyped nils. 468 | func constValueString(cnst *ssa.Const) string { 469 | if cnst.Value == nil { 470 | return "nil" 471 | } 472 | return cnst.Value.String() 473 | } 474 | 475 | // checkFunc checks a single function for unused parameters. 476 | func (c *Checker) checkFunc(fn *ssa.Function, pkg *packages.Package) { 477 | c.debug("func %s\n", fn.RelString(fn.Package().Pkg)) 478 | if dummyImpl(fn.Blocks[0]) { // panic implementation 479 | c.debug(" skip - dummy implementation\n") 480 | return 481 | } 482 | if by := c.signRequiredBy[fn]; by != "" { 483 | c.debug(" skip - func signature required by %s\n", by) 484 | return 485 | } 486 | if recv := fn.Signature.Recv(); recv != nil { 487 | named := findNamed(recv.Type()) 488 | if stringsContains(c.typesImplementing[named], fn.Name()) { 489 | c.debug(" skip - method required to implement an interface\n") 490 | return 491 | } 492 | } 493 | if c.multipleImpls(pkg, fn) { 494 | c.debug(" skip - multiple implementations via build tags\n") 495 | return 496 | } 497 | paramsBy := c.paramsRequiredBy[fn] 498 | resultsBy := c.resultsRequiredBy[fn] 499 | callSites := c.localCallSites[fn] 500 | 501 | results := fn.Signature.Results() 502 | sameConsts := make([]*ssa.Const, results.Len()) 503 | numRets := 0 504 | allRetsExtracting := true 505 | for _, block := range fn.Blocks { 506 | if resultsBy != "" { 507 | continue // we can't change the returns 508 | } 509 | last := block.Instrs[len(block.Instrs)-1] 510 | ret, ok := last.(*ssa.Return) 511 | if !ok { 512 | continue 513 | } 514 | for i, val := range ret.Results { 515 | if _, ok := val.(*ssa.Extract); !ok { 516 | allRetsExtracting = false 517 | } 518 | cnst := constValue(val) 519 | if numRets == 0 { 520 | sameConsts[i] = cnst 521 | } else if !eqlConsts(sameConsts[i], cnst) { 522 | sameConsts[i] = nil 523 | } 524 | } 525 | numRets++ 526 | } 527 | for i, cnst := range sameConsts { 528 | if cnst == nil { 529 | // no consistent returned constant 530 | continue 531 | } 532 | if cnst.Value != nil && numRets == 1 { 533 | // just one return and it's not untyped nil (too many 534 | // false positives) 535 | continue 536 | } 537 | res := results.At(i) 538 | name := paramDesc(i, res) 539 | c.addIssue(fn, res.Pos(), "result %s is always %s", name, constValueString(cnst)) 540 | } 541 | 542 | resLoop: 543 | for i := 0; i < results.Len(); i++ { 544 | if resultsBy != "" { 545 | continue // we can't change the returns 546 | } 547 | if allRetsExtracting { 548 | continue 549 | } 550 | res := results.At(i) 551 | if res.Type() == errorType { 552 | // "error is never used" is less useful, and it's up to 553 | // tools like errcheck anyway. 554 | continue 555 | } 556 | count := 0 557 | for _, site := range callSites { 558 | val := site.Value() 559 | if val == nil { // e.g. go statement 560 | count++ 561 | continue 562 | } 563 | for _, instr := range *val.Referrers() { 564 | extract, ok := instr.(*ssa.Extract) 565 | if !ok { 566 | continue resLoop // direct, real use 567 | } 568 | if extract.Index != i { 569 | continue // not the same result param 570 | } 571 | if len(*extract.Referrers()) > 0 { 572 | continue resLoop // real use after extraction 573 | } 574 | } 575 | count++ 576 | } 577 | if count < 2 { 578 | continue // require ignoring at least twice 579 | } 580 | name := paramDesc(i, res) 581 | c.addIssue(fn, res.Pos(), "result %s is never used", name) 582 | } 583 | 584 | fnIsGeneric := fn.TypeParams().Len() > 0 585 | 586 | for i, par := range fn.Params { 587 | if paramsBy != "" { 588 | continue // we can't change the params 589 | } 590 | if i == 0 && fn.Signature.Recv() != nil { // receiver 591 | continue 592 | } 593 | c.debug("%s\n", par.String()) 594 | if name := par.Object().Name(); name == "" || name[0] == '_' { 595 | c.debug(" skip - no name or underscore name\n") 596 | continue 597 | } 598 | t := par.Type() 599 | // asking for the size of a type param would panic, as it is unknowable 600 | if !fnIsGeneric || !containsTypeParam(t) { 601 | if stdSizes.Sizeof(par.Type()) == 0 { 602 | c.debug(" skip - zero size\n") 603 | continue 604 | } 605 | } else { 606 | c.debug(" examine - type parameter\n") 607 | } 608 | reason := "is unused" 609 | constStr := c.alwaysReceivedConst(callSites, par, i) 610 | if constStr != "" { 611 | reason = fmt.Sprintf("always receives %s", constStr) 612 | } else if c.anyRealUse(par, i, pkg) { 613 | c.debug(" skip - used somewhere in the func body\n") 614 | continue 615 | } 616 | c.addIssue(fn, par.Pos(), "%s %s", par.Name(), reason) 617 | } 618 | } 619 | 620 | func containsTypeParam(t types.Type) bool { 621 | switch t := t.(type) { 622 | case *types.TypeParam, *types.Union: 623 | return true 624 | case *types.Struct: 625 | nf := t.NumFields() 626 | for i := 0; i < nf; i++ { 627 | if containsTypeParam(t.Field(nf).Type()) { 628 | return true 629 | } 630 | } 631 | case *types.Array: 632 | return containsTypeParam(t.Elem()) 633 | case *types.Named: 634 | args := t.TypeArgs() 635 | for i := 0; i < args.Len(); i++ { 636 | if containsTypeParam(args.At(i)) { 637 | return true 638 | } 639 | } 640 | } 641 | return false 642 | } 643 | 644 | // nodeStr stringifies a syntax tree node. It is only meant for simple nodes, 645 | // such as short value expressions. 646 | func nodeStr(node ast.Node) string { 647 | var buf bytes.Buffer 648 | fset := token.NewFileSet() 649 | if err := printer.Fprint(&buf, fset, node); err != nil { 650 | panic(err) 651 | } 652 | return buf.String() 653 | } 654 | 655 | // alwaysReceivedConst checks if a function parameter always receives the same 656 | // constant value, given a list of inbound calls. If it does, a description of 657 | // the value is returned. If not, an empty string is returned. 658 | // 659 | // This function is used to recommend that the parameter be replaced by a direct 660 | // use of the constant. To avoid false positives, the function will return false 661 | // if the number of inbound calls is too low. 662 | func (c *Checker) alwaysReceivedConst(callSites []ssa.CallInstruction, par *ssa.Parameter, pos int) string { 663 | if len(callSites) < 4 { 664 | // We can't possibly receive the same constant value enough 665 | // times, hence a potential false positive. 666 | return "" 667 | } 668 | if ast.IsExported(par.Parent().Name()) { 669 | // we might not have all call sites for an exported func 670 | return "" 671 | } 672 | var seen *ssa.Const 673 | origPos := pos 674 | if par.Parent().Signature.Recv() != nil { 675 | // go/ast's CallExpr.Args does not include the receiver, but 676 | // go/ssa's equivalent does. 677 | origPos-- 678 | } 679 | seenOrig := "" 680 | for _, site := range callSites { 681 | call := site.Common() 682 | if pos >= len(call.Args) { 683 | // TODO: investigate? Weird crash in 684 | // internal/x/net/http2/hpack/hpack_test.go, where we 685 | // roughly do: "at := d.mustAt; at(3)". 686 | return "" 687 | } 688 | cnst := constValue(call.Args[pos]) 689 | if cnst == nil { 690 | return "" // not a constant 691 | } 692 | origArg := "" 693 | origCall := c.callByPos[call.Pos()] 694 | if origPos >= len(origCall.Args) { 695 | // variadic parameter that wasn't given 696 | } else { 697 | origArg = nodeStr(origCall.Args[origPos]) 698 | } 699 | if seen == nil { 700 | seen = cnst // first constant 701 | seenOrig = origArg 702 | } else if !eqlConsts(seen, cnst) { 703 | return "" // different constants 704 | } else if origArg != seenOrig { 705 | seenOrig = "" 706 | } 707 | } 708 | seenStr := constValueString(seen) 709 | if seenOrig != "" && seenStr != seenOrig { 710 | return fmt.Sprintf("%s (%s)", seenOrig, seenStr) 711 | } 712 | return seenStr 713 | } 714 | 715 | func constValue(value ssa.Value) *ssa.Const { 716 | switch x := value.(type) { 717 | case *ssa.Const: 718 | return x 719 | case *ssa.MakeInterface: 720 | return constValue(x.X) 721 | } 722 | return nil 723 | } 724 | 725 | // anyRealUse reports whether a parameter has any relevant use within its 726 | // function body. Certain uses are ignored, such as recursive calls where the 727 | // parameter is re-used as itself. 728 | func (c *Checker) anyRealUse(par *ssa.Parameter, pos int, pkg *packages.Package) bool { 729 | refs := *par.Referrers() 730 | if len(refs) == 0 { 731 | // Look for any uses like "_ = par", which are the developer's 732 | // way to tell they want to keep the parameter. SSA does not 733 | // keep that kind of statement around. 734 | body := c.funcBodyByPos[par.Parent().Pos()] 735 | any := false 736 | ast.Inspect(body, func(node ast.Node) bool { 737 | if any { 738 | return false 739 | } 740 | asgn, ok := node.(*ast.AssignStmt) 741 | if !ok || asgn.Tok != token.ASSIGN || len(asgn.Lhs) != 1 || len(asgn.Rhs) != 1 { 742 | return true 743 | } 744 | if left, ok := asgn.Lhs[0].(*ast.Ident); !ok || left.Name != "_" { 745 | return true 746 | } 747 | if right, ok := asgn.Rhs[0].(*ast.Ident); ok { 748 | obj := pkg.TypesInfo.Uses[right] 749 | if obj != nil && obj.Pos() == par.Pos() { 750 | any = true 751 | } 752 | } 753 | return true 754 | }) 755 | return any 756 | } 757 | refLoop: 758 | for _, ref := range refs { 759 | switch x := ref.(type) { 760 | case *ssa.Call: 761 | if x.Call.Value != par.Parent() { 762 | return true // not a recursive call 763 | } 764 | for i, arg := range x.Call.Args { 765 | if arg != par { 766 | continue 767 | } 768 | if i == pos { 769 | // reused directly in a recursive call 770 | continue refLoop 771 | } 772 | } 773 | return true 774 | case *ssa.Store: 775 | if insertedStore(x) { 776 | continue // inserted by go/ssa, not from the code 777 | } 778 | return true 779 | default: 780 | return true 781 | } 782 | } 783 | return false 784 | } 785 | 786 | // insertedStore reports whether a SSA instruction was inserted by the SSA 787 | // building algorithm. That is, the store was not directly translated from an 788 | // original Go statement. 789 | func insertedStore(instr ssa.Instruction) bool { 790 | if instr.Pos() != token.NoPos { 791 | return false 792 | } 793 | store, ok := instr.(*ssa.Store) 794 | if !ok { 795 | return false 796 | } 797 | alloc, ok := store.Addr.(*ssa.Alloc) 798 | // we want exactly one use of this alloc value for it to be 799 | // inserted by ssa and dummy - the alloc instruction itself. 800 | return ok && len(*alloc.Referrers()) == 1 801 | } 802 | 803 | // rxHarmlessCall matches all the function expression strings which are allowed 804 | // in a dummy implementation. 805 | var rxHarmlessCall = regexp.MustCompile(`(?i)\b(log(ger)?|errors)\b|\bf?print|errorf?$`) 806 | 807 | // dummyImpl reports whether a block is a dummy implementation. This is 808 | // true if the block will almost immediately panic, throw or return 809 | // constants only. 810 | func dummyImpl(blk *ssa.BasicBlock) bool { 811 | var ops [8]*ssa.Value 812 | for _, instr := range blk.Instrs { 813 | if insertedStore(instr) { 814 | continue // inserted by go/ssa, not from the code 815 | } 816 | for _, val := range instr.Operands(ops[:0]) { 817 | switch x := (*val).(type) { 818 | case nil, *ssa.Const, *ssa.ChangeType, *ssa.Alloc, 819 | *ssa.MakeInterface, *ssa.MakeMap, 820 | *ssa.Function, *ssa.Global, 821 | *ssa.IndexAddr, *ssa.Slice, 822 | *ssa.UnOp, *ssa.Parameter: 823 | case *ssa.Call: 824 | if rxHarmlessCall.MatchString(x.Call.Value.String()) { 825 | continue 826 | } 827 | default: 828 | return false 829 | } 830 | } 831 | switch x := instr.(type) { 832 | case *ssa.Alloc, *ssa.Store, *ssa.UnOp, *ssa.BinOp, 833 | *ssa.MakeInterface, *ssa.MakeMap, *ssa.Extract, 834 | *ssa.IndexAddr, *ssa.FieldAddr, *ssa.Slice, 835 | *ssa.Lookup, *ssa.ChangeType, *ssa.TypeAssert, 836 | *ssa.Convert, *ssa.ChangeInterface: 837 | // non-trivial expressions in panic/log/print calls 838 | case *ssa.Return, *ssa.Panic: 839 | return true 840 | case *ssa.Call: 841 | if rxHarmlessCall.MatchString(x.Call.Value.String()) { 842 | continue 843 | } 844 | return x.Call.Value.Name() == "throw" // runtime's panic 845 | default: 846 | return false 847 | } 848 | } 849 | return false 850 | } 851 | 852 | // declCounts reports how many times a package's functions are declared. This is 853 | // used, for example, to find if a function has many implementations. 854 | // 855 | // Since this function parses all of the package's Go source files on disk, its 856 | // results are cached. 857 | func (c *Checker) declCounts(pkgDir, pkgName string) map[string]int { 858 | key := pkgDir + ":" + pkgName 859 | if m, ok := c.cachedDeclCounts[key]; ok { 860 | return m 861 | } 862 | fset := token.NewFileSet() 863 | pkgs, err := parser.ParseDir(fset, pkgDir, nil, 0) 864 | if err != nil { 865 | // Don't panic or error here. In some part of the go/* libraries 866 | // stack, we sometimes end up with a package directory that is 867 | // wrong. That's not our fault, and we can't simply break the 868 | // tool until we fix the underlying issue. 869 | println(err.Error()) 870 | c.cachedDeclCounts[pkgDir] = nil 871 | return nil 872 | } 873 | if len(pkgs) == 0 { 874 | // TODO: investigate why this started happening after switching 875 | // to go/packages 876 | return nil 877 | } 878 | pkg := pkgs[pkgName] 879 | count := make(map[string]int) 880 | for _, file := range pkg.Files { 881 | for _, decl := range file.Decls { 882 | fd, ok := decl.(*ast.FuncDecl) 883 | if !ok { 884 | continue 885 | } 886 | name := recvPrefix(fd.Recv) + fd.Name.Name 887 | count[name]++ 888 | } 889 | } 890 | c.cachedDeclCounts[key] = count 891 | return count 892 | } 893 | 894 | // recvPrefix returns the string prefix for a receiver field list. Star 895 | // expressions are ignored, so as to conservatively assume that pointer and 896 | // non-pointer receivers may still implement the same function. 897 | // 898 | // For example, for "function (*Foo) Bar()", recvPrefix will return "Foo.". 899 | func recvPrefix(recv *ast.FieldList) string { 900 | if recv == nil { 901 | return "" 902 | } 903 | expr := recv.List[0].Type 904 | for { 905 | star, ok := expr.(*ast.StarExpr) 906 | if !ok { 907 | break 908 | } 909 | expr = star.X 910 | } 911 | 912 | return identName(expr) 913 | } 914 | 915 | func identName(expr ast.Expr) string { 916 | switch expr := expr.(type) { 917 | case *ast.Ident: 918 | return expr.Name + "." 919 | case *ast.IndexExpr: 920 | return identName(expr.X) 921 | case *ast.ParenExpr: 922 | return identName(expr.X) 923 | case *ast.IndexListExpr: 924 | return identName(expr.X) 925 | default: 926 | panic(fmt.Sprintf("unexpected receiver AST node: %T", expr)) 927 | } 928 | } 929 | 930 | // multipleImpls reports whether a function has multiple implementations in the 931 | // source code. For example, if there are different function bodies depending on 932 | // the operating system or architecture. That tends to mean that an unused 933 | // parameter in one implementation may not be unused in another. 934 | func (c *Checker) multipleImpls(pkg *packages.Package, fn *ssa.Function) bool { 935 | if fn.Parent() != nil { // nested func 936 | return false 937 | } 938 | path := c.prog.Fset.Position(fn.Pos()).Filename 939 | count := c.declCounts(filepath.Dir(path), pkg.Types.Name()) 940 | name := fn.Name() 941 | if recv := fn.Signature.Recv(); recv != nil { 942 | named := findNamed(recv.Type()) 943 | name = named.Obj().Name() + "." + name 944 | } 945 | return count[name] > 1 946 | } 947 | 948 | // receivesExtractedArgs returns the statically called function, if its multiple 949 | // arguments were all received via another function call. That is, if a call to 950 | // function "foo" was of the form "foo(bar())". This often means that the 951 | // parameters in "foo" are difficult to remove, even if unused. 952 | func receivesExtractedArgs(freeVars map[*ssa.FreeVar]*ssa.Function, call ssa.CallInstruction) *ssa.Function { 953 | comm := call.Common() 954 | callee := findFunction(freeVars, comm.Value) 955 | if callee == nil { 956 | return nil 957 | } 958 | if callee.Signature.Params().Len() < 2 { 959 | // there aren't multiple parameters 960 | return nil 961 | } 962 | args := comm.Args 963 | if callee.Signature.Recv() != nil { 964 | // skip the receiver argument 965 | args = args[1:] 966 | } 967 | if c := callExtract(call, args); c != nil { 968 | return callee 969 | } 970 | return nil 971 | } 972 | 973 | // callExtract returns the call instruction fn(...) if it is used directly as 974 | // arguments to the parent instruction, such as fn2(fn(...)) or return fn(...). 975 | func callExtract(parent ssa.Instruction, values []ssa.Value) *ssa.Call { 976 | if len(values) == 1 { 977 | if call, ok := values[0].(*ssa.Call); ok { 978 | return call 979 | } 980 | } 981 | var prev *ssa.Call 982 | for i, val := range values { 983 | ext, ok := val.(*ssa.Extract) 984 | if !ok { 985 | return nil 986 | } 987 | if ext.Index != i { 988 | return nil // not extracted in the same order 989 | } 990 | call, ok := ext.Tuple.(*ssa.Call) 991 | if !ok { 992 | return nil // not a call 993 | } 994 | if prev == nil { 995 | prev = call 996 | } else if prev != call { 997 | return nil // not the same calls 998 | } 999 | } 1000 | if prev == nil { 1001 | return nil 1002 | } 1003 | if prev.Call.Signature().Results().Len() != len(values) { 1004 | return nil // not extracting all the results 1005 | } 1006 | if prev.Pos() < parent.Pos() { 1007 | // Of the form: 1008 | // 1009 | // a, b := fn() 1010 | // fn2(a, b) 1011 | return nil 1012 | } 1013 | return prev 1014 | } 1015 | 1016 | // paramDesc returns a string describing a parameter variable. If the parameter 1017 | // had no name, the function will fall back to describing the parameter by its 1018 | // position within the parameter list and its type. 1019 | func paramDesc(i int, v *types.Var) string { 1020 | name := v.Name() 1021 | if name != "" && name != "_" { 1022 | return name 1023 | } 1024 | return fmt.Sprintf("%d (%s)", i, v.Type().String()) 1025 | } 1026 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module mvdan.cc/unparam 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a 7 | golang.org/x/tools v0.30.0 8 | ) 9 | 10 | require ( 11 | golang.org/x/mod v0.23.0 // indirect 12 | golang.org/x/sync v0.11.0 // indirect 13 | golang.org/x/sys v0.30.0 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 2 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 3 | github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a h1:w3tdWGKbLGBPtR/8/oO74W6hmz0qE5q0z9aqSAewaaM= 4 | github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a/go.mod h1:S8kfXMp+yh77OxPD4fdM6YUknrZpQxLhvxzS4gDHENY= 5 | golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= 6 | golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 7 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 8 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 9 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 10 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 11 | golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= 12 | golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= 13 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, Daniel Martí 2 | // See LICENSE for licensing information 3 | 4 | // unparam reports unused function parameters and results in your code. 5 | package main 6 | 7 | import ( 8 | "flag" 9 | "fmt" 10 | "os" 11 | 12 | "mvdan.cc/unparam/check" 13 | ) 14 | 15 | var ( 16 | flagSet = flag.NewFlagSet("unparam", flag.ExitOnError) 17 | 18 | tests = flagSet.Bool("tests", false, "load tests too") 19 | exported = flagSet.Bool("exported", false, "inspect exported functions") 20 | debug = flagSet.Bool("debug", false, "debug prints") 21 | ) 22 | 23 | func main() { 24 | flagSet.Usage = func() { 25 | fmt.Fprintln(os.Stderr, "usage: unparam [flags] [package ...]") 26 | flagSet.PrintDefaults() 27 | } 28 | flagSet.Parse(os.Args[1:]) 29 | warns, err := check.UnusedParams(*tests, *exported, *debug, flagSet.Args()...) 30 | if err != nil { 31 | fmt.Fprintln(os.Stderr, err) 32 | os.Exit(1) 33 | } 34 | for _, warn := range warns { 35 | fmt.Println(warn) 36 | } 37 | if len(warns) > 0 { 38 | os.Exit(1) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/rogpeppe/go-internal/gotooltest" 9 | "github.com/rogpeppe/go-internal/testscript" 10 | ) 11 | 12 | func TestMain(m *testing.M) { 13 | testscript.Main(m, map[string]func(){ 14 | "unparam": main, 15 | }) 16 | } 17 | 18 | func TestScript(t *testing.T) { 19 | t.Parallel() 20 | p := testscript.Params{ 21 | Dir: filepath.Join("testdata", "script"), 22 | RequireExplicitExec: true, 23 | Setup: func(env *testscript.Env) error { 24 | env.Vars = append(env.Vars, "/="+string(os.PathSeparator)) 25 | return nil 26 | }, 27 | } 28 | if err := gotooltest.Setup(&p); err != nil { 29 | t.Fatal(err) 30 | } 31 | testscript.Run(t, p) 32 | } 33 | -------------------------------------------------------------------------------- /testdata/script/build_tags.txtar: -------------------------------------------------------------------------------- 1 | ! exec unparam . 2 | cmp stdout stdout.golden 3 | 4 | -- go.mod -- 5 | module testdata.tld/foo 6 | 7 | go 1.18 8 | -- stdout.golden -- 9 | foo_main.go:5:23: oneUnusedMain - b is unused 10 | foo_main.go:24:22: multImplsMethod - f is unused 11 | -- foo.go -- 12 | package foo 13 | 14 | var DoWork func() 15 | 16 | type FooType int 17 | 18 | -- foo_main.go -- 19 | //+build !other 20 | 21 | package foo 22 | 23 | func oneUnusedMain(a, b int) int { 24 | return a + 2 25 | } 26 | 27 | func multipleImpls(f int) int32 { 28 | DoWork() 29 | return 3 30 | } 31 | 32 | func (f FooType) multImplsMethod(f2 int) uint32 { 33 | DoWork() 34 | return 3 35 | } 36 | 37 | func (f *FooType) multImplsMethod2(f3 int) int64 { 38 | DoWork() 39 | return 3 40 | } 41 | 42 | func multImplsMethod(f int) uint64 { 43 | DoWork() 44 | return 3 45 | } 46 | 47 | -- foo_other.go -- 48 | //+build other 49 | 50 | package foo 51 | 52 | func multipleImpls(f int) int32 { 53 | DoWork() 54 | return int32(f) 55 | } 56 | 57 | func (f FooType) multImplsMethod(f2 int) uint32 { 58 | DoWork() 59 | return uint32(f) 60 | } 61 | 62 | func (f *FooType) multImplsMethod2(f3 int) int64 { 63 | DoWork() 64 | return int64(*f) 65 | } 66 | -------------------------------------------------------------------------------- /testdata/script/closure.txtar: -------------------------------------------------------------------------------- 1 | ! exec unparam . 2 | cmp stdout stdout.golden 3 | 4 | -- go.mod -- 5 | module testdata.tld/foo 6 | -- stdout.golden -- 7 | foo.go:5:19: ClosureUse$1 - v is unused 8 | -- foo.go -- 9 | package foo 10 | 11 | func ClosureUse() { 12 | var enclosed int 13 | setValue := func(v *int) { 14 | enclosed = 2 15 | } 16 | var newValue int = 4 17 | println(enclosed) 18 | setValue(&newValue) 19 | println(enclosed) 20 | } 21 | -------------------------------------------------------------------------------- /testdata/script/cmds.txtar: -------------------------------------------------------------------------------- 1 | exec unparam -h 2 | stderr '^usage: unparam \[flags' 3 | ! stderr 'test\.' # don't include flags from testing 4 | ! stderr 'command not specified' 5 | ! stdout . 6 | 7 | ! exec unparam -badflag -- somepkg 8 | stderr '-badflag' 9 | stderr '^usage: unparam \[flags' 10 | -------------------------------------------------------------------------------- /testdata/script/crashers.txtar: -------------------------------------------------------------------------------- 1 | ! exec unparam . 2 | cmp stdout stdout.golden 3 | 4 | -- go.mod -- 5 | module testdata.tld/foo 6 | -- stdout.golden -- 7 | foo.go:3:19: oneUnused - b is unused 8 | -- foo.go -- 9 | package foo 10 | 11 | func oneUnused(a, b int) int { 12 | return a + 123 13 | } 14 | 15 | type RecursiveIface interface { 16 | Foo(RecursiveIface) 17 | } 18 | 19 | var _ = 3 20 | -------------------------------------------------------------------------------- /testdata/script/extractparams.txtar: -------------------------------------------------------------------------------- 1 | ! exec unparam . 2 | cmp stdout stdout.golden 3 | 4 | -- go.mod -- 5 | module testdata.tld/foo 6 | -- stdout.golden -- 7 | foo.go:21:24: funcResultAsParam - a is unused 8 | foo.go:25:38: (FooType).methodResultAsParam - a is unused 9 | foo.go:32:29: funcResultsAsParams - b is unused 10 | foo.go:37:31: funcResultNotAsParams - b is unused 11 | foo.go:58:26: returnResultsOwn - result 0 (testdata.tld/foo.FooType) is never used 12 | -- foo.go -- 13 | package foo 14 | 15 | var DoWork func() 16 | 17 | var Sink interface{} 18 | 19 | type FooType int 20 | 21 | func funcResultAsParams(a, b FooType) FooType { 22 | DoWork() 23 | return a + 1 24 | } 25 | 26 | func (f FooType) methodResultAsParams(a, b FooType) FooType { 27 | DoWork() 28 | return a + 1 29 | } 30 | 31 | func generateResults() (a, b FooType) { return } 32 | 33 | func funcResultAsParam(a FooType) { 34 | DoWork() 35 | } 36 | 37 | func (f FooType) methodResultAsParam(a FooType) FooType { 38 | DoWork() 39 | return 3 40 | } 41 | 42 | func generateResult() (a FooType) { return } 43 | 44 | func funcResultsAsParams(a, b FooType) FooType { 45 | DoWork() 46 | return a + 1 47 | } 48 | 49 | func funcResultNotAsParams(a, b FooType) FooType { 50 | DoWork() 51 | return a + 1 52 | } 53 | 54 | func ResultCalls() { 55 | a, b := generateResults() 56 | Sink = funcResultNotAsParams(a, b) 57 | 58 | var f FooType 59 | Sink = funcResultAsParams(1, 2) 60 | Sink = funcResultAsParams(generateResults()) 61 | Sink = f.methodResultAsParams(1, 2) 62 | Sink = f.methodResultAsParams(generateResults()) 63 | go funcResultAsParam(4) 64 | go funcResultAsParam(generateResult()) 65 | Sink = f.methodResultAsParam(4) 66 | Sink = f.methodResultAsParam(generateResult()) 67 | Sink = funcResultsAsParams(generateResult(), generateResult()) 68 | } 69 | 70 | func returnResultsOwn() (FooType, FooType) { 71 | a := generateResult() 72 | a++ 73 | return a, a * 2 74 | } 75 | 76 | func returnResultsDirectly() (FooType, FooType) { 77 | return generateResults() 78 | } 79 | 80 | func ReturnResultsCalls() { 81 | _, Sink = returnResultsOwn() 82 | _, Sink = returnResultsOwn() 83 | _, Sink = returnResultsDirectly() 84 | _, Sink = returnResultsDirectly() 85 | } 86 | -------------------------------------------------------------------------------- /testdata/script/funclit.txtar: -------------------------------------------------------------------------------- 1 | ! exec unparam . 2 | cmp stdout stdout.golden 3 | 4 | -- go.mod -- 5 | module testdata.tld/foo 6 | -- stdout.golden -- 7 | foo.go:6:20: parent$1 - f is unused 8 | -- foo.go -- 9 | package foo 10 | 11 | var DoWork func() 12 | 13 | func parent() { 14 | oneUnused := func(f int) { 15 | DoWork() 16 | } 17 | oneUnused(3) 18 | } 19 | -------------------------------------------------------------------------------- /testdata/script/ignoredrets.txtar: -------------------------------------------------------------------------------- 1 | ! exec unparam . 2 | cmp stdout stdout.golden 3 | 4 | -- go.mod -- 5 | module testdata.tld/foo 6 | -- stdout.golden -- 7 | foo.go:9:22: singleIgnored - result 0 (rune) is never used 8 | foo.go:30:27: singleIgnoredName - result r is never used 9 | foo.go:40:33: singleIgnoredUnderscore - result 0 (rune) is never used 10 | foo.go:50:20: allIgnored - result 0 (int) is never used 11 | foo.go:50:25: allIgnored - result 1 (string) is never used 12 | foo.go:60:26: someIgnored - result 1 (string) is never used 13 | foo.go:85:29: ignoredGoDefer - result 1 (string) is never used 14 | -- foo.go -- 15 | package foo 16 | 17 | import "errors" 18 | 19 | var DoWork func() 20 | 21 | var Cond bool 22 | 23 | func singleIgnored() rune { 24 | DoWork() 25 | return '0' 26 | } 27 | 28 | func SingleIgnoredUse() { 29 | singleIgnored() 30 | _ = singleIgnored() 31 | } 32 | 33 | func singleNotIgnored() rune { 34 | DoWork() 35 | return '0' 36 | } 37 | 38 | func SingleNotIgnoredUse() { 39 | singleNotIgnored() 40 | r := singleNotIgnored() 41 | println(r) 42 | } 43 | 44 | func singleIgnoredName() (r rune) { 45 | DoWork() 46 | return '0' 47 | } 48 | 49 | func SingleIgnoredNameUse() { 50 | singleIgnoredName() 51 | _ = singleIgnoredName() 52 | } 53 | 54 | func singleIgnoredUnderscore() (_ rune) { 55 | DoWork() 56 | return '0' 57 | } 58 | 59 | func SingleIgnoredUnderscoreUse() { 60 | singleIgnoredUnderscore() 61 | _ = singleIgnoredUnderscore() 62 | } 63 | 64 | func allIgnored() (int, string) { 65 | DoWork() 66 | return 2, "foo" 67 | } 68 | 69 | func AllIgnoredUse() { 70 | allIgnored() 71 | _, _ = allIgnored() 72 | } 73 | 74 | func someIgnored() (int, string) { 75 | DoWork() 76 | return 2, "foo" 77 | } 78 | 79 | func SomeIgnoredUse() { 80 | someIgnored() 81 | i, _ := someIgnored() 82 | println(i) 83 | } 84 | 85 | func errorIgnored() (int, error) { 86 | DoWork() 87 | if Cond { 88 | return 3, errors.New("foo") 89 | } 90 | return 2, nil 91 | } 92 | 93 | func ErrorIgnoredUse() { 94 | errorIgnored() 95 | i, _ := errorIgnored() 96 | println(i) 97 | } 98 | 99 | func ignoredGoDefer() (int, string) { 100 | DoWork() 101 | return 2, "bar" 102 | } 103 | 104 | func IgnoredGoDeferUse() { 105 | go ignoredGoDefer() 106 | defer ignoredGoDefer() 107 | i, _ := ignoredGoDefer() 108 | println(i) 109 | } 110 | -------------------------------------------------------------------------------- /testdata/script/impl.txtar: -------------------------------------------------------------------------------- 1 | ! exec unparam ./... 2 | cmpenv stdout stdout.golden 3 | 4 | -- go.mod -- 5 | module testdata.tld/foo 6 | -- stdout.golden -- 7 | foo.go:7:19: oneUnused - b is unused 8 | foo.go:49:38: (*EnsuredImpl).otherMethod - b is unused 9 | main${/}main.go:5:19: oneUnused - b is unused 10 | -- foo.go -- 11 | package foo 12 | 13 | import "net/http" 14 | 15 | var DoWork func() 16 | 17 | func oneUnused(a, b int) int { 18 | return a + 123 19 | } 20 | 21 | func Handler(w http.ResponseWriter, r *http.Request) { 22 | DoWork() 23 | w.Write([]byte("hi")) 24 | } 25 | 26 | type FooIface interface { 27 | foo(w http.ResponseWriter, code int) error 28 | } 29 | 30 | type FooMayImpl struct{} 31 | 32 | func (f *FooMayImpl) foo(w http.ResponseWriter, code int) error { 33 | DoWork() 34 | w.Write([]byte("hi")) 35 | return nil 36 | } 37 | 38 | func FooMayUse(f FooIface) { 39 | f.foo(nil, 0) 40 | } 41 | 42 | func FooDoesUse(f FooMayImpl) { 43 | FooMayUse(&f) 44 | } 45 | 46 | type EnsuredIface interface { 47 | implMethod(a, b string) string 48 | } 49 | 50 | var _ EnsuredIface = (*EnsuredImpl)(nil) 51 | 52 | type EnsuredImpl struct{} 53 | 54 | func (e *EnsuredImpl) implMethod(a, b string) string { 55 | DoWork() 56 | return a + "bar" 57 | } 58 | 59 | func (e *EnsuredImpl) otherMethod(a, b uint) uint { 60 | DoWork() 61 | return a + 3 62 | } 63 | -- main/main.go -- 64 | package main 65 | 66 | import "flag" 67 | 68 | func oneUnused(a, b int) int { 69 | return a + 123 70 | } 71 | 72 | var DoWork func() 73 | 74 | type flagFunc func(string) 75 | 76 | func (f flagFunc) Set(s string) error { 77 | DoWork() 78 | f(s) 79 | return nil 80 | } 81 | 82 | func (f flagFunc) String() string { return "" } 83 | 84 | func init() { 85 | someFunc := func(s string) { 86 | DoWork() 87 | println(s) 88 | } 89 | flag.Var(flagFunc(someFunc), "someflag", "") 90 | } 91 | -------------------------------------------------------------------------------- /testdata/script/include_test.txtar: -------------------------------------------------------------------------------- 1 | ! exec unparam . 2 | stdout 'fooUnused .* unused' 3 | ! stdout 'footestUnused .* unused' 4 | ! stdout 'footestpkgUnused .* unused' 5 | 6 | ! exec unparam -tests=true . 7 | stdout 'fooUnused .* unused' 8 | stdout 'footestUnused .* unused' 9 | stdout 'footestpkgUnused .* unused' 10 | 11 | -- go.mod -- 12 | module testdata.tld/foo 13 | -- foo.go -- 14 | package foo 15 | 16 | func fooUnused(a, b int) int { 17 | return a + 123 18 | } 19 | -- foo_test.go -- 20 | package foo 21 | 22 | func footestUnused(a, b int) int { 23 | return a + 123 24 | } 25 | -- foopkg_test.go -- 26 | package foo_test 27 | 28 | func footestpkgUnused(a, b int) int { 29 | return a + 123 30 | } 31 | -------------------------------------------------------------------------------- /testdata/script/main.txtar: -------------------------------------------------------------------------------- 1 | ! exec unparam . 2 | cmp stdout stdout.golden 3 | 4 | -- go.mod -- 5 | module testdata.tld/foo 6 | -- stdout.golden -- 7 | main.go:3:19: OneUnused - b is unused 8 | main.go:10:29: ImplementsButUnused - b is unused 9 | -- main.go -- 10 | package main 11 | 12 | func OneUnused(a, b uint) uint { 13 | a += 1 14 | return a 15 | } 16 | 17 | type FnType func(a, b int) int 18 | 19 | func ImplementsButUnused(a, b int) int { 20 | a *= 4 21 | return a 22 | } 23 | 24 | func main() { 25 | var f FnType 26 | f(2, 3) 27 | } 28 | -------------------------------------------------------------------------------- /testdata/script/methods.txtar: -------------------------------------------------------------------------------- 1 | ! exec unparam . 2 | cmp stdout stdout.golden 3 | 4 | -- go.mod -- 5 | module testdata.tld/foo 6 | -- stdout.golden -- 7 | foo.go:8:28: (FooType).oneUnused - a is unused 8 | -- foo.go -- 9 | package foo 10 | 11 | // FooType is exported, as otherwise go/ssa gets rid of it. 12 | type FooType int 13 | 14 | func (f FooType) allUsed(a FooType) FooType { return f + a } 15 | 16 | func (f FooType) oneUnused(a FooType) FooType { return 2 * f } 17 | -------------------------------------------------------------------------------- /testdata/script/names.txtar: -------------------------------------------------------------------------------- 1 | ! exec unparam . 2 | cmp stdout stdout.golden 3 | 4 | -- go.mod -- 5 | module testdata.tld/foo 6 | -- stdout.golden -- 7 | foo.go:5:18: regularName - f is unused 8 | -- foo.go -- 9 | package foo 10 | 11 | var DoWork func() 12 | 13 | func regularName(f int) { DoWork() } 14 | 15 | func noName(int) { DoWork() } 16 | 17 | func underscoreName(_ int) { DoWork() } 18 | 19 | func zeroSizeStruct(f struct{}) { DoWork() } 20 | 21 | func zeroSizeArray(f [0]bool) { DoWork() } 22 | -------------------------------------------------------------------------------- /testdata/script/paramuses.txtar: -------------------------------------------------------------------------------- 1 | ! exec unparam . 2 | cmp stdout stdout.golden 3 | 4 | -- go.mod -- 5 | module testdata.tld/foo 6 | -- stdout.golden -- 7 | foo.go:28:36: notUsedInUnderscoreAssignment - s is unused 8 | -- foo.go -- 9 | package foo 10 | 11 | import ( 12 | "net/http" 13 | "time" 14 | ) 15 | 16 | var DoWork func() 17 | 18 | var Sink interface{} 19 | 20 | func UsedInFuncLit(s string) func() { 21 | return func() { 22 | println(s) 23 | } 24 | } 25 | 26 | func StructUsedInField(path string, expiry time.Time) { 27 | Sink = http.Cookie{Path: path, Expires: expiry} 28 | } 29 | 30 | func usedInUnderscoreAssignment(s string, d time.Duration) { 31 | _ = s 32 | DoWork() 33 | Sink = d 34 | } 35 | 36 | func notUsedInUnderscoreAssignment(s string, d time.Duration) { 37 | if d > 0 { 38 | s := 123 39 | _ = s 40 | } 41 | DoWork() 42 | Sink = d 43 | } 44 | 45 | func UnusedUnderscoreParam(_ int) { DoWork() } 46 | func UnusedUnderscorePrefixedParam(_var int) { DoWork() } 47 | -------------------------------------------------------------------------------- /testdata/script/parenthesized_expression.txtar: -------------------------------------------------------------------------------- 1 | ! exec unparam . 2 | cmp stdout stdout.golden 3 | 4 | -- go.mod -- 5 | module testdata.tld/foo 6 | 7 | go 1.18 8 | -- stdout.golden -- 9 | foo.go:7:37: (*FooType).multImplsMethod - a is unused 10 | foo.go:12:44: (*FooType).multImplsMethod2 - a is unused 11 | -- foo.go -- 12 | package foo 13 | 14 | var DoWork func() 15 | 16 | type FooType int 17 | 18 | func (f *(FooType)) multImplsMethod(a int) int64 { 19 | DoWork() 20 | return 3 21 | } 22 | 23 | func (f *((((FooType))))) multImplsMethod2(a int) int64 { 24 | DoWork() 25 | return 3 26 | } 27 | -------------------------------------------------------------------------------- /testdata/script/phi.txtar: -------------------------------------------------------------------------------- 1 | ! exec unparam . 2 | cmp stdout stdout.golden 3 | 4 | -- go.mod -- 5 | module testdata.tld/foo 6 | -- stdout.golden -- 7 | -- foo.go -- 8 | package foo 9 | 10 | func allUsedPhi(a, b FooType) *FooType { 11 | DoWork() 12 | c := a + b 13 | return &c 14 | } 15 | 16 | func oneUnusedPhi(a, b FooType) *FooType { 17 | DoWork() 18 | c := a + 4 19 | return &c 20 | } 21 | 22 | func PhiUse(fn2 bool) { 23 | var a, b FooType = 2, 3 24 | fn := allUsedPhi 25 | if fn2 { 26 | fn = oneUnusedPhi 27 | } 28 | println(fn(a, b)) 29 | } 30 | -------------------------------------------------------------------------------- /testdata/script/reused.txtar: -------------------------------------------------------------------------------- 1 | ! exec unparam . 2 | cmp stdout stdout.golden 3 | 4 | -- go.mod -- 5 | module testdata.tld/foo 6 | -- stdout.golden -- 7 | foo.go:5:24: reusedRecursively - f is unused 8 | foo.go:26:28: unusedVariadic - bs is unused 9 | foo.go:31:39: reusedRecursivelyVariadic - bs is unused 10 | -- foo.go -- 11 | package foo 12 | 13 | var DoWork func() 14 | 15 | func reusedRecursively(f int, b bool) rune { 16 | if !b { 17 | return 0 18 | } 19 | return reusedRecursively(f, b) 20 | } 21 | 22 | func reusedRecursivelySwapped(f1, f2 int, b bool) rune { 23 | if !b { 24 | return 0 25 | } 26 | return reusedRecursivelySwapped(f2, f1, b) 27 | } 28 | 29 | func reusedRecursivelyModified(f int, b bool) rune { 30 | if !b { 31 | return 0 32 | } 33 | return reusedRecursivelyModified(f+int(1), b) 34 | } 35 | 36 | func unusedVariadic(a int, bs ...byte) { 37 | DoWork() 38 | println(a) 39 | } 40 | 41 | func reusedRecursivelyVariadic(a int, bs ...byte) { 42 | if a == 0 { 43 | reusedRecursivelyVariadic(a, bs...) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /testdata/script/samerets.txtar: -------------------------------------------------------------------------------- 1 | ! exec unparam . 2 | cmp stdout stdout.golden 3 | 4 | -- go.mod -- 5 | module testdata.tld/foo 6 | -- stdout.golden -- 7 | foo.go:12:27: manyReturns - result 0 (int) is always 3 8 | foo.go:30:29: manyReturnsMultiple - result b is always true 9 | foo.go:30:37: manyReturnsMultiple - result s is always "foo" 10 | foo.go:38:30: singleNilError - result 1 (error) is always nil 11 | foo.go:43:28: manyNilError - result 1 (error) is always nil 12 | foo.go:74:39: doubleReturnNotForwarded - result 1 (error) is always nil 13 | foo.go:92:29: neverForwarded - result 1 (error) is always nil 14 | foo.go:99:50: (FooType).neverForwardedPtrMethod - result 1 (error) is always nil 15 | -- foo.go -- 16 | package foo 17 | 18 | var DoWork func() 19 | 20 | var Cond bool 21 | 22 | func singleReturn() int { 23 | DoWork() 24 | return 3 25 | } 26 | 27 | func manyReturns(i int64) int { 28 | if i > 3 { 29 | DoWork() 30 | return 3 31 | } 32 | return 3 33 | } 34 | 35 | func manyReturnsDifferent(b bool) int { 36 | for Cond { 37 | DoWork() 38 | if b { 39 | return 4 40 | } 41 | } 42 | return 3 43 | } 44 | 45 | func manyReturnsMultiple() (b bool, s string) { 46 | if Cond { 47 | DoWork() 48 | return true, "foo" 49 | } 50 | return true, "foo" 51 | } 52 | 53 | func singleNilError() (bool, error) { 54 | DoWork() 55 | return true, nil 56 | } 57 | 58 | func manyNilError() (bool, error) { 59 | if Cond { 60 | DoWork() 61 | return false, nil 62 | } 63 | return true, nil 64 | } 65 | 66 | func manyReturnsForwarded(r rune) int { 67 | if r == '3' { 68 | return 5 69 | } 70 | DoWork() 71 | return 5 72 | } 73 | 74 | func forwarding(r rune) int { 75 | DoWork() 76 | return manyReturnsForwarded(r) 77 | } 78 | 79 | func doubleReturnForwarded() (int, error) { 80 | DoWork() 81 | return 5, nil 82 | } 83 | 84 | func forwardingDouble() (int, error) { 85 | DoWork() 86 | return doubleReturnForwarded() 87 | } 88 | 89 | func doubleReturnNotForwarded() (int, error) { 90 | DoWork() 91 | return 5, nil 92 | } 93 | 94 | func falseForwardingDoubleLen() int { 95 | DoWork() 96 | n, err := doubleReturnNotForwarded() 97 | println(err) 98 | return n 99 | } 100 | 101 | func falseForwardingDoubleOrder() (error, int) { 102 | DoWork() 103 | n, err := doubleReturnNotForwarded() 104 | return err, n 105 | } 106 | 107 | func neverForwarded() (int, error) { 108 | DoWork() 109 | return 5, nil 110 | } 111 | 112 | type FooType int 113 | 114 | func (f FooType) neverForwardedPtrMethod() (int, error) { 115 | DoWork() 116 | return 5, nil 117 | } 118 | 119 | func neverForwarding() { 120 | DoWork() 121 | neverForwarded() 122 | f := new(FooType) 123 | f.neverForwardedPtrMethod() 124 | } 125 | 126 | func forwardingDoubleFuncLits(orig []byte) { 127 | rc := func(data []byte) (byte, error) { 128 | DoWork() 129 | return data[0], nil 130 | } 131 | _ = func() (byte, error) { 132 | return rc(orig) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /testdata/script/simple.txtar: -------------------------------------------------------------------------------- 1 | ! exec unparam . 2 | cmp stdout stdout.golden 3 | 4 | -- go.mod -- 5 | module testdata.tld/foo 6 | -- stdout.golden -- 7 | foo.go:7:19: oneUnused - b is unused 8 | foo.go:15:18: sliceUnused - f is unused 9 | -- foo.go -- 10 | package foo 11 | 12 | var DoWork func() 13 | 14 | func allUsed(a, b int) int { return a + b } 15 | 16 | func oneUnused(a, b int) int { 17 | return a + 123 18 | } 19 | 20 | type fooStruct struct { 21 | f int 22 | } 23 | 24 | func sliceUnused(f []fooStruct) { 25 | DoWork() 26 | } 27 | -------------------------------------------------------------------------------- /testdata/script/stubs.txtar: -------------------------------------------------------------------------------- 1 | ! exec unparam . 2 | cmp stdout stdout.golden 3 | 4 | -- go.mod -- 5 | module testdata.tld/foo 6 | -- stdout.golden -- 7 | foo.go:13:42: nonPanicImpl - f is unused 8 | foo.go:30:43: nonPanicImpl2 - f is unused 9 | foo.go:49:19: nonConstImpl - f is unused 10 | foo.go:49:31: nonConstImpl - s is unused 11 | foo.go:53:28: oneOverwritten - i is unused 12 | -- foo.go -- 13 | package foo 14 | 15 | import ( 16 | "errors" 17 | "log" 18 | "net/http" 19 | ) 20 | 21 | func dummyImpl(f int) {} 22 | 23 | func panicImpl(f int) { panic("dummy") } 24 | 25 | func nonPanicImpl(w http.ResponseWriter, f int) { 26 | for i := 0; i < 10; i++ { 27 | w.Write([]byte("foo")) 28 | } 29 | panic("default") 30 | } 31 | 32 | func throw(v ...interface{}) {} 33 | 34 | func throwImpl(f int) { throw("dummy") } 35 | 36 | func endlessLoop(w http.ResponseWriter) { 37 | for { 38 | w.Write([]byte("foo")) 39 | } 40 | } 41 | 42 | func nonPanicImpl2(w http.ResponseWriter, f int) { 43 | endlessLoop(w) 44 | panic("unreachable") 45 | } 46 | 47 | func zeroImpl(f int) (int, string, []byte) { return 0, "", nil } 48 | 49 | func errorsImpl(f int) error { return errors.New("unimpl") } 50 | 51 | type fooError int 52 | 53 | const constFoo = fooError(123) 54 | 55 | func (f fooError) Error() string { return "foo" } 56 | 57 | func customErrImpl(f fooError) error { return constFoo } 58 | 59 | var iface interface{} 60 | 61 | func nonConstImpl(f fooError, s string) error { return iface.(error) } 62 | 63 | func logImpl(f int) { log.Print("not implemented") } 64 | 65 | func oneOverwritten(a int, i uint8) (int, uint8) { 66 | i = 3 67 | a += 1 68 | return a, i 69 | } 70 | 71 | type fooStruct struct{ 72 | field string 73 | } 74 | 75 | func zeroStructImpl(f fooStruct) fooStruct { 76 | return fooStruct{} 77 | } 78 | 79 | func zeroMapImpl(f int) map[int]int { 80 | return map[int]int{} 81 | } 82 | 83 | func zeroMapMakeImpl(f int) map[int]int { 84 | return make(map[int]int, 0) 85 | } 86 | 87 | func customError(msg string) error { return errors.New("custom: " + msg) } 88 | func customErrorImpl(f fooError) (int8, error) { 89 | return -1, customError("x: bar") 90 | } 91 | 92 | func customErrorf(msg string) error { return errors.New("custom: " + msg) } 93 | func customErrorfImpl(f fooError) (int8, error) { 94 | return -1, customErrorf("x: bar") 95 | } 96 | -------------------------------------------------------------------------------- /testdata/script/typealias.txtar: -------------------------------------------------------------------------------- 1 | ! exec unparam . 2 | cmp stderr stderr.golden 3 | cmp stdout stdout.golden 4 | 5 | -- stderr.golden -- 6 | -- stdout.golden -- 7 | foo.go:9:20: (Alias).bar - s is unused 8 | -- go.mod -- 9 | module testdata.tld/foo 10 | 11 | go 1.18 12 | -- foo.go -- 13 | package foo 14 | 15 | type A struct { 16 | b struct{} 17 | } 18 | 19 | type Alias = A 20 | 21 | func (a Alias) bar(s string) { 22 | _ = a.b // Needed for bar to not be considered a dummy implementation 23 | } 24 | 25 | func test() { 26 | Alias{}.bar("") 27 | } 28 | -------------------------------------------------------------------------------- /testdata/script/typeparams.txtar: -------------------------------------------------------------------------------- 1 | ! exec unparam . 2 | cmp stderr stderr.golden 3 | cmp stdout stdout.golden 4 | 5 | -- stderr.golden -- 6 | -- stdout.golden -- 7 | foo.go:39:38: (Tuple[T1, T2]).unusedGeneric - t1 is unused 8 | foo.go:57:17: g1 - v is unused 9 | foo.go:60:21: g2 - v is unused 10 | foo.go:63:18: g1s - v is unused 11 | foo.go:66:22: g2s - v is unused 12 | foo.go:69:18: g1a - v is unused 13 | foo.go:72:22: g2a - v is unused 14 | foo.go:75:18: g1p - v is unused 15 | foo.go:78:22: g2p - v is unused 16 | foo.go:83:17: nonGeneric - v is unused 17 | foo.go:86:42: (GenericType1[T1]).genericMethod - v is unused 18 | -- go.mod -- 19 | module testdata.tld/foo 20 | 21 | go 1.18 22 | -- foo.go -- 23 | package foo 24 | 25 | var DoWork func() 26 | 27 | func GenericFunc[GenericParamA, B any](x GenericParamA, y B) {} 28 | 29 | type GenericVector[GenericParamT any] []GenericParamT 30 | 31 | type GenericGraph[T any] struct { 32 | Content T 33 | Edges []GenericGraph[T] 34 | } 35 | 36 | type PredeclaredSignedInteger interface { 37 | int | int8 | int16 | int32 | int64 38 | } 39 | 40 | type StringableSignedInteger interface { 41 | ~int | ~int8 | ~int16 | ~int32 | ~int64 42 | 43 | String() string 44 | } 45 | 46 | type CombineEmbeds interface { 47 | string | int 48 | 49 | interface { EmbeddedMethod() } 50 | RegularMethod() 51 | } 52 | 53 | type Tuple[T1 any, T2 any] struct { 54 | left T1 55 | right T2 56 | } 57 | 58 | func (t Tuple[T1, T2]) Left() T1 { return t.left } 59 | func (t Tuple[T1, T2]) Right() T2 { return t.right } 60 | 61 | func (t Tuple[T1, T2]) unusedGeneric(t1 T1, t2 T2) T2 { 62 | DoWork() 63 | return t2 64 | } 65 | 66 | // otherwise Tuple isn't reachable. 67 | var Sink interface{} = new(Tuple[int, string]) 68 | 69 | // The different number of type parameters result in different receiver AST 70 | // nodes on methods: IndexExpr and IndexListExpr. 71 | type GenericType1[T1 any] []T1 72 | type GenericType2[T1 any, T2 any] struct { 73 | t1 T1 74 | t2 T2 75 | } 76 | 77 | // must be able to handle parameters which are not themselves type parameters, 78 | // but have type parameters embedded within them 79 | func g1[T1 any](v GenericType1[T1]) { 80 | DoWork() 81 | } 82 | func g2[T1, T2 any](v GenericType2[T1, T2]) { 83 | DoWork() 84 | } 85 | func g1s[T1 any](v []GenericType1[T1]) { 86 | DoWork() 87 | } 88 | func g2s[T1, T2 any](v []GenericType2[T1, T2]) { 89 | DoWork() 90 | } 91 | func g1a[T1 any](v [2]GenericType1[T1]) { 92 | DoWork() 93 | } 94 | func g2a[T1, T2 any](v [2]GenericType2[T1, T2]) { 95 | DoWork() 96 | } 97 | func g1p[T1 any](v *GenericType1[T1]) { 98 | DoWork() 99 | } 100 | func g2p[T1, T2 any](v *GenericType2[T1, T2]) { 101 | DoWork() 102 | } 103 | 104 | // test logic to skip non-generic functions/methods 105 | func nonGeneric(v any) { 106 | DoWork() 107 | } 108 | func (g1 GenericType1[T1]) genericMethod(v any) { 109 | DoWork() 110 | } 111 | 112 | // make sure nonGeneric & genericMethod are reachable 113 | func Expose() { 114 | nonGeneric(nil) 115 | (GenericType1[any])(nil).genericMethod(nil) 116 | } 117 | 118 | -- foo_main.go -- 119 | //+build !other 120 | 121 | package foo 122 | 123 | func (g *GenericType1[T1]) multImplsMethodGeneric(f1 T1) { 124 | DoWork() 125 | } 126 | 127 | func (g GenericType2[T1, T2]) multImplsMethodGeneric(f1 T1, f2 T2) T2 { 128 | DoWork() 129 | return f2 130 | } 131 | -- foo_other.go -- 132 | //+build other 133 | 134 | package foo 135 | 136 | func (g *GenericType1[T1]) multImplsMethodGeneric(f1 T1) { 137 | DoWork() 138 | } 139 | 140 | func (g GenericType2[T1, T2]) multImplsMethodGeneric(f1 T1, f2 T2) T2 { 141 | DoWork() 142 | return f2 143 | } 144 | -------------------------------------------------------------------------------- /testdata/script/usedas.txtar: -------------------------------------------------------------------------------- 1 | ! exec unparam . 2 | cmp stdout stdout.golden 3 | 4 | -- go.mod -- 5 | module testdata.tld/foo 6 | -- stdout.golden -- 7 | foo.go:9:19: oneUnused - b is unused 8 | -- foo.go -- 9 | package foo 10 | 11 | import "net/http" 12 | 13 | type FooType int 14 | 15 | var DoWork func() 16 | 17 | func oneUnused(a, b int) int { 18 | return a + 123 19 | } 20 | 21 | func funcUsesAsParam(fn func(FooType) string) { fn(0) } 22 | 23 | func funcPassedAsParam(f FooType) string { 24 | DoWork() 25 | return "foo" 26 | } 27 | 28 | func (_ FooType) methodPassedAsParam(f FooType) string { 29 | DoWork() 30 | return "bar" 31 | } 32 | 33 | func PassesFuncsAsParams() { 34 | funcUsesAsParam(funcPassedAsParam) 35 | var f FooType 36 | funcUsesAsParam(f.methodPassedAsParam) 37 | } 38 | 39 | func UsedAsSliceElem() { 40 | for _, f := range []func(FooType, int32){ 41 | func(f FooType, i int32) { 42 | DoWork() 43 | println(i) 44 | }, 45 | } { 46 | f(1, 2) 47 | } 48 | for _, f := range []struct { 49 | f2 func(f FooType, i int64) 50 | }{ 51 | {f2: func(f FooType, i int64) { 52 | DoWork() 53 | println(i) 54 | }}, 55 | } { 56 | f.f2(3, 4) 57 | } 58 | } 59 | 60 | func UsedAsArg() { 61 | foo := func(f func(f FooType, u uint32)) { 62 | f(5, 6) 63 | } 64 | bar := func(v interface{}) { 65 | DoWork() 66 | println(v) 67 | } 68 | foo(func(f FooType, u uint32) { 69 | println(f) 70 | }) 71 | bar(func(f FooType, u uint64) { 72 | println(f) 73 | }) 74 | } 75 | 76 | func globalParam(f func(f FooType, i int8)) { 77 | f(7, 8) 78 | } 79 | 80 | func usedAsGlobalArg(f FooType, i int8) { 81 | DoWork() 82 | println(f) 83 | } 84 | 85 | func GlobArgUse() { 86 | globalParam(usedAsGlobalArg) 87 | } 88 | 89 | var globalFunc func(f FooType, i uint8) 90 | 91 | func usedAsGlobal(f FooType, i uint8) { 92 | DoWork() 93 | println(f) 94 | } 95 | 96 | func UsesAsGlobal() { 97 | globalFunc = usedAsGlobal 98 | } 99 | 100 | type barIface interface { 101 | bar(FooType, uint16) 102 | } 103 | 104 | func usedAsPhi(f FooType, i int16) { 105 | DoWork() 106 | println(f) 107 | } 108 | 109 | var Cond bool 110 | 111 | func UsesAsPhi() { 112 | fn := usedAsPhi 113 | if Cond { 114 | fn = func(f FooType, i int16) { 115 | DoWork() 116 | println(f, i) 117 | } 118 | } 119 | fn(FooType(2), 3) 120 | } 121 | 122 | type barType struct{} 123 | 124 | func (b *barType) bar(f FooType, u uint16) { 125 | DoWork() 126 | println(f) 127 | } 128 | 129 | func barImpl() barIface { return &barType{} } 130 | 131 | func BarIfaceUse() { 132 | b := barImpl() 133 | b.bar(0, 1) 134 | } 135 | 136 | func (f FooType) methodPassedAsFuncLitParam(f2 FooType) bool { 137 | if f == 3 { 138 | DoWork() 139 | return true 140 | } 141 | return true 142 | } 143 | 144 | func (f FooType) methodPassedAsFuncLitParam2() bool { 145 | if f == 4 { 146 | DoWork() 147 | return true 148 | } 149 | return true 150 | } 151 | 152 | func MethodUsedAsFuncLitParam() { 153 | foo := func(f func(f FooType) bool) { 154 | f(2) 155 | } 156 | var f FooType 157 | foo(f.methodPassedAsFuncLitParam) 158 | foo((FooType).methodPassedAsFuncLitParam2) 159 | } 160 | 161 | var funcUsedAsGlobalField = http.Client{ 162 | CheckRedirect: func(req *http.Request, via []*http.Request) error { 163 | if req.Method == "GET" { 164 | println(req.Host) 165 | } 166 | return nil 167 | }, 168 | } 169 | 170 | func (f FooType) checkRedirect(req *http.Request, via []*http.Request) error { 171 | if req.Method == "GET" { 172 | println(req.Host) 173 | } 174 | return nil 175 | } 176 | 177 | func MethodUsedAsParamField(client *http.Client) { 178 | var f FooType 179 | client.CheckRedirect = f.checkRedirect 180 | } 181 | 182 | var mux = http.NewServeMux() 183 | 184 | func UsedAsGlobalParam(someBool bool) { 185 | mux.HandleFunc("/inline", func(w http.ResponseWriter, r *http.Request) { 186 | if r.Method == "GET" { 187 | println(r.Host) 188 | } 189 | }) 190 | closure := func(w http.ResponseWriter, r *http.Request) { 191 | if r.Method == "POST" && someBool { 192 | println(r.Host) 193 | } 194 | } 195 | mux.HandleFunc("/closureVar", closure) 196 | } 197 | 198 | type FuncInterfaceField struct { 199 | Func interface{} 200 | } 201 | 202 | var usedAsGlobalInterfaceField = FuncInterfaceField { 203 | Func: func(i int, bs []byte) { 204 | if i == 0 { 205 | DoWork() 206 | } 207 | }, 208 | } 209 | 210 | type SomeFuncType func(string, string) string 211 | 212 | var convertedToType = SomeFuncType(func(a, b string) string { 213 | DoWork() 214 | return a 215 | }) 216 | 217 | var usedAsGlobalInterfaceMapValue = map[string]interface{}{ 218 | "someFunc": func(i int, s string) { 219 | if i == 0 { 220 | DoWork() 221 | } 222 | }, 223 | } 224 | 225 | func FuncLitUsedAsReturn(fixedVal int64) func(int64) (int64, error) { 226 | return func(int64) (int64, error) { 227 | return fixedVal, nil 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /testdata/script/values.txtar: -------------------------------------------------------------------------------- 1 | ! exec unparam . 2 | cmp stdout stdout.golden 3 | 4 | -- go.mod -- 5 | module testdata.tld/foo 6 | -- stdout.golden -- 7 | foo.go:9:23: receivesSameMany - f always receives 3 8 | foo.go:16:26: receivesSameManyLit - r always receives 'a' (97) 9 | foo.go:23:28: receivesSameManyNamed - f always receives FooConst (123) 10 | foo.go:30:28: receivesSameManyMixed - f always receives 123 11 | foo.go:72:32: receivesSameFromGenerated - f always receives int(2) (2) 12 | foo.go:79:22: receivesSameNil - v always receives nil 13 | foo.go:86:28: receivesSameInterface - v always receives 123 14 | -- foo.go -- 15 | package foo 16 | 17 | import "math/rand" 18 | 19 | var DoWork func() 20 | 21 | const FooConst = 123 22 | 23 | func receivesSameMany(f int) { 24 | DoWork() 25 | if f == 0 { 26 | println(f) 27 | } 28 | } 29 | 30 | func receivesSameManyLit(r rune) { 31 | DoWork() 32 | if r == '0' { 33 | println(r) 34 | } 35 | } 36 | 37 | func receivesSameManyNamed(f int) { 38 | DoWork() 39 | if f == 0 { 40 | println(f) 41 | } 42 | } 43 | 44 | func receivesSameManyMixed(f int) { 45 | DoWork() 46 | if f == 1 { 47 | println(f) 48 | } 49 | } 50 | 51 | func receivesSameOnce(r rune) { 52 | DoWork() 53 | if r == '1' { 54 | println(r) 55 | } 56 | } 57 | 58 | func receivesDifferent(r rune) { 59 | DoWork() 60 | if r == '0' { 61 | println(r) 62 | } 63 | } 64 | 65 | func ReceivesSameExported(r rune) { 66 | DoWork() 67 | if r == '0' { 68 | println(r) 69 | } 70 | } 71 | 72 | func receivesCallExpr(r rune) { 73 | DoWork() 74 | if r == '0' { 75 | println(r) 76 | } 77 | } 78 | 79 | func randRune() rune { return rune(rand.Int31()) } 80 | 81 | func withVariadic(s ...string) { 82 | DoWork() 83 | println(len(s)) 84 | } 85 | 86 | func receivesSameFromGenerated(f int) { 87 | DoWork() 88 | if f == 4 { 89 | println(f) 90 | } 91 | } 92 | 93 | func receivesSameNil(v interface{}) { 94 | DoWork() 95 | if v != nil { 96 | println(v) 97 | } 98 | } 99 | 100 | func receivesSameInterface(v interface{}) { 101 | DoWork() 102 | if v == false { 103 | println(v) 104 | } 105 | } 106 | 107 | func receivesInterfaceDiffType(v interface{}) { 108 | DoWork() 109 | if v == false { 110 | println(v) 111 | } 112 | } 113 | 114 | func CallReceivers() { 115 | receivesSameMany(3) 116 | receivesSameMany(3) 117 | receivesSameMany(3) 118 | receivesSameMany(3) 119 | receivesSameManyLit('a') 120 | receivesSameManyLit('a') 121 | receivesSameManyLit('a') 122 | receivesSameManyLit('a') 123 | receivesSameManyNamed(FooConst) 124 | receivesSameManyNamed(FooConst) 125 | receivesSameManyNamed(FooConst) 126 | receivesSameManyNamed(FooConst) 127 | receivesSameManyMixed(FooConst) 128 | receivesSameManyMixed(FooConst) 129 | receivesSameManyMixed(123) 130 | receivesSameManyMixed(int(123)) 131 | receivesSameOnce('b') 132 | receivesDifferent('a') 133 | receivesDifferent('b') 134 | receivesDifferent('c') 135 | receivesDifferent('d') 136 | ReceivesSameExported('b') 137 | ReceivesSameExported('b') 138 | ReceivesSameExported('b') 139 | ReceivesSameExported('b') 140 | receivesCallExpr(randRune()) 141 | receivesCallExpr(randRune()) 142 | receivesCallExpr(randRune()) 143 | receivesCallExpr(randRune()) 144 | receivesSameNil(nil) 145 | receivesSameNil(nil) 146 | receivesSameNil(nil) 147 | receivesSameNil(nil) 148 | receivesSameInterface(123) 149 | receivesSameInterface(123) 150 | receivesSameInterface(123) 151 | receivesSameInterface(123) 152 | receivesInterfaceDiffType((*int)(nil)) 153 | receivesInterfaceDiffType((*int)(nil)) 154 | receivesInterfaceDiffType((*int)(nil)) 155 | receivesInterfaceDiffType((*uint)(nil)) 156 | withVariadic() 157 | } 158 | -- generated.go -- 159 | // Code generated by some-program. DO NOT EDIT. 160 | 161 | package foo 162 | 163 | func oneUnusedGenerated(a, b int) int { 164 | DoWork() 165 | a += 1 166 | return a 167 | } 168 | 169 | func GeneratedCalls() { 170 | receivesSameFromGenerated(int(2)) 171 | receivesSameFromGenerated(int(2)) 172 | receivesSameFromGenerated(int(2)) 173 | receivesSameFromGenerated(int(2)) 174 | } 175 | --------------------------------------------------------------------------------