blob: 74cd662285c9f40fff0eb63851a0bec625e5dfe6 [file] [log] [blame]
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package unusedparams
import (
_ "embed"
"fmt"
"go/ast"
"go/types"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/gopls/internal/util/slices"
"golang.org/x/tools/internal/analysisinternal"
)
//go:embed doc.go
var doc string
var Analyzer = &analysis.Analyzer{
Name: "unusedparams",
Doc: analysisinternal.MustExtractDoc(doc, "unusedparams"),
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/unusedparams",
}
const FixCategory = "unusedparam" // recognized by gopls ApplyFix
func run(pass *analysis.Pass) (any, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
// First find all "address-taken" functions.
// We must conservatively assume that their parameters
// are all required to conform to some signature.
//
// A named function is address-taken if it is somewhere
// used not in call position:
//
// f(...) // not address-taken
// use(f) // address-taken
//
// A literal function is address-taken if it is not
// immediately bound to a variable, or if that variable is
// used not in call position:
//
// f := func() { ... }; f() used only in call position
// var f func(); f = func() { ...f()... }; f() ditto
// use(func() { ... }) address-taken
//
// Note: this algorithm relies on the assumption that the
// analyzer is called only for the "widest" package for a
// given file: that is, p_test in preference to p, if both
// exist. Analyzing only package p may produce diagnostics
// that would be falsified based on declarations in p_test.go
// files. The gopls analysis driver does this, but most
// drivers to not, so running this command in, say,
// unitchecker or multichecker may produce incorrect results.
// Gather global information:
// - uses of functions not in call position
// - unexported interface methods
// - all referenced variables
usesOutsideCall := make(map[types.Object][]*ast.Ident)
unexportedIMethodNames := make(map[string]bool)
{
callPosn := make(map[*ast.Ident]bool) // all idents f appearing in f() calls
filter := []ast.Node{
(*ast.CallExpr)(nil),
(*ast.InterfaceType)(nil),
}
inspect.Preorder(filter, func(n ast.Node) {
switch n := n.(type) {
case *ast.CallExpr:
// Strip off any generic instantiation.
fun := n.Fun
switch fun_ := fun.(type) {
case *ast.IndexExpr:
fun = fun_.X // f[T]() (funcs[i]() is rejected below)
case *ast.IndexListExpr:
fun = fun_.X // f[K, V]()
}
// Find object:
// record non-exported function, method, or func-typed var.
var id *ast.Ident
switch fun := fun.(type) {
case *ast.Ident:
id = fun
case *ast.SelectorExpr:
id = fun.Sel
}
if id != nil && !id.IsExported() {
switch pass.TypesInfo.Uses[id].(type) {
case *types.Func, *types.Var:
callPosn[id] = true
}
}
case *ast.InterfaceType:
// Record the set of names of unexported interface methods.
// (It would be more precise to record signatures but
// generics makes it tricky, and this conservative
// heuristic is close enough.)
t := pass.TypesInfo.TypeOf(n).(*types.Interface)
for i := 0; i < t.NumExplicitMethods(); i++ {
m := t.ExplicitMethod(i)
if !m.Exported() && m.Name() != "_" {
unexportedIMethodNames[m.Name()] = true
}
}
}
})
for id, obj := range pass.TypesInfo.Uses {
if !callPosn[id] {
// This includes "f = func() {...}", which we deal with below.
usesOutsideCall[obj] = append(usesOutsideCall[obj], id)
}
}
}
// Find all vars (notably parameters) that are used.
usedVars := make(map[*types.Var]bool)
for _, obj := range pass.TypesInfo.Uses {
if v, ok := obj.(*types.Var); ok {
if v.IsField() {
continue // no point gathering these
}
usedVars[v] = true
}
}
// Check each non-address-taken function's parameters are all used.
filter := []ast.Node{
(*ast.FuncDecl)(nil),
(*ast.FuncLit)(nil),
}
inspect.WithStack(filter, func(n ast.Node, push bool, stack []ast.Node) bool {
// (We always return true so that we visit nested FuncLits.)
if !push {
return true
}
var (
fn types.Object // function symbol (*Func, possibly *Var for a FuncLit)
ftype *ast.FuncType
body *ast.BlockStmt
)
switch n := n.(type) {
case *ast.FuncDecl:
// We can't analyze non-Go functions.
if n.Body == nil {
return true
}
// Ignore exported functions and methods: we
// must assume they may be address-taken in
// another package.
if n.Name.IsExported() {
return true
}
// Ignore methods that match the name of any
// interface method declared in this package,
// as the method's signature may need to conform
// to the interface.
if n.Recv != nil && unexportedIMethodNames[n.Name.Name] {
return true
}
fn = pass.TypesInfo.Defs[n.Name].(*types.Func)
ftype, body = n.Type, n.Body
case *ast.FuncLit:
// Find the symbol for the variable (if any)
// to which the FuncLit is bound.
// (We don't bother to allow ParenExprs.)
switch parent := stack[len(stack)-2].(type) {
case *ast.AssignStmt:
// f = func() {...}
// f := func() {...}
for i, rhs := range parent.Rhs {
if rhs == n {
if id, ok := parent.Lhs[i].(*ast.Ident); ok {
fn = pass.TypesInfo.ObjectOf(id)
// Edge case: f = func() {...}
// should not count as a use.
if pass.TypesInfo.Uses[id] != nil {
usesOutsideCall[fn] = slices.Remove(usesOutsideCall[fn], id)
}
if fn == nil && id.Name == "_" {
// Edge case: _ = func() {...}
// has no var. Fake one.
fn = types.NewVar(id.Pos(), pass.Pkg, id.Name, pass.TypesInfo.TypeOf(n))
}
}
break
}
}
case *ast.ValueSpec:
// var f = func() { ... }
// (unless f is an exported package-level var)
for i, val := range parent.Values {
if val == n {
v := pass.TypesInfo.Defs[parent.Names[i]]
if !(v.Parent() == pass.Pkg.Scope() && v.Exported()) {
fn = v
}
break
}
}
}
ftype, body = n.Type, n.Body
}
// Ignore address-taken functions and methods: unused
// parameters may be needed to conform to a func type.
if fn == nil || len(usesOutsideCall[fn]) > 0 {
return true
}
// If there are no parameters, there are no unused parameters.
if ftype.Params.NumFields() == 0 {
return true
}
// To reduce false positives, ignore functions with an
// empty or panic body.
//
// We choose not to ignore functions whose body is a
// single return statement (as earlier versions did)
// func f() { return }
// func f() { return g(...) }
// as we suspect that was just heuristic to reduce
// false positives in the earlier unsound algorithm.
switch len(body.List) {
case 0:
// Empty body. Although the parameter is
// unnecessary, it's pretty obvious to the
// reader that that's the case, so we allow it.
return true // func f() {}
case 1:
if stmt, ok := body.List[0].(*ast.ExprStmt); ok {
// We allow a panic body, as it is often a
// placeholder for a future implementation:
// func f() { panic(...) }
if call, ok := stmt.X.(*ast.CallExpr); ok {
if fun, ok := call.Fun.(*ast.Ident); ok && fun.Name == "panic" {
return true
}
}
}
}
// Report each unused parameter.
for _, field := range ftype.Params.List {
for _, id := range field.Names {
if id.Name == "_" {
continue
}
param := pass.TypesInfo.Defs[id].(*types.Var)
if !usedVars[param] {
start, end := field.Pos(), field.End()
if len(field.Names) > 1 {
start, end = id.Pos(), id.End()
}
// This diagnostic carries both an edit-based fix to
// rename the unused parameter, and a command-based fix
// to remove it (see golang.RemoveUnusedParameter).
pass.Report(analysis.Diagnostic{
Pos: start,
End: end,
Message: fmt.Sprintf("unused parameter: %s", id.Name),
Category: FixCategory,
SuggestedFixes: []analysis.SuggestedFix{
{
Message: `Rename parameter to "_"`,
TextEdits: []analysis.TextEdit{{
Pos: id.Pos(),
End: id.End(),
NewText: []byte("_"),
}},
},
{
Message: fmt.Sprintf("Remove unused parameter %q", id.Name),
// No TextEdits => computed by gopls command
},
},
})
}
}
}
return true
})
return nil, nil
}