1104 lines
31 KiB
Go
1104 lines
31 KiB
Go
package ls
|
|
|
|
import (
|
|
"context"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/microsoft/typescript-go/internal/ast"
|
|
"github.com/microsoft/typescript-go/internal/astnav"
|
|
"github.com/microsoft/typescript-go/internal/checker"
|
|
"github.com/microsoft/typescript-go/internal/compiler"
|
|
"github.com/microsoft/typescript-go/internal/core"
|
|
"github.com/microsoft/typescript-go/internal/debug"
|
|
"github.com/microsoft/typescript-go/internal/ls/lsconv"
|
|
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
|
"github.com/microsoft/typescript-go/internal/printer"
|
|
"github.com/microsoft/typescript-go/internal/scanner"
|
|
)
|
|
|
|
type CallHierarchyDeclaration = *ast.Node
|
|
|
|
// Indictates whether a node is named function or class expression.
|
|
func isNamedExpression(node *ast.Node) bool {
|
|
if node == nil {
|
|
return false
|
|
}
|
|
if !ast.IsFunctionExpression(node) && !ast.IsClassExpression(node) {
|
|
return false
|
|
}
|
|
name := node.Name()
|
|
return name != nil && ast.IsIdentifier(name)
|
|
}
|
|
|
|
func isVariableLike(node *ast.Node) bool {
|
|
if node == nil {
|
|
return false
|
|
}
|
|
return ast.IsPropertyDeclaration(node) || ast.IsVariableDeclaration(node)
|
|
}
|
|
|
|
// Indicates whether a node is a function, arrow, or class expression assigned to a constant variable or class property.
|
|
func isAssignedExpression(node *ast.Node) bool {
|
|
if node == nil {
|
|
return false
|
|
}
|
|
if !(ast.IsFunctionExpression(node) || ast.IsArrowFunction(node) || ast.IsClassExpression(node)) {
|
|
return false
|
|
}
|
|
if node.Name() != nil {
|
|
return false
|
|
}
|
|
parent := node.Parent
|
|
if !isVariableLike(parent) {
|
|
return false
|
|
}
|
|
|
|
if parent.Initializer() != node {
|
|
return false
|
|
}
|
|
|
|
name := parent.Name()
|
|
if !ast.IsIdentifier(name) {
|
|
return false
|
|
}
|
|
|
|
return (ast.GetCombinedNodeFlags(parent)&ast.NodeFlagsConst) != 0 || ast.IsPropertyDeclaration(parent)
|
|
}
|
|
|
|
// Indicates whether a node could possibly be a call hierarchy declaration.
|
|
//
|
|
// See `resolveCallHierarchyDeclaration` for the specific rules.
|
|
func isPossibleCallHierarchyDeclaration(node *ast.Node) bool {
|
|
if node == nil {
|
|
return false
|
|
}
|
|
return ast.IsSourceFile(node) ||
|
|
ast.IsModuleDeclaration(node) ||
|
|
ast.IsFunctionDeclaration(node) ||
|
|
ast.IsFunctionExpression(node) ||
|
|
ast.IsClassDeclaration(node) ||
|
|
ast.IsClassExpression(node) ||
|
|
ast.IsClassStaticBlockDeclaration(node) ||
|
|
ast.IsMethodDeclaration(node) ||
|
|
ast.IsMethodSignatureDeclaration(node) ||
|
|
ast.IsGetAccessorDeclaration(node) ||
|
|
ast.IsSetAccessorDeclaration(node)
|
|
}
|
|
|
|
// Indicates whether a node is a valid a call hierarchy declaration.
|
|
//
|
|
// See `resolveCallHierarchyDeclaration` for the specific rules.
|
|
func isValidCallHierarchyDeclaration(node *ast.Node) bool {
|
|
if node == nil {
|
|
return false
|
|
}
|
|
|
|
if ast.IsSourceFile(node) {
|
|
return true
|
|
}
|
|
|
|
if ast.IsModuleDeclaration(node) {
|
|
return ast.IsIdentifier(node.Name())
|
|
}
|
|
|
|
return ast.IsFunctionDeclaration(node) ||
|
|
ast.IsClassDeclaration(node) ||
|
|
ast.IsClassStaticBlockDeclaration(node) ||
|
|
ast.IsMethodDeclaration(node) ||
|
|
ast.IsMethodSignatureDeclaration(node) ||
|
|
ast.IsGetAccessorDeclaration(node) ||
|
|
ast.IsSetAccessorDeclaration(node) ||
|
|
isNamedExpression(node) ||
|
|
isAssignedExpression(node)
|
|
}
|
|
|
|
// Gets the node that can be used as a reference to a call hierarchy declaration.
|
|
func getCallHierarchyDeclarationReferenceNode(node *ast.Node) *ast.Node {
|
|
if node == nil {
|
|
return nil
|
|
}
|
|
|
|
if ast.IsSourceFile(node) {
|
|
return node
|
|
}
|
|
|
|
if name := node.Name(); name != nil {
|
|
return name
|
|
}
|
|
|
|
if isAssignedExpression(node) {
|
|
return node.Parent.Name()
|
|
}
|
|
|
|
if modifiers := node.Modifiers(); modifiers != nil {
|
|
for _, mod := range modifiers.Nodes {
|
|
if mod.Kind == ast.KindDefaultKeyword {
|
|
return mod
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Gets the symbol for a call hierarchy declaration.
|
|
func getSymbolOfCallHierarchyDeclaration(c *checker.Checker, node *ast.Node) *ast.Symbol {
|
|
if ast.IsClassStaticBlockDeclaration(node) {
|
|
return nil
|
|
}
|
|
location := getCallHierarchyDeclarationReferenceNode(node)
|
|
if location == nil {
|
|
return nil
|
|
}
|
|
return c.GetSymbolAtLocation(location)
|
|
}
|
|
|
|
// Gets the text and range for the name of a call hierarchy declaration.
|
|
func getCallHierarchyItemName(program *compiler.Program, node *ast.Node) (text string, pos int, end int) {
|
|
if ast.IsSourceFile(node) {
|
|
sourceFile := node.AsSourceFile()
|
|
return sourceFile.FileName(), 0, 0
|
|
}
|
|
|
|
if (ast.IsFunctionDeclaration(node) || ast.IsClassDeclaration(node)) && node.Name() == nil {
|
|
if modifiers := node.Modifiers(); modifiers != nil {
|
|
for _, mod := range modifiers.Nodes {
|
|
if mod.Kind == ast.KindDefaultKeyword {
|
|
sourceFile := ast.GetSourceFileOfNode(node)
|
|
start := scanner.SkipTrivia(sourceFile.Text(), mod.Pos())
|
|
return "default", start, mod.End()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ast.IsClassStaticBlockDeclaration(node) {
|
|
sourceFile := ast.GetSourceFileOfNode(node)
|
|
pos := scanner.SkipTrivia(sourceFile.Text(), moveRangePastModifiers(node).Pos())
|
|
end := pos + 6 // "static".length
|
|
c, done := program.GetTypeCheckerForFile(context.Background(), sourceFile)
|
|
defer done()
|
|
symbol := c.GetSymbolAtLocation(node.Parent)
|
|
prefix := ""
|
|
if symbol != nil {
|
|
prefix = c.SymbolToString(symbol) + " "
|
|
}
|
|
return prefix + "static {}", pos, end
|
|
}
|
|
|
|
var declName *ast.Node
|
|
if isAssignedExpression(node) {
|
|
declName = node.Parent.Name()
|
|
} else {
|
|
declName = ast.GetNameOfDeclaration(node)
|
|
}
|
|
|
|
if declName == nil || !ast.NodeIsPresent(declName) {
|
|
sourceFile := ast.GetSourceFileOfNode(node)
|
|
switch {
|
|
case ast.IsFunctionDeclaration(node) || ast.IsFunctionExpression(node):
|
|
kwPos := scanner.SkipTrivia(sourceFile.Text(), moveRangePastModifiers(node).Pos())
|
|
return "(anonymous)", kwPos, kwPos + 8 // "function".length
|
|
case ast.IsClassDeclaration(node) || ast.IsClassExpression(node):
|
|
kwPos := scanner.SkipTrivia(sourceFile.Text(), moveRangePastModifiers(node).Pos())
|
|
return "(anonymous)", kwPos, kwPos + 5 // "class".length
|
|
}
|
|
debug.Assert(declName != nil, "Expected call hierarchy item to have a name")
|
|
}
|
|
|
|
text = getTextOfCallHierarchyName(program, node, declName, node)
|
|
|
|
sourceFile := ast.GetSourceFileOfNode(node)
|
|
namePos := scanner.SkipTrivia(sourceFile.Text(), declName.Pos())
|
|
|
|
return text, namePos, declName.End()
|
|
}
|
|
|
|
func getTextOfCallHierarchyName(program *compiler.Program, sourceNode *ast.Node, name *ast.Node, printNode *ast.Node) string {
|
|
if ast.IsIdentifier(name) || ast.IsStringOrNumericLiteralLike(name) {
|
|
return name.Text()
|
|
}
|
|
if ast.IsComputedPropertyName(name) {
|
|
expr := name.Expression()
|
|
if ast.IsStringOrNumericLiteralLike(expr) {
|
|
return expr.Text()
|
|
}
|
|
}
|
|
|
|
c, done := program.GetTypeCheckerForFile(context.Background(), ast.GetSourceFileOfNode(sourceNode))
|
|
defer done()
|
|
symbol := c.GetSymbolAtLocation(name)
|
|
if symbol != nil {
|
|
text := c.SymbolToString(symbol)
|
|
if text != "" {
|
|
return text
|
|
}
|
|
}
|
|
|
|
sourceFile := ast.GetSourceFileOfNode(sourceNode)
|
|
writer, putWriter := printer.GetSingleLineStringWriter()
|
|
defer putWriter()
|
|
p := printer.NewPrinter(printer.PrinterOptions{RemoveComments: true}, printer.PrintHandlers{}, nil)
|
|
p.Write(printNode, sourceFile, writer, nil)
|
|
return writer.String()
|
|
}
|
|
|
|
func getCallHierarchyItemContainerName(program *compiler.Program, node *ast.Node) string {
|
|
if isAssignedExpression(node) {
|
|
parent := node.Parent
|
|
if ast.IsPropertyDeclaration(parent) && ast.IsClassLike(parent.Parent) {
|
|
if ast.IsClassExpression(parent.Parent) {
|
|
if assignedName := ast.GetAssignedName(parent.Parent); assignedName != nil {
|
|
return getTextOfCallHierarchyName(program, node, assignedName, assignedName)
|
|
}
|
|
} else {
|
|
if name := parent.Parent.Name(); name != nil {
|
|
return getTextOfCallHierarchyName(program, node, name, name)
|
|
}
|
|
}
|
|
}
|
|
if parent.Parent.Parent != nil && parent.Parent.Parent.Parent != nil && ast.IsModuleBlock(parent.Parent.Parent.Parent) {
|
|
modParent := parent.Parent.Parent.Parent.Parent
|
|
if ast.IsModuleDeclaration(modParent) {
|
|
if name := modParent.Name(); name != nil && ast.IsIdentifier(name) {
|
|
return name.Text()
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
switch node.Kind {
|
|
case ast.KindGetAccessor, ast.KindSetAccessor, ast.KindMethodDeclaration:
|
|
if node.Parent.Kind == ast.KindObjectLiteralExpression {
|
|
if assignedName := ast.GetAssignedName(node.Parent); assignedName != nil {
|
|
return getTextOfCallHierarchyName(program, node, assignedName, assignedName)
|
|
}
|
|
}
|
|
if name := ast.GetNameOfDeclaration(node.Parent); name != nil {
|
|
return getTextOfCallHierarchyName(program, node, name, name)
|
|
}
|
|
case ast.KindFunctionDeclaration, ast.KindClassDeclaration, ast.KindModuleDeclaration:
|
|
if ast.IsModuleBlock(node.Parent) {
|
|
if ast.IsModuleDeclaration(node.Parent.Parent) {
|
|
if name := node.Parent.Parent.Name(); name != nil && ast.IsIdentifier(name) {
|
|
return name.Text()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func moveRangePastModifiers(node *ast.Node) core.TextRange {
|
|
if modifiers := node.Modifiers(); modifiers != nil && len(modifiers.Nodes) > 0 {
|
|
lastMod := modifiers.Nodes[len(modifiers.Nodes)-1]
|
|
return core.NewTextRange(lastMod.End(), node.End())
|
|
}
|
|
return core.NewTextRange(node.Pos(), node.End())
|
|
}
|
|
|
|
// Finds the implementation of a function-like declaration, if one exists.
|
|
func findImplementation(c *checker.Checker, node *ast.Node) *ast.Node {
|
|
if node == nil {
|
|
return nil
|
|
}
|
|
|
|
if !ast.IsFunctionLikeDeclaration(node) {
|
|
return node
|
|
}
|
|
|
|
if node.Body() != nil {
|
|
return node
|
|
}
|
|
|
|
if ast.IsConstructorDeclaration(node) {
|
|
return ast.GetFirstConstructorWithBody(node.Parent)
|
|
}
|
|
|
|
if ast.IsFunctionDeclaration(node) || ast.IsMethodDeclaration(node) {
|
|
symbol := getSymbolOfCallHierarchyDeclaration(c, node)
|
|
if symbol != nil && symbol.ValueDeclaration != nil {
|
|
if ast.IsFunctionLikeDeclaration(symbol.ValueDeclaration) && symbol.ValueDeclaration.Body() != nil {
|
|
return symbol.ValueDeclaration
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
func findAllInitialDeclarations(c *checker.Checker, node *ast.Node) []*ast.Node {
|
|
if ast.IsClassStaticBlockDeclaration(node) {
|
|
return nil
|
|
}
|
|
|
|
symbol := getSymbolOfCallHierarchyDeclaration(c, node)
|
|
if symbol == nil || symbol.Declarations == nil {
|
|
return nil
|
|
}
|
|
|
|
type declKey struct {
|
|
file string
|
|
pos int
|
|
}
|
|
|
|
indices := make([]int, len(symbol.Declarations))
|
|
for i := range indices {
|
|
indices[i] = i
|
|
}
|
|
keys := make([]declKey, len(symbol.Declarations))
|
|
for i, decl := range symbol.Declarations {
|
|
keys[i] = declKey{
|
|
file: ast.GetSourceFileOfNode(decl).FileName(),
|
|
pos: decl.Pos(),
|
|
}
|
|
}
|
|
|
|
slices.SortFunc(indices, func(a, b int) int {
|
|
if keys[a].file != keys[b].file {
|
|
return strings.Compare(keys[a].file, keys[b].file)
|
|
}
|
|
return keys[a].pos - keys[b].pos
|
|
})
|
|
|
|
var declarations []*ast.Node
|
|
var lastDecl *ast.Node
|
|
|
|
for _, i := range indices {
|
|
decl := symbol.Declarations[i]
|
|
if isValidCallHierarchyDeclaration(decl) {
|
|
if lastDecl == nil || lastDecl.Parent != decl.Parent || lastDecl.End() != decl.Pos() {
|
|
declarations = append(declarations, decl)
|
|
}
|
|
lastDecl = decl
|
|
}
|
|
}
|
|
|
|
return declarations
|
|
}
|
|
|
|
// Find the implementation or the first declaration for a call hierarchy declaration.
|
|
func findImplementationOrAllInitialDeclarations(c *checker.Checker, node *ast.Node) any {
|
|
if ast.IsClassStaticBlockDeclaration(node) {
|
|
return node
|
|
}
|
|
|
|
if ast.IsFunctionLikeDeclaration(node) {
|
|
if impl := findImplementation(c, node); impl != nil {
|
|
return impl
|
|
}
|
|
if decls := findAllInitialDeclarations(c, node); decls != nil {
|
|
return decls
|
|
}
|
|
return node
|
|
}
|
|
|
|
if decls := findAllInitialDeclarations(c, node); decls != nil {
|
|
return decls
|
|
}
|
|
return node
|
|
}
|
|
|
|
// Resolves the call hierarchy declaration for a node.
|
|
func resolveCallHierarchyDeclaration(program *compiler.Program, location *ast.Node) (result any) {
|
|
// A call hierarchy item must refer to either a SourceFile, Module Declaration, Class Static Block, or something intrinsically callable that has a name:
|
|
// - Class Declarations
|
|
// - Class Expressions (with a name)
|
|
// - Function Declarations
|
|
// - Function Expressions (with a name or assigned to a const variable)
|
|
// - Arrow Functions (assigned to a const variable)
|
|
// - Constructors
|
|
// - Class `static {}` initializer blocks
|
|
// - Methods
|
|
// - Accessors
|
|
//
|
|
// If a call is contained in a non-named callable Node (function expression, arrow function, etc.), then
|
|
// its containing `CallHierarchyItem` is a containing function or SourceFile that matches the above list.
|
|
|
|
c, done := program.GetTypeChecker(context.Background())
|
|
defer done()
|
|
|
|
followingSymbol := false
|
|
|
|
for location != nil {
|
|
if isValidCallHierarchyDeclaration(location) {
|
|
return findImplementationOrAllInitialDeclarations(c, location)
|
|
}
|
|
|
|
if isPossibleCallHierarchyDeclaration(location) {
|
|
ancestor := ast.FindAncestor(location, isValidCallHierarchyDeclaration)
|
|
if ancestor != nil {
|
|
return findImplementationOrAllInitialDeclarations(c, ancestor)
|
|
}
|
|
}
|
|
|
|
if ast.IsDeclarationName(location) {
|
|
if isValidCallHierarchyDeclaration(location.Parent) {
|
|
return findImplementationOrAllInitialDeclarations(c, location.Parent)
|
|
}
|
|
if isPossibleCallHierarchyDeclaration(location.Parent) {
|
|
ancestor := ast.FindAncestor(location.Parent, isValidCallHierarchyDeclaration)
|
|
if ancestor != nil {
|
|
return findImplementationOrAllInitialDeclarations(c, ancestor)
|
|
}
|
|
}
|
|
if isVariableLike(location.Parent) {
|
|
initializer := location.Parent.Initializer()
|
|
if initializer != nil && isAssignedExpression(initializer) {
|
|
return initializer
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if ast.IsConstructorDeclaration(location) {
|
|
if isValidCallHierarchyDeclaration(location.Parent) {
|
|
return location.Parent
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if location.Kind == ast.KindStaticKeyword && ast.IsClassStaticBlockDeclaration(location.Parent) {
|
|
location = location.Parent
|
|
continue
|
|
}
|
|
|
|
// #39453
|
|
if ast.IsVariableDeclaration(location) {
|
|
if initializer := location.Initializer(); initializer != nil && isAssignedExpression(initializer) {
|
|
return initializer
|
|
}
|
|
}
|
|
|
|
if !followingSymbol {
|
|
symbol := c.GetSymbolAtLocation(location)
|
|
if symbol != nil {
|
|
if (symbol.Flags & ast.SymbolFlagsAlias) != 0 {
|
|
symbol = c.GetAliasedSymbol(symbol)
|
|
}
|
|
if symbol.ValueDeclaration != nil {
|
|
followingSymbol = true
|
|
location = symbol.ValueDeclaration
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Creates a `CallHierarchyItem` for a call hierarchy declaration.
|
|
func (l *LanguageService) createCallHierarchyItem(program *compiler.Program, node *ast.Node) *lsproto.CallHierarchyItem {
|
|
sourceFile := ast.GetSourceFileOfNode(node)
|
|
nameText, namePos, nameEnd := getCallHierarchyItemName(program, node)
|
|
containerName := getCallHierarchyItemContainerName(program, node)
|
|
|
|
kind := getSymbolKindFromNode(node)
|
|
|
|
fullStart := scanner.SkipTriviaEx(sourceFile.Text(), node.Pos(), &scanner.SkipTriviaOptions{StopAtComments: true})
|
|
script := l.getScript(sourceFile.FileName())
|
|
span := l.converters.ToLSPRange(script, core.NewTextRange(fullStart, node.End()))
|
|
selectionSpan := l.converters.ToLSPRange(script, core.NewTextRange(namePos, nameEnd))
|
|
|
|
item := &lsproto.CallHierarchyItem{
|
|
Name: nameText,
|
|
Kind: kind,
|
|
Uri: lsconv.FileNameToDocumentURI(sourceFile.FileName()),
|
|
Range: span,
|
|
SelectionRange: selectionSpan,
|
|
}
|
|
|
|
if containerName != "" {
|
|
item.Detail = &containerName
|
|
}
|
|
|
|
return item
|
|
}
|
|
|
|
type callSite struct {
|
|
declaration *ast.Node
|
|
textRange core.TextRange
|
|
sourceFile *ast.Node
|
|
}
|
|
|
|
func convertEntryToCallSite(entry *ReferenceEntry) *callSite {
|
|
if entry.kind != entryKindNode {
|
|
return nil
|
|
}
|
|
|
|
node := entry.node
|
|
if !ast.IsCallOrNewExpressionTarget(node, true /*includeElementAccess*/, true /*skipPastOuterExpressions*/) &&
|
|
!ast.IsTaggedTemplateTag(node, true, true) &&
|
|
!ast.IsDecoratorTarget(node, true, true) &&
|
|
!ast.IsJsxOpeningLikeElementTagName(node, true, true) &&
|
|
!ast.IsRightSideOfPropertyAccess(node) &&
|
|
!ast.IsArgumentExpressionOfElementAccess(node) {
|
|
return nil
|
|
}
|
|
|
|
sourceFile := ast.GetSourceFileOfNode(node)
|
|
ancestor := ast.FindAncestor(node, isValidCallHierarchyDeclaration)
|
|
if ancestor == nil {
|
|
ancestor = sourceFile.AsNode()
|
|
}
|
|
|
|
start := scanner.SkipTrivia(sourceFile.Text(), node.Pos())
|
|
return &callSite{
|
|
declaration: ancestor,
|
|
textRange: core.NewTextRange(start, node.End()),
|
|
sourceFile: sourceFile.AsNode(),
|
|
}
|
|
}
|
|
|
|
func getCallSiteGroupKey(site *callSite) ast.NodeId {
|
|
return ast.GetNodeId(site.declaration)
|
|
}
|
|
|
|
func (l *LanguageService) convertCallSiteGroupToIncomingCall(program *compiler.Program, entries []*callSite) *lsproto.CallHierarchyIncomingCall {
|
|
fromRanges := make([]lsproto.Range, len(entries))
|
|
for i, entry := range entries {
|
|
script := l.getScript(entry.sourceFile.AsSourceFile().FileName())
|
|
fromRanges[i] = l.converters.ToLSPRange(script, entry.textRange)
|
|
}
|
|
|
|
slices.SortFunc(fromRanges, lsproto.CompareRanges)
|
|
|
|
return &lsproto.CallHierarchyIncomingCall{
|
|
From: l.createCallHierarchyItem(program, entries[0].declaration),
|
|
FromRanges: fromRanges,
|
|
}
|
|
}
|
|
|
|
type incomingEntry struct {
|
|
ls *LanguageService
|
|
node *ast.Node
|
|
|
|
sourceFileOnce sync.Once
|
|
sourceFile *ast.SourceFile
|
|
|
|
documentUriOnce sync.Once
|
|
documentUri lsproto.DocumentUri
|
|
|
|
positionOnce sync.Once
|
|
position lsproto.Position
|
|
}
|
|
|
|
var _ lsproto.HasTextDocumentPosition = (*incomingEntry)(nil)
|
|
|
|
func (d *incomingEntry) getSourceFile() *ast.SourceFile {
|
|
d.sourceFileOnce.Do(func() {
|
|
d.sourceFile = ast.GetSourceFileOfNode(d.node)
|
|
})
|
|
return d.sourceFile
|
|
}
|
|
|
|
func (d *incomingEntry) TextDocumentURI() lsproto.DocumentUri {
|
|
d.documentUriOnce.Do(func() {
|
|
d.documentUri = lsconv.FileNameToDocumentURI(d.getSourceFile().FileName())
|
|
})
|
|
return d.documentUri
|
|
}
|
|
|
|
func (d *incomingEntry) TextDocumentPosition() lsproto.Position {
|
|
d.positionOnce.Do(func() {
|
|
start := scanner.GetTokenPosOfNode(d.node, d.getSourceFile(), false /*includeJsDoc*/)
|
|
d.position = d.ls.createLspPosition(start, d.getSourceFile())
|
|
})
|
|
return d.position
|
|
}
|
|
|
|
// Gets the call sites that call into the provided call hierarchy declaration.
|
|
func (l *LanguageService) getIncomingCalls(ctx context.Context, program *compiler.Program, declaration *ast.Node, orchestrator CrossProjectOrchestrator) (lsproto.CallHierarchyIncomingCallsResponse, error) {
|
|
// Source files and modules have no incoming calls.
|
|
if ast.IsSourceFile(declaration) || ast.IsModuleDeclaration(declaration) || ast.IsClassStaticBlockDeclaration(declaration) {
|
|
return lsproto.CallHierarchyIncomingCallsOrNull{}, nil
|
|
}
|
|
|
|
location := getCallHierarchyDeclarationReferenceNode(declaration)
|
|
if location == nil {
|
|
return lsproto.CallHierarchyIncomingCallsOrNull{}, nil
|
|
}
|
|
|
|
incomingEntry := &incomingEntry{
|
|
ls: l,
|
|
node: location,
|
|
}
|
|
|
|
result, err := handleCrossProject(
|
|
l,
|
|
ctx,
|
|
incomingEntry,
|
|
orchestrator,
|
|
(*LanguageService).symbolAndEntriesToIncomingCalls,
|
|
combineIncomingCalls,
|
|
false,
|
|
false,
|
|
symbolEntryTransformOptions{},
|
|
)
|
|
if result.CallHierarchyIncomingCalls != nil {
|
|
slices.SortFunc(*result.CallHierarchyIncomingCalls, func(a, b *lsproto.CallHierarchyIncomingCall) int {
|
|
if uriComp := strings.Compare(string(a.From.Uri), string(b.From.Uri)); uriComp != 0 {
|
|
return uriComp
|
|
}
|
|
if len(a.FromRanges) == 0 || len(b.FromRanges) == 0 {
|
|
return 0
|
|
}
|
|
return lsproto.CompareRanges(a.FromRanges[0], b.FromRanges[0])
|
|
})
|
|
}
|
|
return result, err
|
|
}
|
|
|
|
func (l *LanguageService) symbolAndEntriesToIncomingCalls(ctx context.Context, params *incomingEntry, data SymbolAndEntriesData, options symbolEntryTransformOptions) (lsproto.CallHierarchyIncomingCallsResponse, error) {
|
|
program := l.GetProgram()
|
|
var refEntries []*ReferenceEntry
|
|
for _, symbolAndEntry := range data.SymbolsAndEntries {
|
|
refEntries = append(refEntries, symbolAndEntry.references...)
|
|
}
|
|
|
|
var callSites []*callSite
|
|
for _, entry := range refEntries {
|
|
if site := convertEntryToCallSite(entry); site != nil {
|
|
callSites = append(callSites, site)
|
|
}
|
|
}
|
|
|
|
if len(callSites) == 0 {
|
|
return lsproto.CallHierarchyIncomingCallsOrNull{}, nil
|
|
}
|
|
|
|
grouped := make(map[ast.NodeId][]*callSite)
|
|
for _, site := range callSites {
|
|
key := getCallSiteGroupKey(site)
|
|
grouped[key] = append(grouped[key], site)
|
|
}
|
|
|
|
var result []*lsproto.CallHierarchyIncomingCall
|
|
for _, sites := range grouped {
|
|
result = append(result, l.convertCallSiteGroupToIncomingCall(program, sites))
|
|
}
|
|
return lsproto.CallHierarchyIncomingCallsOrNull{CallHierarchyIncomingCalls: &result}, nil
|
|
}
|
|
|
|
type callSiteCollector struct {
|
|
program *compiler.Program
|
|
callSites []*callSite
|
|
}
|
|
|
|
func (c *callSiteCollector) recordCallSite(node *ast.Node) {
|
|
var target *ast.Node
|
|
|
|
switch {
|
|
case ast.IsTaggedTemplateExpression(node):
|
|
target = node.AsTaggedTemplateExpression().Tag
|
|
case ast.IsJsxOpeningElement(node):
|
|
target = node.TagName()
|
|
case ast.IsJsxSelfClosingElement(node):
|
|
target = node.TagName()
|
|
case ast.IsPropertyAccessExpression(node) || ast.IsElementAccessExpression(node):
|
|
target = node
|
|
case ast.IsClassStaticBlockDeclaration(node):
|
|
target = node
|
|
case ast.IsCallExpression(node):
|
|
target = node.Expression()
|
|
case ast.IsNewExpression(node):
|
|
target = node.Expression()
|
|
case ast.IsDecorator(node):
|
|
target = node.Expression()
|
|
}
|
|
|
|
if target == nil {
|
|
return
|
|
}
|
|
|
|
declaration := resolveCallHierarchyDeclaration(c.program, target)
|
|
if declaration == nil {
|
|
return
|
|
}
|
|
|
|
sourceFile := ast.GetSourceFileOfNode(target)
|
|
start := scanner.SkipTrivia(sourceFile.Text(), target.Pos())
|
|
textRange := core.NewTextRange(start, target.End())
|
|
|
|
switch decl := declaration.(type) {
|
|
case *ast.Node:
|
|
c.callSites = append(c.callSites, &callSite{
|
|
declaration: decl,
|
|
textRange: textRange,
|
|
sourceFile: sourceFile.AsNode(),
|
|
})
|
|
case []*ast.Node:
|
|
for _, d := range decl {
|
|
c.callSites = append(c.callSites, &callSite{
|
|
declaration: d,
|
|
textRange: textRange,
|
|
sourceFile: sourceFile.AsNode(),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *callSiteCollector) collect(node *ast.Node) {
|
|
if node == nil {
|
|
return
|
|
}
|
|
|
|
// do not descend into ambient nodes.
|
|
if (node.Flags & ast.NodeFlagsAmbient) != 0 {
|
|
return
|
|
}
|
|
|
|
// do not descend into other call site declarations, other than class member names
|
|
if isValidCallHierarchyDeclaration(node) {
|
|
if ast.IsClassLike(node) {
|
|
for _, member := range node.Members() {
|
|
if member.Name() != nil && ast.IsComputedPropertyName(member.Name()) {
|
|
c.collect(member.Name().Expression())
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
switch node.Kind {
|
|
case ast.KindIdentifier,
|
|
ast.KindImportEqualsDeclaration,
|
|
ast.KindImportDeclaration,
|
|
ast.KindExportDeclaration,
|
|
ast.KindInterfaceDeclaration,
|
|
ast.KindTypeAliasDeclaration:
|
|
// do not descend into nodes that cannot contain callable nodes
|
|
return
|
|
case ast.KindClassStaticBlockDeclaration:
|
|
c.recordCallSite(node)
|
|
return
|
|
case ast.KindTypeAssertionExpression, ast.KindAsExpression:
|
|
// do not descend into the type side of an assertion
|
|
c.collect(node.Expression())
|
|
return
|
|
case ast.KindVariableDeclaration, ast.KindParameter:
|
|
// do not descend into the type of a variable or parameter declaration
|
|
c.collect(node.Name())
|
|
c.collect(node.Initializer())
|
|
return
|
|
case ast.KindCallExpression:
|
|
// do not descend into the type arguments of a call expression
|
|
c.recordCallSite(node)
|
|
c.collect(node.Expression())
|
|
for _, arg := range node.Arguments() {
|
|
c.collect(arg)
|
|
}
|
|
return
|
|
case ast.KindNewExpression:
|
|
// do not descend into the type arguments of a new expression
|
|
c.recordCallSite(node)
|
|
c.collect(node.Expression())
|
|
for _, arg := range node.Arguments() {
|
|
c.collect(arg)
|
|
}
|
|
return
|
|
case ast.KindTaggedTemplateExpression:
|
|
// do not descend into the type arguments of a tagged template expression
|
|
c.recordCallSite(node)
|
|
taggedTemplate := node.AsTaggedTemplateExpression()
|
|
c.collect(taggedTemplate.Tag)
|
|
c.collect(taggedTemplate.Template)
|
|
return
|
|
case ast.KindJsxOpeningElement, ast.KindJsxSelfClosingElement:
|
|
// do not descend into the type arguments of a JsxOpeningLikeElement
|
|
c.recordCallSite(node)
|
|
c.collect(node.TagName())
|
|
c.collect(node.Attributes())
|
|
return
|
|
case ast.KindDecorator:
|
|
c.recordCallSite(node)
|
|
c.collect(node.Expression())
|
|
return
|
|
case ast.KindPropertyAccessExpression, ast.KindElementAccessExpression:
|
|
c.recordCallSite(node)
|
|
node.ForEachChild(func(child *ast.Node) bool {
|
|
c.collect(child)
|
|
return false
|
|
})
|
|
return
|
|
case ast.KindSatisfiesExpression:
|
|
// do not descend into the type side of an assertion
|
|
c.collect(node.Expression())
|
|
return
|
|
}
|
|
|
|
if ast.IsPartOfTypeNode(node) {
|
|
// do not descend into types
|
|
return
|
|
}
|
|
|
|
node.ForEachChild(func(child *ast.Node) bool {
|
|
c.collect(child)
|
|
return false
|
|
})
|
|
}
|
|
|
|
func collectCallSites(program *compiler.Program, c *checker.Checker, node *ast.Node) []*callSite {
|
|
collector := &callSiteCollector{
|
|
program: program,
|
|
callSites: make([]*callSite, 0),
|
|
}
|
|
|
|
switch node.Kind {
|
|
case ast.KindSourceFile:
|
|
for _, stmt := range node.Statements() {
|
|
collector.collect(stmt)
|
|
}
|
|
|
|
case ast.KindModuleDeclaration:
|
|
if body := node.Body(); !ast.HasSyntacticModifier(node, ast.ModifierFlagsAmbient) && body != nil && ast.IsModuleBlock(body) {
|
|
for _, stmt := range body.Statements() {
|
|
collector.collect(stmt)
|
|
}
|
|
}
|
|
|
|
case ast.KindFunctionDeclaration, ast.KindFunctionExpression, ast.KindArrowFunction,
|
|
ast.KindMethodDeclaration, ast.KindGetAccessor, ast.KindSetAccessor:
|
|
impl := findImplementation(c, node)
|
|
if impl != nil {
|
|
for _, param := range impl.Parameters() {
|
|
collector.collect(param)
|
|
}
|
|
collector.collect(impl.Body())
|
|
}
|
|
|
|
case ast.KindClassDeclaration, ast.KindClassExpression:
|
|
if modifiers := node.Modifiers(); modifiers != nil {
|
|
for _, mod := range modifiers.Nodes {
|
|
collector.collect(mod)
|
|
}
|
|
}
|
|
|
|
heritage := ast.GetClassExtendsHeritageElement(node)
|
|
if heritage != nil {
|
|
collector.collect(heritage.Expression())
|
|
}
|
|
|
|
for _, member := range node.Members() {
|
|
if ast.CanHaveModifiers(member) && member.Modifiers() != nil {
|
|
for _, mod := range member.Modifiers().Nodes {
|
|
collector.collect(mod)
|
|
}
|
|
}
|
|
|
|
if ast.IsPropertyDeclaration(member) {
|
|
collector.collect(member.Initializer())
|
|
} else if ast.IsConstructorDeclaration(member) {
|
|
if body := member.Body(); body != nil {
|
|
for _, param := range member.Parameters() {
|
|
collector.collect(param)
|
|
}
|
|
collector.collect(body)
|
|
}
|
|
} else if ast.IsClassStaticBlockDeclaration(member) {
|
|
collector.collect(member)
|
|
}
|
|
}
|
|
|
|
case ast.KindClassStaticBlockDeclaration:
|
|
staticBlock := node.AsClassStaticBlockDeclaration()
|
|
collector.collect(staticBlock.Body)
|
|
|
|
default:
|
|
debug.AssertNever(node)
|
|
}
|
|
|
|
return collector.callSites
|
|
}
|
|
|
|
func (l *LanguageService) convertCallSiteGroupToOutgoingCall(program *compiler.Program, entries []*callSite) *lsproto.CallHierarchyOutgoingCall {
|
|
fromRanges := make([]lsproto.Range, len(entries))
|
|
for i, entry := range entries {
|
|
script := l.getScript(entry.sourceFile.AsSourceFile().FileName())
|
|
fromRanges[i] = l.converters.ToLSPRange(script, entry.textRange)
|
|
}
|
|
|
|
slices.SortFunc(fromRanges, lsproto.CompareRanges)
|
|
|
|
return &lsproto.CallHierarchyOutgoingCall{
|
|
To: l.createCallHierarchyItem(program, entries[0].declaration),
|
|
FromRanges: fromRanges,
|
|
}
|
|
}
|
|
|
|
// Gets the call sites that call out of the provided call hierarchy declaration.
|
|
func (l *LanguageService) getOutgoingCalls(program *compiler.Program, declaration *ast.Node) []*lsproto.CallHierarchyOutgoingCall {
|
|
if (declaration.Flags&ast.NodeFlagsAmbient) != 0 || ast.IsMethodSignatureDeclaration(declaration) {
|
|
return nil
|
|
}
|
|
|
|
c, done := program.GetTypeChecker(context.Background())
|
|
defer done()
|
|
|
|
callSites := collectCallSites(program, c, declaration)
|
|
|
|
if len(callSites) == 0 {
|
|
return nil
|
|
}
|
|
|
|
grouped := make(map[ast.NodeId][]*callSite)
|
|
for _, site := range callSites {
|
|
key := getCallSiteGroupKey(site)
|
|
grouped[key] = append(grouped[key], site)
|
|
}
|
|
|
|
var result []*lsproto.CallHierarchyOutgoingCall
|
|
for _, sites := range grouped {
|
|
result = append(result, l.convertCallSiteGroupToOutgoingCall(program, sites))
|
|
}
|
|
|
|
slices.SortFunc(result, func(a, b *lsproto.CallHierarchyOutgoingCall) int {
|
|
if uriComp := strings.Compare(string(a.To.Uri), string(b.To.Uri)); uriComp != 0 {
|
|
return uriComp
|
|
}
|
|
if len(a.FromRanges) == 0 || len(b.FromRanges) == 0 {
|
|
return 0
|
|
}
|
|
return lsproto.CompareRanges(a.FromRanges[0], b.FromRanges[0])
|
|
})
|
|
|
|
return result
|
|
}
|
|
|
|
func (l *LanguageService) ProvidePrepareCallHierarchy(
|
|
ctx context.Context,
|
|
documentURI lsproto.DocumentUri,
|
|
position lsproto.Position,
|
|
) (lsproto.CallHierarchyPrepareResponse, error) {
|
|
program, file := l.getProgramAndFile(documentURI)
|
|
node := astnav.GetTouchingPropertyName(file, int(l.converters.LineAndCharacterToPosition(file, position)))
|
|
|
|
if node.Kind == ast.KindSourceFile {
|
|
return lsproto.CallHierarchyItemsOrNull{}, nil
|
|
}
|
|
|
|
declaration := resolveCallHierarchyDeclaration(program, node)
|
|
if declaration == nil {
|
|
return lsproto.CallHierarchyItemsOrNull{}, nil
|
|
}
|
|
|
|
var items []*lsproto.CallHierarchyItem
|
|
switch decl := declaration.(type) {
|
|
case *ast.Node:
|
|
items = []*lsproto.CallHierarchyItem{l.createCallHierarchyItem(program, decl)}
|
|
case []*ast.Node:
|
|
items = make([]*lsproto.CallHierarchyItem, len(decl))
|
|
for i, d := range decl {
|
|
items[i] = l.createCallHierarchyItem(program, d)
|
|
}
|
|
}
|
|
|
|
if items == nil {
|
|
return lsproto.CallHierarchyItemsOrNull{}, nil
|
|
}
|
|
return lsproto.CallHierarchyItemsOrNull{CallHierarchyItems: &items}, nil
|
|
}
|
|
|
|
func (l *LanguageService) ProvideCallHierarchyIncomingCalls(
|
|
ctx context.Context,
|
|
item *lsproto.CallHierarchyItem,
|
|
orchestrator CrossProjectOrchestrator,
|
|
) (lsproto.CallHierarchyIncomingCallsResponse, error) {
|
|
program := l.GetProgram()
|
|
fileName := item.Uri.FileName()
|
|
file := program.GetSourceFile(fileName)
|
|
if file == nil {
|
|
return lsproto.CallHierarchyIncomingCallsOrNull{}, nil
|
|
}
|
|
|
|
pos := int(l.converters.LineAndCharacterToPosition(file, item.SelectionRange.Start))
|
|
var node *ast.Node
|
|
if pos == 0 {
|
|
node = file.AsNode()
|
|
} else {
|
|
node = astnav.GetTouchingPropertyName(file, pos)
|
|
}
|
|
|
|
if node == nil {
|
|
return lsproto.CallHierarchyIncomingCallsOrNull{}, nil
|
|
}
|
|
|
|
declaration := resolveCallHierarchyDeclaration(program, node)
|
|
if declaration == nil {
|
|
return lsproto.CallHierarchyIncomingCallsOrNull{}, nil
|
|
}
|
|
|
|
var decl *ast.Node
|
|
switch d := declaration.(type) {
|
|
case *ast.Node:
|
|
decl = d
|
|
case []*ast.Node:
|
|
if len(d) > 0 {
|
|
decl = d[0]
|
|
}
|
|
}
|
|
|
|
if decl == nil {
|
|
return lsproto.CallHierarchyIncomingCallsOrNull{}, nil
|
|
}
|
|
|
|
return l.getIncomingCalls(ctx, program, decl, orchestrator)
|
|
}
|
|
|
|
func (l *LanguageService) ProvideCallHierarchyOutgoingCalls(
|
|
ctx context.Context,
|
|
item *lsproto.CallHierarchyItem,
|
|
) (lsproto.CallHierarchyOutgoingCallsResponse, error) {
|
|
program := l.GetProgram()
|
|
fileName := item.Uri.FileName()
|
|
file := program.GetSourceFile(fileName)
|
|
if file == nil {
|
|
return lsproto.CallHierarchyOutgoingCallsOrNull{}, nil
|
|
}
|
|
|
|
pos := int(l.converters.LineAndCharacterToPosition(file, item.SelectionRange.Start))
|
|
var node *ast.Node
|
|
if pos == 0 {
|
|
node = file.AsNode()
|
|
} else {
|
|
node = astnav.GetTouchingPropertyName(file, pos)
|
|
}
|
|
|
|
if node == nil {
|
|
return lsproto.CallHierarchyOutgoingCallsOrNull{}, nil
|
|
}
|
|
|
|
declaration := resolveCallHierarchyDeclaration(program, node)
|
|
if declaration == nil {
|
|
return lsproto.CallHierarchyOutgoingCallsOrNull{}, nil
|
|
}
|
|
|
|
var decl *ast.Node
|
|
switch d := declaration.(type) {
|
|
case *ast.Node:
|
|
decl = d
|
|
case []*ast.Node:
|
|
if len(d) > 0 {
|
|
decl = d[0]
|
|
}
|
|
}
|
|
|
|
if decl == nil {
|
|
return lsproto.CallHierarchyOutgoingCallsOrNull{}, nil
|
|
}
|
|
|
|
calls := l.getOutgoingCalls(program, decl)
|
|
if calls == nil {
|
|
return lsproto.CallHierarchyOutgoingCallsOrNull{}, nil
|
|
}
|
|
return lsproto.CallHierarchyOutgoingCallsOrNull{CallHierarchyOutgoingCalls: &calls}, nil
|
|
}
|