diff --git a/internal/ls/definition.go b/internal/ls/definition.go index 6ccc155c2e..d90c8c0b66 100644 --- a/internal/ls/definition.go +++ b/internal/ls/definition.go @@ -5,6 +5,7 @@ import ( "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/core" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/scanner" @@ -20,6 +21,14 @@ func (l *LanguageService) ProvideDefinition(ctx context.Context, documentURI lsp checker, done := program.GetTypeCheckerForFile(ctx, file) defer done() + calledDeclaration := tryGetSignatureDeclaration(checker, node) + if calledDeclaration != nil { + name := ast.GetNameOfDeclaration(calledDeclaration) + if name != nil { + return l.createLocationsFromDeclarations([]*ast.Node{name}) + } + } + if symbol := checker.GetSymbolAtLocation(node); symbol != nil { if symbol.Flags&ast.SymbolFlagsAlias != 0 { if resolved, ok := checker.ResolveAlias(symbol); ok { @@ -27,17 +36,54 @@ func (l *LanguageService) ProvideDefinition(ctx context.Context, documentURI lsp } } - locations := make([]lsproto.Location, 0, len(symbol.Declarations)) - for _, decl := range symbol.Declarations { - file := ast.GetSourceFileOfNode(decl) - loc := decl.Loc - pos := scanner.GetTokenPosOfNode(decl, file, false /*includeJSDoc*/) - locations = append(locations, lsproto.Location{ - Uri: FileNameToDocumentURI(file.FileName()), - Range: l.converters.ToLSPRange(file, core.NewTextRange(pos, loc.End())), - }) - } - return &lsproto.Definition{Locations: &locations}, nil + return l.createLocationsFromDeclarations(symbol.Declarations) } return nil, nil } + +func (l *LanguageService) createLocationsFromDeclarations(declarations []*ast.Node) (*lsproto.Definition, error) { + locations := make([]lsproto.Location, 0, len(declarations)) + for _, decl := range declarations { + file := ast.GetSourceFileOfNode(decl) + loc := decl.Loc + pos := scanner.GetTokenPosOfNode(decl, file, false /*includeJSDoc*/) + locations = append(locations, lsproto.Location{ + Uri: FileNameToDocumentURI(file.FileName()), + Range: l.converters.ToLSPRange(file, core.NewTextRange(pos, loc.End())), + }) + } + return &lsproto.Definition{Locations: &locations}, nil +} + +/** Returns a CallLikeExpression where `node` is the target being invoked. */ +func getAncestorCallLikeExpression(node *ast.Node) *ast.Node { + target := ast.FindAncestor(node, func(n *ast.Node) bool { + return !isRightSideOfPropertyAccess(n) + }) + + callLike := target.Parent + if callLike != nil && ast.IsCallLikeExpression(callLike) && ast.GetInvokedExpression(callLike) == target { + return callLike + } + + return nil +} + +func tryGetSignatureDeclaration(typeChecker *checker.Checker, node *ast.Node) *ast.Node { + var signature *checker.Signature + callLike := getAncestorCallLikeExpression(node) + if callLike != nil { + signature = typeChecker.GetResolvedSignature(callLike) + } + + // Don't go to a function type, go to the value having that type. + var declaration *ast.Node + if signature != nil && signature.Declaration() != nil { + declaration = signature.Declaration() + if ast.IsFunctionLike(declaration) && !ast.IsFunctionTypeNode(declaration) { + return declaration + } + } + + return nil +} diff --git a/internal/ls/definition_test.go b/internal/ls/definition_test.go new file mode 100644 index 0000000000..a932e3eed9 --- /dev/null +++ b/internal/ls/definition_test.go @@ -0,0 +1,74 @@ +package ls_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/bundled" + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/ls" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/testutil/projecttestutil" + "gotest.tools/v3/assert" +) + +func TestDefinition(t *testing.T) { + t.Parallel() + if !bundled.Embedded { + // Without embedding, we'd need to read all of the lib files out from disk into the MapFS. + // Just skip this for now. + t.Skip("bundled files are not embedded") + } + + testCases := []struct { + title string + input string + expected map[string]lsproto.Definition + }{ + { + title: "localFunction", + input: ` +// @filename: index.ts +function localFunction() { } +/*localFunction*/localFunction();`, + expected: map[string]lsproto.Definition{ + "localFunction": { + Locations: &[]lsproto.Location{{ + Uri: ls.FileNameToDocumentURI("/index.ts"), + Range: lsproto.Range{Start: lsproto.Position{Character: 9}, End: lsproto.Position{Character: 22}}, + }}, + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.title, func(t *testing.T) { + t.Parallel() + runDefinitionTest(t, testCase.input, testCase.expected) + }) + } +} + +func runDefinitionTest(t *testing.T, input string, expected map[string]lsproto.Definition) { + testData := fourslash.ParseTestData(t, input, "/mainFile.ts") + file := testData.Files[0].FileName() + markerPositions := testData.MarkerPositions + ctx := projecttestutil.WithRequestID(t.Context()) + languageService, done := createLanguageService(ctx, file, map[string]any{ + file: testData.Files[0].Content, + }) + defer done() + + for markerName, expectedResult := range expected { + marker, ok := markerPositions[markerName] + if !ok { + t.Fatalf("No marker found for '%s'", markerName) + } + locations, err := languageService.ProvideDefinition( + ctx, + ls.FileNameToDocumentURI(file), + marker.LSPosition) + assert.NilError(t, err) + assert.DeepEqual(t, *locations, expectedResult) + } +}