// 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" "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" 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's declaration is not of type function declaration _, ok := identifier.Declaration.node.(*ast.FuncDecl) if !ok { event.Log(ctx, "invalid identifier declaration, 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: fmt.Sprintf("%s %s", identifier.pkg.PkgPath(), filepath.Base(fh.URI().Filename())), 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() 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 } 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 func OutgoingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) ([]protocol.CallHierarchyOutgoingCall, error) { ctx, done := event.Start(ctx, "source.outgoingCalls") defer done() // TODO: Remove this once the context is used. _ = ctx // avoid staticcheck SA4006 warning 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 }