internal: add call hierarchy cmd and lsp scaffolding

* adds gopls command line tool for call hierarchy
* adds lsp setup for call hierarchy
* adds handler for textDocument/prepareCallHierarchy to display selected
  identifier and get incoming/outgoing calls for it
* setup testing

Change-Id: I0a0904abdbe11273a56162b6e5be93b97ceb9c26
Reviewed-on: https://go-review.googlesource.com/c/tools/+/246521
Run-TryBot: Danish Dua <danishdua@google.com>
Reviewed-by: Heschi Kreinick <heschi@google.com>
diff --git a/gopls/integration/parse/protocol.go b/gopls/integration/parse/protocol.go
index d812a54..d1e90ed 100644
--- a/gopls/integration/parse/protocol.go
+++ b/gopls/integration/parse/protocol.go
@@ -87,6 +87,10 @@
 		return new(p.TextDocumentPositionParams)
 	case "textDocument/foldingRange":
 		return new(p.FoldingRangeParams)
+	case "textDocument/incomingCalls":
+		return new(p.CallHierarchyIncomingCallsParams)
+	case "textDocument/outgoingCalls":
+		return new(p.CallHierarchyOutgoingCallsParams)
 	}
 	log.Fatalf("request(%s) undefined", m)
 	return ""
@@ -210,6 +214,10 @@
 		return []interface{}{new(p.Range), nil}
 	case "textDocument/foldingRange":
 		return []interface{}{new([]p.FoldingRange), nil}
+	case "callHierarchy/incomingCalls":
+		return []interface{}{new([]p.CallHierarchyIncomingCall), nil}
+	case "callHierarchy/outgoingCalls":
+		return []interface{}{new([]p.CallHierarchyOutgoingCall), nil}
 	}
 	log.Fatalf("responses(%q) undefined", m)
 	return nil
@@ -307,4 +315,6 @@
 	"textDocument/rename":                          Mreq | Mcl,
 	"textDocument/prepareRename":                   Mreq | Mcl,
 	"textDocument/foldingRange":                    Mreq | Mcl,
+	"callHierarchy/incomingCalls":                  Mreq | Mcl,
+	"callHierarchy/outgoingCalls":                  Mreq | Mcl,
 }
