| // 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" |
| "errors" |
| "fmt" |
| "go/ast" |
| "go/token" |
| "go/types" |
| "path/filepath" |
| |
| "golang.org/x/tools/go/ast/astutil" |
| "golang.org/x/tools/gopls/internal/lsp/protocol" |
| "golang.org/x/tools/gopls/internal/span" |
| "golang.org/x/tools/internal/event" |
| "golang.org/x/tools/internal/event/tag" |
| ) |
| |
| // 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 |
| } |
| |
| // The identifier can be nil if it is an import spec. |
| if identifier == nil || identifier.Declaration.obj == nil { |
| return nil, nil |
| } |
| |
| 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.Pos(), nameIdent.End() |
| if funcLit != nil { |
| nameStart, nameEnd = funcLit.Type.Func, funcLit.Type.Params.Pos() |
| kind = protocol.Function |
| } |
| rng, err := NewMappedRange(pgf.Tok, 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 |
| } |
| node := identifier.Declaration.node |
| if node == nil { |
| return nil, nil |
| } |
| if len(identifier.Declaration.MappedRange) == 0 { |
| return nil, nil |
| } |
| declMappedRange := identifier.Declaration.MappedRange[0] |
| // TODO(adonovan): avoid Fileset.File call by somehow getting at |
| // declMappedRange.spanRange.TokFile, or making Identifier retain the |
| // token.File of the identifier and its declaration, since it looks up both anyway. |
| tokFile := identifier.pkg.FileSet().File(node.Pos()) |
| if tokFile == nil { |
| return nil, fmt.Errorf("no file for position") |
| } |
| callExprs, err := collectCallExpressions(tokFile, declMappedRange.m, node) |
| if err != nil { |
| return nil, err |
| } |
| |
| return toProtocolOutgoingCalls(ctx, snapshot, fh, callExprs) |
| } |
| |
| // collectCallExpressions collects call expression ranges inside a function. |
| func collectCallExpressions(tokFile *token.File, 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 |
| case *ast.FuncLit: |
| // while we don't add the function literal as an 'outgoing' call |
| // we still want to traverse into it |
| return true |
| 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(tokFile, 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, defined by "same declaration |
| // AST node & same identifier name" to provide a unique identifier key even when |
| // the func is declared in a struct or interface. |
| type key struct { |
| decl ast.Node |
| name string |
| } |
| outgoingCalls := map[key]*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[key{identifier.Declaration.node, identifier.Name}]; 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[key{identifier.Declaration.node, identifier.Name}] = &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 |
| } |