Skip to content

Commit 98a190b

Browse files
adonovangopherbot
authored andcommitted
gopls/internal/analysis/unusedfunc: analyzer for unused funcs/methods
This CL defines a new gopls analyzer that reports unused functions and methods using a local heuristic suitable for the analysis framework, delivering some of the value of cmd/deadcode but with the value of near real-time feedback and gopls integration. Like unusedparams, it assumes that it is running within gopls' analysis driver, which always chooses the "widest" package for a given file. Without this assumption, the additional files for an in-package test may invalidate the analyzer's findings. Unfortunately a rather large number of marker tests define throwaway functions called f that not trigger a diagnostic. They have been updated to finesse the problem. + test, doc, relnote Change-Id: I85ef593eee7a6940779ee27a2455d9090a3e8c7c Reviewed-on: https://go-review.googlesource.com/c/tools/+/639716 Reviewed-by: Robert Findley <[email protected]> Auto-Submit: Alan Donovan <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 192ac77 commit 98a190b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+501
-99
lines changed

gopls/doc/analyzers.md

+31
Original file line numberDiff line numberDiff line change
@@ -903,6 +903,37 @@ Default: on.
903903

904904
Package documentation: [unsafeptr](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unsafeptr)
905905

906+
<a id='unusedfunc'></a>
907+
## `unusedfunc`: check for unused functions and methods
908+
909+
910+
The unusedfunc analyzer reports functions and methods that are
911+
never referenced outside of their own declaration.
912+
913+
A function is considered unused if it is unexported and not
914+
referenced (except within its own declaration).
915+
916+
A method is considered unused if it is unexported, not referenced
917+
(except within its own declaration), and its name does not match
918+
that of any method of an interface type declared within the same
919+
package.
920+
921+
The tool may report a false positive for a declaration of an
922+
unexported function that is referenced from another package using
923+
the go:linkname mechanism, if the declaration's doc comment does
924+
not also have a go:linkname comment. (Such code is in any case
925+
strongly discouraged: linkname annotations, if they must be used at
926+
all, should be used on both the declaration and the alias.)
927+
928+
The unusedfunc algorithm is not as precise as the
929+
golang.org/x/tools/cmd/deadcode tool, but it has the advantage that
930+
it runs within the modular analysis framework, enabling near
931+
real-time feedback within gopls.
932+
933+
Default: on.
934+
935+
Package documentation: [unusedfunc](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/unusedfunc)
936+
906937
<a id='unusedparams'></a>
907938
## `unusedparams`: check for unused parameters of functions
908939

gopls/doc/release/v0.18.0.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ checks.
2222

2323
## New `modernize` analyzer
2424

25-
Gopls will now report when code could be simplified or clarified by
25+
Gopls now reports when code could be simplified or clarified by
2626
using more modern features of Go, and provides a quick fix to apply
2727
the change.
2828

@@ -31,6 +31,14 @@ Examples:
3131
- replacement of conditional assignment using an if/else statement by
3232
a call to the `min` or `max` built-in functions added in Go 1.18;
3333

34+
## New `unusedfunc` analyzer
35+
36+
Gopls now reports unused functions and methods, giving you near
37+
real-time feedback about dead code that may be safely deleted.
38+
Because the analysis is local to each package, only unexported
39+
functions and methods are candidates.
40+
(For a more precise analysis that may report unused exported
41+
functions too, use the `golang.org/x/tools/cmd/deadcode` command.)
3442

3543
## "Implementations" supports generics
3644

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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 defines an analyzer that checks for unused
6+
// functions and methods
7+
//
8+
// # Analyzer unusedfunc
9+
//
10+
// unusedfunc: check for unused functions and methods
11+
//
12+
// The unusedfunc analyzer reports functions and methods that are
13+
// never referenced outside of their own declaration.
14+
//
15+
// A function is considered unused if it is unexported and not
16+
// referenced (except within its own declaration).
17+
//
18+
// A method is considered unused if it is unexported, not referenced
19+
// (except within its own declaration), and its name does not match
20+
// that of any method of an interface type declared within the same
21+
// package.
22+
//
23+
// The tool may report a false positive for a declaration of an
24+
// unexported function that is referenced from another package using
25+
// the go:linkname mechanism, if the declaration's doc comment does
26+
// not also have a go:linkname comment. (Such code is in any case
27+
// strongly discouraged: linkname annotations, if they must be used at
28+
// all, should be used on both the declaration and the alias.)
29+
//
30+
// The unusedfunc algorithm is not as precise as the
31+
// golang.org/x/tools/cmd/deadcode tool, but it has the advantage that
32+
// it runs within the modular analysis framework, enabling near
33+
// real-time feedback within gopls.
34+
package unusedfunc
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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+
//go:build ignore
6+
7+
// The unusedfunc command runs the unusedfunc analyzer.
8+
package main
9+
10+
import (
11+
"golang.org/x/tools/go/analysis/singlechecker"
12+
"golang.org/x/tools/gopls/internal/analysis/unusedfunc"
13+
)
14+
15+
func main() { singlechecker.Main(unusedfunc.Analyzer) }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package a
2+
3+
func main() {
4+
_ = live
5+
}
6+
7+
// -- functions --
8+
9+
func Exported() {}
10+
11+
func dead() { // want `function "dead" is unused`
12+
}
13+
14+
func deadRecursive() int { // want `function "deadRecursive" is unused`
15+
return deadRecursive()
16+
}
17+
18+
func live() {}
19+
20+
//go:linkname foo
21+
func apparentlyDeadButHasPrecedingLinknameComment() {}
22+
23+
// -- methods --
24+
25+
type ExportedType int
26+
type unexportedType int
27+
28+
func (ExportedType) Exported() {}
29+
func (unexportedType) Exported() {}
30+
31+
func (x ExportedType) dead() { // want `method "dead" is unused`
32+
x.dead()
33+
}
34+
35+
func (u unexportedType) dead() { // want `method "dead" is unused`
36+
u.dead()
37+
}
38+
39+
func (x ExportedType) dynamic() {} // matches name of interface method => live
40+
41+
type _ interface{ dynamic() }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package a
2+
3+
func main() {
4+
_ = live
5+
}
6+
7+
// -- functions --
8+
9+
func Exported() {}
10+
11+
func live() {}
12+
13+
//go:linkname foo
14+
func apparentlyDeadButHasPrecedingLinknameComment() {}
15+
16+
// -- methods --
17+
18+
type ExportedType int
19+
type unexportedType int
20+
21+
func (ExportedType) Exported() {}
22+
func (unexportedType) Exported() {}
23+
24+
func (x ExportedType) dynamic() {} // matches name of interface method => live
25+
26+
type _ interface{ dynamic() }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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_test
6+
7+
import (
8+
"testing"
9+
10+
"golang.org/x/tools/go/analysis/analysistest"
11+
"golang.org/x/tools/gopls/internal/analysis/unusedfunc"
12+
)
13+
14+
func Test(t *testing.T) {
15+
testdata := analysistest.TestData()
16+
analysistest.RunWithSuggestedFixes(t, testdata, unusedfunc.Analyzer, "a")
17+
}

0 commit comments

Comments
 (0)