mirror of
https://github.com/golang/go
synced 2024-11-05 11:46:12 -07:00
b793a1359e
* Adds outgoing calls call hierarchy for function declarations to gopls. Returns all call ranges and call items for functions/literals being called. * Adds tests for outgoing call. * Updates cmd to account for call ranges and call items being in different files for outgoing calls. * Updates prepare call hierarchy to return declaration as root instead of cursor position. Example: Example shows https://github.com/golang/tools/blob/master/internal/lsp/source/call_hierarchy.go Show Call Hierarchy View: https://imgur.com/a/DA5vc6l Peek Call Hierarchy View: https://imgur.com/a/fuiG0Be Note: * While incoming calls for a function defined in an interface return references to that function, outgoing calls don't return anything since we don't know what implementation to return outgoing calls for.* Outgoing calls to function literals show as variable name used to define the literal, compared to <scope>.func() for incoming calls. Change-Id: Ib8afbd8617675d12952db0b80170ada5988e90ab Reviewed-on: https://go-review.googlesource.com/c/tools/+/248537 Run-TryBot: Danish Dua <danishdua@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Heschi Kreinick <heschi@google.com>
294 lines
9.3 KiB
Go
294 lines
9.3 KiB
Go
// 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"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/token"
|
|
"go/types"
|
|
"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"
|
|
"golang.org/x/tools/internal/span"
|
|
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) || errors.Is(err, errNoObjectFound) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
if _, ok := identifier.Declaration.obj.Type().Underlying().(*types.Signature); !ok {
|
|
return nil, nil
|
|
}
|
|
|
|
if len(identifier.Declaration.MappedRange) == 0 {
|
|
return nil, nil
|
|
}
|
|
declMappedRange := identifier.Declaration.MappedRange[0]
|
|
rng, err := declMappedRange.Range()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
callHierarchyItem := protocol.CallHierarchyItem{
|
|
Name: identifier.Name,
|
|
Kind: protocol.Function,
|
|
Tags: []protocol.SymbolTag{},
|
|
Detail: fmt.Sprintf("%s • %s", identifier.Declaration.obj.Pkg().Path(), filepath.Base(declMappedRange.URI().Filename())),
|
|
URI: protocol.DocumentURI(declMappedRange.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()
|
|
|
|
refs, err := References(ctx, snapshot, fh, pos, false)
|
|
if err != nil {
|
|
if errors.Is(err, ErrNoIdentFound) || errors.Is(err, errNoObjectFound) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
return toProtocolIncomingCalls(ctx, snapshot, refs)
|
|
}
|
|
|
|
// 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.Location]*protocol.CallHierarchyIncomingCall{}
|
|
for _, ref := range refs {
|
|
refRange, err := ref.Range()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
callItem, err := enclosingNodeCallItem(snapshot, ref.pkg, ref.URI(), ref.ident.NamePos)
|
|
if err != nil {
|
|
event.Error(ctx, "error getting enclosing node", err, tag.Method.Of(ref.Name))
|
|
continue
|
|
}
|
|
loc := protocol.Location{
|
|
URI: callItem.URI,
|
|
Range: callItem.Range,
|
|
}
|
|
|
|
if incomingCall, ok := incomingCalls[loc]; ok {
|
|
incomingCall.FromRanges = append(incomingCall.FromRanges, refRange)
|
|
continue
|
|
}
|
|
incomingCalls[loc] = &protocol.CallHierarchyIncomingCall{
|
|
From: callItem,
|
|
FromRanges: []protocol.Range{refRange},
|
|
}
|
|
}
|
|
|
|
incomingCallItems := make([]protocol.CallHierarchyIncomingCall, 0, len(incomingCalls))
|
|
for _, callItem := range incomingCalls {
|
|
incomingCallItems = append(incomingCallItems, *callItem)
|
|
}
|
|
return incomingCallItems, nil
|
|
}
|
|
|
|
// enclosingNodeCallItem creates a CallHierarchyItem representing the function call at pos
|
|
func enclosingNodeCallItem(snapshot Snapshot, pkg Package, uri span.URI, pos token.Pos) (protocol.CallHierarchyItem, error) {
|
|
pgf, err := pkg.File(uri)
|
|
if err != nil {
|
|
return protocol.CallHierarchyItem{}, 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, pos, pos)
|
|
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
|
|
kind := protocol.Package
|
|
if funcDecl != nil {
|
|
nameIdent = funcDecl.Name
|
|
kind = protocol.Function
|
|
}
|
|
|
|
nameStart, nameEnd := nameIdent.NamePos, nameIdent.NamePos+token.Pos(len(nameIdent.Name))
|
|
if funcLit != nil {
|
|
nameStart, nameEnd = funcLit.Type.Func, funcLit.Type.Params.Pos()
|
|
kind = protocol.Function
|
|
}
|
|
rng, err := newMappedRange(snapshot.FileSet(), pgf.Mapper, nameStart, nameEnd).Range()
|
|
if err != nil {
|
|
return protocol.CallHierarchyItem{}, err
|
|
}
|
|
|
|
name := nameIdent.Name
|
|
for i := 0; i < litCount; i++ {
|
|
name += ".func()"
|
|
}
|
|
|
|
return protocol.CallHierarchyItem{
|
|
Name: name,
|
|
Kind: kind,
|
|
Tags: []protocol.SymbolTag{},
|
|
Detail: fmt.Sprintf("%s • %s", pkg.PkgPath(), filepath.Base(uri.Filename())),
|
|
URI: protocol.DocumentURI(uri),
|
|
Range: rng,
|
|
SelectionRange: rng,
|
|
}, 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()
|
|
|
|
identifier, err := Identifier(ctx, snapshot, fh, pos)
|
|
if err != nil {
|
|
if errors.Is(err, ErrNoIdentFound) || errors.Is(err, errNoObjectFound) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
if _, ok := identifier.Declaration.obj.Type().Underlying().(*types.Signature); !ok {
|
|
return nil, nil
|
|
}
|
|
|
|
if len(identifier.Declaration.MappedRange) == 0 {
|
|
return nil, nil
|
|
}
|
|
declMappedRange := identifier.Declaration.MappedRange[0]
|
|
callExprs, err := collectCallExpressions(snapshot.FileSet(), declMappedRange.m, identifier.Declaration.node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return toProtocolOutgoingCalls(ctx, snapshot, fh, callExprs)
|
|
}
|
|
|
|
// collectCallExpressions collects call expression ranges inside a function.
|
|
func collectCallExpressions(fset *token.FileSet, mapper *protocol.ColumnMapper, node ast.Node) ([]protocol.Range, error) {
|
|
type callPos struct {
|
|
start, end token.Pos
|
|
}
|
|
callPositions := []callPos{}
|
|
|
|
ast.Inspect(node, func(n ast.Node) bool {
|
|
if call, ok := n.(*ast.CallExpr); ok {
|
|
var start, end token.Pos
|
|
switch n := call.Fun.(type) {
|
|
case *ast.SelectorExpr:
|
|
start, end = n.Sel.NamePos, call.Lparen
|
|
case *ast.Ident:
|
|
start, end = n.NamePos, call.Lparen
|
|
default:
|
|
// ignore any other kind of call expressions
|
|
// for ex: direct function literal calls since that's not an 'outgoing' call
|
|
return false
|
|
}
|
|
callPositions = append(callPositions, callPos{start: start, end: end})
|
|
}
|
|
return true
|
|
})
|
|
|
|
callRanges := []protocol.Range{}
|
|
for _, call := range callPositions {
|
|
callRange, err := newMappedRange(fset, mapper, call.start, call.end).Range()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
callRanges = append(callRanges, callRange)
|
|
}
|
|
return callRanges, nil
|
|
}
|
|
|
|
// toProtocolOutgoingCalls returns an array of protocol.CallHierarchyOutgoingCall for ast call expressions.
|
|
// Calls to the same function are assigned to the same declaration.
|
|
func toProtocolOutgoingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, callRanges []protocol.Range) ([]protocol.CallHierarchyOutgoingCall, error) {
|
|
// multiple calls could be made to the same function
|
|
var outgoingCalls = map[ast.Node]*protocol.CallHierarchyOutgoingCall{}
|
|
for _, callRange := range callRanges {
|
|
identifier, err := Identifier(ctx, snapshot, fh, callRange.Start)
|
|
if err != nil {
|
|
if errors.Is(err, ErrNoIdentFound) || errors.Is(err, errNoObjectFound) {
|
|
continue
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
// ignore calls to builtin functions
|
|
if identifier.Declaration.obj.Pkg() == nil {
|
|
continue
|
|
}
|
|
|
|
if outgoingCall, ok := outgoingCalls[identifier.Declaration.node]; ok {
|
|
outgoingCall.FromRanges = append(outgoingCall.FromRanges, callRange)
|
|
continue
|
|
}
|
|
|
|
if len(identifier.Declaration.MappedRange) == 0 {
|
|
continue
|
|
}
|
|
declMappedRange := identifier.Declaration.MappedRange[0]
|
|
rng, err := declMappedRange.Range()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
outgoingCalls[identifier.Declaration.node] = &protocol.CallHierarchyOutgoingCall{
|
|
To: protocol.CallHierarchyItem{
|
|
Name: identifier.Name,
|
|
Kind: protocol.Function,
|
|
Tags: []protocol.SymbolTag{},
|
|
Detail: fmt.Sprintf("%s • %s", identifier.Declaration.obj.Pkg().Path(), filepath.Base(declMappedRange.URI().Filename())),
|
|
URI: protocol.DocumentURI(declMappedRange.URI()),
|
|
Range: rng,
|
|
SelectionRange: rng,
|
|
},
|
|
FromRanges: []protocol.Range{callRange},
|
|
}
|
|
}
|
|
|
|
outgoingCallItems := make([]protocol.CallHierarchyOutgoingCall, 0, len(outgoingCalls))
|
|
for _, callItem := range outgoingCalls {
|
|
outgoingCallItems = append(outgoingCallItems, *callItem)
|
|
}
|
|
return outgoingCallItems, nil
|
|
}
|