diff --git a/internal/ast/utilities.go b/internal/ast/utilities.go index ffe9baee60..678ac338d5 100644 --- a/internal/ast/utilities.go +++ b/internal/ast/utilities.go @@ -2801,3 +2801,8 @@ func ForEachChildAndJSDoc(node *Node, sourceFile *SourceFile, v Visitor) bool { } return node.ForEachChild(v) } + +func IsRightSideOfPropertyAccess(node *Node) bool { + parent := node.Parent + return IsPropertyAccessExpression(parent) && parent.AsPropertyAccessExpression().Name() == node +} diff --git a/internal/checker/checker.go b/internal/checker/checker.go index 3383971d34..71c1eaf039 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -5636,7 +5636,7 @@ func (c *Checker) checkDecorators(node *ast.Node) { func (c *Checker) checkDecorator(node *ast.Node) { c.checkGrammarDecorator(node.AsDecorator()) - signature := c.getResolvedSignature(node, nil, CheckModeNormal) + signature := c.GetResolvedSignature(node, nil, CheckModeNormal) c.checkDeprecatedSignature(signature, node) returnType := c.getReturnTypeOfSignature(signature) if returnType.flags&TypeFlagsAny != 0 { @@ -7878,7 +7878,7 @@ func (c *Checker) checkImportCallExpression(node *ast.Node) *Type { */ func (c *Checker) checkCallExpression(node *ast.Node, checkMode CheckMode) *Type { c.checkGrammarTypeArguments(node, node.TypeArgumentList()) - signature := c.getResolvedSignature(node, nil /*candidatesOutArray*/, checkMode) + signature := c.GetResolvedSignature(node, nil /*candidatesOutArray*/, checkMode) if signature == c.resolvingSignature { // CheckMode.SkipGenericFunctions is enabled and this is a call to a generic function that // returns a function type. We defer checking and return silentNeverType. @@ -7925,7 +7925,7 @@ func (c *Checker) checkDeprecatedSignature(sig *Signature, node *ast.Node) { } if sig.declaration != nil && sig.declaration.Flags&ast.NodeFlagsDeprecated != 0 { suggestionNode := c.getDeprecatedSuggestionNode(node) - name := tryGetPropertyAccessOrIdentifierToString(getInvokedExpression(node)) + name := tryGetPropertyAccessOrIdentifierToString(GetInvokedExpression(node)) c.addDeprecatedSuggestionWithSignature(suggestionNode, sig.declaration, name, c.signatureToString(sig)) } } @@ -7962,7 +7962,7 @@ func (c *Checker) isSymbolOrSymbolForCall(node *ast.Node) bool { * the function will fill it up with appropriate candidate signatures * @return a signature of the call-like expression or undefined if one can't be found */ -func (c *Checker) getResolvedSignature(node *ast.Node, candidatesOutArray *[]*Signature, checkMode CheckMode) *Signature { +func (c *Checker) GetResolvedSignature(node *ast.Node, candidatesOutArray *[]*Signature, checkMode CheckMode) *Signature { links := c.signatureLinks.Get(node) // If getResolvedSignature has already been called, we will have cached the resolvedSignature. // However, it is possible that either candidatesOutArray was not passed in the first time, @@ -9582,7 +9582,7 @@ func (c *Checker) checkTaggedTemplateExpression(node *ast.Node) *Type { if !c.checkGrammarTaggedTemplateChain(node.AsTaggedTemplateExpression()) { c.checkGrammarTypeArguments(node, node.TypeArgumentList()) } - signature := c.getResolvedSignature(node, nil, CheckModeNormal) + signature := c.GetResolvedSignature(node, nil, CheckModeNormal) c.checkDeprecatedSignature(signature, node) return c.getReturnTypeOfSignature(signature) } @@ -12325,7 +12325,7 @@ func (c *Checker) checkInstanceOfExpression(left *ast.Expression, right *ast.Exp if !IsTypeAny(leftType) && c.allTypesAssignableToKind(leftType, TypeFlagsPrimitive) { c.error(left, diagnostics.The_left_hand_side_of_an_instanceof_expression_must_be_of_type_any_an_object_type_or_a_type_parameter) } - signature := c.getResolvedSignature(left.Parent, nil /*candidatesOutArray*/, checkMode) + signature := c.GetResolvedSignature(left.Parent, nil /*candidatesOutArray*/, checkMode) if signature == c.resolvingSignature { // CheckMode.SkipGenericFunctions is enabled and this is a call to a generic function that // returns a function type. We defer checking and return silentNeverType. @@ -27546,7 +27546,7 @@ func (c *Checker) getContextualTypeForArgumentAtIndex(callTarget *ast.Node, argI if c.signatureLinks.Get(callTarget).resolvedSignature == c.resolvingSignature { signature = c.resolvingSignature } else { - signature = c.getResolvedSignature(callTarget, nil, CheckModeNormal) + signature = c.GetResolvedSignature(callTarget, nil, CheckModeNormal) } if isJsxOpeningLikeElement(callTarget) && argIndex == 0 { return c.getEffectiveFirstArgumentForJsxSignature(signature, callTarget) diff --git a/internal/checker/flow.go b/internal/checker/flow.go index 58dcf30b9c..3c597bc855 100644 --- a/internal/checker/flow.go +++ b/internal/checker/flow.go @@ -2040,7 +2040,7 @@ func (c *Checker) getEffectsSignature(node *ast.Node) *Signature { case len(signatures) == 1 && signatures[0].typeParameters == nil: signature = signatures[0] case core.Some(signatures, c.hasTypePredicateOrNeverReturnType): - signature = c.getResolvedSignature(node, nil, CheckModeNormal) + signature = c.GetResolvedSignature(node, nil, CheckModeNormal) } if !(signature != nil && c.hasTypePredicateOrNeverReturnType(signature)) { signature = c.unknownSignature diff --git a/internal/checker/jsx.go b/internal/checker/jsx.go index 10a172eec6..cf7034688c 100644 --- a/internal/checker/jsx.go +++ b/internal/checker/jsx.go @@ -126,7 +126,7 @@ func (c *Checker) checkJsxOpeningLikeElementOrOpeningFragment(node *ast.Node) { c.checkJsxPreconditions(node) c.markJsxAliasReferenced(node) if isNodeOpeningLikeElement { - sig := c.getResolvedSignature(node, nil, CheckModeNormal) + sig := c.GetResolvedSignature(node, nil, CheckModeNormal) c.checkDeprecatedSignature(sig, node) elementTypeConstraint := c.getJsxElementTypeTypeAt(node) if elementTypeConstraint != nil { diff --git a/internal/checker/utilities.go b/internal/checker/utilities.go index 4c22427383..e94ed86e14 100644 --- a/internal/checker/utilities.go +++ b/internal/checker/utilities.go @@ -1688,6 +1688,10 @@ func getNonRestParameterCount(sig *Signature) int { return len(sig.parameters) - core.IfElse(signatureHasRestParameter(sig), 1, 0) } +func GetDeclaration(sig *Signature) *ast.Node { + return sig.declaration +} + func minAndMax[T any](slice []T, getValue func(value T) int) (int, int) { var minValue, maxValue int for i, element := range slice { @@ -1972,7 +1976,7 @@ func tryGetPropertyAccessOrIdentifierToString(expr *ast.Node) string { return "" } -func getInvokedExpression(node *ast.Node) *ast.Node { +func GetInvokedExpression(node *ast.Node) *ast.Node { switch node.Kind { case ast.KindTaggedTemplateExpression: return node.AsTaggedTemplateExpression().Tag diff --git a/internal/ls/definition.go b/internal/ls/definition.go index f4ed52124f..5df50c7e65 100644 --- a/internal/ls/definition.go +++ b/internal/ls/definition.go @@ -3,6 +3,7 @@ package ls 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/scanner" ) @@ -15,6 +16,14 @@ func (l *LanguageService) ProvideDefinitions(fileName string, position int) []Lo } checker := program.GetTypeChecker() + calledDeclaration := tryGetSignatureDeclaration(checker, node) + if calledDeclaration != nil { + name := ast.GetNameOfDeclaration(calledDeclaration) + if name != nil { + return createLocationsFromDeclarations([]*ast.Node{name}) + } + } + if symbol := checker.GetSymbolAtLocation(node); symbol != nil { if symbol.Flags&ast.SymbolFlagsAlias != 0 { if resolved, ok := checker.ResolveAlias(symbol); ok { @@ -22,18 +31,55 @@ func (l *LanguageService) ProvideDefinitions(fileName string, position int) []Lo } } - locations := make([]Location, 0, len(symbol.Declarations)) - for _, decl := range symbol.Declarations { - file := ast.GetSourceFileOfNode(decl) - loc := decl.Loc - pos := scanner.GetTokenPosOfNode(decl, file, false /*includeJSDoc*/) + return createLocationsFromDeclarations(symbol.Declarations) + } + return nil +} + +func createLocationsFromDeclarations(declarations []*ast.Node) []Location { + locations := make([]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, Location{ + FileName: file.FileName(), + Range: core.NewTextRange(pos, loc.End()), + }) + } + return locations +} - locations = append(locations, Location{ - FileName: file.FileName(), - Range: core.NewTextRange(pos, loc.End()), - }) +/** 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 !ast.IsRightSideOfPropertyAccess(n) + }) + + callLike := target.Parent + if callLike != nil && ast.IsCallLikeExpression(callLike) && checker.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, nil, checker.CheckModeNormal) + } + + // Don't go to a function type, go to the value having that type. + var declaration *ast.Node + if signature != nil && checker.GetDeclaration(signature) != nil { + declaration = checker.GetDeclaration(signature) + if ast.IsFunctionLike(declaration) && !ast.IsFunctionTypeNode(declaration) { + return declaration } - return locations } + return nil } diff --git a/internal/ls/definition_test.go b/internal/ls/definition_test.go new file mode 100644 index 0000000000..029f46e2d9 --- /dev/null +++ b/internal/ls/definition_test.go @@ -0,0 +1,74 @@ +package ls_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/microsoft/typescript-go/internal/bundled" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/ls" + "github.com/microsoft/typescript-go/internal/testutil/lstestutil" + "gotest.tools/v3/assert" +) + +type definitionTestCase struct { + name string + files map[string]string + expected map[string][]ls.Location +} + +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 := []definitionTestCase{ + { + name: "localFunction", + files: map[string]string{ + mainFileName: ` +function localFunction() { } +/*localFunction*/localFunction();`, + }, + expected: map[string][]ls.Location{ + "localFunction": {{ + FileName: mainFileName, + Range: core.NewTextRange(9, 22), + }}, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + runDefinitionTest(t, testCase.files, testCase.expected) + }) + } +} + +func runDefinitionTest(t *testing.T, files map[string]string, expected map[string][]ls.Location) { + parsedFiles := make(map[string]string) + var markerPositions map[string]*lstestutil.Marker + for fileName, content := range files { + if fileName == mainFileName { + testData := lstestutil.ParseTestData("", content, fileName) + markerPositions = testData.MarkerPositions + parsedFiles[fileName] = testData.Files[0].Content // !!! Assumes no usage of @filename + } else { + parsedFiles[fileName] = content + } + } + languageService := createLanguageService(mainFileName, parsedFiles) + for markerName, expectedResult := range expected { + marker, ok := markerPositions[markerName] + if !ok { + t.Fatalf("No marker found for '%s'", markerName) + } + locations := languageService.ProvideDefinitions(mainFileName, marker.Position) + assert.DeepEqual(t, locations, expectedResult, cmp.AllowUnexported(core.TextRange{})) + } +}