708 lines
25 KiB
Go
708 lines
25 KiB
Go
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 /// <reference path/types="..."/> 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)
|
|
}
|