1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308
|
// 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 = "unusedparams" // 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
}
|