golang.org/x/tools/gopls@v0.15.3/internal/analysis/unusedparams/unusedparams.go (about)

     1  // Copyright 2020 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package unusedparams
     6  
     7  import (
     8  	_ "embed"
     9  	"fmt"
    10  	"go/ast"
    11  	"go/types"
    12  
    13  	"golang.org/x/tools/go/analysis"
    14  	"golang.org/x/tools/go/analysis/passes/inspect"
    15  	"golang.org/x/tools/go/ast/inspector"
    16  	"golang.org/x/tools/gopls/internal/util/slices"
    17  	"golang.org/x/tools/internal/analysisinternal"
    18  )
    19  
    20  //go:embed doc.go
    21  var doc string
    22  
    23  var Analyzer = &analysis.Analyzer{
    24  	Name:     "unusedparams",
    25  	Doc:      analysisinternal.MustExtractDoc(doc, "unusedparams"),
    26  	Requires: []*analysis.Analyzer{inspect.Analyzer},
    27  	Run:      run,
    28  	URL:      "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/unusedparams",
    29  }
    30  
    31  const FixCategory = "unusedparam" // recognized by gopls ApplyFix
    32  
    33  func run(pass *analysis.Pass) (any, error) {
    34  	inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
    35  
    36  	// First find all "address-taken" functions.
    37  	// We must conservatively assume that their parameters
    38  	// are all required to conform to some signature.
    39  	//
    40  	// A named function is address-taken if it is somewhere
    41  	// used not in call position:
    42  	//
    43  	//	f(...)		// not address-taken
    44  	//      use(f)          // address-taken
    45  	//
    46  	// A literal function is address-taken if it is not
    47  	// immediately bound to a variable, or if that variable is
    48  	// used not in call position:
    49  	//
    50  	//    f := func() { ... }; f()     			used only in call position
    51  	//    var f func(); f = func() { ...f()... }; f()     	ditto
    52  	//    use(func() { ... })				address-taken
    53  	//
    54  
    55  	// Note: this algorithm relies on the assumption that the
    56  	// analyzer is called only for the "widest" package for a
    57  	// given file: that is, p_test in preference to p, if both
    58  	// exist. Analyzing only package p may produce diagnostics
    59  	// that would be falsified based on declarations in p_test.go
    60  	// files. The gopls analysis driver does this, but most
    61  	// drivers to not, so running this command in, say,
    62  	// unitchecker or multichecker may produce incorrect results.
    63  
    64  	// Gather global information:
    65  	// - uses of functions not in call position
    66  	// - unexported interface methods
    67  	// - all referenced variables
    68  
    69  	usesOutsideCall := make(map[types.Object][]*ast.Ident)
    70  	unexportedIMethodNames := make(map[string]bool)
    71  	{
    72  		callPosn := make(map[*ast.Ident]bool) // all idents f appearing in f() calls
    73  		filter := []ast.Node{
    74  			(*ast.CallExpr)(nil),
    75  			(*ast.InterfaceType)(nil),
    76  		}
    77  		inspect.Preorder(filter, func(n ast.Node) {
    78  			switch n := n.(type) {
    79  			case *ast.CallExpr:
    80  				// Strip off any generic instantiation.
    81  				fun := n.Fun
    82  				switch fun_ := fun.(type) {
    83  				case *ast.IndexExpr:
    84  					fun = fun_.X // f[T]()  (funcs[i]() is rejected below)
    85  				case *ast.IndexListExpr:
    86  					fun = fun_.X // f[K, V]()
    87  				}
    88  
    89  				// Find object:
    90  				// record non-exported function, method, or func-typed var.
    91  				var id *ast.Ident
    92  				switch fun := fun.(type) {
    93  				case *ast.Ident:
    94  					id = fun
    95  				case *ast.SelectorExpr:
    96  					id = fun.Sel
    97  				}
    98  				if id != nil && !id.IsExported() {
    99  					switch pass.TypesInfo.Uses[id].(type) {
   100  					case *types.Func, *types.Var:
   101  						callPosn[id] = true
   102  					}
   103  				}
   104  
   105  			case *ast.InterfaceType:
   106  				// Record the set of names of unexported interface methods.
   107  				// (It would be more precise to record signatures but
   108  				// generics makes it tricky, and this conservative
   109  				// heuristic is close enough.)
   110  				t := pass.TypesInfo.TypeOf(n).(*types.Interface)
   111  				for i := 0; i < t.NumExplicitMethods(); i++ {
   112  					m := t.ExplicitMethod(i)
   113  					if !m.Exported() && m.Name() != "_" {
   114  						unexportedIMethodNames[m.Name()] = true
   115  					}
   116  				}
   117  			}
   118  		})
   119  
   120  		for id, obj := range pass.TypesInfo.Uses {
   121  			if !callPosn[id] {
   122  				// This includes "f = func() {...}", which we deal with below.
   123  				usesOutsideCall[obj] = append(usesOutsideCall[obj], id)
   124  			}
   125  		}
   126  	}
   127  
   128  	// Find all vars (notably parameters) that are used.
   129  	usedVars := make(map[*types.Var]bool)
   130  	for _, obj := range pass.TypesInfo.Uses {
   131  		if v, ok := obj.(*types.Var); ok {
   132  			if v.IsField() {
   133  				continue // no point gathering these
   134  			}
   135  			usedVars[v] = true
   136  		}
   137  	}
   138  
   139  	// Check each non-address-taken function's parameters are all used.
   140  	filter := []ast.Node{
   141  		(*ast.FuncDecl)(nil),
   142  		(*ast.FuncLit)(nil),
   143  	}
   144  	inspect.WithStack(filter, func(n ast.Node, push bool, stack []ast.Node) bool {
   145  		// (We always return true so that we visit nested FuncLits.)
   146  
   147  		if !push {
   148  			return true
   149  		}
   150  
   151  		var (
   152  			fn    types.Object // function symbol (*Func, possibly *Var for a FuncLit)
   153  			ftype *ast.FuncType
   154  			body  *ast.BlockStmt
   155  		)
   156  		switch n := n.(type) {
   157  		case *ast.FuncDecl:
   158  			// We can't analyze non-Go functions.
   159  			if n.Body == nil {
   160  				return true
   161  			}
   162  
   163  			// Ignore exported functions and methods: we
   164  			// must assume they may be address-taken in
   165  			// another package.
   166  			if n.Name.IsExported() {
   167  				return true
   168  			}
   169  
   170  			// Ignore methods that match the name of any
   171  			// interface method declared in this package,
   172  			// as the method's signature may need to conform
   173  			// to the interface.
   174  			if n.Recv != nil && unexportedIMethodNames[n.Name.Name] {
   175  				return true
   176  			}
   177  
   178  			fn = pass.TypesInfo.Defs[n.Name].(*types.Func)
   179  			ftype, body = n.Type, n.Body
   180  
   181  		case *ast.FuncLit:
   182  			// Find the symbol for the variable (if any)
   183  			// to which the FuncLit is bound.
   184  			// (We don't bother to allow ParenExprs.)
   185  			switch parent := stack[len(stack)-2].(type) {
   186  			case *ast.AssignStmt:
   187  				// f  = func() {...}
   188  				// f := func() {...}
   189  				for i, rhs := range parent.Rhs {
   190  					if rhs == n {
   191  						if id, ok := parent.Lhs[i].(*ast.Ident); ok {
   192  							fn = pass.TypesInfo.ObjectOf(id)
   193  
   194  							// Edge case: f = func() {...}
   195  							// should not count as a use.
   196  							if pass.TypesInfo.Uses[id] != nil {
   197  								usesOutsideCall[fn] = slices.Remove(usesOutsideCall[fn], id)
   198  							}
   199  
   200  							if fn == nil && id.Name == "_" {
   201  								// Edge case: _ = func() {...}
   202  								// has no var. Fake one.
   203  								fn = types.NewVar(id.Pos(), pass.Pkg, id.Name, pass.TypesInfo.TypeOf(n))
   204  							}
   205  						}
   206  						break
   207  					}
   208  				}
   209  
   210  			case *ast.ValueSpec:
   211  				// var f = func() { ... }
   212  				// (unless f is an exported package-level var)
   213  				for i, val := range parent.Values {
   214  					if val == n {
   215  						v := pass.TypesInfo.Defs[parent.Names[i]]
   216  						if !(v.Parent() == pass.Pkg.Scope() && v.Exported()) {
   217  							fn = v
   218  						}
   219  						break
   220  					}
   221  				}
   222  			}
   223  
   224  			ftype, body = n.Type, n.Body
   225  		}
   226  
   227  		// Ignore address-taken functions and methods: unused
   228  		// parameters may be needed to conform to a func type.
   229  		if fn == nil || len(usesOutsideCall[fn]) > 0 {
   230  			return true
   231  		}
   232  
   233  		// If there are no parameters, there are no unused parameters.
   234  		if ftype.Params.NumFields() == 0 {
   235  			return true
   236  		}
   237  
   238  		// To reduce false positives, ignore functions with an
   239  		// empty or panic body.
   240  		//
   241  		// We choose not to ignore functions whose body is a
   242  		// single return statement (as earlier versions did)
   243  		// 	func f() { return }
   244  		// 	func f() { return g(...) }
   245  		// as we suspect that was just heuristic to reduce
   246  		// false positives in the earlier unsound algorithm.
   247  		switch len(body.List) {
   248  		case 0:
   249  			// Empty body. Although the parameter is
   250  			// unnecessary, it's pretty obvious to the
   251  			// reader that that's the case, so we allow it.
   252  			return true // func f() {}
   253  		case 1:
   254  			if stmt, ok := body.List[0].(*ast.ExprStmt); ok {
   255  				// We allow a panic body, as it is often a
   256  				// placeholder for a future implementation:
   257  				// 	func f() { panic(...) }
   258  				if call, ok := stmt.X.(*ast.CallExpr); ok {
   259  					if fun, ok := call.Fun.(*ast.Ident); ok && fun.Name == "panic" {
   260  						return true
   261  					}
   262  				}
   263  			}
   264  		}
   265  
   266  		// Report each unused parameter.
   267  		for _, field := range ftype.Params.List {
   268  			for _, id := range field.Names {
   269  				if id.Name == "_" {
   270  					continue
   271  				}
   272  				param := pass.TypesInfo.Defs[id].(*types.Var)
   273  				if !usedVars[param] {
   274  					start, end := field.Pos(), field.End()
   275  					if len(field.Names) > 1 {
   276  						start, end = id.Pos(), id.End()
   277  					}
   278  					// This diagnostic carries both an edit-based fix to
   279  					// rename the unused parameter, and a command-based fix
   280  					// to remove it (see golang.RemoveUnusedParameter).
   281  					pass.Report(analysis.Diagnostic{
   282  						Pos:      start,
   283  						End:      end,
   284  						Message:  fmt.Sprintf("unused parameter: %s", id.Name),
   285  						Category: FixCategory,
   286  						SuggestedFixes: []analysis.SuggestedFix{
   287  							{
   288  								Message: `Rename parameter to "_"`,
   289  								TextEdits: []analysis.TextEdit{{
   290  									Pos:     id.Pos(),
   291  									End:     id.End(),
   292  									NewText: []byte("_"),
   293  								}},
   294  							},
   295  							{
   296  								Message: fmt.Sprintf("Remove unused parameter %q", id.Name),
   297  								// No TextEdits => computed by gopls command
   298  							},
   299  						},
   300  					})
   301  				}
   302  			}
   303  		}
   304  
   305  		return true
   306  	})
   307  	return nil, nil
   308  }