Skip to content

Commit 6a8bc5f

Browse files
committed
gopls/internal/lsp/source: ignore lambdas in call hierarchy
Previously, anonymous functions were treated as nodes in the call hierarchy view, but it doesn't make sense to do so because we have no reliable way to find references to them, except from within their enclosing function. So, this change treats anonymous functions and their enclosing functions as a single item. Fixes golang/go#64451 Change-Id: I3841adcbad4b13ab190fad58daf38c1bbc6f8baa Reviewed-on: https://go-review.googlesource.com/c/tools/+/546736 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Robert Findley <[email protected]>
1 parent b5dbe8b commit 6a8bc5f

File tree

4 files changed

+109
-48
lines changed

4 files changed

+109
-48
lines changed

gopls/internal/golang/call_hierarchy.go

Lines changed: 42 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -125,48 +125,59 @@ func enclosingNodeCallItem(ctx context.Context, snapshot *cache.Snapshot, pkgPat
125125
return protocol.CallHierarchyItem{}, err
126126
}
127127

128-
// Find the enclosing function, if any, and the number of func literals in between.
129-
var funcDecl *ast.FuncDecl
130-
var funcLit *ast.FuncLit // innermost function literal
131-
var litCount int
128+
// Find the enclosing named function, if any.
129+
//
130+
// It is tempting to treat anonymous functions as nodes in the
131+
// call hierarchy, and historically we used to do that,
132+
// poorly; see #64451. However, it is impossible to track
133+
// references to anonymous functions without much deeper
134+
// analysis. Local analysis is tractable, but ultimately it
135+
// can only detect calls from the outer function to the inner
136+
// function.
137+
//
138+
// It is simpler and clearer to treat the top-level named
139+
// function and all its nested functions as one entity, and it
140+
// allows users to recursively expand the tree where, before,
141+
// the chain would be broken by each lambda.
142+
//
143+
// If the selection is in a global var initializer,
144+
// default to the file's package declaration.
132145
path, _ := astutil.PathEnclosingInterval(pgf.File, start, end)
133-
outer:
146+
var (
147+
name = pgf.File.Name.Name
148+
kind = protocol.Package
149+
)
150+
start, end = pgf.File.Name.Pos(), pgf.File.Name.End()
134151
for _, node := range path {
135-
switch n := node.(type) {
152+
switch node := node.(type) {
136153
case *ast.FuncDecl:
137-
funcDecl = n
138-
break outer
154+
name = node.Name.Name
155+
start, end = node.Name.Pos(), node.Name.End()
156+
kind = protocol.Function
157+
139158
case *ast.FuncLit:
140-
litCount++
141-
if litCount > 1 {
142-
continue
143-
}
144-
funcLit = n
159+
// If the call comes from a FuncLit with
160+
// no enclosing FuncDecl, then use the
161+
// FuncLit's extent.
162+
name = "func"
163+
start, end = node.Pos(), node.Type.End() // signature, sans body
164+
kind = protocol.Function
165+
166+
case *ast.ValueSpec:
167+
// If the call comes from a var (or,
168+
// theoretically, const) initializer outside
169+
// any function, then use the ValueSpec.Names span.
170+
name = "init"
171+
start, end = node.Names[0].Pos(), node.Names[len(node.Names)-1].End()
172+
kind = protocol.Variable
145173
}
146174
}
147175

148-
nameIdent := path[len(path)-1].(*ast.File).Name
149-
kind := protocol.Package
150-
if funcDecl != nil {
151-
nameIdent = funcDecl.Name
152-
kind = protocol.Function
153-
}
154-
155-
nameStart, nameEnd := nameIdent.Pos(), nameIdent.End()
156-
if funcLit != nil {
157-
nameStart, nameEnd = funcLit.Type.Func, funcLit.Type.Params.Pos()
158-
kind = protocol.Function
159-
}
160-
rng, err := pgf.PosRange(nameStart, nameEnd)
176+
rng, err := pgf.PosRange(start, end)
161177
if err != nil {
162178
return protocol.CallHierarchyItem{}, err
163179
}
164180

165-
name := nameIdent.Name
166-
for i := 0; i < litCount; i++ {
167-
name += ".func()"
168-
}
169-
170181
return protocol.CallHierarchyItem{
171182
Name: name,
172183
Kind: kind,
@@ -216,7 +227,6 @@ func OutgoingCalls(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle
216227
return nil, err
217228
}
218229

219-
// Use TypecheckFull as we want to inspect the body of the function declaration.
220230
declPkg, declPGF, err := NarrowestPackageForFile(ctx, snapshot, uri)
221231
if err != nil {
222232
return nil, err

gopls/internal/test/marker/doc.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@ The following markers are supported within marker tests:
175175
- incomingcalls(src location, want ...location): makes a
176176
callHierarchy/incomingCalls query at the src location, and checks that
177177
the set of call.From locations matches want.
178+
(These locations are the declarations of the functions enclosing
179+
the calls, not the calls themselves.)
178180
179181
- item(label, details, kind): defines a completion item with the provided
180182
fields. This information is not positional, and therefore @item markers

gopls/internal/test/marker/testdata/callhierarchy/callhierarchy.txt

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,36 +26,34 @@ func B() { //@loc(outgoingB, "B")
2626
}
2727

2828
-- hierarchy.go --
29-
package callhierarchy
29+
package callhierarchy //@loc(hPkg, "callhierarchy")
3030

3131
import "golang.org/lsptests/callhierarchy/outgoing"
3232

33-
func a() { //@loc(hierarchyA, "a")
33+
func a() { //@loc(hA, "a")
3434
D()
3535
}
3636

37-
func b() { //@loc(hierarchyB, "b")
37+
func b() { //@loc(hB, "b")
3838
D()
3939
}
4040

4141
// C is an exported function
42-
func C() { //@loc(hierarchyC, "C")
42+
func C() { //@loc(hC, "C")
4343
D()
4444
D()
4545
}
4646

4747
// To test hierarchy across function literals
48-
var x = func() { //@loc(hierarchyLiteral, "func"),loc(hierarchyLiteralOut, "x")
49-
D()
50-
}
48+
var x = func() { D() } //@loc(hX, "x"),loc(hXGlobal, "x")
5149

5250
// D is exported to test incoming/outgoing calls across packages
53-
func D() { //@loc(hierarchyD, "D"),incomingcalls(hierarchyD, hierarchyA, hierarchyB, hierarchyC, hierarchyLiteral, incomingA),outgoingcalls(hierarchyD, hierarchyE, hierarchyF, hierarchyG, hierarchyLiteralOut, outgoingB, hierarchyFoo, hierarchyH, hierarchyI, hierarchyJ, hierarchyK)
51+
func D() { //@loc(hD, "D"),incomingcalls(hD, hA, hB, hC, hXGlobal, incomingA),outgoingcalls(hD, hE, hF, hG, hX, outgoingB, hFoo, hH, hI, hJ, hK)
5452
e()
5553
x()
5654
F()
5755
outgoing.B()
58-
foo := func() {} //@loc(hierarchyFoo, "foo"),incomingcalls(hierarchyFoo, hierarchyD),outgoingcalls(hierarchyFoo)
56+
foo := func() {} //@loc(hFoo, "foo"),incomingcalls(hFoo, hD),outgoingcalls(hFoo)
5957
foo()
6058

6159
func() {
@@ -71,16 +69,16 @@ func D() { //@loc(hierarchyD, "D"),incomingcalls(hierarchyD, hierarchyA, hierarc
7169
s.K()
7270
}
7371

74-
func e() {} //@loc(hierarchyE, "e")
72+
func e() {} //@loc(hE, "e")
7573

7674
// F is an exported function
77-
func F() {} //@loc(hierarchyF, "F")
75+
func F() {} //@loc(hF, "F")
7876

79-
func g() {} //@loc(hierarchyG, "g")
77+
func g() {} //@loc(hG, "g")
8078

8179
type Interface interface {
82-
H() //@loc(hierarchyH, "H")
83-
I() //@loc(hierarchyI, "I")
80+
H() //@loc(hH, "H")
81+
I() //@loc(hI, "I")
8482
}
8583

8684
type impl struct{}
@@ -89,6 +87,6 @@ func (i impl) H() {}
8987
func (i impl) I() {}
9088

9189
type Struct struct {
92-
J func() //@loc(hierarchyJ, "J")
93-
K func() //@loc(hierarchyK, "K")
90+
J func() //@loc(hJ, "J")
91+
K func() //@loc(hK, "K")
9492
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
This test checks call hierarchy queries involving lambdas, which are
2+
treated as mere statements of their enclosing name function, since
3+
we can't track calls to them.
4+
5+
Calls from a global var decl are reported at the ValueSpec.Names.
6+
7+
See golang/go#64451.
8+
9+
-- go.mod --
10+
module example.com
11+
go 1.0
12+
13+
-- a/a.go --
14+
package a
15+
16+
func foo() { //@ loc(foo, "foo")
17+
bar()
18+
}
19+
20+
func bar() { //@ loc(bar, "bar")
21+
go func() { baz() }()
22+
}
23+
24+
func baz() { //@ loc(baz, "baz")
25+
bluh()
26+
}
27+
28+
func bluh() { //@ loc(bluh, "bluh")
29+
print()
30+
}
31+
32+
var _ = func() int { //@ loc(global, "_")
33+
baz()
34+
return 0
35+
}()
36+
37+
func init() { //@ loc(init, "init")
38+
baz()
39+
}
40+
41+
//@ outgoingcalls(foo, bar)
42+
//@ outgoingcalls(bar, baz)
43+
//@ outgoingcalls(baz, bluh)
44+
//@ outgoingcalls(bluh)
45+
//@ outgoingcalls(init, baz)
46+
47+
//@ incomingcalls(foo)
48+
//@ incomingcalls(bar, foo)
49+
//@ incomingcalls(baz, bar, global, init)
50+
//@ incomingcalls(bluh, baz)
51+
//@ incomingcalls(init)

0 commit comments

Comments
 (0)