Skip to content

Commit 6fcd778

Browse files
findleyrgopherbot
authored andcommitted
gopls/internal/lsp: add code actions to remove unused parameters
Use the new inlining technology to implement a first change-signature refactoring, by rewriting the declaration to be a trivial wrapper around a declaration with modified signature, inlining the wrapper, and then performing string substitution to replace references to the synthetic delegate. This demonstrates the power of inlining, which can be seen as a more general tool for rewriting source code: express old code as new code, recognize instances of old code (in this case, calls), and inline. However, this CL was still rather difficult, primarily because of (1) the problem of manipulating syntax without breaking formatting and comments, and (2) the problem of composing multiple refactoring operations, which in general requires iterative type checking. Neither of those difficulties have general solutions: any form of nontrivial AST manipulation tends to result in an unacceptable movement of comments, and iterative type checking is difficult because it may involve an arbitrarily modified package graph, and because it is difficult to correlate references in the previous version of the package with references in the new version of the package. But in the case of removing a parameter, these problems are solvable: We can avoid most AST mangling by restricting the scope of rewriting to the function signature. We can type check the new package using the imports of the old package. We can find the next reference in the new package by counting. Fixes golang/go#63534 Change-Id: Iba5fa6b0da503b7723bea1b43fd2c4151576a672 Reviewed-on: https://go-review.googlesource.com/c/tools/+/532495 Reviewed-by: Alan Donovan <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Auto-Submit: Robert Findley <[email protected]>
1 parent 918e96a commit 6fcd778

File tree

21 files changed

+1655
-86
lines changed

21 files changed

+1655
-86
lines changed

gopls/doc/commands.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,26 @@ Args:
8484
}
8585
```
8686

87+
### **performs a "change signature" refactoring.**
88+
Identifier: `gopls.change_signature`
89+
90+
This command is experimental, currently only supporting parameter removal.
91+
Its signature will certainly change in the future (pun intended).
92+
93+
Args:
94+
95+
```
96+
{
97+
"RemoveParameter": {
98+
"uri": string,
99+
"range": {
100+
"start": { ... },
101+
"end": { ... },
102+
},
103+
},
104+
}
105+
```
106+
87107
### **Check for upgrades**
88108
Identifier: `gopls.check_upgrades`
89109

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright 2023 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+
// The stringintconv command runs the stringintconv analyzer.
6+
package main
7+
8+
import (
9+
"golang.org/x/tools/go/analysis/singlechecker"
10+
"golang.org/x/tools/gopls/internal/lsp/analysis/unusedparams"
11+
)
12+
13+
func main() { singlechecker.Main(unusedparams.Analyzer) }

gopls/internal/lsp/analysis/unusedparams/unusedparams.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,20 @@ To reduce false positives it ignores:
2828
- functions in test files
2929
- functions with empty bodies or those with just a return stmt`
3030