diff --git a/internal/lsp/call_hierarchy.go b/internal/lsp/call_hierarchy.go
new file mode 100644
index 0000000..f9307d7
--- /dev/null
+++ b/internal/lsp/call_hierarchy.go
@@ -0,0 +1,39 @@
+// 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 lsp
+
+import (
+	"context"
+
+	"golang.org/x/tools/internal/lsp/protocol"
+	"golang.org/x/tools/internal/lsp/source"
+)
+
+func (s *Server) prepareCallHierarchy(ctx context.Context, params *protocol.CallHierarchyPrepareParams) ([]protocol.CallHierarchyItem, error) {
+	snapshot, fh, ok, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go)
+	if !ok {
+		return nil, err
+	}
+
+	return source.PrepareCallHierarchy(ctx, snapshot, fh, params.Position)
+}
+
+func (s *Server) incomingCalls(ctx context.Context, params *protocol.CallHierarchyIncomingCallsParams) ([]protocol.CallHierarchyIncomingCall, error) {
+	snapshot, fh, ok, err := s.beginFileRequest(ctx, params.Item.URI, source.Go)
+	if !ok {
+		return nil, err
+	}
+
+	return source.IncomingCalls(ctx, snapshot, fh, params.Item.Range.Start)
+}
+
+func (s *Server) outgoingCalls(ctx context.Context, params *protocol.CallHierarchyOutgoingCallsParams) ([]protocol.CallHierarchyOutgoingCall, error) {
+	snapshot, fh, ok, err := s.beginFileRequest(ctx, params.Item.URI, source.Go)
+	if !ok {
+		return nil, err
+	}
+
+	return source.OutgoingCalls(ctx, snapshot, fh, params.Item.Range.Start)
+}
diff --git a/internal/lsp/cmd/call_hierarchy.go b/internal/lsp/cmd/call_hierarchy.go
new file mode 100644
index 0000000..b7b5fee
--- /dev/null
+++ b/internal/lsp/cmd/call_hierarchy.go
@@ -0,0 +1,117 @@
+// 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 cmd
+
+import (
+	"context"
+	"flag"
+	"fmt"
+
+	"golang.org/x/tools/internal/lsp/protocol"
+	"golang.org/x/tools/internal/span"
+	"golang.org/x/tools/internal/tool"
+)
+
+// callHierarchy implements the callHierarchy verb for gopls
+type callHierarchy struct {
+	app *Application
+}
+
+func (c *callHierarchy) Name() string      { return "call_hierarchy" }
+func (c *callHierarchy) Usage() string     { return "<position>" }
+func (c *callHierarchy) ShortHelp() string { return "display selected identifier's call hierarchy" }
+func (c *callHierarchy) DetailedHelp(f *flag.FlagSet) {
+	fmt.Fprint(f.Output(), `
+Example:
+
+  $ # 1-indexed location (:line:column or :#offset) of the target identifier
+  $ gopls call_hierarchy helper/helper.go:8:6
+  $ gopls call_hierarchy helper/helper.go:#53
+
+  gopls call_hierarchy flags are:
+`)
+	f.PrintDefaults()
+}
+
+func (c *callHierarchy) Run(ctx context.Context, args ...string) error {
+	if len(args) != 1 {
+		return tool.CommandLineErrorf("call_hierarchy expects 1 argument (position)")
+	}
+
+	conn, err := c.app.connect(ctx)
+	if err != nil {
+		return err
+	}
+	defer conn.terminate(ctx)
+
+	from := span.Parse(args[0])
+	file := conn.AddFile(ctx, from.URI())
+	if file.err != nil {
+		return file.err
+	}
+
+	columnMapper := file.mapper
+	loc, err := columnMapper.Location(from)
+	if err != nil {
+		return err
+	}
+
+	p := protocol.CallHierarchyPrepareParams{
+		TextDocumentPositionParams: protocol.TextDocumentPositionParams{
+			TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI},
+			Position:     loc.Range.Start,
+		},
+	}
+
+	callItems, err := conn.PrepareCallHierarchy(ctx, &p)
+	if err != nil {
+		return err
+	}
+	if len(callItems) == 0 {
+		return fmt.Errorf("function declaration identifier not found at %v", args[0])
+	}
+
+	for _, item := range callItems {
+		incomingCalls, err := conn.IncomingCalls(ctx, &protocol.CallHierarchyIncomingCallsParams{Item: item})
+		if err != nil {
+			return err
+		}
+		for i, call := range incomingCalls {
+			printString, err := toPrintString(columnMapper, call.From)
+			if err != nil {
+				return err
+			}
+			fmt.Printf("caller[%d]: %s\n", i, printString)
+		}
+
+		printString, err := toPrintString(columnMapper, item)
+		if err != nil {
+			return err
+		}
+		fmt.Printf("identifier: %s\n", printString)
+
+		outgoingCalls, err := conn.OutgoingCalls(ctx, &protocol.CallHierarchyOutgoingCallsParams{Item: item})
+		if err != nil {
+			return err
+		}
+		for i, call := range outgoingCalls {
+			printString, err := toPrintString(columnMapper, call.To)
+			if err != nil {
+				return err
+			}
+			fmt.Printf("callee[%d]: %s\n", i, printString)
+		}
+	}
+
+	return nil
+}
+
+func toPrintString(mapper *protocol.ColumnMapper, item protocol.CallHierarchyItem) (string, error) {
+	span, err := 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
+}
diff --git a/internal/lsp/cmd/cmd.go b/internal/lsp/cmd/cmd.go
index 1b142ec..92a817a 100644
--- a/internal/lsp/cmd/cmd.go
+++ b/internal/lsp/cmd/cmd.go
@@ -175,6 +175,7 @@
 
 func (app *Application) featureCommands() []tool.Application {
 	return []tool.Application{
+		&callHierarchy{app: app},
 		&check{app: app},
 		&definition{app: app},
 		&foldingRanges{app: app},
diff --git a/internal/lsp/cmd/test/call_hierarchy.go b/internal/lsp/cmd/test/call_hierarchy.go
new file mode 100644
index 0000000..6ec689d
--- /dev/null
+++ b/internal/lsp/cmd/test/call_hierarchy.go
@@ -0,0 +1,50 @@
+// 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 cmdtest
+
+import (
+	"fmt"
+	"sort"
+	"strings"
+	"testing"
+
+	"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
+	result = append(result, fmt.Sprint(spn))
+
+	sort.Strings(result) // to make tests deterministic
+	expect := r.Normalize(strings.Join(result, "\n"))
+
+	uri := spn.URI()
+	filename := uri.Filename()
+	target := filename + fmt.Sprintf(":%v:%v", spn.Start().Line(), spn.Start().Column())
+
+	got, stderr := r.NormalizeGoplsCmd(t, "call_hierarchy", target)
+	got = cleanCallHierarchyCmdResult(got)
+	if stderr != "" {
+		t.Errorf("call_hierarchy failed for %s: %s", target, stderr)
+	} else if expect != got {
+		t.Errorf("call_hierarchy failed for %s expected:\n%s\ngot:\n%s", target, expect, got)
+	}
+}
+
+// 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"
+func cleanCallHierarchyCmdResult(output string) string {
+	var clean []string
+	for _, out := range strings.Split(output, "\n") {
+		if out == "" {
+			continue
+		}
+		clean = append(clean, out[strings.LastIndex(out, " ")+1:])
+	}
+	sort.Strings(clean)
+	return strings.Join(clean, "\n")
+}
diff --git a/internal/lsp/general.go b/internal/lsp/general.go
index caab6d2..ed9a74e 100644
--- a/internal/lsp/general.go
+++ b/internal/lsp/general.go
@@ -85,7 +85,8 @@
 
 	return &protocol.InitializeResult{
 		Capabilities: protocol.ServerCapabilities{
-			CodeActionProvider: codeActionProvider,
+			CallHierarchyProvider: true,
+			CodeActionProvider:    codeActionProvider,
 			CompletionProvider: protocol.CompletionOptions{
 				TriggerCharacters: []string{"."},
 			},
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index 3e16515..579f51d 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -99,6 +99,50 @@
 	}
 }
 
+func (r *runner) CallHierarchy(t *testing.T, spn span.Span, expectedCalls *tests.CallHierarchyResult) {
+	mapper, err := r.data.Mapper(spn.URI())
+	if err != nil {
+		t.Fatal(err)
+	}
+	loc, err := mapper.Location(spn)
+	if err != nil {
+		t.Fatalf("failed for %v: %v", spn, err)
+	}
+
+	params := &protocol.CallHierarchyPrepareParams{
+		TextDocumentPositionParams: protocol.TextDocumentPositionParams{
+			TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI},
+			Position:     loc.Range.Start,
+		},
+	}
+
+	items, err := r.server.PrepareCallHierarchy(r.ctx, params)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(items) == 0 {
+		t.Errorf("expected call hierarchy item to be returned for identifier at %v\n", loc.Range)
+	}
+
+	callLocation := protocol.Location{
+		URI:   items[0].URI,
+		Range: items[0].Range,
+	}
+	if callLocation != loc {
+		t.Errorf("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 len(incomingCalls) != 0 {
+		t.Errorf("expected no incoming calls but got %d", len(incomingCalls))
+	}
+	outgoingCalls, err := r.server.OutgoingCalls(r.ctx, &protocol.CallHierarchyOutgoingCallsParams{Item: items[0]})
+	if len(outgoingCalls) != 0 {
+		t.Errorf("expected no outgoing calls but got %d", len(outgoingCalls))
+	}
+}
+
 func (r *runner) CodeLens(t *testing.T, uri span.URI, want []protocol.CodeLens) {
 	if source.DetectLanguage("", uri.Filename()) != source.Mod {
 		return
diff --git a/internal/lsp/server_gen.go b/internal/lsp/server_gen.go
index 8604bb9..6f9eeb8 100644
--- a/internal/lsp/server_gen.go
+++ b/internal/lsp/server_gen.go
@@ -100,8 +100,8 @@
 	return s.implementation(ctx, params)
 }
 
-func (s *Server) IncomingCalls(context.Context, *protocol.CallHierarchyIncomingCallsParams) ([]protocol.CallHierarchyIncomingCall, error) {
-	return nil, notImplemented("IncomingCalls")
+func (s *Server) IncomingCalls(ctx context.Context, params *protocol.CallHierarchyIncomingCallsParams) ([]protocol.CallHierarchyIncomingCall, error) {
+	return s.incomingCalls(ctx, params)
 }
 
 func (s *Server) Initialize(ctx context.Context, params *protocol.ParamInitialize) (*protocol.InitializeResult, error) {
@@ -124,12 +124,12 @@
 	return nil, notImplemented("OnTypeFormatting")
 }
 
-func (s *Server) OutgoingCalls(context.Context, *protocol.CallHierarchyOutgoingCallsParams) ([]protocol.CallHierarchyOutgoingCall, error) {
-	return nil, notImplemented("OutgoingCalls")
+func (s *Server) OutgoingCalls(ctx context.Context, params *protocol.CallHierarchyOutgoingCallsParams) ([]protocol.CallHierarchyOutgoingCall, error) {
+	return s.outgoingCalls(ctx, params)
 }
 
-func (s *Server) PrepareCallHierarchy(context.Context, *protocol.CallHierarchyPrepareParams) ([]protocol.CallHierarchyItem, error) {
-	return nil, notImplemented("PrepareCallHierarchy")
+func (s *Server) PrepareCallHierarchy(ctx context.Context, params *protocol.CallHierarchyPrepareParams) ([]protocol.CallHierarchyItem, error) {
+	return s.prepareCallHierarchy(ctx, params)
 }
 
 func (s *Server) PrepareRename(ctx context.Context, params *protocol.PrepareRenameParams) (*protocol.Range, error) {
diff --git a/internal/lsp/source/call_hierarchy.go b/internal/lsp/source/call_hierarchy.go
new file mode 100644
index 0000000..6ec7a71
--- /dev/null
+++ b/internal/lsp/source/call_hierarchy.go
@@ -0,0 +1,68 @@
+// 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 source
+
+import (
+	"context"
+	"go/ast"
+
+	"golang.org/x/tools/internal/event"
+	"golang.org/x/tools/internal/lsp/debug/tag"
+	"golang.org/x/tools/internal/lsp/protocol"
+	errors "golang.org/x/xerrors"
+)
+
+// PrepareCallHierarchy returns an array of CallHierarchyItem for a file and the position within the file
+func PrepareCallHierarchy(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) ([]protocol.CallHierarchyItem, error) {
+	ctx, done := event.Start(ctx, "source.prepareCallHierarchy")
+	defer done()
+
+	identifier, err := Identifier(ctx, snapshot, fh, pos)
+	if err != nil {
+		if 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
+	}
+
+	// if identifier is not of type function
+	_, ok := identifier.Declaration.node.(*ast.FuncDecl)
+	if !ok {
+		event.Log(ctx, "invalid identifier type, expected funtion declaration", tag.Position.Of(pos))
+		return nil, nil
+	}
+	rng, err := identifier.Range()
+	if err != nil {
+		return nil, err
+	}
+	callHierarchyItem := protocol.CallHierarchyItem{
+		Name:           identifier.Name,
+		Kind:           protocol.Function,
+		Tags:           []protocol.SymbolTag{},
+		Detail:         "func()",
+		URI:            protocol.DocumentURI(fh.URI()),
+		Range:          rng,
+		SelectionRange: rng,
+	}
+	return []protocol.CallHierarchyItem{callHierarchyItem}, nil
+}
+
+// IncomingCalls returns an array of CallHierarchyIncomingCall for a file and the position within the file
+func IncomingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) ([]protocol.CallHierarchyIncomingCall, error) {
+	ctx, done := event.Start(ctx, "source.incomingCalls")
+	defer done()
+
+	return []protocol.CallHierarchyIncomingCall{}, nil
+}
+
+// OutgoingCalls returns an array of CallHierarchyOutgoingCall for a file and the position within the file
+func OutgoingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) ([]protocol.CallHierarchyOutgoingCall, error) {
+	ctx, done := event.Start(ctx, "source.outgoingCalls")
+	defer done()
+
+	return []protocol.CallHierarchyOutgoingCall{}, nil
+}
diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go
index 5db5d64..c8e4d99 100644
--- a/internal/lsp/source/source_test.go
+++ b/internal/lsp/source/source_test.go
@@ -96,6 +96,47 @@
 	}
 }
 
+func (r *runner) CallHierarchy(t *testing.T, spn span.Span, expectedCalls *tests.CallHierarchyResult) {
+	mapper, err := r.data.Mapper(spn.URI())
+	if err != nil {
+		t.Fatal(err)
+	}
+	loc, err := mapper.Location(spn)
+	if err != nil {
+		t.Fatalf("failed for %v: %v", spn, err)
+	}
+	fh, err := r.view.Snapshot().GetFile(r.ctx, spn.URI())
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	items, err := source.PrepareCallHierarchy(r.ctx, r.view.Snapshot(), fh, loc.Range.Start)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(items) == 0 {
+		t.Errorf("expected call hierarchy item to be returned for identifier at %v\n", loc.Range)
+	}
+
+	callLocation := protocol.Location{
+		URI:   items[0].URI,
+		Range: items[0].Range,
+	}
+	if callLocation != loc {
+		t.Errorf("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.view.Snapshot(), fh, loc.Range.Start)
+	if len(incomingCalls) != 0 {
+		t.Errorf("expected no incoming calls but got %d", len(incomingCalls))
+	}
+	outgoingCalls, err := source.OutgoingCalls(r.ctx, r.view.Snapshot(), fh, loc.Range.Start)
+	if len(outgoingCalls) != 0 {
+		t.Errorf("expected no outgoing calls but got %d", len(outgoingCalls))
+	}
+}
+
 func (r *runner) Diagnostics(t *testing.T, uri span.URI, want []*source.Diagnostic) {
 	fileID, got, err := source.FileDiagnostics(r.ctx, r.snapshot, uri)
 	if err != nil {
diff --git a/internal/lsp/testdata/lsp/primarymod/callhierarchy/callhierarchy.go b/internal/lsp/testdata/lsp/primarymod/callhierarchy/callhierarchy.go
new file mode 100644
index 0000000..b7c774b
--- /dev/null
+++ b/internal/lsp/testdata/lsp/primarymod/callhierarchy/callhierarchy.go
@@ -0,0 +1,35 @@
+// 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 main
+
+func a() { //@mark(funcA, "a")
+	d()
+}
+
+func b() { //@mark(funcB, "b")
+	d()
+}
+
+func c() { //@mark(funcC, "c")
+	d()
+}
+
+func d() { //@mark(funcD, "d"),incomingcalls("d", funcA, funcB, funcC),outgoingcalls("d", funcE, funcF, funcG)
+	e()
+	f()
+	g()
+}
+
+func e() {} //@mark(funcE, "e")
+
+func f() {} //@mark(funcF, "f")
+
+func g() {} //@mark(funcG, "g")
+
+func main() {
+	a()
+	b()
+	c()
+}
diff --git a/internal/lsp/testdata/lsp/summary.txt.golden b/internal/lsp/testdata/lsp/summary.txt.golden
index 91b872a..1b3e050 100644
--- a/internal/lsp/testdata/lsp/summary.txt.golden
+++ b/internal/lsp/testdata/lsp/summary.txt.golden
@@ -1,4 +1,5 @@
 -- summary --
+CallHierarchyCount = 1
 CodeLensCount = 4
 CompletionsCount = 241
 CompletionSnippetCount = 81
diff --git a/internal/lsp/testdata/missingdep/summary.txt.golden b/internal/lsp/testdata/missingdep/summary.txt.golden
index 4be7cf6..354b1db 100644
--- a/internal/lsp/testdata/missingdep/summary.txt.golden
+++ b/internal/lsp/testdata/missingdep/summary.txt.golden
@@ -1,4 +1,5 @@
 -- summary --
+CallHierarchyCount = 0
 CodeLensCount = 0
 CompletionsCount = 0
 CompletionSnippetCount = 0
diff --git a/internal/lsp/testdata/missingtwodep/summary.txt.golden b/internal/lsp/testdata/missingtwodep/summary.txt.golden
index ce246c0..5cf929d 100644
--- a/internal/lsp/testdata/missingtwodep/summary.txt.golden
+++ b/internal/lsp/testdata/missingtwodep/summary.txt.golden
@@ -1,4 +1,5 @@
 -- summary --
+CallHierarchyCount = 0
 CodeLensCount = 0
 CompletionsCount = 0
 CompletionSnippetCount = 0
diff --git a/internal/lsp/testdata/unused/summary.txt.golden b/internal/lsp/testdata/unused/summary.txt.golden
index 3f09a08..d77a6c5 100644
--- a/internal/lsp/testdata/unused/summary.txt.golden
+++ b/internal/lsp/testdata/unused/summary.txt.golden
@@ -1,4 +1,5 @@
 -- summary --
+CallHierarchyCount = 0
 CodeLensCount = 0
 CompletionsCount = 0
 CompletionSnippetCount = 0
diff --git a/internal/lsp/testdata/upgradedep/summary.txt.golden b/internal/lsp/testdata/upgradedep/summary.txt.golden
index 2719246..db65585 100644
--- a/internal/lsp/testdata/upgradedep/summary.txt.golden
+++ b/internal/lsp/testdata/upgradedep/summary.txt.golden
@@ -1,4 +1,5 @@
 -- summary --
+CallHierarchyCount = 0
 CodeLensCount = 2
 CompletionsCount = 0
 CompletionSnippetCount = 0
diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go
index c3178c3..bfdecd2 100644
--- a/internal/lsp/tests/tests.go
+++ b/internal/lsp/tests/tests.go
@@ -43,6 +43,7 @@
 
 var UpdateGolden = flag.Bool("golden", false, "Update golden files")
 
+type CallHierarchy map[span.Span]*CallHierarchyResult
 type CodeLens map[span.URI][]protocol.CodeLens
 type Diagnostics map[span.URI][]*source.Diagnostic
 type CompletionItems map[token.Pos]*source.CompletionItem
@@ -74,6 +75,7 @@
 type Data struct {
 	Config                        packages.Config
 	Exported                      *packagestest.Exported
+	CallHierarchy                 CallHierarchy
 	CodeLens                      CodeLens
 	Diagnostics                   Diagnostics
 	CompletionItems               CompletionItems
@@ -117,6 +119,7 @@
 }
 
 type Tests interface {
+	CallHierarchy(*testing.T, span.Span, *CallHierarchyResult)
 	CodeLens(*testing.T, span.URI, []protocol.CodeLens)
 	Diagnostics(*testing.T, span.URI, []*source.Diagnostic)
 	Completion(*testing.T, span.Span, Completion, CompletionItems)
@@ -197,6 +200,10 @@
 	PlaceholderSnippet string
 }
 
+type CallHierarchyResult struct {
+	IncomingCalls, OutgoingCalls []span.Span
+}
+
 type Link struct {
 	Src          span.Span
 	Target       string
@@ -274,6 +281,7 @@
 	var data []*Data
 	for _, folder := range folders {
 		datum := &Data{
+			CallHierarchy:                 make(CallHierarchy),
 			CodeLens:                      make(CodeLens),
 			Diagnostics:                   make(Diagnostics),
 			CompletionItems:               make(CompletionItems),
@@ -425,6 +433,8 @@
 			"link":            datum.collectLinks,
 			"suggestedfix":    datum.collectSuggestedFixes,
 			"extractfunc":     datum.collectFunctionExtractions,
+			"incomingcalls":   datum.collectIncomingCalls,
+			"outgoingcalls":   datum.collectOutgoingCalls,
 		}); err != nil {
 			t.Fatal(err)
 		}
@@ -495,6 +505,16 @@
 		}
 	}
 
+	t.Run("CallHierarchy", func(t *testing.T) {
+		t.Helper()
+		for spn, callHierarchyResult := range data.CallHierarchy {
+			t.Run(SpanName(spn), func(t *testing.T) {
+				t.Helper()
+				tests.CallHierarchy(t, spn, callHierarchyResult)
+			})
+		}
+	})
+
 	t.Run("Completion", func(t *testing.T) {
 		t.Helper()
 		eachCompletion(t, data.Completions, tests.Completion)
@@ -807,6 +827,7 @@
 		return count
 	}
 
+	fmt.Fprintf(buf, "CallHierarchyCount = %v\n", len(data.CallHierarchy))
 	fmt.Fprintf(buf, "CodeLensCount = %v\n", countCodeLens(data.CodeLens))
 	fmt.Fprintf(buf, "CompletionsCount = %v\n", countCompletions(data.Completions))
 	fmt.Fprintf(buf, "CompletionSnippetCount = %v\n", snippetCount)
@@ -1060,6 +1081,26 @@
 	data.Implementations[src] = targets
 }
 
+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,
+		}
+	}
+}
+
+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,
+		}
+	}
+}
+
 func (data *Data) collectHoverDefinitions(src, target span.Span) {
 	data.Definitions[src] = Definition{
 		Src:       src,