1
0
mirror of https://github.com/golang/go synced 2024-09-30 12:28:35 -06:00

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>
This commit is contained in:
Danish Dua 2020-08-03 14:16:44 -04:00
parent f15f0bfc61
commit a5d4502270
17 changed files with 459 additions and 7 deletions

View File

@ -87,6 +87,10 @@ func Requests(m string) interface{} {
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 @@ func Responses(m string) []interface{} {
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 @@ var fromMethod = map[string]Msgtype{
"textDocument/rename": Mreq | Mcl,
"textDocument/prepareRename": Mreq | Mcl,
"textDocument/foldingRange": Mreq | Mcl,
"callHierarchy/incomingCalls": Mreq | Mcl,
"callHierarchy/outgoingCalls": Mreq | Mcl,
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -175,6 +175,7 @@ func (app *Application) mainCommands() []tool.Application {
func (app *Application) featureCommands() []tool.Application {
return []tool.Application{
&callHierarchy{app: app},
&check{app: app},
&definition{app: app},
&foldingRanges{app: app},

View File

@ -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")
}

View File

@ -85,7 +85,8 @@ func (s *Server) initialize(ctx context.Context, params *protocol.ParamInitializ
return &protocol.InitializeResult{
Capabilities: protocol.ServerCapabilities{
CodeActionProvider: codeActionProvider,
CallHierarchyProvider: true,
CodeActionProvider: codeActionProvider,
CompletionProvider: protocol.CompletionOptions{
TriggerCharacters: []string{"."},
},

View File

@ -99,6 +99,50 @@ func testLSP(t *testing.T, exporter packagestest.Exporter) {
}
}
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

View File

@ -100,8 +100,8 @@ func (s *Server) Implementation(ctx context.Context, params *protocol.Implementa
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 @@ func (s *Server) OnTypeFormatting(context.Context, *protocol.DocumentOnTypeForma
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) {

View File

@ -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
}

View File

@ -96,6 +96,47 @@ func testSource(t *testing.T, exporter packagestest.Exporter) {
}
}
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 {

View File

@ -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()
}

View File

@ -1,4 +1,5 @@
-- summary --
CallHierarchyCount = 1
CodeLensCount = 4
CompletionsCount = 241
CompletionSnippetCount = 81

View File

@ -1,4 +1,5 @@
-- summary --
CallHierarchyCount = 0
CodeLensCount = 0
CompletionsCount = 0
CompletionSnippetCount = 0

View File

@ -1,4 +1,5 @@
-- summary --
CallHierarchyCount = 0
CodeLensCount = 0
CompletionsCount = 0
CompletionSnippetCount = 0

View File

@ -1,4 +1,5 @@
-- summary --
CallHierarchyCount = 0
CodeLensCount = 0
CompletionsCount = 0
CompletionSnippetCount = 0

View File

@ -1,4 +1,5 @@
-- summary --
CallHierarchyCount = 0
CodeLensCount = 2
CompletionsCount = 0
CompletionSnippetCount = 0

View File

@ -43,6 +43,7 @@ const (
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 Links map[span.URI][]Link
type Data struct {
Config packages.Config
Exported *packagestest.Exported
CallHierarchy CallHierarchy
CodeLens CodeLens
Diagnostics Diagnostics
CompletionItems CompletionItems
@ -117,6 +119,7 @@ type Data struct {
}
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 @@ type CompletionSnippet struct {
PlaceholderSnippet string
}
type CallHierarchyResult struct {
IncomingCalls, OutgoingCalls []span.Span
}
type Link struct {
Src span.Span
Target string
@ -274,6 +281,7 @@ func Load(t testing.TB, exporter packagestest.Exporter, dir string) []*Data {
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 @@ func Load(t testing.TB, exporter packagestest.Exporter, dir string) []*Data {
"link": datum.collectLinks,
"suggestedfix": datum.collectSuggestedFixes,
"extractfunc": datum.collectFunctionExtractions,
"incomingcalls": datum.collectIncomingCalls,
"outgoingcalls": datum.collectOutgoingCalls,
}); err != nil {
t.Fatal(err)
}
@ -495,6 +505,16 @@ func Run(t *testing.T, tests Tests, data *Data) {
}
}
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 @@ func checkData(t *testing.T, data *Data) {
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 @@ func (data *Data) collectImplementations(src span.Span, targets []span.Span) {
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,