github.com/april1989/origin-go-tools@v0.0.32/internal/lsp/source/call_hierarchy.go (about)

     1  // Copyright 2020 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package source
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"go/ast"
    11  	"go/token"
    12  	"go/types"
    13  	"path/filepath"
    14  
    15  	"github.com/april1989/origin-go-tools/go/ast/astutil"
    16  	"github.com/april1989/origin-go-tools/internal/event"
    17  	"github.com/april1989/origin-go-tools/internal/lsp/debug/tag"
    18  	"github.com/april1989/origin-go-tools/internal/lsp/protocol"
    19  	"github.com/april1989/origin-go-tools/internal/span"
    20  	errors "golang.org/x/xerrors"
    21  )
    22  
    23  // PrepareCallHierarchy returns an array of CallHierarchyItem for a file and the position within the file.
    24  func PrepareCallHierarchy(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) ([]protocol.CallHierarchyItem, error) {
    25  	ctx, done := event.Start(ctx, "source.PrepareCallHierarchy")
    26  	defer done()
    27  
    28  	identifier, err := Identifier(ctx, snapshot, fh, pos)
    29  	if err != nil {
    30  		if errors.Is(err, ErrNoIdentFound) || errors.Is(err, errNoObjectFound) {
    31  			return nil, nil
    32  		}
    33  		return nil, err
    34  	}
    35  
    36  	if _, ok := identifier.Declaration.obj.Type().Underlying().(*types.Signature); !ok {
    37  		return nil, nil
    38  	}
    39  
    40  	if len(identifier.Declaration.MappedRange) == 0 {
    41  		return nil, nil
    42  	}
    43  	declMappedRange := identifier.Declaration.MappedRange[0]
    44  	rng, err := declMappedRange.Range()
    45  	if err != nil {
    46  		return nil, err
    47  	}
    48  
    49  	callHierarchyItem := protocol.CallHierarchyItem{
    50  		Name:           identifier.Name,
    51  		Kind:           protocol.Function,
    52  		Tags:           []protocol.SymbolTag{},
    53  		Detail:         fmt.Sprintf("%s • %s", identifier.Declaration.obj.Pkg().Path(), filepath.Base(declMappedRange.URI().Filename())),
    54  		URI:            protocol.DocumentURI(declMappedRange.URI()),
    55  		Range:          rng,
    56  		SelectionRange: rng,
    57  	}
    58  	return []protocol.CallHierarchyItem{callHierarchyItem}, nil
    59  }
    60  
    61  // IncomingCalls returns an array of CallHierarchyIncomingCall for a file and the position within the file.
    62  func IncomingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) ([]protocol.CallHierarchyIncomingCall, error) {
    63  	ctx, done := event.Start(ctx, "source.IncomingCalls")
    64  	defer done()
    65  
    66  	refs, err := References(ctx, snapshot, fh, pos, false)
    67  	if err != nil {
    68  		if errors.Is(err, ErrNoIdentFound) || errors.Is(err, errNoObjectFound) {
    69  			return nil, nil
    70  		}
    71  		return nil, err
    72  	}
    73  
    74  	return toProtocolIncomingCalls(ctx, snapshot, refs)
    75  }
    76  
    77  // toProtocolIncomingCalls returns an array of protocol.CallHierarchyIncomingCall for ReferenceInfo's.
    78  // References inside same enclosure are assigned to the same enclosing function.
    79  func toProtocolIncomingCalls(ctx context.Context, snapshot Snapshot, refs []*ReferenceInfo) ([]protocol.CallHierarchyIncomingCall, error) {
    80  	// an enclosing node could have multiple calls to a reference, we only show the enclosure
    81  	// once in the result but highlight all calls using FromRanges (ranges at which the calls occur)
    82  	var incomingCalls = map[protocol.Location]*protocol.CallHierarchyIncomingCall{}
    83  	for _, ref := range refs {
    84  		refRange, err := ref.Range()
    85  		if err != nil {
    86  			return nil, err
    87  		}
    88  
    89  		callItem, err := enclosingNodeCallItem(snapshot, ref.pkg, ref.URI(), ref.ident.NamePos)
    90  		if err != nil {
    91  			event.Error(ctx, "error getting enclosing node", err, tag.Method.Of(ref.Name))
    92  			continue
    93  		}
    94  		loc := protocol.Location{
    95  			URI:   callItem.URI,
    96  			Range: callItem.Range,
    97  		}
    98  
    99  		if incomingCall, ok := incomingCalls[loc]; ok {
   100  			incomingCall.FromRanges = append(incomingCall.FromRanges, refRange)
   101  			continue
   102  		}
   103  		incomingCalls[loc] = &protocol.CallHierarchyIncomingCall{
   104  			From:       callItem,
   105  			FromRanges: []protocol.Range{refRange},
   106  		}
   107  	}
   108  
   109  	incomingCallItems := make([]protocol.CallHierarchyIncomingCall, 0, len(incomingCalls))
   110  	for _, callItem := range incomingCalls {
   111  		incomingCallItems = append(incomingCallItems, *callItem)
   112  	}
   113  	return incomingCallItems, nil
   114  }
   115  
   116  // enclosingNodeCallItem creates a CallHierarchyItem representing the function call at pos
   117  func enclosingNodeCallItem(snapshot Snapshot, pkg Package, uri span.URI, pos token.Pos) (protocol.CallHierarchyItem, error) {
   118  	pgf, err := pkg.File(uri)
   119  	if err != nil {
   120  		return protocol.CallHierarchyItem{}, err
   121  	}
   122  
   123  	var funcDecl *ast.FuncDecl
   124  	var funcLit *ast.FuncLit // innermost function literal
   125  	var litCount int
   126  	// Find the enclosing function, if any, and the number of func literals in between.
   127  	path, _ := astutil.PathEnclosingInterval(pgf.File, pos, pos)
   128  outer:
   129  	for _, node := range path {
   130  		switch n := node.(type) {
   131  		case *ast.FuncDecl:
   132  			funcDecl = n
   133  			break outer
   134  		case *ast.FuncLit:
   135  			litCount++
   136  			if litCount > 1 {
   137  				continue
   138  			}
   139  			funcLit = n
   140  		}
   141  	}
   142  
   143  	nameIdent := path[len(path)-1].(*ast.File).Name
   144  	kind := protocol.Package
   145  	if funcDecl != nil {
   146  		nameIdent = funcDecl.Name
   147  		kind = protocol.Function
   148  	}
   149  
   150  	nameStart, nameEnd := nameIdent.NamePos, nameIdent.NamePos+token.Pos(len(nameIdent.Name))
   151  	if funcLit != nil {
   152  		nameStart, nameEnd = funcLit.Type.Func, funcLit.Type.Params.Pos()
   153  		kind = protocol.Function
   154  	}
   155  	rng, err := newMappedRange(snapshot.FileSet(), pgf.Mapper, nameStart, nameEnd).Range()
   156  	if err != nil {
   157  		return protocol.CallHierarchyItem{}, err
   158  	}
   159  
   160  	name := nameIdent.Name
   161  	for i := 0; i < litCount; i++ {
   162  		name += ".func()"
   163  	}
   164  
   165  	return protocol.CallHierarchyItem{
   166  		Name:           name,
   167  		Kind:           kind,
   168  		Tags:           []protocol.SymbolTag{},
   169  		Detail:         fmt.Sprintf("%s • %s", pkg.PkgPath(), filepath.Base(uri.Filename())),
   170  		URI:            protocol.DocumentURI(uri),
   171  		Range:          rng,
   172  		SelectionRange: rng,
   173  	}, nil
   174  }
   175  
   176  // OutgoingCalls returns an array of CallHierarchyOutgoingCall for a file and the position within the file.
   177  func OutgoingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) ([]protocol.CallHierarchyOutgoingCall, error) {
   178  	ctx, done := event.Start(ctx, "source.OutgoingCalls")
   179  	defer done()
   180  
   181  	identifier, err := Identifier(ctx, snapshot, fh, pos)
   182  	if err != nil {
   183  		if errors.Is(err, ErrNoIdentFound) || errors.Is(err, errNoObjectFound) {
   184  			return nil, nil
   185  		}
   186  		return nil, err
   187  	}
   188  
   189  	if _, ok := identifier.Declaration.obj.Type().Underlying().(*types.Signature); !ok {
   190  		return nil, nil
   191  	}
   192  
   193  	if len(identifier.Declaration.MappedRange) == 0 {
   194  		return nil, nil
   195  	}
   196  	declMappedRange := identifier.Declaration.MappedRange[0]
   197  	callExprs, err := collectCallExpressions(snapshot.FileSet(), declMappedRange.m, identifier.Declaration.node)
   198  	if err != nil {
   199  		return nil, err
   200  	}
   201  
   202  	return toProtocolOutgoingCalls(ctx, snapshot, fh, callExprs)
   203  }
   204  
   205  // collectCallExpressions collects call expression ranges inside a function.
   206  func collectCallExpressions(fset *token.FileSet, mapper *protocol.ColumnMapper, node ast.Node) ([]protocol.Range, error) {
   207  	type callPos struct {
   208  		start, end token.Pos
   209  	}
   210  	callPositions := []callPos{}
   211  
   212  	ast.Inspect(node, func(n ast.Node) bool {
   213  		if call, ok := n.(*ast.CallExpr); ok {
   214  			var start, end token.Pos
   215  			switch n := call.Fun.(type) {
   216  			case *ast.SelectorExpr:
   217  				start, end = n.Sel.NamePos, call.Lparen
   218  			case *ast.Ident:
   219  				start, end = n.NamePos, call.Lparen
   220  			default:
   221  				// ignore any other kind of call expressions
   222  				// for ex: direct function literal calls since that's not an 'outgoing' call
   223  				return false
   224  			}
   225  			callPositions = append(callPositions, callPos{start: start, end: end})
   226  		}
   227  		return true
   228  	})
   229  
   230  	callRanges := []protocol.Range{}
   231  	for _, call := range callPositions {
   232  		callRange, err := newMappedRange(fset, mapper, call.start, call.end).Range()
   233  		if err != nil {
   234  			return nil, err
   235  		}
   236  		callRanges = append(callRanges, callRange)
   237  	}
   238  	return callRanges, nil
   239  }
   240  
   241  // toProtocolOutgoingCalls returns an array of protocol.CallHierarchyOutgoingCall for ast call expressions.
   242  // Calls to the same function are assigned to the same declaration.
   243  func toProtocolOutgoingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, callRanges []protocol.Range) ([]protocol.CallHierarchyOutgoingCall, error) {
   244  	// multiple calls could be made to the same function
   245  	var outgoingCalls = map[ast.Node]*protocol.CallHierarchyOutgoingCall{}
   246  	for _, callRange := range callRanges {
   247  		identifier, err := Identifier(ctx, snapshot, fh, callRange.Start)
   248  		if err != nil {
   249  			if errors.Is(err, ErrNoIdentFound) || errors.Is(err, errNoObjectFound) {
   250  				continue
   251  			}
   252  			return nil, err
   253  		}
   254  
   255  		// ignore calls to builtin functions
   256  		if identifier.Declaration.obj.Pkg() == nil {
   257  			continue
   258  		}
   259  
   260  		if outgoingCall, ok := outgoingCalls[identifier.Declaration.node]; ok {
   261  			outgoingCall.FromRanges = append(outgoingCall.FromRanges, callRange)
   262  			continue
   263  		}
   264  
   265  		if len(identifier.Declaration.MappedRange) == 0 {
   266  			continue
   267  		}
   268  		declMappedRange := identifier.Declaration.MappedRange[0]
   269  		rng, err := declMappedRange.Range()
   270  		if err != nil {
   271  			return nil, err
   272  		}
   273  
   274  		outgoingCalls[identifier.Declaration.node] = &protocol.CallHierarchyOutgoingCall{
   275  			To: protocol.CallHierarchyItem{
   276  				Name:           identifier.Name,
   277  				Kind:           protocol.Function,
   278  				Tags:           []protocol.SymbolTag{},
   279  				Detail:         fmt.Sprintf("%s • %s", identifier.Declaration.obj.Pkg().Path(), filepath.Base(declMappedRange.URI().Filename())),
   280  				URI:            protocol.DocumentURI(declMappedRange.URI()),
   281  				Range:          rng,
   282  				SelectionRange: rng,
   283  			},
   284  			FromRanges: []protocol.Range{callRange},
   285  		}
   286  	}
   287  
   288  	outgoingCallItems := make([]protocol.CallHierarchyOutgoingCall, 0, len(outgoingCalls))
   289  	for _, callItem := range outgoingCalls {
   290  		outgoingCallItems = append(outgoingCallItems, *callItem)
   291  	}
   292  	return outgoingCallItems, nil
   293  }