Skip to content

Commit 16f2979

Browse files
committed
gopls/internal/analysis: unusedvariable
The unused variable quickfix removes more than it should when there are comments following the unused variable statement. This CL accounts for comments when determing the end position of the text edit in the quickfix. Fixes golang/go#54373 Change-Id: I9c920983c0c70b26b23dbb457cffeaa39a8ba721 Reviewed-on: https://go-review.googlesource.com/c/tools/+/640579 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Robert Findley <[email protected]> Reviewed-by: Alan Donovan <[email protected]>
1 parent fc2161a commit 16f2979

File tree

7 files changed

+59
-10
lines changed

7 files changed

+59
-10
lines changed

gopls/doc/analyzers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1011,7 +1011,7 @@ Package documentation: [unusedresult](https://pkg.go.dev/golang.org/x/tools/go/a
10111011

10121012

10131013

1014-
Default: off. Enable by setting `"analyses": {"unusedvariable": true}`.
1014+
Default: on.
10151015

10161016
Package documentation: [unusedvariable](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/unusedvariable)
10171017

gopls/doc/release/v0.18.0.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ this fashion (or with `%s` for the port) is passed to `net.Dial` or a
4949
related function, and offers a fix to use `net.JoinHostPort`
5050
instead.
5151

52+
## `unusedvariable` analyzer now on by default
53+
54+
This analyzer suggests deleting the unused variable declaration.
55+
5256
## "Implementations" supports generics
5357

5458
At long last, the "Go to Implementations" feature now fully supports

gopls/internal/analysis/unusedvariable/testdata/src/assign/a.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,17 @@ func commentAbove() {
6565
v := "s" // want `declared (and|but) not used`
6666
}
6767

68+
func commentBelow() {
69+
v := "s" // want `declared (and|but) not used`
70+
// v is a variable
71+
}
72+
73+
func commentSpaceBelow() {
74+
v := "s" // want `declared (and|but) not used`
75+
76+
// v is a variable
77+
}
78+
6879
func fBool() bool {
6980
return true
7081
}

gopls/internal/analysis/unusedvariable/testdata/src/assign/a.go.golden

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ func commentAbove() {
5050
// v is a variable
5151
}
5252

53+
func commentBelow() {
54+
// v is a variable
55+
}
56+
57+
func commentSpaceBelow() {
58+
// v is a variable
59+
}
60+
5361
func fBool() bool {
5462
return true
5563
}

gopls/internal/analysis/unusedvariable/unusedvariable.go

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717

1818
"golang.org/x/tools/go/analysis"
1919
"golang.org/x/tools/go/ast/astutil"
20+
"golang.org/x/tools/gopls/internal/util/safetoken"
2021
)
2122

2223
const Doc = `check for unused variables and suggest fixes`
@@ -37,7 +38,7 @@ var unusedVariableRegexp = []*regexp.Regexp{
3738
regexp.MustCompile("^declared and not used: (.*)$"), // Go 1.23+
3839
}
3940

