Skip to content

Commit dcfb0b6

Browse files
committed
gopls/internal/golang: change signature via renaming 'func'
More for testing and debugging than anything else, allow changing signatures by invoking a renaming on the 'func' keyword. For now, this is still restricted to parameter permutation or removal. For golang/go#38028 Change-Id: I62e387cc7d7f46fc892b7b20050d99fac6800e3f Reviewed-on: https://go-review.googlesource.com/c/tools/+/631296 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Alan Donovan <[email protected]>
1 parent bfcbc1b commit dcfb0b6

File tree

8 files changed

+319
-21
lines changed

8 files changed

+319
-21
lines changed

gopls/internal/golang/change_signature.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,6 @@ func removeParam(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle,
146146
// - Stream type checking via ForEachPackage.
147147
// - Avoid unnecessary additional type checking.
148148
func ChangeSignature(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, rng protocol.Range, newParams []int) ([]protocol.DocumentChange, error) {
149-
150149
// Changes to our heuristics for whether we can remove a parameter must also
151150
// be reflected in the canRemoveParameter helper.
152151
if perrors, terrors := pkg.ParseErrors(), pkg.TypeErrors(); len(perrors) > 0 || len(terrors) > 0 {
@@ -160,8 +159,8 @@ func ChangeSignature(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.P
160159
}
161160

162161
info := findParam(pgf, rng)
163-
if info == nil || info.field == nil {
164-
return nil, fmt.Errorf("failed to find field")
162+
if info == nil || info.decl == nil {
163+
return nil, fmt.Errorf("failed to find declaration")
165164
}
166165

167166
// Step 1: create the new declaration, which is a copy of the original decl
@@ -437,9 +436,12 @@ func findParam(pgf *parsego.File, rng protocol.Range) *paramInfo {
437436
info.decl = n
438437
}
439438
}
440-
if info.decl == nil || field == nil {
439+
if info.decl == nil {
441440
return nil
442441
}
442+
if field == nil {
443+
return &info
444+
}
443445
pi := 0
444446
// Search for field and id among parameters of decl.
445447
// This search may fail, even if one or both of id and field are non nil:

gopls/internal/golang/rename.go

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,13 @@ package golang
4242
// - FileID-based de-duplication of edits to different URIs for the same file.
4343

4444
import (
45+
"bytes"
4546
"context"
4647
"errors"
4748
"fmt"
4849
"go/ast"
50+
"go/parser"
51+
"go/printer"
4952
"go/token"
5053
"go/types"
5154
"path"
@@ -64,8 +67,10 @@ import (
6467
"golang.org/x/tools/gopls/internal/cache/parsego"
6568
"golang.org/x/tools/gopls/internal/file"
6669
"golang.org/x/tools/gopls/internal/protocol"
70+
goplsastutil "golang.org/x/tools/gopls/internal/util/astutil"
6771
"golang.org/x/tools/gopls/internal/util/bug"
6872
"golang.org/x/tools/gopls/internal/util/safetoken"
73+
internalastutil "golang.org/x/tools/internal/astutil"
6974
"golang.org/x/tools/internal/diff"
7075
"golang.org/x/tools/internal/event"
7176
"golang.org/x/tools/internal/typesinternal"
@@ -126,6 +131,15 @@ func PrepareRename(ctx context.Context, snapshot *cache.Snapshot, f file.Handle,
126131
if err != nil {
127132
return nil, nil, err
128133
}
134+
135+
// Check if we're in a 'func' keyword. If so, we hijack the renaming to
136+
// change the function signature.
137+
if item, err := prepareRenameFuncSignature(pgf, pos); err != nil {
138+
return nil, nil, err
139+
} else if item != nil {
140+
return item, nil, nil
141+
}
142+
129143
targets, node, err := objectsAt(pkg.TypesInfo(), pgf.File, pos)
130144
if err != nil {
131145
return nil, nil, err
@@ -193,6 +207,169 @@ func prepareRenamePackageName(ctx context.Context, snapshot *cache.Snapshot, pgf
193207
}, nil
194208
}
195209

210+
// prepareRenameFuncSignature prepares a change signature refactoring initiated
211+
// through invoking a rename request at the 'func' keyword of a function
212+
// declaration.
213+
//
214+
// The resulting text is the signature of the function, which may be edited to
215+
// the new signature.
216+
func prepareRenameFuncSignature(pgf *parsego.File, pos token.Pos) (*PrepareItem, error) {
217+
fdecl := funcKeywordDecl(pgf, pos)
218+
if fdecl == nil {
219+
return nil, nil
220+
}
221+
ftyp := nameBlankParams(fdecl.Type)
222+
var buf bytes.Buffer
223+
if err := printer.Fprint(&buf, token.NewFileSet(), ftyp); err != nil { // use a new fileset so that the signature is formatted on a single line
224+
return nil, err
225+
}
226+
rng, err := pgf.PosRange(ftyp.Func, ftyp.Func+token.Pos(len("func")))
227+
if err != nil {
228+
return nil, err
229+
}
230+
text := buf.String()
231+
return &PrepareItem{
232+
Range: rng,
233+
Text: text,
234+
}, nil
235+
}
236+
237+
// nameBlankParams returns a copy of ftype with blank or unnamed params
238+
// assigned a unique name.
239+
func nameBlankParams(ftype *ast.FuncType) *ast.FuncType {
240+
ftype = internalastutil.CloneNode(ftype)
241+
242+
// First, collect existing names.
243+
scope := make(map[string]bool)
244+
for name := range goplsastutil.FlatFields(ftype.Params) {
245+
if name != nil {
246+
scope[name.Name] = true
247+
}
248+
}
249+
blanks := 0
250+
for name, field := range goplsastutil.FlatFields(ftype.Params) {
251+
if name == nil {
252+
name = ast.NewIdent("_")
253+
field.Names = append(field.Names, name) // ok to append
254+
}
255+
if name.Name == "" || name.Name == "_" {
256+
for {
257+
newName := fmt.Sprintf("_%d", blanks)
258+
blanks++
259+
if !scope[newName] {
260+
name.Name = newName
261+
break
262+
}
263+
}
264+
}
265+
}
266+
return ftype
267+
}
268+
269+
// renameFuncSignature computes and applies the effective change signature
270+
// operation resulting from a 'renamed' (=rewritten) signature.
271+
func renameFuncSignature(ctx context.Context, snapshot *cache.Snapshot, f file.Handle, pp protocol.Position, newName string) (map[protocol.DocumentURI][]protocol.TextEdit, error) {
272+
// Find the renamed signature.
273+
pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, f.URI())
274+
if err != nil {
275+
return nil, err
276+
}
277+
pos, err := pgf.PositionPos(pp)
278+
if err != nil {
279+
return nil, err
280+
}
281+
fdecl := funcKeywordDecl(pgf, pos)
282+
if fdecl == nil {
283+
return nil, nil
284+
}
285+
ftyp := nameBlankParams(fdecl.Type)
286+
287+
// Parse the user's requested new signature.
288+
parsed, err := parser.ParseExpr(newName)
289+
if err != nil {
290+
return nil, err
291+
}
292+
newType, _ := parsed.(*ast.FuncType)
293+
if newType == nil {
294+
return nil, fmt.Errorf("parsed signature is %T, not a function type", parsed)
295+
}
296+
297+
// Check results, before we get into handling permutations of parameters.
298+
if got, want := newType.Results.NumFields(), ftyp.Results.NumFields(); got != want {
299+
return nil, fmt.Errorf("changing results not yet supported (got %d results, want %d)", got, want)
300+
}
301+
var resultTypes []string
302+
for _, field := range goplsastutil.FlatFields(ftyp.Results) {
303+
resultTypes = append(resultTypes, FormatNode(token.NewFileSet(), field.Type))
304+
}
305+
resultIndex := 0
306+
for _, field := range goplsastutil.FlatFields(newType.Results) {
307+
if FormatNode(token.NewFileSet(), field.Type) != resultTypes[resultIndex] {
308+
return nil, fmt.Errorf("changing results not yet supported")
309+
}
310+
resultIndex++
311+
}
312+
313+
type paramInfo struct {
314+
idx int
315+
typ string
316+
}
317+
oldParams := make(map[string]paramInfo)
318+
for name, field := range goplsastutil.FlatFields(ftyp.Params) {
319+
oldParams[name.Name] = paramInfo{
320+
idx: len(oldParams),
321+
typ: types.ExprString(field.Type),
322+
}
323+
}
324+
325+
var newParams []int
326+
for name, field := range goplsastutil.FlatFields(newType.Params) {
327+
if name == nil {
328+
return nil, fmt.Errorf("need named fields")
329+
}
330+
info, ok := oldParams[name.Name]
331+
if !ok {
332+
return nil, fmt.Errorf("couldn't find name %s: adding parameters not yet supported", name)
333+
}
334+
if newType := types.ExprString(field.Type); newType != info.typ {
335+
return nil, fmt.Errorf("changing types (%s to %s) not yet supported", info.typ, newType)
336+
}
337+
newParams = append(newParams, info.idx)
338+
}
339+
340+
rng, err := pgf.PosRange(ftyp.Func, ftyp.Func)
341+
if err != nil {
342+
return nil, err
343+
}
344+
changes, err := ChangeSignature(ctx, snapshot, pkg, pgf, rng, newParams)
345+
if err != nil {
346+
return nil, err
347+
}
348+
transposed := make(map[protocol.DocumentURI][]protocol.TextEdit)
349+
for _, change := range changes {
350+
transposed[change.TextDocumentEdit.TextDocument.URI] = protocol.AsTextEdits(change.TextDocumentEdit.Edits)
351+
}
352+
return transposed, nil
353+
}
354+
355+
// funcKeywordDecl returns the FuncDecl for which pos is in the 'func' keyword,
356+
// if any.
357+
func funcKeywordDecl(pgf *parsego.File, pos token.Pos) *ast.FuncDecl {
358+
path, _ := astutil.PathEnclosingInterval(pgf.File, pos, pos)
359+
if len(path) < 1 {
360+
return nil
361+
}
362+
fdecl, _ := path[0].(*ast.FuncDecl)
363+
if fdecl == nil {
364+
return nil
365+
}
366+
ftyp := fdecl.Type
367+
if pos < ftyp.Func || pos > ftyp.Func+token.Pos(len("func")) { // tolerate renaming immediately after 'func'
368+
return nil
369+
}
370+
return fdecl
371+
}
372+
196373
func checkRenamable(obj types.Object) error {
197374
switch obj := obj.(type) {
198375
case *types.Var:
@@ -219,6 +396,12 @@ func Rename(ctx context.Context, snapshot *cache.Snapshot, f file.Handle, pp pro
219396
ctx, done := event.Start(ctx, "golang.Rename")
220397
defer done()
221398

399+
if edits, err := renameFuncSignature(ctx, snapshot, f, pp, newName); err != nil {
400+
return nil, false, err
401+
} else if edits != nil {
402+
return edits, false, nil
403+
}
404+
222405
if !isValidIdentifier(newName) {
223406
return nil, false, fmt.Errorf("invalid identifier to rename: %q", newName)
224407
}

gopls/internal/test/marker/doc.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -226,10 +226,11 @@ Here is the list of supported action markers:
226226
callHierarchy/outgoingCalls query at the src location, and checks that
227227
the set of call.To locations matches want.
228228
229-
- preparerename(src, spn, placeholder): asserts that a textDocument/prepareRename
230-
request at the src location expands to the spn location, with given
231-
placeholder. If placeholder is "", this is treated as a negative
232-
assertion and prepareRename should return nil.
229+
- preparerename(src location, placeholder string, span=location): asserts
230+
that a textDocument/prepareRename request at the src location has the given
231+
placeholder text. If present, the optional span argument is verified to be
232+
the span of the prepareRename result. If placeholder is "", this is treated
233+
as a negative assertion and prepareRename should return nil.
233234
234235
- quickfix(location, regexp, golden): like diag, the location and
235236
regexp identify an expected diagnostic, which must have exactly one

gopls/internal/test/marker/marker_test.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,12 @@ func namedArg[T any](mark marker, name string, dflt T) T {
513513
if e, ok := v.(T); ok {
514514
return e
515515
} else {
516-
mark.errorf("invalid value for %q: %v", name, v)
516+
v, err := convert(mark, v, reflect.TypeOf(dflt))
517+
if err != nil {
518+
mark.errorf("invalid value for %q: could not convert %v (%T) to %T", name, v, v, dflt)
519+
return dflt
520+
}
521+
return v.(T)
517522
}
518523
}
519524
return dflt
@@ -579,7 +584,7 @@ var actionMarkerFuncs = map[string]func(marker){
579584
"incomingcalls": actionMarkerFunc(incomingCallsMarker),
580585
"inlayhints": actionMarkerFunc(inlayhintsMarker),
581586
"outgoingcalls": actionMarkerFunc(outgoingCallsMarker),
582-
"preparerename": actionMarkerFunc(prepareRenameMarker),
587+
"preparerename": actionMarkerFunc(prepareRenameMarker, "span"),
583588
"rank": actionMarkerFunc(rankMarker),
584589
"refs": actionMarkerFunc(refsMarker),
585590
"rename": actionMarkerFunc(renameMarker),
@@ -2474,7 +2479,7 @@ func inlayhintsMarker(mark marker, g *Golden) {
24742479
compareGolden(mark, got, g)
24752480
}
24762481

2477-
func prepareRenameMarker(mark marker, src, spn protocol.Location, placeholder string) {
2482+
func prepareRenameMarker(mark marker, src protocol.Location, placeholder string) {
24782483
params := &protocol.PrepareRenameParams{
24792484
TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(src),
24802485
}
@@ -2488,7 +2493,15 @@ func prepareRenameMarker(mark marker, src, spn protocol.Location, placeholder st
24882493
}
24892494
return
24902495
}
2491-
want := &protocol.PrepareRenameResult{Range: spn.Range, Placeholder: placeholder}
2496+
2497+
want := &protocol.PrepareRenameResult{
2498+
Placeholder: placeholder,
2499+
}
2500+
if span := namedArg(mark, "span", protocol.Location{}); span != (protocol.Location{}) {
2501+
want.Range = span.Range
2502+
} else {
2503+
got.Range = protocol.Range{} // ignore Range
2504+
}
24922505
if diff := cmp.Diff(want, got); diff != "" {
24932506
mark.errorf("mismatching PrepareRename result:\n%s", diff)
24942507
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
This test checks basic functionality for renaming (=changing) a function
2+
signature.
3+
4+
-- go.mod --
5+
module example.com
6+
7+
go 1.20
8+
9+
-- a/a.go --
10+
package a
11+
12+
//@rename(Foo, "func(i int, s string)", unchanged)
13+
//@rename(Foo, "func(s string, i int)", reverse)
14+
//@rename(Foo, "func(s string)", dropi)
15+
//@rename(Foo, "func(i int)", drops)
16+
//@rename(Foo, "func()", dropboth)
17+
//@renameerr(Foo, "func(i int, s string, t bool)", "not yet supported")
18+
//@renameerr(Foo, "func(i string)", "not yet supported")
19+
//@renameerr(Foo, "func(i int, s string) int", "not yet supported")
20+
21+
func Foo(i int, s string) { //@loc(Foo, "func")
22+
}
23+
24+
func _() {
25+
Foo(0, "hi")
26+
}
27+
-- @dropboth/a/a.go --
28+
@@ -12 +12 @@
29+
-func Foo(i int, s string) { //@loc(Foo, "func")
30+
+func Foo() { //@loc(Foo, "func")
31+
@@ -16 +16 @@
32+
- Foo(0, "hi")
33+
+ Foo()
34+
-- @dropi/a/a.go --
35+
@@ -12 +12 @@
36+
-func Foo(i int, s string) { //@loc(Foo, "func")
37+
+func Foo(s string) { //@loc(Foo, "func")
38+
@@ -16 +16 @@
39+
- Foo(0, "hi")
40+
+ Foo("hi")
41+
-- @drops/a/a.go --
42+
@@ -12 +12 @@
43+
-func Foo(i int, s string) { //@loc(Foo, "func")
44+
+func Foo(i int) { //@loc(Foo, "func")
45+
@@ -16 +16 @@
46+
- Foo(0, "hi")
47+
+ Foo(0)
48+
-- @reverse/a/a.go --
49+
@@ -12 +12 @@
50+
-func Foo(i int, s string) { //@loc(Foo, "func")
51+
+func Foo(s string, i int) { //@loc(Foo, "func")
52+
@@ -16 +16 @@
53+
- Foo(0, "hi")
54+
+ Foo("hi", 0)
55+
-- @unchanged/a/a.go --

gopls/internal/test/marker/testdata/rename/issue43616.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ fields.
44
-- p.go --
55
package issue43616
66

7-
type foo int //@rename("foo", "bar", fooToBar),preparerename("oo","foo","foo")
7+
type foo int //@rename("foo", "bar", fooToBar),preparerename("oo","foo",span="foo")
88

99
var x struct{ foo } //@renameerr("foo", "baz", "rename the type directly")
1010

1111
var _ = x.foo //@renameerr("foo", "quux", "rename the type directly")
1212
-- @fooToBar/p.go --
1313
@@ -3 +3 @@
14-
-type foo int //@rename("foo", "bar", fooToBar),preparerename("oo","foo","foo")
15-
+type bar int //@rename("foo", "bar", fooToBar),preparerename("oo","foo","foo")
14+
-type foo int //@rename("foo", "bar", fooToBar),preparerename("oo","foo",span="foo")
15+
+type bar int //@rename("foo", "bar", fooToBar),preparerename("oo","foo",span="foo")
1616
@@ -5 +5 @@
1717
-var x struct{ foo } //@renameerr("foo", "baz", "rename the type directly")
1818
+var x struct{ bar } //@renameerr("foo", "baz", "rename the type directly")

0 commit comments

Comments
 (0)