internal/lsp: add incoming calls hierarchy to gopls
* Adds incoming calls hierarchy to gopls. Returns function declarations/function literals/files enclosing the call/s to the function being inpected.
* Updates cmd to show ranges where calls to function in consideration are made by the caller.
* Added tests for incoming calls.
Example:
This example shows call hierarchy for PathEnclosingInterval in tools/go/ast/astutil.go
Show Call Hierarchy View: https://imgur.com/a/9VhspgA
Peek Call Hierarchy View: https://imgur.com/a/XlKubFk
Note:
* Function literals show up as <scope>.func() in call hierarchy since they don't have a name. Here scope is either the function enclosing the literal or a file for top level declarations
* Top level calls (calls not inside a function, ex: to initialize exported variables) show up as the file name
* Clicking on an item shows the the range where a call is made in the scope
Change-Id: I56426139e4e550dfabe43c9e9f1838efd1e43e38
Reviewed-on: https://go-review.googlesource.com/c/tools/+/247699
Run-TryBot: Danish Dua <danishdua@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
diff --git a/internal/lsp/cmd/call_hierarchy.go b/internal/lsp/cmd/call_hierarchy.go
index b7b5fee..478c4c2 100644
--- a/internal/lsp/cmd/call_hierarchy.go
+++ b/internal/lsp/cmd/call_hierarchy.go
@@ -8,6 +8,7 @@
"context"
"flag"
"fmt"
+ "strings"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/span"
@@ -52,8 +53,7 @@
return file.err
}
- columnMapper := file.mapper
- loc, err := columnMapper.Location(from)
+ loc, err := file.mapper.Location(from)
if err != nil {
return err
}
@@ -79,14 +79,14 @@
return err
}
for i, call := range incomingCalls {
- printString, err := toPrintString(columnMapper, call.From)
+ printString, err := callItemPrintString(ctx, conn, call.From, call.FromRanges)
if err != nil {
return err
}
fmt.Printf("caller[%d]: %s\n", i, printString)
}
- printString, err := toPrintString(columnMapper, item)
+ printString, err := callItemPrintString(ctx, conn, item, []protocol.Range{})
if err != nil {
return err
}
@@ -97,7 +97,7 @@
return err
}
for i, call := range outgoingCalls {
- printString, err := toPrintString(columnMapper, call.To)
+ printString, err := callItemPrintString(ctx, conn, call.To, call.FromRanges)
if err != nil {
return err
}
@@ -108,10 +108,30 @@
return nil
}
-func toPrintString(mapper *protocol.ColumnMapper, item protocol.CallHierarchyItem) (string, error) {
- span, err := mapper.Span(protocol.Location{URI: item.URI, Range: item.Range})
+func callItemPrintString(ctx context.Context, conn *connection, item protocol.CallHierarchyItem, rngs []protocol.Range) (string, error) {
+ file := conn.AddFile(ctx, span.URIFromURI(string(item.URI)))
+ if file.err != nil {
+ return "", file.err
+ }
+ enclosingSpan, err := file.mapper.Span(protocol.Location{URI: item.URI, Range: item.Range})
if err != nil {
return "", err
}
- return fmt.Sprintf("%v %v at %v", item.Detail, item.Name, span), nil
+
+ var ranges []string
+ for _, rng := range rngs {
+ callSpan, err := file.mapper.Span(protocol.Location{URI: item.URI, Range: rng})
+ if err != nil {
+ return "", err
+ }
+
+ spn := fmt.Sprint(callSpan)
+ ranges = append(ranges, fmt.Sprintf("%s", spn[strings.Index(spn, ":")+1:]))
+ }
+
+ printString := fmt.Sprintf("function %s at %v", item.Name, enclosingSpan)
+ if len(rngs) > 0 {
+ printString = fmt.Sprintf("ranges %s in %s", strings.Join(ranges, ", "), printString)
+ }
+ return printString, nil
}
diff --git a/internal/lsp/cmd/test/call_hierarchy.go b/internal/lsp/cmd/test/call_hierarchy.go
index 6ec689d..c83720b 100644
--- a/internal/lsp/cmd/test/call_hierarchy.go
+++ b/internal/lsp/cmd/test/call_hierarchy.go
@@ -10,13 +10,25 @@
"strings"
"testing"
+ "golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/tests"
"golang.org/x/tools/internal/span"
)
func (r *runner) CallHierarchy(t *testing.T, spn span.Span, expectedCalls *tests.CallHierarchyResult) {
var result []string
- // TODO: add expectedCalls.IncomingCalls and expectedCalls.OutgoingCalls to this array once implemented
+ // TODO: add expectedCalls.OutgoingCalls to this array once implemented
+ for _, call := range expectedCalls.IncomingCalls {
+ mapper, err := r.data.Mapper(call.URI.SpanURI())
+ if err != nil {
+ t.Fatal(err)
+ }
+ callSpan, err := mapper.Span(protocol.Location{URI: call.URI, Range: call.Range})
+ if err != nil {
+ t.Fatal(err)
+ }
+ result = append(result, fmt.Sprint(callSpan))
+ }
result = append(result, fmt.Sprint(spn))
sort.Strings(result) // to make tests deterministic
@@ -35,8 +47,8 @@
}
}
-// removes all info except URI and Range from printed output and sorts the result
-// ex: "identifier: func() d at file://callhierarchy/callhierarchy.go:19:6-7" -> "file://callhierarchy/callhierarchy.go:19:6-7"
+// removes all info except function URI and Range from printed output and sorts the result
+// ex: "identifier: function d at .../callhierarchy/callhierarchy.go:19:6-7" -> ".../callhierarchy/callhierarchy.go:19:6-7"
func cleanCallHierarchyCmdResult(output string) string {
var clean []string
for _, out := range strings.Split(output, "\n") {
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index bda2092..3b308a8 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -121,7 +121,7 @@
t.Fatal(err)
}
if len(items) == 0 {
- t.Errorf("expected call hierarchy item to be returned for identifier at %v\n", loc.Range)
+ t.Fatalf("expected call hierarchy item to be returned for identifier at %v\n", loc.Range)
}
callLocation := protocol.Location{
@@ -132,20 +132,27 @@
t.Fatalf("expected server.PrepareCallHierarchy to return identifier at %v but got %v\n", loc, callLocation)
}
- // TODO: add span comparison tests for expectedCalls once call hierarchy is implemented
incomingCalls, err := r.server.IncomingCalls(r.ctx, &protocol.CallHierarchyIncomingCallsParams{Item: items[0]})
if err != nil {
t.Fatal(err)
}
- if len(incomingCalls) != 0 {
- t.Fatalf("expected no incoming calls but got %d", len(incomingCalls))
+ var incomingCallItems []protocol.CallHierarchyItem
+ for _, item := range incomingCalls {
+ incomingCallItems = append(incomingCallItems, item.From)
}
+
+ msg := tests.DiffCallHierarchyItems(incomingCallItems, expectedCalls.IncomingCalls)
+ if msg != "" {
+ t.Error(msg)
+ }
+
+ // TODO: add span comparison tests for expectedCalls.OutgoingCalls once OutgoingCalls is implemented
outgoingCalls, err := r.server.OutgoingCalls(r.ctx, &protocol.CallHierarchyOutgoingCallsParams{Item: items[0]})
if err != nil {
t.Fatal(err)
}
if len(outgoingCalls) != 0 {
- t.Fatalf("expected no outgoing calls but got %d", len(outgoingCalls))
+ t.Errorf("expected no outgoing calls but got %d", len(outgoingCalls))
}
}
diff --git a/internal/lsp/source/call_hierarchy.go b/internal/lsp/source/call_hierarchy.go
index 23425b9..ecf19a4 100644
--- a/internal/lsp/source/call_hierarchy.go
+++ b/internal/lsp/source/call_hierarchy.go
@@ -6,8 +6,12 @@
import (
"context"
+ "fmt"
"go/ast"
+ "go/token"
+ "path/filepath"
+ "golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/lsp/debug/tag"
"golang.org/x/tools/internal/lsp/protocol"
@@ -29,10 +33,10 @@
return nil, nil
}
- // if identifier is not of type function
+ // if identifier's declaration is not of type function declaration
_, ok := identifier.Declaration.node.(*ast.FuncDecl)
if !ok {
- event.Log(ctx, "invalid identifier type, expected funtion declaration", tag.Position.Of(pos))
+ event.Log(ctx, "invalid identifier declaration, expected funtion declaration", tag.Position.Of(pos))
return nil, nil
}
rng, err := identifier.Range()
@@ -43,7 +47,7 @@
Name: identifier.Name,
Kind: protocol.Function,
Tags: []protocol.SymbolTag{},
- Detail: "func()",
+ Detail: fmt.Sprintf("%s %s", identifier.pkg.PkgPath(), filepath.Base(fh.URI().Filename())),
URI: protocol.DocumentURI(fh.URI()),
Range: rng,
SelectionRange: rng,
@@ -56,10 +60,22 @@
ctx, done := event.Start(ctx, "source.incomingCalls")
defer done()
- // TODO: Remove this once the context is used.
- _ = ctx // avoid staticcheck SA4006 warning
+ qualifiedObjs, err := qualifiedObjsAtProtocolPos(ctx, snapshot, fh, pos)
+ if err != nil {
+ if errors.Is(err, errBuiltin) || errors.Is(err, ErrNoIdentFound) {
+ event.Log(ctx, err.Error(), tag.Position.Of(pos))
+ } else {
+ event.Error(ctx, "error getting identifier", err, tag.Position.Of(pos))
+ }
+ return nil, nil
+ }
- return []protocol.CallHierarchyIncomingCall{}, nil
+ refs, err := references(ctx, snapshot, qualifiedObjs, false)
+ if err != nil {
+ return nil, err
+ }
+
+ return toProtocolIncomingCalls(ctx, snapshot, refs)
}
// OutgoingCalls returns an array of CallHierarchyOutgoingCall for a file and the position within the file
@@ -72,3 +88,110 @@
return []protocol.CallHierarchyOutgoingCall{}, nil
}
+
+// toProtocolIncomingCalls returns an array of protocol.CallHierarchyIncomingCall for ReferenceInfo's.
+// References inside same enclosure are assigned to the same enclosing function.
+func toProtocolIncomingCalls(ctx context.Context, snapshot Snapshot, refs []*ReferenceInfo) ([]protocol.CallHierarchyIncomingCall, error) {
+ // an enclosing node could have multiple calls to a reference, we only show the enclosure
+ // once in the result but highlight all calls using FromRanges (ranges at which the calls occur)
+ var incomingCalls = map[protocol.Range]*protocol.CallHierarchyIncomingCall{}
+ for _, ref := range refs {
+ refRange, err := ref.Range()
+ if err != nil {
+ return nil, err
+ }
+
+ enclosingName, enclosingRange, err := enclosingNodeInfo(snapshot, ref)
+ if err != nil {
+ event.Error(ctx, "error getting enclosing node", err, tag.Method.Of(ref.Name))
+ continue
+ }
+
+ if incomingCall, ok := incomingCalls[enclosingRange]; ok {
+ incomingCall.FromRanges = append(incomingCall.FromRanges, refRange)
+ continue
+ }
+
+ incomingCalls[enclosingRange] = &protocol.CallHierarchyIncomingCall{
+ From: protocol.CallHierarchyItem{
+ Name: enclosingName,
+ Kind: protocol.Function,
+ Tags: []protocol.SymbolTag{},
+ Detail: fmt.Sprintf("%s • %s", ref.pkg.PkgPath(), filepath.Base(ref.URI().Filename())),
+ URI: protocol.DocumentURI(ref.URI()),
+ Range: enclosingRange,
+ SelectionRange: enclosingRange,
+ },
+ FromRanges: []protocol.Range{refRange},
+ }
+ }
+
+ incomingCallItems := make([]protocol.CallHierarchyIncomingCall, 0, len(incomingCalls))
+ for _, callItem := range incomingCalls {
+ incomingCallItems = append(incomingCallItems, *callItem)
+ }
+ return incomingCallItems, nil
+}
+
+// enclosingNodeInfo returns name and position for package/function declaration/function literal
+// containing given call reference
+func enclosingNodeInfo(snapshot Snapshot, ref *ReferenceInfo) (string, protocol.Range, error) {
+ pgf, err := ref.pkg.File(ref.URI())
+ if err != nil {
+ return "", protocol.Range{}, err
+ }
+
+ var funcDecl *ast.FuncDecl
+ var funcLit *ast.FuncLit // innermost function literal
+ var litCount int
+ // Find the enclosing function, if any, and the number of func literals in between.
+ path, _ := astutil.PathEnclosingInterval(pgf.File, ref.ident.NamePos, ref.ident.NamePos)
+outer:
+ for _, node := range path {
+ switch n := node.(type) {
+ case *ast.FuncDecl:
+ funcDecl = n
+ break outer
+ case *ast.FuncLit:
+ litCount++
+ if litCount > 1 {
+ continue
+ }
+ funcLit = n
+ }
+ }
+
+ nameIdent := path[len(path)-1].(*ast.File).Name
+ if funcDecl != nil {
+ nameIdent = funcDecl.Name
+ }
+
+ nameStart, nameEnd := nameIdent.NamePos, nameIdent.NamePos+token.Pos(len(nameIdent.Name))
+ if funcLit != nil {
+ nameStart, nameEnd = funcLit.Type.Func, funcLit.Type.Params.Pos()
+ }
+ rng, err := posToProtocolRange(snapshot, ref.pkg, nameStart, nameEnd)
+ if err != nil {
+ return "", protocol.Range{}, err
+ }
+
+ name := nameIdent.Name
+ for i := 0; i < litCount; i++ {
+ name += ".func()"
+ }
+
+ return name, rng, nil
+}
+
+// posToProtocolRange returns protocol.Range for start and end token.Pos
+func posToProtocolRange(snapshot Snapshot, pkg Package, start, end token.Pos) (protocol.Range, error) {
+ mappedRange, err := posToMappedRange(snapshot, pkg, start, end)
+ if err != nil {
+ return protocol.Range{}, err
+ }
+ protocolRange, err := mappedRange.Range()
+ if err != nil {
+ return protocol.Range{}, err
+ }
+ return protocolRange, nil
+}
diff --git a/internal/lsp/source/references.go b/internal/lsp/source/references.go
index da33917..1eb8308 100644
--- a/internal/lsp/source/references.go
+++ b/internal/lsp/source/references.go
@@ -43,8 +43,7 @@
return references(ctx, s, qualifiedObjs, includeDeclaration)
}
-// references is a helper function used by both References and Rename,
-// to avoid recomputing qualifiedObjsAtProtocolPos.
+// references is a helper function to avoid recomputing qualifiedObjsAtProtocolPos.
func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject, includeDeclaration bool) ([]*ReferenceInfo, error) {
var (
references []*ReferenceInfo
diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go
index 5906ccf..a3d3657 100644
--- a/internal/lsp/source/source_test.go
+++ b/internal/lsp/source/source_test.go
@@ -115,7 +115,7 @@
t.Fatal(err)
}
if len(items) == 0 {
- t.Errorf("expected call hierarchy item to be returned for identifier at %v\n", loc.Range)
+ t.Fatalf("expected call hierarchy item to be returned for identifier at %v\n", loc.Range)
}
callLocation := protocol.Location{
@@ -126,20 +126,26 @@
t.Fatalf("expected source.PrepareCallHierarchy to return identifier at %v but got %v\n", loc, callLocation)
}
- // TODO: add span comparison tests for expectedCalls once call hierarchy is implemented
incomingCalls, err := source.IncomingCalls(r.ctx, r.snapshot, fh, loc.Range.Start)
if err != nil {
t.Fatal(err)
}
- if len(incomingCalls) != 0 {
- t.Fatalf("expected no incoming calls but got %d", len(incomingCalls))
+ var incomingCallItems []protocol.CallHierarchyItem
+ for _, item := range incomingCalls {
+ incomingCallItems = append(incomingCallItems, item.From)
}
+ msg := tests.DiffCallHierarchyItems(incomingCallItems, expectedCalls.IncomingCalls)
+ if msg != "" {
+ t.Error(msg)
+ }
+
+ // TODO: add span comparison tests for expectedCalls.OutgoingCalls once OutgoingCalls is implemented
outgoingCalls, err := source.OutgoingCalls(r.ctx, r.snapshot, fh, loc.Range.Start)
if err != nil {
t.Fatal(err)
}
if len(outgoingCalls) != 0 {
- t.Fatalf("expected no outgoing calls but got %d", len(outgoingCalls))
+ t.Errorf("expected no outgoing calls but got %d", len(outgoingCalls))
}
}
diff --git a/internal/lsp/testdata/lsp/primarymod/callhierarchy/callhierarchy.go b/internal/lsp/testdata/lsp/primarymod/callhierarchy/callhierarchy.go
index b7c774b..fffa641 100644
--- a/internal/lsp/testdata/lsp/primarymod/callhierarchy/callhierarchy.go
+++ b/internal/lsp/testdata/lsp/primarymod/callhierarchy/callhierarchy.go
@@ -2,34 +2,39 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package main
+package callhierarchy
-func a() { //@mark(funcA, "a")
- d()
+import "golang.org/x/tools/internal/lsp/callhierarchy/outgoing"
+
+func a() { //@mark(hierarchyA, "a")
+ D()
}
-func b() { //@mark(funcB, "b")
- d()
+func b() { //@mark(hierarchyB, "b")
+ D()
}
-func c() { //@mark(funcC, "c")
- d()
+// C is an exported function
+func C() { //@mark(hierarchyC, "C")
+ D()
+ D()
}
-func d() { //@mark(funcD, "d"),incomingcalls("d", funcA, funcB, funcC),outgoingcalls("d", funcE, funcF, funcG)
+// To test hierarchy across function literals
+const x = func() { //@mark(hierarchyLiteral, "func")
+ D()
+}
+
+// D is exported to test incoming/outgoing calls across packages
+func D() { //@mark(hierarchyD, "D"),incomingcalls(hierarchyD, hierarchyA, hierarchyB, hierarchyC, hierarchyLiteral, incomingA),outgoingcalls(hierarchyD, hierarchyE, hierarchyF, hierarchyG, outgoingB)
e()
f()
g()
+ outgoing.B()
}
-func e() {} //@mark(funcE, "e")
+func e() {} //@mark(hierarchyE, "e")
-func f() {} //@mark(funcF, "f")
+func f() {} //@mark(hierarchyF, "f")
-func g() {} //@mark(funcG, "g")
-
-func main() {
- a()
- b()
- c()
-}
+func g() {} //@mark(hierarchyG, "g")
diff --git a/internal/lsp/testdata/lsp/primarymod/callhierarchy/incoming/incoming.go b/internal/lsp/testdata/lsp/primarymod/callhierarchy/incoming/incoming.go
new file mode 100644
index 0000000..3bfb4ad
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/callhierarchy/incoming/incoming.go
@@ -0,0 +1,12 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package incoming
+
+import "golang.org/x/tools/internal/lsp/callhierarchy"
+
+// A is exported to test incoming calls across packages
+func A() { //@mark(incomingA, "A")
+ callhierarchy.D()
+}
diff --git a/internal/lsp/testdata/lsp/primarymod/callhierarchy/outgoing/outgoing.go b/internal/lsp/testdata/lsp/primarymod/callhierarchy/outgoing/outgoing.go
new file mode 100644
index 0000000..74362d4
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/callhierarchy/outgoing/outgoing.go
@@ -0,0 +1,9 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package outgoing
+
+// B is exported to test outgoing calls across packages
+func B() { //@mark(outgoingB, "B")
+}
diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go
index bfdecd2..63bed53 100644
--- a/internal/lsp/tests/tests.go
+++ b/internal/lsp/tests/tests.go
@@ -201,7 +201,7 @@
}
type CallHierarchyResult struct {
- IncomingCalls, OutgoingCalls []span.Span
+ IncomingCalls, OutgoingCalls []protocol.CallHierarchyItem
}
type Link struct {
@@ -1082,21 +1082,55 @@
}
func (data *Data) collectIncomingCalls(src span.Span, calls []span.Span) {
- if data.CallHierarchy[src] != nil {
- data.CallHierarchy[src].IncomingCalls = calls
- } else {
- data.CallHierarchy[src] = &CallHierarchyResult{
- IncomingCalls: calls,
+ for _, call := range calls {
+ m, err := data.Mapper(call.URI())
+ if err != nil {
+ data.t.Fatal(err)
+ }
+ rng, err := m.Range(call)
+ if err != nil {
+ data.t.Fatal(err)
+ }
+ // we're only comparing protocol.range
+ if data.CallHierarchy[src] != nil {
+ data.CallHierarchy[src].IncomingCalls = append(data.CallHierarchy[src].IncomingCalls,
+ protocol.CallHierarchyItem{
+ URI: protocol.DocumentURI(call.URI()),
+ Range: rng,
+ })
+ } else {
+ data.CallHierarchy[src] = &CallHierarchyResult{
+ IncomingCalls: []protocol.CallHierarchyItem{
+ {URI: protocol.DocumentURI(call.URI()), Range: rng},
+ },
+ }
}
}
}
func (data *Data) collectOutgoingCalls(src span.Span, calls []span.Span) {
- if data.CallHierarchy[src] != nil {
- data.CallHierarchy[src].OutgoingCalls = calls
- } else {
- data.CallHierarchy[src] = &CallHierarchyResult{
- OutgoingCalls: calls,
+ for _, call := range calls {
+ m, err := data.Mapper(call.URI())
+ if err != nil {
+ data.t.Fatal(err)
+ }
+ rng, err := m.Range(call)
+ if err != nil {
+ data.t.Fatal(err)
+ }
+ // we're only comparing protocol.range
+ if data.CallHierarchy[src] != nil {
+ data.CallHierarchy[src].OutgoingCalls = append(data.CallHierarchy[src].OutgoingCalls,
+ protocol.CallHierarchyItem{
+ URI: protocol.DocumentURI(call.URI()),
+ Range: rng,
+ })
+ } else {
+ data.CallHierarchy[src] = &CallHierarchyResult{
+ OutgoingCalls: []protocol.CallHierarchyItem{
+ {URI: protocol.DocumentURI(call.URI()), Range: rng},
+ },
+ }
}
}
}
diff --git a/internal/lsp/tests/util.go b/internal/lsp/tests/util.go
index b859a2c..7970135 100644
--- a/internal/lsp/tests/util.go
+++ b/internal/lsp/tests/util.go
@@ -310,6 +310,28 @@
return ""
}
+// DiffCallHierarchyItems returns the diff between expected and actual call locations for incoming/outgoing call hierarchies
+func DiffCallHierarchyItems(gotCalls []protocol.CallHierarchyItem, expectedCalls []protocol.CallHierarchyItem) string {
+ expected := make(map[protocol.Location]bool)
+ for _, call := range expectedCalls {
+ expected[protocol.Location{URI: call.URI, Range: call.Range}] = true
+ }
+
+ got := make(map[protocol.Location]bool)
+ for _, call := range gotCalls {
+ got[protocol.Location{URI: call.URI, Range: call.Range}] = true
+ }
+ if len(got) != len(expected) {
+ return fmt.Sprintf("expected %d incoming calls but got %d", len(expected), len(got))
+ }
+ for spn := range got {
+ if !expected[spn] {
+ return fmt.Sprintf("incorrect incoming calls, expected locations %v but got locations %v", expected, got)
+ }
+ }
+ return ""
+}
+
func ToProtocolCompletionItems(items []source.CompletionItem) []protocol.CompletionItem {
var result []protocol.CompletionItem
for _, item := range items {