|
| 1 | +// Copyright 2024 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 unusedfunc |
| 6 | + |
| 7 | +import ( |
| 8 | + _ "embed" |
| 9 | + "fmt" |
| 10 | + "go/ast" |
| 11 | + "go/types" |
| 12 | + "strings" |
| 13 | + |
| 14 | + "golang.org/x/tools/go/analysis" |
| 15 | + "golang.org/x/tools/go/analysis/passes/inspect" |
| 16 | + "golang.org/x/tools/go/ast/inspector" |
| 17 | + "golang.org/x/tools/gopls/internal/util/astutil" |
| 18 | + "golang.org/x/tools/internal/analysisinternal" |
| 19 | +) |
| 20 | + |
| 21 | +// Assumptions |
| 22 | +// |
| 23 | +// Like unusedparams, this analyzer depends on the invariant of the |
| 24 | +// gopls analysis driver that only the "widest" package (the one with |
| 25 | +// the most files) for a given file is analyzed. This invariant allows |
| 26 | +// the algorithm to make "closed world" assumptions about the target |
| 27 | +// package. (In general, analysis of Go test packages cannot make that |
| 28 | +// assumption because in-package tests add new files to existing |
| 29 | +// packages, potentially invalidating results.) Consequently, running |
| 30 | +// this analyzer in, say, unitchecker or multichecker may produce |
| 31 | +// incorrect results. |
| 32 | +// |
| 33 | +// A function is unreferenced if it is never referenced except within |
| 34 | +// its own declaration, and it is unexported. (Exported functions must |
| 35 | +// be assumed to be referenced from other packages.) |
| 36 | +// |
| 37 | +// For methods, we assume that the receiver type is "live" (variables |
| 38 | +// of that type are created) and "address taken" (its rtype ends up in |
| 39 | +// an at least one interface value). This means exported methods may |
| 40 | +// be called via reflection or by interfaces defined in other |
| 41 | +// packages, so again we are concerned only with unexported methods. |
| 42 | +// |
| 43 | +// To discount the possibility of a method being called via an |
| 44 | +// interface, we must additionally ensure that no literal interface |
| 45 | +// type within the package has a method of the same name. |
| 46 | +// (Unexported methods cannot be called through interfaces declared |
| 47 | +// in other packages because each package has a private namespace |
| 48 | +// for unexported identifiers.) |
| 49 | + |
| 50 | +//go:embed doc.go |
| 51 | +var doc string |
| 52 | + |
| 53 | +var Analyzer = &analysis.Analyzer{ |
| 54 | + Name: "unusedfunc", |
| 55 | + Doc: analysisinternal.MustExtractDoc(doc, "unusedfunc"), |
| 56 | + Requires: []*analysis.Analyzer{inspect.Analyzer}, |
| 57 | + Run: run, |
| 58 | + URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/unusedfunc", |
| 59 | +} |
| 60 | + |
| 61 | +func run(pass *analysis.Pass) (any, error) { |
| 62 | + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) |
| 63 | + |
| 64 | + // Gather names of unexported interface methods declared in this package. |
| 65 | + localIfaceMethods := make(map[string]bool) |
| 66 | + nodeFilter := []ast.Node{(*ast.InterfaceType)(nil)} |
| 67 | + inspect.Preorder(nodeFilter, func(n ast.Node) { |
| 68 | + iface := n.(*ast.InterfaceType) |
| 69 | + for _, field := range iface.Methods.List { |
| 70 | + if len(field.Names) > 0 { |
| 71 | + id := field.Names[0] |
| 72 | + if !id.IsExported() { |
| 73 | + // TODO(adonovan): check not just name but signature too. |
| 74 | + localIfaceMethods[id.Name] = true |
| 75 | + } |
| 76 | + } |
| 77 | + } |
| 78 | + }) |
| 79 | + |
| 80 | + // Map each function/method symbol to its declaration. |
| 81 | + decls := make(map[*types.Func]*ast.FuncDecl) |
| 82 | + for _, file := range pass.Files { |
| 83 | + if ast.IsGenerated(file) { |
| 84 | + continue // skip generated files |
| 85 | + } |
| 86 | + |
| 87 | + for _, decl := range file.Decls { |
| 88 | + if decl, ok := decl.(*ast.FuncDecl); ok { |
| 89 | + id := decl.Name |
| 90 | + // Exported functions may be called from other packages. |
| 91 | + if id.IsExported() { |
| 92 | + continue |
| 93 | + } |
| 94 | + |
| 95 | + // Blank functions are exempt from diagnostics. |
| 96 | + if id.Name == "_" { |
| 97 | + continue |
| 98 | + } |
| 99 | + |
| 100 | + // An (unexported) method whose name matches an |
| 101 | + // interface method declared in the same package |
| 102 | + // may be dynamically called via that interface. |
| 103 | + if decl.Recv != nil && localIfaceMethods[id.Name] { |
| 104 | + continue |
| 105 | + } |
| 106 | + |
| 107 | + // main and init functions are implicitly always used |
| 108 | + if decl.Recv == nil && (id.Name == "init" || id.Name == "main") { |
| 109 | + continue |
| 110 | + } |
| 111 | + |
| 112 | + fn := pass.TypesInfo.Defs[id].(*types.Func) |
| 113 | + decls[fn] = decl |
| 114 | + } |
| 115 | + } |
| 116 | + } |
| 117 | + |
| 118 | + // Scan for uses of each function symbol. |
| 119 | + // (Ignore uses within the function's body.) |
| 120 | + use := func(ref ast.Node, obj types.Object) { |
| 121 | + if fn, ok := obj.(*types.Func); ok { |
| 122 | + if fn := fn.Origin(); fn.Pkg() == pass.Pkg { |
| 123 | + if decl, ok := decls[fn]; ok { |
| 124 | + // Ignore uses within the function's body. |
| 125 | + if decl.Body != nil && astutil.NodeContains(decl.Body, ref.Pos()) { |
| 126 | + return |
| 127 | + } |
| 128 | + delete(decls, fn) // symbol is referenced |
| 129 | + } |
| 130 | + } |
| 131 | + } |
| 132 | + } |
| 133 | + for id, obj := range pass.TypesInfo.Uses { |
| 134 | + use(id, obj) |
| 135 | + } |
| 136 | + for sel, seln := range pass.TypesInfo.Selections { |
| 137 | + use(sel, seln.Obj()) |
| 138 | + } |
| 139 | + |
| 140 | + // Report the remaining unreferenced symbols. |
| 141 | +nextDecl: |
| 142 | + for fn, decl := range decls { |
| 143 | + noun := "function" |
| 144 | + if decl.Recv != nil { |
| 145 | + noun = "method" |
| 146 | + } |
| 147 | + |
| 148 | + pos := decl.Pos() // start of func decl or associated comment |
| 149 | + if decl.Doc != nil { |
| 150 | + pos = decl.Doc.Pos() |
| 151 | + |
| 152 | + // Skip if there's a preceding //go:linkname directive. |
| 153 | + // |
| 154 | + // (A program can link fine without such a directive, |
| 155 | + // but it is bad style; and the directive may |
| 156 | + // appear anywhere, not just on the preceding line, |
| 157 | + // but again that is poor form.) |
| 158 | + // |
| 159 | + // TODO(adonovan): use ast.ParseDirective when #68021 lands. |
| 160 | + for _, comment := range decl.Doc.List { |
| 161 | + if strings.HasPrefix(comment.Text, "//go:linkname ") { |
| 162 | + continue nextDecl |
| 163 | + } |
| 164 | + } |
| 165 | + } |
| 166 | + |
| 167 | + pass.Report(analysis.Diagnostic{ |
| 168 | + Pos: decl.Name.Pos(), |
| 169 | + End: decl.Name.End(), |
| 170 | + Message: fmt.Sprintf("%s %q is unused", noun, fn.Name()), |
| 171 | + SuggestedFixes: []analysis.SuggestedFix{{ |
| 172 | + Message: fmt.Sprintf("Delete %s %q", noun, fn.Name()), |
| 173 | + TextEdits: []analysis.TextEdit{{ |
| 174 | + // delete declaration |
| 175 | + Pos: pos, |
| 176 | + End: decl.End(), |
| 177 | + }}, |
| 178 | + }}, |
| 179 | + }) |
| 180 | + } |
| 181 | + |
| 182 | + return nil, nil |
| 183 | +} |
0 commit comments