package ls import ( "context" "math" "slices" "strings" "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/astnav" "github.com/microsoft/typescript-go/internal/binder" "github.com/microsoft/typescript-go/internal/checker" "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/compiler" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/module" "github.com/microsoft/typescript-go/internal/modulespecifiers" "github.com/microsoft/typescript-go/internal/parser" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" ) func (l *LanguageService) ProvideSourceDefinition( ctx context.Context, documentURI lsproto.DocumentUri, position lsproto.Position, ) (lsproto.DefinitionResponse, error) { caps := lsproto.GetClientCapabilities(ctx) clientSupportsLink := caps.TextDocument.Definition.LinkSupport program, file := l.getProgramAndFile(documentURI) pos := int(l.converters.LineAndCharacterToPosition(file, position)) resolver := l.newSourceDefResolver(program, file.FileName()) node := astnav.GetTouchingPropertyName(file, pos) if node.Kind == ast.KindSourceFile { // Triple-slash directives are comments, not AST nodes, so // GetTouchingPropertyName returns the SourceFile node. if declarations, ref := resolver.resolveTripleSlashReference(file, pos, program); len(declarations) != 0 { originSelectionRange := l.createLspRangeFromBounds(ref.Pos(), ref.End(), file) return l.createDefinitionLocations(originSelectionRange, clientSupportsLink, declarations, nil /*reference*/), nil } return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{}, nil } originSelectionRange := l.createLspRangeFromNode(node, file) // If the cursor is directly on a module specifier string, resolve to the // implementation file's entry point. containingModuleSpecifier := findContainingModuleSpecifier(node) if node == containingModuleSpecifier { specifierMode := program.GetModeForUsageLocation(file, containingModuleSpecifier) if implementationFile := resolver.resolveImplementation(containingModuleSpecifier.Text(), specifierMode); implementationFile != "" { if sourceFile := resolver.getOrParseSourceFile(implementationFile); sourceFile != nil { return l.createDefinitionLocations(originSelectionRange, clientSupportsLink, getSourceDefinitionEntryDeclarations(sourceFile), nil), nil } } return l.provideDefinitionWorker(ctx, documentURI, position) } // Phase 1: Syntactic fast path — when the cursor is inside an // import/require/export, forward-resolve the module specifier to an // implementation file and search it directly. This avoids acquiring // the type checker entirely when the fast path succeeds. var resolvedImplFile string if containingModuleSpecifier != nil { specifierMode := program.GetModeForUsageLocation(file, containingModuleSpecifier) resolvedImplFile = resolver.resolveImplementation(containingModuleSpecifier.Text(), specifierMode) } if resolvedImplFile != "" { names := getCandidateSourceDeclarationNames(node, nil) moduleResults := resolver.searchImplementationFile(node, resolvedImplFile, names) if len(moduleResults) != 0 { if !ast.IsPartOfTypeNode(node) && !ast.IsPartOfTypeOnlyImportOrExportDeclaration(node) || hasConcreteSourceDeclarations(moduleResults) { return l.createDefinitionLocations(originSelectionRange, clientSupportsLink, uniqueDeclarationNodes(moduleResults), nil), nil } } } // Phase 2: Type checker path — acquire the checker for the original file // and use its declarations and module specifier to map to source // implementations. This is the only point where the checker is used; // after this, only the NoDts module resolver and file parsing are needed. checkerDeclarations, moduleSpecifier := getSourceDefCheckerInfo(ctx, program, file, node) // Phase 3: Map checker results to source definitions. declarations := resolver.resolveFromCheckerInfo(node, resolvedImplFile, checkerDeclarations, moduleSpecifier) if len(declarations) == 0 { // If we resolved an implementation file from an import/export but // couldn't find specific declarations, fall back to the file entry // point rather than the standard definition provider — unless the // checker found declarations that are all type-only (e.g. interfaces), // in which case the .d.ts definition is more appropriate. if containingModuleSpecifier != nil && resolvedImplFile != "" && !hasConcreteSourceDeclarations(checkerDeclarations) { if sourceFile := resolver.getOrParseSourceFile(resolvedImplFile); sourceFile != nil { return l.createDefinitionLocations(originSelectionRange, clientSupportsLink, getSourceDefinitionEntryDeclarations(sourceFile), nil), nil } } return l.provideDefinitionWorker(ctx, documentURI, position) } return l.createDefinitionLocations(originSelectionRange, clientSupportsLink, declarations, nil /*reference*/), nil } // sourceDefResolver resolves source definitions by mapping .d.ts declarations // to their implementation files (.js/.ts). It uses the NoDts module resolver // and file parsing for resolution, but never acquires the type checker or // the original program; all checker-dependent work is done before results // are passed in. type sourceDefResolver struct { ls *LanguageService fs vfs.FS options *core.CompilerOptions getSourceFile func(string) *ast.SourceFile resolveFrom string resolver *module.Resolver parsedFiles map[string]*ast.SourceFile } func (l *LanguageService) newSourceDefResolver( program *compiler.Program, resolveFrom string, ) *sourceDefResolver { options := program.Options() noDtsOptions := options.Clone() noDtsOptions.NoDtsResolution = core.TSTrue return &sourceDefResolver{ ls: l, fs: program.Host().FS(), options: options, getSourceFile: program.GetSourceFile, resolveFrom: resolveFrom, resolver: module.NewResolver(program.Host(), noDtsOptions, program.GetGlobalTypingsCacheLocation(), ""), } } // resolveFromCheckerInfo maps type-checker declarations to source // implementations. It uses only the NoDts module resolver and file parsing; // the type checker and original request file are not needed. func (r *sourceDefResolver) resolveFromCheckerInfo( node *ast.Node, resolvedImplFile string, checkerDeclarations []*ast.Node, moduleSpecifier string, ) []*ast.Node { // If we don't yet have a forward-resolved implementation file, try to // recover a module specifier from the checker (e.g. from the import that // brought the symbol into scope, or from the root of an access expression). if resolvedImplFile == "" && moduleSpecifier != "" { resolvedImplFile = r.resolveImplementation(moduleSpecifier, r.inferImpliedNodeFormat(r.resolveFrom)) } // For property access where the checker found no declarations (e.g. // mapped types), search the implementation file for the property name. if len(checkerDeclarations) == 0 && resolvedImplFile != "" { names := getCandidateSourceDeclarationNames(node, nil) if results := r.searchImplementationFile(node, resolvedImplFile, names); results != nil { return uniqueDeclarationNodes(results) } } var declarations []*ast.Node for _, declaration := range checkerDeclarations { declarations = append(declarations, r.mapDeclarationToSource(node, declaration, resolvedImplFile)...) } declarations = uniqueDeclarationNodes(declarations) if hasConcreteSourceDeclarations(declarations) { return declarations } return nil } // getSourceDefCheckerInfo acquires the type checker for the given file and // returns the definition declarations for node along with the module specifier // of the import that brought the symbol into scope (empty if not applicable). func getSourceDefCheckerInfo( ctx context.Context, program *compiler.Program, file *ast.SourceFile, node *ast.Node, ) ([]*ast.Node, string) { c, done := program.GetTypeCheckerForFile(ctx, file) defer done() declarations := getDeclarationsFromLocation(c, node) isPropertyName := node.Parent != nil && ast.IsAccessExpression(node.Parent) && node.Parent.Name() == node if len(declarations) == 0 && isPropertyName { if left := node.Parent.Expression(); left != nil { if prop := c.GetPropertyOfType(c.GetTypeAtLocation(left), node.Text()); prop != nil { declarations = prop.Declarations } } } if calledDeclaration := tryGetSignatureDeclaration(c, node); calledDeclaration != nil { nonFunctionDeclarations := core.Filter(declarations, func(node *ast.Node) bool { return !ast.IsFunctionLike(node) }) declarations = append(nonFunctionDeclarations, calledDeclaration) } // Extract module specifier from the import that brought this symbol into // scope. For property access (obj.prop), walk up the access chain to the // root expression's symbol. var moduleSpecifier string resolveNode := node if isPropertyName { expr := node.Parent.Expression() for expr != nil && ast.IsAccessExpression(expr) { expr = expr.Expression() } if expr != nil { resolveNode = expr } } if sym := c.GetSymbolAtLocation(resolveNode); sym != nil { for _, d := range sym.Declarations { if !ast.IsImportSpecifier(d) && !ast.IsImportClause(d) && !ast.IsNamespaceImport(d) && !ast.IsImportEqualsDeclaration(d) { continue } if spec := checker.TryGetModuleSpecifierFromDeclaration(d); spec != nil { moduleSpecifier = spec.Text() break } } } return declarations, moduleSpecifier } // resolveTripleSlashReference handles /// directives. // For path references to .js files, it returns the entry declarations directly. // For path references to .d.ts files or type references, it uses the NoDts // resolver to find the corresponding implementation file. func (r *sourceDefResolver) resolveTripleSlashReference(file *ast.SourceFile, pos int, program *compiler.Program) ([]*ast.Node, *ast.FileReference) { ref := getReferenceAtPosition(file, pos, program) if ref == nil || ref.file == nil { return nil, nil } // If the referenced file is already an implementation file, return it directly. if !ref.file.IsDeclarationFile { return getSourceDefinitionEntryDeclarations(ref.file), ref.reference } // The referenced file is a .d.ts. Try to find the implementation file // using the NoDts module resolver via findImplementationFileFromDtsFileName. dtsFileName := ref.file.FileName() preferredMode := r.inferImpliedNodeFormat(dtsFileName) implementationFile := r.findImplementationFileFromDtsFileName(dtsFileName, preferredMode) if implementationFile == "" { return nil, nil } sourceFile := r.getOrParseSourceFile(implementationFile) if sourceFile == nil { return nil, nil } return getSourceDefinitionEntryDeclarations(sourceFile), ref.reference } // searchImplementationFile searches an implementation file for declarations // matching the given names. Returns nil when no declarations matched; callers // fall through to the checker path or to the standard definition provider. func (r *sourceDefResolver) searchImplementationFile( originalNode *ast.Node, implementationFile string, names []string, ) []*ast.Node { if implementationFile == "" { return nil } sourceFile := r.getOrParseSourceFile(implementationFile) if sourceFile == nil { return nil } if isDefaultImportName(originalNode) { // For default imports, only search for "default" declarations to avoid // matching unrelated declarations with the same identifier name. defaultDeclarations := r.findDeclarationsInFile(implementationFile, []string{"default"}, &collections.Set[string]{}) if len(defaultDeclarations) != 0 { return filterPreferredSourceDeclarations(originalNode, defaultDeclarations) } return getSourceDefinitionEntryDeclarations(sourceFile) } declarations := r.findDeclarationsInFile(implementationFile, names, &collections.Set[string]{}) if len(declarations) != 0 { return filterPreferredSourceDeclarations(originalNode, declarations) } return nil } func isDefaultImportName(node *ast.Node) bool { if node == nil || node.Parent == nil || !ast.IsImportClause(node.Parent) || node.Parent.Name() != node || node.Parent.Parent == nil { return false } return ast.IsDefaultImport(node.Parent.Parent) } func getSourceDefinitionEntryNode(sourceFile *ast.SourceFile) *ast.Node { if len(sourceFile.Statements.Nodes) != 0 { return sourceFile.Statements.Nodes[0].AsNode() } return sourceFile.AsNode() } func getSourceDefinitionEntryDeclarations(sourceFile *ast.SourceFile) []*ast.Node { return []*ast.Node{getSourceDefinitionEntryNode(sourceFile)} } func (r *sourceDefResolver) mapDeclarationToSource( originalNode *ast.Node, declaration *ast.Node, resolvedImplFile string, ) []*ast.Node { file, startPos := getFileAndStartPosFromDeclaration(declaration) fileName := file.FileName() if mapped := r.ls.tryGetSourcePosition(fileName, startPos); mapped != nil { if sourceFile := r.getOrParseSourceFile(mapped.FileName); sourceFile != nil { return []*ast.Node{findClosestDeclarationNode(sourceFile, mapped.Pos)} } } if !tspath.IsDeclarationFileName(fileName) { return []*ast.Node{declaration} } implementationFile := resolvedImplFile if implementationFile == "" { // Reverse-resolve .d.ts path to implementation file. This path is only // reached for declarations with no associated module specifier (e.g. // globals, ambient declarations, or when forward resolution failed). dtsFileName := ast.GetSourceFileOfNode(declaration).FileName() preferredMode := r.inferImpliedNodeFormat(dtsFileName) implementationFile = r.findImplementationFileFromDtsFileName(dtsFileName, preferredMode) } return r.searchImplementationFile(originalNode, implementationFile, getCandidateSourceDeclarationNames(originalNode, declaration)) } func (r *sourceDefResolver) findImplementationFileFromDtsFileName( dtsFileName string, preferredMode core.ResolutionMode, ) string { if jsExt := module.TryGetJSExtensionForFile(dtsFileName, r.options); jsExt != "" { candidate := tspath.ChangeExtension(dtsFileName, jsExt) if r.fs.FileExists(candidate) { return candidate } } parts := modulespecifiers.GetNodeModulePathParts(dtsFileName) if parts == nil { return "" } // Ensure the file only contains one /node_modules/ segment. If there's more // than one, the package name extraction may be incorrect, so bail out. if strings.LastIndex(dtsFileName, "/node_modules/") != parts.TopLevelNodeModulesIndex { return "" } packageNamePathPart := dtsFileName[parts.TopLevelPackageNameIndex+1 : parts.PackageRootIndex] packageName := module.GetPackageNameFromTypesPackageName(module.UnmangleScopedPackageName(packageNamePathPart)) if packageName == "" { return "" } pathToFileInPackage := dtsFileName[parts.PackageRootIndex+1:] // Try resolving as a package subpath first (e.g. "pkg/dist/utils"), then // fall back to the bare package name (e.g. "pkg"). This covers both main // entrypoints and deep imports without needing to inspect package.json // entrypoints. if pathToFileInPackage != "" { specifier := packageName + "/" + tspath.RemoveFileExtension(pathToFileInPackage) if implementationFile := r.resolveImplementation(specifier, preferredMode); implementationFile != "" { return implementationFile } } return r.resolveImplementation(packageName, preferredMode) } func (r *sourceDefResolver) resolveImplementation( moduleName string, preferredMode core.ResolutionMode, ) string { return r.resolveImplementationFrom(moduleName, r.resolveFrom, preferredMode) } func (r *sourceDefResolver) resolveImplementationFrom( moduleName string, resolveFromFile string, preferredMode core.ResolutionMode, ) string { modes := []core.ResolutionMode{preferredMode} if preferredMode != core.ModuleKindESNext { modes = append(modes, core.ModuleKindESNext) } if preferredMode != core.ModuleKindCommonJS { modes = append(modes, core.ModuleKindCommonJS) } for _, mode := range modes { resolved, _ := r.resolver.ResolveModuleName(moduleName, resolveFromFile, mode, nil) if resolved != nil && resolved.IsResolved() && !tspath.IsDeclarationFileName(resolved.ResolvedFileName) { return resolved.ResolvedFileName } } return "" } func (r *sourceDefResolver) getOrParseSourceFile(fileName string) *ast.SourceFile { if sourceFile := r.getSourceFile(fileName); sourceFile != nil { return sourceFile } if sourceFile, ok := r.parsedFiles[fileName]; ok { return sourceFile } var sourceFile *ast.SourceFile if text, ok := r.ls.ReadFile(fileName); ok { sourceFile = parser.ParseSourceFile( ast.SourceFileParseOptions{FileName: fileName, Path: r.ls.toPath(fileName)}, text, core.GetScriptKindFromFileName(fileName), ) binder.BindSourceFile(sourceFile) } if r.parsedFiles == nil { r.parsedFiles = map[string]*ast.SourceFile{} } r.parsedFiles[fileName] = sourceFile return sourceFile } // inferImpliedNodeFormat determines the module format for a source file that may not be // in the program, using the file extension and nearest package.json "type" field. func (r *sourceDefResolver) inferImpliedNodeFormat(fileName string) core.ResolutionMode { var packageJsonType string if scope := r.resolver.GetPackageScopeForPath(tspath.GetDirectoryPath(fileName)); scope.Exists() { if value, ok := scope.Contents.Type.GetValue(); ok { packageJsonType = value } } return ast.GetImpliedNodeFormatForFile(fileName, packageJsonType) } func findContainingModuleSpecifier(node *ast.Node) *ast.Node { for current := node; current != nil; current = current.Parent { if ast.IsAnyImportOrReExport(current) || ast.IsRequireCall(current, true /*requireStringLiteralLikeArgument*/) || ast.IsImportCall(current) { if moduleSpecifier := ast.GetExternalModuleName(current); moduleSpecifier != nil && ast.IsStringLiteralLike(moduleSpecifier) { return moduleSpecifier } } } return nil } func (r *sourceDefResolver) findDeclarationsInFile( fileName string, names []string, seen *collections.Set[string], ) []*ast.Node { if fileName == "" || len(names) == 0 { return nil } if !seen.AddIfAbsent(fileName) { return nil } sourceFile := r.getOrParseSourceFile(fileName) if sourceFile == nil { return nil } declarations := findDeclarationNodesByName(sourceFile, names) if len(declarations) != 0 && hasConcreteSourceDeclarations(declarations) { return declarations } var forwarded []*ast.Node for _, forwardedFile := range r.getForwardedImplementationFiles(sourceFile) { forwarded = append(forwarded, r.findDeclarationsInFile(forwardedFile, names, seen)...) } if len(forwarded) != 0 { if hasConcreteSourceDeclarations(forwarded) { return uniqueDeclarationNodes(forwarded) } return uniqueDeclarationNodes(append(slices.Clip(declarations), forwarded...)) } return declarations } func (r *sourceDefResolver) getForwardedImplementationFiles(sourceFile *ast.SourceFile) []string { preferredMode := r.inferImpliedNodeFormat(sourceFile.FileName()) var files []string for _, imp := range sourceFile.Imports() { moduleName := imp.Text() if implementationFile := r.resolveImplementationFrom(moduleName, sourceFile.FileName(), preferredMode); implementationFile != "" { files = append(files, implementationFile) } } return core.Deduplicate(files) } func getCandidateSourceDeclarationNames(originalNode *ast.Node, declaration *ast.Node) []string { var names []string if declaration != nil { if name := ast.GetNameOfDeclaration(declaration); name != nil { if text := ast.GetTextOfPropertyName(name); text != "" { names = append(names, text) } } if declaration.Kind == ast.KindExportAssignment { names = append(names, "default") } if (ast.IsFunctionDeclaration(declaration) || ast.IsClassDeclaration(declaration)) && declaration.ModifierFlags()&ast.ModifierFlagsExportDefault == ast.ModifierFlagsExportDefault { names = append(names, "default") } if ast.IsImportSpecifier(declaration) || ast.IsExportSpecifier(declaration) { if propName := declaration.PropertyName(); propName != nil { names = append(names, propName.Text()) } } } if originalNode != nil { if ast.IsIdentifier(originalNode) || ast.IsPrivateIdentifier(originalNode) { names = append(names, originalNode.Text()) } if isDefaultImportName(originalNode) { names = append(names, "default") } if originalNode.Parent != nil { if ast.IsImportSpecifier(originalNode.Parent) || ast.IsExportSpecifier(originalNode.Parent) { if propName := originalNode.Parent.PropertyName(); propName != nil { names = append(names, propName.Text()) } } } } return names } func findDeclarationNodesByName(sourceFile *ast.SourceFile, names []string) []*ast.Node { names = core.Deduplicate(core.Filter(names, func(name string) bool { return name != "" })) if len(names) == 0 { return nil } var wanted collections.Set[string] wantDefault := false for _, name := range names { if name == "default" { wantDefault = true continue } wanted.Add(name) } type candidate struct { node *ast.Node depth int } var candidates []candidate minDepth := math.MaxInt var visit ast.Visitor visit = func(node *ast.Node) bool { matched := false if name := ast.GetNameOfDeclaration(node); name != nil { if text := ast.GetTextOfPropertyName(name); text != "" { if wanted.Has(text) { matched = true } } } if wantDefault && node.Kind == ast.KindExportAssignment { matched = true } if wantDefault && (ast.IsFunctionDeclaration(node) || ast.IsClassDeclaration(node)) && node.ModifierFlags()&ast.ModifierFlagsExportDefault == ast.ModifierFlagsExportDefault { matched = true } if matched { depth := getContainerDepth(node) candidates = append(candidates, candidate{node: node, depth: depth}) if depth < minDepth { minDepth = depth } } return node.ForEachChild(visit) } sourceFile.AsNode().ForEachChild(visit) // Only keep declarations at the shallowest depth, like getTopMostDeclarationNamesInFile. var declarations []*ast.Node for _, c := range candidates { if c.depth == minDepth { declarations = append(declarations, c.node) } } return uniqueDeclarationNodes(declarations) } // getContainerDepth counts the number of container nodes above a declaration, // matching the behavior of getDepth in getTopMostDeclarationNamesInFile. func getContainerDepth(node *ast.Node) int { depth := 0 current := node for current != nil { current = getContainerNode(current) depth++ } return depth } func filterPreferredSourceDeclarations(originalNode *ast.Node, declarations []*ast.Node) []*ast.Node { if len(declarations) <= 1 || originalNode == nil { return declarations } if preferred := getPropertyLikeSourceDeclarations(originalNode, declarations); len(preferred) != 0 { return preferred } if preferred := core.Filter(declarations, isConcreteSourceDeclaration); len(preferred) != 0 { return preferred } return declarations } func getPropertyLikeSourceDeclarations(originalNode *ast.Node, declarations []*ast.Node) []*ast.Node { if originalNode.Parent == nil || !ast.IsAccessExpression(originalNode.Parent) || originalNode.Parent.Name() != originalNode { return nil } return core.Filter(declarations, func(node *ast.Node) bool { switch node.Kind { case ast.KindPropertyAssignment, ast.KindShorthandPropertyAssignment, ast.KindPropertyDeclaration, ast.KindPropertySignature, ast.KindMethodDeclaration, ast.KindMethodSignature, ast.KindGetAccessor, ast.KindSetAccessor, ast.KindEnumMember: return true default: return false } }) } func hasConcreteSourceDeclarations(declarations []*ast.Node) bool { return slices.ContainsFunc(declarations, isConcreteSourceDeclaration) } func isConcreteSourceDeclaration(node *ast.Node) bool { if !ast.IsDeclaration(node) || node.Kind == ast.KindExportAssignment { return false } if (ast.IsBinaryExpression(node) || ast.IsCallExpression(node)) && ast.GetAssignmentDeclarationKind(node) != ast.JSDeclarationKindNone { return false } switch node.Kind { case ast.KindParameter, ast.KindTypeParameter, ast.KindBindingElement, ast.KindImportClause, ast.KindImportSpecifier, ast.KindNamespaceImport, ast.KindExportSpecifier, ast.KindPropertyAccessExpression, ast.KindElementAccessExpression: return false default: return true } } func uniqueDeclarationNodes(nodes []*ast.Node) []*ast.Node { type declarationKey struct { fileName string loc core.TextRange } var seen collections.Set[declarationKey] result := make([]*ast.Node, 0, len(nodes)) for _, node := range nodes { if node == nil { continue } fileName := ast.GetSourceFileOfNode(node).FileName() key := declarationKey{fileName: fileName, loc: node.Loc} if !seen.AddIfAbsent(key) { continue } result = append(result, node) } return result } func findClosestDeclarationNode(sourceFile *ast.SourceFile, pos int) *ast.Node { node := astnav.GetTouchingPropertyName(sourceFile, pos) for current := node; current != nil; current = current.Parent { if ast.IsDeclaration(current) || current.Kind == ast.KindExportAssignment { return current } } return getSourceDefinitionEntryNode(sourceFile) }