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 {