31-
var Analyzer = &analysis.Analyzer{
32-
Name: "unusedparams",
33-
Doc: Doc,
34-
Requires: []*analysis.Analyzer{inspect.Analyzer},
35-
Run: run,
31+
var (
32+
Analyzer = &analysis.Analyzer{
33+
Name: "unusedparams",
34+
Doc: Doc,
35+
Requires: []*analysis.Analyzer{inspect.Analyzer},
36+
Run: run,
37+
}
38+
inspectLits bool
39+
inspectWrappers bool
40+
)
41+
42+
func init() {
43+
Analyzer.Flags.BoolVar(&inspectLits, "lits", true, "inspect function literals")
44+
Analyzer.Flags.BoolVar(&inspectWrappers, "wrappers", false, "inspect functions whose body consists of a single return statement")
3645
}
3746

3847
type paramData struct {
@@ -45,7 +54,9 @@ func run(pass *analysis.Pass) (interface{}, error) {
4554
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
4655
nodeFilter := []ast.Node{
4756
(*ast.FuncDecl)(nil),
48-
(*ast.FuncLit)(nil),
57+
}
58+
if inspectLits {
59+
nodeFilter = append(nodeFilter, (*ast.FuncLit)(nil))
4960
}
5061

5162
inspect.Preorder(nodeFilter, func(n ast.Node) {
@@ -62,6 +73,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
6273
if f.Recv != nil {
6374
return
6475
}
76+
6577
// Ignore functions in _test.go files to reduce false positives.
6678
if file := pass.Fset.File(n.Pos()); file != nil && strings.HasSuffix(file.Name(), "_test.go") {
6779
return
@@ -76,8 +88,10 @@ func run(pass *analysis.Pass) (interface{}, error) {
7688

7789
switch expr := body.List[0].(type) {
7890
case *ast.ReturnStmt:
79-
// Ignore functions that only contain a return statement to reduce false positives.
80-
return
91+
if !inspectWrappers {
92+
// Ignore functions that only contain a return statement to reduce false positives.
93+
return
94+
}
8195
case *ast.ExprStmt:
8296
callExpr, ok := expr.X.(*ast.CallExpr)
8397
if !ok || len(body.List) > 1 {

gopls/internal/lsp/cache/check.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1487,6 +1487,7 @@ func typeCheckImpl(ctx context.Context, b *typeCheckBatch, inputs typeCheckInput
14871487
return pkg, nil
14881488
}
14891489

1490+
// TODO(golang/go#63472): this looks wrong with the new Go version syntax.
14901491
var goVersionRx = regexp.MustCompile(`^go([1-9][0-9]*)\.(0|[1-9][0-9]*)$`)
14911492

14921493
func doTypeCheck(ctx context.Context, b *typeCheckBatch, inputs typeCheckInputs) (*syntaxPackage, error) {

gopls/internal/lsp/code_action.go

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,26 @@ func refactorRewrite(ctx context.Context, snapshot source.Snapshot, pkg source.P
430430
rerr = bug.Errorf("refactor.rewrite code actions panicked: %v", r)
431431
}
432432
}()
433+
434+
var actions []protocol.CodeAction
435+
436+
if canRemoveParameter(pkg, pgf, rng) {
437+
cmd, err := command.NewChangeSignatureCommand("remove unused parameter", command.ChangeSignatureArgs{
438+
RemoveParameter: protocol.Location{
439+
URI: protocol.URIFromSpanURI(pgf.URI),
440+
Range: rng,
441+
},
442+
})
443+
if err != nil {
444+
return nil, err
445+
}
446+
actions = append(actions, protocol.CodeAction{
447+
Title: "Refactor: remove unused parameter",
448+
Kind: protocol.RefactorRewrite,
449+
Command: &cmd,
450+
})
451+
}
452+
433453
start, end, err := pgf.RangePos(rng)
434454
if err != nil {
435455
return nil, err
@@ -471,7 +491,6 @@ func refactorRewrite(ctx context.Context, snapshot source.Snapshot, pkg source.P
471491
}
472492
}
473493

474-
var actions []protocol.CodeAction
475494
for i := range commands {
476495
actions = append(actions, protocol.CodeAction{
477496
Title: commands[i].Title,
@@ -510,6 +529,43 @@ func refactorRewrite(ctx context.Context, snapshot source.Snapshot, pkg source.P
510529
return actions, nil
511530
}
512531

532+
// canRemoveParameter reports whether we can remove the function parameter
533+
// indicated by the given [start, end) range.
534+
//
535+
// This is true if:
536+
// - [start, end) is contained within an unused field or parameter name
537+
// - ... of a non-method function declaration.
538+
func canRemoveParameter(pkg source.Package, pgf *source.ParsedGoFile, rng protocol.Range) bool {
539+
info := source.FindParam(pgf, rng)
540+
if info.Decl == nil || info.Field == nil {
541+
return false
542+
}
543+
544+
if len(info.Field.Names) == 0 {
545+
return true // no names => field is unused
546+
}
547+
if info.Name == nil {
548+
return false // no name is indicated
549+
}
550+
if info.Name.Name == "_" {
551+
return true // trivially unused
552+
}
553+
554+
obj := pkg.GetTypesInfo().Defs[info.Name]
555+
if obj == nil {
556+
return false // something went wrong
557+
}
558+
559+
used := false
560+
ast.Inspect(info.Decl.Body, func(node ast.Node) bool {
561+
if n, ok := node.(*ast.Ident); ok && pkg.GetTypesInfo().Uses[n] == obj {
562+
used = true
563+
}
564+
return !used // keep going until we find a use
565+
})
566+
return !used
567+
}
568+
513569
// refactorInline returns inline actions available at the specified range.
514570
func refactorInline(ctx context.Context, snapshot source.Snapshot, pkg source.Package, pgf *source.ParsedGoFile, fh source.FileHandle, rng protocol.Range) ([]protocol.CodeAction, error) {
515571
var commands []protocol.Command

gopls/internal/lsp/command.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1210,3 +1210,25 @@ func showDocumentImpl(ctx context.Context, cli protocol.Client, url protocol.URI
12101210
event.Log(ctx, fmt.Sprintf("client declined to open document %v", url))
12111211
}
12121212
}
1213+
1214+
func (c *commandHandler) ChangeSignature(ctx context.Context, args command.ChangeSignatureArgs) error {
1215+
return c.run(ctx, commandConfig{
1216+
forURI: args.RemoveParameter.URI,
1217+
}, func(ctx context.Context, deps commandDeps) error {
1218+
// For now, gopls only supports removing unused parameters.
1219+
changes, err := source.RemoveUnusedParameter(ctx, deps.fh, args.RemoveParameter.Range, deps.snapshot)
1220+
if err != nil {
1221+
return err
1222+
}
1223+
r, err := c.s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{
1224+
Edit: protocol.WorkspaceEdit{
1225+
DocumentChanges: changes,
1226+
},
1227+
})
1228+
if !r.Applied {
1229+
return fmt.Errorf("failed to apply edits: %v", r.FailureReason)
1230+
}
1231+
1232+
return nil
1233+
})
1234+
}

gopls/internal/lsp/command/command_gen.go

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gopls/internal/lsp/command/interface.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,12 @@ type Interface interface {
201201
// the user to ask if they want to enable Go telemetry uploading. If the user
202202
// responds 'Yes', the telemetry mode is set to "on".
203203
MaybePromptForTelemetry(context.Context) error
204+
205+
// ChangeSignature: performs a "change signature" refactoring.
206+
//
207+
// This command is experimental, currently only supporting parameter removal.
208+
// Its signature will certainly change in the future (pun intended).
209+
ChangeSignature(context.Context, ChangeSignatureArgs) error
204210
}
205211

206212
type RunTestsArgs struct {
@@ -519,3 +525,8 @@ type AddTelemetryCountersArgs struct {
519525
Names []string // Name of counters.
520526
Values []int64 // Values added to the corresponding counters. Must be non-negative.
521527
}
528+
529+
// ChangeSignatureArgs specifies a "change signature" refactoring to perform.
530+
type ChangeSignatureArgs struct {
531+
RemoveParameter protocol.Location
532+
}

gopls/internal/lsp/regtest/marker.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1001,6 +1001,9 @@ func formatTest(test *markerTest) ([]byte, error) {
10011001
// ...followed by any new golden files.
10021002
var newGoldenFiles []txtar.File
10031003
for filename, data := range updatedGolden {
1004+
// TODO(rfindley): it looks like this implicitly removes trailing newlines
1005+
// from golden content. Is there any way to fix that? Perhaps we should
1006+
// just make the diff tolerant of missing newlines?
10041007
newGoldenFiles = append(newGoldenFiles, txtar.File{Name: filename, Data: data})
10051008
}
10061009
// Sort new golden files lexically.
@@ -2047,7 +2050,7 @@ func codeAction(env *Env, uri protocol.DocumentURI, rng protocol.Range, actionKi
20472050
Command: action.Command.Command,
20482051
Arguments: action.Command.Arguments,
20492052
}); err != nil {
2050-
env.T.Fatalf("error converting command %q to edits: %v", action.Command.Command, err)
2053+
return nil, err
20512054
}
20522055

20532056
if err := applyDocumentChanges(env, env.Awaiter.takeDocumentChanges(), fileChanges); err != nil {

gopls/internal/lsp/source/api_json.go

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)