40-
func run(pass *analysis.Pass) (interface{}, error) {
41+
func run(pass *analysis.Pass) (any, error) {
4142
for _, typeErr := range pass.TypeErrors {
4243
for _, re := range unusedVariableRegexp {
4344
match := re.FindStringSubmatch(typeErr.Msg)
@@ -113,7 +114,7 @@ func runForError(pass *analysis.Pass, err types.Error, name string) error {
113114
continue
114115
}
115116

116-
fixes := removeVariableFromAssignment(path, stmt, ident)
117+
fixes := removeVariableFromAssignment(pass.Fset, path, stmt, ident)
117118
// fixes may be nil
118119
if len(fixes) > 0 {
119120
diag.SuggestedFixes = fixes
@@ -164,7 +165,7 @@ func removeVariableFromSpec(pass *analysis.Pass, path []ast.Node, stmt *ast.Valu
164165
// Find parent DeclStmt and delete it
165166
for _, node := range path {
166167
if declStmt, ok := node.(*ast.DeclStmt); ok {
167-
edits := deleteStmtFromBlock(path, declStmt)
168+
edits := deleteStmtFromBlock(pass.Fset, path, declStmt)
168169
if len(edits) == 0 {
169170
return nil // can this happen?
170171
}
@@ -198,7 +199,7 @@ func removeVariableFromSpec(pass *analysis.Pass, path []ast.Node, stmt *ast.Valu
198199
}
199200
}
200201

201-
func removeVariableFromAssignment(path []ast.Node, stmt *ast.AssignStmt, ident *ast.Ident) []analysis.SuggestedFix {
202+
func removeVariableFromAssignment(fset *token.FileSet, path []ast.Node, stmt *ast.AssignStmt, ident *ast.Ident) []analysis.SuggestedFix {
202203
// The only variable in the assignment is unused
203204
if len(stmt.Lhs) == 1 {
204205
// If LHS has only one expression to be valid it has to have 1 expression
@@ -221,7 +222,7 @@ func removeVariableFromAssignment(path []ast.Node, stmt *ast.AssignStmt, ident *
221222
}
222223

223224
// RHS does not have any side effects, delete the whole statement
224-
edits := deleteStmtFromBlock(path, stmt)
225+
edits := deleteStmtFromBlock(fset, path, stmt)
225226
if len(edits) == 0 {
226227
return nil // can this happen?
227228
}
@@ -252,7 +253,7 @@ func suggestedFixMessage(name string) string {
252253
return fmt.Sprintf("Remove variable %s", name)
253254
}
254255

255-
func deleteStmtFromBlock(path []ast.Node, stmt ast.Stmt) []analysis.TextEdit {
256+
func deleteStmtFromBlock(fset *token.FileSet, path []ast.Node, stmt ast.Stmt) []analysis.TextEdit {
256257
// Find innermost enclosing BlockStmt.
257258
var block *ast.BlockStmt
258259
for i := range path {
@@ -282,6 +283,31 @@ func deleteStmtFromBlock(path []ast.Node, stmt ast.Stmt) []analysis.TextEdit {
282283
end = block.List[nodeIndex+1].Pos()
283284
}
284285

286+
// Account for comments within the block containing the statement
287+
// TODO(adonovan): when golang/go#20744 is addressed, query the AST
288+
// directly for comments between stmt.End() and end. For now we
289+
// must scan the entire file's comments (though we could binary search).
290+
astFile := path[len(path)-1].(*ast.File)
291+
currFile := fset.File(end)
292+
stmtEndLine := safetoken.Line(currFile, stmt.End())
293+
outer:
294+
for _, cg := range astFile.Comments {
295+
for _, co := range cg.List {
296+
if stmt.End() <= co.Pos() && co.Pos() <= end {
297+
coLine := safetoken.Line(currFile, co.Pos())
298+
// If a comment exists within the current block, after the unused variable statement,
299+
// and before the next statement, we shouldn't delete it.
300+
if coLine > stmtEndLine {
301+
end = co.Pos()
302+
break outer
303+
}
304+
if co.Pos() > end {
305+
break outer
306+
}
307+
}
308+
}
309+
}
310+
285311
return []analysis.TextEdit{
286312
{
287313
Pos: stmt.Pos(),

gopls/internal/doc/api.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,7 @@
608608
{
609609
"Name": "\"unusedvariable\"",
610610
"Doc": "check for unused variables and suggest fixes",
611-
"Default": "false"
611+
"Default": "true"
612612
},
613613
{
614614
"Name": "\"unusedwrite\"",
@@ -1267,7 +1267,7 @@
12671267
"Name": "unusedvariable",
12681268
"Doc": "check for unused variables and suggest fixes",
12691269
"URL": "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/unusedvariable",
1270-
"Default": false
1270+
"Default": true
12711271
},
12721272
{
12731273
"Name": "unusedwrite",

gopls/internal/settings/analysis.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ func init() {
197197
{analyzer: fillreturns.Analyzer},
198198
{analyzer: nonewvars.Analyzer},
199199
{analyzer: noresultvalues.Analyzer},
200-
{analyzer: unusedvariable.Analyzer, nonDefault: true}, // not fully baked; see #54373
200+
{analyzer: unusedvariable.Analyzer},
201201
}
202202
for _, analyzer := range analyzers {
203203
DefaultAnalyzers[analyzer.analyzer.Name] = analyzer

0 commit comments

Comments
 (0)