diff --git a/kitcom/internal/tsgo/ls/api.go b/kitcom/internal/tsgo/ls/api.go deleted file mode 100644 index 4c2924f..0000000 --- a/kitcom/internal/tsgo/ls/api.go +++ /dev/null @@ -1,44 +0,0 @@ -package ls - -import ( - "context" - "errors" - "fmt" - - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/checker" -) - -var ( - ErrNoSourceFile = errors.New("source file not found") - ErrNoTokenAtPosition = errors.New("no token found at position") -) - -func (l *LanguageService) GetSymbolAtPosition(ctx context.Context, fileName string, position int) (*ast.Symbol, error) { - program, file := l.tryGetProgramAndFile(fileName) - if file == nil { - return nil, fmt.Errorf("%w: %s", ErrNoSourceFile, fileName) - } - node := astnav.GetTokenAtPosition(file, position) - if node == nil { - return nil, fmt.Errorf("%w: %s:%d", ErrNoTokenAtPosition, fileName, position) - } - checker, done := program.GetTypeCheckerForFile(ctx, file) - defer done() - return checker.GetSymbolAtLocation(node), nil -} - -func (l *LanguageService) GetSymbolAtLocation(ctx context.Context, node *ast.Node) *ast.Symbol { - program := l.GetProgram() - checker, done := program.GetTypeCheckerForFile(ctx, ast.GetSourceFileOfNode(node)) - defer done() - return checker.GetSymbolAtLocation(node) -} - -func (l *LanguageService) GetTypeOfSymbol(ctx context.Context, symbol *ast.Symbol) *checker.Type { - program := l.GetProgram() - checker, done := program.GetTypeChecker(ctx) - defer done() - return checker.GetTypeOfSymbolAtLocation(symbol, nil) -} diff --git a/kitcom/internal/tsgo/ls/autoImports_stringer_generated.go b/kitcom/internal/tsgo/ls/autoImports_stringer_generated.go deleted file mode 100644 index d39f192..0000000 --- a/kitcom/internal/tsgo/ls/autoImports_stringer_generated.go +++ /dev/null @@ -1,27 +0,0 @@ -// Code generated by "stringer -type=ExportKind -output=autoImports_stringer_generated.go"; DO NOT EDIT. - -package ls - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[ExportKindNamed-0] - _ = x[ExportKindDefault-1] - _ = x[ExportKindExportEquals-2] - _ = x[ExportKindUMD-3] - _ = x[ExportKindModule-4] -} - -const _ExportKind_name = "ExportKindNamedExportKindDefaultExportKindExportEqualsExportKindUMDExportKindModule" - -var _ExportKind_index = [...]uint8{0, 15, 32, 54, 67, 83} - -func (i ExportKind) String() string { - if i < 0 || i >= ExportKind(len(_ExportKind_index)-1) { - return "ExportKind(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _ExportKind_name[_ExportKind_index[i]:_ExportKind_index[i+1]] -} diff --git a/kitcom/internal/tsgo/ls/autoimportfixes.go b/kitcom/internal/tsgo/ls/autoimportfixes.go deleted file mode 100644 index 2790714..0000000 --- a/kitcom/internal/tsgo/ls/autoimportfixes.go +++ /dev/null @@ -1,325 +0,0 @@ -package ls - -import ( - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/debug" -) - -type Import struct { - name string - kind ImportKind // ImportKindCommonJS | ImportKindNamespace - addAsTypeOnly AddAsTypeOnly - propertyName string // Use when needing to generate an `ImportSpecifier with a `propertyName`; the name preceding "as" keyword (propertyName = "" when "as" is absent) -} - -func (ct *changeTracker) addNamespaceQualifier(sourceFile *ast.SourceFile, qualification *Qualification) { - ct.insertText(sourceFile, qualification.usagePosition, qualification.namespacePrefix+".") -} - -func (ct *changeTracker) doAddExistingFix( - sourceFile *ast.SourceFile, - clause *ast.Node, // ImportClause | ObjectBindingPattern, - defaultImport *Import, - namedImports []*Import, - // removeExistingImportSpecifiers *core.Set[ImportSpecifier | BindingElement] // !!! remove imports not implemented - preferences *UserPreferences, -) { - switch clause.Kind { - case ast.KindObjectBindingPattern: - if clause.Kind == ast.KindObjectBindingPattern { - // bindingPattern := clause.AsBindingPattern() - // !!! adding *and* removing imports not implemented - // if (removeExistingImportSpecifiers && core.Some(bindingPattern.Elements, func(e *ast.Node) bool { - // return removeExistingImportSpecifiers.Has(e) - // })) { - // If we're both adding and removing elements, just replace and reprint the whole - // node. The change tracker doesn't understand all the operations and can insert or - // leave behind stray commas. - // ct.replaceNode( - // sourceFile, - // bindingPattern, - // ct.NodeFactory.NewObjectBindingPattern([ - // ...bindingPattern.Elements.Filter(func(e *ast.Node) bool { - // return !removeExistingImportSpecifiers.Has(e) - // }), - // ...defaultImport ? [ct.NodeFactory.createBindingElement(/*dotDotDotToken*/ nil, /*propertyName*/ "default", defaultImport.name)] : emptyArray, - // ...namedImports.map(i => ct.NodeFactory.createBindingElement(/*dotDotDotToken*/ nil, i.propertyName, i.name)), - // ]), - // ) - // return - // } - if defaultImport != nil { - ct.addElementToBindingPattern(sourceFile, clause, defaultImport.name, ptrTo("default")) - } - for _, specifier := range namedImports { - ct.addElementToBindingPattern(sourceFile, clause, specifier.name, &specifier.propertyName) - } - return - } - case ast.KindImportClause: - - importClause := clause.AsImportClause() - - // promoteFromTypeOnly = true if we need to promote the entire original clause from type only - promoteFromTypeOnly := importClause.IsTypeOnly() && core.Some(append(namedImports, defaultImport), func(i *Import) bool { - if i == nil { - return false - } - return i.addAsTypeOnly == AddAsTypeOnlyNotAllowed - }) - - existingSpecifiers := []*ast.Node{} // []*ast.ImportSpecifier - if importClause.NamedBindings != nil && importClause.NamedBindings.Kind == ast.KindNamedImports { - existingSpecifiers = importClause.NamedBindings.Elements() - } - - if defaultImport != nil { - debug.Assert(clause.Name() == nil, "Cannot add a default import to an import clause that already has one") - ct.insertNodeAt(sourceFile, core.TextPos(astnav.GetStartOfNode(clause, sourceFile, false)), ct.NodeFactory.NewIdentifier(defaultImport.name), changeNodeOptions{suffix: ", "}) - } - - if len(namedImports) > 0 { - // !!! OrganizeImports not yet implemented - // specifierComparer, isSorted := OrganizeImports.getNamedImportSpecifierComparerWithDetection(importClause.Parent, preferences, sourceFile); - newSpecifiers := core.Map(namedImports, func(namedImport *Import) *ast.Node { - var identifier *ast.Node - if namedImport.propertyName != "" { - identifier = ct.NodeFactory.NewIdentifier(namedImport.propertyName).AsIdentifier().AsNode() - } - return ct.NodeFactory.NewImportSpecifier( - (!importClause.IsTypeOnly() || promoteFromTypeOnly) && shouldUseTypeOnly(namedImport.addAsTypeOnly, preferences), - identifier, - ct.NodeFactory.NewIdentifier(namedImport.name), - ) - }) // !!! sort with specifierComparer - - // !!! remove imports not implemented - // if (removeExistingImportSpecifiers) { - // // If we're both adding and removing specifiers, just replace and reprint the whole - // // node. The change tracker doesn't understand all the operations and can insert or - // // leave behind stray commas. - // ct.replaceNode( - // sourceFile, - // importClause.NamedBindings, - // ct.NodeFactory.updateNamedImports( - // importClause.NamedBindings.AsNamedImports(), - // append(core.Filter(existingSpecifiers, func (s *ast.ImportSpecifier) bool {return !removeExistingImportSpecifiers.Has(s)}), newSpecifiers...), // !!! sort with specifierComparer - // ), - // ); - // } else if (len(existingSpecifiers) > 0 && isSorted != false) { - // !!! OrganizeImports not implemented - // The sorting preference computed earlier may or may not have validated that these particular - // import specifiers are sorted. If they aren't, `getImportSpecifierInsertionIndex` will return - // nonsense. So if there are existing specifiers, even if we know the sorting preference, we - // need to ensure that the existing specifiers are sorted according to the preference in order - // to do a sorted insertion. - // changed to check if existing specifiers are sorted - // if we're promoting the clause from type-only, we need to transform the existing imports before attempting to insert the new named imports - // transformedExistingSpecifiers := existingSpecifiers - // if promoteFromTypeOnly && existingSpecifiers { - // transformedExistingSpecifiers = ct.NodeFactory.updateNamedImports( - // importClause.NamedBindings.AsNamedImports(), - // core.SameMap(existingSpecifiers, func(e *ast.ImportSpecifier) *ast.ImportSpecifier { - // return ct.NodeFactory.updateImportSpecifier(e, /*isTypeOnly*/ true, e.propertyName, e.name) - // }), - // ).elements - // } - // for _, spec := range newSpecifiers { - // insertionIndex := OrganizeImports.getImportSpecifierInsertionIndex(transformedExistingSpecifiers, spec, specifierComparer); - // ct.insertImportSpecifierAtIndex(sourceFile, spec, importClause.namedBindings as NamedImports, insertionIndex); - // } - // } else - if len(existingSpecifiers) > 0 { - for _, spec := range newSpecifiers { - ct.insertNodeInListAfter(sourceFile, existingSpecifiers[len(existingSpecifiers)-1], spec.AsNode(), existingSpecifiers) - } - } else { - if len(newSpecifiers) > 0 { - namedImports := ct.NodeFactory.NewNamedImports(ct.NodeFactory.NewNodeList(newSpecifiers)) - if importClause.NamedBindings != nil { - ct.replaceNode(sourceFile, importClause.NamedBindings, namedImports, nil) - } else { - if clause.Name() == nil { - panic("Import clause must have either named imports or a default import") - } - ct.insertNodeAfter(sourceFile, clause.Name(), namedImports) - } - } - } - } - - if promoteFromTypeOnly { - // !!! promote type-only imports not implemented - - // ct.delete(sourceFile, getTypeKeywordOfTypeOnlyImport(clause, sourceFile)); - // if (existingSpecifiers) { - // // We used to convert existing specifiers to type-only only if compiler options indicated that - // // would be meaningful (see the `importNameElisionDisabled` utility function), but user - // // feedback indicated a preference for preserving the type-onlyness of existing specifiers - // // regardless of whether it would make a difference in emit. - // for _, specifier := range existingSpecifiers { - // ct.insertModifierBefore(sourceFile, SyntaxKind.TypeKeyword, specifier); - // } - // } - } - default: - panic("Unsupported clause kind: " + clause.Kind.String() + "for doAddExistingFix") - } -} - -func (ct *changeTracker) addElementToBindingPattern(sourceFile *ast.SourceFile, bindingPattern *ast.Node, name string, propertyName *string) { - element := ct.newBindingElementFromNameAndPropertyName(name, propertyName) - if len(bindingPattern.Elements()) > 0 { - ct.insertNodeInListAfter(sourceFile, bindingPattern.Elements()[len(bindingPattern.Elements())-1], element, nil) - } else { - ct.replaceNode(sourceFile, bindingPattern, ct.NodeFactory.NewBindingPattern( - ast.KindObjectBindingPattern, - ct.NodeFactory.NewNodeList([]*ast.Node{element}), - ), nil) - } -} - -func (ct *changeTracker) newBindingElementFromNameAndPropertyName(name string, propertyName *string) *ast.Node { - var newPropertyNameIdentifier *ast.Node - if propertyName != nil { - newPropertyNameIdentifier = ct.NodeFactory.NewIdentifier(*propertyName) - } - return ct.NodeFactory.NewBindingElement( - nil, /*dotDotDotToken*/ - newPropertyNameIdentifier, - ct.NodeFactory.NewIdentifier(name), - nil, /* initializer */ - ) -} - -func (ct *changeTracker) insertImports(sourceFile *ast.SourceFile, imports []*ast.Statement, blankLineBetween bool, preferences *UserPreferences) { - var existingImportStatements []*ast.Statement - - if imports[0].Kind == ast.KindVariableStatement { - existingImportStatements = core.Filter(sourceFile.Statements.Nodes, ast.IsRequireVariableStatement) - } else { - existingImportStatements = core.Filter(sourceFile.Statements.Nodes, ast.IsAnyImportSyntax) - } - // !!! OrganizeImports - // { comparer, isSorted } := OrganizeImports.getOrganizeImportsStringComparerWithDetection(existingImportStatements, preferences); - // sortedNewImports := isArray(imports) ? toSorted(imports, (a, b) => OrganizeImports.compareImportsOrRequireStatements(a, b, comparer)) : [imports]; - sortedNewImports := imports - // !!! FutureSourceFile - // if !isFullSourceFile(sourceFile) { - // for _, newImport := range sortedNewImports { - // // Insert one at a time to send correct original source file for accurate text reuse - // // when some imports are cloned from existing ones in other files. - // ct.insertStatementsInNewFile(sourceFile.fileName, []*ast.Node{newImport}, ast.GetSourceFileOfNode(getOriginalNode(newImport))) - // } - // return; - // } - - // if len(existingImportStatements) > 0 && isSorted { - // for _, newImport := range sortedNewImports { - // insertionIndex := OrganizeImports.getImportDeclarationInsertionIndex(existingImportStatements, newImport, comparer) - // if insertionIndex == 0 { - // // If the first import is top-of-file, insert after the leading comment which is likely the header. - // options := existingImportStatements[0] == sourceFile.statements[0] ? { leadingTriviaOption: textchanges.LeadingTriviaOption.Exclude } : {}; - // ct.insertNodeBefore(sourceFile, existingImportStatements[0], newImport, /*blankLineBetween*/ false, options); - // } else { - // prevImport := existingImportStatements[insertionIndex - 1] - // ct.insertNodeAfter(sourceFile, prevImport, newImport); - // } - // } - // return - // } - - if len(existingImportStatements) > 0 { - ct.insertNodesAfter(sourceFile, existingImportStatements[len(existingImportStatements)-1], sortedNewImports) - } else { - ct.insertAtTopOfFile(sourceFile, sortedNewImports, blankLineBetween) - } -} - -func (ct *changeTracker) makeImport(defaultImport *ast.IdentifierNode, namedImports []*ast.Node, moduleSpecifier *ast.Expression, isTypeOnly bool) *ast.Statement { - var newNamedImports *ast.Node - if len(namedImports) > 0 { - newNamedImports = ct.NodeFactory.NewNamedImports(ct.NodeFactory.NewNodeList(namedImports)) - } - var importClause *ast.Node - if defaultImport != nil || newNamedImports != nil { - importClause = ct.NodeFactory.NewImportClause(core.IfElse(isTypeOnly, ast.KindTypeKeyword, ast.KindUnknown), defaultImport, newNamedImports) - } - return ct.NodeFactory.NewImportDeclaration( /*modifiers*/ nil, importClause, moduleSpecifier, nil /*attributes*/) -} - -func (ct *changeTracker) getNewImports( - moduleSpecifier string, - // quotePreference quotePreference, // !!! quotePreference - defaultImport *Import, - namedImports []*Import, - namespaceLikeImport *Import, // { importKind: ImportKind.CommonJS | ImportKind.Namespace; } - compilerOptions *core.CompilerOptions, - preferences *UserPreferences, -) []*ast.Statement { - moduleSpecifierStringLiteral := ct.NodeFactory.NewStringLiteral(moduleSpecifier) - var statements []*ast.Statement // []AnyImportSyntax - if defaultImport != nil || len(namedImports) > 0 { - // `verbatimModuleSyntax` should prefer top-level `import type` - - // even though it's not an error, it would add unnecessary runtime emit. - topLevelTypeOnly := (defaultImport == nil || needsTypeOnly(defaultImport.addAsTypeOnly)) && - core.Every(namedImports, func(i *Import) bool { return needsTypeOnly(i.addAsTypeOnly) }) || - (compilerOptions.VerbatimModuleSyntax.IsTrue() || preferences.PreferTypeOnlyAutoImports) && - defaultImport != nil && defaultImport.addAsTypeOnly != AddAsTypeOnlyNotAllowed && !core.Some(namedImports, func(i *Import) bool { return i.addAsTypeOnly == AddAsTypeOnlyNotAllowed }) - - var defaultImportNode *ast.Node - if defaultImport != nil { - defaultImportNode = ct.NodeFactory.NewIdentifier(defaultImport.name) - } - - statements = append(statements, ct.makeImport(defaultImportNode, core.Map(namedImports, func(namedImport *Import) *ast.Node { - var namedImportPropertyName *ast.Node - if namedImport.propertyName != "" { - namedImportPropertyName = ct.NodeFactory.NewIdentifier(namedImport.propertyName) - } - return ct.NodeFactory.NewImportSpecifier( - !topLevelTypeOnly && shouldUseTypeOnly(namedImport.addAsTypeOnly, preferences), - namedImportPropertyName, - ct.NodeFactory.NewIdentifier(namedImport.name), - ) - }), moduleSpecifierStringLiteral, topLevelTypeOnly)) - } - - if namespaceLikeImport != nil { - var declaration *ast.Statement - if namespaceLikeImport.kind == ImportKindCommonJS { - declaration = ct.NodeFactory.NewImportEqualsDeclaration( - /*modifiers*/ nil, - shouldUseTypeOnly(namespaceLikeImport.addAsTypeOnly, preferences), - ct.NodeFactory.NewIdentifier(namespaceLikeImport.name), - ct.NodeFactory.NewExternalModuleReference(moduleSpecifierStringLiteral), - ) - } else { - declaration = ct.NodeFactory.NewImportDeclaration( - /*modifiers*/ nil, - ct.NodeFactory.NewImportClause( - /*phaseModifier*/ core.IfElse(shouldUseTypeOnly(namespaceLikeImport.addAsTypeOnly, preferences), ast.KindTypeKeyword, ast.KindUnknown), - /*name*/ nil, - ct.NodeFactory.NewNamespaceImport(ct.NodeFactory.NewIdentifier(namespaceLikeImport.name)), - ), - moduleSpecifierStringLiteral, - /*attributes*/ nil, - ) - } - statements = append(statements, declaration) - } - if len(statements) == 0 { - panic("No statements to insert for new imports") - } - return statements -} - -func needsTypeOnly(addAsTypeOnly AddAsTypeOnly) bool { - return addAsTypeOnly == AddAsTypeOnlyRequired -} - -func shouldUseTypeOnly(addAsTypeOnly AddAsTypeOnly, preferences *UserPreferences) bool { - return needsTypeOnly(addAsTypeOnly) || addAsTypeOnly != AddAsTypeOnlyNotAllowed && preferences.PreferTypeOnlyAutoImports -} diff --git a/kitcom/internal/tsgo/ls/autoimports.go b/kitcom/internal/tsgo/ls/autoimports.go deleted file mode 100644 index d484d25..0000000 --- a/kitcom/internal/tsgo/ls/autoimports.go +++ /dev/null @@ -1,1490 +0,0 @@ -package ls - -import ( - "context" - "fmt" - "strings" - - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/binder" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/checker" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/compiler" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/debug" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/diagnostics" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/module" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/modulespecifiers" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/stringutil" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath" -) - -type SymbolExportInfo struct { - symbol *ast.Symbol - moduleSymbol *ast.Symbol - moduleFileName string - exportKind ExportKind - targetFlags ast.SymbolFlags - isFromPackageJson bool -} - -type symbolExportEntry struct { - symbol *ast.Symbol - moduleSymbol *ast.Symbol -} - -type ExportInfoMapKey struct { - SymbolName string - SymbolId ast.SymbolId - AmbientModuleName string - ModuleFile tspath.Path -} - -func newExportInfoMapKey(importedName string, symbol *ast.Symbol, ambientModuleNameKey string, ch *checker.Checker) ExportInfoMapKey { - return ExportInfoMapKey{ - SymbolName: importedName, - SymbolId: ast.GetSymbolId(ch.SkipAlias(symbol)), - AmbientModuleName: ambientModuleNameKey, - } -} - -type CachedSymbolExportInfo struct { - // Used to rehydrate `symbol` and `moduleSymbol` when transient - id int - symbolTableKey string - symbolName string - capitalizedSymbolName string - moduleName string - moduleFile *ast.SourceFile // may be nil - packageName string - - symbol *ast.Symbol // may be nil - moduleSymbol *ast.Symbol // may be nil - moduleFileName string // may be "" - targetFlags ast.SymbolFlags - exportKind ExportKind - isFromPackageJson bool -} - -type exportInfoMap struct { - exportInfo collections.MultiMap[ExportInfoMapKey, CachedSymbolExportInfo] - symbols map[int]symbolExportEntry - exportInfoId int - usableByFileName tspath.Path - packages map[string]string - - globalTypingsCacheLocation string - - // !!! releaseSymbols func() - // !!! onFileChanged func(oldSourceFile *ast.SourceFile, newSourceFile *ast.SourceFile, typeAcquisitionEnabled bool) bool -} - -func (e *exportInfoMap) clear() { - e.symbols = map[int]symbolExportEntry{} - e.exportInfo = collections.MultiMap[ExportInfoMapKey, CachedSymbolExportInfo]{} - e.usableByFileName = "" -} - -func (e *exportInfoMap) get(importingFile tspath.Path, ch *checker.Checker, key ExportInfoMapKey) []*SymbolExportInfo { - if e.usableByFileName != importingFile { - return nil - } - return core.Map(e.exportInfo.Get(key), func(info CachedSymbolExportInfo) *SymbolExportInfo { return e.rehydrateCachedInfo(ch, info) }) -} - -func (e *exportInfoMap) add( - importingFile tspath.Path, - symbol *ast.Symbol, - symbolTableKey string, - moduleSymbol *ast.Symbol, - moduleFile *ast.SourceFile, - exportKind ExportKind, - isFromPackageJson bool, - ch *checker.Checker, - symbolNameMatch func(string) bool, - flagMatch func(ast.SymbolFlags) bool, -) { - if importingFile != e.usableByFileName { - e.clear() - e.usableByFileName = importingFile - } - - packageName := "" - if moduleFile != nil { - if nodeModulesPathParts := modulespecifiers.GetNodeModulePathParts(moduleFile.FileName()); nodeModulesPathParts != nil { - topLevelNodeModulesIndex := nodeModulesPathParts.TopLevelNodeModulesIndex - topLevelPackageNameIndex := nodeModulesPathParts.TopLevelPackageNameIndex - packageRootIndex := nodeModulesPathParts.PackageRootIndex - packageName = module.UnmangleScopedPackageName(modulespecifiers.GetPackageNameFromTypesPackageName(moduleFile.FileName()[topLevelPackageNameIndex+1 : packageRootIndex])) - if strings.HasPrefix(string(importingFile), string(moduleFile.Path())[0:topLevelNodeModulesIndex]) { - nodeModulesPath := moduleFile.FileName()[0 : topLevelPackageNameIndex+1] - if prevDeepestNodeModulesPath, ok := e.packages[packageName]; ok { - prevDeepestNodeModulesIndex := strings.Index(prevDeepestNodeModulesPath, "/node_modules/") - if topLevelNodeModulesIndex > prevDeepestNodeModulesIndex { - e.packages[packageName] = nodeModulesPath - } - } else { - e.packages[packageName] = nodeModulesPath - } - } - } - } - - isDefault := exportKind == ExportKindDefault - namedSymbol := symbol - if isDefault { - if s := binder.GetLocalSymbolForExportDefault(symbol); s != nil { - namedSymbol = s - } - } - // 1. A named export must be imported by its key in `moduleSymbol.exports` or `moduleSymbol.members`. - // 2. A re-export merged with an export from a module augmentation can result in `symbol` - // being an external module symbol; the name it is re-exported by will be `symbolTableKey` - // (which comes from the keys of `moduleSymbol.exports`.) - // 3. Otherwise, we have a default/namespace import that can be imported by any name, and - // `symbolTableKey` will be something undesirable like `export=` or `default`, so we try to - // get a better name. - names := []string{} - if exportKind == ExportKindNamed || checker.IsExternalModuleSymbol(namedSymbol) { - names = append(names, symbolTableKey) - } else { - names = getNamesForExportedSymbol(namedSymbol, ch, core.ScriptTargetNone) - } - - symbolName := names[0] - if symbolNameMatch != nil && !symbolNameMatch(symbolName) { - return - } - - capitalizedSymbolName := "" - if len(names) > 1 { - capitalizedSymbolName = names[1] - } - - moduleName := stringutil.StripQuotes(moduleSymbol.Name) - id := e.exportInfoId + 1 - target := ch.SkipAlias(symbol) - - if flagMatch != nil && !flagMatch(target.Flags) { - return - } - - var storedSymbol, storedModuleSymbol *ast.Symbol - - if symbol.Flags&ast.SymbolFlagsTransient == 0 { - storedSymbol = symbol - } - if moduleSymbol.Flags&ast.SymbolFlagsTransient == 0 { - storedModuleSymbol = moduleSymbol - } - - if storedSymbol == nil || storedModuleSymbol == nil { - e.symbols[id] = symbolExportEntry{storedSymbol, storedModuleSymbol} - } - - moduleKey := "" - if !tspath.IsExternalModuleNameRelative(moduleName) { - moduleKey = moduleName - } - - moduleFileName := "" - if moduleFile != nil { - moduleFileName = moduleFile.FileName() - } - e.exportInfo.Add(newExportInfoMapKey(symbolName, symbol, moduleKey, ch), CachedSymbolExportInfo{ - id: id, - symbolTableKey: symbolTableKey, - symbolName: symbolName, - capitalizedSymbolName: capitalizedSymbolName, - moduleName: moduleName, - moduleFile: moduleFile, - moduleFileName: moduleFileName, - packageName: packageName, - - symbol: storedSymbol, - moduleSymbol: storedModuleSymbol, - exportKind: exportKind, - targetFlags: target.Flags, - isFromPackageJson: isFromPackageJson, - }) -} - -func (e *exportInfoMap) search( - ch *checker.Checker, - importingFile tspath.Path, - preferCapitalized bool, - matches func(name string, targetFlags ast.SymbolFlags) bool, - action func(info []*SymbolExportInfo, symbolName string, isFromAmbientModule bool, key ExportInfoMapKey) []*SymbolExportInfo, -) []*SymbolExportInfo { - if importingFile != e.usableByFileName { - return nil - } - for key, info := range e.exportInfo.M { - symbolName, ambientModuleName := key.SymbolName, key.AmbientModuleName - if preferCapitalized && info[0].capitalizedSymbolName != "" { - symbolName = info[0].capitalizedSymbolName - } - if matches(symbolName, info[0].targetFlags) { - rehydrated := core.Map(info, func(info CachedSymbolExportInfo) *SymbolExportInfo { - return e.rehydrateCachedInfo(ch, info) - }) - filtered := core.FilterIndex(rehydrated, func(r *SymbolExportInfo, i int, _ []*SymbolExportInfo) bool { - return e.isNotShadowedByDeeperNodeModulesPackage(r, info[i].packageName) - }) - if len(filtered) > 0 { - if res := action(filtered, symbolName, ambientModuleName != "", key); res != nil { - return res - } - } - } - } - return nil -} - -func (e *exportInfoMap) isNotShadowedByDeeperNodeModulesPackage(info *SymbolExportInfo, packageName string) bool { - if packageName == "" || info.moduleFileName == "" { - return true - } - if e.globalTypingsCacheLocation != "" && strings.HasPrefix(info.moduleFileName, e.globalTypingsCacheLocation) { - return true - } - packageDeepestNodeModulesPath, ok := e.packages[packageName] - return !ok || strings.HasPrefix(info.moduleFileName, packageDeepestNodeModulesPath) -} - -func (e *exportInfoMap) rehydrateCachedInfo(ch *checker.Checker, info CachedSymbolExportInfo) *SymbolExportInfo { - if info.symbol != nil && info.moduleSymbol != nil { - return &SymbolExportInfo{ - symbol: info.symbol, - moduleSymbol: info.moduleSymbol, - moduleFileName: info.moduleFileName, - exportKind: info.exportKind, - targetFlags: info.targetFlags, - isFromPackageJson: info.isFromPackageJson, - } - } - cached := e.symbols[info.id] - cachedSymbol, cachedModuleSymbol := cached.symbol, cached.moduleSymbol - if cachedSymbol != nil && cachedModuleSymbol != nil { - return &SymbolExportInfo{ - symbol: cachedSymbol, - moduleSymbol: cachedModuleSymbol, - moduleFileName: info.moduleFileName, - exportKind: info.exportKind, - targetFlags: info.targetFlags, - isFromPackageJson: info.isFromPackageJson, - } - } - - moduleSymbol := core.Coalesce(info.moduleSymbol, cachedModuleSymbol) - if moduleSymbol == nil { - if info.moduleFile != nil { - moduleSymbol = ch.GetMergedSymbol(info.moduleFile.Symbol) - } else { - moduleSymbol = ch.TryFindAmbientModule(info.moduleName) - } - } - if moduleSymbol == nil { - panic(fmt.Sprintf("Could not find module symbol for %s in exportInfoMap", info.moduleName)) - } - symbol := core.Coalesce(info.symbol, cachedSymbol) - if symbol == nil { - if info.exportKind == ExportKindExportEquals { - symbol = ch.ResolveExternalModuleSymbol(moduleSymbol) - } else { - symbol = ch.TryGetMemberInModuleExportsAndProperties(info.symbolTableKey, moduleSymbol) - } - } - - if symbol == nil { - panic(fmt.Sprintf("Could not find symbol '%s' by key '%s' in module %s", info.symbolName, info.symbolTableKey, moduleSymbol.Name)) - } - e.symbols[info.id] = symbolExportEntry{symbol, moduleSymbol} - return &SymbolExportInfo{ - symbol, - moduleSymbol, - info.moduleFileName, - info.exportKind, - info.targetFlags, - info.isFromPackageJson, - } -} - -func getNamesForExportedSymbol(defaultExport *ast.Symbol, ch *checker.Checker, scriptTarget core.ScriptTarget) []string { - var names []string - forEachNameOfDefaultExport(defaultExport, ch, scriptTarget, func(name, capitalizedName string) string { - if capitalizedName != "" { - names = []string{name, capitalizedName} - } else { - names = []string{name} - } - return name - }) - return names -} - -type packageJsonImportFilter struct { - allowsImportingAmbientModule func(moduleSymbol *ast.Symbol, host modulespecifiers.ModuleSpecifierGenerationHost) bool - getSourceFileInfo func(sourceFile *ast.SourceFile, host modulespecifiers.ModuleSpecifierGenerationHost) packageJsonFilterResult - /** - * Use for a specific module specifier that has already been resolved. - * Use `allowsImportingAmbientModule` or `allowsImportingSourceFile` to resolve - * the best module specifier for a given module _and_ determine if it's importable. - */ - allowsImportingSpecifier func(moduleSpecifier string) bool -} - -type packageJsonFilterResult struct { - importable bool - packageName string -} -type projectPackageJsonInfo struct { - fileName string - parseable bool - dependencies map[string]string - devDependencies map[string]string - peerDependencies map[string]string - optionalDependencies map[string]string -} - -func (info *projectPackageJsonInfo) has(dependencyName string) bool { - if _, ok := info.dependencies[dependencyName]; ok { - return true - } - if _, ok := info.devDependencies[dependencyName]; ok { - return true - } - - if _, ok := info.peerDependencies[dependencyName]; ok { - return true - } - if _, ok := info.optionalDependencies[dependencyName]; ok { - return true - } - - return false -} - -func (l *LanguageService) getImportCompletionAction( - ctx context.Context, - ch *checker.Checker, - targetSymbol *ast.Symbol, - moduleSymbol *ast.Symbol, - sourceFile *ast.SourceFile, - position int, - exportMapKey ExportInfoMapKey, - symbolName string, // !!! needs *string ? - isJsxTagName bool, - // formatContext *formattingContext, - preferences *UserPreferences, -) (string, codeAction) { - var exportInfos []*SymbolExportInfo - // `exportMapKey` should be in the `itemData` of each auto-import completion entry and sent in resolving completion entry requests - exportInfos = l.getExportInfos(ctx, ch, sourceFile, preferences, exportMapKey) - if len(exportInfos) == 0 { - panic("Some exportInfo should match the specified exportMapKey") - } - - isValidTypeOnlyUseSite := ast.IsValidTypeOnlyAliasUseSite(astnav.GetTokenAtPosition(sourceFile, position)) - fix := l.getImportFixForSymbol(ch, sourceFile, exportInfos, position, ptrTo(isValidTypeOnlyUseSite), preferences) - if fix == nil { - lineAndChar := l.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(position)) - panic(fmt.Sprintf("expected importFix at %s: (%v,%v)", sourceFile.FileName(), lineAndChar.Line, lineAndChar.Character)) - } - return fix.moduleSpecifier, l.codeActionForFix(ctx, sourceFile, symbolName, fix /*includeSymbolNameInDescription*/, false, preferences) -} - -func NewExportInfoMap(globalsTypingCacheLocation string) *exportInfoMap { - return &exportInfoMap{ - packages: map[string]string{}, - symbols: map[int]symbolExportEntry{}, - exportInfo: collections.MultiMap[ExportInfoMapKey, CachedSymbolExportInfo]{}, - globalTypingsCacheLocation: globalsTypingCacheLocation, - } -} - -func (l *LanguageService) isImportable( - fromFile *ast.SourceFile, - toFile *ast.SourceFile, - toModule *ast.Symbol, - preferences *UserPreferences, - packageJsonFilter *packageJsonImportFilter, - // moduleSpecifierResolutionHost ModuleSpecifierResolutionHost, - // moduleSpecifierCache ModuleSpecifierCache, -) bool { - // !!! moduleSpecifierResolutionHost := l.GetModuleSpecifierResolutionHost() - moduleSpecifierResolutionHost := l.GetProgram() - - // Ambient module - if toFile == nil { - moduleName := stringutil.StripQuotes(toModule.Name) - if _, ok := core.NodeCoreModules()[moduleName]; ok { - if useNodePrefix := shouldUseUriStyleNodeCoreModules(fromFile, l.GetProgram()); useNodePrefix { - return useNodePrefix == strings.HasPrefix(moduleName, "node:") - } - } - return packageJsonFilter == nil || - packageJsonFilter.allowsImportingAmbientModule(toModule, moduleSpecifierResolutionHost) || - fileContainsPackageImport(fromFile, moduleName) - } - - if fromFile == toFile { - return false - } - - // !!! moduleSpecifierCache - // cachedResult := moduleSpecifierCache?.get(fromFile.path, toFile.path, preferences, {}) - // if cachedResult?.isBlockedByPackageJsonDependencies != nil { - // return !cachedResult.isBlockedByPackageJsonDependencies || cachedResult.packageName != nil && fileContainsPackageImport(fromFile, cachedResult.packageName) - // } - - fromPath := fromFile.FileName() - useCaseSensitiveFileNames := moduleSpecifierResolutionHost.UseCaseSensitiveFileNames() - globalTypingsCache := l.GetProgram().GetGlobalTypingsCacheLocation() - modulePaths := modulespecifiers.GetEachFileNameOfModule( - fromPath, - toFile.FileName(), - moduleSpecifierResolutionHost, - /*preferSymlinks*/ false, - ) - hasImportablePath := false - for _, module := range modulePaths { - file := l.GetProgram().GetSourceFile(module.FileName) - - // Determine to import using toPath only if toPath is what we were looking at - // or there doesnt exist the file in the program by the symlink - if file == nil || file != toFile { - continue - } - - // If it's in a `node_modules` but is not reachable from here via a global import, don't bother. - toNodeModules := tspath.ForEachAncestorDirectoryStoppingAtGlobalCache( - globalTypingsCache, - module.FileName, - func(ancestor string) (string, bool) { - if tspath.GetBaseFileName(ancestor) == "node_modules" { - return ancestor, true - } else { - return "", false - } - }, - ) - toNodeModulesParent := "" - if toNodeModules != "" { - toNodeModulesParent = tspath.GetDirectoryPath(tspath.GetCanonicalFileName(toNodeModules, useCaseSensitiveFileNames)) - } - hasImportablePath = toNodeModulesParent != "" || - strings.HasPrefix(tspath.GetCanonicalFileName(fromPath, useCaseSensitiveFileNames), toNodeModulesParent) || - (globalTypingsCache != "" && strings.HasPrefix(tspath.GetCanonicalFileName(globalTypingsCache, useCaseSensitiveFileNames), toNodeModulesParent)) - if hasImportablePath { - break - } - } - - if packageJsonFilter != nil { - if hasImportablePath { - importInfo := packageJsonFilter.getSourceFileInfo(toFile, moduleSpecifierResolutionHost) - // moduleSpecifierCache?.setBlockedByPackageJsonDependencies(fromFile.path, toFile.path, preferences, {}, importInfo?.packageName, !importInfo?.importable) - return importInfo.importable || hasImportablePath && importInfo.packageName != "" && fileContainsPackageImport(fromFile, importInfo.packageName) - } - return false - } - - return hasImportablePath -} - -func fileContainsPackageImport(sourceFile *ast.SourceFile, packageName string) bool { - return core.Some(sourceFile.Imports(), func(i *ast.Node) bool { - text := i.Text() - return text == packageName || strings.HasPrefix(text, packageName+"/") - }) -} - -func isImportableSymbol(symbol *ast.Symbol, ch *checker.Checker) bool { - return !ch.IsUndefinedSymbol(symbol) && !ch.IsUnknownSymbol(symbol) && !checker.IsKnownSymbol(symbol) // !!! && !checker.IsPrivateIdentifierSymbol(symbol); -} - -func getDefaultLikeExportInfo(moduleSymbol *ast.Symbol, ch *checker.Checker) *ExportInfo { - exportEquals := ch.ResolveExternalModuleSymbol(moduleSymbol) - if exportEquals != moduleSymbol { - if defaultExport := ch.TryGetMemberInModuleExports(ast.InternalSymbolNameDefault, exportEquals); defaultExport != nil { - return &ExportInfo{defaultExport, ExportKindDefault} - } - return &ExportInfo{exportEquals, ExportKindExportEquals} - } - if defaultExport := ch.TryGetMemberInModuleExports(ast.InternalSymbolNameDefault, moduleSymbol); defaultExport != nil { - return &ExportInfo{defaultExport, ExportKindDefault} - } - return nil -} - -type importSpecifierResolverForCompletions struct { - *ast.SourceFile // importingFile - *UserPreferences - l *LanguageService - filter *packageJsonImportFilter -} - -func (r *importSpecifierResolverForCompletions) packageJsonImportFilter() *packageJsonImportFilter { - if r.filter == nil { - r.filter = r.l.createPackageJsonImportFilter(r.SourceFile, *r.UserPreferences) - } - return r.filter -} - -func (i *importSpecifierResolverForCompletions) getModuleSpecifierForBestExportInfo( - ch *checker.Checker, - exportInfo []*SymbolExportInfo, - position int, - isValidTypeOnlyUseSite bool, -) *ImportFix { - // !!! caching - // used in completions, usually calculated once per `getCompletionData` call - var userPreferences UserPreferences - if i.UserPreferences == nil { - userPreferences = UserPreferences{} - } else { - userPreferences = *i.UserPreferences - } - packageJsonImportFilter := i.packageJsonImportFilter() - _, fixes := i.l.getImportFixes(ch, exportInfo, ptrTo(i.l.converters.PositionToLineAndCharacter(i.SourceFile, core.TextPos(position))), ptrTo(isValidTypeOnlyUseSite), ptrTo(false), i.SourceFile, userPreferences, false /* fromCacheOnly */) - return i.l.getBestFix(fixes, i.SourceFile, packageJsonImportFilter.allowsImportingSpecifier, userPreferences) -} - -func (l *LanguageService) getImportFixForSymbol( - ch *checker.Checker, - sourceFile *ast.SourceFile, - exportInfos []*SymbolExportInfo, - position int, - isValidTypeOnlySite *bool, - preferences *UserPreferences, -) *ImportFix { - var userPreferences UserPreferences - if preferences != nil { - userPreferences = *preferences - } - - if isValidTypeOnlySite == nil { - isValidTypeOnlySite = ptrTo(ast.IsValidTypeOnlyAliasUseSite(astnav.GetTokenAtPosition(sourceFile, position))) - } - useRequire := getShouldUseRequire(sourceFile, l.GetProgram()) - packageJsonImportFilter := l.createPackageJsonImportFilter(sourceFile, userPreferences) - _, fixes := l.getImportFixes(ch, exportInfos, ptrTo(l.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(position))), isValidTypeOnlySite, &useRequire, sourceFile, userPreferences, false /* fromCacheOnly */) - return l.getBestFix(fixes, sourceFile, packageJsonImportFilter.allowsImportingSpecifier, userPreferences) -} - -func (l *LanguageService) getBestFix(fixes []*ImportFix, sourceFile *ast.SourceFile, allowsImportingSpecifier func(moduleSpecifier string) bool, preferences UserPreferences) *ImportFix { - if len(fixes) == 0 { - return nil - } - - // These will always be placed first if available, and are better than other kinds - if fixes[0].kind == ImportFixKindUseNamespace || fixes[0].kind == ImportFixKindAddToExisting { - return fixes[0] - } - - best := fixes[0] - for _, fix := range fixes { - // Takes true branch of conditional if `fix` is better than `best` - if compareModuleSpecifiers( - fix, - best, - sourceFile, - l.GetProgram(), - preferences, - allowsImportingSpecifier, - func(fileName string) tspath.Path { - return tspath.ToPath(fileName, l.GetProgram().GetCurrentDirectory(), l.GetProgram().UseCaseSensitiveFileNames()) - }, - ) < 0 { - best = fix - } - } - - return best -} - -func (l *LanguageService) getImportFixes( - ch *checker.Checker, - exportInfos []*SymbolExportInfo, // | FutureSymbolExportInfo[], - usagePosition *lsproto.Position, - isValidTypeOnlyUseSite *bool, - useRequire *bool, - sourceFile *ast.SourceFile, // | FutureSourceFile, - preferences UserPreferences, - // importMap *importMap, - fromCacheOnly bool, -) (int, []*ImportFix) { - // if importMap == nil { && !!! isFullSourceFile(sourceFile) - importMap := createExistingImportMap(sourceFile, l.GetProgram(), ch) - var existingImports []*FixAddToExistingImportInfo - if importMap != nil { - existingImports = core.FlatMap(exportInfos, importMap.getImportsForExportInfo) - } - var useNamespace []*ImportFix - if usagePosition != nil { - if namespaceImport := tryUseExistingNamespaceImport(existingImports, *usagePosition); namespaceImport != nil { - useNamespace = append(useNamespace, namespaceImport) - } - } - if addToExisting := tryAddToExistingImport(existingImports, isValidTypeOnlyUseSite, ch, l.GetProgram().Options()); addToExisting != nil { - // Don't bother providing an action to add a new import if we can add to an existing one. - return 0, append(useNamespace, addToExisting) - } - - result := l.getFixesForAddImport( - ch, - exportInfos, - existingImports, - sourceFile, - usagePosition, - *isValidTypeOnlyUseSite, - *useRequire, - preferences, - fromCacheOnly, - ) - computedWithoutCacheCount := 0 - // if result.computedWithoutCacheCount != nil { - // computedWithoutCacheCount = *result.computedWithoutCacheCount - // } - return computedWithoutCacheCount, append(useNamespace, result...) -} - -func (l *LanguageService) createPackageJsonImportFilter(fromFile *ast.SourceFile, preferences UserPreferences) *packageJsonImportFilter { - packageJsons := []*projectPackageJsonInfo{} - // packageJsons := ( - // (host.getPackageJsonsVisibleToFile && host.getPackageJsonsVisibleToFile(fromFile.fileName)) || getPackageJsonsVisibleToFile(fromFile.fileName, host) - // ).filter(p => p.parseable); - - var usesNodeCoreModules *bool - ambientModuleCache := map[*ast.Symbol]bool{} - sourceFileCache := map[*ast.SourceFile]packageJsonFilterResult{} - - getNodeModuleRootSpecifier := func(fullSpecifier string) string { - components := tspath.GetPathComponents(modulespecifiers.GetPackageNameFromTypesPackageName(fullSpecifier), "")[1:] - // Scoped packages - if strings.HasPrefix(components[0], "@") { - return fmt.Sprintf("%s/%s", components[0], components[1]) - } - return components[0] - } - - moduleSpecifierIsCoveredByPackageJson := func(specifier string) bool { - packageName := getNodeModuleRootSpecifier(specifier) - for _, packageJson := range packageJsons { - if packageJson.has(packageName) || packageJson.has(module.GetTypesPackageName(packageName)) { - return true - } - } - return false - } - - isAllowedCoreNodeModulesImport := func(moduleSpecifier string) bool { - // If we're in JavaScript, it can be difficult to tell whether the user wants to import - // from Node core modules or not. We can start by seeing if the user is actually using - // any node core modules, as opposed to simply having @types/node accidentally as a - // dependency of a dependency. - if /*isFullSourceFile(fromFile) &&*/ ast.IsSourceFileJS(fromFile) && core.NodeCoreModules()[moduleSpecifier] { - if usesNodeCoreModules == nil { - usesNodeCoreModules = ptrTo(consumesNodeCoreModules(fromFile)) - } - if *usesNodeCoreModules { - return true - } - } - return false - } - - getNodeModulesPackageNameFromFileName := func(importedFileName string, moduleSpecifierResolutionHost modulespecifiers.ModuleSpecifierGenerationHost) *string { - if !strings.Contains(importedFileName, "node_modules") { - return nil - } - specifier := modulespecifiers.GetNodeModulesPackageName( - l.program.Options(), - fromFile, - importedFileName, - moduleSpecifierResolutionHost, - preferences.ModuleSpecifierPreferences(), - modulespecifiers.ModuleSpecifierOptions{}, - ) - if specifier == "" { - return nil - } - // Paths here are not node_modules, so we don't care about them; - // returning anything will trigger a lookup in package.json. - if !tspath.PathIsRelative(specifier) && !tspath.IsRootedDiskPath(specifier) { - return ptrTo(getNodeModuleRootSpecifier(specifier)) - } - return nil - } - - allowsImportingAmbientModule := func(moduleSymbol *ast.Symbol, moduleSpecifierResolutionHost modulespecifiers.ModuleSpecifierGenerationHost) bool { - if len(packageJsons) > 0 || moduleSymbol.ValueDeclaration == nil { - return true - } - - if cached, ok := ambientModuleCache[moduleSymbol]; ok { - return cached - } - - declaredModuleSpecifier := stringutil.StripQuotes(moduleSymbol.Name) - if isAllowedCoreNodeModulesImport(declaredModuleSpecifier) { - ambientModuleCache[moduleSymbol] = true - return true - } - - declaringSourceFile := ast.GetSourceFileOfNode(moduleSymbol.ValueDeclaration) - declaringNodeModuleName := getNodeModulesPackageNameFromFileName(declaringSourceFile.FileName(), moduleSpecifierResolutionHost) - if declaringNodeModuleName == nil { - ambientModuleCache[moduleSymbol] = true - return true - } - - result := moduleSpecifierIsCoveredByPackageJson(*declaringNodeModuleName) - if !result { - result = moduleSpecifierIsCoveredByPackageJson(declaredModuleSpecifier) - } - ambientModuleCache[moduleSymbol] = result - return result - } - - getSourceFileInfo := func(sourceFile *ast.SourceFile, moduleSpecifierResolutionHost modulespecifiers.ModuleSpecifierGenerationHost) packageJsonFilterResult { - result := packageJsonFilterResult{ - importable: true, - packageName: "", - } - - if len(packageJsons) == 0 { - return result - } - if cached, ok := sourceFileCache[sourceFile]; ok { - return cached - } - - if packageName := getNodeModulesPackageNameFromFileName(sourceFile.FileName(), moduleSpecifierResolutionHost); packageName != nil { - result = packageJsonFilterResult{importable: moduleSpecifierIsCoveredByPackageJson(*packageName), packageName: *packageName} - } - sourceFileCache[sourceFile] = result - return result - } - - allowsImportingSpecifier := func(moduleSpecifier string) bool { - if len(packageJsons) == 0 || isAllowedCoreNodeModulesImport(moduleSpecifier) { - return true - } - if tspath.PathIsRelative(moduleSpecifier) || tspath.IsRootedDiskPath(moduleSpecifier) { - return true - } - return moduleSpecifierIsCoveredByPackageJson(moduleSpecifier) - } - - return &packageJsonImportFilter{ - allowsImportingAmbientModule, - getSourceFileInfo, - allowsImportingSpecifier, - } -} - -func consumesNodeCoreModules(sourceFile *ast.SourceFile) bool { - for _, importStatement := range sourceFile.Imports() { - if core.NodeCoreModules()[importStatement.Text()] { - return true - } - } - return false -} - -func createExistingImportMap(importingFile *ast.SourceFile, program *compiler.Program, ch *checker.Checker) *importMap { - m := collections.MultiMap[ast.SymbolId, *ast.Statement]{} - for _, moduleSpecifier := range importingFile.Imports() { - i := tryGetImportFromModuleSpecifier(moduleSpecifier) - if i == nil { - panic("error: did not expect node kind " + moduleSpecifier.Kind.String()) - } else if ast.IsVariableDeclarationInitializedToRequire(i.Parent) { - if moduleSymbol := ch.ResolveExternalModuleName(moduleSpecifier); moduleSymbol != nil { - m.Add(ast.GetSymbolId(moduleSymbol), i.Parent) - } - } else if i.Kind == ast.KindImportDeclaration || i.Kind == ast.KindImportEqualsDeclaration || i.Kind == ast.KindJSDocImportTag { - if moduleSymbol := ch.GetSymbolAtLocation(moduleSpecifier); moduleSymbol != nil { - m.Add(ast.GetSymbolId(moduleSymbol), i) - } - } - } - return &importMap{importingFile: importingFile, program: program, m: m} -} - -type importMap struct { - importingFile *ast.SourceFile - program *compiler.Program - m collections.MultiMap[ast.SymbolId, *ast.Statement] // !!! anyImportOrRequire -} - -func (i *importMap) getImportsForExportInfo(info *SymbolExportInfo /* | FutureSymbolExportInfo*/) []*FixAddToExistingImportInfo { - matchingDeclarations := i.m.Get(ast.GetSymbolId(info.moduleSymbol)) - if len(matchingDeclarations) == 0 { - return nil - } - - // Can't use an es6 import for a type in JS. - if ast.IsSourceFileJS(i.importingFile) && info.targetFlags&ast.SymbolFlagsValue == 0 && !core.Every(matchingDeclarations, ast.IsJSDocImportTag) { - return nil - } - - importKind := getImportKind(i.importingFile, info.exportKind, i.program, false) - return core.Map(matchingDeclarations, func(d *ast.Statement) *FixAddToExistingImportInfo { - return &FixAddToExistingImportInfo{declaration: d, importKind: importKind, symbol: info.symbol, targetFlags: info.targetFlags} - }) -} - -func tryUseExistingNamespaceImport(existingImports []*FixAddToExistingImportInfo, position lsproto.Position) *ImportFix { - // It is possible that multiple import statements with the same specifier exist in the file. - // e.g. - // - // import * as ns from "foo"; - // import { member1, member2 } from "foo"; - // - // member3/**/ <-- cusor here - // - // in this case we should provie 2 actions: - // 1. change "member3" to "ns.member3" - // 2. add "member3" to the second import statement's import list - // and it is up to the user to decide which one fits best. - for _, existingImport := range existingImports { - if existingImport.importKind != ImportKindNamed { - continue - } - var namespacePrefix string - declaration := existingImport.declaration - switch declaration.Kind { - case ast.KindVariableDeclaration, ast.KindImportEqualsDeclaration: - name := declaration.Name() - if declaration.Kind == ast.KindVariableDeclaration && (name == nil || name.Kind != ast.KindIdentifier) { - continue - } - namespacePrefix = name.Text() - case ast.KindJSDocImportTag, ast.KindImportDeclaration: - importClause := ast.GetImportClauseOfDeclaration(declaration) - if importClause == nil || importClause.NamedBindings == nil || importClause.NamedBindings.Kind != ast.KindNamespaceImport { - continue - } - namespacePrefix = importClause.NamedBindings.Name().Text() - default: - debug.AssertNever(declaration) - } - if namespacePrefix == "" { - continue - } - moduleSpecifier := checker.TryGetModuleSpecifierFromDeclaration(declaration) - if moduleSpecifier != nil && moduleSpecifier.Text() != "" { - return getUseNamespaceImport( - moduleSpecifier.Text(), - modulespecifiers.ResultKindNone, - namespacePrefix, - position, - ) - } - } - return nil -} - -func tryAddToExistingImport(existingImports []*FixAddToExistingImportInfo, isValidTypeOnlyUseSite *bool, ch *checker.Checker, compilerOptions *core.CompilerOptions) *ImportFix { - var best *ImportFix - - typeOnly := false - if isValidTypeOnlyUseSite != nil { - typeOnly = *isValidTypeOnlyUseSite - } - - for _, existingImport := range existingImports { - fix := existingImport.getAddToExistingImportFix(typeOnly, ch, compilerOptions) - if fix == nil { - continue - } - isTypeOnly := ast.IsTypeOnlyImportDeclaration(fix.importClauseOrBindingPattern) - if (fix.addAsTypeOnly != AddAsTypeOnlyNotAllowed && isTypeOnly) || (fix.addAsTypeOnly == AddAsTypeOnlyNotAllowed && !isTypeOnly) { - // Give preference to putting types in existing type-only imports and avoiding conversions - // of import statements to/from type-only. - return fix - } - if best == nil { - best = fix - } - } - return best -} - -func (info *FixAddToExistingImportInfo) getAddToExistingImportFix(isValidTypeOnlyUseSite bool, ch *checker.Checker, compilerOptions *core.CompilerOptions) *ImportFix { - if info.importKind == ImportKindCommonJS || info.importKind == ImportKindNamespace || info.declaration.Kind == ast.KindImportEqualsDeclaration { - // These kinds of imports are not combinable with anything - return nil - } - - if info.declaration.Kind == ast.KindVariableDeclaration { - if (info.importKind == ImportKindNamed || info.importKind == ImportKindDefault) && info.declaration.Name().Kind == ast.KindObjectBindingPattern { - return getAddToExistingImport( - info.declaration.Name(), - info.importKind, - info.declaration.Initializer().Arguments()[0].Text(), - modulespecifiers.ResultKindNone, - AddAsTypeOnlyNotAllowed, - ) - } - return nil - } - - importClause := ast.GetImportClauseOfDeclaration(info.declaration) - if importClause == nil || !ast.IsStringLiteralLike(info.declaration.ModuleSpecifier()) { - return nil - } - namedBindings := importClause.NamedBindings - // A type-only import may not have both a default and named imports, so the only way a name can - // be added to an existing type-only import is adding a named import to existing named bindings. - if importClause.IsTypeOnly() && !(info.importKind == ImportKindNamed && namedBindings != nil) { - return nil - } - - // N.B. we don't have to figure out whether to use the main program checker - // or the AutoImportProvider checker because we're adding to an existing import; the existence of - // the import guarantees the symbol came from the main program. - addAsTypeOnly := getAddAsTypeOnly(isValidTypeOnlyUseSite, info.symbol, info.targetFlags, ch, compilerOptions) - - if info.importKind == ImportKindDefault && (importClause.Name() != nil || // Cannot add a default import to a declaration that already has one - addAsTypeOnly == AddAsTypeOnlyRequired && namedBindings != nil) { // Cannot add a default import as type-only if the import already has named bindings - - return nil - } - - // Cannot add a named import to a declaration that has a namespace import - if info.importKind == ImportKindNamed && namedBindings != nil && namedBindings.Kind == ast.KindNamespaceImport { - return nil - } - - return getAddToExistingImport( - importClause.AsNode(), - info.importKind, - info.declaration.ModuleSpecifier().Text(), - modulespecifiers.ResultKindNone, - addAsTypeOnly, - ) -} - -func (l *LanguageService) getFixesForAddImport( - ch *checker.Checker, - exportInfos []*SymbolExportInfo, // !!! | readonly FutureSymbolExportInfo[], - existingImports []*FixAddToExistingImportInfo, - sourceFile *ast.SourceFile, // !!! | FutureSourceFile, - usagePosition *lsproto.Position, - isValidTypeOnlyUseSite bool, - useRequire bool, - preferences UserPreferences, - fromCacheOnly bool, -) []*ImportFix { - // tries to create a new import statement using an existing import specifier - var importWithExistingSpecifier *ImportFix - - for _, existingImport := range existingImports { - if fix := existingImport.getNewImportFromExistingSpecifier(isValidTypeOnlyUseSite, useRequire, ch, l.GetProgram().Options()); fix != nil { - importWithExistingSpecifier = fix - break - } - } - - if importWithExistingSpecifier != nil { - return []*ImportFix{importWithExistingSpecifier} - } - - return l.getNewImportFixes(ch, sourceFile, usagePosition, isValidTypeOnlyUseSite, useRequire, exportInfos, preferences, fromCacheOnly) -} - -func (l *LanguageService) getNewImportFixes( - ch *checker.Checker, - sourceFile *ast.SourceFile, // | FutureSourceFile, - usagePosition *lsproto.Position, - isValidTypeOnlyUseSite bool, - useRequire bool, - exportInfos []*SymbolExportInfo, // !!! (SymbolExportInfo | FutureSymbolExportInfo)[], - preferences UserPreferences, - fromCacheOnly bool, -) []*ImportFix /* FixAddNewImport | FixAddJsdocTypeImport */ { - isJs := tspath.HasJSFileExtension(sourceFile.FileName()) - compilerOptions := l.GetProgram().Options() - // !!! packagejsonAutoimportProvider - // getChecker := createGetChecker(program, host)// memoized typechecker based on `isFromPackageJson` bool - - getModuleSpecifiers := func(moduleSymbol *ast.Symbol, checker *checker.Checker) ([]string, modulespecifiers.ResultKind) { - return modulespecifiers.GetModuleSpecifiersWithInfo(moduleSymbol, checker, compilerOptions, sourceFile, l.GetProgram(), preferences.ModuleSpecifierPreferences(), modulespecifiers.ModuleSpecifierOptions{}, true /*forAutoImport*/) - } - // fromCacheOnly - // ? (exportInfo: SymbolExportInfo | FutureSymbolExportInfo) => moduleSpecifiers.tryGetModuleSpecifiersFromCache(exportInfo.moduleSymbol, sourceFile, moduleSpecifierResolutionHost, preferences) - // : (exportInfo: SymbolExportInfo | FutureSymbolExportInfo, checker: TypeChecker) => moduleSpecifiers.getModuleSpecifiersWithCacheInfo(exportInfo.moduleSymbol, checker, compilerOptions, sourceFile, moduleSpecifierResolutionHost, preferences, /*options*/ nil, /*forAutoImport*/ true); - - // computedWithoutCacheCount = 0; - var fixes []*ImportFix /* FixAddNewImport | FixAddJsdocTypeImport */ - for i, exportInfo := range exportInfos { - moduleSpecifiers, moduleSpecifierKind := getModuleSpecifiers(exportInfo.moduleSymbol, ch) - importedSymbolHasValueMeaning := exportInfo.targetFlags&ast.SymbolFlagsValue != 0 - addAsTypeOnly := getAddAsTypeOnly(isValidTypeOnlyUseSite, exportInfo.symbol, exportInfo.targetFlags, ch, compilerOptions) - // computedWithoutCacheCount += computedWithoutCache ? 1 : 0; - for _, moduleSpecifier := range moduleSpecifiers { - if modulespecifiers.ContainsNodeModules(moduleSpecifier) { - continue - } - if !importedSymbolHasValueMeaning && isJs && usagePosition != nil { - // `position` should only be undefined at a missing jsx namespace, in which case we shouldn't be looking for pure types. - fixes = append(fixes, getAddJsdocTypeImport( - moduleSpecifier, - moduleSpecifierKind, - usagePosition, - exportInfo, - ptrTo(i > 0)), // isReExport - ) - continue - } - importKind := getImportKind(sourceFile, exportInfo.exportKind, l.GetProgram(), false) - var qualification *Qualification - if usagePosition != nil && importKind == ImportKindCommonJS && exportInfo.exportKind == ExportKindNamed { - // Compiler options are restricting our import options to a require, but we need to access - // a named export or property of the exporting module. We need to import the entire module - // and insert a property access, e.g. `writeFile` becomes - // - // import fs = require("fs"); // or const in JS - // fs.writeFile - exportEquals := ch.ResolveExternalModuleSymbol(exportInfo.moduleSymbol) - var namespacePrefix *string - if exportEquals != exportInfo.moduleSymbol { - namespacePrefix = strPtrTo(forEachNameOfDefaultExport( - exportEquals, - ch, - compilerOptions.GetEmitScriptTarget(), - func(a, _ string) string { return a }, // Identity - )) - } - if namespacePrefix == nil { - namespacePrefix = ptrTo(moduleSymbolToValidIdentifier( - exportInfo.moduleSymbol, - compilerOptions.GetEmitScriptTarget(), - /*forceCapitalize*/ false, - )) - } - qualification = &Qualification{*usagePosition, *namespacePrefix} - } - fixes = append(fixes, getNewAddNewImport( - moduleSpecifier, - moduleSpecifierKind, - importKind, - useRequire, - addAsTypeOnly, - exportInfo, - ptrTo(i > 0), // isReExport - qualification, - )) - } - } - - return fixes -} - -func getAddAsTypeOnly( - isValidTypeOnlyUseSite bool, - symbol *ast.Symbol, - targetFlags ast.SymbolFlags, - ch *checker.Checker, - compilerOptions *core.CompilerOptions, -) AddAsTypeOnly { - if !isValidTypeOnlyUseSite { - // Can't use a type-only import if the usage is an emitting position - return AddAsTypeOnlyNotAllowed - } - if symbol != nil && compilerOptions.VerbatimModuleSyntax.IsTrue() && - (targetFlags&ast.SymbolFlagsValue == 0 || ch.GetTypeOnlyAliasDeclaration(symbol) != nil) { - // A type-only import is required for this symbol if under these settings if the symbol will - // be erased, which will happen if the target symbol is purely a type or if it was exported/imported - // as type-only already somewhere between this import and the target. - return AddAsTypeOnlyRequired - } - return AddAsTypeOnlyAllowed -} - -func getShouldUseRequire( - sourceFile *ast.SourceFile, // !!! | FutureSourceFile - program *compiler.Program, -) bool { - // 1. TypeScript files don't use require variable declarations - if !tspath.HasJSFileExtension(sourceFile.FileName()) { - return false - } - - // 2. If the current source file is unambiguously CJS or ESM, go with that - switch { - case sourceFile.CommonJSModuleIndicator != nil && sourceFile.ExternalModuleIndicator == nil: - return true - case sourceFile.ExternalModuleIndicator != nil && sourceFile.CommonJSModuleIndicator == nil: - return false - } - - // 3. If there's a tsconfig/jsconfig, use its module setting - if program.Options().ConfigFilePath != "" { - return program.Options().GetEmitModuleKind() < core.ModuleKindES2015 - } - - // 4. In --module nodenext, assume we're not emitting JS -> JS, so use - // whatever syntax Node expects based on the detected module kind - // TODO: consider removing `impliedNodeFormatForEmit` - switch program.GetImpliedNodeFormatForEmit(sourceFile) { - case core.ModuleKindCommonJS: - return true - case core.ModuleKindESNext: - return false - } - - // 5. Match the first other JS file in the program that's unambiguously CJS or ESM - for _, otherFile := range program.GetSourceFiles() { - switch { - case otherFile == sourceFile, !ast.IsSourceFileJS(otherFile), program.IsSourceFileFromExternalLibrary(otherFile): - continue - case otherFile.CommonJSModuleIndicator != nil && otherFile.ExternalModuleIndicator == nil: - return true - case otherFile.ExternalModuleIndicator != nil && otherFile.CommonJSModuleIndicator == nil: - return false - } - } - - // 6. Literally nothing to go on - return true -} - -/** - * @param forceImportKeyword Indicates that the user has already typed `import`, so the result must start with `import`. - * (In other words, do not allow `const x = require("...")` for JS files.) - * - * @internal - */ -func getImportKind(importingFile *ast.SourceFile /*| FutureSourceFile*/, exportKind ExportKind, program *compiler.Program, forceImportKeyword bool) ImportKind { - if program.Options().VerbatimModuleSyntax.IsTrue() && program.GetEmitModuleFormatOfFile(importingFile) == core.ModuleKindCommonJS { - // TODO: if the exporting file is ESM under nodenext, or `forceImport` is given in a JS file, this is impossible - return ImportKindCommonJS - } - switch exportKind { - case ExportKindNamed: - return ImportKindNamed - case ExportKindDefault: - return ImportKindDefault - case ExportKindExportEquals: - return getExportEqualsImportKind(importingFile, program.Options(), forceImportKeyword) - case ExportKindUMD: - return getUmdImportKind(importingFile, program, forceImportKeyword) - case ExportKindModule: - return ImportKindNamespace - } - panic("unexpected export kind: " + exportKind.String()) -} - -func getExportEqualsImportKind(importingFile *ast.SourceFile /* | FutureSourceFile*/, compilerOptions *core.CompilerOptions, forceImportKeyword bool) ImportKind { - allowSyntheticDefaults := compilerOptions.GetAllowSyntheticDefaultImports() - isJS := tspath.HasJSFileExtension(importingFile.FileName()) - // 1. 'import =' will not work in es2015+ TS files, so the decision is between a default - // and a namespace import, based on allowSyntheticDefaultImports/esModuleInterop. - if !isJS && compilerOptions.GetEmitModuleKind() >= core.ModuleKindES2015 { - if allowSyntheticDefaults { - return ImportKindDefault - } - return ImportKindNamespace - } - // 2. 'import =' will not work in JavaScript, so the decision is between a default import, - // a namespace import, and const/require. - if isJS { - if importingFile.ExternalModuleIndicator != nil || forceImportKeyword { - if allowSyntheticDefaults { - return ImportKindDefault - } - return ImportKindNamespace - } - return ImportKindCommonJS - } - // 3. At this point the most correct choice is probably 'import =', but people - // really hate that, so look to see if the importing file has any precedent - // on how to handle it. - for _, statement := range importingFile.Statements.Nodes { - // `import foo` parses as an ImportEqualsDeclaration even though it could be an ImportDeclaration - if ast.IsImportEqualsDeclaration(statement) && !ast.NodeIsMissing(statement.AsImportEqualsDeclaration().ModuleReference) { - return ImportKindCommonJS - } - } - // 4. We have no precedent to go on, so just use a default import if - // allowSyntheticDefaultImports/esModuleInterop is enabled. - if allowSyntheticDefaults { - return ImportKindDefault - } - return ImportKindCommonJS -} - -func getUmdImportKind(importingFile *ast.SourceFile /* | FutureSourceFile */, program *compiler.Program, forceImportKeyword bool) ImportKind { - // Import a synthetic `default` if enabled. - if program.Options().GetAllowSyntheticDefaultImports() { - return ImportKindDefault - } - - // When a synthetic `default` is unavailable, use `import..require` if the module kind supports it. - moduleKind := program.Options().GetEmitModuleKind() - switch moduleKind { - case core.ModuleKindCommonJS: - if tspath.HasJSFileExtension(importingFile.FileName()) && (importingFile.ExternalModuleIndicator != nil || forceImportKeyword) { - return ImportKindNamespace - } - return ImportKindCommonJS - case core.ModuleKindES2015, core.ModuleKindES2020, core.ModuleKindES2022, core.ModuleKindESNext, core.ModuleKindNone, core.ModuleKindPreserve: - // Fall back to the `import * as ns` style import. - return ImportKindNamespace - case core.ModuleKindNode16, core.ModuleKindNode18, core.ModuleKindNode20, core.ModuleKindNodeNext: - if program.GetImpliedNodeFormatForEmit(importingFile) == core.ModuleKindESNext { - return ImportKindNamespace - } - return ImportKindCommonJS - default: - panic(`Unexpected moduleKind :` + moduleKind.String()) - } -} - -/** - * May call `cb` multiple times with the same name. - * Terminates when `cb` returns a truthy value. - */ -func forEachNameOfDefaultExport(defaultExport *ast.Symbol, ch *checker.Checker, scriptTarget core.ScriptTarget, cb func(name string, capitalizedName string) string) string { - var chain []*ast.Symbol - current := defaultExport - seen := collections.Set[*ast.Symbol]{} - - for current != nil { - // The predecessor to this function also looked for a name on the `localSymbol` - // of default exports, but I think `getDefaultLikeExportNameFromDeclaration` - // accomplishes the same thing via syntax - no tests failed when I removed it. - fromDeclaration := getDefaultLikeExportNameFromDeclaration(current) - if fromDeclaration != "" { - final := cb(fromDeclaration, "") - if final != "" { - return final - } - } - - if current.Name != ast.InternalSymbolNameDefault && current.Name != ast.InternalSymbolNameExportEquals { - if final := cb(current.Name, ""); final != "" { - return final - } - } - - chain = append(chain, current) - if !seen.AddIfAbsent(current) { - break - } - if current.Flags&ast.SymbolFlagsAlias != 0 { - current = ch.GetImmediateAliasedSymbol(current) - } else { - current = nil - } - } - - for _, symbol := range chain { - if symbol.Parent != nil && checker.IsExternalModuleSymbol(symbol.Parent) { - final := cb( - moduleSymbolToValidIdentifier(symbol.Parent, scriptTarget /*forceCapitalize*/, false), - moduleSymbolToValidIdentifier(symbol.Parent, scriptTarget /*forceCapitalize*/, true), - ) - if final != "" { - return final - } - } - } - return "" -} - -func getDefaultLikeExportNameFromDeclaration(symbol *ast.Symbol) string { - for _, d := range symbol.Declarations { - // "export default" in this case. See `ExportAssignment`for more details. - if ast.IsExportAssignment(d) { - if innerExpression := ast.SkipOuterExpressions(d.Expression(), ast.OEKAll); ast.IsIdentifier(innerExpression) { - return innerExpression.Text() - } - continue - } - // "export { ~ as default }" - if ast.IsExportSpecifier(d) && d.Symbol().Flags == ast.SymbolFlagsAlias && d.PropertyName() != nil { - if d.PropertyName().Kind == ast.KindIdentifier { - return d.PropertyName().Text() - } - continue - } - // GH#52694 - if name := ast.GetNameOfDeclaration(d); name != nil && name.Kind == ast.KindIdentifier { - return name.Text() - } - if symbol.Parent != nil && !checker.IsExternalModuleSymbol(symbol.Parent) { - return symbol.Parent.Name - } - } - return "" -} - -func forEachExternalModuleToImportFrom( - ch *checker.Checker, - program *compiler.Program, - preferences *UserPreferences, - // useAutoImportProvider bool, - cb func(module *ast.Symbol, moduleFile *ast.SourceFile, checker *checker.Checker, isFromPackageJson bool), -) { - // !!! excludePatterns - // excludePatterns := preferences.autoImportFileExcludePatterns && getIsExcludedPatterns(preferences, useCaseSensitiveFileNames) - - forEachExternalModule( - ch, - program.GetSourceFiles(), - // !!! excludePatterns, - func(module *ast.Symbol, file *ast.SourceFile) { - cb(module, file, ch, false) - }, - ) - - // !!! autoImportProvider - // if autoImportProvider := useAutoImportProvider && l.getPackageJsonAutoImportProvider(); autoImportProvider != nil { - // // start := timestamp(); - // forEachExternalModule(autoImportProvider.getTypeChecker(), autoImportProvider.getSourceFiles(), excludePatterns, host, func (module *ast.Symbol, file *ast.SourceFile) { - // if (file && !program.getSourceFile(file.FileName()) || !file && !checker.resolveName(module.Name, /*location*/ nil, ast.SymbolFlagsModule, /*excludeGlobals*/ false)) { - // // The AutoImportProvider filters files already in the main program out of its *root* files, - // // but non-root files can still be present in both programs, and already in the export info map - // // at this point. This doesn't create any incorrect behavior, but is a waste of time and memory, - // // so we filter them out here. - // cb(module, file, autoImportProvide.checker, /*isFromPackageJson*/ true); - // } - // }); - // // host.log?.(`forEachExternalModuleToImportFrom autoImportProvider: ${timestamp() - start}`); - // } -} - -func forEachExternalModule( - ch *checker.Checker, - allSourceFiles []*ast.SourceFile, - // excludePatterns []RegExp, - cb func(moduleSymbol *ast.Symbol, sourceFile *ast.SourceFile), -) { - // !!! excludePatterns - // isExcluded := excludePatterns && getIsExcluded(excludePatterns, host) - - for _, ambient := range ch.GetAmbientModules() { - if !strings.Contains(ambient.Name, "*") /* && !(excludePatterns && ambient.Declarations.every(func (d){ return isExcluded(d.getSourceFile())})) */ { - cb(ambient, nil /*sourceFile*/) - } - } - for _, sourceFile := range allSourceFiles { - if ast.IsExternalOrCommonJSModule(sourceFile) /* && !isExcluded(sourceFile) */ { - cb(ch.GetMergedSymbol(sourceFile.Symbol), sourceFile) - } - } -} - -// ======================== generate code actions ======================= - -func (l *LanguageService) codeActionForFix( - ctx context.Context, - sourceFile *ast.SourceFile, - symbolName string, - fix *ImportFix, - includeSymbolNameInDescription bool, - preferences *UserPreferences, -) codeAction { - tracker := l.newChangeTracker(ctx) // !!! changetracker.with - diag := l.codeActionForFixWorker(tracker, sourceFile, symbolName, fix, includeSymbolNameInDescription, preferences) - changes := tracker.getChanges()[sourceFile.FileName()] - return codeAction{description: diag.Message(), changes: changes} -} - -func (l *LanguageService) codeActionForFixWorker( - changeTracker *changeTracker, - sourceFile *ast.SourceFile, - symbolName string, - fix *ImportFix, - includeSymbolNameInDescription bool, - preferences *UserPreferences, -) *diagnostics.Message { - switch fix.kind { - case ImportFixKindUseNamespace: - changeTracker.addNamespaceQualifier(sourceFile, fix.qualification()) - return diagnostics.FormatMessage(diagnostics.Change_0_to_1, symbolName, `${fix.namespacePrefix}.${symbolName}`) - case ImportFixKindJsdocTypeImport: - // !!! not implemented - // changeTracker.addImportType(changeTracker, sourceFile, fix, quotePreference); - // return diagnostics.FormatMessage(diagnostics.Change_0_to_1, symbolName, getImportTypePrefix(fix.moduleSpecifier, quotePreference) + symbolName); - case ImportFixKindAddToExisting: - changeTracker.doAddExistingFix( - sourceFile, - fix.importClauseOrBindingPattern, - core.IfElse(fix.importKind == ImportKindDefault, &Import{name: symbolName, addAsTypeOnly: fix.addAsTypeOnly}, nil), - core.IfElse(fix.importKind == ImportKindNamed, []*Import{{name: symbolName, addAsTypeOnly: fix.addAsTypeOnly}}, nil), - // nil /*removeExistingImportSpecifiers*/, - preferences, - ) - moduleSpecifierWithoutQuotes := stringutil.StripQuotes(fix.moduleSpecifier) - if includeSymbolNameInDescription { - return diagnostics.FormatMessage(diagnostics.Import_0_from_1, symbolName, moduleSpecifierWithoutQuotes) - } - return diagnostics.FormatMessage(diagnostics.Update_import_from_0, moduleSpecifierWithoutQuotes) - case ImportFixKindAddNew: - var declarations []*ast.Statement - defaultImport := core.IfElse(fix.importKind == ImportKindDefault, &Import{name: symbolName, addAsTypeOnly: fix.addAsTypeOnly}, nil) - namedImports := core.IfElse(fix.importKind == ImportKindNamed, []*Import{{name: symbolName, addAsTypeOnly: fix.addAsTypeOnly}}, nil) - var namespaceLikeImport *Import - qualification := fix.qualification() - if fix.importKind == ImportKindNamespace || fix.importKind == ImportKindCommonJS { - namespaceLikeImport = &Import{kind: fix.importKind, addAsTypeOnly: fix.addAsTypeOnly, name: symbolName} - if qualification != nil && qualification.namespacePrefix != "" { - namespaceLikeImport.name = qualification.namespacePrefix - } - } - - if fix.useRequire { - // !!! require - // declarations = getNewRequires(fixAddNew.moduleSpecifier, quotePreference, defaultImport, namedImports, namespaceLikeImport, l.GetProgram().Options(), preferences) - } else { - declarations = changeTracker.getNewImports(fix.moduleSpecifier, defaultImport, namedImports, namespaceLikeImport, l.GetProgram().Options(), preferences) - } - - changeTracker.insertImports( - sourceFile, - declarations, - /*blankLineBetween*/ true, - preferences, - ) - if qualification != nil { - changeTracker.addNamespaceQualifier(sourceFile, qualification) - } - if includeSymbolNameInDescription { - return diagnostics.FormatMessage(diagnostics.Import_0_from_1, symbolName, fix.moduleSpecifier) - } - return diagnostics.FormatMessage(diagnostics.Add_import_from_0, fix.moduleSpecifier) - case ImportFixKindPromoteTypeOnly: - // !!! type only - // promotedDeclaration := promoteFromTypeOnly(changes, fix.typeOnlyAliasDeclaration, program, sourceFile, preferences); - // if promotedDeclaration.Kind == ast.KindImportSpecifier { - // return diagnostics.FormatMessage(diagnostics.Remove_type_from_import_of_0_from_1, symbolName, getModuleSpecifierText(promotedDeclaration.parent.parent)) - // } - // return diagnostics.FormatMessage(diagnostics.Remove_type_from_import_declaration_from_0, getModuleSpecifierText(promotedDeclaration)); - default: - panic(fmt.Sprintf(`Unexpected fix kind %v`, fix.kind)) - } - return nil -} - -func getModuleSpecifierText(promotedDeclaration *ast.ImportDeclaration) string { - if promotedDeclaration.Kind == ast.KindImportEqualsDeclaration { - importEqualsDeclaration := promotedDeclaration.AsImportEqualsDeclaration() - if ast.IsExternalModuleReference(importEqualsDeclaration.ModuleReference) { - expr := importEqualsDeclaration.ModuleReference.Expression() - if expr != nil && expr.Kind == ast.KindStringLiteral { - return expr.Text() - } - - } - return importEqualsDeclaration.ModuleReference.Text() - } - return promotedDeclaration.Parent.ModuleSpecifier().Text() -} diff --git a/kitcom/internal/tsgo/ls/autoimportsexportinfo.go b/kitcom/internal/tsgo/ls/autoimportsexportinfo.go deleted file mode 100644 index 0c3736f..0000000 --- a/kitcom/internal/tsgo/ls/autoimportsexportinfo.go +++ /dev/null @@ -1,181 +0,0 @@ -package ls - -import ( - "context" - - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/checker" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner" -) - -func (l *LanguageService) getExportInfos( - ctx context.Context, - ch *checker.Checker, - importingFile *ast.SourceFile, - preferences *UserPreferences, - exportMapKey ExportInfoMapKey, -) []*SymbolExportInfo { - expInfoMap := NewExportInfoMap(l.GetProgram().GetGlobalTypingsCacheLocation()) - moduleCount := 0 - symbolNameMatch := func(symbolName string) bool { - return symbolName == exportMapKey.SymbolName - } - forEachExternalModuleToImportFrom( - ch, - l.GetProgram(), - preferences, - // /*useAutoImportProvider*/ true, - func(moduleSymbol *ast.Symbol, moduleFile *ast.SourceFile, ch *checker.Checker, isFromPackageJson bool) { - if moduleCount = moduleCount + 1; moduleCount%100 == 0 && ctx.Err() != nil { - return - } - if moduleFile == nil && moduleSymbol.Name != exportMapKey.AmbientModuleName { - return - } - seenExports := collections.Set[string]{} - defaultInfo := getDefaultLikeExportInfo(moduleSymbol, ch) - var exportingModuleSymbol *ast.Symbol - if defaultInfo != nil { - exportingModuleSymbol = defaultInfo.exportingModuleSymbol - // Note: I think we shouldn't actually see resolved module symbols here, but weird merges - // can cause it to happen: see 'completionsImport_mergedReExport.ts' - if isImportableSymbol(exportingModuleSymbol, ch) { - expInfoMap.add( - importingFile.Path(), - exportingModuleSymbol, - core.IfElse(defaultInfo.exportKind == ExportKindDefault, ast.InternalSymbolNameDefault, ast.InternalSymbolNameExportEquals), - moduleSymbol, - moduleFile, - defaultInfo.exportKind, - isFromPackageJson, - ch, - symbolNameMatch, - nil, - ) - } - } - ch.ForEachExportAndPropertyOfModule(moduleSymbol, func(exported *ast.Symbol, key string) { - if exported != exportingModuleSymbol && isImportableSymbol(exported, ch) && seenExports.AddIfAbsent(key) { - expInfoMap.add( - importingFile.Path(), - exported, - key, - moduleSymbol, - moduleFile, - ExportKindNamed, - isFromPackageJson, - ch, - symbolNameMatch, - nil, - ) - } - }) - }) - return expInfoMap.get(importingFile.Path(), ch, exportMapKey) -} - -func (l *LanguageService) searchExportInfosForCompletions( - ctx context.Context, - ch *checker.Checker, - importingFile *ast.SourceFile, - preferences *UserPreferences, - isForImportStatementCompletion bool, - isRightOfOpenTag bool, - isTypeOnlyLocation bool, - lowerCaseTokenText string, - action func([]*SymbolExportInfo, string, bool, ExportInfoMapKey) []*SymbolExportInfo, -) { - symbolNameMatches := map[string]bool{} - symbolNameMatch := func(symbolName string) bool { - if !scanner.IsIdentifierText(symbolName, importingFile.LanguageVariant) { - return false - } - if b, ok := symbolNameMatches[symbolName]; ok { - return b - } - if isNonContextualKeyword(scanner.StringToToken(symbolName)) { - symbolNameMatches[symbolName] = false - return false - } - // Do not try to auto-import something with a lowercase first letter for a JSX tag - firstChar := rune(symbolName[0]) - if isRightOfOpenTag && (firstChar < 'A' || firstChar > 'Z') { - symbolNameMatches[symbolName] = false - return false - } - - symbolNameMatches[symbolName] = charactersFuzzyMatchInString(symbolName, lowerCaseTokenText) - return symbolNameMatches[symbolName] - } - flagMatch := func(targetFlags ast.SymbolFlags) bool { - if !isTypeOnlyLocation && !isForImportStatementCompletion && (targetFlags&ast.SymbolFlagsValue) == 0 { - return false - } - if isTypeOnlyLocation && (targetFlags&(ast.SymbolFlagsModule|ast.SymbolFlagsType) == 0) { - return false - } - return true - } - - expInfoMap := NewExportInfoMap(l.GetProgram().GetGlobalTypingsCacheLocation()) - moduleCount := 0 - forEachExternalModuleToImportFrom( - ch, - l.GetProgram(), - preferences, - // /*useAutoImportProvider*/ true, - func(moduleSymbol *ast.Symbol, moduleFile *ast.SourceFile, ch *checker.Checker, isFromPackageJson bool) { - if moduleCount = moduleCount + 1; moduleCount%100 == 0 && ctx.Err() != nil { - return - } - seenExports := collections.Set[string]{} - defaultInfo := getDefaultLikeExportInfo(moduleSymbol, ch) - // Note: I think we shouldn't actually see resolved module symbols here, but weird merges - // can cause it to happen: see 'completionsImport_mergedReExport.ts' - if defaultInfo != nil && isImportableSymbol(defaultInfo.exportingModuleSymbol, ch) { - expInfoMap.add( - importingFile.Path(), - defaultInfo.exportingModuleSymbol, - core.IfElse(defaultInfo.exportKind == ExportKindDefault, ast.InternalSymbolNameDefault, ast.InternalSymbolNameExportEquals), - moduleSymbol, - moduleFile, - defaultInfo.exportKind, - isFromPackageJson, - ch, - symbolNameMatch, - flagMatch, - ) - } - var exportingModuleSymbol *ast.Symbol - if defaultInfo != nil { - exportingModuleSymbol = defaultInfo.exportingModuleSymbol - } - ch.ForEachExportAndPropertyOfModule(moduleSymbol, func(exported *ast.Symbol, key string) { - if exported != exportingModuleSymbol && isImportableSymbol(exported, ch) && seenExports.AddIfAbsent(key) { - expInfoMap.add( - importingFile.Path(), - exported, - key, - moduleSymbol, - moduleFile, - ExportKindNamed, - isFromPackageJson, - ch, - symbolNameMatch, - flagMatch, - ) - } - }) - }) - expInfoMap.search( - ch, - importingFile.Path(), - /*preferCapitalized*/ isRightOfOpenTag, - func(symbolName string, targetFlags ast.SymbolFlags) bool { - return symbolNameMatch(symbolName) && flagMatch(targetFlags) - }, - action, - ) -} diff --git a/kitcom/internal/tsgo/ls/autoimportstypes.go b/kitcom/internal/tsgo/ls/autoimportstypes.go deleted file mode 100644 index 0f087bc..0000000 --- a/kitcom/internal/tsgo/ls/autoimportstypes.go +++ /dev/null @@ -1,215 +0,0 @@ -package ls - -import ( - "fmt" - - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/checker" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/modulespecifiers" -) - -//go:generate go tool golang.org/x/tools/cmd/stringer -type=ExportKind -output=autoImports_stringer_generated.go -//go:generate go tool mvdan.cc/gofumpt -w autoImports_stringer_generated.go - -type ImportKind int - -const ( - ImportKindNamed ImportKind = 0 - ImportKindDefault ImportKind = 1 - ImportKindNamespace ImportKind = 2 - ImportKindCommonJS ImportKind = 3 -) - -type ExportKind int - -const ( - ExportKindNamed ExportKind = 0 - ExportKindDefault ExportKind = 1 - ExportKindExportEquals ExportKind = 2 - ExportKindUMD ExportKind = 3 - ExportKindModule ExportKind = 4 -) - -type ImportFixKind int - -const ( - // Sorted with the preferred fix coming first. - ImportFixKindUseNamespace ImportFixKind = 0 - ImportFixKindJsdocTypeImport ImportFixKind = 1 - ImportFixKindAddToExisting ImportFixKind = 2 - ImportFixKindAddNew ImportFixKind = 3 - ImportFixKindPromoteTypeOnly ImportFixKind = 4 -) - -type AddAsTypeOnly int - -const ( - // These should not be combined as bitflags, but are given powers of 2 values to - // easily detect conflicts between `NotAllowed` and `Required` by giving them a unique sum. - // They're also ordered in terms of increasing priority for a fix-all scenario (see - // `reduceAddAsTypeOnlyValues`). - AddAsTypeOnlyAllowed AddAsTypeOnly = 1 << 0 - AddAsTypeOnlyRequired AddAsTypeOnly = 1 << 1 - AddAsTypeOnlyNotAllowed AddAsTypeOnly = 1 << 2 -) - -type ImportFix struct { - kind ImportFixKind - isReExport *bool - exportInfo *SymbolExportInfo // !!! | FutureSymbolExportInfo | undefined - moduleSpecifierKind modulespecifiers.ResultKind - moduleSpecifier string - usagePosition *lsproto.Position - namespacePrefix *string - - importClauseOrBindingPattern *ast.Node // ImportClause | ObjectBindingPattern - importKind ImportKind // ImportKindDefault | ImportKindNamed - addAsTypeOnly AddAsTypeOnly - propertyName string // !!! not implemented - - useRequire bool - - typeOnlyAliasDeclaration *ast.Declaration // TypeOnlyAliasDeclaration -} - -func (i *ImportFix) qualification() *Qualification { - switch i.kind { - case ImportFixKindAddNew: - if i.usagePosition == nil || strPtrIsEmpty(i.namespacePrefix) { - return nil - } - fallthrough - case ImportFixKindUseNamespace: - return &Qualification{ - usagePosition: *i.usagePosition, - namespacePrefix: *i.namespacePrefix, - } - } - panic(fmt.Sprintf("no qualification with ImportFixKind %v", i.kind)) -} - -type Qualification struct { - usagePosition lsproto.Position - namespacePrefix string -} - -func getUseNamespaceImport( - moduleSpecifier string, - moduleSpecifierKind modulespecifiers.ResultKind, - namespacePrefix string, - usagePosition lsproto.Position, -) *ImportFix { - return &ImportFix{ - kind: ImportFixKindUseNamespace, - moduleSpecifierKind: moduleSpecifierKind, - moduleSpecifier: moduleSpecifier, - - usagePosition: ptrTo(usagePosition), - namespacePrefix: strPtrTo(namespacePrefix), - } -} - -func getAddJsdocTypeImport( - moduleSpecifier string, - moduleSpecifierKind modulespecifiers.ResultKind, - usagePosition *lsproto.Position, - exportInfo *SymbolExportInfo, - isReExport *bool, -) *ImportFix { - return &ImportFix{ - kind: ImportFixKindJsdocTypeImport, - isReExport: isReExport, - exportInfo: exportInfo, - moduleSpecifierKind: moduleSpecifierKind, - moduleSpecifier: moduleSpecifier, - usagePosition: usagePosition, - } -} - -func getAddToExistingImport( - importClauseOrBindingPattern *ast.Node, - importKind ImportKind, - moduleSpecifier string, - moduleSpecifierKind modulespecifiers.ResultKind, - addAsTypeOnly AddAsTypeOnly, -) *ImportFix { - return &ImportFix{ - kind: ImportFixKindAddToExisting, - moduleSpecifierKind: moduleSpecifierKind, - moduleSpecifier: moduleSpecifier, - importClauseOrBindingPattern: importClauseOrBindingPattern, - importKind: importKind, - addAsTypeOnly: addAsTypeOnly, - } -} - -func getNewAddNewImport( - moduleSpecifier string, - moduleSpecifierKind modulespecifiers.ResultKind, - importKind ImportKind, - useRequire bool, - addAsTypeOnly AddAsTypeOnly, - exportInfo *SymbolExportInfo, // !!! | FutureSymbolExportInfo - isReExport *bool, - qualification *Qualification, -) *ImportFix { - return &ImportFix{ - kind: ImportFixKindAddNew, - isReExport: isReExport, - exportInfo: exportInfo, - moduleSpecifierKind: modulespecifiers.ResultKindNone, - moduleSpecifier: moduleSpecifier, - importKind: importKind, - addAsTypeOnly: addAsTypeOnly, - useRequire: useRequire, - } -} - -func getNewPromoteTypeOnlyImport(typeOnlyAliasDeclaration *ast.Declaration) *ImportFix { - // !!! function stub - return &ImportFix{ - kind: ImportFixKindPromoteTypeOnly, - // isReExport *bool - // exportInfo *SymbolExportInfo // !!! | FutureSymbolExportInfo | undefined - // moduleSpecifierKind modulespecifiers.ResultKind - // moduleSpecifier string - typeOnlyAliasDeclaration: typeOnlyAliasDeclaration, - } -} - -/** Information needed to augment an existing import declaration. */ -// !!! after full implementation, rename to AddToExistingImportInfo -type FixAddToExistingImportInfo struct { - declaration *ast.Declaration - importKind ImportKind - targetFlags ast.SymbolFlags - symbol *ast.Symbol -} - -func (info *FixAddToExistingImportInfo) getNewImportFromExistingSpecifier( - isValidTypeOnlyUseSite bool, - useRequire bool, - ch *checker.Checker, - compilerOptions *core.CompilerOptions, -) *ImportFix { - moduleSpecifier := checker.TryGetModuleSpecifierFromDeclaration(info.declaration) - if moduleSpecifier == nil || moduleSpecifier.Text() == "" { - return nil - } - addAsTypeOnly := AddAsTypeOnlyNotAllowed - if !useRequire { - addAsTypeOnly = getAddAsTypeOnly(isValidTypeOnlyUseSite, info.symbol, info.targetFlags, ch, compilerOptions) - } - return getNewAddNewImport( - moduleSpecifier.Text(), - modulespecifiers.ResultKindNone, - info.importKind, - useRequire, - addAsTypeOnly, - nil, // exportInfo - nil, // isReExport - nil, // qualification - ) -} diff --git a/kitcom/internal/tsgo/ls/changetracker.go b/kitcom/internal/tsgo/ls/changetracker.go deleted file mode 100644 index 00dcecf..0000000 --- a/kitcom/internal/tsgo/ls/changetracker.go +++ /dev/null @@ -1,336 +0,0 @@ -package ls - -import ( - "context" - "slices" - - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/format" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/printer" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/stringutil" -) - -type changeNodeOptions struct { - // Text to be inserted before the new node - prefix string - - // Text to be inserted after the new node - suffix string - - // Text of inserted node will be formatted with this indentation, otherwise indentation will be inferred from the old node - indentation *int - - // Text of inserted node will be formatted with this delta, otherwise delta will be inferred from the new node kind - delta *int - - leadingTriviaOption - trailingTriviaOption - joiner string -} - -type leadingTriviaOption int - -const ( - leadingTriviaOptionNone leadingTriviaOption = 0 - leadingTriviaOptionExclude leadingTriviaOption = 1 - leadingTriviaOptionIncludeAll leadingTriviaOption = 2 - leadingTriviaOptionJSDoc leadingTriviaOption = 3 - leadingTriviaOptionStartLine leadingTriviaOption = 4 -) - -type trailingTriviaOption int - -const ( - trailingTriviaOptionNone trailingTriviaOption = 0 - trailingTriviaOptionExclude trailingTriviaOption = 1 - trailingTriviaOptionExcludeWhitespace trailingTriviaOption = 2 - trailingTriviaOptionInclude trailingTriviaOption = 3 -) - -type trackerEditKind int - -const ( - trackerEditKindText trackerEditKind = 1 - trackerEditKindRemove trackerEditKind = 2 - trackerEditKindReplaceWithSingleNode trackerEditKind = 3 - trackerEditKindReplaceWithMultipleNodes trackerEditKind = 4 -) - -type trackerEdit struct { - kind trackerEditKind - lsproto.Range - - NewText string // kind == text - - *ast.Node // single - nodes []*ast.Node // multiple - options changeNodeOptions -} - -type changeTracker struct { - // initialized with - formatSettings *format.FormatCodeSettings - newLine string - ls *LanguageService - ctx context.Context - *printer.EmitContext - - *ast.NodeFactory - changes *collections.MultiMap[*ast.SourceFile, *trackerEdit] - - // created during call to getChanges - writer *printer.ChangeTrackerWriter - // printer -} - -func (ls *LanguageService) newChangeTracker(ctx context.Context) *changeTracker { - emitContext := printer.NewEmitContext() - newLine := ls.GetProgram().Options().NewLine.GetNewLineCharacter() - formatCodeSettings := format.GetDefaultFormatCodeSettings(newLine) // !!! format.GetFormatCodeSettingsFromContext(ctx), - ctx = format.WithFormatCodeSettings(ctx, formatCodeSettings, newLine) - return &changeTracker{ - ls: ls, - EmitContext: emitContext, - NodeFactory: &emitContext.Factory.NodeFactory, - changes: &collections.MultiMap[*ast.SourceFile, *trackerEdit]{}, - ctx: ctx, - formatSettings: formatCodeSettings, - newLine: newLine, - } -} - -// !!! address strada note -// - Note: after calling this, the TextChanges object must be discarded! -func (ct *changeTracker) getChanges() map[string][]*lsproto.TextEdit { - // !!! finishDeleteDeclarations - // !!! finishClassesWithNodesInsertedAtStart - changes := ct.getTextChangesFromChanges() - // !!! changes for new files - return changes -} - -func (ct *changeTracker) replaceNode(sourceFile *ast.SourceFile, oldNode *ast.Node, newNode *ast.Node, options *changeNodeOptions) { - if options == nil { - // defaults to `useNonAdjustedPositions` - options = &changeNodeOptions{ - leadingTriviaOption: leadingTriviaOptionExclude, - trailingTriviaOption: trailingTriviaOptionExclude, - } - } - ct.replaceRange(sourceFile, ct.getAdjustedRange(sourceFile, oldNode, oldNode, options.leadingTriviaOption, options.trailingTriviaOption), newNode, *options) -} - -func (ct *changeTracker) replaceRange(sourceFile *ast.SourceFile, lsprotoRange lsproto.Range, newNode *ast.Node, options changeNodeOptions) { - ct.changes.Add(sourceFile, &trackerEdit{kind: trackerEditKindReplaceWithSingleNode, Range: lsprotoRange, options: options, Node: newNode}) -} - -func (ct *changeTracker) replaceRangeWithText(sourceFile *ast.SourceFile, lsprotoRange lsproto.Range, text string) { - ct.changes.Add(sourceFile, &trackerEdit{kind: trackerEditKindText, Range: lsprotoRange, NewText: text}) -} - -func (ct *changeTracker) replaceRangeWithNodes(sourceFile *ast.SourceFile, lsprotoRange lsproto.Range, newNodes []*ast.Node, options changeNodeOptions) { - if len(newNodes) == 1 { - ct.replaceRange(sourceFile, lsprotoRange, newNodes[0], options) - return - } - ct.changes.Add(sourceFile, &trackerEdit{kind: trackerEditKindReplaceWithMultipleNodes, Range: lsprotoRange, nodes: newNodes, options: options}) -} - -func (ct *changeTracker) insertText(sourceFile *ast.SourceFile, pos lsproto.Position, text string) { - ct.replaceRangeWithText(sourceFile, lsproto.Range{Start: pos, End: pos}, text) -} - -func (ct *changeTracker) insertNodeAt(sourceFile *ast.SourceFile, pos core.TextPos, newNode *ast.Node, options changeNodeOptions) { - lsPos := ct.ls.converters.PositionToLineAndCharacter(sourceFile, pos) - ct.replaceRange(sourceFile, lsproto.Range{Start: lsPos, End: lsPos}, newNode, options) -} - -func (ct *changeTracker) insertNodesAt(sourceFile *ast.SourceFile, pos core.TextPos, newNodes []*ast.Node, options changeNodeOptions) { - lsPos := ct.ls.converters.PositionToLineAndCharacter(sourceFile, pos) - ct.replaceRangeWithNodes(sourceFile, lsproto.Range{Start: lsPos, End: lsPos}, newNodes, options) -} - -func (ct *changeTracker) insertNodeAfter(sourceFile *ast.SourceFile, after *ast.Node, newNode *ast.Node) { - endPosition := ct.endPosForInsertNodeAfter(sourceFile, after, newNode) - ct.insertNodeAt(sourceFile, endPosition, newNode, ct.getInsertNodeAfterOptions(sourceFile, after)) -} - -func (ct *changeTracker) insertNodesAfter(sourceFile *ast.SourceFile, after *ast.Node, newNodes []*ast.Node) { - endPosition := ct.endPosForInsertNodeAfter(sourceFile, after, newNodes[0]) - ct.insertNodesAt(sourceFile, endPosition, newNodes, ct.getInsertNodeAfterOptions(sourceFile, after)) -} - -func (ct *changeTracker) endPosForInsertNodeAfter(sourceFile *ast.SourceFile, after *ast.Node, newNode *ast.Node) core.TextPos { - if (needSemicolonBetween(after, newNode)) && (rune(sourceFile.Text()[after.End()-1]) != ';') { - // check if previous statement ends with semicolon - // if not - insert semicolon to preserve the code from changing the meaning due to ASI - endPos := ct.ls.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(after.End())) - ct.replaceRange(sourceFile, - lsproto.Range{Start: endPos, End: endPos}, - sourceFile.GetOrCreateToken(ast.KindSemicolonToken, after.End(), after.End(), after.Parent), - changeNodeOptions{}, - ) - } - return core.TextPos(ct.getAdjustedEndPosition(sourceFile, after, trailingTriviaOptionNone)) -} - -/** -* This function should be used to insert nodes in lists when nodes don't carry separators as the part of the node range, -* i.e. arguments in arguments lists, parameters in parameter lists etc. -* Note that separators are part of the node in statements and class elements. - */ -func (ct *changeTracker) insertNodeInListAfter(sourceFile *ast.SourceFile, after *ast.Node, newNode *ast.Node, containingList []*ast.Node) { - if len(containingList) == 0 { - containingList = format.GetContainingList(after, sourceFile).Nodes - } - index := slices.Index(containingList, after) - if index < 0 { - return - } - if index != len(containingList)-1 { - // any element except the last one - // use next sibling as an anchor - if nextToken := astnav.GetTokenAtPosition(sourceFile, after.End()); nextToken != nil && isSeparator(after, nextToken) { - // for list - // a, b, c - // create change for adding 'e' after 'a' as - // - find start of next element after a (it is b) - // - use next element start as start and end position in final change - // - build text of change by formatting the text of node + whitespace trivia of b - - // in multiline case it will work as - // a, - // b, - // c, - // result - '*' denotes leading trivia that will be inserted after new text (displayed as '#') - // a, - // insertedtext# - // ###b, - // c, - nextNode := containingList[index+1] - startPos := scanner.SkipTriviaEx(sourceFile.Text(), nextNode.Pos(), &scanner.SkipTriviaOptions{StopAfterLineBreak: true, StopAtComments: false}) - - // write separator and leading trivia of the next element as suffix - suffix := scanner.TokenToString(nextToken.Kind) + sourceFile.Text()[nextNode.End():startPos] - ct.insertNodeAt(sourceFile, core.TextPos(startPos), newNode, changeNodeOptions{suffix: suffix}) - } - return - } - - afterStart := astnav.GetStartOfNode(after, sourceFile, false) - afterStartLinePosition := format.GetLineStartPositionForPosition(afterStart, sourceFile) - - // insert element after the last element in the list that has more than one item - // pick the element preceding the after element to: - // - pick the separator - // - determine if list is a multiline - multilineList := false - - // if list has only one element then we'll format is as multiline if node has comment in trailing trivia, or as singleline otherwise - // i.e. var x = 1 // this is x - // | new element will be inserted at this position - separator := ast.KindCommaToken // SyntaxKind.CommaToken | SyntaxKind.SemicolonToken - if len(containingList) != 1 { - // otherwise, if list has more than one element, pick separator from the list - tokenBeforeInsertPosition := astnav.FindPrecedingToken(sourceFile, after.Pos()) - separator = core.IfElse(isSeparator(after, tokenBeforeInsertPosition), tokenBeforeInsertPosition.Kind, ast.KindCommaToken) - // determine if list is multiline by checking lines of after element and element that precedes it. - afterMinusOneStartLinePosition := format.GetLineStartPositionForPosition(astnav.GetStartOfNode(containingList[index-1], sourceFile, false), sourceFile) - multilineList = afterMinusOneStartLinePosition != afterStartLinePosition - } - if hasCommentsBeforeLineBreak(sourceFile.Text(), after.End()) || printer.GetLinesBetweenPositions(sourceFile, containingList[0].Pos(), containingList[len(containingList)-1].End()) != 0 { - // in this case we'll always treat containing list as multiline - multilineList = true - } - - separatorString := scanner.TokenToString(separator) - end := ct.ls.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(after.End())) - if !multilineList { - ct.replaceRange(sourceFile, lsproto.Range{Start: end, End: end}, newNode, changeNodeOptions{prefix: separatorString}) - return - } - - // insert separator immediately following the 'after' node to preserve comments in trailing trivia - // !!! formatcontext - ct.replaceRange(sourceFile, lsproto.Range{Start: end, End: end}, sourceFile.GetOrCreateToken(separator, after.End(), after.End()+len(separatorString), after.Parent), changeNodeOptions{}) - // use the same indentation as 'after' item - indentation := format.FindFirstNonWhitespaceColumn(afterStartLinePosition, afterStart, sourceFile, ct.formatSettings) - // insert element before the line break on the line that contains 'after' element - insertPos := scanner.SkipTriviaEx(sourceFile.Text(), after.End(), &scanner.SkipTriviaOptions{StopAfterLineBreak: true, StopAtComments: false}) - // find position before "\n" or "\r\n" - for insertPos != after.End() && stringutil.IsLineBreak(rune(sourceFile.Text()[insertPos-1])) { - insertPos-- - } - insertLSPos := ct.ls.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(insertPos)) - ct.replaceRange( - sourceFile, - lsproto.Range{Start: insertLSPos, End: insertLSPos}, - newNode, - changeNodeOptions{ - indentation: ptrTo(indentation), - prefix: ct.newLine, - }, - ) -} - -func (ct *changeTracker) insertAtTopOfFile(sourceFile *ast.SourceFile, insert []*ast.Statement, blankLineBetween bool) { - if len(insert) == 0 { - return - } - - pos := ct.getInsertionPositionAtSourceFileTop(sourceFile) - options := changeNodeOptions{} - if pos != 0 { - options.prefix = ct.newLine - } - if !stringutil.IsLineBreak(rune(sourceFile.Text()[pos])) { - options.suffix = ct.newLine - } - if blankLineBetween { - options.suffix += ct.newLine - } - - if len(insert) == 1 { - ct.insertNodeAt(sourceFile, core.TextPos(pos), insert[0], options) - } else { - ct.insertNodesAt(sourceFile, core.TextPos(pos), insert, options) - } -} - -func (ct *changeTracker) getInsertNodeAfterOptions(sourceFile *ast.SourceFile, node *ast.Node) changeNodeOptions { - newLineChar := ct.newLine - var options changeNodeOptions - switch node.Kind { - case ast.KindParameter: - // default opts - options = changeNodeOptions{} - case ast.KindClassDeclaration, ast.KindModuleDeclaration: - options = changeNodeOptions{prefix: newLineChar, suffix: newLineChar} - - case ast.KindVariableDeclaration, ast.KindStringLiteral, ast.KindIdentifier: - options = changeNodeOptions{prefix: ", "} - - case ast.KindPropertyAssignment: - options = changeNodeOptions{suffix: "," + newLineChar} - - case ast.KindExportKeyword: - options = changeNodeOptions{prefix: " "} - - default: - if !(ast.IsStatement(node) || ast.IsClassOrTypeElement(node)) { - // Else we haven't handled this kind of node yet -- add it - panic("unimplemented node type " + node.Kind.String() + " in changeTracker.getInsertNodeAfterOptions") - } - options = changeNodeOptions{suffix: newLineChar} - } - if node.End() == sourceFile.End() && ast.IsStatement(node) { - options.prefix = "\n" + options.prefix - } - - return options -} diff --git a/kitcom/internal/tsgo/ls/changetrackerimpl.go b/kitcom/internal/tsgo/ls/changetrackerimpl.go deleted file mode 100644 index 7273bde..0000000 --- a/kitcom/internal/tsgo/ls/changetrackerimpl.go +++ /dev/null @@ -1,413 +0,0 @@ -package ls - -import ( - "fmt" - "slices" - "strings" - "unicode" - - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/format" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/parser" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/printer" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/stringutil" -) - -func (ct *changeTracker) getTextChangesFromChanges() map[string][]*lsproto.TextEdit { - changes := map[string][]*lsproto.TextEdit{} - for sourceFile, changesInFile := range ct.changes.M { - // order changes by start position - // If the start position is the same, put the shorter range first, since an empty range (x, x) may precede (x, y) but not vice-versa. - slices.SortStableFunc(changesInFile, func(a, b *trackerEdit) int { return CompareRanges(ptrTo(a.Range), ptrTo(b.Range)) }) - // verify that change intervals do not overlap, except possibly at end points. - for i := range len(changesInFile) - 1 { - if ComparePositions(changesInFile[i].Range.End, changesInFile[i+1].Range.Start) > 0 { - // assert change[i].End <= change[i + 1].Start - panic(fmt.Sprintf("changes overlap: %v and %v", changesInFile[i].Range, changesInFile[i+1].Range)) - } - } - - textChanges := core.MapNonNil(changesInFile, func(change *trackerEdit) *lsproto.TextEdit { - // !!! targetSourceFile - - newText := ct.computeNewText(change, sourceFile, sourceFile) - // span := createTextSpanFromRange(c.Range) - // !!! - // Filter out redundant changes. - // if (span.length == newText.length && stringContainsAt(targetSourceFile.text, newText, span.start)) { return nil } - - return &lsproto.TextEdit{ - NewText: newText, - Range: change.Range, - } - }) - - if len(textChanges) > 0 { - changes[sourceFile.FileName()] = textChanges - } - } - return changes -} - -func (ct *changeTracker) computeNewText(change *trackerEdit, targetSourceFile *ast.SourceFile, sourceFile *ast.SourceFile) string { - switch change.kind { - case trackerEditKindRemove: - return "" - case trackerEditKindText: - return change.NewText - } - - pos := int(ct.ls.converters.LineAndCharacterToPosition(sourceFile, change.Range.Start)) - formatNode := func(n *ast.Node) string { - return ct.getFormattedTextOfNode(n, targetSourceFile, sourceFile, pos, change.options) - } - - var text string - switch change.kind { - - case trackerEditKindReplaceWithMultipleNodes: - if change.options.joiner == "" { - change.options.joiner = ct.newLine - } - text = strings.Join(core.Map(change.nodes, func(n *ast.Node) string { return strings.TrimSuffix(formatNode(n), ct.newLine) }), change.options.joiner) - case trackerEditKindReplaceWithSingleNode: - text = formatNode(change.Node) - default: - panic(fmt.Sprintf("change kind %d should have been handled earlier", change.kind)) - } - // strip initial indentation (spaces or tabs) if text will be inserted in the middle of the line - noIndent := text - if !(change.options.indentation != nil && *change.options.indentation != 0 || format.GetLineStartPositionForPosition(pos, targetSourceFile) == pos) { - noIndent = strings.TrimLeftFunc(text, unicode.IsSpace) - } - return change.options.prefix + noIndent // !!! +((!options.suffix || endsWith(noIndent, options.suffix)) ? "" : options.suffix); -} - -/** Note: this may mutate `nodeIn`. */ -func (ct *changeTracker) getFormattedTextOfNode(nodeIn *ast.Node, targetSourceFile *ast.SourceFile, sourceFile *ast.SourceFile, pos int, options changeNodeOptions) string { - text, sourceFileLike := ct.getNonformattedText(nodeIn, targetSourceFile) - // !!! if (validate) validate(node, text); - formatOptions := getFormatCodeSettingsForWriting(ct.formatSettings, targetSourceFile) - - var initialIndentation, delta int - if options.indentation == nil { - // !!! indentation for position - // initialIndentation = format.GetIndentationForPos(pos, sourceFile, formatOptions, options.prefix == ct.newLine || scanner.GetLineStartPositionForPosition(pos, targetFileLineMap) == pos); - } else { - initialIndentation = *options.indentation - } - - if options.delta != nil { - delta = *options.delta - } else if formatOptions.IndentSize != 0 && format.ShouldIndentChildNode(formatOptions, nodeIn, nil, nil) { - delta = formatOptions.IndentSize - } - - changes := format.FormatNodeGivenIndentation(ct.ctx, sourceFileLike, sourceFileLike.AsSourceFile(), targetSourceFile.LanguageVariant, initialIndentation, delta) - return core.ApplyBulkEdits(text, changes) -} - -func getFormatCodeSettingsForWriting(options *format.FormatCodeSettings, sourceFile *ast.SourceFile) *format.FormatCodeSettings { - shouldAutoDetectSemicolonPreference := options.Semicolons == format.SemicolonPreferenceIgnore - shouldRemoveSemicolons := options.Semicolons == format.SemicolonPreferenceRemove || shouldAutoDetectSemicolonPreference && !probablyUsesSemicolons(sourceFile) - if shouldRemoveSemicolons { - options.Semicolons = format.SemicolonPreferenceRemove - } - - return options -} - -/** Note: output node may be mutated input node. */ -func (ct *changeTracker) getNonformattedText(node *ast.Node, sourceFile *ast.SourceFile) (string, *ast.Node) { - nodeIn := node - eofToken := ct.Factory.NewToken(ast.KindEndOfFile) - if ast.IsStatement(node) { - nodeIn = ct.Factory.NewSourceFile( - ast.SourceFileParseOptions{FileName: sourceFile.FileName(), Path: sourceFile.Path()}, - "", - ct.Factory.NewNodeList([]*ast.Node{node}), - ct.Factory.NewToken(ast.KindEndOfFile), - ) - } - writer := printer.NewChangeTrackerWriter(ct.newLine) - printer.NewPrinter( - printer.PrinterOptions{ - NewLine: core.GetNewLineKind(ct.newLine), - NeverAsciiEscape: true, - PreserveSourceNewlines: true, - TerminateUnterminatedLiterals: true, - }, - writer.GetPrintHandlers(), - ct.EmitContext, - ).Write(nodeIn, sourceFile, writer, nil) - - text := writer.String() - - nodeOut := writer.AssignPositionsToNode(nodeIn, ct.NodeFactory) - var sourceFileLike *ast.Node - if !ast.IsStatement(node) { - nodeList := ct.Factory.NewNodeList([]*ast.Node{nodeOut}) - nodeList.Loc = nodeOut.Loc - eofToken.Loc = core.NewTextRange(nodeOut.End(), nodeOut.End()) - sourceFileLike = ct.Factory.NewSourceFile( - ast.SourceFileParseOptions{FileName: sourceFile.FileName(), Path: sourceFile.Path()}, - text, - nodeList, - eofToken, - ) - sourceFileLike.ForEachChild(func(child *ast.Node) bool { - child.Parent = sourceFileLike - return true - }) - sourceFileLike.Loc = nodeOut.Loc - } else { - sourceFileLike = nodeOut - } - return text, sourceFileLike -} - -// method on the changeTracker because use of converters -func (ct *changeTracker) getAdjustedRange(sourceFile *ast.SourceFile, startNode *ast.Node, endNode *ast.Node, leadingOption leadingTriviaOption, trailingOption trailingTriviaOption) lsproto.Range { - return *ct.ls.createLspRangeFromBounds( - ct.getAdjustedStartPosition(sourceFile, startNode, leadingOption, false), - ct.getAdjustedEndPosition(sourceFile, endNode, trailingOption), - sourceFile, - ) -} - -// method on the changeTracker because use of converters -func (ct *changeTracker) getAdjustedStartPosition(sourceFile *ast.SourceFile, node *ast.Node, leadingOption leadingTriviaOption, hasTrailingComment bool) int { - if leadingOption == leadingTriviaOptionJSDoc { - if JSDocComments := parser.GetJSDocCommentRanges(ct.NodeFactory, nil, node, sourceFile.Text()); len(JSDocComments) > 0 { - return format.GetLineStartPositionForPosition(JSDocComments[0].Pos(), sourceFile) - } - } - - start := astnav.GetStartOfNode(node, sourceFile, false) - startOfLinePos := format.GetLineStartPositionForPosition(start, sourceFile) - - switch leadingOption { - case leadingTriviaOptionExclude: - return start - case leadingTriviaOptionStartLine: - if node.Loc.ContainsInclusive(startOfLinePos) { - return startOfLinePos - } - return start - } - - fullStart := node.Pos() - if fullStart == start { - return start - } - lineStarts := sourceFile.ECMALineMap() - fullStartLineIndex := scanner.ComputeLineOfPosition(lineStarts, fullStart) - fullStartLinePos := int(lineStarts[fullStartLineIndex]) - if startOfLinePos == fullStartLinePos { - // full start and start of the node are on the same line - // a, b; - // ^ ^ - // | start - // fullstart - // when b is replaced - we usually want to keep the leading trvia - // when b is deleted - we delete it - if leadingOption == leadingTriviaOptionIncludeAll { - return fullStart - } - return start - } - - // if node has a trailing comments, use comment end position as the text has already been included. - if hasTrailingComment { - // Check first for leading comments as if the node is the first import, we want to exclude the trivia; - // otherwise we get the trailing comments. - comments := slices.Collect(scanner.GetLeadingCommentRanges(ct.NodeFactory, sourceFile.Text(), fullStart)) - if len(comments) == 0 { - comments = slices.Collect(scanner.GetTrailingCommentRanges(ct.NodeFactory, sourceFile.Text(), fullStart)) - } - if len(comments) > 0 { - return scanner.SkipTriviaEx(sourceFile.Text(), comments[0].End(), &scanner.SkipTriviaOptions{StopAfterLineBreak: true, StopAtComments: true}) - } - } - - // get start position of the line following the line that contains fullstart position - // (but only if the fullstart isn't the very beginning of the file) - nextLineStart := core.IfElse(fullStart > 0, 1, 0) - adjustedStartPosition := int(lineStarts[fullStartLineIndex+nextLineStart]) - // skip whitespaces/newlines - adjustedStartPosition = scanner.SkipTriviaEx(sourceFile.Text(), adjustedStartPosition, &scanner.SkipTriviaOptions{StopAtComments: true}) - return int(lineStarts[scanner.ComputeLineOfPosition(lineStarts, adjustedStartPosition)]) -} - -// method on the changeTracker because of converters -// Return the end position of a multiline comment of it is on another line; otherwise returns `undefined`; -func (ct *changeTracker) getEndPositionOfMultilineTrailingComment(sourceFile *ast.SourceFile, node *ast.Node, trailingOpt trailingTriviaOption) int { - if trailingOpt == trailingTriviaOptionInclude { - // If the trailing comment is a multiline comment that extends to the next lines, - // return the end of the comment and track it for the next nodes to adjust. - lineStarts := sourceFile.ECMALineMap() - nodeEndLine := scanner.ComputeLineOfPosition(lineStarts, node.End()) - for comment := range scanner.GetTrailingCommentRanges(ct.NodeFactory, sourceFile.Text(), node.End()) { - // Single line can break the loop as trivia will only be this line. - // Comments on subsequest lines are also ignored. - if comment.Kind == ast.KindSingleLineCommentTrivia || scanner.ComputeLineOfPosition(lineStarts, comment.Pos()) > nodeEndLine { - break - } - - // Get the end line of the comment and compare against the end line of the node. - // If the comment end line position and the multiline comment extends to multiple lines, - // then is safe to return the end position. - if commentEndLine := scanner.ComputeLineOfPosition(lineStarts, comment.End()); commentEndLine > nodeEndLine { - return scanner.SkipTriviaEx(sourceFile.Text(), comment.End(), &scanner.SkipTriviaOptions{StopAfterLineBreak: true, StopAtComments: true}) - } - } - } - - return 0 -} - -// method on the changeTracker because of converters -func (ct *changeTracker) getAdjustedEndPosition(sourceFile *ast.SourceFile, node *ast.Node, trailingTriviaOption trailingTriviaOption) int { - if trailingTriviaOption == trailingTriviaOptionExclude { - return node.End() - } - if trailingTriviaOption == trailingTriviaOptionExcludeWhitespace { - if comments := slices.AppendSeq( - slices.Collect(scanner.GetTrailingCommentRanges(ct.NodeFactory, sourceFile.Text(), node.End())), - scanner.GetLeadingCommentRanges(ct.NodeFactory, sourceFile.Text(), node.End()), - ); len(comments) > 0 { - if realEnd := comments[len(comments)-1].End(); realEnd != 0 { - return realEnd - } - } - return node.End() - } - - if multilineEndPosition := ct.getEndPositionOfMultilineTrailingComment(sourceFile, node, trailingTriviaOption); multilineEndPosition != 0 { - return multilineEndPosition - } - - newEnd := scanner.SkipTriviaEx(sourceFile.Text(), node.End(), &scanner.SkipTriviaOptions{StopAfterLineBreak: true}) - - if newEnd != node.End() && (trailingTriviaOption == trailingTriviaOptionInclude || stringutil.IsLineBreak(rune(sourceFile.Text()[newEnd-1]))) { - return newEnd - } - return node.End() -} - -// ============= utilities ============= - -func hasCommentsBeforeLineBreak(text string, start int) bool { - for _, ch := range []rune(text[start:]) { - if !stringutil.IsWhiteSpaceSingleLine(ch) { - return ch == '/' - } - } - return false -} - -func needSemicolonBetween(a, b *ast.Node) bool { - return (ast.IsPropertySignatureDeclaration(a) || ast.IsPropertyDeclaration(a)) && - ast.IsClassOrTypeElement(b) && - b.Name().Kind == ast.KindComputedPropertyName || - ast.IsStatementButNotDeclaration(a) && - ast.IsStatementButNotDeclaration(b) // TODO: only if b would start with a `(` or `[` -} - -func (ct *changeTracker) getInsertionPositionAtSourceFileTop(sourceFile *ast.SourceFile) int { - var lastPrologue *ast.Node - for _, node := range sourceFile.Statements.Nodes { - if ast.IsPrologueDirective(node) { - lastPrologue = node - } else { - break - } - } - - position := 0 - text := sourceFile.Text() - advancePastLineBreak := func() { - if position >= len(text) { - return - } - if char := rune(text[position]); stringutil.IsLineBreak(char) { - position++ - if position < len(text) && char == '\r' && rune(text[position]) == '\n' { - position++ - } - } - } - if lastPrologue != nil { - position = lastPrologue.End() - advancePastLineBreak() - return position - } - - shebang := scanner.GetShebang(text) - if shebang != "" { - position = len(shebang) - advancePastLineBreak() - } - - ranges := slices.Collect(scanner.GetLeadingCommentRanges(ct.NodeFactory, text, position)) - if len(ranges) == 0 { - return position - } - // Find the first attached comment to the first node and add before it - var lastComment *ast.CommentRange - pinnedOrTripleSlash := false - firstNodeLine := -1 - - lenStatements := len(sourceFile.Statements.Nodes) - lineMap := sourceFile.ECMALineMap() - for _, r := range ranges { - if r.Kind == ast.KindMultiLineCommentTrivia { - if printer.IsPinnedComment(text, r) { - lastComment = &r - pinnedOrTripleSlash = true - continue - } - } else if printer.IsRecognizedTripleSlashComment(text, r) { - lastComment = &r - pinnedOrTripleSlash = true - continue - } - - if lastComment != nil { - // Always insert after pinned or triple slash comments - if pinnedOrTripleSlash { - break - } - - // There was a blank line between the last comment and this comment. - // This comment is not part of the copyright comments - commentLine := scanner.ComputeLineOfPosition(lineMap, r.Pos()) - lastCommentEndLine := scanner.ComputeLineOfPosition(lineMap, lastComment.End()) - if commentLine >= lastCommentEndLine+2 { - break - } - } - - if lenStatements > 0 { - if firstNodeLine == -1 { - firstNodeLine = scanner.ComputeLineOfPosition(lineMap, astnav.GetStartOfNode(sourceFile.Statements.Nodes[0], sourceFile, false)) - } - commentEndLine := scanner.ComputeLineOfPosition(lineMap, r.End()) - if firstNodeLine < commentEndLine+2 { - break - } - } - lastComment = &r - pinnedOrTripleSlash = false - } - - if lastComment != nil { - position = lastComment.End() - advancePastLineBreak() - } - return position -} diff --git a/kitcom/internal/tsgo/ls/completions.go b/kitcom/internal/tsgo/ls/completions.go deleted file mode 100644 index 9700c4f..0000000 --- a/kitcom/internal/tsgo/ls/completions.go +++ /dev/null @@ -1,6171 +0,0 @@ -package ls - -import ( - "context" - "errors" - "fmt" - "maps" - "slices" - "strings" - "sync" - "unicode" - "unicode/utf8" - - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/binder" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/checker" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/compiler" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/debug" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/format" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/jsnum" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsutil" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/nodebuilder" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/printer" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/stringutil" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath" - "github.com/go-json-experiment/json" -) - -func (l *LanguageService) ProvideCompletion( - ctx context.Context, - documentURI lsproto.DocumentUri, - LSPPosition lsproto.Position, - context *lsproto.CompletionContext, - clientOptions *lsproto.CompletionClientCapabilities, - preferences *UserPreferences, -) (lsproto.CompletionResponse, error) { - _, file := l.getProgramAndFile(documentURI) - var triggerCharacter *string - if context != nil { - triggerCharacter = context.TriggerCharacter - } - position := int(l.converters.LineAndCharacterToPosition(file, LSPPosition)) - completionList := l.getCompletionsAtPosition( - ctx, - file, - position, - triggerCharacter, - preferences, - clientOptions, - ) - completionList = ensureItemData(file.FileName(), position, completionList) - return lsproto.CompletionItemsOrListOrNull{List: completionList}, nil -} - -func ensureItemData(fileName string, pos int, list *lsproto.CompletionList) *lsproto.CompletionList { - if list == nil { - return nil - } - for _, item := range list.Items { - if item.Data == nil { - var data any = &itemData{ - FileName: fileName, - Position: pos, - Name: item.Label, - } - item.Data = &data - } - } - return list -} - -// *completionDataData | *completionDataKeyword | *completionDataJSDocTagName | *completionDataJSDocTag | *completionDataJSDocParameterName -type completionData = any - -type completionDataData struct { - symbols []*ast.Symbol - completionKind CompletionKind - isInSnippetScope bool - // Note that the presence of this alone doesn't mean that we need a conversion. Only do that if the completion is not an ordinary identifier. - propertyAccessToConvert *ast.PropertyAccessExpressionNode - isNewIdentifierLocation bool - location *ast.Node - keywordFilters KeywordCompletionFilters - literals []literalValue - symbolToOriginInfoMap map[ast.SymbolId]*symbolOriginInfo - symbolToSortTextMap map[ast.SymbolId]sortText - recommendedCompletion *ast.Symbol - previousToken *ast.Node - contextToken *ast.Node - jsxInitializer jsxInitializer - insideJSDocTagTypeExpression bool - isTypeOnlyLocation bool - // In JSX tag name and attribute names, identifiers like "my-tag" or "aria-name" is valid identifier. - isJsxIdentifierExpected bool - isRightOfOpenTag bool - isRightOfDotOrQuestionDot bool - importStatementCompletion *importStatementCompletionInfo // !!! - hasUnresolvedAutoImports bool // !!! - // flags CompletionInfoFlags // !!! - defaultCommitCharacters []string -} - -type completionDataKeyword struct { - keywordCompletions []*lsproto.CompletionItem - isNewIdentifierLocation bool -} - -type completionDataJSDocTagName struct{} - -type completionDataJSDocTag struct{} - -type completionDataJSDocParameterName struct { - tag *ast.JSDocParameterTag -} - -type importStatementCompletionInfo struct { - isKeywordOnlyCompletion bool - keywordCompletion ast.Kind // TokenKind - isNewIdentifierLocation bool - isTopLevelTypeOnly bool - couldBeTypeOnlyImportSpecifier bool - replacementSpan *lsproto.Range -} - -// If we're after the `=` sign but no identifier has been typed yet, -// value will be `true` but initializer will be `nil`. -type jsxInitializer struct { - isInitializer bool - initializer *ast.IdentifierNode -} - -type KeywordCompletionFilters int - -const ( - KeywordCompletionFiltersNone KeywordCompletionFilters = iota // No keywords - KeywordCompletionFiltersAll // Every possible kewyord - KeywordCompletionFiltersClassElementKeywords // Keywords inside class body - KeywordCompletionFiltersInterfaceElementKeywords // Keywords inside interface body - KeywordCompletionFiltersConstructorParameterKeywords // Keywords at constructor parameter - KeywordCompletionFiltersFunctionLikeBodyKeywords // Keywords at function like body - KeywordCompletionFiltersTypeAssertionKeywords - KeywordCompletionFiltersTypeKeywords - KeywordCompletionFiltersTypeKeyword // Literally just `type` - KeywordCompletionFiltersLast = KeywordCompletionFiltersTypeKeyword -) - -func keywordFiltersFromSyntaxKind(keywordCompletion ast.Kind) KeywordCompletionFilters { - switch keywordCompletion { - case ast.KindTypeKeyword: - return KeywordCompletionFiltersTypeKeyword - default: - panic("Unknown mapping from ast.Kind `" + keywordCompletion.String() + "` to KeywordCompletionFilters") - } -} - -type CompletionKind int - -const ( - CompletionKindNone CompletionKind = iota - CompletionKindObjectPropertyDeclaration - CompletionKindGlobal - CompletionKindPropertyAccess - CompletionKindMemberLike - CompletionKindString -) - -var TriggerCharacters = []string{".", `"`, "'", "`", "/", "@", "<", "#", " "} - -// All commit characters, valid when `isNewIdentifierLocation` is false. -var allCommitCharacters = []string{".", ",", ";"} - -// Commit characters valid at expression positions where we could be inside a parameter list. -var noCommaCommitCharacters = []string{".", ";"} - -var emptyCommitCharacters = []string{} - -type sortText string - -const ( - SortTextLocalDeclarationPriority sortText = "10" - SortTextLocationPriority sortText = "11" - SortTextOptionalMember sortText = "12" - SortTextMemberDeclaredBySpreadAssignment sortText = "13" - SortTextSuggestedClassMembers sortText = "14" - SortTextGlobalsOrKeywords sortText = "15" - SortTextAutoImportSuggestions sortText = "16" - SortTextClassMemberSnippets sortText = "17" - SortTextJavascriptIdentifiers sortText = "18" -) - -func DeprecateSortText(original sortText) sortText { - return "z" + original -} - -func sortBelow(original sortText) sortText { - return original + "1" -} - -type symbolOriginInfoKind int - -const ( - symbolOriginInfoKindThisType symbolOriginInfoKind = 1 << iota - symbolOriginInfoKindSymbolMember - symbolOriginInfoKindExport - symbolOriginInfoKindPromise - symbolOriginInfoKindNullable - symbolOriginInfoKindTypeOnlyAlias - symbolOriginInfoKindObjectLiteralMethod - symbolOriginInfoKindIgnore - symbolOriginInfoKindComputedPropertyName - - symbolOriginInfoKindSymbolMemberNoExport symbolOriginInfoKind = symbolOriginInfoKindSymbolMember - symbolOriginInfoKindSymbolMemberExport = symbolOriginInfoKindSymbolMember | symbolOriginInfoKindExport -) - -type symbolOriginInfo struct { - kind symbolOriginInfoKind - isDefaultExport bool - isFromPackageJson bool - fileName string - data any -} - -func (origin *symbolOriginInfo) symbolName() string { - switch origin.data.(type) { - case *symbolOriginInfoExport: - return origin.data.(*symbolOriginInfoExport).symbolName - case *symbolOriginInfoComputedPropertyName: - return origin.data.(*symbolOriginInfoComputedPropertyName).symbolName - default: - panic(fmt.Sprintf("symbolOriginInfo: unknown data type for symbolName(): %T", origin.data)) - } -} - -func (origin *symbolOriginInfo) moduleSymbol() *ast.Symbol { - switch origin.data.(type) { - case *symbolOriginInfoExport: - return origin.data.(*symbolOriginInfoExport).moduleSymbol - default: - panic(fmt.Sprintf("symbolOriginInfo: unknown data type for moduleSymbol(): %T", origin.data)) - } -} - -func (origin *symbolOriginInfo) toCompletionEntryData() *completionEntryData { - debug.Assert(origin.kind&symbolOriginInfoKindExport != 0, fmt.Sprintf("completionEntryData is not generated for symbolOriginInfo of type %T", origin.data)) - var ambientModuleName *string - if origin.fileName == "" { - ambientModuleName = strPtrTo(stringutil.StripQuotes(origin.moduleSymbol().Name)) - } - var isPackageJsonImport core.Tristate - if origin.isFromPackageJson { - isPackageJsonImport = core.TSTrue - } - - data := origin.data.(*symbolOriginInfoExport) - return &completionEntryData{ - ExportName: data.exportName, - ExportMapKey: data.exportMapKey, - ModuleSpecifier: data.moduleSpecifier, - AmbientModuleName: ambientModuleName, - FileName: strPtrTo(origin.fileName), - IsPackageJsonImport: isPackageJsonImport, - } -} - -type symbolOriginInfoExport struct { - symbolName string - moduleSymbol *ast.Symbol - exportName string - exportMapKey ExportInfoMapKey - moduleSpecifier string -} - -func (s *symbolOriginInfo) asExport() *symbolOriginInfoExport { - return s.data.(*symbolOriginInfoExport) -} - -type symbolOriginInfoObjectLiteralMethod struct { - insertText string - labelDetails *lsproto.CompletionItemLabelDetails - isSnippet bool -} - -func (s *symbolOriginInfo) asObjectLiteralMethod() *symbolOriginInfoObjectLiteralMethod { - return s.data.(*symbolOriginInfoObjectLiteralMethod) -} - -type symbolOriginInfoTypeOnlyAlias struct { - declaration *ast.TypeOnlyImportDeclaration -} - -type symbolOriginInfoComputedPropertyName struct { - symbolName string -} - -// Special values for `CompletionInfo['source']` used to disambiguate -// completion items with the same `name`. (Each completion item must -// have a unique name/source combination, because those two fields -// comprise `CompletionEntryIdentifier` in `getCompletionEntryDetails`. -// -// When the completion item is an auto-import suggestion, the source -// is the module specifier of the suggestion. To avoid collisions, -// the values here should not be a module specifier we would ever -// generate for an auto-import. -type completionSource string - -const ( - // Completions that require `this.` insertion text. - completionSourceThisProperty completionSource = "ThisProperty/" - // Auto-import that comes attached to a class member snippet. - completionSourceClassMemberSnippet completionSource = "ClassMemberSnippet/" - // A type-only import that needs to be promoted in order to be used at the completion location. - completionSourceTypeOnlyAlias completionSource = "TypeOnlyAlias/" - // Auto-import that comes attached to an object literal method snippet. - completionSourceObjectLiteralMethodSnippet completionSource = "ObjectLiteralMethodSnippet/" - // Case completions for switch statements. - completionSourceSwitchCases completionSource = "SwitchCases/" - // Completions for an object literal expression. - completionSourceObjectLiteralMemberWithComma completionSource = "ObjectLiteralMemberWithComma/" -) - -// Value is set to false for global variables or completions from external module exports, -// true otherwise. -type uniqueNamesMap = map[string]bool - -// string | jsnum.Number | PseudoBigInt -type literalValue any - -type globalsSearch int - -const ( - globalsSearchContinue globalsSearch = iota - globalsSearchSuccess - globalsSearchFail -) - -func (l *LanguageService) getCompletionsAtPosition( - ctx context.Context, - file *ast.SourceFile, - position int, - triggerCharacter *string, - preferences *UserPreferences, - clientOptions *lsproto.CompletionClientCapabilities, -) *lsproto.CompletionList { - _, previousToken := getRelevantTokens(position, file) - if triggerCharacter != nil && !IsInString(file, position, previousToken) && !isValidTrigger(file, *triggerCharacter, previousToken, position) { - return nil - } - - if triggerCharacter != nil && *triggerCharacter == " " { - // `isValidTrigger` ensures we are at `import |` - if preferences.IncludeCompletionsForImportStatements.IsTrue() { - return &lsproto.CompletionList{ - IsIncomplete: true, - } - } - return nil - } - - compilerOptions := l.GetProgram().Options() - - // !!! see if incomplete completion list and continue or clean - - stringCompletions := l.getStringLiteralCompletions( - ctx, - file, - position, - previousToken, - compilerOptions, - preferences, - clientOptions, - ) - if stringCompletions != nil { - return stringCompletions - } - - if previousToken != nil && (previousToken.Kind == ast.KindBreakKeyword || - previousToken.Kind == ast.KindContinueKeyword || - previousToken.Kind == ast.KindIdentifier) && - ast.IsBreakOrContinueStatement(previousToken.Parent) { - return l.getLabelCompletionsAtPosition( - previousToken.Parent, - clientOptions, - file, - position, - l.getOptionalReplacementSpan(previousToken, file), - ) - } - - checker, done := l.GetProgram().GetTypeCheckerForFile(ctx, file) - defer done() - data := l.getCompletionData(ctx, checker, file, position, preferences) - if data == nil { - return nil - } - - switch data := data.(type) { - case *completionDataData: - optionalReplacementSpan := l.getOptionalReplacementSpan(data.location, file) - response := l.completionInfoFromData( - ctx, - checker, - file, - compilerOptions, - data, - preferences, - position, - clientOptions, - optionalReplacementSpan, - ) - // !!! check if response is incomplete - return response - case *completionDataKeyword: - optionalReplacementSpan := l.getOptionalReplacementSpan(previousToken, file) - return l.specificKeywordCompletionInfo( - clientOptions, - position, - file, - data.keywordCompletions, - data.isNewIdentifierLocation, - optionalReplacementSpan, - ) - case *completionDataJSDocTagName: - // If the current position is a jsDoc tag name, only tag names should be provided for completion - items := getJSDocTagNameCompletions() - items = append(items, getJSDocParameterCompletions( - clientOptions, - file, - position, - checker, - compilerOptions, - preferences, - /*tagNameOnly*/ true, - )...) - return l.jsDocCompletionInfo(clientOptions, position, file, items) - case *completionDataJSDocTag: - // If the current position is a jsDoc tag, only tags should be provided for completion - items := getJSDocTagCompletions() - items = append(items, getJSDocParameterCompletions( - clientOptions, - file, - position, - checker, - compilerOptions, - preferences, - /*tagNameOnly*/ false, - )...) - return l.jsDocCompletionInfo(clientOptions, position, file, items) - case *completionDataJSDocParameterName: - return l.jsDocCompletionInfo(clientOptions, position, file, getJSDocParameterNameCompletions(data.tag)) - default: - panic("getCompletionData() returned unexpected type: " + fmt.Sprintf("%T", data)) - } -} - -func (l *LanguageService) getCompletionData( - ctx context.Context, - typeChecker *checker.Checker, - file *ast.SourceFile, - position int, - preferences *UserPreferences, -) completionData { - inCheckedFile := isCheckedFile(file, l.GetProgram().Options()) - - currentToken := astnav.GetTokenAtPosition(file, position) - - insideComment := isInComment(file, position, currentToken) - - insideJSDocTagTypeExpression := false - insideJsDocImportTag := false - isInSnippetScope := false - if insideComment != nil { - if hasDocComment(file, position) { - if file.Text()[position] == '@' { - // The current position is next to the '@' sign, when no tag name being provided yet. - // Provide a full list of tag names - return &completionDataJSDocTagName{} - } else { - // When completion is requested without "@", we will have check to make sure that - // there are no comments prefix the request position. We will only allow "*" and space. - // e.g - // /** |c| /* - // - // /** - // |c| - // */ - // - // /** - // * |c| - // */ - // - // /** - // * |c| - // */ - lineStart := format.GetLineStartPositionForPosition(position, file) - noCommentPrefix := true - for _, r := range file.Text()[lineStart:position] { - if !(stringutil.IsWhiteSpaceSingleLine(r) || r == '*' || r == '/' || r == '(' || r == ')' || r == '|') { - noCommentPrefix = false - break - } - } - if noCommentPrefix { - return &completionDataJSDocTag{} - } - } - } - - // Completion should work inside certain JSDoc tags. For example: - // /** @type {number | string} */ - // Completion should work in the brackets - if tag := getJSDocTagAtPosition(currentToken, position); tag != nil { - if tag.TagName().Pos() <= position && position <= tag.TagName().End() { - return &completionDataJSDocTagName{} - } - if ast.IsJSDocImportTag(tag) { - insideJsDocImportTag = true - } else { - if typeExpression := tryGetTypeExpressionFromTag(tag); typeExpression != nil { - currentToken = astnav.GetTokenAtPosition(file, position) - if currentToken == nil || - (!ast.IsDeclarationName(currentToken) && - (currentToken.Parent.Kind != ast.KindJSDocPropertyTag || - currentToken.Parent.Name() != currentToken)) { - // Use as type location if inside tag's type expression - insideJSDocTagTypeExpression = isCurrentlyEditingNode(typeExpression, file, position) - } - } - if !insideJSDocTagTypeExpression && - ast.IsJSDocParameterTag(tag) && - (ast.NodeIsMissing(tag.Name()) || tag.Name().Pos() <= position && position <= tag.Name().End()) { - return &completionDataJSDocParameterName{ - tag: tag.AsJSDocParameterOrPropertyTag(), - } - } - } - } - - if !insideJSDocTagTypeExpression && !insideJsDocImportTag { - // Proceed if the current position is in JSDoc tag expression; otherwise it is a normal - // comment or the plain text part of a JSDoc comment, so no completion should be available - return nil - } - } - - // The decision to provide completion depends on the contextToken, which is determined through the previousToken. - // Note: 'previousToken' (and thus 'contextToken') can be undefined if we are the beginning of the file - isJSOnlyLocation := !insideJSDocTagTypeExpression && !insideJsDocImportTag && ast.IsSourceFileJS(file) - contextToken, previousToken := getRelevantTokens(position, file) - - // Find the node where completion is requested on. - // Also determine whether we are trying to complete with members of that node - // or attributes of a JSX tag. - node := currentToken - var propertyAccessToConvert *ast.PropertyAccessExpressionNode - isRightOfDot := false - isRightOfQuestionDot := false - isRightOfOpenTag := false - isStartingCloseTag := false - var jsxInitializer jsxInitializer - isJsxIdentifierExpected := false - var importStatementCompletion *importStatementCompletionInfo - location := astnav.GetTouchingPropertyName(file, position) - keywordFilters := KeywordCompletionFiltersNone - isNewIdentifierLocation := false - // !!! flags := CompletionInfoFlagsNone - var defaultCommitCharacters []string - - if contextToken != nil { - importStatementCompletionInfo := l.getImportStatementCompletionInfo(contextToken, file) - if importStatementCompletionInfo.keywordCompletion != ast.KindUnknown { - if importStatementCompletionInfo.isKeywordOnlyCompletion { - return &completionDataKeyword{ - keywordCompletions: []*lsproto.CompletionItem{{ - Label: scanner.TokenToString(importStatementCompletionInfo.keywordCompletion), - Kind: ptrTo(lsproto.CompletionItemKindKeyword), - SortText: ptrTo(string(SortTextGlobalsOrKeywords)), - }}, - isNewIdentifierLocation: importStatementCompletionInfo.isNewIdentifierLocation, - } - } - keywordFilters = keywordFiltersFromSyntaxKind(importStatementCompletionInfo.keywordCompletion) - } - if importStatementCompletionInfo.replacementSpan != nil && preferences.IncludeCompletionsForImportStatements.IsTrue() { - // !!! flags |= CompletionInfoFlags.IsImportStatementCompletion; - importStatementCompletion = &importStatementCompletionInfo - isNewIdentifierLocation = importStatementCompletionInfo.isNewIdentifierLocation - } - // Bail out if this is a known invalid completion location. - if isCompletionListBlocker(contextToken, previousToken, location, file, position, typeChecker) { - if keywordFilters != KeywordCompletionFiltersNone { - isNewIdentifierLocation, _ := computeCommitCharactersAndIsNewIdentifier(contextToken, file, position) - return keywordCompletionData(keywordFilters, isJSOnlyLocation, isNewIdentifierLocation) - } - return nil - } - - parent := contextToken.Parent - if contextToken.Kind == ast.KindDotToken || contextToken.Kind == ast.KindQuestionDotToken { - isRightOfDot = contextToken.Kind == ast.KindDotToken - isRightOfQuestionDot = contextToken.Kind == ast.KindQuestionDotToken - switch parent.Kind { - case ast.KindPropertyAccessExpression: - propertyAccessToConvert = parent - node = propertyAccessToConvert.Expression() - leftMostAccessExpression := ast.GetLeftmostAccessExpression(parent) - if ast.NodeIsMissing(leftMostAccessExpression) || - ((ast.IsCallExpression(node) || ast.IsFunctionLike(node)) && - node.End() == contextToken.Pos() && - lsutil.GetLastChild(node, file).Kind != ast.KindCloseParenToken) { - // This is likely dot from incorrectly parsed expression and user is starting to write spread - // eg: Math.min(./**/) - // const x = function (./**/) {} - // ({./**/}) - return nil - } - case ast.KindQualifiedName: - node = parent.AsQualifiedName().Left - case ast.KindModuleDeclaration: - node = parent.Name() - case ast.KindImportType: - node = parent - case ast.KindMetaProperty: - node = lsutil.GetFirstToken(parent, file) - if node.Kind != ast.KindImportKeyword && node.Kind != ast.KindNewKeyword { - panic("Unexpected token kind: " + node.Kind.String()) - } - default: - // There is nothing that precedes the dot, so this likely just a stray character - // or leading into a '...' token. Just bail out instead. - return nil - } - } else { // !!! else if (!importStatementCompletion) - // - // If the tagname is a property access expression, we will then walk up to the top most of property access expression. - // Then, try to get a JSX container and its associated attributes type. - if parent != nil && parent.Kind == ast.KindPropertyAccessExpression { - contextToken = parent - parent = parent.Parent - } - - // Fix location - if parent == location { - switch currentToken.Kind { - case ast.KindGreaterThanToken: - if parent.Kind == ast.KindJsxElement || parent.Kind == ast.KindJsxOpeningElement { - location = currentToken - } - case ast.KindLessThanSlashToken: - if parent.Kind == ast.KindJsxSelfClosingElement { - location = currentToken - } - } - } - - switch parent.Kind { - case ast.KindJsxClosingElement: - if contextToken.Kind == ast.KindLessThanSlashToken { - isStartingCloseTag = true - location = contextToken - } - case ast.KindBinaryExpression: - if !binaryExpressionMayBeOpenTag(parent.AsBinaryExpression()) { - break - } - fallthrough - case ast.KindJsxSelfClosingElement, ast.KindJsxElement, ast.KindJsxOpeningElement: - isJsxIdentifierExpected = true - if contextToken.Kind == ast.KindLessThanToken { - isRightOfOpenTag = true - location = contextToken - } - case ast.KindJsxExpression, ast.KindJsxSpreadAttribute: - // First case is for `
` or `
`, - // `parent` will be `{true}` and `previousToken` will be `}`. - // Second case is for `
`. - // Second case must not match for `
`. - if previousToken.Kind == ast.KindCloseBraceToken || - previousToken.Kind == ast.KindIdentifier && previousToken.Parent.Kind == ast.KindJsxAttribute { - isJsxIdentifierExpected = true - } - case ast.KindJsxAttribute: - // For `
`, `parent` will be JsxAttribute and `previousToken` will be its initializer. - if parent.Initializer() == previousToken && previousToken.End() < position { - isJsxIdentifierExpected = true - } else { - switch previousToken.Kind { - case ast.KindEqualsToken: - jsxInitializer.isInitializer = true - case ast.KindIdentifier: - isJsxIdentifierExpected = true - // For `
` we don't want to treat this as a jsx inializer, instead it's the attribute name. - if parent != previousToken.Parent && - parent.Initializer() == nil && - findChildOfKind(parent, ast.KindEqualsToken, file) != nil { - jsxInitializer.initializer = previousToken - } - } - } - } - } - } - - completionKind := CompletionKindNone - hasUnresolvedAutoImports := false - // This also gets mutated in nested-functions after the return - var symbols []*ast.Symbol - symbolToOriginInfoMap := map[ast.SymbolId]*symbolOriginInfo{} - symbolToSortTextMap := map[ast.SymbolId]sortText{} - var seenPropertySymbols collections.Set[ast.SymbolId] - importSpecifierResolver := &importSpecifierResolverForCompletions{SourceFile: file, UserPreferences: preferences, l: l} - isTypeOnlyLocation := insideJSDocTagTypeExpression || insideJsDocImportTag || - importStatementCompletion != nil && ast.IsTypeOnlyImportOrExportDeclaration(location.Parent) || - !isContextTokenValueLocation(contextToken) && - (isPossiblyTypeArgumentPosition(contextToken, file, typeChecker) || - ast.IsPartOfTypeNode(location) || - isContextTokenTypeLocation(contextToken)) - - addSymbolOriginInfo := func(symbol *ast.Symbol, insertQuestionDot bool, insertAwait bool) { - symbolId := ast.GetSymbolId(symbol) - if insertAwait && seenPropertySymbols.AddIfAbsent(symbolId) { - symbolToOriginInfoMap[symbolId] = &symbolOriginInfo{kind: getNullableSymbolOriginInfoKind(symbolOriginInfoKindPromise, insertQuestionDot)} - } else if insertQuestionDot { - symbolToOriginInfoMap[symbolId] = &symbolOriginInfo{kind: symbolOriginInfoKindNullable} - } - } - - addSymbolSortInfo := func(symbol *ast.Symbol) { - symbolId := ast.GetSymbolId(symbol) - if isStaticProperty(symbol) { - symbolToSortTextMap[symbolId] = SortTextLocalDeclarationPriority - } - } - - addPropertySymbol := func(symbol *ast.Symbol, insertAwait bool, insertQuestionDot bool) { - // For a computed property with an accessible name like `Symbol.iterator`, - // we'll add a completion for the *name* `Symbol` instead of for the property. - // If this is e.g. [Symbol.iterator], add a completion for `Symbol`. - computedPropertyName := core.FirstNonNil(symbol.Declarations, func(decl *ast.Node) *ast.Node { - name := ast.GetNameOfDeclaration(decl) - if name != nil && name.Kind == ast.KindComputedPropertyName { - return name - } - return nil - }) - - if computedPropertyName != nil { - leftMostName := getLeftMostName(computedPropertyName.Expression()) // The completion is for `Symbol`, not `iterator`. - var nameSymbol *ast.Symbol - if leftMostName != nil { - nameSymbol = typeChecker.GetSymbolAtLocation(leftMostName) - } - // If this is nested like for `namespace N { export const sym = Symbol(); }`, we'll add the completion for `N`. - var firstAccessibleSymbol *ast.Symbol - if nameSymbol != nil { - firstAccessibleSymbol = getFirstSymbolInChain(nameSymbol, contextToken, typeChecker) - } - var firstAccessibleSymbolId ast.SymbolId - if firstAccessibleSymbol != nil { - firstAccessibleSymbolId = ast.GetSymbolId(firstAccessibleSymbol) - } - if firstAccessibleSymbolId != 0 && seenPropertySymbols.AddIfAbsent(firstAccessibleSymbolId) { - symbols = append(symbols, firstAccessibleSymbol) - symbolToSortTextMap[firstAccessibleSymbolId] = SortTextGlobalsOrKeywords - moduleSymbol := firstAccessibleSymbol.Parent - if moduleSymbol == nil || - !checker.IsExternalModuleSymbol(moduleSymbol) || - typeChecker.TryGetMemberInModuleExportsAndProperties(firstAccessibleSymbol.Name, moduleSymbol) != firstAccessibleSymbol { - symbolToOriginInfoMap[firstAccessibleSymbolId] = &symbolOriginInfo{kind: getNullableSymbolOriginInfoKind(symbolOriginInfoKindSymbolMemberNoExport, insertQuestionDot)} - } else { - var fileName string - if tspath.IsExternalModuleNameRelative(stringutil.StripQuotes(moduleSymbol.Name)) { - fileName = ast.GetSourceFileOfModule(moduleSymbol).FileName() - } - result := importSpecifierResolver.getModuleSpecifierForBestExportInfo( - typeChecker, - []*SymbolExportInfo{{ - exportKind: ExportKindNamed, - moduleFileName: fileName, - isFromPackageJson: false, - moduleSymbol: moduleSymbol, - symbol: firstAccessibleSymbol, - targetFlags: typeChecker.SkipAlias(firstAccessibleSymbol).Flags, - }}, - position, - ast.IsValidTypeOnlyAliasUseSite(location), - ) - - if result != nil { - symbolToOriginInfoMap[ast.GetSymbolId(symbol)] = &symbolOriginInfo{ - kind: getNullableSymbolOriginInfoKind(symbolOriginInfoKindSymbolMemberExport, insertQuestionDot), - isDefaultExport: false, - fileName: fileName, - data: symbolOriginInfoExport{ - moduleSymbol: moduleSymbol, - symbolName: firstAccessibleSymbol.Name, - exportName: firstAccessibleSymbol.Name, - moduleSpecifier: result.moduleSpecifier, - }, - } - } - } - } else if firstAccessibleSymbolId == 0 || !seenPropertySymbols.Has(firstAccessibleSymbolId) { - symbols = append(symbols, symbol) - addSymbolOriginInfo(symbol, insertQuestionDot, insertAwait) - addSymbolSortInfo(symbol) - } - } else { - symbols = append(symbols, symbol) - addSymbolOriginInfo(symbol, insertQuestionDot, insertAwait) - addSymbolSortInfo(symbol) - } - } - - addTypeProperties := func(t *checker.Type, insertAwait bool, insertQuestionDot bool) { - if typeChecker.GetStringIndexType(t) != nil { - isNewIdentifierLocation = true - defaultCommitCharacters = []string{} - } - if isRightOfQuestionDot && len(typeChecker.GetCallSignatures(t)) != 0 { - isNewIdentifierLocation = true - if defaultCommitCharacters == nil { - defaultCommitCharacters = slices.Clone(allCommitCharacters) // Only invalid commit character here would be `(`. - } - } - - var propertyAccess *ast.Node - if node.Kind == ast.KindImportType { - propertyAccess = node - } else { - propertyAccess = node.Parent - } - - if inCheckedFile { - for _, symbol := range typeChecker.GetApparentProperties(t) { - if typeChecker.IsValidPropertyAccessForCompletions(propertyAccess, t, symbol) { - addPropertySymbol(symbol, false /*insertAwait*/, insertQuestionDot) - } - } - } else { - // In javascript files, for union types, we don't just get the members that - // the individual types have in common, we also include all the members that - // each individual type has. This is because we're going to add all identifiers - // anyways. So we might as well elevate the members that were at least part - // of the individual types to a higher status since we know what they are. - for _, symbol := range getPropertiesForCompletion(t, typeChecker) { - if typeChecker.IsValidPropertyAccessForCompletions(propertyAccess, t, symbol) { - symbols = append(symbols, symbol) - } - } - } - - if insertAwait { - promiseType := typeChecker.GetPromisedTypeOfPromise(t) - if promiseType != nil { - for _, symbol := range typeChecker.GetApparentProperties(promiseType) { - if typeChecker.IsValidPropertyAccessForCompletions(propertyAccess, promiseType, symbol) { - addPropertySymbol(symbol, true /*insertAwait*/, insertQuestionDot) - } - } - } - } - } - - getTypeScriptMemberSymbols := func() { - // Right of dot member completion list - completionKind = CompletionKindPropertyAccess - - // Since this is qualified name check it's a type node location - isImportType := ast.IsLiteralImportTypeNode(node) - isTypeLocation := (isImportType && !node.AsImportTypeNode().IsTypeOf) || - ast.IsPartOfTypeNode(node.Parent) || - isPossiblyTypeArgumentPosition(contextToken, file, typeChecker) - isRhsOfImportDeclaration := isInRightSideOfInternalImportEqualsDeclaration(node) - if ast.IsEntityName(node) || isImportType || ast.IsPropertyAccessExpression(node) { - isNamespaceName := ast.IsModuleDeclaration(node.Parent) - if isNamespaceName { - isNewIdentifierLocation = true - defaultCommitCharacters = []string{} - } - symbol := typeChecker.GetSymbolAtLocation(node) - if symbol != nil { - symbol := checker.SkipAlias(symbol, typeChecker) - if symbol.Flags&(ast.SymbolFlagsModule|ast.SymbolFlagsEnum) != 0 { - var valueAccessNode *ast.Node - if isImportType { - valueAccessNode = node - } else { - valueAccessNode = node.Parent - } - // Extract module or enum members - exportedSymbols := typeChecker.GetExportsOfModule(symbol) - for _, exportedSymbol := range exportedSymbols { - if exportedSymbol == nil { - panic("getExporsOfModule() should all be defined") - } - isValidValueAccess := func(s *ast.Symbol) bool { - return typeChecker.IsValidPropertyAccess(valueAccessNode, s.Name) - } - isValidTypeAccess := func(s *ast.Symbol) bool { - return symbolCanBeReferencedAtTypeLocation(s, typeChecker, collections.Set[ast.SymbolId]{}) - } - var isValidAccess bool - if isNamespaceName { - // At `namespace N.M/**/`, if this is the only declaration of `M`, don't include `M` as a completion. - isValidAccess = exportedSymbol.Flags&ast.SymbolFlagsNamespace != 0 && - !core.Every(exportedSymbol.Declarations, func(declaration *ast.Declaration) bool { - return declaration.Parent == node.Parent - }) - } else if isRhsOfImportDeclaration { - // Any kind is allowed when dotting off namespace in internal import equals declaration - isValidAccess = isValidTypeAccess(exportedSymbol) || isValidValueAccess(exportedSymbol) - } else if isTypeLocation || insideJSDocTagTypeExpression { - isValidAccess = isValidTypeAccess(exportedSymbol) - } else { - isValidAccess = isValidValueAccess(exportedSymbol) - } - if isValidAccess { - symbols = append(symbols, exportedSymbol) - } - } - - // If the module is merged with a value, we must get the type of the class and add its properties (for inherited static methods). - if !isTypeLocation && !insideJSDocTagTypeExpression && - core.Some( - symbol.Declarations, - func(decl *ast.Declaration) bool { - return decl.Kind != ast.KindSourceFile && decl.Kind != ast.KindModuleDeclaration && decl.Kind != ast.KindEnumDeclaration - }) { - t := typeChecker.GetNonOptionalType(typeChecker.GetTypeOfSymbolAtLocation(symbol, node)) - insertQuestionDot := false - if typeChecker.IsNullableType(t) { - canCorrectToQuestionDot := isRightOfDot && !isRightOfQuestionDot && - !preferences.IncludeAutomaticOptionalChainCompletions.IsFalse() - if canCorrectToQuestionDot || isRightOfQuestionDot { - t = typeChecker.GetNonNullableType(t) - if canCorrectToQuestionDot { - insertQuestionDot = true - } - } - } - addTypeProperties(t, node.Flags&ast.NodeFlagsAwaitContext != 0, insertQuestionDot) - } - - return - } - } - } - - if !isTypeLocation || checker.IsInTypeQuery(node) { - // microsoft/TypeScript#39946. Pulling on the type of a node inside of a function with a contextual `this` parameter can result in a circularity - // if the `node` is part of the exprssion of a `yield` or `return`. This circularity doesn't exist at compile time because - // we will check (and cache) the type of `this` *before* checking the type of the node. - typeChecker.TryGetThisTypeAtEx(node, false /*includeGlobalThis*/, nil) - t := typeChecker.GetNonOptionalType(typeChecker.GetTypeAtLocation(node)) - - if !isTypeLocation { - insertQuestionDot := false - if typeChecker.IsNullableType(t) { - canCorrectToQuestionDot := isRightOfDot && !isRightOfQuestionDot && - !preferences.IncludeAutomaticOptionalChainCompletions.IsFalse() - - if canCorrectToQuestionDot || isRightOfQuestionDot { - t = typeChecker.GetNonNullableType(t) - if canCorrectToQuestionDot { - insertQuestionDot = true - } - } - } - addTypeProperties(t, node.Flags&ast.NodeFlagsAwaitContext != 0, insertQuestionDot) - } else { - addTypeProperties(typeChecker.GetNonNullableType(t), false /*insertAwait*/, false /*insertQuestionDot*/) - } - } - } - - // Aggregates relevant symbols for completion in object literals in type argument positions. - tryGetObjectTypeLiteralInTypeArgumentCompletionSymbols := func() globalsSearch { - typeLiteralNode := tryGetTypeLiteralNode(contextToken) - if typeLiteralNode == nil { - return globalsSearchContinue - } - - intersectionTypeNode := core.IfElse( - ast.IsIntersectionTypeNode(typeLiteralNode.Parent), - typeLiteralNode.Parent, - nil) - containerTypeNode := core.IfElse( - intersectionTypeNode != nil, - intersectionTypeNode, - typeLiteralNode) - - containerExpectedType := getConstraintOfTypeArgumentProperty(containerTypeNode, typeChecker) - if containerExpectedType == nil { - return globalsSearchContinue - } - - containerActualType := typeChecker.GetTypeFromTypeNode(containerTypeNode) - - members := getPropertiesForCompletion(containerExpectedType, typeChecker) - existingMembers := getPropertiesForCompletion(containerActualType, typeChecker) - - existingMemberNames := collections.Set[string]{} - for _, member := range existingMembers { - existingMemberNames.Add(member.Name) - } - - symbols = append( - symbols, - core.Filter(members, func(member *ast.Symbol) bool { return !existingMemberNames.Has(member.Name) })...) - - completionKind = CompletionKindObjectPropertyDeclaration - isNewIdentifierLocation = true - - return globalsSearchSuccess - } - - // Aggregates relevant symbols for completion in object literals and object binding patterns. - // Relevant symbols are stored in the captured 'symbols' variable. - tryGetObjectLikeCompletionSymbols := func() globalsSearch { - if contextToken != nil && contextToken.Kind == ast.KindDotDotDotToken { - return globalsSearchContinue - } - objectLikeContainer := tryGetObjectLikeCompletionContainer(contextToken, position, file) - if objectLikeContainer == nil { - return globalsSearchContinue - } - - // We're looking up possible property names from contextual/inferred/declared type. - completionKind = CompletionKindObjectPropertyDeclaration - - var typeMembers []*ast.Symbol - var existingMembers []*ast.Declaration - - if objectLikeContainer.Kind == ast.KindObjectLiteralExpression { - instantiatedType := tryGetObjectLiteralContextualType(objectLikeContainer, typeChecker) - - // Check completions for Object property value shorthand - if instantiatedType == nil { - if objectLikeContainer.Flags&ast.NodeFlagsInWithStatement != 0 { - return globalsSearchFail - } - return globalsSearchContinue - } - completionsType := typeChecker.GetContextualType(objectLikeContainer, checker.ContextFlagsCompletions) - t := core.IfElse(completionsType != nil, completionsType, instantiatedType) - stringIndexType := typeChecker.GetStringIndexType(t) - numberIndexType := typeChecker.GetNumberIndexType(t) - isNewIdentifierLocation = stringIndexType != nil || numberIndexType != nil - typeMembers = getPropertiesForObjectExpression(instantiatedType, completionsType, objectLikeContainer, typeChecker) - properties := objectLikeContainer.AsObjectLiteralExpression().Properties - if properties != nil { - existingMembers = properties.Nodes - } - - if len(typeMembers) == 0 { - // Edge case: If NumberIndexType exists - if numberIndexType == nil { - return globalsSearchContinue - } - } - } else { - if objectLikeContainer.Kind != ast.KindObjectBindingPattern { - panic("Expected 'objectLikeContainer' to be an object binding pattern.") - } - // We are *only* completing on properties from the type being destructured. - isNewIdentifierLocation = false - rootDeclaration := ast.GetRootDeclaration(objectLikeContainer.Parent) - if !ast.IsVariableLike(rootDeclaration) { - panic("Root declaration is not variable-like.") - } - - // We don't want to complete using the type acquired by the shape - // of the binding pattern; we are only interested in types acquired - // through type declaration or inference. - // Also proceed if rootDeclaration is a parameter and if its containing function expression/arrow function is contextually typed - - // type of parameter will flow in from the contextual type of the function. - canGetType := ast.HasInitializer(rootDeclaration) || - ast.GetTypeAnnotationNode(rootDeclaration) != nil || - rootDeclaration.Parent.Parent.Kind == ast.KindForOfStatement - if !canGetType && rootDeclaration.Kind == ast.KindParameter { - if ast.IsExpression(rootDeclaration.Parent) { - canGetType = typeChecker.GetContextualType(rootDeclaration.Parent, checker.ContextFlagsNone) != nil - } else if rootDeclaration.Parent.Kind == ast.KindMethodDeclaration || - rootDeclaration.Parent.Kind == ast.KindSetAccessor { - canGetType = ast.IsExpression(rootDeclaration.Parent.Parent) && - typeChecker.GetContextualType(rootDeclaration.Parent.Parent, checker.ContextFlagsNone) != nil - } - } - if canGetType { - typeForObject := typeChecker.GetTypeAtLocation(objectLikeContainer) - if typeForObject == nil { - return globalsSearchFail - } - typeMembers = core.Filter( - typeChecker.GetPropertiesOfType(typeForObject), - func(propertySymbol *ast.Symbol) bool { - return typeChecker.IsPropertyAccessible( - objectLikeContainer, - false, /*isSuper*/ - false, /*isWrite*/ - typeForObject, - propertySymbol, - ) - }, - ) - elements := objectLikeContainer.AsBindingPattern().Elements - if elements != nil { - existingMembers = elements.Nodes - } - } - } - - if len(typeMembers) > 0 { - // Add filtered items to the completion list. - filteredMembers, spreadMemberNames := filterObjectMembersList( - typeMembers, - core.CheckEachDefined(existingMembers, "object like properties or elements should all be defined"), - file, - position, - typeChecker, - ) - symbols = append(symbols, filteredMembers...) - - // Set sort texts. - transformObjectLiteralMembers := preferences.IncludeCompletionsWithObjectLiteralMethodSnippets.IsTrue() && - objectLikeContainer.Kind == ast.KindObjectLiteralExpression - for _, member := range filteredMembers { - symbolId := ast.GetSymbolId(member) - if spreadMemberNames.Has(member.Name) { - symbolToSortTextMap[symbolId] = SortTextMemberDeclaredBySpreadAssignment - } - if member.Flags&ast.SymbolFlagsOptional != 0 { - _, ok := symbolToSortTextMap[symbolId] - if !ok { - symbolToSortTextMap[symbolId] = SortTextOptionalMember - } - } - if transformObjectLiteralMembers { - // !!! object literal member snippet completions - } - } - } - - return globalsSearchSuccess - } - - shouldOfferImportCompletions := func() bool { - // If already typing an import statement, provide completions for it. - if importStatementCompletion != nil { - return true - } - // If not already a module, must have modules enabled. - if !preferences.IncludeCompletionsForModuleExports.IsTrue() { - return false - } - // Always using ES modules in 6.0+ - return true - } - - // Mutates `symbols`, `symbolToOriginInfoMap`, and `symbolToSortTextMap` - collectAutoImports := func() { - if !shouldOfferImportCompletions() { - return - } - // !!! CompletionInfoFlags - - // import { type | -> token text should be blank - var lowerCaseTokenText string - if previousToken != nil && ast.IsIdentifier(previousToken) && !(previousToken == contextToken && importStatementCompletion != nil) { - lowerCaseTokenText = strings.ToLower(previousToken.Text()) - } - - // !!! timestamp - // Under `--moduleResolution nodenext` or `bundler`, we have to resolve module specifiers up front, because - // package.json exports can mean we *can't* resolve a module specifier (that doesn't include a - // relative path into node_modules), and we want to filter those completions out entirely. - // Import statement completions always need specifier resolution because the module specifier is - // part of their `insertText`, not the `codeActions` creating edits away from the cursor. - // Finally, `autoImportSpecifierExcludeRegexes` necessitates eagerly resolving module specifiers - // because completion items are being explcitly filtered out by module specifier. - isValidTypeOnlyUseSite := ast.IsValidTypeOnlyAliasUseSite(location) - - // !!! moduleSpecifierCache := host.getModuleSpecifierCache(); - // !!! packageJsonAutoImportProvider := host.getPackageJsonAutoImportProvider(); - addSymbolToList := func(info []*SymbolExportInfo, symbolName string, isFromAmbientModule bool, exportMapKey ExportInfoMapKey) []*SymbolExportInfo { - // Do a relatively cheap check to bail early if all re-exports are non-importable - // due to file location or package.json dependency filtering. For non-node16+ - // module resolution modes, getting past this point guarantees that we'll be - // able to generate a suitable module specifier, so we can safely show a completion, - // even if we defer computing the module specifier. - info = core.Filter(info, func(i *SymbolExportInfo) bool { - var toFile *ast.SourceFile - if ast.IsSourceFile(i.moduleSymbol.ValueDeclaration) { - toFile = i.moduleSymbol.ValueDeclaration.AsSourceFile() - } - return l.isImportable( - file, - toFile, - i.moduleSymbol, - preferences, - importSpecifierResolver.packageJsonImportFilter(), - ) - }) - if len(info) == 0 { - return nil - } - - // In node16+, module specifier resolution can fail due to modules being blocked - // by package.json `exports`. If that happens, don't show a completion item. - // N.B. We always try to resolve module specifiers here, because we have to know - // now if it's going to fail so we can omit the completion from the list. - result := importSpecifierResolver.getModuleSpecifierForBestExportInfo(typeChecker, info, position, isValidTypeOnlyUseSite) - if result == nil { - return nil - } - - // If we skipped resolving module specifiers, our selection of which ExportInfo - // to use here is arbitrary, since the info shown in the completion list derived from - // it should be identical regardless of which one is used. During the subsequent - // `CompletionEntryDetails` request, we'll get all the ExportInfos again and pick - // the best one based on the module specifier it produces. - moduleSpecifier := result.moduleSpecifier - exportInfo := info[0] - if result.exportInfo != nil { - exportInfo = result.exportInfo - } - - isDefaultExport := exportInfo.exportKind == ExportKindDefault - if exportInfo.symbol == nil { - panic("should have handled `futureExportSymbolInfo` earlier") - } - symbol := exportInfo.symbol - if isDefaultExport { - if defaultSymbol := binder.GetLocalSymbolForExportDefault(symbol); defaultSymbol != nil { - symbol = defaultSymbol - } - } - - // pushAutoImportSymbol - symbolId := ast.GetSymbolId(symbol) - if symbolToSortTextMap[symbolId] == SortTextGlobalsOrKeywords { - // If an auto-importable symbol is available as a global, don't push the auto import - return nil - } - originInfo := &symbolOriginInfo{ - kind: symbolOriginInfoKindExport, - isDefaultExport: isDefaultExport, - isFromPackageJson: exportInfo.isFromPackageJson, - fileName: exportInfo.moduleFileName, - data: &symbolOriginInfoExport{ - symbolName: symbolName, - moduleSymbol: exportInfo.moduleSymbol, - exportName: core.IfElse(exportInfo.exportKind == ExportKindExportEquals, ast.InternalSymbolNameExportEquals, exportInfo.symbol.Name), - exportMapKey: exportMapKey, - moduleSpecifier: moduleSpecifier, - }, - } - symbolToOriginInfoMap[symbolId] = originInfo - symbolToSortTextMap[symbolId] = core.IfElse(importStatementCompletion != nil, SortTextLocationPriority, SortTextAutoImportSuggestions) - symbols = append(symbols, symbol) - return nil - } - l.searchExportInfosForCompletions(ctx, - typeChecker, - file, - preferences, - importStatementCompletion != nil, - isRightOfOpenTag, - isTypeOnlyLocation, - lowerCaseTokenText, - addSymbolToList, - ) - - // !!! completionInfoFlags - // !!! logging - } - - tryGetImportCompletionSymbols := func() globalsSearch { - if importStatementCompletion == nil { - return globalsSearchContinue - } - isNewIdentifierLocation = true - collectAutoImports() - return globalsSearchSuccess - } - - // Aggregates relevant symbols for completion in import clauses and export clauses - // whose declarations have a module specifier; for instance, symbols will be aggregated for - // - // import { | } from "moduleName"; - // export { a as foo, | } from "moduleName"; - // - // but not for - // - // export { | }; - // - // Relevant symbols are stored in the captured 'symbols' variable. - tryGetImportOrExportClauseCompletionSymbols := func() globalsSearch { - if contextToken == nil { - return globalsSearchContinue - } - - // `import { |` or `import { a as 0, | }` or `import { type | }` - var namedImportsOrExports *ast.NamedImportsOrExports - if contextToken.Kind == ast.KindOpenBraceToken || contextToken.Kind == ast.KindCommaToken { - namedImportsOrExports = core.IfElse(isNamedImportsOrExports(contextToken.Parent), contextToken.Parent, nil) - } else if isTypeKeywordTokenOrIdentifier(contextToken) { - namedImportsOrExports = core.IfElse( - isNamedImportsOrExports(contextToken.Parent.Parent), - contextToken.Parent.Parent, - nil, - ) - } - - if namedImportsOrExports == nil { - return globalsSearchContinue - } - - // We can at least offer `type` at `import { |` - if !isTypeKeywordTokenOrIdentifier(contextToken) { - keywordFilters = KeywordCompletionFiltersTypeKeyword - } - - // try to show exported member for imported/re-exported module - moduleSpecifier := core.IfElse( - namedImportsOrExports.Kind == ast.KindNamedImports, - namedImportsOrExports.Parent.Parent, - namedImportsOrExports.Parent).ModuleSpecifier() - if moduleSpecifier == nil { - isNewIdentifierLocation = true - if namedImportsOrExports.Kind == ast.KindNamedImports { - return globalsSearchFail - } - return globalsSearchContinue - } - - moduleSpecifierSymbol := typeChecker.GetSymbolAtLocation(moduleSpecifier) - if moduleSpecifierSymbol == nil { - isNewIdentifierLocation = true - return globalsSearchFail - } - - completionKind = CompletionKindMemberLike - isNewIdentifierLocation = false - exports := typeChecker.GetExportsAndPropertiesOfModule(moduleSpecifierSymbol) - - existing := collections.Set[string]{} - for _, element := range namedImportsOrExports.Elements() { - if isCurrentlyEditingNode(element, file, position) { - continue - } - existing.Add(element.PropertyNameOrName().Text()) - } - uniques := core.Filter(exports, func(symbol *ast.Symbol) bool { - return ast.SymbolName(symbol) != ast.InternalSymbolNameDefault && !existing.Has(ast.SymbolName(symbol)) - }) - - symbols = append(symbols, uniques...) - if len(uniques) == 0 { - // If there's nothing else to import, don't offer `type` either. - keywordFilters = KeywordCompletionFiltersNone - } - return globalsSearchSuccess - } - - // import { x } from "foo" with { | } - tryGetImportAttributesCompletionSymbols := func() globalsSearch { - if contextToken == nil { - return globalsSearchContinue - } - - var importAttributes *ast.ImportAttributesNode - switch contextToken.Kind { - case ast.KindOpenBraceToken, ast.KindCommaToken: - importAttributes = core.IfElse(ast.IsImportAttributes(contextToken.Parent), contextToken.Parent, nil) - case ast.KindColonToken: - importAttributes = core.IfElse(ast.IsImportAttributes(contextToken.Parent.Parent), contextToken.Parent.Parent, nil) - } - - if importAttributes == nil { - return globalsSearchContinue - } - - var elements []*ast.Node - if importAttributes.AsImportAttributes().Attributes != nil { - elements = importAttributes.AsImportAttributes().Attributes.Nodes - } - existing := collections.NewSetFromItems(core.Map(elements, (*ast.Node).Text)...) - uniques := core.Filter( - typeChecker.GetApparentProperties(typeChecker.GetTypeAtLocation(importAttributes)), - func(symbol *ast.Symbol) bool { - return !existing.Has(ast.SymbolName(symbol)) - }) - symbols = append(symbols, uniques...) - return globalsSearchSuccess - } - - // Adds local declarations for completions in named exports: - // export { | }; - // Does not check for the absence of a module specifier (`export {} from "./other"`) - // because `tryGetImportOrExportClauseCompletionSymbols` runs first and handles that, - // preventing this function from running. - tryGetLocalNamedExportCompletionSymbols := func() globalsSearch { - if contextToken == nil { - return globalsSearchContinue - } - var namedExports *ast.NamedExportsNode - if contextToken.Kind == ast.KindOpenBraceToken || contextToken.Kind == ast.KindCommaToken { - namedExports = core.IfElse(ast.IsNamedExports(contextToken.Parent), contextToken.Parent, nil) - } - - if namedExports == nil { - return globalsSearchContinue - } - - localsContainer := ast.FindAncestor(namedExports, func(node *ast.Node) bool { - return ast.IsSourceFile(node) || ast.IsModuleDeclaration(node) - }) - completionKind = CompletionKindNone - isNewIdentifierLocation = false - localSymbol := localsContainer.Symbol() - var localExports ast.SymbolTable - if localSymbol != nil { - localExports = localSymbol.Exports - } - for name, symbol := range localsContainer.Locals() { - symbols = append(symbols, symbol) - if _, ok := localExports[name]; ok { - symbolId := ast.GetSymbolId(symbol) - symbolToSortTextMap[symbolId] = SortTextOptionalMember - } - } - - return globalsSearchSuccess - } - - tryGetConstructorCompletion := func() globalsSearch { - if tryGetConstructorLikeCompletionContainer(contextToken) == nil { - return globalsSearchContinue - } - - // no members, only keywords - completionKind = CompletionKindNone - // Declaring new property/method/accessor - isNewIdentifierLocation = true - // Has keywords for constructor parameter - keywordFilters = KeywordCompletionFiltersConstructorParameterKeywords - return globalsSearchSuccess - } - - // Aggregates relevant symbols for completion in class declaration - // Relevant symbols are stored in the captured 'symbols' variable. - tryGetClassLikeCompletionSymbols := func() globalsSearch { - decl := tryGetObjectTypeDeclarationCompletionContainer(file, contextToken, location, position) - if decl == nil { - return globalsSearchContinue - } - - // We're looking up possible property names from parent type. - completionKind = CompletionKindMemberLike - // Declaring new property/method/accessor - isNewIdentifierLocation = true - if contextToken.Kind == ast.KindAsteriskToken { - keywordFilters = KeywordCompletionFiltersNone - } else if ast.IsClassLike(decl) { - keywordFilters = KeywordCompletionFiltersClassElementKeywords - } else { - keywordFilters = KeywordCompletionFiltersInterfaceElementKeywords - } - - // If you're in an interface you don't want to repeat things from super-interface. So just stop here. - if !ast.IsClassLike(decl) { - return globalsSearchSuccess - } - - var classElement *ast.Node - if contextToken.Kind == ast.KindSemicolonToken { - classElement = contextToken.Parent.Parent - } else { - classElement = contextToken.Parent - } - var classElementModifierFlags ast.ModifierFlags - if ast.IsClassElement(classElement) { - classElementModifierFlags = classElement.ModifierFlags() - } - // If this is context token is not something we are editing now, consider if this would lead to be modifier. - if contextToken.Kind == ast.KindIdentifier && !isCurrentlyEditingNode(contextToken, file, position) { - switch contextToken.Text() { - case "private": - classElementModifierFlags |= ast.ModifierFlagsPrivate - case "static": - classElementModifierFlags |= ast.ModifierFlagsStatic - case "override": - classElementModifierFlags |= ast.ModifierFlagsOverride - } - } - if ast.IsClassStaticBlockDeclaration(classElement) { - classElementModifierFlags |= ast.ModifierFlagsStatic - } - - // No member list for private methods - if classElementModifierFlags&ast.ModifierFlagsPrivate == 0 { - // List of property symbols of base type that are not private and already implemented - var baseTypeNodes []*ast.Node - if ast.IsClassLike(decl) && classElementModifierFlags&ast.ModifierFlagsOverride != 0 { - baseTypeNodes = core.SingleElementSlice(ast.GetClassExtendsHeritageElement(decl)) - } else { - baseTypeNodes = getAllSuperTypeNodes(decl) - } - var baseSymbols []*ast.Symbol - for _, baseTypeNode := range baseTypeNodes { - t := typeChecker.GetTypeAtLocation(baseTypeNode) - if classElementModifierFlags&ast.ModifierFlagsStatic != 0 { - if t.Symbol() != nil { - baseSymbols = append( - baseSymbols, - typeChecker.GetPropertiesOfType(typeChecker.GetTypeOfSymbolAtLocation(t.Symbol(), decl))...) - } - } else if t != nil { - baseSymbols = append(baseSymbols, typeChecker.GetPropertiesOfType(t)...) - } - } - - symbols = append(symbols, - filterClassMembersList(baseSymbols, decl.Members(), classElementModifierFlags, file, position)...) - for _, symbol := range symbols { - declaration := symbol.ValueDeclaration - if declaration != nil && ast.IsClassElement(declaration) && - declaration.Name() != nil && - ast.IsComputedPropertyName(declaration.Name()) { - symbolId := ast.GetSymbolId(symbol) - origin := &symbolOriginInfo{ - kind: symbolOriginInfoKindComputedPropertyName, - data: &symbolOriginInfoComputedPropertyName{symbolName: typeChecker.SymbolToString(symbol)}, - } - symbolToOriginInfoMap[symbolId] = origin - } - } - } - - return globalsSearchSuccess - } - - tryGetJsxCompletionSymbols := func() globalsSearch { - jsxContainer := tryGetContainingJsxElement(contextToken, file) - if jsxContainer == nil { - return globalsSearchContinue - } - // Cursor is inside a JSX self-closing element or opening element. - attrsType := typeChecker.GetContextualType(jsxContainer.Attributes(), checker.ContextFlagsNone) - if attrsType == nil { - return globalsSearchContinue - } - completionsType := typeChecker.GetContextualType(jsxContainer.Attributes(), checker.ContextFlagsCompletions) - filteredSymbols, spreadMemberNames := filterJsxAttributes( - getPropertiesForObjectExpression(attrsType, completionsType, jsxContainer.Attributes(), typeChecker), - jsxContainer.Attributes().Properties(), - file, - position, - typeChecker, - ) - - symbols = append(symbols, filteredSymbols...) - // Set sort texts. - for _, symbol := range filteredSymbols { - symbolId := ast.GetSymbolId(symbol) - if spreadMemberNames.Has(ast.SymbolName(symbol)) { - symbolToSortTextMap[symbolId] = SortTextMemberDeclaredBySpreadAssignment - } - if symbol.Flags&ast.SymbolFlagsOptional != 0 { - _, ok := symbolToSortTextMap[symbolId] - if !ok { - symbolToSortTextMap[symbolId] = SortTextOptionalMember - } - } - } - - completionKind = CompletionKindMemberLike - isNewIdentifierLocation = false - return globalsSearchSuccess - } - - getGlobalCompletions := func() globalsSearch { - if tryGetFunctionLikeBodyCompletionContainer(contextToken) != nil { - keywordFilters = KeywordCompletionFiltersFunctionLikeBodyKeywords - } else { - keywordFilters = KeywordCompletionFiltersAll - } - // Get all entities in the current scope. - completionKind = CompletionKindGlobal - isNewIdentifierLocation, defaultCommitCharacters = computeCommitCharactersAndIsNewIdentifier(contextToken, file, position) - - if previousToken != contextToken { - if previousToken == nil { - panic("Expected 'contextToken' to be defined when different from 'previousToken'.") - } - } - - // We need to find the node that will give us an appropriate scope to begin - // aggregating completion candidates. This is achieved in 'getScopeNode' - // by finding the first node that encompasses a position, accounting for whether a node - // is "complete" to decide whether a position belongs to the node. - // - // However, at the end of an identifier, we are interested in the scope of the identifier - // itself, but fall outside of the identifier. For instance: - // - // xyz => x$ - // - // the cursor is outside of both the 'x' and the arrow function 'xyz => x', - // so 'xyz' is not returned in our results. - // - // We define 'adjustedPosition' so that we may appropriately account for - // being at the end of an identifier. The intention is that if requesting completion - // at the end of an identifier, it should be effectively equivalent to requesting completion - // anywhere inside/at the beginning of the identifier. So in the previous case, the - // 'adjustedPosition' will work as if requesting completion in the following: - // - // xyz => $x - // - // If previousToken !== contextToken, then - // - 'contextToken' was adjusted to the token prior to 'previousToken' - // because we were at the end of an identifier. - // - 'previousToken' is defined. - var adjustedPosition int - if previousToken != contextToken { - adjustedPosition = astnav.GetStartOfNode(previousToken, file, false /*includeJSDoc*/) - } else { - adjustedPosition = position - } - - scopeNode := getScopeNode(contextToken, adjustedPosition, file) - if scopeNode == nil { - scopeNode = file.AsNode() - } - isInSnippetScope = isSnippetScope(scopeNode) - - symbolMeanings := core.IfElse(isTypeOnlyLocation, ast.SymbolFlagsNone, ast.SymbolFlagsValue) | - ast.SymbolFlagsType | ast.SymbolFlagsNamespace | ast.SymbolFlagsAlias - typeOnlyAliasNeedsPromotion := previousToken != nil && !ast.IsValidTypeOnlyAliasUseSite(previousToken) - - symbols = append(symbols, typeChecker.GetSymbolsInScope(scopeNode, symbolMeanings)...) - core.CheckEachDefined(symbols, "getSymbolsInScope() should all be defined") - for _, symbol := range symbols { - symbolId := ast.GetSymbolId(symbol) - if !typeChecker.IsArgumentsSymbol(symbol) && - !core.Some(symbol.Declarations, func(decl *ast.Declaration) bool { - return ast.GetSourceFileOfNode(decl) == file - }) { - symbolToSortTextMap[symbolId] = SortTextGlobalsOrKeywords - } - if typeOnlyAliasNeedsPromotion && symbol.Flags&ast.SymbolFlagsValue == 0 { - typeOnlyAliasDeclaration := core.Find(symbol.Declarations, ast.IsTypeOnlyImportDeclaration) - if typeOnlyAliasDeclaration != nil { - origin := &symbolOriginInfo{ - kind: symbolOriginInfoKindTypeOnlyAlias, - data: &symbolOriginInfoTypeOnlyAlias{declaration: typeOnlyAliasDeclaration}, - } - symbolToOriginInfoMap[symbolId] = origin - } - } - } - - // Need to insert 'this.' before properties of `this` type. - if scopeNode.Kind != ast.KindSourceFile { - thisType := typeChecker.TryGetThisTypeAtEx( - scopeNode, - false, /*includeGlobalThis*/ - core.IfElse(ast.IsClassLike(scopeNode.Parent), scopeNode, nil)) - if thisType != nil && !isProbablyGlobalType(thisType, file, typeChecker) { - for _, symbol := range getPropertiesForCompletion(thisType, typeChecker) { - symbolId := ast.GetSymbolId(symbol) - symbols = append(symbols, symbol) - symbolToOriginInfoMap[symbolId] = &symbolOriginInfo{kind: symbolOriginInfoKindThisType} - symbolToSortTextMap[symbolId] = SortTextSuggestedClassMembers - } - } - } - - collectAutoImports() - if isTypeOnlyLocation { - if contextToken != nil && ast.IsAssertionExpression(contextToken.Parent) { - keywordFilters = KeywordCompletionFiltersTypeAssertionKeywords - } else { - keywordFilters = KeywordCompletionFiltersTypeKeywords - } - } - - return globalsSearchSuccess - } - - tryGetGlobalSymbols := func() bool { - var result globalsSearch - globalSearchFuncs := []func() globalsSearch{ - tryGetObjectTypeLiteralInTypeArgumentCompletionSymbols, - tryGetObjectLikeCompletionSymbols, - tryGetImportCompletionSymbols, - tryGetImportOrExportClauseCompletionSymbols, - tryGetImportAttributesCompletionSymbols, - tryGetLocalNamedExportCompletionSymbols, - tryGetConstructorCompletion, - tryGetClassLikeCompletionSymbols, - tryGetJsxCompletionSymbols, - getGlobalCompletions, - } - for _, globalSearchFunc := range globalSearchFuncs { - result = globalSearchFunc() - if result != globalsSearchContinue { - break - } - } - return result == globalsSearchSuccess - } - - if isRightOfDot || isRightOfQuestionDot { - getTypeScriptMemberSymbols() - } else if isRightOfOpenTag { - symbols = typeChecker.GetJsxIntrinsicTagNamesAt(location) - core.CheckEachDefined(symbols, "GetJsxIntrinsicTagNamesAt() should all be defined") - tryGetGlobalSymbols() - completionKind = CompletionKindGlobal - keywordFilters = KeywordCompletionFiltersNone - } else if isStartingCloseTag { - tagName := contextToken.Parent.Parent.AsJsxElement().OpeningElement.TagName() - tagSymbol := typeChecker.GetSymbolAtLocation(tagName) - if tagSymbol != nil { - symbols = []*ast.Symbol{tagSymbol} - } - completionKind = CompletionKindGlobal - keywordFilters = KeywordCompletionFiltersNone - } else { - // For JavaScript or TypeScript, if we're not after a dot, then just try to get the - // global symbols in scope. These results should be valid for either language as - // the set of symbols that can be referenced from this location. - if !tryGetGlobalSymbols() { - if keywordFilters != KeywordCompletionFiltersNone { - return keywordCompletionData(keywordFilters, isJSOnlyLocation, isNewIdentifierLocation) - } - return nil - } - } - - var contextualType *checker.Type - if previousToken != nil { - contextualType = getContextualType(previousToken, position, file, typeChecker) - } - - // exclude literal suggestions after microsoft/TypeScript#51667) and after closing quote (microsoft/TypeScript#52675) - // for strings getStringLiteralCompletions handles completions - isLiteralExpected := !(previousToken != nil && ast.IsStringLiteralLike(previousToken)) && !isJsxIdentifierExpected - var literals []literalValue - if isLiteralExpected { - var types []*checker.Type - if contextualType != nil && contextualType.IsUnion() { - types = contextualType.Types() - } else if contextualType != nil { - types = []*checker.Type{contextualType} - } - literals = core.MapNonNil(types, func(t *checker.Type) literalValue { - if isLiteral(t) && !t.IsEnumLiteral() { - return t.AsLiteralType().Value() - } - return nil - }) - } - - var recommendedCompletion *ast.Symbol - if previousToken != nil && contextualType != nil { - recommendedCompletion = getRecommendedCompletion(previousToken, contextualType, typeChecker) - } - - if defaultCommitCharacters == nil { - defaultCommitCharacters = getDefaultCommitCharacters(isNewIdentifierLocation) - } - - return &completionDataData{ - symbols: symbols, - completionKind: completionKind, - isInSnippetScope: isInSnippetScope, - propertyAccessToConvert: propertyAccessToConvert, - isNewIdentifierLocation: isNewIdentifierLocation, - location: location, - keywordFilters: keywordFilters, - literals: literals, - symbolToOriginInfoMap: symbolToOriginInfoMap, - symbolToSortTextMap: symbolToSortTextMap, - recommendedCompletion: recommendedCompletion, - previousToken: previousToken, - contextToken: contextToken, - jsxInitializer: jsxInitializer, - insideJSDocTagTypeExpression: insideJSDocTagTypeExpression, - isTypeOnlyLocation: isTypeOnlyLocation, - isJsxIdentifierExpected: isJsxIdentifierExpected, - isRightOfOpenTag: isRightOfOpenTag, - isRightOfDotOrQuestionDot: isRightOfDot || isRightOfQuestionDot, - importStatementCompletion: importStatementCompletion, - hasUnresolvedAutoImports: hasUnresolvedAutoImports, - defaultCommitCharacters: defaultCommitCharacters, - } -} - -func keywordCompletionData( - keywordFilters KeywordCompletionFilters, - filterOutTSOnlyKeywords bool, - isNewIdentifierLocation bool, -) *completionDataKeyword { - return &completionDataKeyword{ - keywordCompletions: getKeywordCompletions(keywordFilters, filterOutTSOnlyKeywords), - isNewIdentifierLocation: isNewIdentifierLocation, - } -} - -func getDefaultCommitCharacters(isNewIdentifierLocation bool) []string { - if isNewIdentifierLocation { - return []string{} - } - return slices.Clone(allCommitCharacters) -} - -func (l *LanguageService) completionInfoFromData( - ctx context.Context, - typeChecker *checker.Checker, - file *ast.SourceFile, - compilerOptions *core.CompilerOptions, - data *completionDataData, - preferences *UserPreferences, - position int, - clientOptions *lsproto.CompletionClientCapabilities, - optionalReplacementSpan *lsproto.Range, -) *lsproto.CompletionList { - keywordFilters := data.keywordFilters - isNewIdentifierLocation := data.isNewIdentifierLocation - contextToken := data.contextToken - literals := data.literals - - // Verify if the file is JSX language variant - if file.LanguageVariant == core.LanguageVariantJSX { - list := l.getJsxClosingTagCompletion(data.location, file, position, clientOptions) - if list != nil { - return list - } - } - - // When the completion is for the expression of a case clause (e.g. `case |`), - // filter literals & enum symbols whose values are already present in existing case clauses. - caseClause := ast.FindAncestor(contextToken, ast.IsCaseClause) - if caseClause != nil && - (contextToken.Kind == ast.KindCaseKeyword || - ast.IsNodeDescendantOf(contextToken, caseClause.Expression())) { - tracker := newCaseClauseTracker(typeChecker, caseClause.Parent.AsCaseBlock().Clauses.Nodes) - literals = core.Filter(literals, func(literal literalValue) bool { - return !tracker.hasValue(literal) - }) - data.symbols = core.Filter(data.symbols, func(symbol *ast.Symbol) bool { - if symbol.ValueDeclaration != nil && ast.IsEnumMember(symbol.ValueDeclaration) { - value := typeChecker.GetConstantValue(symbol.ValueDeclaration) - if value != nil && tracker.hasValue(value) { - return false - } - } - return true - }) - } - - isChecked := isCheckedFile(file, compilerOptions) - if isChecked && !isNewIdentifierLocation && len(data.symbols) == 0 && keywordFilters == KeywordCompletionFiltersNone { - return nil - } - - uniqueNames, sortedEntries := l.getCompletionEntriesFromSymbols( - ctx, - data, - nil, /*replacementToken*/ - position, - file, - preferences, - compilerOptions, - clientOptions, - ) - - if data.keywordFilters != KeywordCompletionFiltersNone { - keywordCompletions := getKeywordCompletions(data.keywordFilters, !data.insideJSDocTagTypeExpression && ast.IsSourceFileJS(file)) - for _, keywordEntry := range keywordCompletions { - if data.isTypeOnlyLocation && isTypeKeyword(scanner.StringToToken(keywordEntry.Label)) || - !data.isTypeOnlyLocation && isContextualKeywordInAutoImportableExpressionSpace(keywordEntry.Label) || - !uniqueNames.Has(keywordEntry.Label) { - uniqueNames.Add(keywordEntry.Label) - sortedEntries = core.InsertSorted(sortedEntries, keywordEntry, compareCompletionEntries) - } - } - } - - for _, keywordEntry := range getContextualKeywords(file, contextToken, position) { - if !uniqueNames.Has(keywordEntry.Label) { - uniqueNames.Add(keywordEntry.Label) - sortedEntries = core.InsertSorted(sortedEntries, keywordEntry, compareCompletionEntries) - } - } - - for _, literal := range literals { - literalEntry := createCompletionItemForLiteral(file, preferences, literal) - uniqueNames.Add(literalEntry.Label) - sortedEntries = core.InsertSorted(sortedEntries, literalEntry, compareCompletionEntries) - } - - if !isChecked { - sortedEntries = l.getJSCompletionEntries( - ctx, - file, - position, - &uniqueNames, - sortedEntries, - ) - } - - // !!! exhaustive case completions - - itemDefaults := l.setItemDefaults( - clientOptions, - position, - file, - sortedEntries, - &data.defaultCommitCharacters, - optionalReplacementSpan, - ) - - return &lsproto.CompletionList{ - IsIncomplete: data.hasUnresolvedAutoImports, - ItemDefaults: itemDefaults, - Items: sortedEntries, - } -} - -func (l *LanguageService) getCompletionEntriesFromSymbols( - ctx context.Context, - data *completionDataData, - replacementToken *ast.Node, - position int, - file *ast.SourceFile, - preferences *UserPreferences, - compilerOptions *core.CompilerOptions, - clientOptions *lsproto.CompletionClientCapabilities, -) (uniqueNames collections.Set[string], sortedEntries []*lsproto.CompletionItem) { - closestSymbolDeclaration := getClosestSymbolDeclaration(data.contextToken, data.location) - useSemicolons := probablyUsesSemicolons(file) - typeChecker, done := l.GetProgram().GetTypeCheckerForFile(ctx, file) - defer done() - isMemberCompletion := isMemberCompletionKind(data.completionKind) - // Tracks unique names. - // Value is set to false for global variables or completions from external module exports, because we can have multiple of those; - // true otherwise. Based on the order we add things we will always see locals first, then globals, then module exports. - // So adding a completion for a local will prevent us from adding completions for external module exports sharing the same name. - uniques := make(uniqueNamesMap) - for _, symbol := range data.symbols { - symbolId := ast.GetSymbolId(symbol) - origin := data.symbolToOriginInfoMap[symbolId] - name, needsConvertPropertyAccess := getCompletionEntryDisplayNameForSymbol( - symbol, - origin, - data.completionKind, - data.isJsxIdentifierExpected, - ) - if name == "" || - uniques[name] && (origin == nil || !originIsObjectLiteralMethod(origin)) || - data.completionKind == CompletionKindGlobal && - !shouldIncludeSymbol(symbol, data, closestSymbolDeclaration, file, typeChecker, compilerOptions) { - continue - } - - // When in a value location in a JS file, ignore symbols that definitely seem to be type-only. - if !data.isTypeOnlyLocation && ast.IsSourceFileJS(file) && symbolAppearsToBeTypeOnly(symbol, typeChecker) { - continue - } - - originalSortText := data.symbolToSortTextMap[ast.GetSymbolId(symbol)] - if originalSortText == "" { - originalSortText = SortTextLocationPriority - } - - var sortText sortText - if isDeprecated(symbol, typeChecker) { - sortText = DeprecateSortText(originalSortText) - } else { - sortText = originalSortText - } - entry := l.createCompletionItem( - ctx, - typeChecker, - symbol, - sortText, - replacementToken, - data, - position, - file, - name, - needsConvertPropertyAccess, - origin, - useSemicolons, - compilerOptions, - preferences, - clientOptions, - isMemberCompletion, - ) - if entry == nil { - continue - } - - // True for locals; false for globals, module exports from other files, `this.` completions. - shouldShadowLaterSymbols := (origin == nil || originIsTypeOnlyAlias(origin)) && - !(symbol.Parent == nil && - !core.Some(symbol.Declarations, func(d *ast.Node) bool { return ast.GetSourceFileOfNode(d) == file })) - uniques[name] = shouldShadowLaterSymbols - sortedEntries = core.InsertSorted(sortedEntries, entry, compareCompletionEntries) - } - - uniqueSet := collections.NewSetWithSizeHint[string](len(uniques)) - for name := range maps.Keys(uniques) { - uniqueSet.Add(name) - } - return *uniqueSet, sortedEntries -} - -func completionNameForLiteral( - file *ast.SourceFile, - preferences *UserPreferences, - literal literalValue, -) string { - switch literal := literal.(type) { - case string: - return quote(file, preferences, literal) - case jsnum.Number: - name, _ := core.StringifyJson(literal, "" /*prefix*/, "" /*suffix*/) - return name - case jsnum.PseudoBigInt: - return literal.String() + "n" - } - panic(fmt.Sprintf("Unhandled literal value: %v", literal)) -} - -func createCompletionItemForLiteral( - file *ast.SourceFile, - preferences *UserPreferences, - literal literalValue, -) *lsproto.CompletionItem { - return &lsproto.CompletionItem{ - Label: completionNameForLiteral(file, preferences, literal), - Kind: ptrTo(lsproto.CompletionItemKindConstant), - SortText: ptrTo(string(SortTextLocationPriority)), - CommitCharacters: ptrTo([]string{}), - } -} - -func (l *LanguageService) createCompletionItem( - ctx context.Context, - typeChecker *checker.Checker, - symbol *ast.Symbol, - sortText sortText, - replacementToken *ast.Node, - data *completionDataData, - position int, - file *ast.SourceFile, - name string, - needsConvertPropertyAccess bool, - origin *symbolOriginInfo, - useSemicolons bool, - compilerOptions *core.CompilerOptions, - preferences *UserPreferences, - clientOptions *lsproto.CompletionClientCapabilities, - isMemberCompletion bool, -) *lsproto.CompletionItem { - contextToken := data.contextToken - var insertText string - var filterText string - replacementSpan := l.getReplacementRangeForContextToken(file, replacementToken, position) - var isSnippet, hasAction bool - source := getSourceFromOrigin(origin) - var labelDetails *lsproto.CompletionItemLabelDetails - - insertQuestionDot := originIsNullableMember(origin) - useBraces := originIsSymbolMember(origin) || needsConvertPropertyAccess - if originIsThisType(origin) { - if needsConvertPropertyAccess { - insertText = fmt.Sprintf( - "this%s[%s]", - core.IfElse(insertQuestionDot, "?.", ""), - quotePropertyName(file, preferences, name)) - } else { - insertText = fmt.Sprintf( - "this%s%s", - core.IfElse(insertQuestionDot, "?.", "."), - name) - } - } else if data.propertyAccessToConvert != nil && (useBraces || insertQuestionDot) { - // We should only have needsConvertPropertyAccess if there's a property access to convert. But see microsoft/TypeScript#21790. - // Somehow there was a global with a non-identifier name. Hopefully someone will complain about getting a "foo bar" global completion and provide a repro. - if useBraces { - if needsConvertPropertyAccess { - insertText = fmt.Sprintf("[%s]", quotePropertyName(file, preferences, name)) - } else { - insertText = fmt.Sprintf("[%s]", name) - } - } else { - insertText = name - } - - if insertQuestionDot || data.propertyAccessToConvert.AsPropertyAccessExpression().QuestionDotToken != nil { - insertText = "?." + insertText - } - - dot := findChildOfKind(data.propertyAccessToConvert, ast.KindDotToken, file) - if dot == nil { - dot = findChildOfKind(data.propertyAccessToConvert, ast.KindQuestionDotToken, file) - } - - if dot == nil { - return nil - } - - // If the text after the '.' starts with this name, write over it. Else, add new text. - var end int - if strings.HasPrefix(name, data.propertyAccessToConvert.Name().Text()) { - end = data.propertyAccessToConvert.End() - } else { - end = dot.End() - } - replacementSpan = l.createLspRangeFromBounds(astnav.GetStartOfNode(dot, file, false /*includeJSDoc*/), end, file) - } - - if data.jsxInitializer.isInitializer { - if insertText == "" { - insertText = name - } - insertText = fmt.Sprintf("{%s}", insertText) - if data.jsxInitializer.initializer != nil { - replacementSpan = l.createLspRangeFromNode(data.jsxInitializer.initializer, file) - } - } - - if originIsPromise(origin) && data.propertyAccessToConvert != nil { - if insertText == "" { - insertText = name - } - precedingToken := astnav.FindPrecedingToken(file, data.propertyAccessToConvert.Pos()) - var awaitText string - if precedingToken != nil && lsutil.PositionIsASICandidate(precedingToken.End(), precedingToken.Parent, file) { - awaitText = ";" - } - - awaitText += "(await " + scanner.GetTextOfNode(data.propertyAccessToConvert.Expression()) + ")" - if needsConvertPropertyAccess { - insertText = awaitText + insertText - } else { - dotStr := core.IfElse(insertQuestionDot, "?.", ".") - insertText = awaitText + dotStr + insertText - } - isInAwaitExpression := ast.IsAwaitExpression(data.propertyAccessToConvert.Parent) - wrapNode := core.IfElse( - isInAwaitExpression, - data.propertyAccessToConvert.Parent, - data.propertyAccessToConvert.Expression(), - ) - replacementSpan = l.createLspRangeFromBounds( - astnav.GetStartOfNode(wrapNode, file, false /*includeJSDoc*/), - data.propertyAccessToConvert.End(), - file) - } - - if originIsExport(origin) { - resolvedOrigin := origin.asExport() - labelDetails = &lsproto.CompletionItemLabelDetails{ - Description: &resolvedOrigin.moduleSpecifier, // !!! vscode @link support - } - if data.importStatementCompletion != nil { - quotedModuleSpecifier := escapeSnippetText(quote(file, preferences, resolvedOrigin.moduleSpecifier)) - exportKind := ExportKindNamed - if origin.isDefaultExport { - exportKind = ExportKindDefault - } else if resolvedOrigin.exportName == ast.InternalSymbolNameExportEquals { - exportKind = ExportKindExportEquals - } - - insertText = "import " - typeOnlyText := scanner.TokenToString(ast.KindTypeKeyword) + " " - if data.importStatementCompletion.isTopLevelTypeOnly { - insertText += typeOnlyText - } - tabStop := core.IfElse(ptrIsTrue(clientOptions.CompletionItem.SnippetSupport), "$1", "") - importKind := getImportKind(file, exportKind, l.GetProgram(), true /*forceImportKeyword*/) - escapedSnippet := escapeSnippetText(name) - suffix := core.IfElse(useSemicolons, ";", "") - switch importKind { - case ImportKindCommonJS: - insertText += fmt.Sprintf(`%s%s = require(%s)%s`, escapedSnippet, tabStop, quotedModuleSpecifier, suffix) - case ImportKindDefault: - insertText += fmt.Sprintf(`%s%s from %s%s`, escapedSnippet, tabStop, quotedModuleSpecifier, suffix) - case ImportKindNamespace: - insertText += fmt.Sprintf(`* as %s from %s%s`, escapedSnippet, quotedModuleSpecifier, suffix) - case ImportKindNamed: - importSpecifierTypeOnlyText := core.IfElse(data.importStatementCompletion.couldBeTypeOnlyImportSpecifier, typeOnlyText, "") - insertText += fmt.Sprintf(`{ %s%s%s } from %s%s`, importSpecifierTypeOnlyText, escapedSnippet, tabStop, quotedModuleSpecifier, suffix) - } - - replacementSpan = data.importStatementCompletion.replacementSpan - isSnippet = ptrIsTrue(clientOptions.CompletionItem.SnippetSupport) - } - } - - if originIsTypeOnlyAlias(origin) { - hasAction = true - } - - // Provide object member completions when missing commas, and insert missing commas. - // For example: - // - // interface I { - // a: string; - // b: number - // } - // - // const cc: I = { a: "red" | } - // - // Completion should add a comma after "red" and provide completions for b - if data.completionKind == CompletionKindObjectPropertyDeclaration && - contextToken != nil && - !ast.NodeHasKind(astnav.FindPrecedingTokenEx(file, contextToken.Pos(), contextToken, false /*excludeJSDoc*/), ast.KindCommaToken) { - if ast.IsMethodDeclaration(contextToken.Parent.Parent) || - ast.IsGetAccessorDeclaration(contextToken.Parent.Parent) || - ast.IsSetAccessorDeclaration(contextToken.Parent.Parent) || - ast.IsSpreadAssignment(contextToken.Parent) || - lsutil.GetLastToken(ast.FindAncestor(contextToken.Parent, ast.IsPropertyAssignment), file) == contextToken || - ast.IsShorthandPropertyAssignment(contextToken.Parent) && - getLineOfPosition(file, contextToken.End()) != getLineOfPosition(file, position) { - source = string(completionSourceObjectLiteralMemberWithComma) - hasAction = true - } - } - - if preferences.IncludeCompletionsWithClassMemberSnippets.IsTrue() && - data.completionKind == CompletionKindMemberLike && - isClassLikeMemberCompletion(symbol, data.location, file) { - // !!! class member completions - } - - if originIsObjectLiteralMethod(origin) { - insertText = origin.asObjectLiteralMethod().insertText - isSnippet = origin.asObjectLiteralMethod().isSnippet - labelDetails = origin.asObjectLiteralMethod().labelDetails // !!! check if this can conflict with case above where we set label details - if !clientSupportsItemLabelDetails(clientOptions) { - name = name + *origin.asObjectLiteralMethod().labelDetails.Detail - labelDetails = nil - } - source = string(completionSourceObjectLiteralMethodSnippet) - sortText = sortBelow(sortText) - } - - if data.isJsxIdentifierExpected && - !data.isRightOfOpenTag && - clientSupportsItemSnippet(clientOptions) && - preferences.JsxAttributeCompletionStyle != JsxAttributeCompletionStyleNone && - !(ast.IsJsxAttribute(data.location.Parent) && data.location.Parent.Initializer() != nil) { - useBraces := preferences.JsxAttributeCompletionStyle == JsxAttributeCompletionStyleBraces - t := typeChecker.GetTypeOfSymbolAtLocation(symbol, data.location) - - // If is boolean like or undefined, don't return a snippet, we want to return just the completion. - if preferences.JsxAttributeCompletionStyle == JsxAttributeCompletionStyleAuto && - !t.IsBooleanLike() && - !(t.IsUnion() && core.Some(t.Types(), (*checker.Type).IsBooleanLike)) { - if t.IsStringLike() || - t.IsUnion() && - core.Every( - t.Types(), - func(t *checker.Type) bool { - return t.Flags()&(checker.TypeFlagsStringLike|checker.TypeFlagsUndefined) != 0 || - isStringAndEmptyAnonymousObjectIntersection(typeChecker, t) - }) { - // If type is string-like or undefined, use quotes. - insertText = fmt.Sprintf("%s=%s", escapeSnippetText(name), quote(file, preferences, "$1")) - isSnippet = true - } else { - // Use braces for everything else. - useBraces = true - } - } - - if useBraces { - insertText = escapeSnippetText(name) + "={$1}" - isSnippet = true - } - } - - var autoImportData *completionEntryData - if originIsExport(origin) { - autoImportData = origin.toCompletionEntryData() - hasAction = data.importStatementCompletion == nil - } - - parentNamedImportOrExport := ast.FindAncestor(data.location, isNamedImportsOrExports) - if parentNamedImportOrExport != nil { - if !scanner.IsIdentifierText(name, core.LanguageVariantStandard) { - insertText = quotePropertyName(file, preferences, name) - - if parentNamedImportOrExport.Kind == ast.KindNamedImports { - // Check if it is `import { ^here as name } from '...'``. - // We have to access the scanner here to check if it is `{ ^here as name }`` or `{ ^here, as, name }`. - scanner := scanner.NewScanner() - scanner.SetText(file.Text()) - scanner.ResetPos(position) - if !(scanner.Scan() == ast.KindAsKeyword && scanner.Scan() == ast.KindIdentifier) { - insertText += " as " + generateIdentifierForArbitraryString(name) - } - } - } else if parentNamedImportOrExport.Kind == ast.KindNamedImports { - possibleToken := scanner.StringToToken(name) - if possibleToken != ast.KindUnknown && - (possibleToken == ast.KindAwaitKeyword || isNonContextualKeyword(possibleToken)) { - insertText = fmt.Sprintf("%s as %s_", name, name) - } - } - } - - // Commit characters - - elementKind := getSymbolKind(typeChecker, symbol, data.location) - var commitCharacters *[]string - if clientSupportsItemCommitCharacters(clientOptions) { - if elementKind == ScriptElementKindWarning || elementKind == ScriptElementKindString { - commitCharacters = &[]string{} - } else if !clientSupportsDefaultCommitCharacters(clientOptions) { - commitCharacters = ptrTo(data.defaultCommitCharacters) - } - // Otherwise use the completion list default. - } - - preselect := isRecommendedCompletionMatch(symbol, data.recommendedCompletion, typeChecker) - kindModifiers := getSymbolModifiers(typeChecker, symbol) - - return l.createLSPCompletionItem( - name, - insertText, - filterText, - sortText, - elementKind, - kindModifiers, - replacementSpan, - commitCharacters, - labelDetails, - file, - position, - clientOptions, - isMemberCompletion, - isSnippet, - hasAction, - preselect, - source, - autoImportData, - ) -} - -func isRecommendedCompletionMatch(localSymbol *ast.Symbol, recommendedCompletion *ast.Symbol, typeChecker *checker.Checker) bool { - return localSymbol == recommendedCompletion || - localSymbol.Flags&ast.SymbolFlagsExportValue != 0 && typeChecker.GetExportSymbolOfSymbol(localSymbol) == recommendedCompletion -} - -// Ported from vscode. -var wordSeparators = collections.NewSetFromItems( - '`', '~', '!', '@', '%', '^', '&', '*', '(', ')', '-', '=', '+', '[', '{', ']', '}', '\\', '|', - ';', ':', '\'', '"', ',', '.', '<', '>', '/', '?', -) - -// Finds the length and first rune of the word that ends at the given position. -// e.g. for "abc def.ghi|jkl", the word length is 3 and the word start is 'g'. -func getWordLengthAndStart(sourceFile *ast.SourceFile, position int) (wordLength int, wordStart rune) { - // !!! Port other case of vscode's `DEFAULT_WORD_REGEXP` that covers words that start like numbers, e.g. -123.456abcd. - text := sourceFile.Text()[:position] - totalSize := 0 - var firstRune rune - for r, size := utf8.DecodeLastRuneInString(text); size != 0; r, size = utf8.DecodeLastRuneInString(text[:len(text)-totalSize]) { - if wordSeparators.Has(r) || unicode.IsSpace(r) { - break - } - totalSize += size - firstRune = r - } - // If word starts with `@`, disregard this first character. - if firstRune == '@' { - totalSize -= 1 - firstRune, _ = utf8.DecodeRuneInString(text[len(text)-totalSize:]) - } - return totalSize, firstRune -} - -// `["ab c"]` -> `ab c` -// `['ab c']` -> `ab c` -// `[123]` -> `123` -func trimElementAccess(text string) string { - text = strings.TrimPrefix(text, "[") - text = strings.TrimSuffix(text, "]") - if strings.HasPrefix(text, `'`) && strings.HasSuffix(text, `'`) { - text = strings.TrimPrefix(strings.TrimSuffix(text, `'`), `'`) - } - if strings.HasPrefix(text, `"`) && strings.HasSuffix(text, `"`) { - text = strings.TrimPrefix(strings.TrimSuffix(text, `"`), `"`) - } - return text -} - -// Ported from vscode ts extension: `getFilterText`. -func getFilterText( - file *ast.SourceFile, - position int, - insertText string, - label string, - wordStart rune, - dotAccessor string, -) string { - // Private field completion, e.g. label `#bar`. - if strings.HasPrefix(label, "#") { - if insertText != "" { - if strings.HasPrefix(insertText, "this.#") { - if wordStart == '#' { - // `method() { this.#| }` - // `method() { #| }` - return "" - } else { - // `method() { this.| }` - // `method() { | }` - return strings.TrimPrefix(insertText, "this.#") - } - } - } else { - if wordStart == '#' { - // `method() { this.#| }` - return "" - } else { - // `method() { this.| }` - // `method() { | }` - return strings.TrimPrefix(label, "#") - } - } - } - - // For `this.` completions, generally don't set the filter text since we don't want them to be overly deprioritized. microsoft/vscode#74164 - if strings.HasPrefix(insertText, "this.") { - return "" - } - - // Handle the case: - // ``` - // const xyz = { 'ab c': 1 }; - // xyz.ab| - // ``` - // In which case we want to insert a bracket accessor but should use `.abc` as the filter text instead of - // the bracketed insert text. - if strings.HasPrefix(insertText, "[") { - return dotAccessor + trimElementAccess(insertText) - } - - if strings.HasPrefix(insertText, "?.") { - // Handle this case like the case above: - // ``` - // const xyz = { 'ab c': 1 } | undefined; - // xyz.ab| - // ``` - // filterText should be `.ab c` instead of `?.['ab c']`. - if strings.HasPrefix(insertText, "?.[") { - return dotAccessor + trimElementAccess(insertText[2:]) - } else { - // ``` - // const xyz = { abc: 1 } | undefined; - // xyz.ab| - // ``` - // filterText should be `.abc` instead of `?.abc. - return dotAccessor + insertText[2:] - } - } - - // In all other cases, fall back to using the insertText. - return insertText -} - -// Ported from vscode's `provideCompletionItems`. -func getDotAccessor(file *ast.SourceFile, position int) string { - text := file.Text()[:position] - totalSize := 0 - if strings.HasSuffix(text, "?.") { - totalSize += 2 - return file.Text()[position-totalSize : position] - } - if strings.HasSuffix(text, ".") { - totalSize += 1 - return file.Text()[position-totalSize : position] - } - return "" -} - -func strPtrIsEmpty(ptr *string) bool { - if ptr == nil { - return true - } - return *ptr == "" -} - -func strPtrTo(v string) *string { - if v == "" { - return nil - } - return &v -} - -func ptrIsTrue(ptr *bool) bool { - if ptr == nil { - return false - } - return *ptr -} - -func ptrIsFalse(ptr *bool) bool { - if ptr == nil { - return false - } - return !*ptr -} - -func boolToPtr(v bool) *bool { - if v { - return ptrTo(true) - } - return nil -} - -func getLineOfPosition(file *ast.SourceFile, pos int) int { - line, _ := scanner.GetECMALineAndCharacterOfPosition(file, pos) - return line -} - -func getLineEndOfPosition(file *ast.SourceFile, pos int) int { - line := getLineOfPosition(file, pos) - lineStarts := scanner.GetECMALineStarts(file) - var lastCharPos int - if line+1 >= len(lineStarts) { - lastCharPos = file.End() - } else { - lastCharPos = int(lineStarts[line+1]) - 1 - } - fullText := file.Text() - if lastCharPos > 0 && lastCharPos < len(fullText) && fullText[lastCharPos] == '\n' && fullText[lastCharPos-1] == '\r' { - return lastCharPos - 1 - } - return lastCharPos -} - -func isClassLikeMemberCompletion(symbol *ast.Symbol, location *ast.Node, file *ast.SourceFile) bool { - // !!! class member completions - return false -} - -func symbolAppearsToBeTypeOnly(symbol *ast.Symbol, typeChecker *checker.Checker) bool { - flags := checker.GetCombinedLocalAndExportSymbolFlags(checker.SkipAlias(symbol, typeChecker)) - return flags&ast.SymbolFlagsValue == 0 && - (len(symbol.Declarations) == 0 || !ast.IsInJSFile(symbol.Declarations[0]) || flags&ast.SymbolFlagsType != 0) -} - -func shouldIncludeSymbol( - symbol *ast.Symbol, - data *completionDataData, - closestSymbolDeclaration *ast.Declaration, - file *ast.SourceFile, - typeChecker *checker.Checker, - compilerOptions *core.CompilerOptions, -) bool { - allFlags := symbol.Flags - location := data.location - // export = /**/ here we want to get all meanings, so any symbol is ok - if location.Parent != nil && ast.IsExportAssignment(location.Parent) { - return true - } - - // Filter out variables from their own initializers - // `const a = /* no 'a' here */` - if closestSymbolDeclaration != nil && - ast.IsVariableDeclaration(closestSymbolDeclaration) && - symbol.ValueDeclaration == closestSymbolDeclaration { - return false - } - - // Filter out current and latter parameters from defaults - // `function f(a = /* no 'a' and 'b' here */, b) { }` or - // `function f(a: T, b: T2) { }` - var symbolDeclaration *ast.Declaration - if symbol.ValueDeclaration != nil { - symbolDeclaration = symbol.ValueDeclaration - } else if len(symbol.Declarations) > 0 { - symbolDeclaration = symbol.Declarations[0] - } - - if closestSymbolDeclaration != nil && symbolDeclaration != nil { - if ast.IsParameter(closestSymbolDeclaration) && ast.IsParameter(symbolDeclaration) { - parameters := closestSymbolDeclaration.Parent.ParameterList() - if symbolDeclaration.Pos() >= closestSymbolDeclaration.Pos() && - symbolDeclaration.Pos() < parameters.End() { - return false - } - } else if ast.IsTypeParameterDeclaration(closestSymbolDeclaration) && - ast.IsTypeParameterDeclaration(symbolDeclaration) { - if closestSymbolDeclaration == symbolDeclaration && data.contextToken != nil && data.contextToken.Kind == ast.KindExtendsKeyword { - // filter out the directly self-recursive type parameters - // `type A = K` - return false - } - if isInTypeParameterDefault(data.contextToken) && !ast.IsInferTypeNode(closestSymbolDeclaration.Parent) { - typeParameters := closestSymbolDeclaration.Parent.TypeParameterList() - if typeParameters != nil && symbolDeclaration.Pos() >= closestSymbolDeclaration.Pos() && - symbolDeclaration.Pos() < typeParameters.End() { - return false - } - } - } - } - - // External modules can have global export declarations that will be - // available as global keywords in all scopes. But if the external module - // already has an explicit export and user only wants to use explicit - // module imports then the global keywords will be filtered out so auto - // import suggestions will win in the completion. - symbolOrigin := checker.SkipAlias(symbol, typeChecker) - // We only want to filter out the global keywords. - // Auto Imports are not available for scripts so this conditional is always false. - if file.AsSourceFile().ExternalModuleIndicator != nil && - compilerOptions.AllowUmdGlobalAccess != core.TSTrue && - data.symbolToSortTextMap[ast.GetSymbolId(symbol)] == SortTextGlobalsOrKeywords && - (data.symbolToSortTextMap[ast.GetSymbolId(symbolOrigin)] == SortTextAutoImportSuggestions || - data.symbolToSortTextMap[ast.GetSymbolId(symbolOrigin)] == SortTextLocationPriority) { - return false - } - - allFlags = allFlags | checker.GetCombinedLocalAndExportSymbolFlags(symbolOrigin) - - // import m = /**/ <-- It can only access namespace (if typing import = x. this would get member symbols and not namespace) - if isInRightSideOfInternalImportEqualsDeclaration(data.location) { - return allFlags&ast.SymbolFlagsNamespace != 0 - } - - if data.isTypeOnlyLocation { - // It's a type, but you can reach it by namespace.type as well. - return symbolCanBeReferencedAtTypeLocation(symbol, typeChecker, collections.Set[ast.SymbolId]{}) - } - - // expressions are value space (which includes the value namespaces) - return allFlags&ast.SymbolFlagsValue != 0 -} - -func getCompletionEntryDisplayNameForSymbol( - symbol *ast.Symbol, - origin *symbolOriginInfo, - completionKind CompletionKind, - isJsxIdentifierExpected bool, -) (displayName string, needsConvertPropertyAccess bool) { - if originIsIgnore(origin) { - return "", false - } - - var name string - if originIncludesSymbolName(origin) { - name = origin.symbolName() - } else { - name = ast.SymbolName(symbol) - } - if name == "" || - // If the symbol is external module, don't show it in the completion list - // (i.e declare module "http" { const x; } | // <= request completion here, "http" should not be there) - symbol.Flags&ast.SymbolFlagsModule != 0 && startsWithQuote(name) || - // If the symbol is the internal name of an ES symbol, it is not a valid entry. Internal names for ES symbols start with "__@" - checker.IsKnownSymbol(symbol) { - return "", false - } - - variant := core.IfElse(isJsxIdentifierExpected, core.LanguageVariantJSX, core.LanguageVariantStandard) - // name is a valid identifier or private identifier text - if scanner.IsIdentifierText(name, variant) || - symbol.ValueDeclaration != nil && ast.IsPrivateIdentifierClassElementDeclaration(symbol.ValueDeclaration) { - return name, false - } - if symbol.Flags&ast.SymbolFlagsAlias != 0 { - // Allow non-identifier import/export aliases since we can insert them as string literals - return name, true - } - - switch completionKind { - case CompletionKindMemberLike: - if originIsComputedPropertyName(origin) { - return origin.symbolName(), false - } - return "", false - case CompletionKindObjectPropertyDeclaration: - // TODO: microsoft/TypeScript#18169 - escapedName, _ := core.StringifyJson(name, "", "") - return escapedName, false - case CompletionKindPropertyAccess, CompletionKindGlobal: - // For a 'this.' completion it will be in a global context, but may have a non-identifier name. - // Don't add a completion for a name starting with a space. See https://github.com/Microsoft/TypeScript/pull/20547 - ch, _ := utf8.DecodeRuneInString(name) - if ch == ' ' { - return "", false - } - return name, true - case CompletionKindNone, CompletionKindString: - return name, false - default: - panic(fmt.Sprintf("Unexpected completion kind: %v", completionKind)) - } -} - -// !!! refactor symbolOriginInfo so that we can tell the difference between flags and the kind of data it has -func originIsIgnore(origin *symbolOriginInfo) bool { - return origin != nil && origin.kind&symbolOriginInfoKindIgnore != 0 -} - -func originIncludesSymbolName(origin *symbolOriginInfo) bool { - return originIsExport(origin) || originIsComputedPropertyName(origin) -} - -func originIsExport(origin *symbolOriginInfo) bool { - return origin != nil && origin.kind&symbolOriginInfoKindExport != 0 -} - -func originIsComputedPropertyName(origin *symbolOriginInfo) bool { - return origin != nil && origin.kind&symbolOriginInfoKindComputedPropertyName != 0 -} - -func originIsObjectLiteralMethod(origin *symbolOriginInfo) bool { - return origin != nil && origin.kind&symbolOriginInfoKindObjectLiteralMethod != 0 -} - -func originIsThisType(origin *symbolOriginInfo) bool { - return origin != nil && origin.kind&symbolOriginInfoKindThisType != 0 -} - -func originIsTypeOnlyAlias(origin *symbolOriginInfo) bool { - return origin != nil && origin.kind&symbolOriginInfoKindTypeOnlyAlias != 0 -} - -func originIsSymbolMember(origin *symbolOriginInfo) bool { - return origin != nil && origin.kind&symbolOriginInfoKindSymbolMember != 0 -} - -func originIsNullableMember(origin *symbolOriginInfo) bool { - return origin != nil && origin.kind&symbolOriginInfoKindNullable != 0 -} - -func originIsPromise(origin *symbolOriginInfo) bool { - return origin != nil && origin.kind&symbolOriginInfoKindPromise != 0 -} - -func getSourceFromOrigin(origin *symbolOriginInfo) string { - if originIsExport(origin) { - return stringutil.StripQuotes(ast.SymbolName(origin.asExport().moduleSymbol)) - } - - if originIsExport(origin) { - return origin.asExport().moduleSpecifier - } - - if originIsThisType(origin) { - return string(completionSourceThisProperty) - } - - if originIsTypeOnlyAlias(origin) { - return string(completionSourceTypeOnlyAlias) - } - - return "" -} - -// In a scenarion such as `const x = 1 * |`, the context and previous tokens are both `*`. -// In `const x = 1 * o|`, the context token is *, and the previous token is `o`. -// `contextToken` and `previousToken` can both be nil if we are at the beginning of the file. -func getRelevantTokens(position int, file *ast.SourceFile) (contextToken *ast.Node, previousToken *ast.Node) { - previousToken = astnav.FindPrecedingToken(file, position) - if previousToken != nil && position <= previousToken.End() && (ast.IsMemberName(previousToken) || ast.IsKeywordKind(previousToken.Kind)) { - contextToken := astnav.FindPrecedingToken(file, previousToken.Pos()) - return contextToken, previousToken - } - return previousToken, previousToken -} - -// "." | '"' | "'" | "`" | "/" | "@" | "<" | "#" | " " -type CompletionsTriggerCharacter = string - -func isValidTrigger(file *ast.SourceFile, triggerCharacter CompletionsTriggerCharacter, contextToken *ast.Node, position int) bool { - switch triggerCharacter { - case ".", "@": - return true - case "\"", "'", "`": - // Only automatically bring up completions if this is an opening quote. - return contextToken != nil && - isStringLiteralOrTemplate(contextToken) && - position == astnav.GetStartOfNode(contextToken, file, false /*includeJSDoc*/)+1 - case "#": - return contextToken != nil && - ast.IsPrivateIdentifier(contextToken) && - ast.GetContainingClass(contextToken) != nil - case "<": - // Opening JSX tag - return contextToken != nil && - contextToken.Kind == ast.KindLessThanToken && - (!ast.IsBinaryExpression(contextToken.Parent) || binaryExpressionMayBeOpenTag(contextToken.Parent.AsBinaryExpression())) - case "/": - if contextToken == nil { - return false - } - if ast.IsStringLiteralLike(contextToken) { - return tryGetImportFromModuleSpecifier(contextToken) != nil - } - return contextToken.Kind == ast.KindLessThanSlashToken && ast.IsJsxClosingElement(contextToken.Parent) - case " ": - return contextToken != nil && contextToken.Kind == ast.KindImportKeyword && contextToken.Parent.Kind == ast.KindSourceFile - default: - panic("Unknown trigger character: " + triggerCharacter) - } -} - -func isStringLiteralOrTemplate(node *ast.Node) bool { - switch node.Kind { - case ast.KindStringLiteral, ast.KindNoSubstitutionTemplateLiteral, ast.KindTemplateExpression, - ast.KindTaggedTemplateExpression: - return true - } - return false -} - -func binaryExpressionMayBeOpenTag(binaryExpression *ast.BinaryExpression) bool { - return ast.NodeIsMissing(binaryExpression.Left) -} - -func isCheckedFile(file *ast.SourceFile, compilerOptions *core.CompilerOptions) bool { - return !ast.IsSourceFileJS(file) || ast.IsCheckJSEnabledForFile(file, compilerOptions) -} - -func isContextTokenValueLocation(contextToken *ast.Node) bool { - return contextToken != nil && ((contextToken.Kind == ast.KindTypeOfKeyword && - (contextToken.Parent.Kind == ast.KindTypeQuery || ast.IsTypeOfExpression(contextToken.Parent))) || - (contextToken.Kind == ast.KindAssertsKeyword && contextToken.Parent.Kind == ast.KindTypePredicate)) -} - -func isPossiblyTypeArgumentPosition(token *ast.Node, sourceFile *ast.SourceFile, typeChecker *checker.Checker) bool { - info := getPossibleTypeArgumentsInfo(token, sourceFile) - return info != nil && (ast.IsPartOfTypeNode(info.called) || - len(getPossibleGenericSignatures(info.called, info.nTypeArguments, typeChecker)) != 0 || - isPossiblyTypeArgumentPosition(info.called, sourceFile, typeChecker)) -} - -func isContextTokenTypeLocation(contextToken *ast.Node) bool { - if contextToken != nil { - parentKind := contextToken.Parent.Kind - switch contextToken.Kind { - case ast.KindColonToken: - return parentKind == ast.KindPropertyDeclaration || - parentKind == ast.KindPropertySignature || - parentKind == ast.KindParameter || - parentKind == ast.KindVariableDeclaration || - ast.IsFunctionLikeKind(parentKind) - case ast.KindEqualsToken: - return parentKind == ast.KindTypeAliasDeclaration || parentKind == ast.KindTypeParameter - case ast.KindAsKeyword: - return parentKind == ast.KindAsExpression - case ast.KindLessThanToken: - return parentKind == ast.KindTypeReference || parentKind == ast.KindTypeAssertionExpression - case ast.KindExtendsKeyword: - return parentKind == ast.KindTypeParameter - case ast.KindSatisfiesKeyword: - return parentKind == ast.KindSatisfiesExpression - } - } - return false -} - -// True if symbol is a type or a module containing at least one type. -func symbolCanBeReferencedAtTypeLocation(symbol *ast.Symbol, typeChecker *checker.Checker, seenModules collections.Set[ast.SymbolId]) bool { - // Since an alias can be merged with a local declaration, we need to test both the alias and its target. - // This code used to just test the result of `skipAlias`, but that would ignore any locally introduced meanings. - return nonAliasCanBeReferencedAtTypeLocation(symbol, typeChecker, seenModules) || - nonAliasCanBeReferencedAtTypeLocation( - checker.SkipAlias(core.IfElse(symbol.ExportSymbol != nil, symbol.ExportSymbol, symbol), typeChecker), - typeChecker, - seenModules, - ) -} - -func nonAliasCanBeReferencedAtTypeLocation(symbol *ast.Symbol, typeChecker *checker.Checker, seenModules collections.Set[ast.SymbolId]) bool { - return symbol.Flags&ast.SymbolFlagsType != 0 || typeChecker.IsUnknownSymbol(symbol) || - symbol.Flags&ast.SymbolFlagsModule != 0 && seenModules.AddIfAbsent(ast.GetSymbolId(symbol)) && - core.Some( - typeChecker.GetExportsOfModule(symbol), - func(e *ast.Symbol) bool { return symbolCanBeReferencedAtTypeLocation(e, typeChecker, seenModules) }) -} - -// Gets all properties on a type, but if that type is a union of several types, -// excludes array-like types or callable/constructable types. -func getPropertiesForCompletion(t *checker.Type, typeChecker *checker.Checker) []*ast.Symbol { - if t.IsUnion() { - return core.CheckEachDefined(typeChecker.GetAllPossiblePropertiesOfTypes(t.Types()), "getAllPossiblePropertiesOfTypes() should all be defined.") - } else { - return core.CheckEachDefined(typeChecker.GetApparentProperties(t), "getApparentProperties() should all be defined.") - } -} - -// Given 'a.b.c', returns 'a'. -func getLeftMostName(e *ast.Expression) *ast.IdentifierNode { - if ast.IsIdentifier(e) { - return e - } else if ast.IsPropertyAccessExpression(e) { - return getLeftMostName(e.Expression()) - } else { - return nil - } -} - -func getFirstSymbolInChain(symbol *ast.Symbol, enclosingDeclaration *ast.Node, typeChecker *checker.Checker) *ast.Symbol { - chain := typeChecker.GetAccessibleSymbolChain( - symbol, - enclosingDeclaration, - ast.SymbolFlagsAll, /*meaning*/ - false /*useOnlyExternalAliasing*/) - if len(chain) > 0 { - return chain[0] - } - if symbol.Parent != nil { - if isModuleSymbol(symbol.Parent) { - return symbol - } - return getFirstSymbolInChain(symbol.Parent, enclosingDeclaration, typeChecker) - } - return nil -} - -func isModuleSymbol(symbol *ast.Symbol) bool { - return core.Some(symbol.Declarations, func(decl *ast.Declaration) bool { return decl.Kind == ast.KindSourceFile }) -} - -func getNullableSymbolOriginInfoKind(kind symbolOriginInfoKind, insertQuestionDot bool) symbolOriginInfoKind { - if insertQuestionDot { - kind |= symbolOriginInfoKindNullable - } - return kind -} - -func isStaticProperty(symbol *ast.Symbol) bool { - return symbol.ValueDeclaration != nil && - symbol.ValueDeclaration.ModifierFlags()&ast.ModifierFlagsStatic != 0 && - ast.IsClassLike(symbol.ValueDeclaration.Parent) -} - -func getContextualType(previousToken *ast.Node, position int, file *ast.SourceFile, typeChecker *checker.Checker) *checker.Type { - parent := previousToken.Parent - switch previousToken.Kind { - case ast.KindIdentifier: - return getContextualTypeFromParent(previousToken, typeChecker, checker.ContextFlagsNone) - case ast.KindEqualsToken: - switch parent.Kind { - case ast.KindVariableDeclaration: - return typeChecker.GetContextualType(parent.Initializer(), checker.ContextFlagsNone) - case ast.KindBinaryExpression: - return typeChecker.GetTypeAtLocation(parent.AsBinaryExpression().Left) - case ast.KindJsxAttribute: - return typeChecker.GetContextualTypeForJsxAttribute(parent) - default: - return nil - } - case ast.KindNewKeyword: - return typeChecker.GetContextualType(parent, checker.ContextFlagsNone) - case ast.KindCaseKeyword: - caseClause := core.IfElse(ast.IsCaseClause(parent), parent, nil) - if caseClause != nil { - return getSwitchedType(caseClause, typeChecker) - } - return nil - case ast.KindOpenBraceToken: - if ast.IsJsxExpression(parent) && !ast.IsJsxElement(parent.Parent) && !ast.IsJsxFragment(parent.Parent) { - return typeChecker.GetContextualTypeForJsxAttribute(parent.Parent) - } - return nil - default: - argInfo := getArgumentInfoForCompletions(previousToken, position, file, typeChecker) - if argInfo != nil { - return typeChecker.GetContextualTypeForArgumentAtIndex(argInfo.invocation, argInfo.argumentIndex) - } else if isEqualityOperatorKind(previousToken.Kind) && ast.IsBinaryExpression(parent) && isEqualityOperatorKind(parent.AsBinaryExpression().OperatorToken.Kind) { - // completion at `x ===/**/` - return typeChecker.GetTypeAtLocation(parent.AsBinaryExpression().Left) - } else { - contextualType := typeChecker.GetContextualType(previousToken, checker.ContextFlagsCompletions) - if contextualType != nil { - return contextualType - } - return typeChecker.GetContextualType(previousToken, checker.ContextFlagsNone) - } - } -} - -func getContextualTypeFromParent(node *ast.Expression, typeChecker *checker.Checker, contextFlags checker.ContextFlags) *checker.Type { - parent := ast.WalkUpParenthesizedExpressions(node.Parent) - switch parent.Kind { - case ast.KindNewExpression: - return typeChecker.GetContextualType(parent, contextFlags) - case ast.KindBinaryExpression: - if isEqualityOperatorKind(parent.AsBinaryExpression().OperatorToken.Kind) { - return typeChecker.GetTypeAtLocation( - core.IfElse(node == parent.AsBinaryExpression().Right, parent.AsBinaryExpression().Left, parent.AsBinaryExpression().Right)) - } - return typeChecker.GetContextualType(node, contextFlags) - case ast.KindCaseClause: - return getSwitchedType(parent, typeChecker) - default: - return typeChecker.GetContextualType(node, contextFlags) - } -} - -func getSwitchedType(caseClause *ast.CaseClauseNode, typeChecker *checker.Checker) *checker.Type { - return typeChecker.GetTypeAtLocation(caseClause.Parent.Parent.Expression()) -} - -func isEqualityOperatorKind(kind ast.Kind) bool { - switch kind { - case ast.KindEqualsEqualsEqualsToken, ast.KindEqualsEqualsToken, - ast.KindExclamationEqualsEqualsToken, ast.KindExclamationEqualsToken: - return true - default: - return false - } -} - -func isLiteral(t *checker.Type) bool { - return t.IsStringLiteral() || t.IsNumberLiteral() || t.IsBigIntLiteral() -} - -func getRecommendedCompletion(previousToken *ast.Node, contextualType *checker.Type, typeChecker *checker.Checker) *ast.Symbol { - var types []*checker.Type - if contextualType.IsUnion() { - types = contextualType.Types() - } else { - types = []*checker.Type{contextualType} - } - // For a union, return the first one with a recommended completion. - return core.FirstNonNil( - types, - func(t *checker.Type) *ast.Symbol { - symbol := t.Symbol() - // Don't make a recommended completion for an abstract class. - if symbol != nil && - symbol.Flags&(ast.SymbolFlagsEnumMember|ast.SymbolFlagsEnum|ast.SymbolFlagsClass) != 0 && - !isAbstractConstructorSymbol(symbol) { - return getFirstSymbolInChain(symbol, previousToken, typeChecker) - } - return nil - }, - ) -} - -func isAbstractConstructorSymbol(symbol *ast.Symbol) bool { - if symbol.Flags&ast.SymbolFlagsClass != 0 { - declaration := ast.GetClassLikeDeclarationOfSymbol(symbol) - return declaration != nil && ast.HasSyntacticModifier(declaration, ast.ModifierFlagsAbstract) - } - return false -} - -func startsWithQuote(s string) bool { - r, _ := utf8.DecodeRuneInString(s) - return r == '"' || r == '\'' -} - -func getClosestSymbolDeclaration(contextToken *ast.Node, location *ast.Node) *ast.Declaration { - if contextToken == nil { - return nil - } - - closestDeclaration := ast.FindAncestorOrQuit(contextToken, func(node *ast.Node) ast.FindAncestorResult { - if ast.IsFunctionBlock(node) || isArrowFunctionBody(node) || ast.IsBindingPattern(node) { - return ast.FindAncestorQuit - } - - if (ast.IsParameter(node) || ast.IsTypeParameterDeclaration(node)) && - !ast.IsIndexSignatureDeclaration(node.Parent) { - return ast.FindAncestorTrue - } - return ast.FindAncestorFalse - }) - - if closestDeclaration == nil { - closestDeclaration = ast.FindAncestorOrQuit(location, func(node *ast.Node) ast.FindAncestorResult { - if ast.IsFunctionBlock(node) || isArrowFunctionBody(node) || ast.IsBindingPattern(node) { - return ast.FindAncestorQuit - } - - if ast.IsVariableDeclaration(node) { - return ast.FindAncestorTrue - } - return ast.FindAncestorFalse - }) - } - return closestDeclaration -} - -func isArrowFunctionBody(node *ast.Node) bool { - return node.Parent != nil && ast.IsArrowFunction(node.Parent) && - (node.Parent.Body() == node || - // const a = () => /**/; - node.Kind == ast.KindEqualsGreaterThanToken) -} - -func isInTypeParameterDefault(contextToken *ast.Node) bool { - if contextToken == nil { - return false - } - - node := contextToken - parent := contextToken.Parent - for parent != nil { - if ast.IsTypeParameterDeclaration(parent) { - return parent.AsTypeParameter().DefaultType == node || node.Kind == ast.KindEqualsToken - } - node = parent - parent = parent.Parent - } - - return false -} - -func isDeprecated(symbol *ast.Symbol, typeChecker *checker.Checker) bool { - declarations := checker.SkipAlias(symbol, typeChecker).Declarations - return len(declarations) > 0 && core.Every(declarations, func(decl *ast.Declaration) bool { return typeChecker.IsDeprecatedDeclaration(decl) }) -} - -func (l *LanguageService) getReplacementRangeForContextToken(file *ast.SourceFile, contextToken *ast.Node, position int) *lsproto.Range { - if contextToken == nil { - return nil - } - - // !!! ensure range is single line - switch contextToken.Kind { - case ast.KindStringLiteral, ast.KindNoSubstitutionTemplateLiteral: - return l.createRangeFromStringLiteralLikeContent(file, contextToken, position) - default: - return l.createLspRangeFromNode(contextToken, file) - } -} - -func (l *LanguageService) createRangeFromStringLiteralLikeContent(file *ast.SourceFile, node *ast.StringLiteralLike, position int) *lsproto.Range { - replacementEnd := node.End() - 1 - nodeStart := astnav.GetStartOfNode(node, file, false /*includeJSDoc*/) - if ast.IsUnterminatedLiteral(node) { - // we return no replacement range only if unterminated string is empty - if nodeStart == replacementEnd { - return nil - } - replacementEnd = min(position, node.End()) - } - return l.createLspRangeFromBounds(nodeStart+1, replacementEnd, file) -} - -func quotePropertyName(file *ast.SourceFile, preferences *UserPreferences, name string) string { - r, _ := utf8.DecodeRuneInString(name) - if unicode.IsDigit(r) { - return name - } - return quote(file, preferences, name) -} - -// Checks whether type is `string & {}`, which is semantically equivalent to string but -// is not reduced by the checker as a special case used for supporting string literal completions -// for string type. -func isStringAndEmptyAnonymousObjectIntersection(typeChecker *checker.Checker, t *checker.Type) bool { - if !t.IsIntersection() { - return false - } - - return len(t.Types()) == 2 && - (areIntersectedTypesAvoidingStringReduction(typeChecker, t.Types()[0], t.Types()[1]) || - areIntersectedTypesAvoidingStringReduction(typeChecker, t.Types()[1], t.Types()[0])) -} - -func areIntersectedTypesAvoidingStringReduction(typeChecker *checker.Checker, t1 *checker.Type, t2 *checker.Type) bool { - return t1.IsString() && typeChecker.IsEmptyAnonymousObjectType(t2) -} - -func escapeSnippetText(text string) string { - return strings.ReplaceAll(text, `$`, `\$`) -} - -func isNamedImportsOrExports(node *ast.Node) bool { - return ast.IsNamedImports(node) || ast.IsNamedExports(node) -} - -func generateIdentifierForArbitraryString(text string) string { - needsUnderscore := false - identifier := "" - var ch rune - var size int - - // Convert "(example, text)" into "_example_text_" - for pos := 0; pos < len(text); pos += size { - ch, size = utf8.DecodeRuneInString(text[pos:]) - var validChar bool - if pos == 0 { - validChar = scanner.IsIdentifierStart(ch) - } else { - validChar = scanner.IsIdentifierPart(ch) - } - if size > 0 && validChar { - if needsUnderscore { - identifier += "_" - } - identifier += string(ch) - needsUnderscore = false - } else { - needsUnderscore = true - } - } - - if needsUnderscore { - identifier += "_" - } - - // Default to "_" if the provided text was empty - if identifier == "" { - return "_" - } - - return identifier -} - -// Copied from vscode TS extension. -func getCompletionsSymbolKind(kind ScriptElementKind) lsproto.CompletionItemKind { - switch kind { - case ScriptElementKindPrimitiveType, ScriptElementKindKeyword: - return lsproto.CompletionItemKindKeyword - case ScriptElementKindConstElement, ScriptElementKindLetElement, ScriptElementKindVariableElement, - ScriptElementKindLocalVariableElement, ScriptElementKindAlias, ScriptElementKindParameterElement: - return lsproto.CompletionItemKindVariable - - case ScriptElementKindMemberVariableElement, ScriptElementKindMemberGetAccessorElement, - ScriptElementKindMemberSetAccessorElement: - return lsproto.CompletionItemKindField - - case ScriptElementKindFunctionElement, ScriptElementKindLocalFunctionElement: - return lsproto.CompletionItemKindFunction - - case ScriptElementKindMemberFunctionElement, ScriptElementKindConstructSignatureElement, - ScriptElementKindCallSignatureElement, ScriptElementKindIndexSignatureElement: - return lsproto.CompletionItemKindMethod - - case ScriptElementKindEnumElement: - return lsproto.CompletionItemKindEnum - - case ScriptElementKindEnumMemberElement: - return lsproto.CompletionItemKindEnumMember - - case ScriptElementKindModuleElement, ScriptElementKindExternalModuleName: - return lsproto.CompletionItemKindModule - - case ScriptElementKindClassElement, ScriptElementKindTypeElement: - return lsproto.CompletionItemKindClass - - case ScriptElementKindInterfaceElement: - return lsproto.CompletionItemKindInterface - - case ScriptElementKindWarning: - return lsproto.CompletionItemKindText - - case ScriptElementKindScriptElement: - return lsproto.CompletionItemKindFile - - case ScriptElementKindDirectory: - return lsproto.CompletionItemKindFolder - - case ScriptElementKindString: - return lsproto.CompletionItemKindConstant - - default: - return lsproto.CompletionItemKindProperty - } -} - -// Editors will use the `sortText` and then fall back to `name` for sorting, but leave ties in response order. -// So, it's important that we sort those ties in the order we want them displayed if it matters. We don't -// strictly need to sort by name or SortText here since clients are going to do it anyway, but we have to -// do the work of comparing them so we can sort those ties appropriately; plus, it makes the order returned -// by the language service consistent with what TS Server does and what editors typically do. This also makes -// completions tests make more sense. We used to sort only alphabetically and only in the server layer, but -// this made tests really weird, since most fourslash tests don't use the server. -func compareCompletionEntries(entryInSlice *lsproto.CompletionItem, entryToInsert *lsproto.CompletionItem) int { - compareStrings := stringutil.CompareStringsCaseInsensitiveThenSensitive - result := compareStrings(*entryInSlice.SortText, *entryToInsert.SortText) - if result == stringutil.ComparisonEqual { - result = compareStrings(entryInSlice.Label, entryToInsert.Label) - } - if result == stringutil.ComparisonEqual && entryInSlice.Data != nil && entryToInsert.Data != nil { - sliceEntryData, ok1 := (*entryInSlice.Data).(*completionEntryData) - insertEntryData, ok2 := (*entryToInsert.Data).(*completionEntryData) - if ok1 && ok2 && sliceEntryData.ModuleSpecifier != "" && insertEntryData.ModuleSpecifier != "" { - // Sort same-named auto-imports by module specifier - result = compareNumberOfDirectorySeparators( - sliceEntryData.ModuleSpecifier, - insertEntryData.ModuleSpecifier, - ) - } - } - if result == stringutil.ComparisonEqual { - // Fall back to symbol order - if we return `EqualTo`, `insertSorted` will put later symbols first. - return stringutil.ComparisonLessThan - } - return result -} - -// True if the first character of `lowercaseCharacters` is the first character -// of some "word" in `identiferString` (where the string is split into "words" -// by camelCase and snake_case segments), then if the remaining characters of -// `lowercaseCharacters` appear, in order, in the rest of `identifierString`.// -// True: -// 'state' in 'useState' -// 'sae' in 'useState' -// 'viable' in 'ENVIRONMENT_VARIABLE'// -// False: -// 'staet' in 'useState' -// 'tate' in 'useState' -// 'ment' in 'ENVIRONMENT_VARIABLE' -func charactersFuzzyMatchInString(identifierString string, lowercaseCharacters string) bool { - if lowercaseCharacters == "" { - return true - } - - var prevChar rune - matchedFirstCharacter := false - characterIndex := 0 - lowerCaseRunes := []rune(lowercaseCharacters) - testChar := lowerCaseRunes[characterIndex] - - for _, strChar := range []rune(identifierString) { - if strChar == testChar || strChar == unicode.ToUpper(testChar) { - willMatchFirstChar := prevChar == 0 || // Beginning of word - 'a' <= prevChar && prevChar <= 'z' && 'A' <= strChar && strChar <= 'Z' || // camelCase transition - prevChar == '_' && strChar != '_' // snake_case transition - matchedFirstCharacter = matchedFirstCharacter || willMatchFirstChar - if !matchedFirstCharacter { - continue - } - characterIndex++ - if characterIndex == len(lowerCaseRunes) { - return true - } else { - testChar = lowerCaseRunes[characterIndex] - } - } - prevChar = strChar - } - - // Did not find all characters - return false -} - -var ( - keywordCompletionsCache = collections.SyncMap[KeywordCompletionFilters, []*lsproto.CompletionItem]{} - allKeywordCompletions = sync.OnceValue(func() []*lsproto.CompletionItem { - result := make([]*lsproto.CompletionItem, 0, ast.KindLastKeyword-ast.KindFirstKeyword+1) - for i := ast.KindFirstKeyword; i <= ast.KindLastKeyword; i++ { - result = append(result, &lsproto.CompletionItem{ - Label: scanner.TokenToString(i), - Kind: ptrTo(lsproto.CompletionItemKindKeyword), - SortText: ptrTo(string(SortTextGlobalsOrKeywords)), - }) - } - return result - }) -) - -func cloneItems(items []*lsproto.CompletionItem) []*lsproto.CompletionItem { - result := make([]*lsproto.CompletionItem, len(items)) - for i, item := range items { - itemClone := *item - result[i] = &itemClone - } - return result -} - -func getKeywordCompletions(keywordFilter KeywordCompletionFilters, filterOutTsOnlyKeywords bool) []*lsproto.CompletionItem { - if !filterOutTsOnlyKeywords { - return cloneItems(getTypescriptKeywordCompletions(keywordFilter)) - } - - index := keywordFilter + KeywordCompletionFiltersLast + 1 - if cached, ok := keywordCompletionsCache.Load(index); ok { - return cloneItems(cached) - } - result := core.Filter( - getTypescriptKeywordCompletions(keywordFilter), - func(ci *lsproto.CompletionItem) bool { - return !isTypeScriptOnlyKeyword(scanner.StringToToken(ci.Label)) - }) - keywordCompletionsCache.Store(index, result) - return cloneItems(result) -} - -func getTypescriptKeywordCompletions(keywordFilter KeywordCompletionFilters) []*lsproto.CompletionItem { - if cached, ok := keywordCompletionsCache.Load(keywordFilter); ok { - return cached - } - result := core.Filter(allKeywordCompletions(), func(entry *lsproto.CompletionItem) bool { - kind := scanner.StringToToken(entry.Label) - switch keywordFilter { - case KeywordCompletionFiltersNone: - return false - case KeywordCompletionFiltersAll: - return isFunctionLikeBodyKeyword(kind) || - kind == ast.KindDeclareKeyword || - kind == ast.KindModuleKeyword || - kind == ast.KindTypeKeyword || - kind == ast.KindNamespaceKeyword || - kind == ast.KindAbstractKeyword || - isTypeKeyword(kind) && kind != ast.KindUndefinedKeyword - case KeywordCompletionFiltersFunctionLikeBodyKeywords: - return isFunctionLikeBodyKeyword(kind) - case KeywordCompletionFiltersClassElementKeywords: - return isClassMemberCompletionKeyword(kind) - case KeywordCompletionFiltersInterfaceElementKeywords: - return isInterfaceOrTypeLiteralCompletionKeyword(kind) - case KeywordCompletionFiltersConstructorParameterKeywords: - return ast.IsParameterPropertyModifier(kind) - case KeywordCompletionFiltersTypeAssertionKeywords: - return isTypeKeyword(kind) || kind == ast.KindConstKeyword - case KeywordCompletionFiltersTypeKeywords: - return isTypeKeyword(kind) - case KeywordCompletionFiltersTypeKeyword: - return kind == ast.KindTypeKeyword - default: - panic(fmt.Sprintf("Unknown keyword filter: %v", keywordFilter)) - } - }) - - keywordCompletionsCache.Store(keywordFilter, result) - return result -} - -func isTypeScriptOnlyKeyword(kind ast.Kind) bool { - switch kind { - case ast.KindAbstractKeyword, - ast.KindAnyKeyword, - ast.KindBigIntKeyword, - ast.KindBooleanKeyword, - ast.KindDeclareKeyword, - ast.KindEnumKeyword, - ast.KindGlobalKeyword, - ast.KindImplementsKeyword, - ast.KindInferKeyword, - ast.KindInterfaceKeyword, - ast.KindIsKeyword, - ast.KindKeyOfKeyword, - ast.KindModuleKeyword, - ast.KindNamespaceKeyword, - ast.KindNeverKeyword, - ast.KindNumberKeyword, - ast.KindObjectKeyword, - ast.KindOverrideKeyword, - ast.KindPrivateKeyword, - ast.KindProtectedKeyword, - ast.KindPublicKeyword, - ast.KindReadonlyKeyword, - ast.KindStringKeyword, - ast.KindSymbolKeyword, - ast.KindTypeKeyword, - ast.KindUniqueKeyword, - ast.KindUnknownKeyword: - return true - default: - return false - } -} - -func isFunctionLikeBodyKeyword(kind ast.Kind) bool { - return kind == ast.KindAsyncKeyword || - kind == ast.KindAwaitKeyword || - kind == ast.KindUsingKeyword || - kind == ast.KindAsKeyword || - kind == ast.KindSatisfiesKeyword || - kind == ast.KindTypeKeyword || - !ast.IsContextualKeyword(kind) && !isClassMemberCompletionKeyword(kind) -} - -func isClassMemberCompletionKeyword(kind ast.Kind) bool { - switch kind { - case ast.KindAbstractKeyword, ast.KindAccessorKeyword, ast.KindConstructorKeyword, ast.KindGetKeyword, - ast.KindSetKeyword, ast.KindAsyncKeyword, ast.KindDeclareKeyword, ast.KindOverrideKeyword: - return true - default: - return ast.IsClassMemberModifier(kind) - } -} - -func isInterfaceOrTypeLiteralCompletionKeyword(kind ast.Kind) bool { - return kind == ast.KindReadonlyKeyword -} - -func isContextualKeywordInAutoImportableExpressionSpace(keyword string) bool { - return keyword == "abstract" || - keyword == "async" || - keyword == "await" || - keyword == "declare" || - keyword == "module" || - keyword == "namespace" || - keyword == "type" || - keyword == "satisfies" || - keyword == "as" -} - -func getContextualKeywords(file *ast.SourceFile, contextToken *ast.Node, position int) []*lsproto.CompletionItem { - var entries []*lsproto.CompletionItem - // An `AssertClause` can come after an import declaration: - // import * from "foo" | - // import "foo" | - // or after a re-export declaration that has a module specifier: - // export { foo } from "foo" | - // Source: https://tc39.es/proposal-import-assertions/ - if contextToken != nil { - parent := contextToken.Parent - tokenLine, _ := scanner.GetECMALineAndCharacterOfPosition(file, contextToken.End()) - currentLine, _ := scanner.GetECMALineAndCharacterOfPosition(file, position) - if (ast.IsImportDeclaration(parent) || - ast.IsExportDeclaration(parent) && parent.AsExportDeclaration().ModuleSpecifier != nil) && - contextToken == parent.ModuleSpecifier() && - tokenLine == currentLine { - entries = append(entries, &lsproto.CompletionItem{ - Label: scanner.TokenToString(ast.KindAssertKeyword), - Kind: ptrTo(lsproto.CompletionItemKindKeyword), - SortText: ptrTo(string(SortTextGlobalsOrKeywords)), - }) - } - } - return entries -} - -func (l *LanguageService) getJSCompletionEntries( - ctx context.Context, - file *ast.SourceFile, - position int, - uniqueNames *collections.Set[string], - sortedEntries []*lsproto.CompletionItem, -) []*lsproto.CompletionItem { - nameTable := getNameTable(file) - for name, pos := range nameTable { - // Skip identifiers produced only from the current location - if pos == position { - continue - } - if !uniqueNames.Has(name) && scanner.IsIdentifierText(name, core.LanguageVariantStandard) { - uniqueNames.Add(name) - sortedEntries = core.InsertSorted( - sortedEntries, - &lsproto.CompletionItem{ - Label: name, - Kind: ptrTo(lsproto.CompletionItemKindText), - SortText: ptrTo(string(SortTextJavascriptIdentifiers)), - CommitCharacters: ptrTo([]string{}), - }, - compareCompletionEntries, - ) - } - } - return sortedEntries -} - -func (l *LanguageService) getOptionalReplacementSpan(location *ast.Node, file *ast.SourceFile) *lsproto.Range { - // StringLiteralLike locations are handled separately in stringCompletions.ts - if location != nil && (location.Kind == ast.KindIdentifier || location.Kind == ast.KindPrivateIdentifier) { - start := astnav.GetStartOfNode(location, file, false /*includeJSDoc*/) - return l.createLspRangeFromBounds(start, location.End(), file) - } - return nil -} - -func isMemberCompletionKind(kind CompletionKind) bool { - return kind == CompletionKindObjectPropertyDeclaration || - kind == CompletionKindMemberLike || - kind == CompletionKindPropertyAccess -} - -func tryGetFunctionLikeBodyCompletionContainer(contextToken *ast.Node) *ast.Node { - if contextToken == nil { - return nil - } - - var prev *ast.Node - container := ast.FindAncestorOrQuit(contextToken, func(node *ast.Node) ast.FindAncestorResult { - if ast.IsClassLike(node) { - return ast.FindAncestorQuit - } - if ast.IsFunctionLikeDeclaration(node) && prev == node.Body() { - return ast.FindAncestorTrue - } - prev = node - return ast.FindAncestorFalse - }) - return container -} - -func computeCommitCharactersAndIsNewIdentifier( - contextToken *ast.Node, - file *ast.SourceFile, - position int, -) (isNewIdentifierLocation bool, defaultCommitCharacters []string) { - if contextToken == nil { - return false, allCommitCharacters - } - containingNodeKind := contextToken.Parent.Kind - tokenKind := keywordForNode(contextToken) - // Previous token may have been a keyword that was converted to an identifier. - switch tokenKind { - case ast.KindCommaToken: - switch containingNodeKind { - // func( a, | - // new C(a, | - case ast.KindCallExpression, ast.KindNewExpression: - expression := contextToken.Parent.Expression() - // func\n(a, | - if getLineOfPosition(file, expression.End()) != getLineOfPosition(file, position) { - return true, noCommaCommitCharacters - } - return true, allCommitCharacters - // const x = (a, | - case ast.KindBinaryExpression: - return true, noCommaCommitCharacters - // constructor( a, | /* public, protected, private keywords are allowed here, so show completion */ - // var x: (s: string, list| - // const obj = { x, | - case ast.KindConstructor, ast.KindFunctionType, ast.KindObjectLiteralExpression: - return true, emptyCommitCharacters - // [a, | - case ast.KindArrayLiteralExpression: - return true, allCommitCharacters - default: - return false, allCommitCharacters - } - case ast.KindOpenParenToken: - switch containingNodeKind { - // func( | - // new C(a| - case ast.KindCallExpression, ast.KindNewExpression: - expression := contextToken.Parent.Expression() - // func\n( | - if getLineOfPosition(file, expression.End()) != getLineOfPosition(file, position) { - return true, noCommaCommitCharacters - } - return true, allCommitCharacters - // const x = (a| - case ast.KindParenthesizedExpression: - return true, noCommaCommitCharacters - // constructor( | - // function F(pred: (a| /* this can become an arrow function, where 'a' is the argument */ - case ast.KindConstructor, ast.KindParenthesizedType: - return true, emptyCommitCharacters - default: - return false, allCommitCharacters - } - case ast.KindOpenBracketToken: - switch containingNodeKind { - // [ | - // [ | : string ] - // [ | : string ] - // [ | /* this can become an index signature */ - case ast.KindArrayLiteralExpression, ast.KindIndexSignature, ast.KindTupleType, ast.KindComputedPropertyName: - return true, allCommitCharacters - default: - return false, allCommitCharacters - } - // module | - // namespace | - // import | - case ast.KindModuleKeyword, ast.KindNamespaceKeyword, ast.KindImportKeyword: - return true, emptyCommitCharacters - case ast.KindDotToken: - switch containingNodeKind { - // module A.| - case ast.KindModuleDeclaration: - return true, emptyCommitCharacters - default: - return false, allCommitCharacters - } - case ast.KindOpenBraceToken: - switch containingNodeKind { - // class A { | - // const obj = { | - case ast.KindClassDeclaration, ast.KindObjectLiteralExpression: - return true, emptyCommitCharacters - default: - return false, allCommitCharacters - } - case ast.KindEqualsToken: - switch containingNodeKind { - // const x = a| - // x = a| - case ast.KindVariableDeclaration, ast.KindBinaryExpression: - return true, allCommitCharacters - default: - return false, allCommitCharacters - } - case ast.KindTemplateHead: - // `aa ${| - return containingNodeKind == ast.KindTemplateExpression, allCommitCharacters - case ast.KindTemplateMiddle: - // `aa ${10} dd ${| - return containingNodeKind == ast.KindTemplateSpan, allCommitCharacters - case ast.KindAsyncKeyword: - // const obj = { async c|() - // const obj = { async c| - if containingNodeKind == ast.KindMethodDeclaration || containingNodeKind == ast.KindShorthandPropertyAssignment { - return true, emptyCommitCharacters - } - return false, allCommitCharacters - case ast.KindAsteriskToken: - // const obj = { * c| - if containingNodeKind == ast.KindMethodDeclaration { - return true, emptyCommitCharacters - } - return false, allCommitCharacters - } - - if isClassMemberCompletionKeyword(tokenKind) { - return true, emptyCommitCharacters - } - - return false, allCommitCharacters -} - -func keywordForNode(node *ast.Node) ast.Kind { - if ast.IsIdentifier(node) { - return scanner.IdentifierToKeywordKind(node.AsIdentifier()) - } - return node.Kind -} - -// Finds the first node that "embraces" the position, so that one may -// accurately aggregate locals from the closest containing scope. -func getScopeNode(initialToken *ast.Node, position int, file *ast.SourceFile) *ast.Node { - scope := initialToken - for scope != nil && !positionBelongsToNode(scope, position, file) { - scope = scope.Parent - } - return scope -} - -func isSnippetScope(scopeNode *ast.Node) bool { - switch scopeNode.Kind { - case ast.KindSourceFile, - ast.KindTemplateExpression, - ast.KindJsxExpression, - ast.KindBlock: - return true - default: - return ast.IsStatement(scopeNode) - } -} - -// Determines if a type is exactly the same type resolved by the global 'self', 'global', or 'globalThis'. -func isProbablyGlobalType(t *checker.Type, file *ast.SourceFile, typeChecker *checker.Checker) bool { - // The type of `self` and `window` is the same in lib.dom.d.ts, but `window` does not exist in - // lib.webworker.d.ts, so checking against `self` is also a check against `window` when it exists. - selfSymbol := typeChecker.GetGlobalSymbol("self", ast.SymbolFlagsValue, nil /*diagnostic*/) - if selfSymbol != nil && typeChecker.GetTypeOfSymbolAtLocation(selfSymbol, file.AsNode()) == t { - return true - } - globalSymbol := typeChecker.GetGlobalSymbol("global", ast.SymbolFlagsValue, nil /*diagnostic*/) - if globalSymbol != nil && typeChecker.GetTypeOfSymbolAtLocation(globalSymbol, file.AsNode()) == t { - return true - } - globalThisSymbol := typeChecker.GetGlobalSymbol("globalThis", ast.SymbolFlagsValue, nil /*diagnostic*/) - if globalThisSymbol != nil && typeChecker.GetTypeOfSymbolAtLocation(globalThisSymbol, file.AsNode()) == t { - return true - } - return false -} - -func tryGetTypeLiteralNode(node *ast.Node) *ast.TypeLiteral { - if node == nil { - return nil - } - - parent := node.Parent - switch node.Kind { - case ast.KindOpenBraceToken: - if ast.IsTypeLiteralNode(parent) { - return parent - } - case ast.KindSemicolonToken, ast.KindCommaToken, ast.KindIdentifier: - if parent.Kind == ast.KindPropertySignature && ast.IsTypeLiteralNode(parent.Parent) { - return parent.Parent - } - } - - return nil -} - -func getConstraintOfTypeArgumentProperty(node *ast.Node, typeChecker *checker.Checker) *checker.Type { - if node == nil { - return nil - } - - if ast.IsTypeNode(node) && ast.IsTypeReferenceType(node.Parent) { - return typeChecker.GetTypeArgumentConstraint(node) - } - - t := getConstraintOfTypeArgumentProperty(node.Parent, typeChecker) - if t == nil { - return nil - } - - switch node.Kind { - case ast.KindPropertySignature: - return typeChecker.GetTypeOfPropertyOfContextualType(t, node.Symbol().Name) - case ast.KindIntersectionType, ast.KindTypeLiteral, ast.KindUnionType: - return t - } - - return nil -} - -func tryGetObjectLikeCompletionContainer(contextToken *ast.Node, position int, file *ast.SourceFile) *ast.ObjectLiteralLike { - if contextToken == nil { - return nil - } - - parent := contextToken.Parent - switch contextToken.Kind { - // const x = { | - // const x = { a: 0, | - case ast.KindOpenBraceToken, ast.KindCommaToken: - if ast.IsObjectLiteralExpression(parent) || ast.IsObjectBindingPattern(parent) { - return parent - } - case ast.KindAsteriskToken: - if ast.IsMethodDeclaration(parent) && ast.IsObjectLiteralExpression(parent.Parent) { - return parent.Parent - } - case ast.KindAsyncKeyword: - if ast.IsObjectLiteralExpression(parent.Parent) { - return parent.Parent - } - case ast.KindIdentifier: - if contextToken.Text() == "async" && ast.IsShorthandPropertyAssignment(parent) { - return parent.Parent - } else { - if ast.IsObjectLiteralExpression(parent.Parent) && - (ast.IsSpreadAssignment(parent) || - ast.IsShorthandPropertyAssignment(parent) && - getLineOfPosition(file, contextToken.End()) != getLineOfPosition(file, position)) { - return parent.Parent - } - ancestorNode := ast.FindAncestor(parent, ast.IsPropertyAssignment) - if ancestorNode != nil && lsutil.GetLastToken(ancestorNode, file) == contextToken && ast.IsObjectLiteralExpression(ancestorNode.Parent) { - return ancestorNode.Parent - } - } - default: - if parent.Parent != nil && parent.Parent.Parent != nil && - (ast.IsMethodDeclaration(parent.Parent) || - ast.IsGetAccessorDeclaration(parent.Parent) || - ast.IsSetAccessorDeclaration(parent.Parent)) && - ast.IsObjectLiteralExpression(parent.Parent.Parent) { - return parent.Parent.Parent - } - if ast.IsSpreadAssignment(parent) && ast.IsObjectLiteralExpression(parent.Parent) { - return parent.Parent - } - ancestorNode := ast.FindAncestor(parent, ast.IsPropertyAssignment) - if contextToken.Kind != ast.KindColonToken && - ancestorNode != nil && lsutil.GetLastToken(ancestorNode, file) == contextToken && - ast.IsObjectLiteralExpression(ancestorNode.Parent) { - return ancestorNode.Parent - } - } - - return nil -} - -func tryGetObjectLiteralContextualType(node *ast.ObjectLiteralExpressionNode, typeChecker *checker.Checker) *checker.Type { - t := typeChecker.GetContextualType(node, checker.ContextFlagsNone) - if t != nil { - return t - } - - parent := ast.WalkUpParenthesizedExpressions(node.Parent) - if ast.IsBinaryExpression(parent) && - parent.AsBinaryExpression().OperatorToken.Kind == ast.KindEqualsToken && - node == parent.AsBinaryExpression().Left { - // Object literal is assignment pattern: ({ | } = x) - return typeChecker.GetTypeAtLocation(parent) - } - if ast.IsExpression(parent) { - // f(() => (({ | }))); - return typeChecker.GetContextualType(parent, checker.ContextFlagsNone) - } - - return nil -} - -func getPropertiesForObjectExpression( - contextualType *checker.Type, - completionsType *checker.Type, - obj *ast.Node, - typeChecker *checker.Checker, -) []*ast.Symbol { - hasCompletionsType := completionsType != nil && completionsType != contextualType - var types []*checker.Type - if contextualType.IsUnion() { - types = contextualType.Types() - } else { - types = []*checker.Type{contextualType} - } - promiseFilteredContextualType := typeChecker.GetUnionType(core.Filter(types, func(t *checker.Type) bool { - return typeChecker.GetPromisedTypeOfPromise(t) == nil - })) - - var t *checker.Type - if hasCompletionsType && completionsType.Flags()&checker.TypeFlagsAnyOrUnknown == 0 { - t = typeChecker.GetUnionType([]*checker.Type{promiseFilteredContextualType, completionsType}) - } else { - t = promiseFilteredContextualType - } - - // Filter out members whose only declaration is the object literal itself to avoid - // self-fulfilling completions like: - // - // function f(x: T) {} - // f({ abc/**/: "" }) // `abc` is a member of `T` but only because it declares itself - hasDeclarationOtherThanSelf := func(member *ast.Symbol) bool { - if len(member.Declarations) == 0 { - return true - } - return core.Some(member.Declarations, func(decl *ast.Declaration) bool { return decl.Parent != obj }) - } - - properties := getApparentProperties(t, obj, typeChecker) - if t.IsClass() && containsNonPublicProperties(properties) { - return nil - } else if hasCompletionsType { - return core.Filter(properties, hasDeclarationOtherThanSelf) - } else { - return properties - } -} - -func getApparentProperties(t *checker.Type, node *ast.Node, typeChecker *checker.Checker) []*ast.Symbol { - if !t.IsUnion() { - return typeChecker.GetApparentProperties(t) - } - return typeChecker.GetAllPossiblePropertiesOfTypes(core.Filter(t.Types(), func(memberType *checker.Type) bool { - return !(memberType.Flags()&checker.TypeFlagsPrimitive != 0 || - typeChecker.IsArrayLikeType(memberType) || - typeChecker.IsTypeInvalidDueToUnionDiscriminant(memberType, node) || - typeChecker.TypeHasCallOrConstructSignatures(memberType) || - memberType.IsClass() && containsNonPublicProperties(typeChecker.GetApparentProperties(memberType))) - })) -} - -func containsNonPublicProperties(props []*ast.Symbol) bool { - return core.Some(props, func(p *ast.Symbol) bool { - return checker.GetDeclarationModifierFlagsFromSymbol(p)&ast.ModifierFlagsNonPublicAccessibilityModifier != 0 - }) -} - -// Filters out members that are already declared in the object literal or binding pattern. -// Also computes the set of existing members declared by spread assignment. -func filterObjectMembersList( - contextualMemberSymbols []*ast.Symbol, - existingMembers []*ast.Declaration, - file *ast.SourceFile, - position int, - typeChecker *checker.Checker, -) (filteredMembers []*ast.Symbol, spreadMemberNames collections.Set[string]) { - if len(existingMembers) == 0 { - return contextualMemberSymbols, collections.Set[string]{} - } - - membersDeclaredBySpreadAssignment := collections.Set[string]{} - existingMemberNames := collections.Set[string]{} - for _, member := range existingMembers { - // Ignore omitted expressions for missing members. - if member.Kind != ast.KindPropertyAssignment && - member.Kind != ast.KindShorthandPropertyAssignment && - member.Kind != ast.KindBindingElement && - member.Kind != ast.KindMethodDeclaration && - member.Kind != ast.KindGetAccessor && - member.Kind != ast.KindSetAccessor && - member.Kind != ast.KindSpreadAssignment { - continue - } - - // If this is the current item we are editing right now, do not filter it out. - if isCurrentlyEditingNode(member, file, position) { - continue - } - - var existingName string - - if ast.IsSpreadAssignment(member) { - setMemberDeclaredBySpreadAssignment(member, &membersDeclaredBySpreadAssignment, typeChecker) - } else if ast.IsBindingElement(member) && member.AsBindingElement().PropertyName != nil { - // include only identifiers in completion list - if member.AsBindingElement().PropertyName.Kind == ast.KindIdentifier { - existingName = member.AsBindingElement().PropertyName.Text() - } - } else { - // TODO: Account for computed property name - // NOTE: if one only performs this step when m.name is an identifier, - // things like '__proto__' are not filtered out. - name := ast.GetNameOfDeclaration(member) - if name != nil && ast.IsPropertyNameLiteral(name) { - existingName = name.Text() - } - } - - if existingName != "" { - existingMemberNames.Add(existingName) - } - } - - filteredSymbols := core.Filter(contextualMemberSymbols, func(m *ast.Symbol) bool { - return !existingMemberNames.Has(m.Name) - }) - - return filteredSymbols, membersDeclaredBySpreadAssignment -} - -func isCurrentlyEditingNode(node *ast.Node, file *ast.SourceFile, position int) bool { - start := astnav.GetStartOfNode(node, file, false /*includeJSDoc*/) - return start <= position && position <= node.End() -} - -func setMemberDeclaredBySpreadAssignment(declaration *ast.Node, members *collections.Set[string], typeChecker *checker.Checker) { - expression := declaration.Expression() - symbol := typeChecker.GetSymbolAtLocation(expression) - var t *checker.Type - if symbol != nil { - t = typeChecker.GetTypeOfSymbolAtLocation(symbol, expression) - } - var properties []*ast.Symbol - if t != nil { - properties = t.AsStructuredType().Properties() - } - for _, property := range properties { - members.Add(property.Name) - } -} - -// Returns the immediate owning class declaration of a context token, -// on the condition that one exists and that the context implies completion should be given. -func tryGetConstructorLikeCompletionContainer(contextToken *ast.Node) *ast.ConstructorDeclarationNode { - if contextToken == nil { - return nil - } - - parent := contextToken.Parent - switch contextToken.Kind { - case ast.KindOpenParenToken, ast.KindCommaToken: - if ast.IsConstructorDeclaration(parent) { - return parent - } - return nil - default: - if isConstructorParameterCompletion(contextToken) { - return parent.Parent - } - } - return nil -} - -func isConstructorParameterCompletion(node *ast.Node) bool { - return node.Parent != nil && ast.IsParameter(node.Parent) && ast.IsConstructorDeclaration(node.Parent.Parent) && - (ast.IsParameterPropertyModifier(node.Kind) || ast.IsDeclarationName(node)) -} - -// Returns the immediate owning class declaration of a context token, -// on the condition that one exists and that the context implies completion should be given. -func tryGetObjectTypeDeclarationCompletionContainer( - file *ast.SourceFile, - contextToken *ast.Node, - location *ast.Node, - position int, -) *ast.ObjectTypeDeclaration { - // class c { method() { } | method2() { } } - switch location.Kind { - case ast.KindSyntaxList: - if ast.IsObjectTypeDeclaration(location.Parent) { - return location.Parent - } - return nil - case ast.KindEndOfFile: - stmtList := location.Parent.AsSourceFile().Statements - if stmtList != nil && len(stmtList.Nodes) > 0 && ast.IsObjectTypeDeclaration(stmtList.Nodes[len(stmtList.Nodes)-1]) { - cls := stmtList.Nodes[len(stmtList.Nodes)-1] - if findChildOfKind(cls, ast.KindCloseBraceToken, file) == nil { - return cls - } - } - case ast.KindPrivateIdentifier: - if ast.IsPropertyDeclaration(location.Parent) { - return ast.FindAncestor(location, ast.IsClassLike) - } - case ast.KindIdentifier: - originalKeywordKind := scanner.IdentifierToKeywordKind(location.AsIdentifier()) - if originalKeywordKind != ast.KindUnknown { - return nil - } - // class c { public prop = c| } - if ast.IsPropertyDeclaration(location.Parent) && location.Parent.Initializer() == location { - return nil - } - // class c extends React.Component { a: () => 1\n compon| } - if isFromObjectTypeDeclaration(location) { - return ast.FindAncestor(location, ast.IsObjectTypeDeclaration) - } - } - - if contextToken == nil { - return nil - } - - // class C { blah; constructor/**/ } - // or - // class C { blah \n constructor/**/ } - if location.Kind == ast.KindConstructorKeyword || - (ast.IsIdentifier(contextToken) && ast.IsPropertyDeclaration(contextToken.Parent) && ast.IsClassLike(location)) { - return ast.FindAncestor(contextToken, ast.IsClassLike) - } - - switch contextToken.Kind { - // class c { public prop = | /* global completions */ } - case ast.KindEqualsToken: - return nil - // class c {getValue(): number; | } - // class c { method() { } | } - case ast.KindSemicolonToken, ast.KindCloseBraceToken: - // class c { method() { } b| } - if isFromObjectTypeDeclaration(location) && location.Parent.Name() == location { - return location.Parent.Parent - } - if ast.IsObjectTypeDeclaration(location) { - return location - } - return nil - // class c { | - // class c {getValue(): number, | } - case ast.KindOpenBraceToken, ast.KindCommaToken: - if ast.IsObjectTypeDeclaration(contextToken.Parent) { - return contextToken.Parent - } - return nil - default: - if ast.IsObjectTypeDeclaration(location) { - // class C extends React.Component { a: () => 1\n| } - // class C { prop = ""\n | } - if getLineOfPosition(file, contextToken.End()) != getLineOfPosition(file, position) { - return location - } - isValidKeyword := core.IfElse( - ast.IsClassLike(contextToken.Parent.Parent), - isClassMemberCompletionKeyword, - isInterfaceOrTypeLiteralCompletionKeyword, - ) - - if isValidKeyword(contextToken.Kind) || contextToken.Kind == ast.KindAsteriskToken || - ast.IsIdentifier(contextToken) && isValidKeyword(scanner.IdentifierToKeywordKind(contextToken.AsIdentifier())) { - return contextToken.Parent.Parent - } - } - - return nil - } -} - -func isFromObjectTypeDeclaration(node *ast.Node) bool { - return node.Parent != nil && ast.IsClassOrTypeElement(node.Parent) && ast.IsObjectTypeDeclaration(node.Parent.Parent) -} - -// Filters out completion suggestions for class elements. -func filterClassMembersList( - baseSymbols []*ast.Symbol, - existingMembers []*ast.ClassElement, - classElementModifierFlags ast.ModifierFlags, - file *ast.SourceFile, - position int, -) []*ast.Symbol { - existingMemberNames := collections.Set[string]{} - for _, member := range existingMembers { - // Ignore omitted expressions for missing members. - if member.Kind != ast.KindPropertyDeclaration && - member.Kind != ast.KindMethodDeclaration && - member.Kind != ast.KindGetAccessor && - member.Kind != ast.KindSetAccessor { - continue - } - - // If this is the current item we are editing right now, do not filter it out - if isCurrentlyEditingNode(member, file, position) { - continue - } - - // Don't filter member even if the name matches if it is declared private in the list. - if member.ModifierFlags()&ast.ModifierFlagsPrivate != 0 { - continue - } - - // Do not filter it out if the static presence doesn't match. - if ast.IsStatic(member) != (classElementModifierFlags&ast.ModifierFlagsStatic != 0) { - continue - } - - existingName := ast.GetPropertyNameForPropertyNameNode(member.Name()) - if existingName != "" { - existingMemberNames.Add(existingName) - } - } - - return core.Filter(baseSymbols, func(propertySymbol *ast.Symbol) bool { - return !existingMemberNames.Has(ast.SymbolName(propertySymbol)) && - len(propertySymbol.Declarations) > 0 && - checker.GetDeclarationModifierFlagsFromSymbol(propertySymbol)&ast.ModifierFlagsPrivate == 0 && - !(propertySymbol.ValueDeclaration != nil && ast.IsPrivateIdentifierClassElementDeclaration(propertySymbol.ValueDeclaration)) - }) -} - -func tryGetContainingJsxElement(contextToken *ast.Node, file *ast.SourceFile) *ast.JsxOpeningLikeElement { - if contextToken == nil { - return nil - } - - parent := contextToken.Parent - switch contextToken.Kind { - case ast.KindGreaterThanToken, ast.KindLessThanSlashToken, ast.KindSlashToken, ast.KindIdentifier, - ast.KindPropertyAccessExpression, ast.KindJsxAttributes, ast.KindJsxAttribute, ast.KindJsxSpreadAttribute: - if parent != nil && (parent.Kind == ast.KindJsxSelfClosingElement || parent.Kind == ast.KindJsxOpeningElement) { - if contextToken.Kind == ast.KindGreaterThanToken { - precedingToken := astnav.FindPrecedingToken(file, contextToken.Pos()) - if len(parent.TypeArguments()) == 0 || - precedingToken != nil && precedingToken.Kind == ast.KindSlashToken { - return nil - } - } - return parent - } else if parent != nil && parent.Kind == ast.KindJsxAttribute { - // Currently we parse JsxOpeningLikeElement as: - // JsxOpeningLikeElement - // attributes: JsxAttributes - // properties: NodeArray - return parent.Parent.Parent - } - // The context token is the closing } or " of an attribute, which means - // its parent is a JsxExpression, whose parent is a JsxAttribute, - // whose parent is a JsxOpeningLikeElement - case ast.KindStringLiteral: - if parent != nil && (parent.Kind == ast.KindJsxAttribute || parent.Kind == ast.KindJsxSpreadAttribute) { - // Currently we parse JsxOpeningLikeElement as: - // JsxOpeningLikeElement - // attributes: JsxAttributes - // properties: NodeArray - return parent.Parent.Parent - } - case ast.KindCloseBraceToken: - if parent != nil && parent.Kind == ast.KindJsxExpression && - parent.Parent != nil && parent.Parent.Kind == ast.KindJsxAttribute { - // Currently we parse JsxOpeningLikeElement as: - // JsxOpeningLikeElement - // attributes: JsxAttributes - // properties: NodeArray - // each JsxAttribute can have initializer as JsxExpression - return parent.Parent.Parent.Parent - } - if parent != nil && parent.Kind == ast.KindJsxSpreadAttribute { - // Currently we parse JsxOpeningLikeElement as: - // JsxOpeningLikeElement - // attributes: JsxAttributes - // properties: NodeArray - return parent.Parent.Parent - } - } - - return nil -} - -// Filters out completion suggestions from 'symbols' according to existing JSX attributes. -// @returns Symbols to be suggested in a JSX element, barring those whose attributes -// do not occur at the current position and have not otherwise been typed. -func filterJsxAttributes( - symbols []*ast.Symbol, - attributes []*ast.JsxAttributeLike, - file *ast.SourceFile, - position int, - typeChecker *checker.Checker, -) (filteredMembers []*ast.Symbol, spreadMemberNames *collections.Set[string]) { - existingNames := collections.Set[string]{} - membersDeclaredBySpreadAssignment := collections.Set[string]{} - for _, attr := range attributes { - // If this is the item we are editing right now, do not filter it out. - if isCurrentlyEditingNode(attr, file, position) { - continue - } - - if attr.Kind == ast.KindJsxAttribute { - existingNames.Add(attr.Name().Text()) - } else if ast.IsJsxSpreadAttribute(attr) { - setMemberDeclaredBySpreadAssignment(attr, &membersDeclaredBySpreadAssignment, typeChecker) - } - } - - return core.Filter(symbols, func(a *ast.Symbol) bool { return !existingNames.Has(a.Name) }), - &membersDeclaredBySpreadAssignment -} - -func isTypeKeywordTokenOrIdentifier(node *ast.Node) bool { - return ast.IsTypeKeywordToken(node) || - ast.IsIdentifier(node) && scanner.IdentifierToKeywordKind(node.AsIdentifier()) == ast.KindTypeKeyword -} - -// Returns the item defaults for completion items, if that capability is supported. -// Otherwise, if some item default is not supported by client, sets that property on each item. -func (l *LanguageService) setItemDefaults( - clientOptions *lsproto.CompletionClientCapabilities, - position int, - file *ast.SourceFile, - items []*lsproto.CompletionItem, - defaultCommitCharacters *[]string, - optionalReplacementSpan *lsproto.Range, -) *lsproto.CompletionItemDefaults { - var itemDefaults *lsproto.CompletionItemDefaults - if defaultCommitCharacters != nil { - supportsItemCommitCharacters := clientSupportsItemCommitCharacters(clientOptions) - if clientSupportsDefaultCommitCharacters(clientOptions) && supportsItemCommitCharacters { - itemDefaults = &lsproto.CompletionItemDefaults{ - CommitCharacters: defaultCommitCharacters, - } - } else if supportsItemCommitCharacters { - for _, item := range items { - if item.CommitCharacters == nil { - item.CommitCharacters = defaultCommitCharacters - } - } - } - } - if optionalReplacementSpan != nil { - // Ported from vscode ts extension. - insertRange := lsproto.Range{ - Start: optionalReplacementSpan.Start, - End: l.createLspPosition(position, file), - } - if clientSupportsDefaultEditRange(clientOptions) { - itemDefaults = core.OrElse(itemDefaults, &lsproto.CompletionItemDefaults{}) - itemDefaults.EditRange = &lsproto.RangeOrEditRangeWithInsertReplace{ - EditRangeWithInsertReplace: &lsproto.EditRangeWithInsertReplace{ - Insert: insertRange, - Replace: *optionalReplacementSpan, - }, - } - for _, item := range items { - // If `editRange` is set, `insertText` is ignored by the client, so we need to - // provide `textEdit` instead. - if item.InsertText != nil && item.TextEdit == nil { - item.TextEdit = &lsproto.TextEditOrInsertReplaceEdit{ - InsertReplaceEdit: &lsproto.InsertReplaceEdit{ - NewText: *item.InsertText, - Insert: insertRange, - Replace: *optionalReplacementSpan, - }, - } - item.InsertText = nil - } - } - } else if clientSupportsItemInsertReplace(clientOptions) { - for _, item := range items { - if item.TextEdit == nil { - item.TextEdit = &lsproto.TextEditOrInsertReplaceEdit{ - InsertReplaceEdit: &lsproto.InsertReplaceEdit{ - NewText: *core.OrElse(item.InsertText, &item.Label), - Insert: insertRange, - Replace: *optionalReplacementSpan, - }, - } - } - } - } - } - - return itemDefaults -} - -func (l *LanguageService) specificKeywordCompletionInfo( - clientOptions *lsproto.CompletionClientCapabilities, - position int, - file *ast.SourceFile, - items []*lsproto.CompletionItem, - isNewIdentifierLocation bool, - optionalReplacementSpan *lsproto.Range, -) *lsproto.CompletionList { - defaultCommitCharacters := getDefaultCommitCharacters(isNewIdentifierLocation) - itemDefaults := l.setItemDefaults( - clientOptions, - position, - file, - items, - &defaultCommitCharacters, - optionalReplacementSpan, - ) - return &lsproto.CompletionList{ - IsIncomplete: false, - ItemDefaults: itemDefaults, - Items: items, - } -} - -func (l *LanguageService) getJsxClosingTagCompletion( - location *ast.Node, - file *ast.SourceFile, - position int, - clientOptions *lsproto.CompletionClientCapabilities, -) *lsproto.CompletionList { - // We wanna walk up the tree till we find a JSX closing element. - jsxClosingElement := ast.FindAncestorOrQuit(location, func(node *ast.Node) ast.FindAncestorResult { - switch node.Kind { - case ast.KindJsxClosingElement: - return ast.FindAncestorTrue - case ast.KindLessThanSlashToken, ast.KindGreaterThanToken, ast.KindIdentifier, ast.KindPropertyAccessExpression: - return ast.FindAncestorFalse - default: - return ast.FindAncestorQuit - } - }) - - if jsxClosingElement == nil { - return nil - } - - // In the TypeScript JSX element, if such element is not defined. When users query for completion at closing tag, - // instead of simply giving unknown value, the completion will return the tag-name of an associated opening-element. - // For example: - // var x =
" with type any - // And at `
` (with a closing `>`), the completion list will contain "div". - // And at property access expressions ` ` the completion will - // return full closing tag with an optional replacement span - // For example: - // var x = - // var y = - // the completion list at "1" and "2" will contain "MainComponent.Child" with a replacement span of closing tag name - hasClosingAngleBracket := findChildOfKind(jsxClosingElement, ast.KindGreaterThanToken, file) != nil - tagName := jsxClosingElement.Parent.AsJsxElement().OpeningElement.TagName() - closingTag := scanner.GetTextOfNode(tagName) - fullClosingTag := closingTag + core.IfElse(hasClosingAngleBracket, "", ">") - optionalReplacementSpan := l.createLspRangeFromNode(jsxClosingElement.TagName(), file) - defaultCommitCharacters := getDefaultCommitCharacters(false /*isNewIdentifierLocation*/) - - item := l.createLSPCompletionItem( - fullClosingTag, /*name*/ - "", /*insertText*/ - "", /*filterText*/ - SortTextLocationPriority, - ScriptElementKindClassElement, - collections.Set[ScriptElementKindModifier]{}, /*kindModifiers*/ - nil, /*replacementSpan*/ - nil, /*commitCharacters*/ - nil, /*labelDetails*/ - file, - position, - clientOptions, - true, /*isMemberCompletion*/ - false, /*isSnippet*/ - false, /*hasAction*/ - false, /*preselect*/ - "", /*source*/ - nil, /*autoImportEntryData*/ // !!! jsx autoimports - ) - items := []*lsproto.CompletionItem{item} - itemDefaults := l.setItemDefaults( - clientOptions, - position, - file, - items, - &defaultCommitCharacters, - optionalReplacementSpan, - ) - - return &lsproto.CompletionList{ - IsIncomplete: false, - ItemDefaults: itemDefaults, - Items: items, - } -} - -func (l *LanguageService) createLSPCompletionItem( - name string, - insertText string, - filterText string, - sortText sortText, - elementKind ScriptElementKind, - kindModifiers collections.Set[ScriptElementKindModifier], - replacementSpan *lsproto.Range, - commitCharacters *[]string, - labelDetails *lsproto.CompletionItemLabelDetails, - file *ast.SourceFile, - position int, - clientOptions *lsproto.CompletionClientCapabilities, - isMemberCompletion bool, - isSnippet bool, - hasAction bool, - preselect bool, - source string, - autoImportEntryData *completionEntryData, -) *lsproto.CompletionItem { - kind := getCompletionsSymbolKind(elementKind) - var data any = &itemData{ - FileName: file.FileName(), - Position: position, - Source: source, - Name: name, - AutoImport: autoImportEntryData, - } - - // Text edit - var textEdit *lsproto.TextEditOrInsertReplaceEdit - if replacementSpan != nil { - textEdit = &lsproto.TextEditOrInsertReplaceEdit{ - TextEdit: &lsproto.TextEdit{ - NewText: core.IfElse(insertText == "", name, insertText), - Range: *replacementSpan, - }, - } - } - - // Filter text - - // Ported from vscode ts extension. - wordSize, wordStart := getWordLengthAndStart(file, position) - dotAccessor := getDotAccessor(file, position-wordSize) - if filterText == "" { - filterText = getFilterText(file, position, insertText, name, wordStart, dotAccessor) - } - - // Adjustements based on kind modifiers. - var tags *[]lsproto.CompletionItemTag - var detail *string - // Copied from vscode ts extension: `MyCompletionItem.constructor`. - if kindModifiers.Has(ScriptElementKindModifierOptional) { - if insertText == "" { - insertText = name - } - if filterText == "" { - filterText = name - } - name = name + "?" - } - if kindModifiers.Has(ScriptElementKindModifierDeprecated) { - tags = &[]lsproto.CompletionItemTag{lsproto.CompletionItemTagDeprecated} - } - if kind == lsproto.CompletionItemKindFile { - for _, extensionModifier := range fileExtensionKindModifiers { - if kindModifiers.Has(extensionModifier) { - if strings.HasSuffix(name, string(extensionModifier)) { - detail = ptrTo(name) - } else { - detail = ptrTo(name + string(extensionModifier)) - } - break - } - } - } - - if hasAction && source != "" { - // !!! adjust label like vscode does - } - - // Client assumes plain text by default. - var insertTextFormat *lsproto.InsertTextFormat - if isSnippet { - insertTextFormat = ptrTo(lsproto.InsertTextFormatSnippet) - } - - return &lsproto.CompletionItem{ - Label: name, - LabelDetails: labelDetails, - Kind: &kind, - Tags: tags, - Detail: detail, - Preselect: boolToPtr(preselect), - SortText: ptrTo(string(sortText)), - FilterText: strPtrTo(filterText), - InsertText: strPtrTo(insertText), - InsertTextFormat: insertTextFormat, - TextEdit: textEdit, - CommitCharacters: commitCharacters, - Data: &data, - } -} - -func (l *LanguageService) getLabelCompletionsAtPosition( - node *ast.BreakOrContinueStatement, - clientOptions *lsproto.CompletionClientCapabilities, - file *ast.SourceFile, - position int, - optionalReplacementSpan *lsproto.Range, -) *lsproto.CompletionList { - items := l.getLabelStatementCompletions(node, clientOptions, file, position) - if len(items) == 0 { - return nil - } - defaultCommitCharacters := getDefaultCommitCharacters(false /*isNewIdentifierLocation*/) - itemDefaults := l.setItemDefaults( - clientOptions, - position, - file, - items, - &defaultCommitCharacters, - optionalReplacementSpan, - ) - return &lsproto.CompletionList{ - IsIncomplete: false, - ItemDefaults: itemDefaults, - Items: items, - } -} - -func (l *LanguageService) getLabelStatementCompletions( - node *ast.BreakOrContinueStatement, - clientOptions *lsproto.CompletionClientCapabilities, - file *ast.SourceFile, - position int, -) []*lsproto.CompletionItem { - var uniques collections.Set[string] - var items []*lsproto.CompletionItem - current := node - for current != nil { - if ast.IsFunctionLike(current) { - break - } - if ast.IsLabeledStatement(current) { - name := current.Label().Text() - if !uniques.Has(name) { - uniques.Add(name) - items = append(items, l.createLSPCompletionItem( - name, - "", /*insertText*/ - "", /*filterText*/ - SortTextLocationPriority, - ScriptElementKindLabel, - collections.Set[ScriptElementKindModifier]{}, /*kindModifiers*/ - nil, /*replacementSpan*/ - nil, /*commitCharacters*/ - nil, /*labelDetails*/ - file, - position, - clientOptions, - false, /*isMemberCompletion*/ - false, /*isSnippet*/ - false, /*hasAction*/ - false, /*preselect*/ - "", /*source*/ - nil, /*autoImportEntryData*/ - )) - } - } - current = current.Parent - } - return items -} - -func isCompletionListBlocker( - contextToken *ast.Node, - previousToken *ast.Node, - location *ast.Node, - file *ast.SourceFile, - position int, - typeChecker *checker.Checker, -) bool { - return isInStringOrRegularExpressionOrTemplateLiteral(contextToken, position) || - isSolelyIdentifierDefinitionLocation(contextToken, previousToken, file, position, typeChecker) || - isDotOfNumericLiteral(contextToken, file) || - isInJsxText(contextToken, location) || - ast.IsBigIntLiteral(contextToken) -} - -func isInStringOrRegularExpressionOrTemplateLiteral(contextToken *ast.Node, position int) bool { - // To be "in" one of these literals, the position has to be: - // 1. entirely within the token text. - // 2. at the end position of an unterminated token. - // 3. at the end of a regular expression (due to trailing flags like '/foo/g'). - return (ast.IsRegularExpressionLiteral(contextToken) || ast.IsStringTextContainingNode(contextToken)) && - (contextToken.Loc.ContainsExclusive(position)) || - position == contextToken.End() && - (ast.IsUnterminatedLiteral(contextToken) || ast.IsRegularExpressionLiteral(contextToken)) -} - -// true if we are certain that the currently edited location must define a new location; false otherwise. -func isSolelyIdentifierDefinitionLocation( - contextToken *ast.Node, - previousToken *ast.Node, - file *ast.SourceFile, - position int, - typeChecker *checker.Checker, -) bool { - parent := contextToken.Parent - containingNodeKind := parent.Kind - switch contextToken.Kind { - case ast.KindCommaToken: - return containingNodeKind == ast.KindVariableDeclaration || - isVariableDeclarationListButNotTypeArgument(contextToken, file, typeChecker) || - containingNodeKind == ast.KindVariableStatement || - containingNodeKind == ast.KindEnumDeclaration || // enum a { foo, | - isFunctionLikeButNotConstructor(containingNodeKind) || - containingNodeKind == ast.KindInterfaceDeclaration || // interface A= contextToken.Pos()) - case ast.KindDotToken: - return containingNodeKind == ast.KindArrayBindingPattern // var [.| - case ast.KindColonToken: - return containingNodeKind == ast.KindBindingElement // var {x :html| - case ast.KindOpenBracketToken: - return containingNodeKind == ast.KindArrayBindingPattern // var [x| - case ast.KindOpenParenToken: - return containingNodeKind == ast.KindCatchClause || isFunctionLikeButNotConstructor(containingNodeKind) - case ast.KindOpenBraceToken: - return containingNodeKind == ast.KindEnumDeclaration // enum a { | - case ast.KindLessThanToken: - return containingNodeKind == ast.KindClassDeclaration || // class A< | - containingNodeKind == ast.KindClassExpression || // var C = class D< | - containingNodeKind == ast.KindInterfaceDeclaration || // interface A< | - containingNodeKind == ast.KindTypeAliasDeclaration || // type List< | - ast.IsFunctionLikeKind(containingNodeKind) - case ast.KindStaticKeyword: - return containingNodeKind == ast.KindPropertyDeclaration && - !ast.IsClassLike(parent.Parent) - case ast.KindDotDotDotToken: - return containingNodeKind == ast.KindParameter || - (parent.Parent != nil && parent.Parent.Kind == ast.KindArrayBindingPattern) // var [...z| - case ast.KindPublicKeyword, ast.KindPrivateKeyword, ast.KindProtectedKeyword: - return containingNodeKind == ast.KindParameter && !ast.IsConstructorDeclaration(parent.Parent) - case ast.KindAsKeyword: - return containingNodeKind == ast.KindImportSpecifier || - containingNodeKind == ast.KindExportSpecifier || - containingNodeKind == ast.KindNamespaceImport - case ast.KindGetKeyword, ast.KindSetKeyword: - return !isFromObjectTypeDeclaration(contextToken) - case ast.KindIdentifier: - if (containingNodeKind == ast.KindImportSpecifier || containingNodeKind == ast.KindExportSpecifier) && - contextToken == parent.Name() && - contextToken.Text() == "type" { - // import { type | } - return false - } - ancestorVariableDeclaration := ast.FindAncestor(parent, ast.IsVariableDeclaration) - if ancestorVariableDeclaration != nil && getLineEndOfPosition(file, contextToken.End()) < position { - // let a - // | - return false - } - case ast.KindClassKeyword, ast.KindEnumKeyword, ast.KindInterfaceKeyword, ast.KindFunctionKeyword, - ast.KindVarKeyword, ast.KindImportKeyword, ast.KindLetKeyword, ast.KindConstKeyword, ast.KindInferKeyword: - return true - case ast.KindTypeKeyword: - // import { type foo| } - return containingNodeKind != ast.KindImportSpecifier - case ast.KindAsteriskToken: - return ast.IsFunctionLike(parent) && !ast.IsMethodDeclaration(parent) - } - - // If the previous token is keyword corresponding to class member completion keyword - // there will be completion available here - if isClassMemberCompletionKeyword(keywordForNode(contextToken)) && isFromObjectTypeDeclaration(contextToken) { - return false - } - - if isConstructorParameterCompletion(contextToken) { - // constructor parameter completion is available only if - // - its modifier of the constructor parameter or - // - its name of the parameter and not being edited - // eg. constructor(a |<- this shouldnt show completion - if !ast.IsIdentifier(contextToken) || - ast.IsParameterPropertyModifier(keywordForNode(contextToken)) || - isCurrentlyEditingNode(contextToken, file, position) { - return false - } - } - - // Previous token may have been a keyword that was converted to an identifier. - switch keywordForNode(contextToken) { - case ast.KindAbstractKeyword, ast.KindClassKeyword, ast.KindConstKeyword, ast.KindDeclareKeyword, - ast.KindEnumKeyword, ast.KindFunctionKeyword, ast.KindInterfaceKeyword, ast.KindLetKeyword, - ast.KindPrivateKeyword, ast.KindProtectedKeyword, ast.KindPublicKeyword, - ast.KindStaticKeyword, ast.KindVarKeyword: - return true - case ast.KindAsyncKeyword: - return ast.IsPropertyDeclaration(contextToken.Parent) - } - - // If we are inside a class declaration, and `constructor` is totally not present, - // but we request a completion manually at a whitespace... - ancestorClassLike := ast.FindAncestor(parent, ast.IsClassLike) - if ancestorClassLike != nil && contextToken == previousToken && - isPreviousPropertyDeclarationTerminated(contextToken, file, position) { - // Don't block completions. - return false - } - - ancestorPropertyDeclaration := ast.FindAncestor(parent, ast.IsPropertyDeclaration) - // If we are inside a class declaration and typing `constructor` after property declaration... - if ancestorPropertyDeclaration != nil && contextToken != previousToken && - ast.IsClassLike(previousToken.Parent.Parent) && - // And the cursor is at the token... - position <= previousToken.End() { - // If we are sure that the previous property declaration is terminated according to newline or semicolon... - if isPreviousPropertyDeclarationTerminated(contextToken, file, previousToken.End()) { - // Don't block completions. - return false - } else if contextToken.Kind != ast.KindEqualsToken && - // Should not block: `class C { blah = c/**/ }` - // But should block: `class C { blah = somewhat c/**/ }` and `class C { blah: SomeType c/**/ }` - (ast.IsInitializedProperty(ancestorPropertyDeclaration) || ancestorPropertyDeclaration.Type() != nil) { - return true - } - } - - return ast.IsDeclarationName(contextToken) && - !ast.IsShorthandPropertyAssignment(parent) && - !ast.IsJsxAttribute(parent) && - // Don't block completions if we're in `class C /**/`, `interface I /**/` or `` , - // because we're *past* the end of the identifier and might want to complete `extends`. - // If `contextToken !== previousToken`, this is `class C ex/**/`, `interface I ex/**/` or ``. - !((ast.IsClassLike(parent) || ast.IsInterfaceDeclaration(parent) || ast.IsTypeParameterDeclaration(parent)) && - (contextToken != previousToken || position > previousToken.End())) -} - -func isVariableDeclarationListButNotTypeArgument(node *ast.Node, file *ast.SourceFile, typeChecker *checker.Checker) bool { - return node.Parent.Kind == ast.KindVariableDeclarationList && - !isPossiblyTypeArgumentPosition(node, file, typeChecker) -} - -func isFunctionLikeButNotConstructor(kind ast.Kind) bool { - return ast.IsFunctionLikeKind(kind) && kind != ast.KindConstructor -} - -func isPreviousPropertyDeclarationTerminated(contextToken *ast.Node, file *ast.SourceFile, position int) bool { - return contextToken.Kind != ast.KindEqualsToken && - (contextToken.Kind == ast.KindSemicolonToken || - getLineOfPosition(file, contextToken.End()) != getLineOfPosition(file, position)) -} - -func isDotOfNumericLiteral(contextToken *ast.Node, file *ast.SourceFile) bool { - if contextToken.Kind == ast.KindNumericLiteral { - text := file.Text()[contextToken.Pos():contextToken.End()] - r, _ := utf8.DecodeLastRuneInString(text) - return r == '.' - } - - return false -} - -func isInJsxText(contextToken *ast.Node, location *ast.Node) bool { - if contextToken.Kind == ast.KindJsxText { - return true - } - - if contextToken.Kind == ast.KindGreaterThanToken && contextToken.Parent != nil { - // /**/ /> - // /**/ > - // - contextToken: GreaterThanToken (before cursor) - // - location: JsxSelfClosingElement or JsxOpeningElement - // - contextToken.parent === location - if location == contextToken.Parent && ast.IsJsxOpeningLikeElement(location) { - return false - } - - if contextToken.Parent.Kind == ast.KindJsxOpeningElement { - //
/**/ - // - contextToken: GreaterThanToken (before cursor) - // - location: JSXElement - // - different parents (JSXOpeningElement, JSXElement) - return location.Parent.Kind != ast.KindJsxOpeningElement - } - - if contextToken.Parent.Kind == ast.KindJsxClosingElement || - contextToken.Parent.Kind == ast.KindJsxSelfClosingElement { - return contextToken.Parent.Parent != nil && contextToken.Parent.Parent.Kind == ast.KindJsxElement - } - } - - return false -} - -func hasCompletionItem(clientOptions *lsproto.CompletionClientCapabilities) bool { - return clientOptions != nil && clientOptions.CompletionItem != nil -} - -func clientSupportsItemLabelDetails(clientOptions *lsproto.CompletionClientCapabilities) bool { - return hasCompletionItem(clientOptions) && ptrIsTrue(clientOptions.CompletionItem.LabelDetailsSupport) -} - -func clientSupportsItemSnippet(clientOptions *lsproto.CompletionClientCapabilities) bool { - return hasCompletionItem(clientOptions) && ptrIsTrue(clientOptions.CompletionItem.SnippetSupport) -} - -func clientSupportsItemCommitCharacters(clientOptions *lsproto.CompletionClientCapabilities) bool { - return hasCompletionItem(clientOptions) && ptrIsTrue(clientOptions.CompletionItem.CommitCharactersSupport) -} - -func clientSupportsItemInsertReplace(clientOptions *lsproto.CompletionClientCapabilities) bool { - return hasCompletionItem(clientOptions) && ptrIsTrue(clientOptions.CompletionItem.InsertReplaceSupport) -} - -func clientSupportsDefaultCommitCharacters(clientOptions *lsproto.CompletionClientCapabilities) bool { - if clientOptions == nil || clientOptions.CompletionList == nil || clientOptions.CompletionList.ItemDefaults == nil { - return false - } - return slices.Contains(*clientOptions.CompletionList.ItemDefaults, "commitCharacters") -} - -func clientSupportsDefaultEditRange(clientOptions *lsproto.CompletionClientCapabilities) bool { - if clientOptions == nil || clientOptions.CompletionList == nil || clientOptions.CompletionList.ItemDefaults == nil { - return false - } - return slices.Contains(*clientOptions.CompletionList.ItemDefaults, "editRange") -} - -type argumentInfoForCompletions struct { - invocation *ast.CallLikeExpression - argumentIndex int - argumentCount int -} - -func getArgumentInfoForCompletions(node *ast.Node, position int, file *ast.SourceFile, typeChecker *checker.Checker) *argumentInfoForCompletions { - info := getImmediatelyContainingArgumentInfo(node, position, file, typeChecker) - if info == nil || info.isTypeParameterList || info.invocation.callInvocation == nil { - return nil - } - return &argumentInfoForCompletions{ - invocation: info.invocation.callInvocation.node, - argumentIndex: info.argumentIndex, - argumentCount: info.argumentCount, - } -} - -type itemData struct { - FileName string `json:"fileName"` - Position int `json:"position"` - Source string `json:"source,omitempty"` - Name string `json:"name,omitempty"` - AutoImport *completionEntryData `json:"autoImport,omitempty"` -} - -type completionEntryData struct { - /** - * The name of the property or export in the module's symbol table. Differs from the completion name - * in the case of InternalSymbolName.ExportEquals and InternalSymbolName.Default. - */ - ExportName string `json:"exportName"` - ExportMapKey ExportInfoMapKey `json:"exportMapKey"` - ModuleSpecifier string `json:"moduleSpecifier"` - - /** The file name declaring the export's module symbol, if it was an external module */ - FileName *string `json:"fileName"` - /** The module name (with quotes stripped) of the export's module symbol, if it was an ambient module */ - AmbientModuleName *string `json:"ambientModuleName"` - - /** True if the export was found in the package.json AutoImportProvider */ - IsPackageJsonImport core.Tristate `json:"isPackageJsonImport"` -} - -func (d *completionEntryData) toSymbolOriginExport(symbolName string, moduleSymbol *ast.Symbol, isDefaultExport bool) *symbolOriginInfoExport { - return &symbolOriginInfoExport{ - symbolName: symbolName, - moduleSymbol: moduleSymbol, - exportName: d.ExportName, - exportMapKey: d.ExportMapKey, - moduleSpecifier: d.ModuleSpecifier, - } -} - -// Special values for `CompletionInfo['source']` used to disambiguate -// completion items with the same `name`. (Each completion item must -// have a unique name/source combination, because those two fields -// comprise `CompletionEntryIdentifier` in `getCompletionEntryDetails`. -// -// When the completion item is an auto-import suggestion, the source -// is the module specifier of the suggestion. To avoid collisions, -// the values here should not be a module specifier we would ever -// generate for an auto-import. -const ( - // Completions that require `this.` insertion text - SourceThisProperty = "ThisProperty/" - // Auto-import that comes attached to a class member snippet - SourceClassMemberSnippet = "ClassMemberSnippet/" - // A type-only import that needs to be promoted in order to be used at the completion location - SourceTypeOnlyAlias = "TypeOnlyAlias/" - // Auto-import that comes attached to an object literal method snippet - SourceObjectLiteralMethodSnippet = "ObjectLiteralMethodSnippet/" - // Case completions for switch statements - SourceSwitchCases = "SwitchCases/" - // Completions for an object literal expression - SourceObjectLiteralMemberWithComma = "ObjectLiteralMemberWithComma/" -) - -func (l *LanguageService) ResolveCompletionItem( - ctx context.Context, - item *lsproto.CompletionItem, - data *itemData, - clientOptions *lsproto.CompletionClientCapabilities, - preferences *UserPreferences, -) (*lsproto.CompletionItem, error) { - if data == nil { - return nil, errors.New("completion item data is nil") - } - - program, file := l.tryGetProgramAndFile(data.FileName) - if file == nil { - return nil, fmt.Errorf("file not found: %s", data.FileName) - } - - return l.getCompletionItemDetails(ctx, program, data.Position, file, item, data, clientOptions, preferences), nil -} - -func GetCompletionItemData(item *lsproto.CompletionItem) (*itemData, error) { - bytes, err := json.Marshal(item.Data) - if err != nil { - return nil, fmt.Errorf("failed to marshal completion item data: %w", err) - } - var itemData itemData - if err := json.Unmarshal(bytes, &itemData); err != nil { - return nil, fmt.Errorf("failed to unmarshal completion item data: %w", err) - } - return &itemData, nil -} - -func (l *LanguageService) getCompletionItemDetails( - ctx context.Context, - program *compiler.Program, - position int, - file *ast.SourceFile, - item *lsproto.CompletionItem, - itemData *itemData, - clientOptions *lsproto.CompletionClientCapabilities, - preferences *UserPreferences, -) *lsproto.CompletionItem { - checker, done := program.GetTypeCheckerForFile(ctx, file) - defer done() - contextToken, previousToken := getRelevantTokens(position, file) - if IsInString(file, position, previousToken) { - return l.getStringLiteralCompletionDetails( - ctx, - checker, - item, - itemData.Name, - file, - position, - contextToken, - preferences, - ) - } - - // Compute all the completion symbols again. - symbolCompletion := l.getSymbolCompletionFromItemData( - ctx, - checker, - file, - position, - itemData, - clientOptions, - preferences, - ) - switch { - case symbolCompletion.request != nil: - request := *symbolCompletion.request - switch request := request.(type) { - case *completionDataJSDocTagName: - return createSimpleDetails(item, itemData.Name) - case *completionDataJSDocTag: - return createSimpleDetails(item, itemData.Name) - case *completionDataJSDocParameterName: - return createSimpleDetails(item, itemData.Name) - case *completionDataKeyword: - if core.Some(request.keywordCompletions, func(c *lsproto.CompletionItem) bool { - return c.Label == itemData.Name - }) { - return createSimpleDetails(item, itemData.Name) - } - return item - default: - panic(fmt.Sprintf("Unexpected completion data type: %T", request)) - } - case symbolCompletion.symbol != nil: - symbolDetails := symbolCompletion.symbol - actions := l.getCompletionItemActions(ctx, checker, file, position, itemData, symbolDetails, preferences) - return createCompletionDetailsForSymbol( - item, - symbolDetails.symbol, - checker, - symbolDetails.location, - actions, - ) - case symbolCompletion.literal != nil: - literal := symbolCompletion.literal - return createSimpleDetails(item, completionNameForLiteral(file, preferences, *literal)) - case symbolCompletion.cases != nil: - // !!! exhaustive case completions - return item - default: - // Didn't find a symbol with this name. See if we can find a keyword instead. - if core.Some(allKeywordCompletions(), func(c *lsproto.CompletionItem) bool { - return c.Label == itemData.Name - }) { - return createSimpleDetails(item, itemData.Name) - } - return item - } -} - -type detailsData struct { - symbol *symbolDetails - request *completionData - literal *literalValue - cases *struct{} -} - -type symbolDetails struct { - symbol *ast.Symbol - location *ast.Node - origin *symbolOriginInfo - previousToken *ast.Node - contextToken *ast.Node - jsxInitializer jsxInitializer - isTypeOnlyLocation bool -} - -func (l *LanguageService) getSymbolCompletionFromItemData( - ctx context.Context, - ch *checker.Checker, - file *ast.SourceFile, - position int, - itemData *itemData, - clientOptions *lsproto.CompletionClientCapabilities, - preferences *UserPreferences, -) detailsData { - if itemData.Source == SourceSwitchCases { - return detailsData{ - cases: &struct{}{}, - } - } - if itemData.AutoImport != nil { - if autoImportSymbolData := l.getAutoImportSymbolFromCompletionEntryData(ch, itemData.AutoImport.ExportName, itemData.AutoImport); autoImportSymbolData != nil { - autoImportSymbolData.contextToken, autoImportSymbolData.previousToken = getRelevantTokens(position, file) - autoImportSymbolData.location = astnav.GetTouchingPropertyName(file, position) - autoImportSymbolData.jsxInitializer = jsxInitializer{false, nil} - autoImportSymbolData.isTypeOnlyLocation = false - return detailsData{symbol: autoImportSymbolData} - } - } - - completionData := l.getCompletionData(ctx, ch, file, position, &UserPreferences{IncludeCompletionsForModuleExports: core.TSTrue, IncludeCompletionsForImportStatements: core.TSTrue}) - if completionData == nil { - return detailsData{} - } - - if _, ok := completionData.(*completionDataData); !ok { - return detailsData{ - request: &completionData, - } - } - - data := completionData.(*completionDataData) - - var literal literalValue - for _, l := range data.literals { - if completionNameForLiteral(file, preferences, l) == itemData.Name { - literal = l - break - } - } - if literal != nil { - return detailsData{ - literal: &literal, - } - } - - // Find the symbol with the matching entry name. - // We don't need to perform character checks here because we're only comparing the - // name against 'entryName' (which is known to be good), not building a new - // completion entry. - for _, symbol := range data.symbols { - symbolId := ast.GetSymbolId(symbol) - origin := data.symbolToOriginInfoMap[symbolId] - displayName, _ := getCompletionEntryDisplayNameForSymbol(symbol, origin, data.completionKind, data.isJsxIdentifierExpected) - if displayName == itemData.Name && - (itemData.Source == string(completionSourceClassMemberSnippet) && symbol.Flags&ast.SymbolFlagsClassMember != 0 || - itemData.Source == string(completionSourceObjectLiteralMethodSnippet) && symbol.Flags&(ast.SymbolFlagsProperty|ast.SymbolFlagsMethod) != 0 || - getSourceFromOrigin(origin) == itemData.Source || - itemData.Source == string(completionSourceObjectLiteralMemberWithComma)) { - return detailsData{ - symbol: &symbolDetails{ - symbol: symbol, - location: data.location, - origin: origin, - previousToken: data.previousToken, - contextToken: data.contextToken, - jsxInitializer: data.jsxInitializer, - isTypeOnlyLocation: data.isTypeOnlyLocation, - }, - } - } - } - return detailsData{} -} - -func (l *LanguageService) getAutoImportSymbolFromCompletionEntryData(ch *checker.Checker, name string, autoImportData *completionEntryData) *symbolDetails { - containingProgram := l.GetProgram() // !!! isPackageJson ? packageJsonAutoimportProvider : program - var moduleSymbol *ast.Symbol - if autoImportData.AmbientModuleName != nil { - moduleSymbol = ch.TryFindAmbientModule(*autoImportData.AmbientModuleName) - } else if autoImportData.FileName != nil { - moduleSymbolSourceFile := containingProgram.GetSourceFile(*autoImportData.FileName) - if moduleSymbolSourceFile == nil { - panic("module sourceFile not found: " + *autoImportData.FileName) - } - moduleSymbol = ch.GetMergedSymbol(moduleSymbolSourceFile.Symbol) - } - if moduleSymbol == nil { - return nil - } - - var symbol *ast.Symbol - if autoImportData.ExportName == ast.InternalSymbolNameExportEquals { - symbol = ch.ResolveExternalModuleSymbol(moduleSymbol) - } else { - symbol = ch.TryGetMemberInModuleExportsAndProperties(autoImportData.ExportName, moduleSymbol) - } - if symbol == nil { - return nil - } - - isDefaultExport := autoImportData.ExportName == ast.InternalSymbolNameDefault - if isDefaultExport { - if localSymbol := binder.GetLocalSymbolForExportDefault(symbol); localSymbol != nil { - symbol = localSymbol - } - } - origin := &symbolOriginInfo{ - kind: symbolOriginInfoKindExport, - fileName: *autoImportData.FileName, - isFromPackageJson: autoImportData.IsPackageJsonImport.IsTrue(), - isDefaultExport: isDefaultExport, - data: autoImportData.toSymbolOriginExport(name, moduleSymbol, isDefaultExport), - } - - return &symbolDetails{symbol: symbol, origin: origin} -} - -func createSimpleDetails( - item *lsproto.CompletionItem, - name string, -) *lsproto.CompletionItem { - return createCompletionDetails(item, name, "" /*documentation*/) -} - -func createCompletionDetails( - item *lsproto.CompletionItem, - detail string, - documentation string, -) *lsproto.CompletionItem { - // !!! fill in additionalTextEdits from code actions - if item.Detail == nil && detail != "" { - item.Detail = &detail - } - if documentation != "" { - item.Documentation = &lsproto.StringOrMarkupContent{ - MarkupContent: &lsproto.MarkupContent{ - Kind: lsproto.MarkupKindMarkdown, - Value: documentation, - }, - } - } - return item -} - -type codeAction struct { - // Description of the code action to display in the UI of the editor - description string - // Text changes to apply to each file as part of the code action - changes []*lsproto.TextEdit -} - -func createCompletionDetailsForSymbol( - item *lsproto.CompletionItem, - symbol *ast.Symbol, - checker *checker.Checker, - location *ast.Node, - actions []codeAction, -) *lsproto.CompletionItem { - details := make([]string, 0, len(actions)+1) - edits := make([]*lsproto.TextEdit, 0, len(actions)) - for _, action := range actions { - details = append(details, action.description) - edits = append(edits, action.changes...) - } - quickInfo, documentation := getQuickInfoAndDocumentationForSymbol(checker, symbol, location) - details = append(details, quickInfo) - if len(edits) != 0 { - item.AdditionalTextEdits = &edits - } - return createCompletionDetails(item, strings.Join(details, "\n\n"), documentation) -} - -// !!! snippets -func (l *LanguageService) getCompletionItemActions(ctx context.Context, ch *checker.Checker, file *ast.SourceFile, position int, itemData *itemData, symbolDetails *symbolDetails, preferences *UserPreferences) []codeAction { - if itemData.AutoImport != nil && itemData.AutoImport.ModuleSpecifier != "" && symbolDetails.previousToken != nil { - // Import statement completion: 'import c|' - if symbolDetails.contextToken != nil && l.getImportStatementCompletionInfo(symbolDetails.contextToken, file).replacementSpan != nil { - return nil - } else if l.getImportStatementCompletionInfo(symbolDetails.previousToken, file).replacementSpan != nil { - return nil // !!! sourceDisplay [textPart(data.moduleSpecifier)] - } - } - // !!! CompletionSource.ClassMemberSnippet - // !!! origin.isTypeOnlyAlias - // entryId.source == CompletionSourceObjectLiteralMemberWithComma && contextToken - - if symbolDetails.origin == nil { - return nil - } - - symbol := symbolDetails.symbol - if symbol.ExportSymbol != nil { - symbol = symbol.ExportSymbol - } - targetSymbol := ch.GetMergedSymbol(ch.SkipAlias(symbol)) - isJsxOpeningTagName := symbolDetails.contextToken != nil && symbolDetails.contextToken.Kind == ast.KindLessThanToken && ast.IsJsxOpeningLikeElement(symbolDetails.contextToken.Parent) - if symbolDetails.previousToken != nil && ast.IsIdentifier(symbolDetails.previousToken) { - // If the previous token is an identifier, we can use its start position. - position = astnav.GetStartOfNode(symbolDetails.previousToken, file, false) - } - - moduleSymbol := symbolDetails.origin.moduleSymbol() - - var exportMapkey ExportInfoMapKey - if itemData.AutoImport != nil { - exportMapkey = itemData.AutoImport.ExportMapKey - } - moduleSpecifier, importCompletionAction := l.getImportCompletionAction( - ctx, - ch, - targetSymbol, - moduleSymbol, - file, - position, - exportMapkey, - itemData.Name, - isJsxOpeningTagName, - // formatContext, - preferences, - ) - - if !(moduleSpecifier == itemData.AutoImport.ModuleSpecifier || itemData.AutoImport.ModuleSpecifier == "") { - panic("") - } - return []codeAction{importCompletionAction} -} - -func (l *LanguageService) getImportStatementCompletionInfo(contextToken *ast.Node, sourceFile *ast.SourceFile) importStatementCompletionInfo { - result := importStatementCompletionInfo{} - var candidate *ast.Node - parent := contextToken.Parent - switch { - case ast.IsImportEqualsDeclaration(parent): - // import Foo | - // import Foo f| - lastToken := lsutil.GetLastToken(parent, sourceFile) - if contextToken.Kind == ast.KindIdentifier && lastToken != contextToken { - result.keywordCompletion = ast.KindFromKeyword - result.isKeywordOnlyCompletion = true - } else { - if contextToken.Kind != ast.KindTypeKeyword { - result.keywordCompletion = ast.KindTypeKeyword - } - if isModuleSpecifierMissingOrEmpty(parent.AsImportEqualsDeclaration().ModuleReference) { - candidate = parent - } - } - - case couldBeTypeOnlyImportSpecifier(parent, contextToken) && canCompleteFromNamedBindings(parent.Parent): - candidate = parent - case ast.IsNamedImports(parent) || ast.IsNamespaceImport(parent): - if !parent.Parent.IsTypeOnly() && (contextToken.Kind == ast.KindOpenBraceToken || - contextToken.Kind == ast.KindImportKeyword || - contextToken.Kind == ast.KindCommaToken) { - result.keywordCompletion = ast.KindTypeKeyword - } - if canCompleteFromNamedBindings(parent) { - // At `import { ... } |` or `import * as Foo |`, the only possible completion is `from` - if contextToken.Kind == ast.KindCloseBraceToken || contextToken.Kind == ast.KindIdentifier { - result.isKeywordOnlyCompletion = true - result.keywordCompletion = ast.KindFromKeyword - } else { - candidate = parent.Parent.Parent - } - } - - case ast.IsExportDeclaration(parent) && contextToken.Kind == ast.KindAsteriskToken, - ast.IsNamedExports(parent) && contextToken.Kind == ast.KindCloseBraceToken: - result.isKeywordOnlyCompletion = true - result.keywordCompletion = ast.KindFromKeyword - - case contextToken.Kind == ast.KindImportKeyword: - if ast.IsSourceFile(parent) { - // A lone import keyword with nothing following it does not parse as a statement at all - result.keywordCompletion = ast.KindTypeKeyword - candidate = contextToken - } else if ast.IsImportDeclaration(parent) { - // `import s| from` - result.keywordCompletion = ast.KindTypeKeyword - if isModuleSpecifierMissingOrEmpty(parent.ModuleSpecifier()) { - candidate = parent - } - } - } - - if candidate != nil { - result.isNewIdentifierLocation = true - result.replacementSpan = l.getSingleLineReplacementSpanForImportCompletionNode(candidate) - result.couldBeTypeOnlyImportSpecifier = couldBeTypeOnlyImportSpecifier(candidate, contextToken) - if ast.IsImportDeclaration(candidate) { - result.isTopLevelTypeOnly = candidate.AsImportDeclaration().ImportClause.IsTypeOnly() - } else if candidate.Kind == ast.KindImportEqualsDeclaration { - result.isTopLevelTypeOnly = candidate.IsTypeOnly() - } - } else { - result.isNewIdentifierLocation = result.keywordCompletion == ast.KindTypeKeyword - } - return result -} - -func (l *LanguageService) getSingleLineReplacementSpanForImportCompletionNode(node *ast.Node) *lsproto.Range { - // node is ImportDeclaration | ImportEqualsDeclaration | ImportSpecifier | JSDocImportTag | Token - if ancestor := ast.FindAncestor(node, core.Or(ast.IsImportDeclaration, ast.IsImportEqualsDeclaration, ast.IsJSDocImportTag)); ancestor != nil { - node = ancestor - } - sourceFile := ast.GetSourceFileOfNode(node) - if printer.GetLinesBetweenPositions(sourceFile, node.Pos(), node.End()) == 0 { - return l.createLspRangeFromNode(node, sourceFile) - } - - if node.Kind == ast.KindImportKeyword || node.Kind == ast.KindImportSpecifier { - panic("ImportKeyword was necessarily on one line; ImportSpecifier was necessarily parented in an ImportDeclaration") - } - - // Guess which point in the import might actually be a later statement parsed as part of the import - // during parser recovery - either in the middle of named imports, or the module specifier. - var potentialSplitPoint *ast.Node - if node.Kind == ast.KindImportDeclaration || node.Kind == ast.KindJSDocImportTag { - var specifier *ast.Node - if importClause := node.ImportClause(); importClause != nil { - specifier = getPotentiallyInvalidImportSpecifier(importClause.AsImportClause().NamedBindings) - } - if specifier != nil { - potentialSplitPoint = specifier - } else { - potentialSplitPoint = node.ModuleSpecifier() - } - } else { - potentialSplitPoint = node.AsImportEqualsDeclaration().ModuleReference - } - - withoutModuleSpecifier := core.NewTextRange(scanner.GetTokenPosOfNode(lsutil.GetFirstToken(node, sourceFile), sourceFile, false), potentialSplitPoint.Pos()) - // The module specifier/reference was previously found to be missing, empty, or - // not a string literal - in this last case, it's likely that statement on a following - // line was parsed as the module specifier of a partially-typed import, e.g. - // import Foo| - // interface Blah {} - // This appears to be a multiline-import, and editors can't replace multiple lines. - // But if everything but the "module specifier" is on one line, by this point we can - // assume that the "module specifier" is actually just another statement, and return - // the single-line range of the import excluding that probable statement. - if printer.GetLinesBetweenPositions(sourceFile, withoutModuleSpecifier.Pos(), withoutModuleSpecifier.End()) == 0 { - return l.createLspRangeFromBounds(withoutModuleSpecifier.Pos(), withoutModuleSpecifier.End(), sourceFile) - } - return nil -} - -func couldBeTypeOnlyImportSpecifier(importSpecifier *ast.Node, contextToken *ast.Node) bool { - return ast.IsImportSpecifier(importSpecifier) && (importSpecifier.IsTypeOnly() || contextToken == importSpecifier.Name() && isTypeKeywordTokenOrIdentifier(contextToken)) -} - -func canCompleteFromNamedBindings(namedBindings *ast.NamedImportBindings) bool { - if !isModuleSpecifierMissingOrEmpty(namedBindings.Parent.Parent.ModuleSpecifier()) || namedBindings.Parent.Name() != nil { - return false - } - if ast.IsNamedImports(namedBindings) { - // We can only complete on named imports if there are no other named imports already, - // but parser recovery sometimes puts later statements in the named imports list, so - // we try to only consider the probably-valid ones. - invalidNamedImport := getPotentiallyInvalidImportSpecifier(namedBindings) - elements := namedBindings.Elements() - validImports := len(elements) - if invalidNamedImport != nil { - validImports = slices.Index(elements, invalidNamedImport) - } - - return validImports < 2 && validImports > -1 - } - return true -} - -// Tries to identify the first named import that is not really a named import, but rather -// just parser recovery for a situation like: -// -// import { Foo| -// interface Bar {} -// -// in which `Foo`, `interface`, and `Bar` are all parsed as import specifiers. The caller -// will also check if this token is on a separate line from the rest of the import. -func getPotentiallyInvalidImportSpecifier(namedBindings *ast.NamedImportBindings) *ast.Node { - if namedBindings.Kind != ast.KindNamedImports { - return nil - } - return core.Find(namedBindings.Elements(), func(e *ast.Node) bool { - return e.PropertyName() == nil && isNonContextualKeyword(scanner.StringToToken(e.Name().Text())) && - astnav.FindPrecedingToken(ast.GetSourceFileOfNode(namedBindings), e.Name().Pos()).Kind != ast.KindCommaToken - }) -} - -func isModuleSpecifierMissingOrEmpty(specifier *ast.Expression) bool { - if ast.NodeIsMissing(specifier) { - return true - } - node := specifier - if ast.IsExternalModuleReference(node) { - node = node.Expression() - } - if !ast.IsStringLiteralLike(node) { - return true - } - return node.Text() == "" -} - -func hasDocComment(file *ast.SourceFile, position int) bool { - token := astnav.GetTokenAtPosition(file, position) - return ast.FindAncestor(token, (*ast.Node).IsJSDoc) != nil -} - -// Get the corresponding JSDocTag node if the position is in a JSDoc comment -func getJSDocTagAtPosition(node *ast.Node, position int) *ast.JSDocTag { - return ast.FindAncestorOrQuit(node, func(n *ast.Node) ast.FindAncestorResult { - if ast.IsJSDocTag(n) && n.Loc.ContainsInclusive(position) { - return ast.FindAncestorTrue - } - if n.IsJSDoc() { - return ast.FindAncestorQuit - } - return ast.FindAncestorFalse - }) -} - -func tryGetTypeExpressionFromTag(tag *ast.JSDocTag) *ast.Node { - if isTagWithTypeExpression(tag) { - var typeExpression *ast.Node - if ast.IsJSDocTemplateTag(tag) { - typeExpression = tag.AsJSDocTemplateTag().Constraint - } else { - typeExpression = tag.TypeExpression() - } - if typeExpression != nil && typeExpression.Kind == ast.KindJSDocTypeExpression { - return typeExpression - } - } - if ast.IsJSDocAugmentsTag(tag) || ast.IsJSDocImplementsTag(tag) { - return tag.ClassName() - } - return nil -} - -func isTagWithTypeExpression(tag *ast.JSDocTag) bool { - switch tag.Kind { - case ast.KindJSDocParameterTag, ast.KindJSDocPropertyTag, ast.KindJSDocReturnTag, ast.KindJSDocTypeTag, - ast.KindJSDocTypedefTag, ast.KindJSDocSatisfiesTag: - return true - case ast.KindJSDocTemplateTag: - return tag.AsJSDocTemplateTag().Constraint != nil - default: - return false - } -} - -func (l *LanguageService) jsDocCompletionInfo( - clientOptions *lsproto.CompletionClientCapabilities, - position int, - file *ast.SourceFile, - items []*lsproto.CompletionItem, -) *lsproto.CompletionList { - defaultCommitCharacters := getDefaultCommitCharacters(false /*isNewIdentifierLocation*/) - itemDefaults := l.setItemDefaults( - clientOptions, - position, - file, - items, - &defaultCommitCharacters, - nil, /*optionalReplacementSpan*/ - ) - return &lsproto.CompletionList{ - IsIncomplete: false, - ItemDefaults: itemDefaults, - Items: items, - } -} - -var jsDocTagNames = []string{ - "abstract", - "access", - "alias", - "argument", - "async", - "augments", - "author", - "borrows", - "callback", - "class", - "classdesc", - "constant", - "constructor", - "constructs", - "copyright", - "default", - "deprecated", - "description", - "emits", - "enum", - "event", - "example", - "exports", - "extends", - "external", - "field", - "file", - "fileoverview", - "fires", - "function", - "generator", - "global", - "hideconstructor", - "host", - "ignore", - "implements", - "import", - "inheritdoc", - "inner", - "instance", - "interface", - "kind", - "lends", - "license", - "link", - "linkcode", - "linkplain", - "listens", - "member", - "memberof", - "method", - "mixes", - "module", - "name", - "namespace", - "overload", - "override", - "package", - "param", - "private", - "prop", - "property", - "protected", - "public", - "readonly", - "requires", - "returns", - "satisfies", - "see", - "since", - "static", - "summary", - "template", - "this", - "throws", - "todo", - "tutorial", - "type", - "typedef", - "var", - "variation", - "version", - "virtual", - "yields", -} - -var jsDocTagNameCompletionItems = sync.OnceValue(func() []*lsproto.CompletionItem { - items := make([]*lsproto.CompletionItem, 0, len(jsDocTagNames)) - for _, tagName := range jsDocTagNames { - item := &lsproto.CompletionItem{ - Label: tagName, - Kind: ptrTo(lsproto.CompletionItemKindKeyword), - SortText: ptrTo(string(SortTextLocationPriority)), - } - items = append(items, item) - } - return items -}) - -var jsDocTagCompletionItems = sync.OnceValue(func() []*lsproto.CompletionItem { - items := make([]*lsproto.CompletionItem, 0, len(jsDocTagNames)) - for _, tagName := range jsDocTagNames { - item := &lsproto.CompletionItem{ - Label: "@" + tagName, - Kind: ptrTo(lsproto.CompletionItemKindKeyword), - SortText: ptrTo(string(SortTextLocationPriority)), - } - items = append(items, item) - } - return items -}) - -func getJSDocTagNameCompletions() []*lsproto.CompletionItem { - return cloneItems(jsDocTagNameCompletionItems()) -} - -func getJSDocTagCompletions() []*lsproto.CompletionItem { - return cloneItems(jsDocTagCompletionItems()) -} - -func getJSDocParameterCompletions( - clientOptions *lsproto.CompletionClientCapabilities, - file *ast.SourceFile, - position int, - typeChecker *checker.Checker, - options *core.CompilerOptions, - preferences *UserPreferences, - tagNameOnly bool, -) []*lsproto.CompletionItem { - currentToken := astnav.GetTokenAtPosition(file, position) - if !ast.IsJSDocTag(currentToken) && !currentToken.IsJSDoc() { - return nil - } - var jsDoc *ast.JSDocNode - if currentToken.IsJSDoc() { - jsDoc = currentToken - } else { - jsDoc = currentToken.Parent - } - if !jsDoc.IsJSDoc() { - return nil - } - fun := jsDoc.Parent - if !ast.IsFunctionLike(fun) { - return nil - } - - isJS := ast.IsSourceFileJS(file) - // isSnippet := clientSupportsItemSnippet(clientOptions) - isSnippet := false // !!! need snippet printer - paramTagCount := 0 - var tags []*ast.JSDocTag - if jsDoc.AsJSDoc().Tags != nil { - tags = jsDoc.AsJSDoc().Tags.Nodes - } - for _, tag := range tags { - if ast.IsJSDocParameterTag(tag) && - astnav.GetStartOfNode(tag, file, false /*includeJSDoc*/) < position && - ast.IsIdentifier(tag.Name()) { - paramTagCount++ - } - } - paramIndex := -1 - return core.MapNonNil(fun.Parameters(), func(param *ast.ParameterDeclarationNode) *lsproto.CompletionItem { - paramIndex++ - if paramIndex < paramTagCount { - // This parameter is already annotated. - return nil - } - if ast.IsIdentifier(param.Name()) { // Named parameter - tabstopCounter := 1 - paramName := param.Name().Text() - displayText := getJSDocParamAnnotation( - paramName, - param.Initializer(), - param.AsParameterDeclaration().DotDotDotToken, - isJS, - /*isObject*/ false, - /*isSnippet*/ false, - typeChecker, - options, - preferences, - &tabstopCounter, - ) - var snippetText string - if isSnippet { - snippetText = getJSDocParamAnnotation( - paramName, - param.Initializer(), - param.AsParameterDeclaration().DotDotDotToken, - isJS, - /*isObject*/ false, - /*isSnippet*/ true, - typeChecker, - options, - preferences, - &tabstopCounter, - ) - } - if tagNameOnly { // Remove `@` - displayText = displayText[1:] - if snippetText != "" { - snippetText = snippetText[1:] - } - } - - return &lsproto.CompletionItem{ - Label: displayText, - Kind: ptrTo(lsproto.CompletionItemKindVariable), - SortText: ptrTo(string(SortTextLocationPriority)), - InsertText: strPtrTo(snippetText), - InsertTextFormat: core.IfElse(isSnippet, ptrTo(lsproto.InsertTextFormatSnippet), nil), - } - } else if paramIndex == paramTagCount { - // Destructuring parameter; do it positionally - paramPath := fmt.Sprintf("param%d", paramIndex) - displayTextResult := generateJSDocParamTagsForDestructuring( - paramPath, - param.Name(), - param.Initializer(), - param.AsParameterDeclaration().DotDotDotToken, - isJS, - /*isSnippet*/ false, - typeChecker, - options, - preferences, - ) - var snippetText string - if isSnippet { - snippetTextResult := generateJSDocParamTagsForDestructuring( - paramPath, - param.Name(), - param.Initializer(), - param.AsParameterDeclaration().DotDotDotToken, - isJS, - /*isSnippet*/ true, - typeChecker, - options, - preferences, - ) - snippetText = strings.Join(snippetTextResult, options.NewLine.GetNewLineCharacter()+"* ") - } - displayText := strings.Join(displayTextResult, options.NewLine.GetNewLineCharacter()+"* ") - if tagNameOnly { // Remove `@` - displayText = strings.TrimPrefix(displayText, "@") - snippetText = strings.TrimPrefix(snippetText, "@") - } - return &lsproto.CompletionItem{ - Label: displayText, - Kind: ptrTo(lsproto.CompletionItemKindVariable), - SortText: ptrTo(string(SortTextLocationPriority)), - InsertText: strPtrTo(snippetText), - InsertTextFormat: core.IfElse(isSnippet, ptrTo(lsproto.InsertTextFormatSnippet), nil), - } - } - return nil - }) -} - -func getJSDocParamAnnotation( - paramName string, - initializer *ast.Expression, - dotDotDotToken *ast.TokenNode, - isJS bool, - isObject bool, - isSnippet bool, - typeChecker *checker.Checker, - options *core.CompilerOptions, - preferences *UserPreferences, - tabstopCounter *int, -) string { - if isSnippet { - debug.AssertIsDefined(tabstopCounter) - } - if initializer != nil { - paramName = getJSDocParamNameWithInitializer(paramName, initializer) - } - if isSnippet { - paramName = escapeSnippetText(paramName) - } - if isJS { - t := "*" - if isObject { - debug.AssertNil(dotDotDotToken, `Cannot annotate a rest parameter with type 'object'.`) - t = "object" - } else { - if initializer != nil { - inferredType := typeChecker.GetTypeAtLocation(initializer.Parent) - if inferredType.Flags()&(checker.TypeFlagsAny|checker.TypeFlagsVoid) == 0 { - file := ast.GetSourceFileOfNode(initializer) - quotePreference := getQuotePreference(file, preferences) - builderFlags := core.IfElse( - quotePreference == quotePreferenceSingle, - nodebuilder.FlagsUseSingleQuotesForStringLiteralType, - nodebuilder.FlagsNone, - ) - typeNode := typeChecker.TypeToTypeNode(inferredType, ast.FindAncestor(initializer, ast.IsFunctionLike), builderFlags) - if typeNode != nil { - emitContext := printer.NewEmitContext() - // !!! snippet p - p := printer.NewPrinter(printer.PrinterOptions{ - RemoveComments: true, - // !!! - // Module: options.Module, - // ModuleResolution: options.ModuleResolution, - // Target: options.Target, - }, printer.PrintHandlers{}, emitContext) - emitContext.SetEmitFlags(typeNode, printer.EFSingleLine) - t = p.Emit(typeNode, file) - } - } - } - if isSnippet && t == "*" { - tabstop := *tabstopCounter - *tabstopCounter++ - t = fmt.Sprintf("${%d:%s}", tabstop, t) - } - } - dotDotDot := core.IfElse(!isObject && dotDotDotToken != nil, "...", "") - var description string - if isSnippet { - tabstop := *tabstopCounter - *tabstopCounter++ - description = fmt.Sprintf("${%d}", tabstop) - } - return fmt.Sprintf("@param {%s%s} %s %s", dotDotDot, t, paramName, description) - } else { - var description string - if isSnippet { - tabstop := *tabstopCounter - *tabstopCounter++ - description = fmt.Sprintf("${%d}", tabstop) - } - return fmt.Sprintf("@param %s %s", paramName, description) - } -} - -func getJSDocParamNameWithInitializer(paramName string, initializer *ast.Expression) string { - initializerText := strings.TrimSpace(scanner.GetTextOfNode(initializer)) - if strings.Contains(initializerText, "\n") || len(initializerText) > 80 { - return fmt.Sprintf("[%s]", paramName) - } - return fmt.Sprintf("[%s=%s]", paramName, initializerText) -} - -func generateJSDocParamTagsForDestructuring( - path string, - pattern *ast.BindingPatternNode, - initializer *ast.Expression, - dotDotDotToken *ast.TokenNode, - isJS bool, - isSnippet bool, - typeChecker *checker.Checker, - options *core.CompilerOptions, - preferences *UserPreferences, -) []string { - tabstopCounter := 1 - if !isJS { - return []string{getJSDocParamAnnotation( - path, - initializer, - dotDotDotToken, - isJS, - /*isObject*/ false, - isSnippet, - typeChecker, - options, - preferences, - &tabstopCounter, - )} - } - return jsDocParamPatternWorker( - path, - pattern, - initializer, - dotDotDotToken, - isJS, - isSnippet, - typeChecker, - options, - preferences, - &tabstopCounter, - ) -} - -func jsDocParamPatternWorker( - path string, - pattern *ast.BindingPatternNode, - initializer *ast.Expression, - dotDotDotToken *ast.TokenNode, - isJS bool, - isSnippet bool, - typeChecker *checker.Checker, - options *core.CompilerOptions, - preferences *UserPreferences, - counter *int, -) []string { - if ast.IsObjectBindingPattern(pattern) && dotDotDotToken == nil { - childCounter := *counter - rootParam := getJSDocParamAnnotation( - path, - initializer, - dotDotDotToken, - isJS, - /*isObject*/ true, - isSnippet, - typeChecker, - options, - preferences, - &childCounter, - ) - var childTags []string - for _, element := range pattern.Elements() { - elementTags := jsDocParamElementWorker( - path, - element, - initializer, - dotDotDotToken, - isJS, - isSnippet, - typeChecker, - options, - preferences, - &childCounter, - ) - if len(elementTags) == 0 { - childTags = nil - break - } - childTags = append(childTags, elementTags...) - } - if len(childTags) > 0 { - *counter = childCounter - return append([]string{rootParam}, childTags...) - } - } - return []string{ - getJSDocParamAnnotation( - path, - initializer, - dotDotDotToken, - isJS, - /*isObject*/ false, - isSnippet, - typeChecker, - options, - preferences, - counter, - ), - } -} - -// Assumes binding element is inside object binding pattern. -// We can't deeply annotate an array binding pattern. -func jsDocParamElementWorker( - path string, - element *ast.BindingElementNode, - initializer *ast.Expression, - dotDotDotToken *ast.TokenNode, - isJS bool, - isSnippet bool, - typeChecker *checker.Checker, - options *core.CompilerOptions, - preferences *UserPreferences, - counter *int, -) []string { - if ast.IsIdentifier(element.Name()) { // `{ b }` or `{ b: newB }` - var propertyName string - if element.PropertyName() != nil { - propertyName, _ = ast.TryGetTextOfPropertyName(element.PropertyName()) - } else { - propertyName = element.Name().Text() - } - if propertyName == "" { - return nil - } - paramName := fmt.Sprintf("%s.%s", path, propertyName) - return []string{ - getJSDocParamAnnotation( - paramName, - element.Initializer(), - element.AsBindingElement().DotDotDotToken, - isJS, - /*isObject*/ false, - isSnippet, - typeChecker, - options, - preferences, - counter, - ), - } - } else if element.PropertyName() != nil { // `{ b: {...} }` or `{ b: [...] }` - propertyName, _ := ast.TryGetTextOfPropertyName(element.PropertyName()) - if propertyName == "" { - return nil - } - return jsDocParamPatternWorker( - fmt.Sprintf("%s.%s", path, propertyName), - element.Name(), - element.Initializer(), - element.AsBindingElement().DotDotDotToken, - isJS, - isSnippet, - typeChecker, - options, - preferences, - counter, - ) - } - return nil -} - -func getJSDocParameterNameCompletions(tag *ast.JSDocParameterTag) []*lsproto.CompletionItem { - if !ast.IsIdentifier(tag.Name()) { - return nil - } - nameThusFar := tag.Name().Text() - jsDoc := tag.Parent - fn := jsDoc.Parent - if !ast.IsFunctionLike(fn) { - return nil - } - - var tags []*ast.JSDocTag - if jsDoc.AsJSDoc().Tags != nil { - tags = jsDoc.AsJSDoc().Tags.Nodes - } - - return core.MapNonNil(fn.Parameters(), func(param *ast.ParameterDeclarationNode) *lsproto.CompletionItem { - if !ast.IsIdentifier(param.Name()) { - return nil - } - - name := param.Name().Text() - if core.Some(tags, func(t *ast.JSDocTag) bool { - return t != tag.AsNode() && - ast.IsJSDocParameterTag(t) && - ast.IsIdentifier(t.Name()) && - t.Name().Text() == name - }) || nameThusFar != "" && !strings.HasPrefix(name, nameThusFar) { - return nil - } - - return &lsproto.CompletionItem{ - Label: name, - Kind: ptrTo(lsproto.CompletionItemKindVariable), - SortText: ptrTo(string(SortTextLocationPriority)), - } - }) -} diff --git a/kitcom/internal/tsgo/ls/constants.go b/kitcom/internal/tsgo/ls/constants.go deleted file mode 100644 index 60f240b..0000000 --- a/kitcom/internal/tsgo/ls/constants.go +++ /dev/null @@ -1,6 +0,0 @@ -package ls - -const ( - moduleSpecifierResolutionLimit = 100 - moduleSpecifierResolutionCacheAttemptLimit = 1000 -) diff --git a/kitcom/internal/tsgo/ls/converters.go b/kitcom/internal/tsgo/ls/converters.go deleted file mode 100644 index 6533895..0000000 --- a/kitcom/internal/tsgo/ls/converters.go +++ /dev/null @@ -1,199 +0,0 @@ -package ls - -import ( - "fmt" - "net/url" - "slices" - "strings" - "unicode/utf16" - "unicode/utf8" - - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath" -) - -type Converters struct { - getLineMap func(fileName string) *LSPLineMap - positionEncoding lsproto.PositionEncodingKind -} - -type Script interface { - FileName() string - Text() string -} - -func NewConverters(positionEncoding lsproto.PositionEncodingKind, getLineMap func(fileName string) *LSPLineMap) *Converters { - return &Converters{ - getLineMap: getLineMap, - positionEncoding: positionEncoding, - } -} - -func (c *Converters) ToLSPRange(script Script, textRange core.TextRange) lsproto.Range { - return lsproto.Range{ - Start: c.PositionToLineAndCharacter(script, core.TextPos(textRange.Pos())), - End: c.PositionToLineAndCharacter(script, core.TextPos(textRange.End())), - } -} - -func (c *Converters) FromLSPRange(script Script, textRange lsproto.Range) core.TextRange { - return core.NewTextRange( - int(c.LineAndCharacterToPosition(script, textRange.Start)), - int(c.LineAndCharacterToPosition(script, textRange.End)), - ) -} - -func (c *Converters) FromLSPTextChange(script Script, change *lsproto.TextDocumentContentChangePartial) core.TextChange { - return core.TextChange{ - TextRange: c.FromLSPRange(script, change.Range), - NewText: change.Text, - } -} - -func (c *Converters) ToLSPLocation(script Script, rng core.TextRange) lsproto.Location { - return lsproto.Location{ - Uri: FileNameToDocumentURI(script.FileName()), - Range: c.ToLSPRange(script, rng), - } -} - -func LanguageKindToScriptKind(languageID lsproto.LanguageKind) core.ScriptKind { - switch languageID { - case "typescript": - return core.ScriptKindTS - case "typescriptreact": - return core.ScriptKindTSX - case "javascript": - return core.ScriptKindJS - case "javascriptreact": - return core.ScriptKindJSX - case "json": - return core.ScriptKindJSON - default: - return core.ScriptKindUnknown - } -} - -// https://github.com/microsoft/vscode-uri/blob/edfdccd976efaf4bb8fdeca87e97c47257721729/src/uri.ts#L455 -var extraEscapeReplacer = strings.NewReplacer( - ":", "%3A", - "/", "%2F", - "?", "%3F", - "#", "%23", - "[", "%5B", - "]", "%5D", - "@", "%40", - - "!", "%21", - "$", "%24", - "&", "%26", - "'", "%27", - "(", "%28", - ")", "%29", - "*", "%2A", - "+", "%2B", - ",", "%2C", - ";", "%3B", - "=", "%3D", - - " ", "%20", -) - -func FileNameToDocumentURI(fileName string) lsproto.DocumentUri { - if strings.HasPrefix(fileName, "^/") { - scheme, rest, ok := strings.Cut(fileName[2:], "/") - if !ok { - panic("invalid file name: " + fileName) - } - authority, path, ok := strings.Cut(rest, "/") - if !ok { - panic("invalid file name: " + fileName) - } - if authority == "ts-nul-authority" { - return lsproto.DocumentUri(scheme + ":" + path) - } - return lsproto.DocumentUri(scheme + "://" + authority + "/" + path) - } - - volume, fileName, _ := tspath.SplitVolumePath(fileName) - if volume != "" { - volume = "/" + extraEscapeReplacer.Replace(volume) - } - - fileName = strings.TrimPrefix(fileName, "//") - - parts := strings.Split(fileName, "/") - for i, part := range parts { - parts[i] = extraEscapeReplacer.Replace(url.PathEscape(part)) - } - - return lsproto.DocumentUri("file://" + volume + strings.Join(parts, "/")) -} - -func (c *Converters) LineAndCharacterToPosition(script Script, lineAndCharacter lsproto.Position) core.TextPos { - // UTF-8/16 0-indexed line and character to UTF-8 offset - - lineMap := c.getLineMap(script.FileName()) - - line := core.TextPos(lineAndCharacter.Line) - char := core.TextPos(lineAndCharacter.Character) - - if line < 0 || int(line) >= len(lineMap.LineStarts) { - panic(fmt.Sprintf("bad line number. Line: %d, lineMap length: %d", line, len(lineMap.LineStarts))) - } - - start := lineMap.LineStarts[line] - if lineMap.AsciiOnly || c.positionEncoding == lsproto.PositionEncodingKindUTF8 { - return start + char - } - - var utf8Char core.TextPos - var utf16Char core.TextPos - - for i, r := range script.Text()[start:] { - u16Len := core.TextPos(utf16.RuneLen(r)) - if utf16Char+u16Len > char { - break - } - utf16Char += u16Len - utf8Char = core.TextPos(i + utf8.RuneLen(r)) - } - - return start + utf8Char -} - -func (c *Converters) PositionToLineAndCharacter(script Script, position core.TextPos) lsproto.Position { - // UTF-8 offset to UTF-8/16 0-indexed line and character - - lineMap := c.getLineMap(script.FileName()) - - line, isLineStart := slices.BinarySearch(lineMap.LineStarts, position) - if !isLineStart { - line-- - } - line = max(0, line) - - // The current line ranges from lineMap.LineStarts[line] (or 0) to lineMap.LineStarts[line+1] (or len(text)). - - start := lineMap.LineStarts[line] - - var character core.TextPos - if lineMap.AsciiOnly || c.positionEncoding == lsproto.PositionEncodingKindUTF8 { - character = position - start - } else { - // We need to rescan the text as UTF-16 to find the character offset. - for _, r := range script.Text()[start:position] { - character += core.TextPos(utf16.RuneLen(r)) - } - } - - return lsproto.Position{ - Line: uint32(line), - Character: uint32(character), - } -} - -func ptrTo[T any](v T) *T { - return &v -} diff --git a/kitcom/internal/tsgo/ls/converters_test.go b/kitcom/internal/tsgo/ls/converters_test.go deleted file mode 100644 index cdbb84d..0000000 --- a/kitcom/internal/tsgo/ls/converters_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package ls_test - -import ( - "testing" - - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ls" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto" - "gotest.tools/v3/assert" -) - -func TestDocumentURIToFileName(t *testing.T) { - t.Parallel() - - tests := []struct { - uri lsproto.DocumentUri - fileName string - }{ - {"file:///path/to/file.ts", "/path/to/file.ts"}, - {"file://server/share/file.ts", "//server/share/file.ts"}, - {"file:///d%3A/work/tsgo932/lib/utils.ts", "d:/work/tsgo932/lib/utils.ts"}, - {"file:///D%3A/work/tsgo932/lib/utils.ts", "d:/work/tsgo932/lib/utils.ts"}, - {"file:///d%3A/work/tsgo932/app/%28test%29/comp/comp-test.tsx", "d:/work/tsgo932/app/(test)/comp/comp-test.tsx"}, - {"file:///path/to/file.ts#section", "/path/to/file.ts"}, - {"file:///c:/test/me", "c:/test/me"}, - {"file://shares/files/c%23/p.cs", "//shares/files/c#/p.cs"}, - {"file:///c:/Source/Z%C3%BCrich%20or%20Zurich%20(%CB%88zj%CA%8A%C9%99r%C9%AAk,/Code/resources/app/plugins/c%23/plugin.json", "c:/Source/Zürich or Zurich (ˈzjÊŠÉ™rɪk,/Code/resources/app/plugins/c#/plugin.json"}, - {"file:///c:/test %25/path", "c:/test %/path"}, - // {"file:?q", "/"}, - {"file:///_:/path", "/_:/path"}, - {"file:///users/me/c%23-projects/", "/users/me/c#-projects/"}, - {"file://localhost/c%24/GitDevelopment/express", "//localhost/c$/GitDevelopment/express"}, - {"file:///c%3A/test%20with%20%2525/c%23code", "c:/test with %25/c#code"}, - - {"untitled:Untitled-1", "^/untitled/ts-nul-authority/Untitled-1"}, - {"untitled:Untitled-1#fragment", "^/untitled/ts-nul-authority/Untitled-1#fragment"}, - {"untitled:c:/Users/jrieken/Code/abc.txt", "^/untitled/ts-nul-authority/c:/Users/jrieken/Code/abc.txt"}, - {"untitled:C:/Users/jrieken/Code/abc.txt", "^/untitled/ts-nul-authority/C:/Users/jrieken/Code/abc.txt"}, - {"untitled://wsl%2Bubuntu/home/jabaile/work/TypeScript-go/newfile.ts", "^/untitled/wsl%2Bubuntu/home/jabaile/work/TypeScript-go/newfile.ts"}, - } - - for _, test := range tests { - t.Run(string(test.uri), func(t *testing.T) { - t.Parallel() - assert.Equal(t, test.uri.FileName(), test.fileName) - }) - } -} - -func TestFileNameToDocumentURI(t *testing.T) { - t.Parallel() - - tests := []struct { - fileName string - uri lsproto.DocumentUri - }{ - {"/path/to/file.ts", "file:///path/to/file.ts"}, - {"//server/share/file.ts", "file://server/share/file.ts"}, - {"d:/work/tsgo932/lib/utils.ts", "file:///d%3A/work/tsgo932/lib/utils.ts"}, - {"d:/work/tsgo932/lib/utils.ts", "file:///d%3A/work/tsgo932/lib/utils.ts"}, - {"d:/work/tsgo932/app/(test)/comp/comp-test.tsx", "file:///d%3A/work/tsgo932/app/%28test%29/comp/comp-test.tsx"}, - {"/path/to/file.ts", "file:///path/to/file.ts"}, - {"c:/test/me", "file:///c%3A/test/me"}, - {"//shares/files/c#/p.cs", "file://shares/files/c%23/p.cs"}, - {"c:/Source/Zürich or Zurich (ˈzjÊŠÉ™rɪk,/Code/resources/app/plugins/c#/plugin.json", "file:///c%3A/Source/Z%C3%BCrich%20or%20Zurich%20%28%CB%88zj%CA%8A%C9%99r%C9%AAk%2C/Code/resources/app/plugins/c%23/plugin.json"}, - {"c:/test %/path", "file:///c%3A/test%20%25/path"}, - {"/", "file:///"}, - {"/_:/path", "file:///_%3A/path"}, - {"/users/me/c#-projects/", "file:///users/me/c%23-projects/"}, - {"//localhost/c$/GitDevelopment/express", "file://localhost/c%24/GitDevelopment/express"}, - {"c:/test with %25/c#code", "file:///c%3A/test%20with%20%2525/c%23code"}, - - {"^/untitled/ts-nul-authority/Untitled-1", "untitled:Untitled-1"}, - {"^/untitled/ts-nul-authority/c:/Users/jrieken/Code/abc.txt", "untitled:c:/Users/jrieken/Code/abc.txt"}, - {"^/untitled/ts-nul-authority///wsl%2Bubuntu/home/jabaile/work/TypeScript-go/newfile.ts", "untitled://wsl%2Bubuntu/home/jabaile/work/TypeScript-go/newfile.ts"}, - } - - for _, test := range tests { - t.Run(test.fileName, func(t *testing.T) { - t.Parallel() - assert.Equal(t, ls.FileNameToDocumentURI(test.fileName), test.uri) - }) - } -} diff --git a/kitcom/internal/tsgo/ls/definition.go b/kitcom/internal/tsgo/ls/definition.go deleted file mode 100644 index 89b22cb..0000000 --- a/kitcom/internal/tsgo/ls/definition.go +++ /dev/null @@ -1,234 +0,0 @@ -package ls - -import ( - "context" - "slices" - - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/checker" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner" -) - -func (l *LanguageService) ProvideDefinition(ctx context.Context, documentURI lsproto.DocumentUri, position lsproto.Position) (lsproto.DefinitionResponse, error) { - program, file := l.getProgramAndFile(documentURI) - node := astnav.GetTouchingPropertyName(file, int(l.converters.LineAndCharacterToPosition(file, position))) - if node.Kind == ast.KindSourceFile { - return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{}, nil - } - - c, done := program.GetTypeCheckerForFile(ctx, file) - defer done() - - if node.Kind == ast.KindOverrideKeyword { - if sym := getSymbolForOverriddenMember(c, node); sym != nil { - return l.createLocationsFromDeclarations(sym.Declarations), nil - } - } - - if ast.IsJumpStatementTarget(node) { - if label := getTargetLabel(node.Parent, node.Text()); label != nil { - return l.createLocationsFromDeclarations([]*ast.Node{label}), nil - } - } - - if node.Kind == ast.KindCaseKeyword || node.Kind == ast.KindDefaultKeyword && ast.IsDefaultClause(node.Parent) { - if stmt := ast.FindAncestor(node.Parent, ast.IsSwitchStatement); stmt != nil { - file := ast.GetSourceFileOfNode(stmt) - return l.createLocationFromFileAndRange(file, scanner.GetRangeOfTokenAtPosition(file, stmt.Pos())), nil - } - } - - if node.Kind == ast.KindReturnKeyword || node.Kind == ast.KindYieldKeyword || node.Kind == ast.KindAwaitKeyword { - if fn := ast.FindAncestor(node, ast.IsFunctionLikeDeclaration); fn != nil { - return l.createLocationsFromDeclarations([]*ast.Node{fn}), nil - } - } - - declarations := getDeclarationsFromLocation(c, node) - calledDeclaration := tryGetSignatureDeclaration(c, node) - if calledDeclaration != nil { - // If we can resolve a call signature, remove all function-like declarations and add that signature. - nonFunctionDeclarations := core.Filter(slices.Clip(declarations), func(node *ast.Node) bool { return !ast.IsFunctionLike(node) }) - declarations = append(nonFunctionDeclarations, calledDeclaration) - } - return l.createLocationsFromDeclarations(declarations), nil -} - -func (l *LanguageService) ProvideTypeDefinition(ctx context.Context, documentURI lsproto.DocumentUri, position lsproto.Position) (lsproto.DefinitionResponse, error) { - program, file := l.getProgramAndFile(documentURI) - node := astnav.GetTouchingPropertyName(file, int(l.converters.LineAndCharacterToPosition(file, position))) - if node.Kind == ast.KindSourceFile { - return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{}, nil - } - - c, done := program.GetTypeCheckerForFile(ctx, file) - defer done() - - node = getDeclarationNameForKeyword(node) - - if symbol := c.GetSymbolAtLocation(node); symbol != nil { - symbolType := getTypeOfSymbolAtLocation(c, symbol, node) - declarations := getDeclarationsFromType(symbolType) - if typeArgument := c.GetFirstTypeArgumentFromKnownType(symbolType); typeArgument != nil { - declarations = core.Concatenate(getDeclarationsFromType(typeArgument), declarations) - } - if len(declarations) != 0 { - return l.createLocationsFromDeclarations(declarations), nil - } - if symbol.Flags&ast.SymbolFlagsValue == 0 && symbol.Flags&ast.SymbolFlagsType != 0 { - return l.createLocationsFromDeclarations(symbol.Declarations), nil - } - } - - return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{}, nil -} - -func getDeclarationNameForKeyword(node *ast.Node) *ast.Node { - if node.Kind >= ast.KindFirstKeyword && node.Kind <= ast.KindLastKeyword { - if ast.IsVariableDeclarationList(node.Parent) { - if decl := core.FirstOrNil(node.Parent.AsVariableDeclarationList().Declarations.Nodes); decl != nil && decl.Name() != nil { - return decl.Name() - } - } else if node.Parent.DeclarationData() != nil && node.Parent.Name() != nil && node.Pos() < node.Parent.Name().Pos() { - return node.Parent.Name() - } - } - return node -} - -func (l *LanguageService) createLocationsFromDeclarations(declarations []*ast.Node) lsproto.DefinitionResponse { - locations := make([]lsproto.Location, 0, len(declarations)) - for _, decl := range declarations { - file := ast.GetSourceFileOfNode(decl) - name := core.OrElse(ast.GetNameOfDeclaration(decl), decl) - nodeRange := createRangeFromNode(name, file) - mappedLocation := l.getMappedLocation(file.FileName(), nodeRange) - locations = core.AppendIfUnique(locations, mappedLocation) - } - return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{Locations: &locations} -} - -func (l *LanguageService) createLocationFromFileAndRange(file *ast.SourceFile, textRange core.TextRange) lsproto.DefinitionResponse { - mappedLocation := l.getMappedLocation(file.FileName(), textRange) - return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{ - Location: &mappedLocation, - } -} - -func getDeclarationsFromLocation(c *checker.Checker, node *ast.Node) []*ast.Node { - if ast.IsIdentifier(node) && ast.IsShorthandPropertyAssignment(node.Parent) { - return c.GetResolvedSymbol(node).Declarations - } - node = getDeclarationNameForKeyword(node) - if symbol := c.GetSymbolAtLocation(node); symbol != nil { - if symbol.Flags&ast.SymbolFlagsClass != 0 && symbol.Flags&(ast.SymbolFlagsFunction|ast.SymbolFlagsVariable) == 0 && node.Kind == ast.KindConstructorKeyword { - if constructor := symbol.Members[ast.InternalSymbolNameConstructor]; constructor != nil { - symbol = constructor - } - } - if symbol.Flags&ast.SymbolFlagsAlias != 0 { - if resolved, ok := c.ResolveAlias(symbol); ok { - symbol = resolved - } - } - if symbol.Flags&(ast.SymbolFlagsProperty|ast.SymbolFlagsMethod|ast.SymbolFlagsAccessor) != 0 && symbol.Parent != nil && symbol.Parent.Flags&ast.SymbolFlagsObjectLiteral != 0 { - if objectLiteral := core.FirstOrNil(symbol.Parent.Declarations); objectLiteral != nil { - if declarations := c.GetContextualDeclarationsForObjectLiteralElement(objectLiteral, symbol.Name); len(declarations) != 0 { - return declarations - } - } - } - return symbol.Declarations - } - if indexInfos := c.GetIndexSignaturesAtLocation(node); len(indexInfos) != 0 { - return indexInfos - } - return 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 -} - -func getSymbolForOverriddenMember(typeChecker *checker.Checker, node *ast.Node) *ast.Symbol { - classElement := ast.FindAncestor(node, ast.IsClassElement) - if classElement == nil || classElement.Name() == nil { - return nil - } - baseDeclaration := ast.FindAncestor(classElement, ast.IsClassLike) - if baseDeclaration == nil { - return nil - } - baseTypeNode := ast.GetClassExtendsHeritageElement(baseDeclaration) - if baseTypeNode == nil { - return nil - } - expression := ast.SkipParentheses(baseTypeNode.Expression()) - var base *ast.Symbol - if ast.IsClassExpression(expression) { - base = expression.Symbol() - } else { - base = typeChecker.GetSymbolAtLocation(expression) - } - if base == nil { - return nil - } - name := ast.GetTextOfPropertyName(classElement.Name()) - if ast.HasStaticModifier(classElement) { - return typeChecker.GetPropertyOfType(typeChecker.GetTypeOfSymbol(base), name) - } - return typeChecker.GetPropertyOfType(typeChecker.GetDeclaredTypeOfSymbol(base), name) -} - -func getTypeOfSymbolAtLocation(c *checker.Checker, symbol *ast.Symbol, node *ast.Node) *checker.Type { - t := c.GetTypeOfSymbolAtLocation(symbol, node) - // If the type is just a function's inferred type, go-to-type should go to the return type instead since - // go-to-definition takes you to the function anyway. - if t.Symbol() == symbol || t.Symbol() != nil && symbol.ValueDeclaration != nil && ast.IsVariableDeclaration(symbol.ValueDeclaration) && symbol.ValueDeclaration.Initializer() == t.Symbol().ValueDeclaration { - sigs := c.GetCallSignatures(t) - if len(sigs) == 1 { - return c.GetReturnTypeOfSignature(sigs[0]) - } - } - return t -} - -func getDeclarationsFromType(t *checker.Type) []*ast.Node { - var result []*ast.Node - for _, t := range t.Distributed() { - if t.Symbol() != nil { - for _, decl := range t.Symbol().Declarations { - result = core.AppendIfUnique(result, decl) - } - } - } - return result -} diff --git a/kitcom/internal/tsgo/ls/diagnostics.go b/kitcom/internal/tsgo/ls/diagnostics.go deleted file mode 100644 index 47e9aca..0000000 --- a/kitcom/internal/tsgo/ls/diagnostics.go +++ /dev/null @@ -1,109 +0,0 @@ -package ls - -import ( - "context" - "strings" - - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/diagnostics" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/diagnosticwriter" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto" -) - -func (l *LanguageService) ProvideDiagnostics(ctx context.Context, uri lsproto.DocumentUri) (lsproto.DocumentDiagnosticResponse, error) { - program, file := l.getProgramAndFile(uri) - - diagnostics := make([][]*ast.Diagnostic, 0, 4) - diagnostics = append(diagnostics, program.GetSyntacticDiagnostics(ctx, file)) - diagnostics = append(diagnostics, program.GetSemanticDiagnostics(ctx, file)) - // !!! user preference for suggestion diagnostics; keep only unnecessary/deprecated? - // See: https://github.com/microsoft/vscode/blob/3dbc74129aaae102e5cb485b958fa5360e8d3e7a/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts#L114 - diagnostics = append(diagnostics, program.GetSuggestionDiagnostics(ctx, file)) - if program.Options().GetEmitDeclarations() { - diagnostics = append(diagnostics, program.GetDeclarationDiagnostics(ctx, file)) - } - - return lsproto.RelatedFullDocumentDiagnosticReportOrUnchangedDocumentDiagnosticReport{ - FullDocumentDiagnosticReport: &lsproto.RelatedFullDocumentDiagnosticReport{ - Items: toLSPDiagnostics(l.converters, diagnostics...), - }, - }, nil -} - -func toLSPDiagnostics(converters *Converters, diagnostics ...[]*ast.Diagnostic) []*lsproto.Diagnostic { - size := 0 - for _, diagSlice := range diagnostics { - size += len(diagSlice) - } - lspDiagnostics := make([]*lsproto.Diagnostic, 0, size) - for _, diagSlice := range diagnostics { - for _, diag := range diagSlice { - lspDiagnostics = append(lspDiagnostics, toLSPDiagnostic(converters, diag)) - } - } - return lspDiagnostics -} - -func toLSPDiagnostic(converters *Converters, diagnostic *ast.Diagnostic) *lsproto.Diagnostic { - var severity lsproto.DiagnosticSeverity - switch diagnostic.Category() { - case diagnostics.CategorySuggestion: - severity = lsproto.DiagnosticSeverityHint - case diagnostics.CategoryMessage: - severity = lsproto.DiagnosticSeverityInformation - case diagnostics.CategoryWarning: - severity = lsproto.DiagnosticSeverityWarning - default: - severity = lsproto.DiagnosticSeverityError - } - - relatedInformation := make([]*lsproto.DiagnosticRelatedInformation, 0, len(diagnostic.RelatedInformation())) - for _, related := range diagnostic.RelatedInformation() { - relatedInformation = append(relatedInformation, &lsproto.DiagnosticRelatedInformation{ - Location: lsproto.Location{ - Uri: FileNameToDocumentURI(related.File().FileName()), - Range: converters.ToLSPRange(related.File(), related.Loc()), - }, - Message: related.Message(), - }) - } - - var tags []lsproto.DiagnosticTag - if diagnostic.ReportsUnnecessary() || diagnostic.ReportsDeprecated() { - tags = make([]lsproto.DiagnosticTag, 0, 2) - if diagnostic.ReportsUnnecessary() { - tags = append(tags, lsproto.DiagnosticTagUnnecessary) - } - if diagnostic.ReportsDeprecated() { - tags = append(tags, lsproto.DiagnosticTagDeprecated) - } - } - - return &lsproto.Diagnostic{ - Range: converters.ToLSPRange(diagnostic.File(), diagnostic.Loc()), - Code: &lsproto.IntegerOrString{ - Integer: ptrTo(diagnostic.Code()), - }, - Severity: &severity, - Message: messageChainToString(diagnostic), - Source: ptrTo("ts"), - RelatedInformation: ptrToSliceIfNonEmpty(relatedInformation), - Tags: ptrToSliceIfNonEmpty(tags), - } -} - -func messageChainToString(diagnostic *ast.Diagnostic) string { - if len(diagnostic.MessageChain()) == 0 { - return diagnostic.Message() - } - var b strings.Builder - diagnosticwriter.WriteFlattenedDiagnosticMessage(&b, diagnostic, "\n") - return b.String() -} - -func ptrToSliceIfNonEmpty[T any](s []T) *[]T { - if len(s) == 0 { - return nil - } - return &s -} diff --git a/kitcom/internal/tsgo/ls/documenthighlights.go b/kitcom/internal/tsgo/ls/documenthighlights.go deleted file mode 100644 index 873860a..0000000 --- a/kitcom/internal/tsgo/ls/documenthighlights.go +++ /dev/null @@ -1,690 +0,0 @@ -package ls - -import ( - "context" - - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/compiler" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsutil" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/stringutil" - - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto" -) - -func (l *LanguageService) ProvideDocumentHighlights(ctx context.Context, documentUri lsproto.DocumentUri, documentPosition lsproto.Position) (lsproto.DocumentHighlightResponse, error) { - program, sourceFile := l.getProgramAndFile(documentUri) - position := int(l.converters.LineAndCharacterToPosition(sourceFile, documentPosition)) - node := astnav.GetTouchingPropertyName(sourceFile, position) - if node.Parent != nil && (node.Parent.Kind == ast.KindJsxClosingElement || (node.Parent.Kind == ast.KindJsxOpeningElement && node.Parent.TagName() == node)) { - var openingElement, closingElement *ast.Node - if ast.IsJsxElement(node.Parent.Parent) { - openingElement = node.Parent.Parent.AsJsxElement().OpeningElement - closingElement = node.Parent.Parent.AsJsxElement().ClosingElement - } - var documentHighlights []*lsproto.DocumentHighlight - kind := lsproto.DocumentHighlightKindRead - if openingElement != nil { - documentHighlights = append(documentHighlights, &lsproto.DocumentHighlight{ - Range: *l.createLspRangeFromNode(openingElement, sourceFile), - Kind: &kind, - }) - } - if closingElement != nil { - documentHighlights = append(documentHighlights, &lsproto.DocumentHighlight{ - Range: *l.createLspRangeFromNode(closingElement, sourceFile), - Kind: &kind, - }) - } - return lsproto.DocumentHighlightsOrNull{ - DocumentHighlights: &documentHighlights, - }, nil - } - documentHighlights := l.getSemanticDocumentHighlights(ctx, position, node, program, sourceFile) - if len(documentHighlights) == 0 { - documentHighlights = l.getSyntacticDocumentHighlights(node, sourceFile) - } - // if nil is passed here we never generate an error, just pass an empty higlight - return lsproto.DocumentHighlightsOrNull{DocumentHighlights: &documentHighlights}, nil -} - -func (l *LanguageService) getSemanticDocumentHighlights(ctx context.Context, position int, node *ast.Node, program *compiler.Program, sourceFile *ast.SourceFile) []*lsproto.DocumentHighlight { - options := refOptions{use: referenceUseReferences} - referenceEntries := l.getReferencedSymbolsForNode(ctx, position, node, program, []*ast.SourceFile{sourceFile}, options, &collections.Set[string]{}) - if referenceEntries == nil { - return nil - } - var highlights []*lsproto.DocumentHighlight - for _, entry := range referenceEntries { - for _, ref := range entry.references { - if ref.node != nil { - fileName, highlight := l.toDocumentHighlight(ref) - if fileName == sourceFile.FileName() { - highlights = append(highlights, highlight) - } - } - } - } - return highlights -} - -func (l *LanguageService) toDocumentHighlight(entry *referenceEntry) (string, *lsproto.DocumentHighlight) { - entry = l.resolveEntry(entry) - - kind := lsproto.DocumentHighlightKindRead - if entry.kind == entryKindRange { - return entry.fileName, &lsproto.DocumentHighlight{ - Range: *entry.textRange, - Kind: &kind, - } - } - - // Determine write access for node references. - if ast.IsWriteAccessForReference(entry.node) { - kind = lsproto.DocumentHighlightKindWrite - } - - dh := &lsproto.DocumentHighlight{ - Range: *entry.textRange, - Kind: &kind, - } - - return entry.fileName, dh -} - -func (l *LanguageService) getSyntacticDocumentHighlights(node *ast.Node, sourceFile *ast.SourceFile) []*lsproto.DocumentHighlight { - switch node.Kind { - case ast.KindIfKeyword, ast.KindElseKeyword: - if ast.IsIfStatement(node.Parent) { - return l.getIfElseOccurrences(node.Parent.AsIfStatement(), sourceFile) - } - return nil - case ast.KindReturnKeyword: - return l.useParent(node.Parent, ast.IsReturnStatement, getReturnOccurrences, sourceFile) - case ast.KindThrowKeyword: - return l.useParent(node.Parent, ast.IsThrowStatement, getThrowOccurrences, sourceFile) - case ast.KindTryKeyword, ast.KindCatchKeyword, ast.KindFinallyKeyword: - var tryStatement *ast.Node - if node.Kind == ast.KindCatchKeyword { - tryStatement = node.Parent.Parent - } else { - tryStatement = node.Parent - } - return l.useParent(tryStatement, ast.IsTryStatement, getTryCatchFinallyOccurrences, sourceFile) - case ast.KindSwitchKeyword: - return l.useParent(node.Parent, ast.IsSwitchStatement, getSwitchCaseDefaultOccurrences, sourceFile) - case ast.KindCaseKeyword, ast.KindDefaultKeyword: - if ast.IsDefaultClause(node.Parent) || ast.IsCaseClause(node.Parent) { - return l.useParent(node.Parent.Parent.Parent, ast.IsSwitchStatement, getSwitchCaseDefaultOccurrences, sourceFile) - } - return nil - case ast.KindBreakKeyword, ast.KindContinueKeyword: - return l.useParent(node.Parent, ast.IsBreakOrContinueStatement, getBreakOrContinueStatementOccurrences, sourceFile) - case ast.KindForKeyword, ast.KindWhileKeyword, ast.KindDoKeyword: - return l.useParent(node.Parent, func(n *ast.Node) bool { - return ast.IsIterationStatement(n, true) - }, getLoopBreakContinueOccurrences, sourceFile) - case ast.KindConstructorKeyword: - return l.getFromAllDeclarations(ast.IsConstructorDeclaration, []ast.Kind{ast.KindConstructorKeyword}, node, sourceFile) - case ast.KindGetKeyword, ast.KindSetKeyword: - return l.getFromAllDeclarations(ast.IsAccessor, []ast.Kind{ast.KindGetKeyword, ast.KindSetKeyword}, node, sourceFile) - case ast.KindAwaitKeyword: - return l.useParent(node.Parent, ast.IsAwaitExpression, getAsyncAndAwaitOccurrences, sourceFile) - case ast.KindAsyncKeyword: - return l.highlightSpans(getAsyncAndAwaitOccurrences(node, sourceFile), sourceFile) - case ast.KindYieldKeyword: - return l.highlightSpans(getYieldOccurrences(node, sourceFile), sourceFile) - case ast.KindInKeyword, ast.KindOutKeyword: - return nil - default: - if ast.IsModifierKind(node.Kind) && (ast.IsDeclaration(node.Parent) || ast.IsVariableStatement(node.Parent)) { - return l.highlightSpans(getModifierOccurrences(node.Kind, node.Parent, sourceFile), sourceFile) - } - return nil - } -} - -func (l *LanguageService) useParent(node *ast.Node, nodeTest func(*ast.Node) bool, getNodes func(*ast.Node, *ast.SourceFile) []*ast.Node, sourceFile *ast.SourceFile) []*lsproto.DocumentHighlight { - if nodeTest(node) { - return l.highlightSpans(getNodes(node, sourceFile), sourceFile) - } - return nil -} - -func (l *LanguageService) highlightSpans(nodes []*ast.Node, sourceFile *ast.SourceFile) []*lsproto.DocumentHighlight { - if len(nodes) == 0 { - return nil - } - var highlights []*lsproto.DocumentHighlight - kind := lsproto.DocumentHighlightKindRead - for _, node := range nodes { - if node != nil { - highlights = append(highlights, &lsproto.DocumentHighlight{ - Range: *l.createLspRangeFromNode(node, sourceFile), - Kind: &kind, - }) - } - } - return highlights -} - -func (l *LanguageService) getFromAllDeclarations(nodeTest func(*ast.Node) bool, keywords []ast.Kind, node *ast.Node, sourceFile *ast.SourceFile) []*lsproto.DocumentHighlight { - return l.useParent(node.Parent, nodeTest, func(decl *ast.Node, sf *ast.SourceFile) []*ast.Node { - var symbolDecls []*ast.Node - if ast.CanHaveSymbol(decl) { - if symbol := decl.Symbol(); symbol != nil { - for _, d := range symbol.Declarations { - if nodeTest(d) { - outer: - for _, c := range getChildrenFromNonJSDocNode(d, sourceFile) { - for _, k := range keywords { - if c.Kind == k { - symbolDecls = append(symbolDecls, c) - break outer - } - } - } - } - } - } - } - return symbolDecls - }, sourceFile) -} - -func (l *LanguageService) getIfElseOccurrences(ifStatement *ast.IfStatement, sourceFile *ast.SourceFile) []*lsproto.DocumentHighlight { - keywords := getIfElseKeywords(ifStatement, sourceFile) - kind := lsproto.DocumentHighlightKindRead - var highlights []*lsproto.DocumentHighlight - - // We'd like to highlight else/ifs together if they are only separated by whitespace - // (i.e. the keywords are separated by no comments, no newlines). - for i := 0; i < len(keywords); i++ { - if keywords[i].Kind == ast.KindElseKeyword && i < len(keywords)-1 { - elseKeyword := keywords[i] - ifKeyword := keywords[i+1] // this *should* always be an 'if' keyword. - shouldCombine := true - - // Avoid recalculating getStart() by iterating backwards. - ifTokenStart := scanner.GetTokenPosOfNode(ifKeyword, sourceFile, false) - if ifTokenStart < 0 { - ifTokenStart = ifKeyword.Pos() - } - for j := ifTokenStart - 1; j >= elseKeyword.End(); j-- { - if !stringutil.IsWhiteSpaceSingleLine(rune(sourceFile.Text()[j])) { - shouldCombine = false - break - } - } - if shouldCombine { - highlights = append(highlights, &lsproto.DocumentHighlight{ - Range: *l.createLspRangeFromBounds(scanner.SkipTrivia(sourceFile.Text(), elseKeyword.Pos()), ifKeyword.End(), sourceFile), - Kind: &kind, - }) - i++ // skip the next keyword - continue - } - } - // Ordinary case: just highlight the keyword. - highlights = append(highlights, &lsproto.DocumentHighlight{ - Range: *l.createLspRangeFromNode(keywords[i], sourceFile), - Kind: &kind, - }) - } - return highlights -} - -func getIfElseKeywords(ifStatement *ast.IfStatement, sourceFile *ast.SourceFile) []*ast.Node { - // Traverse upwards through all parent if-statements linked by their else-branches. - // Is this cast error safe or should i be checking if elseStatement exists first? - for ast.IsIfStatement(ifStatement.Parent) && ifStatement.Parent.AsIfStatement().ElseStatement.AsIfStatement() == ifStatement { - ifStatement = ifStatement.Parent.AsIfStatement() - } - - var keywords []*ast.Node - - // Traverse back down through the else branches, aggregating if/else keywords of if-statements. - for { - children := getChildrenFromNonJSDocNode(ifStatement.AsNode(), sourceFile) - if len(children) > 0 && children[0].Kind == ast.KindIfKeyword { - keywords = append(keywords, children[0]) - } - // Generally the 'else' keyword is second-to-last, so traverse backwards. - for i := len(children) - 1; i >= 0; i-- { - if children[i].Kind == ast.KindElseKeyword { - keywords = append(keywords, children[i]) - break - } - } - elseStatement := ifStatement.ElseStatement - if elseStatement == nil || !ast.IsIfStatement(elseStatement) { - break - } - ifStatement = elseStatement.AsIfStatement() - } - return keywords -} - -func getReturnOccurrences(node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node { - funcNode := ast.FindAncestor(node.Parent, ast.IsFunctionLike) - if funcNode == nil { - return nil - } - - var keywords []*ast.Node - body := funcNode.Body() - if body != nil { - ast.ForEachReturnStatement(body, func(ret *ast.Node) bool { - keyword := findChildOfKind(ret, ast.KindReturnKeyword, sourceFile) - if keyword != nil { - keywords = append(keywords, keyword) - } - return false // continue traversal - }) - - // Get all throw statements not in a try block - throwStatements := aggregateOwnedThrowStatements(body, sourceFile) - for _, throw := range throwStatements { - keyword := findChildOfKind(throw, ast.KindThrowKeyword, sourceFile) - if keyword != nil { - keywords = append(keywords, keyword) - } - } - } - return keywords -} - -func aggregateOwnedThrowStatements(node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node { - if ast.IsThrowStatement(node) { - return []*ast.Node{node} - } - if ast.IsTryStatement(node) { - // Exceptions thrown within a try block lacking a catch clause are "owned" in the current context. - statement := node.AsTryStatement() - tryBlock := statement.TryBlock - catchClause := statement.CatchClause - finallyBlock := statement.FinallyBlock - - var result []*ast.Node - if catchClause != nil { - result = aggregateOwnedThrowStatements(catchClause, sourceFile) - } else if tryBlock != nil { - result = aggregateOwnedThrowStatements(tryBlock, sourceFile) - } - if finallyBlock != nil { - result = append(result, aggregateOwnedThrowStatements(finallyBlock, sourceFile)...) - } - return result - } - // Do not cross function boundaries. - if ast.IsFunctionLike(node) { - return nil - } - return flatMapChildren(node, sourceFile, aggregateOwnedThrowStatements) -} - -func flatMapChildren[T any](node *ast.Node, sourceFile *ast.SourceFile, cb func(child *ast.Node, sourceFile *ast.SourceFile) []T) []T { - var result []T - - node.ForEachChild(func(child *ast.Node) bool { - value := cb(child, sourceFile) - if value != nil { - result = append(result, value...) - } - return false // continue traversal - }) - return result -} - -func getThrowOccurrences(node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node { - owner := getThrowStatementOwner(node) - if owner == nil { - return nil - } - - var keywords []*ast.Node - - // Aggregate all throw statements "owned" by this owner. - throwStatements := aggregateOwnedThrowStatements(owner, sourceFile) - for _, throw := range throwStatements { - keyword := findChildOfKind(throw, ast.KindThrowKeyword, sourceFile) - if keyword != nil { - keywords = append(keywords, keyword) - } - } - - // If the "owner" is a function, then we equate 'return' and 'throw' statements in their - // ability to "jump out" of the function, and include occurrences for both - if ast.IsFunctionBlock(owner) { - ast.ForEachReturnStatement(owner, func(ret *ast.Node) bool { - keyword := findChildOfKind(ret, ast.KindReturnKeyword, sourceFile) - if keyword != nil { - keywords = append(keywords, keyword) - } - return false // continue traversal - }) - } - - return keywords -} - -// For lack of a better name, this function takes a throw statement and returns the -// nearest ancestor that is a try-block (whose try statement has a catch clause), -// function-block, or source file. -func getThrowStatementOwner(throwStatement *ast.Node) *ast.Node { - child := throwStatement - for child.Parent != nil { - parent := child.Parent - - if ast.IsFunctionBlock(parent) || parent.Kind == ast.KindSourceFile { - return parent - } - - // A throw-statement is only owned by a try-statement if the try-statement has - // a catch clause, and if the throw-statement occurs within the try block. - if ast.IsTryStatement(parent) { - tryStatement := parent.AsTryStatement() - if tryStatement.TryBlock == child && tryStatement.CatchClause != nil { - return child - } - } - - child = parent - } - return nil -} - -func getTryCatchFinallyOccurrences(node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node { - tryStatement := node.AsTryStatement() - - var keywords []*ast.Node - token := lsutil.GetFirstToken(node, sourceFile) - if token.Kind == ast.KindTryKeyword { - keywords = append(keywords, token) - } - - if tryStatement.CatchClause != nil { - catchToken := lsutil.GetFirstToken(tryStatement.CatchClause.AsNode(), sourceFile) - if catchToken.Kind == ast.KindCatchKeyword { - keywords = append(keywords, catchToken) - } - } - - if tryStatement.FinallyBlock != nil { - finallyKeyword := findChildOfKind(node, ast.KindFinallyKeyword, sourceFile) - if finallyKeyword.Kind == ast.KindFinallyKeyword { - keywords = append(keywords, finallyKeyword) - } - } - - return keywords -} - -func getSwitchCaseDefaultOccurrences(node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node { - switchStatement := node.AsSwitchStatement() - - var keywords []*ast.Node - token := lsutil.GetFirstToken(node, sourceFile) - if token.Kind == ast.KindSwitchKeyword { - keywords = append(keywords, token) - } - - clauses := switchStatement.CaseBlock.AsCaseBlock().Clauses - for _, clause := range clauses.Nodes { - clauseToken := lsutil.GetFirstToken(clause.AsNode(), sourceFile) - if clauseToken.Kind == ast.KindCaseKeyword || clauseToken.Kind == ast.KindDefaultKeyword { - keywords = append(keywords, clauseToken) - } - - breakAndContinueStatements := aggregateAllBreakAndContinueStatements(clause, sourceFile) - for _, statement := range breakAndContinueStatements { - if statement.Kind == ast.KindBreakStatement && ownsBreakOrContinueStatement(switchStatement.AsNode(), statement) { - keywords = append(keywords, lsutil.GetFirstToken(statement, sourceFile)) - } - } - } - - return keywords -} - -func aggregateAllBreakAndContinueStatements(node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node { - if ast.IsBreakOrContinueStatement(node) { - return []*ast.Node{node} - } - if ast.IsFunctionLike(node) { - return nil - } - return flatMapChildren(node, sourceFile, aggregateAllBreakAndContinueStatements) -} - -func ownsBreakOrContinueStatement(owner *ast.Node, statement *ast.Node) bool { - actualOwner := getBreakOrContinueOwner(statement) - if actualOwner == nil { - return false - } - return actualOwner == owner -} - -func getBreakOrContinueOwner(statement *ast.Node) *ast.Node { - return ast.FindAncestorOrQuit(statement, func(node *ast.Node) ast.FindAncestorResult { - switch node.Kind { - case ast.KindSwitchStatement: - if statement.Kind == ast.KindContinueStatement { - return ast.FindAncestorFalse - } - fallthrough - case ast.KindForStatement, - ast.KindForInStatement, - ast.KindForOfStatement, - ast.KindWhileStatement, - ast.KindDoStatement: - // If the statement is labeled, check if the node is labeled by the statement's label. - if statement.Label() == nil || isLabeledBy(node, statement.Label().Text()) { - return ast.FindAncestorTrue - } - return ast.FindAncestorFalse - default: - // Don't cross function boundaries. - if ast.IsFunctionLike(node) { - return ast.FindAncestorQuit - } - return ast.FindAncestorFalse - } - }) -} - -// Whether or not a 'node' is preceded by a label of the given string. -// Note: 'node' cannot be a SourceFile. -func isLabeledBy(node *ast.Node, labelName string) bool { - return ast.FindAncestorOrQuit(node.Parent, func(owner *ast.Node) ast.FindAncestorResult { - if !ast.IsLabeledStatement(owner) { - return ast.FindAncestorQuit - } - if owner.Label().Text() == labelName { - return ast.FindAncestorTrue - } - return ast.FindAncestorFalse - }) != nil -} - -func getBreakOrContinueStatementOccurrences(node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node { - if owner := getBreakOrContinueOwner(node); owner != nil { - switch owner.Kind { - case ast.KindForStatement, ast.KindForInStatement, ast.KindForOfStatement, ast.KindDoStatement, ast.KindWhileStatement: - return getLoopBreakContinueOccurrences(owner, sourceFile) - case ast.KindSwitchStatement: - return getSwitchCaseDefaultOccurrences(owner, sourceFile) - } - } - return nil -} - -func getLoopBreakContinueOccurrences(node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node { - var keywords []*ast.Node - - token := lsutil.GetFirstToken(node, sourceFile) - if token.Kind == ast.KindForKeyword || token.Kind == ast.KindDoKeyword || token.Kind == ast.KindWhileKeyword { - keywords = append(keywords, token) - if node.Kind == ast.KindDoStatement { - loopTokens := getChildrenFromNonJSDocNode(node, sourceFile) - for i := len(loopTokens) - 1; i >= 0; i-- { - if loopTokens[i].Kind == ast.KindWhileKeyword { - keywords = append(keywords, loopTokens[i]) - break - } - } - } - } - - breakAndContinueStatements := aggregateAllBreakAndContinueStatements(node, sourceFile) - for _, statement := range breakAndContinueStatements { - token := lsutil.GetFirstToken(statement, sourceFile) - if ownsBreakOrContinueStatement(node, statement) && (token.Kind == ast.KindBreakKeyword || token.Kind == ast.KindContinueKeyword) { - keywords = append(keywords, token) - } - } - - return keywords -} - -func getAsyncAndAwaitOccurrences(node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node { - fun := ast.GetContainingFunction(node) - if fun == nil { - return nil - } - - var keywords []*ast.Node - - modifiers := fun.Modifiers() - if modifiers != nil { - for _, modifier := range modifiers.Nodes { - if modifier.Kind == ast.KindAsyncKeyword { - keywords = append(keywords, modifier) - } - } - } - - fun.ForEachChild(func(child *ast.Node) bool { - traverseWithoutCrossingFunction(child, sourceFile, func(child *ast.Node) { - if ast.IsAwaitExpression(child) { - token := lsutil.GetFirstToken(child, sourceFile) - if token.Kind == ast.KindAwaitKeyword { - keywords = append(keywords, token) - } - } - }) - return false // continue traversal - }) - - return keywords -} - -func getYieldOccurrences(node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node { - parentFunc := ast.FindAncestor(node.Parent, ast.IsFunctionLike).AsFunctionDeclaration() - if parentFunc == nil { - return nil - } - - var keywords []*ast.Node - - parentFunc.ForEachChild(func(child *ast.Node) bool { - traverseWithoutCrossingFunction(child, sourceFile, func(child *ast.Node) { - if ast.IsYieldExpression(child) { - token := lsutil.GetFirstToken(child, sourceFile) - if token.Kind == ast.KindYieldKeyword { - keywords = append(keywords, token) - } - } - }) - return false // continue traversal - }) - - return keywords -} - -func traverseWithoutCrossingFunction(node *ast.Node, sourceFile *ast.SourceFile, cb func(*ast.Node)) { - cb(node) - if !ast.IsFunctionLike(node) && !ast.IsClassLike(node) && !ast.IsInterfaceDeclaration(node) && !ast.IsModuleDeclaration(node) && !ast.IsTypeAliasDeclaration(node) && !ast.IsTypeNode(node) { - node.ForEachChild(func(child *ast.Node) bool { - traverseWithoutCrossingFunction(child, sourceFile, cb) - return false // continue traversal - }) - } -} - -func getModifierOccurrences(kind ast.Kind, node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node { - var result []*ast.Node - - nodesToSearch := getNodesToSearchForModifier(node, ast.ModifierToFlag(kind)) - for _, n := range nodesToSearch { - modifier := findModifier(n, kind) - if modifier != nil { - result = append(result, modifier) - } - } - return result -} - -func getNodesToSearchForModifier(declaration *ast.Node, modifierFlag ast.ModifierFlags) []*ast.Node { - var result []*ast.Node - - container := declaration.Parent - if container == nil { - return nil - } - - // Types of node whose children might have modifiers. - switch container.Kind { - case ast.KindModuleBlock, ast.KindSourceFile, ast.KindBlock, ast.KindCaseClause, ast.KindDefaultClause: - // Container is either a class declaration or the declaration is a classDeclaration - if (modifierFlag&ast.ModifierFlagsAbstract) != 0 && ast.IsClassDeclaration(declaration) { - return append(append(result, declaration.Members()...), declaration) - } else { - return append(result, container.Statements()...) - } - case ast.KindConstructor, ast.KindMethodDeclaration, ast.KindFunctionDeclaration: - // Parameters and, if inside a class, also class members - result = append(result, container.Parameters()...) - if ast.IsClassLike(container.Parent) { - result = append(result, container.Parent.Members()...) - } - return result - case ast.KindClassDeclaration, ast.KindClassExpression, ast.KindInterfaceDeclaration, ast.KindTypeLiteral: - nodes := container.Members() - result = append(result, nodes...) - // If we're an accessibility modifier, we're in an instance member and should search - // the constructor's parameter list for instance members as well. - if (modifierFlag & (ast.ModifierFlagsAccessibilityModifier | ast.ModifierFlagsReadonly)) != 0 { - var constructor *ast.Node - - for _, member := range nodes { - if ast.IsConstructorDeclaration(member) { - constructor = member - break - } - } - if constructor != nil { - result = append(result, constructor.Parameters()...) - } - } else if (modifierFlag & ast.ModifierFlagsAbstract) != 0 { - result = append(result, container) - } - return result - default: - // Syntactically invalid positions or unsupported containers - return nil - } -} - -func findModifier(node *ast.Node, kind ast.Kind) *ast.Node { - if modifiers := node.Modifiers(); modifiers != nil { - for _, modifier := range modifiers.Nodes { - if modifier.Kind == kind { - return modifier - } - } - } - return nil -} diff --git a/kitcom/internal/tsgo/ls/findallreferences.go b/kitcom/internal/tsgo/ls/findallreferences.go deleted file mode 100644 index 2d0acb3..0000000 --- a/kitcom/internal/tsgo/ls/findallreferences.go +++ /dev/null @@ -1,2045 +0,0 @@ -package ls - -import ( - "cmp" - "context" - "fmt" - "slices" - "strings" - - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/binder" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/checker" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/compiler" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/debug" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/stringutil" - - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath" -) - -// === types for settings === -type referenceUse int - -const ( - referenceUseNone referenceUse = 0 - referenceUseOther referenceUse = 1 - referenceUseReferences referenceUse = 2 - referenceUseRename referenceUse = 3 -) - -type refOptions struct { - findInStrings bool - findInComments bool - use referenceUse // other, references, rename - implementations bool - useAliasesForRename bool // renamed from providePrefixAndSuffixTextForRename. default: true -} - -// === types for results === - -type refInfo struct { - file *ast.SourceFile - fileName string - reference *ast.FileReference - unverified bool -} - -type SymbolAndEntries struct { - definition *Definition - references []*referenceEntry -} - -func NewSymbolAndEntries(kind definitionKind, node *ast.Node, symbol *ast.Symbol, references []*referenceEntry) *SymbolAndEntries { - return &SymbolAndEntries{ - &Definition{ - Kind: kind, - node: node, - symbol: symbol, - }, - references, - } -} - -type definitionKind int - -const ( - definitionKindSymbol definitionKind = 0 - definitionKindLabel definitionKind = 1 - definitionKindKeyword definitionKind = 2 - definitionKindThis definitionKind = 3 - definitionKindString definitionKind = 4 - definitionKindTripleSlashReference definitionKind = 5 -) - -type Definition struct { - Kind definitionKind - symbol *ast.Symbol - node *ast.Node - tripleSlashFileRef *tripleSlashDefinition -} -type tripleSlashDefinition struct { - reference *ast.FileReference - file *ast.SourceFile -} - -type entryKind int - -const ( - entryKindNone entryKind = 0 - entryKindRange entryKind = 1 - entryKindNode entryKind = 2 - entryKindStringLiteral entryKind = 3 - entryKindSearchedLocalFoundProperty entryKind = 4 - entryKindSearchedPropertyFoundLocal entryKind = 5 -) - -type referenceEntry struct { - kind entryKind - node *ast.Node - context *ast.Node // !!! ContextWithStartAndEndNode, optional - fileName string - textRange *lsproto.Range -} - -func (l *LanguageService) getRangeOfEntry(entry *referenceEntry) *lsproto.Range { - return l.resolveEntry(entry).textRange -} - -func (l *LanguageService) getFileNameOfEntry(entry *referenceEntry) string { - return l.resolveEntry(entry).fileName -} - -func (l *LanguageService) resolveEntry(entry *referenceEntry) *referenceEntry { - if entry.textRange == nil { - sourceFile := ast.GetSourceFileOfNode(entry.node) - entry.textRange = l.getRangeOfNode(entry.node, sourceFile, nil /*endNode*/) - entry.fileName = sourceFile.FileName() - } - return entry -} - -func (l *LanguageService) newRangeEntry(file *ast.SourceFile, start, end int) *referenceEntry { - // !!! used in not-yet implemented features - return &referenceEntry{ - kind: entryKindRange, - fileName: file.FileName(), - textRange: l.createLspRangeFromBounds(start, end, file), - } -} - -func newNodeEntryWithKind(node *ast.Node, kind entryKind) *referenceEntry { - e := newNodeEntry(node) - e.kind = kind - return e -} - -func newNodeEntry(node *ast.Node) *referenceEntry { - // creates nodeEntry with `kind == entryKindNode` - return &referenceEntry{ - kind: entryKindNode, - node: core.OrElse(node.Name(), node), - context: getContextNodeForNodeEntry(node), - } -} - -func getContextNodeForNodeEntry(node *ast.Node) *ast.Node { - if ast.IsDeclaration(node) { - return getContextNode(node) - } - - if node.Parent == nil { - return nil - } - - if !ast.IsDeclaration(node.Parent) && node.Parent.Kind != ast.KindExportAssignment && node.Parent.Kind != ast.KindJSExportAssignment { - // Special property assignment in javascript - if ast.IsInJSFile(node) { - // !!! jsdoc: check if branch still needed - binaryExpression := core.IfElse(node.Parent.Kind == ast.KindBinaryExpression, - node.Parent, - core.IfElse(ast.IsAccessExpression(node.Parent) && node.Parent.Parent.Kind == ast.KindBinaryExpression && node.Parent.Parent.AsBinaryExpression().Left == node.Parent, - node.Parent.Parent, - nil)) - if binaryExpression != nil && ast.GetAssignmentDeclarationKind(binaryExpression.AsBinaryExpression()) != ast.JSDeclarationKindNone { - return getContextNode(binaryExpression) - } - } - - // Jsx Tags - switch node.Parent.Kind { - case ast.KindJsxOpeningElement, ast.KindJsxClosingElement: - return node.Parent.Parent - case ast.KindJsxSelfClosingElement, ast.KindLabeledStatement, ast.KindBreakStatement, ast.KindContinueStatement: - return node.Parent - case ast.KindStringLiteral, ast.KindNoSubstitutionTemplateLiteral: - if validImport := tryGetImportFromModuleSpecifier(node); validImport != nil { - declOrStatement := ast.FindAncestor(validImport, func(*ast.Node) bool { - return ast.IsDeclaration(node) || ast.IsStatement(node) || ast.IsJSDocTag(node) - }) - if ast.IsDeclaration(declOrStatement) { - return getContextNode(declOrStatement) - } - return declOrStatement - } - } - - // Handle computed property name - propertyName := ast.FindAncestor(node, ast.IsComputedPropertyName) - if propertyName != nil { - return getContextNode(propertyName.Parent) - } - return nil - } - - if node.Parent.Name() == node || // node is name of declaration, use parent - node.Parent.Kind == ast.KindConstructor || - node.Parent.Kind == ast.KindExportAssignment || - node.Parent.Kind == ast.KindJSExportAssignment || - // Property name of the import export specifier or binding pattern, use parent - ((ast.IsImportOrExportSpecifier(node.Parent) || node.Parent.Kind == ast.KindBindingElement) && node.Parent.PropertyName() == node) || - // Is default export - (node.Kind == ast.KindDefaultKeyword && ast.HasSyntacticModifier(node.Parent, ast.ModifierFlagsExportDefault)) { - return getContextNode(node.Parent) - } - - return nil -} - -func getContextNode(node *ast.Node) *ast.Node { - if node == nil { - return nil - } - switch node.Kind { - case ast.KindVariableDeclaration: - if !ast.IsVariableDeclarationList(node.Parent) || len(node.Parent.AsVariableDeclarationList().Declarations.Nodes) != 1 { - return node - } else if ast.IsVariableStatement(node.Parent.Parent) { - return node.Parent.Parent - } else if ast.IsForInOrOfStatement(node.Parent.Parent) { - return getContextNode(node.Parent.Parent) - } - return node.Parent - - case ast.KindBindingElement: - return getContextNode(node.Parent.Parent) - - case ast.KindImportSpecifier: - return node.Parent.Parent.Parent - - case ast.KindExportSpecifier, ast.KindNamespaceImport: - return node.Parent.Parent - - case ast.KindImportClause, ast.KindNamespaceExport: - return node.Parent - - case ast.KindBinaryExpression: - return core.IfElse(node.Parent.Kind == ast.KindExpressionStatement, node.Parent, node) - - case ast.KindForOfStatement, ast.KindForInStatement: - // !!! not implemented - return nil - - case ast.KindPropertyAssignment, ast.KindShorthandPropertyAssignment: - if ast.IsArrayLiteralOrObjectLiteralDestructuringPattern(node.Parent) { - return getContextNode(ast.FindAncestor(node.Parent, func(node *ast.Node) bool { - return node.Kind == ast.KindBinaryExpression || ast.IsForInOrOfStatement(node) - })) - } - return node - case ast.KindSwitchStatement: - // !!! not implemented - return nil - default: - return node - } -} - -// utils -func (l *LanguageService) getRangeOfNode(node *ast.Node, sourceFile *ast.SourceFile, endNode *ast.Node) *lsproto.Range { - if sourceFile == nil { - sourceFile = ast.GetSourceFileOfNode(node) - } - start := scanner.GetTokenPosOfNode(node, sourceFile, false /*includeJsDoc*/) - end := core.IfElse(endNode != nil, endNode, node).End() - if ast.IsStringLiteralLike(node) && (end-start) > 2 { - if endNode != nil { - panic("endNode is not nil for stringLiteralLike") - } - start += 1 - end -= 1 - } - if endNode != nil && endNode.Kind == ast.KindCaseBlock { - end = endNode.Pos() - } - return l.createLspRangeFromBounds(start, end, sourceFile) -} - -func isValidReferencePosition(node *ast.Node, searchSymbolName string) bool { - switch node.Kind { - case ast.KindPrivateIdentifier: - // !!! - // if (isJSDocMemberName(node.Parent)) { - // return true; - // } - return len(node.Text()) == len(searchSymbolName) - case ast.KindIdentifier: - return len(node.Text()) == len(searchSymbolName) - case ast.KindNoSubstitutionTemplateLiteral, ast.KindStringLiteral: - return len(node.Text()) == len(searchSymbolName) && (isLiteralNameOfPropertyDeclarationOrIndexAccess(node) || - isNameOfModuleDeclaration(node) || - isExpressionOfExternalModuleImportEqualsDeclaration(node) || - // !!! object.defineProperty - // (ast.IsCallExpression(node.Parent) && ast.IsBindableObjectDefinePropertyCall(node.Parent) && node.Parent.Arguments()[1] == node) || - ast.IsImportOrExportSpecifier(node.Parent)) - case ast.KindNumericLiteral: - return isLiteralNameOfPropertyDeclarationOrIndexAccess(node) && len(node.Text()) == len(searchSymbolName) - case ast.KindDefaultKeyword: - return len("default") == len(searchSymbolName) - } - return false -} - -func isForRenameWithPrefixAndSuffixText(options refOptions) bool { - return options.use == referenceUseRename && options.useAliasesForRename -} - -func skipPastExportOrImportSpecifierOrUnion(symbol *ast.Symbol, node *ast.Node, checker *checker.Checker, useLocalSymbolForExportSpecifier bool) *ast.Symbol { - if node == nil { - return nil - } - parent := node.Parent - if parent.Kind == ast.KindExportSpecifier && useLocalSymbolForExportSpecifier { - return getLocalSymbolForExportSpecifier(node.AsIdentifier(), symbol, parent.AsExportSpecifier(), checker) - } - // If the symbol is declared as part of a declaration like `{ type: "a" } | { type: "b" }`, use the property on the union type to get more references. - return core.FirstNonNil(symbol.Declarations, func(decl *ast.Node) *ast.Symbol { - if decl.Parent == nil { - // Ignore UMD module and global merge - if symbol.Flags&ast.SymbolFlagsTransient != 0 { - return nil - } - // Assertions for GH#21814. We should be handling SourceFile symbols in `getReferencedSymbolsForModule` instead of getting here. - panic(fmt.Sprintf("Unexpected symbol at %s: %s", node.Kind.String(), symbol.Name)) - } - if decl.Parent.Kind == ast.KindTypeLiteral && decl.Parent.Parent.Kind == ast.KindUnionType { - return checker.GetPropertyOfType(checker.GetTypeFromTypeNode(decl.Parent.Parent), symbol.Name) - } - return nil - }) -} - -func getSymbolScope(symbol *ast.Symbol) *ast.Node { - // If this is the symbol of a named function expression or named class expression, - // then named references are limited to its own scope. - valueDeclaration := symbol.ValueDeclaration - if valueDeclaration != nil && (valueDeclaration.Kind == ast.KindFunctionExpression || valueDeclaration.Kind == ast.KindClassExpression) { - return valueDeclaration - } - - if len(symbol.Declarations) == 0 { - return nil - } - - declarations := symbol.Declarations - // If this is private property or method, the scope is the containing class - if symbol.Flags&(ast.SymbolFlagsProperty|ast.SymbolFlagsMethod) != 0 { - privateDeclaration := core.Find(declarations, func(d *ast.Node) bool { - return ast.HasModifier(d, ast.ModifierFlagsPrivate) || ast.IsPrivateIdentifierClassElementDeclaration(d) - }) - if privateDeclaration != nil { - return ast.FindAncestorKind(privateDeclaration, ast.KindClassDeclaration) - } - // Else this is a public property and could be accessed from anywhere. - return nil - } - - // If symbol is of object binding pattern element without property name we would want to - // look for property too and that could be anywhere - if core.Some(declarations, isObjectBindingElementWithoutPropertyName) { - return nil - } - - /* - If the symbol has a parent, it's globally visible unless: - - It's a private property (handled above). - - It's a type parameter. - - The parent is an external module: then we should only search in the module (and recurse on the export later). - - But if the parent has `export as namespace`, the symbol is globally visible through that namespace. - */ - exposedByParent := symbol.Parent != nil && symbol.Flags&ast.SymbolFlagsTypeParameter == 0 - if exposedByParent && !(checker.IsExternalModuleSymbol(symbol.Parent) && symbol.Parent.GlobalExports == nil) { - return nil - } - - var scope *ast.Node - for _, declaration := range declarations { - container := getContainerNode(declaration) - if scope != nil && scope != container { - // Different declarations have different containers, bail out - return nil - } - - if container == nil || (container.Kind == ast.KindSourceFile && !ast.IsExternalOrCommonJSModule(container.AsSourceFile())) { - // This is a global variable and not an external module, any declaration defined - // within this scope is visible outside the file - return nil - } - - scope = container - } - - // If symbol.parent, this means we are in an export of an external module. (Otherwise we would have returned `undefined` above.) - // For an export of a module, we may be in a declaration file, and it may be accessed elsewhere. E.g.: - // declare module "a" { export type T = number; } - // declare module "b" { import { T } from "a"; export const x: T; } - // So we must search the whole source file. (Because we will mark the source file as seen, we we won't return to it when searching for imports.) - if exposedByParent { - return ast.GetSourceFileOfNode(scope).AsNode() - } - return scope // TODO: GH#18217 -} - -// === functions on (*ls) === - -func (l *LanguageService) ProvideReferences(ctx context.Context, params *lsproto.ReferenceParams) (lsproto.ReferencesResponse, error) { - // `findReferencedSymbols` except only computes the information needed to return reference locations - program, sourceFile := l.getProgramAndFile(params.TextDocument.Uri) - position := int(l.converters.LineAndCharacterToPosition(sourceFile, params.Position)) - - node := astnav.GetTouchingPropertyName(sourceFile, position) - options := refOptions{use: referenceUseReferences} - - symbolsAndEntries := l.getReferencedSymbolsForNode(ctx, position, node, program, program.GetSourceFiles(), options, nil) - - locations := core.FlatMap(symbolsAndEntries, l.convertSymbolAndEntriesToLocations) - return lsproto.LocationsOrNull{Locations: &locations}, nil -} - -func (l *LanguageService) ProvideImplementations(ctx context.Context, params *lsproto.ImplementationParams) (lsproto.ImplementationResponse, error) { - program, sourceFile := l.getProgramAndFile(params.TextDocument.Uri) - position := int(l.converters.LineAndCharacterToPosition(sourceFile, params.Position)) - node := astnav.GetTouchingPropertyName(sourceFile, position) - - var seenNodes collections.Set[*ast.Node] - var entries []*referenceEntry - queue := l.getImplementationReferenceEntries(ctx, program, node, position) - for len(queue) != 0 { - if ctx.Err() != nil { - return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{}, ctx.Err() - } - - entry := queue[0] - queue = queue[1:] - if !seenNodes.Has(entry.node) { - seenNodes.Add(entry.node) - entries = append(entries, entry) - queue = append(queue, l.getImplementationReferenceEntries(ctx, program, entry.node, entry.node.Pos())...) - } - } - - locations := l.convertEntriesToLocations(entries) - return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{Locations: &locations}, nil -} - -func (l *LanguageService) getImplementationReferenceEntries(ctx context.Context, program *compiler.Program, node *ast.Node, position int) []*referenceEntry { - options := refOptions{use: referenceUseReferences, implementations: true} - symbolsAndEntries := l.getReferencedSymbolsForNode(ctx, position, node, program, program.GetSourceFiles(), options, nil) - return core.FlatMap(symbolsAndEntries, func(s *SymbolAndEntries) []*referenceEntry { return s.references }) -} - -func (l *LanguageService) ProvideRename(ctx context.Context, params *lsproto.RenameParams) (lsproto.WorkspaceEditOrNull, error) { - program, sourceFile := l.getProgramAndFile(params.TextDocument.Uri) - position := int(l.converters.LineAndCharacterToPosition(sourceFile, params.Position)) - node := astnav.GetTouchingPropertyName(sourceFile, position) - if node.Kind != ast.KindIdentifier { - return lsproto.WorkspaceEditOrNull{}, nil - } - options := refOptions{use: referenceUseRename, useAliasesForRename: true} - symbolsAndEntries := l.getReferencedSymbolsForNode(ctx, position, node, program, program.GetSourceFiles(), options, nil) - entries := core.FlatMap(symbolsAndEntries, func(s *SymbolAndEntries) []*referenceEntry { return s.references }) - changes := make(map[lsproto.DocumentUri][]*lsproto.TextEdit) - checker, done := program.GetTypeChecker(ctx) - defer done() - for _, entry := range entries { - uri := FileNameToDocumentURI(l.getFileNameOfEntry(entry)) - textEdit := &lsproto.TextEdit{ - Range: *l.getRangeOfEntry(entry), - NewText: l.getTextForRename(node, entry, params.NewName, checker), - } - changes[uri] = append(changes[uri], textEdit) - } - return lsproto.WorkspaceEditOrNull{ - WorkspaceEdit: &lsproto.WorkspaceEdit{ - Changes: &changes, - }, - }, nil -} - -func (l *LanguageService) getTextForRename(originalNode *ast.Node, entry *referenceEntry, newText string, checker *checker.Checker) string { - if entry.kind != entryKindRange && (ast.IsIdentifier(originalNode) || ast.IsStringLiteralLike(originalNode)) { - node := entry.node - kind := entry.kind - parent := node.Parent - name := originalNode.Text() - isShorthandAssignment := ast.IsShorthandPropertyAssignment(parent) - switch { - case isShorthandAssignment || (isObjectBindingElementWithoutPropertyName(parent) && parent.Name() == node && parent.AsBindingElement().DotDotDotToken == nil): - if kind == entryKindSearchedLocalFoundProperty { - return name + ": " + newText - } - if kind == entryKindSearchedPropertyFoundLocal { - return newText + ": " + name - } - // In `const o = { x }; o.x`, symbolAtLocation at `x` in `{ x }` is the property symbol. - // For a binding element `const { x } = o;`, symbolAtLocation at `x` is the property symbol. - if isShorthandAssignment { - grandParent := parent.Parent - if ast.IsObjectLiteralExpression(grandParent) && ast.IsBinaryExpression(grandParent.Parent) && ast.IsModuleExportsAccessExpression(grandParent.Parent.AsBinaryExpression().Left) { - return name + ": " + newText - } - return newText + ": " + name - } - return name + ": " + newText - case ast.IsImportSpecifier(parent) && parent.PropertyName() == nil: - // If the original symbol was using this alias, just rename the alias. - var originalSymbol *ast.Symbol - if ast.IsExportSpecifier(originalNode.Parent) { - originalSymbol = checker.GetExportSpecifierLocalTargetSymbol(originalNode.Parent) - } else { - originalSymbol = checker.GetSymbolAtLocation(originalNode) - } - if slices.Contains(originalSymbol.Declarations, parent) { - return name + " as " + newText - } - return newText - case ast.IsExportSpecifier(parent) && parent.PropertyName() == nil: - // If the symbol for the node is same as declared node symbol use prefix text - if originalNode == entry.node || checker.GetSymbolAtLocation(originalNode) == checker.GetSymbolAtLocation(entry.node) { - return name + " as " + newText - } - return newText + " as " + name - } - } - return newText -} - -// == functions for conversions == -func (l *LanguageService) convertSymbolAndEntriesToLocations(s *SymbolAndEntries) []lsproto.Location { - return l.convertEntriesToLocations(s.references) -} - -func (l *LanguageService) convertEntriesToLocations(entries []*referenceEntry) []lsproto.Location { - locations := make([]lsproto.Location, len(entries)) - for i, entry := range entries { - locations[i] = lsproto.Location{ - Uri: FileNameToDocumentURI(l.getFileNameOfEntry(entry)), - Range: *l.getRangeOfEntry(entry), - } - } - return locations -} - -func (l *LanguageService) mergeReferences(program *compiler.Program, referencesToMerge ...[]*SymbolAndEntries) []*SymbolAndEntries { - result := []*SymbolAndEntries{} - getSourceFileIndexOfEntry := func(program *compiler.Program, entry *referenceEntry) int { - var sourceFile *ast.SourceFile - if entry.kind == entryKindRange { - sourceFile = program.GetSourceFile(entry.fileName) - } else { - sourceFile = ast.GetSourceFileOfNode(entry.node) - } - return slices.Index(program.SourceFiles(), sourceFile) - } - - for _, references := range referencesToMerge { - if len(references) == 0 { - continue - } - if len(result) == 0 { - result = references - continue - } - for _, entry := range references { - if entry.definition == nil || entry.definition.Kind != definitionKindSymbol { - result = append(result, entry) - continue - } - symbol := entry.definition.symbol - refIndex := core.FindIndex(result, func(ref *SymbolAndEntries) bool { - return ref.definition != nil && - ref.definition.Kind == definitionKindSymbol && - ref.definition.symbol == symbol - }) - if refIndex == -1 { - result = append(result, entry) - continue - } - - reference := result[refIndex] - sortedRefs := append(reference.references, entry.references...) - slices.SortStableFunc(sortedRefs, func(entry1, entry2 *referenceEntry) int { - entry1File := getSourceFileIndexOfEntry(program, entry1) - entry2File := getSourceFileIndexOfEntry(program, entry2) - if entry1File != entry2File { - return cmp.Compare(entry1File, entry2File) - } - - return CompareRanges(l.getRangeOfEntry(entry1), l.getRangeOfEntry(entry2)) - }) - result[refIndex] = &SymbolAndEntries{ - definition: reference.definition, - references: sortedRefs, - } - } - } - return result -} - -// === functions for find all ref implementation === - -func (l *LanguageService) getReferencedSymbolsForNode(ctx context.Context, position int, node *ast.Node, program *compiler.Program, sourceFiles []*ast.SourceFile, options refOptions, sourceFilesSet *collections.Set[string]) []*SymbolAndEntries { - // !!! cancellationToken - if sourceFilesSet == nil || sourceFilesSet.Len() == 0 { - sourceFilesSet = collections.NewSetWithSizeHint[string](len(sourceFiles)) - for _, file := range sourceFiles { - sourceFilesSet.Add(file.FileName()) - } - } - - checker, done := program.GetTypeChecker(ctx) - defer done() - - if node.Kind == ast.KindSourceFile { - resolvedRef := getReferenceAtPosition(node.AsSourceFile(), position, program) - if resolvedRef == nil || resolvedRef.file == nil { - return nil - } - - if moduleSymbol := checker.GetMergedSymbol(resolvedRef.file.Symbol); moduleSymbol != nil { - return getReferencedSymbolsForModule(ctx, program, moduleSymbol /*excludeImportTypeOfExportEquals*/, false, sourceFiles, sourceFilesSet) - } - - // !!! not implemented - // fileIncludeReasons := program.getFileIncludeReasons(); - // if (!fileIncludeReasons) { - // return nil - // } - return []*SymbolAndEntries{{ - definition: &Definition{Kind: definitionKindTripleSlashReference, tripleSlashFileRef: &tripleSlashDefinition{reference: resolvedRef.reference}}, - references: getReferencesForNonModule(resolvedRef.file, program /*fileIncludeReasons,*/), - }} - } - - if !options.implementations { - // !!! cancellationToken - if special := getReferencedSymbolsSpecial(node, sourceFiles); special != nil { - return special - } - } - - // constructors should use the class symbol, detected by name, if present - symbol := checker.GetSymbolAtLocation(core.IfElse(node.Kind == ast.KindConstructor && node.Parent.Name() != nil, node.Parent.Name(), node)) - // Could not find a symbol e.g. unknown identifier - if symbol == nil { - // String literal might be a property (and thus have a symbol), so do this here rather than in getReferencedSymbolsSpecial. - if !options.implementations && ast.IsStringLiteralLike(node) { - if isModuleSpecifierLike(node) { - // !!! not implemented - // fileIncludeReasons := program.GetFileIncludeReasons() - // if referencedFile := program.GetResolvedModuleFromModuleSpecifier(node, nil /*sourceFile*/); referencedFile != nil { - // return []*SymbolAndEntries{{ - // definition: &Definition{Kind: definitionKindString, node: node}, - // references: getReferencesForNonModule(referencedFile, program /*fileIncludeReasons,*/), - // }} - // } - // Fall through to string literal references. This is not very likely to return - // anything useful, but I guess it's better than nothing, and there's an existing - // test that expects this to happen (fourslash/cases/untypedModuleImport.ts). - } - // !!! not implemented - // return getReferencesForStringLiteral(node, sourceFiles, checker) // !!! cancellationToken - return nil - } - return nil - } - - if symbol.Name == ast.InternalSymbolNameExportEquals { - return getReferencedSymbolsForModule(ctx, program, symbol.Parent, false /*excludeImportTypeOfExportEquals*/, sourceFiles, sourceFilesSet) - } - - moduleReferences := l.getReferencedSymbolsForModuleIfDeclaredBySourceFile(ctx, symbol, program, sourceFiles, checker, options, sourceFilesSet) // !!! cancellationToken - if moduleReferences != nil && symbol.Flags&ast.SymbolFlagsTransient == 0 { - return moduleReferences - } - - aliasedSymbol := getMergedAliasedSymbolOfNamespaceExportDeclaration(node, symbol, checker) - moduleReferencesOfExportTarget := l.getReferencedSymbolsForModuleIfDeclaredBySourceFile(ctx, aliasedSymbol, program, sourceFiles, checker, options, sourceFilesSet) // !!! cancellationToken - - references := getReferencedSymbolsForSymbol(symbol, node, sourceFiles, sourceFilesSet, checker, options) // !!! cancellationToken - return l.mergeReferences(program, moduleReferences, references, moduleReferencesOfExportTarget) -} - -func (l *LanguageService) getReferencedSymbolsForModuleIfDeclaredBySourceFile(ctx context.Context, symbol *ast.Symbol, program *compiler.Program, sourceFiles []*ast.SourceFile, checker *checker.Checker, options refOptions, sourceFilesSet *collections.Set[string]) []*SymbolAndEntries { - moduleSourceFileName := "" - if symbol == nil || !((symbol.Flags&ast.SymbolFlagsModule != 0) && len(symbol.Declarations) != 0) { - return nil - } - if moduleSourceFile := core.Find(symbol.Declarations, ast.IsSourceFile); moduleSourceFile != nil { - moduleSourceFileName = moduleSourceFile.AsSourceFile().FileName() - } else { - return nil - } - exportEquals := symbol.Exports[ast.InternalSymbolNameExportEquals] - // If exportEquals != nil, we're about to add references to `import("mod")` anyway, so don't double-count them. - moduleReferences := getReferencedSymbolsForModule(ctx, program, symbol, exportEquals != nil, sourceFiles, sourceFilesSet) - if exportEquals == nil || !sourceFilesSet.Has(moduleSourceFileName) { - return moduleReferences - } - symbol, _ = checker.ResolveAlias(exportEquals) - return l.mergeReferences(program, moduleReferences, getReferencedSymbolsForSymbol(symbol /*node*/, nil, sourceFiles, sourceFilesSet, checker /*, cancellationToken*/, options)) -} - -func getReferencedSymbolsSpecial(node *ast.Node, sourceFiles []*ast.SourceFile) []*SymbolAndEntries { - if isTypeKeyword(node.Kind) { - // A void expression (i.e., `void foo()`) is not special, but the `void` type is. - if node.Kind == ast.KindVoidKeyword && node.Parent.Kind == ast.KindVoidExpression { - return nil - } - - // A modifier readonly (like on a property declaration) is not special; - // a readonly type keyword (like `readonly string[]`) is. - if node.Kind == ast.KindReadonlyKeyword && !isReadonlyTypeOperator(node) { - return nil - } - // Likewise, when we *are* looking for a special keyword, make sure we - // *don't* include readonly member modifiers. - return getAllReferencesForKeyword( - sourceFiles, - node.Kind, - // cancellationToken, - node.Kind == ast.KindReadonlyKeyword, - ) - } - - if ast.IsImportMeta(node.Parent) && node.Parent.Name() == node { - return getAllReferencesForImportMeta(sourceFiles) - } - - if node.Kind == ast.KindStaticKeyword && node.Parent.Kind == ast.KindClassStaticBlockDeclaration { - return []*SymbolAndEntries{{definition: &Definition{Kind: definitionKindKeyword, node: node}, references: []*referenceEntry{newNodeEntry(node)}}} - } - - // Labels - if isJumpStatementTarget(node) { - // if we have a label definition, look within its statement for references, if not, then - // the label is undefined and we have no results.. - if labelDefinition := getTargetLabel(node.Parent, node.Text()); labelDefinition != nil { - return getLabelReferencesInNode(labelDefinition.Parent, labelDefinition) - } - return nil - } - - if isLabelOfLabeledStatement(node) { - // it is a label definition and not a target, search within the parent labeledStatement - return getLabelReferencesInNode(node.Parent, node) - } - - if isThis(node) { - return getReferencesForThisKeyword(node, sourceFiles /*, cancellationToken*/) - } - - if node.Kind == ast.KindSuperKeyword { - return getReferencesForSuperKeyword(node) - } - - return nil -} - -func getLabelReferencesInNode(container *ast.Node, targetLabel *ast.Node) []*SymbolAndEntries { - sourceFile := ast.GetSourceFileOfNode(container) - labelName := targetLabel.Text() - references := core.MapNonNil(getPossibleSymbolReferenceNodes(sourceFile, labelName, container), func(node *ast.Node) *referenceEntry { - // Only pick labels that are either the target label, or have a target that is the target label - if node == targetLabel.AsNode() || (isJumpStatementTarget(node) && getTargetLabel(node, labelName) == targetLabel) { - return newNodeEntry(node) - } - return nil - }) - return []*SymbolAndEntries{NewSymbolAndEntries(definitionKindLabel, targetLabel, nil, references)} -} - -func getReferencesForThisKeyword(thisOrSuperKeyword *ast.Node, sourceFiles []*ast.SourceFile) []*SymbolAndEntries { - searchSpaceNode := ast.GetThisContainer(thisOrSuperKeyword, false /*includeArrowFunctions*/, false /*includeClassComputedPropertyName*/) - - // Whether 'this' occurs in a static context within a class. - staticFlag := ast.ModifierFlagsStatic - isParameterName := func(node *ast.Node) bool { - return node.Kind == ast.KindIdentifier && node.Parent.Kind == ast.KindParameter && node.Parent.Name() == node - } - - switch searchSpaceNode.Kind { - case ast.KindMethodDeclaration, ast.KindMethodSignature, - ast.KindPropertyDeclaration, ast.KindPropertySignature, ast.KindConstructor, ast.KindGetAccessor, ast.KindSetAccessor: - if (searchSpaceNode.Kind == ast.KindMethodDeclaration || searchSpaceNode.Kind == ast.KindMethodSignature) && ast.IsObjectLiteralMethod(searchSpaceNode) { - staticFlag &= searchSpaceNode.ModifierFlags() - searchSpaceNode = searchSpaceNode.Parent // re-assign to be the owning object literals - break - } - staticFlag &= searchSpaceNode.ModifierFlags() - searchSpaceNode = searchSpaceNode.Parent // re-assign to be the owning class - case ast.KindSourceFile: - if ast.IsExternalModule(searchSpaceNode.AsSourceFile()) || isParameterName(thisOrSuperKeyword) { - return nil - } - case ast.KindFunctionDeclaration, ast.KindFunctionExpression: - // Computed properties in classes are not handled here because references to this are illegal, - // so there is no point finding references to them. - default: - return nil - } - - filesToSearch := sourceFiles - if searchSpaceNode.Kind == ast.KindSourceFile { - filesToSearch = []*ast.SourceFile{searchSpaceNode.AsSourceFile()} - } - references := core.Map( - core.FlatMap(filesToSearch, func(sourceFile *ast.SourceFile) []*ast.Node { - // cancellationToken.throwIfCancellationRequested(); - return core.Filter( - getPossibleSymbolReferenceNodes(sourceFile, "this", core.IfElse(searchSpaceNode.Kind == ast.KindSourceFile, sourceFile.AsNode(), searchSpaceNode)), - func(node *ast.Node) bool { - if !isThis(node) { - return false - } - container := ast.GetThisContainer(node /*includeArrowFunctions*/, false /*includeClassComputedPropertyName*/, false) - if !ast.CanHaveSymbol(container) { - return false - } - switch searchSpaceNode.Kind { - case ast.KindFunctionExpression, ast.KindFunctionDeclaration: - return searchSpaceNode.Symbol() == container.Symbol() - case ast.KindMethodDeclaration, ast.KindMethodSignature: - return ast.IsObjectLiteralMethod(searchSpaceNode) && searchSpaceNode.Symbol() == container.Symbol() - case ast.KindClassExpression, ast.KindClassDeclaration, ast.KindObjectLiteralExpression: - // Make sure the container belongs to the same class/object literals - // and has the appropriate static modifier from the original container. - return container.Parent != nil && ast.CanHaveSymbol(container.Parent) && searchSpaceNode.Symbol() == container.Parent.Symbol() && ast.IsStatic(container) == (staticFlag != ast.ModifierFlagsNone) - case ast.KindSourceFile: - return container.Kind == ast.KindSourceFile && !ast.IsExternalModule(container.AsSourceFile()) && !isParameterName(node) - } - return false - }) - }), - func(n *ast.Node) *referenceEntry { return newNodeEntry(n) }, - ) - - thisParameter := core.FirstNonNil(references, func(ref *referenceEntry) *ast.Node { - if ref.node.Parent.Kind == ast.KindParameter { - return ref.node - } - return nil - }) - if thisParameter == nil { - thisParameter = thisOrSuperKeyword - } - return []*SymbolAndEntries{NewSymbolAndEntries(definitionKindThis, thisParameter, nil, references)} -} - -func getReferencesForSuperKeyword(superKeyword *ast.Node) []*SymbolAndEntries { - searchSpaceNode := ast.GetSuperContainer(superKeyword, false /*stopOnFunctions*/) - if searchSpaceNode == nil { - return nil - } - // Whether 'super' occurs in a static context within a class. - staticFlag := ast.ModifierFlagsStatic - - switch searchSpaceNode.Kind { - case ast.KindPropertyDeclaration, ast.KindPropertySignature, ast.KindMethodDeclaration, ast.KindMethodSignature, ast.KindConstructor, ast.KindGetAccessor, ast.KindSetAccessor: - staticFlag &= searchSpaceNode.ModifierFlags() - searchSpaceNode = searchSpaceNode.Parent // re-assign to be the owning class - default: - return nil - } - - sourceFile := ast.GetSourceFileOfNode(searchSpaceNode) - references := core.MapNonNil(getPossibleSymbolReferenceNodes(sourceFile, "super", searchSpaceNode), func(node *ast.Node) *referenceEntry { - if node.Kind != ast.KindSuperKeyword { - return nil - } - - container := ast.GetSuperContainer(node, false /*stopOnFunctions*/) - - // If we have a 'super' container, we must have an enclosing class. - // Now make sure the owning class is the same as the search-space - // and has the same static qualifier as the original 'super's owner. - if container != nil && ast.IsStatic(container) == (staticFlag != ast.ModifierFlagsNone) && container.Parent.Symbol() == searchSpaceNode.Symbol() { - return newNodeEntry(node) - } - return nil - }) - - return []*SymbolAndEntries{NewSymbolAndEntries(definitionKindSymbol, nil, searchSpaceNode.Symbol(), references)} -} - -func getAllReferencesForImportMeta(sourceFiles []*ast.SourceFile) []*SymbolAndEntries { - references := core.FlatMap(sourceFiles, func(sourceFile *ast.SourceFile) []*referenceEntry { - return core.MapNonNil(getPossibleSymbolReferenceNodes(sourceFile, "meta", sourceFile.AsNode()), func(node *ast.Node) *referenceEntry { - parent := node.Parent - if ast.IsImportMeta(parent) { - return newNodeEntry(parent) - } - return nil - }) - }) - if len(references) == 0 { - return nil - } - return []*SymbolAndEntries{{definition: &Definition{Kind: definitionKindKeyword, node: references[0].node}, references: references}} -} - -func getAllReferencesForKeyword(sourceFiles []*ast.SourceFile, keywordKind ast.Kind, filterReadOnlyTypeOperator bool) []*SymbolAndEntries { - // references is a list of NodeEntry - references := core.FlatMap(sourceFiles, func(sourceFile *ast.SourceFile) []*referenceEntry { - // cancellationToken.throwIfCancellationRequested(); - return core.MapNonNil(getPossibleSymbolReferenceNodes(sourceFile, scanner.TokenToString(keywordKind), sourceFile.AsNode()), func(referenceLocation *ast.Node) *referenceEntry { - if referenceLocation.Kind == keywordKind && (!filterReadOnlyTypeOperator || isReadonlyTypeOperator(referenceLocation)) { - return newNodeEntry(referenceLocation) - } - return nil - }) - }) - if len(references) == 0 { - return nil - } - return []*SymbolAndEntries{NewSymbolAndEntries(definitionKindKeyword, references[0].node, nil, references)} -} - -func getPossibleSymbolReferenceNodes(sourceFile *ast.SourceFile, symbolName string, container *ast.Node) []*ast.Node { - return core.MapNonNil(getPossibleSymbolReferencePositions(sourceFile, symbolName, container), func(pos int) *ast.Node { - if referenceLocation := astnav.GetTouchingPropertyName(sourceFile, pos); referenceLocation != sourceFile.AsNode() { - return referenceLocation - } - return nil - }) -} - -func getPossibleSymbolReferencePositions(sourceFile *ast.SourceFile, symbolName string, container *ast.Node) []int { - positions := []int{} - - /// TODO: Cache symbol existence for files to save text search - // Also, need to make this work for unicode escapes. - - // Be resilient in the face of a symbol with no name or zero length name - if symbolName == "" { - return positions - } - - text := sourceFile.Text() - sourceLength := len(text) - symbolNameLength := len(symbolName) - - if container == nil { - container = sourceFile.AsNode() - } - - position := strings.Index(text[container.Pos():], symbolName) - endPos := container.End() - for position >= 0 && position < endPos { - // We found a match. Make sure it's not part of a larger word (i.e. the char - // before and after it have to be a non-identifier char). - endPosition := position + symbolNameLength - - if (position == 0 || !scanner.IsIdentifierPart(rune(text[position-1]))) && - (endPosition == sourceLength || !scanner.IsIdentifierPart(rune(text[endPosition]))) { - // Found a real match. Keep searching. - positions = append(positions, position) - } - startIndex := position + symbolNameLength + 1 - if startIndex > len(text) { - break - } - if foundIndex := strings.Index(text[startIndex:], symbolName); foundIndex != -1 { - position = startIndex + foundIndex - } else { - break - } - } - - return positions -} - -// findFirstJsxNode recursively searches for the first JSX element, self-closing element, or fragment -func findFirstJsxNode(root *ast.Node) *ast.Node { - var visit func(*ast.Node) *ast.Node - visit = func(node *ast.Node) *ast.Node { - // Check if this is a JSX node we're looking for - switch node.Kind { - case ast.KindJsxElement, ast.KindJsxSelfClosingElement, ast.KindJsxFragment: - return node - } - - // Skip subtree if it doesn't contain JSX - if node.SubtreeFacts()&ast.SubtreeContainsJsx == 0 { - return nil - } - - // Traverse children to find JSX node - var result *ast.Node - node.ForEachChild(func(child *ast.Node) bool { - result = visit(child) - return result != nil // Stop if found - }) - return result - } - - return visit(root) -} - -func getReferencesForNonModule(referencedFile *ast.SourceFile, program *compiler.Program) []*referenceEntry { - // !!! not implemented - return []*referenceEntry{} -} - -func getMergedAliasedSymbolOfNamespaceExportDeclaration(node *ast.Node, symbol *ast.Symbol, checker *checker.Checker) *ast.Symbol { - if node.Parent != nil && node.Parent.Kind == ast.KindNamespaceExportDeclaration { - if aliasedSymbol, ok := checker.ResolveAlias(symbol); ok { - targetSymbol := checker.GetMergedSymbol(aliasedSymbol) - if aliasedSymbol != targetSymbol { - return targetSymbol - } - } - } - return nil -} - -func getReferencedSymbolsForModule(ctx context.Context, program *compiler.Program, symbol *ast.Symbol, excludeImportTypeOfExportEquals bool, sourceFiles []*ast.SourceFile, sourceFilesSet *collections.Set[string]) []*SymbolAndEntries { - debug.Assert(symbol.ValueDeclaration != nil) - - checker, done := program.GetTypeChecker(ctx) - defer done() - - moduleRefs := findModuleReferences(program, sourceFiles, symbol, checker) - references := core.MapNonNil(moduleRefs, func(reference ModuleReference) *referenceEntry { - switch reference.kind { - case ModuleReferenceKindImport: - parent := reference.literal.Parent - if ast.IsLiteralTypeNode(parent) { - importType := parent.Parent - if ast.IsImportTypeNode(importType) { - importTypeNode := importType.AsImportTypeNode() - if excludeImportTypeOfExportEquals && importTypeNode.Qualifier == nil { - return nil - } - } - } - // import("foo") with no qualifier will reference the `export =` of the module, which may be referenced anyway. - return newNodeEntry(reference.literal) - case ModuleReferenceKindImplicit: - // For implicit references (e.g., JSX runtime imports), return the first JSX node, - // the first statement, or the whole file - var rangeNode *ast.Node - - // Skip the JSX search for tslib imports - if reference.literal.Text() != "tslib" { - rangeNode = findFirstJsxNode(reference.referencingFile.AsNode()) - } - - if rangeNode == nil { - if reference.referencingFile.Statements != nil && len(reference.referencingFile.Statements.Nodes) > 0 { - rangeNode = reference.referencingFile.Statements.Nodes[0] - } else { - rangeNode = reference.referencingFile.AsNode() - } - } - return newNodeEntry(rangeNode) - case ModuleReferenceKindReference: - // or - // We can't easily create a proper range entry here without access to LanguageService, - // but we can create a node-based entry pointing to the source file which will be resolved later - return newNodeEntry(reference.referencingFile.AsNode()) - } - return nil - }) - - // Add references to the module declarations themselves - if len(symbol.Declarations) > 0 { - for _, decl := range symbol.Declarations { - switch decl.Kind { - case ast.KindSourceFile: - // Don't include the source file itself. (This may not be ideal behavior, but awkward to include an entire file as a reference.) - continue - case ast.KindModuleDeclaration: - if sourceFilesSet.Has(ast.GetSourceFileOfNode(decl).FileName()) { - references = append(references, newNodeEntry(decl.AsModuleDeclaration().Name())) - } - default: - // This may be merged with something. - debug.Assert(symbol.Flags&ast.SymbolFlagsTransient != 0, "Expected a module symbol to be declared by a SourceFile or ModuleDeclaration.") - } - } - } - - // Handle export equals declarations - exported := symbol.Exports[ast.InternalSymbolNameExportEquals] - if exported != nil && len(exported.Declarations) > 0 { - for _, decl := range exported.Declarations { - sourceFile := ast.GetSourceFileOfNode(decl) - if sourceFilesSet.Has(sourceFile.FileName()) { - var node *ast.Node - // At `module.exports = ...`, reference node is `module` - if ast.IsBinaryExpression(decl) && ast.IsPropertyAccessExpression(decl.AsBinaryExpression().Left) { - node = decl.AsBinaryExpression().Left.AsPropertyAccessExpression().Expression - } else if ast.IsExportAssignment(decl) { - // Find the export keyword - node = findChildOfKind(decl, ast.KindExportKeyword, sourceFile) - debug.Assert(node != nil, "Expected to find export keyword") - } else { - node = ast.GetNameOfDeclaration(decl) - if node == nil { - node = decl - } - } - references = append(references, newNodeEntry(node)) - } - } - } - - if len(references) > 0 { - return []*SymbolAndEntries{{ - definition: &Definition{Kind: definitionKindSymbol, symbol: symbol}, - references: references, - }} - } - return nil -} - -func getReferenceAtPosition(sourceFile *ast.SourceFile, position int, program *compiler.Program) *refInfo { - if referencePath := findReferenceInPosition(sourceFile.ReferencedFiles, position); referencePath != nil { - if file := program.GetSourceFileFromReference(sourceFile, referencePath); file != nil { - return &refInfo{reference: referencePath, fileName: file.FileName(), file: file, unverified: false} - } - return nil - } - - if typeReferenceDirective := findReferenceInPosition(sourceFile.TypeReferenceDirectives, position); typeReferenceDirective != nil { - if reference := program.GetResolvedTypeReferenceDirectiveFromTypeReferenceDirective(typeReferenceDirective, sourceFile); reference != nil { - if file := program.GetSourceFile(reference.ResolvedFileName); file != nil { - return &refInfo{reference: typeReferenceDirective, fileName: file.FileName(), file: file, unverified: false} - } - } - return nil - } - - if libReferenceDirective := findReferenceInPosition(sourceFile.LibReferenceDirectives, position); libReferenceDirective != nil { - if file := program.GetLibFileFromReference(libReferenceDirective); file != nil { - return &refInfo{reference: libReferenceDirective, fileName: file.FileName(), file: file, unverified: false} - } - return nil - } - - if len(sourceFile.Imports()) == 0 && len(sourceFile.ModuleAugmentations) == 0 { - return nil - } - - node := astnav.GetTouchingToken(sourceFile, position) - if !isModuleSpecifierLike(node) || !tspath.IsExternalModuleNameRelative(node.Text()) { - return nil - } - if resolution := program.GetResolvedModuleFromModuleSpecifier(sourceFile, node); resolution != nil { - verifiedFileName := resolution.ResolvedFileName - fileName := resolution.ResolvedFileName - if fileName == "" { - fileName = tspath.ResolvePath(tspath.GetDirectoryPath(sourceFile.FileName()), node.Text()) - } - return &refInfo{ - file: program.GetSourceFile(fileName), - fileName: fileName, - reference: nil, - unverified: verifiedFileName != "", - } - } - - return nil -} - -// -- Core algorithm for find all references -- -func getSpecialSearchKind(node *ast.Node) string { - if node == nil { - return "none" - } - switch node.Kind { - case ast.KindConstructor, ast.KindConstructorKeyword: - return "constructor" - case ast.KindIdentifier: - if ast.IsClassLike(node.Parent) { - debug.Assert(node.Parent.Name() == node) - return "class" - } - fallthrough - default: - return "none" - } -} - -func getReferencedSymbolsForSymbol(originalSymbol *ast.Symbol, node *ast.Node, sourceFiles []*ast.SourceFile, sourceFilesSet *collections.Set[string], checker *checker.Checker, options refOptions) []*SymbolAndEntries { - // Core find-all-references algorithm for a normal symbol. - - symbol := core.Coalesce(skipPastExportOrImportSpecifierOrUnion(originalSymbol, node, checker /*useLocalSymbolForExportSpecifier*/, !isForRenameWithPrefixAndSuffixText(options)), originalSymbol) - - // Compute the meaning from the location and the symbol it references - searchMeaning := ast.SemanticMeaningAll - if options.use != referenceUseRename { - searchMeaning = getIntersectingMeaningFromDeclarations(node, symbol, ast.SemanticMeaningAll) - } - state := newState(sourceFiles, sourceFilesSet, node, checker /*, cancellationToken*/, searchMeaning, options) - - var exportSpecifier *ast.Node - if !isForRenameWithPrefixAndSuffixText(options) || len(symbol.Declarations) == 0 { - exportSpecifier = core.Find(symbol.Declarations, ast.IsExportSpecifier) - } - if exportSpecifier != nil { - // !!! not implemented - - // When renaming at an export specifier, rename the export and not the thing being exported. - // state.getReferencesAtExportSpecifier(exportSpecifier.Name(), symbol, exportSpecifier.AsExportSpecifier(), state.createSearch(node, originalSymbol, comingFromUnknown /*comingFrom*/, "", nil), true /*addReferencesHere*/, true /*alwaysGetReferences*/) - } else if node != nil && node.Kind == ast.KindDefaultKeyword && symbol.Name == ast.InternalSymbolNameDefault && symbol.Parent != nil { - state.addReference(node, symbol, entryKindNone) - state.searchForImportsOfExport(node, symbol, &ExportInfo{exportingModuleSymbol: symbol.Parent, exportKind: ExportKindDefault}) - } else { - search := state.createSearch(node, symbol, ImpExpKindUnknown /*comingFrom*/, "", state.populateSearchSymbolSet(symbol, node, options.use == referenceUseRename, options.useAliasesForRename, options.implementations)) - state.getReferencesInContainerOrFiles(symbol, search) - } - - return state.result -} - -// Symbol that is currently being searched for. -// This will be replaced if we find an alias for the symbol. -type refSearch struct { - // If coming from an export, we will not recursively search for the imported symbol (since that's where we came from). - comingFrom ImpExpKind // import, export - - symbol *ast.Symbol - text string - escapedText string - - // Only set if `options.implementations` is true. These are the symbols checked to get the implementations of a property access. - parents []*ast.Symbol - - allSearchSymbols []*ast.Symbol - - // Whether a symbol is in the search set. - // Do not compare directly to `symbol` because there may be related symbols to search for. See `populateSearchSymbolSet`. - includes func(symbol *ast.Symbol) bool -} - -type inheritKey struct { - symbol *ast.Symbol - parent *ast.Symbol -} - -type refState struct { - sourceFiles []*ast.SourceFile - sourceFilesSet *collections.Set[string] - specialSearchKind string // "none", "constructor", or "class" - checker *checker.Checker - // cancellationToken CancellationToken - searchMeaning ast.SemanticMeaning - options refOptions - result []*SymbolAndEntries - inheritsFromCache map[inheritKey]bool - seenContainingTypeReferences collections.Set[*ast.Node] // node seen tracker - // seenReExportRHS *collections.Set[*ast.Node] // node seen tracker - importTracker ImportTracker - symbolToReferences map[*ast.Symbol]*SymbolAndEntries - sourceFileToSeenSymbols map[*ast.SourceFile]*collections.Set[*ast.Symbol] -} - -func newState(sourceFiles []*ast.SourceFile, sourceFilesSet *collections.Set[string], node *ast.Node, checker *checker.Checker, searchMeaning ast.SemanticMeaning, options refOptions) *refState { - return &refState{ - sourceFiles: sourceFiles, - sourceFilesSet: sourceFilesSet, - specialSearchKind: getSpecialSearchKind(node), - checker: checker, - searchMeaning: searchMeaning, - options: options, - inheritsFromCache: map[inheritKey]bool{}, - // seenReExportRHS: &collections.Set[*ast.Node]{}, - symbolToReferences: map[*ast.Symbol]*SymbolAndEntries{}, - sourceFileToSeenSymbols: map[*ast.SourceFile]*collections.Set[*ast.Symbol]{}, - } -} - -func (state *refState) includesSourceFile(sourceFile *ast.SourceFile) bool { - return state.sourceFilesSet.Has(sourceFile.FileName()) -} - -func (state *refState) getImportSearches(exportSymbol *ast.Symbol, exportInfo *ExportInfo) *ImportsResult { - if state.importTracker == nil { - state.importTracker = createImportTracker(state.sourceFiles, state.sourceFilesSet, state.checker) - } - return state.importTracker(exportSymbol, exportInfo, state.options.use == referenceUseRename) -} - -// @param allSearchSymbols set of additional symbols for use by `includes` -func (state *refState) createSearch(location *ast.Node, symbol *ast.Symbol, comingFrom ImpExpKind, text string, allSearchSymbols []*ast.Symbol) *refSearch { - // Note: if this is an external module symbol, the name doesn't include quotes. - // Note: getLocalSymbolForExportDefault handles `export default class C {}`, but not `export default C` or `export { C as default }`. - // The other two forms seem to be handled downstream (e.g. in `skipPastExportOrImportSpecifier`), so special-casing the first form - // here appears to be intentional). - if text == "" { - s := binder.GetLocalSymbolForExportDefault(symbol) - if s == nil { - s = getNonModuleSymbolOfMergedModuleSymbol(symbol) - if s == nil { - s = symbol - } - } - text = stringutil.StripQuotes(ast.SymbolName(s)) - } - if len(allSearchSymbols) == 0 { - allSearchSymbols = []*ast.Symbol{symbol} - } - search := &refSearch{ - symbol: symbol, - comingFrom: comingFrom, - text: text, - escapedText: text, - allSearchSymbols: allSearchSymbols, - includes: func(sym *ast.Symbol) bool { return slices.Contains(allSearchSymbols, sym) }, - } - if state.options.implementations && location != nil { - search.parents = getParentSymbolsOfPropertyAccess(location, symbol, state.checker) - } - return search -} - -func (state *refState) referenceAdder(searchSymbol *ast.Symbol) func(*ast.Node, entryKind) { - symbolAndEntries := state.symbolToReferences[searchSymbol] - if symbolAndEntries == nil { - symbolAndEntries = NewSymbolAndEntries(definitionKindSymbol, nil, searchSymbol, nil) - state.symbolToReferences[searchSymbol] = symbolAndEntries - state.result = append(state.result, symbolAndEntries) - } - return func(node *ast.Node, kind entryKind) { - symbolAndEntries.references = append(symbolAndEntries.references, newNodeEntryWithKind(node, kind)) - } -} - -func (state *refState) addReference(referenceLocation *ast.Node, symbol *ast.Symbol, kind entryKind) { - // if rename symbol from default export anonymous function, for example `export default function() {}`, we do not need to add reference - if state.options.use == referenceUseRename && referenceLocation.Kind == ast.KindDefaultKeyword { - return - } - - addRef := state.referenceAdder(symbol) - if state.options.implementations { - state.addImplementationReferences(referenceLocation, func(n *ast.Node) { addRef(n, kind) }) - } else { - addRef(referenceLocation, kind) - } -} - -func getReferenceEntriesForShorthandPropertyAssignment(node *ast.Node, checker *checker.Checker, addReference func(*ast.Node)) { - refSymbol := checker.GetSymbolAtLocation(node) - if refSymbol == nil || refSymbol.ValueDeclaration == nil { - return - } - shorthandSymbol := checker.GetShorthandAssignmentValueSymbol(refSymbol.ValueDeclaration) - if shorthandSymbol != nil && len(shorthandSymbol.Declarations) > 0 { - for _, declaration := range shorthandSymbol.Declarations { - if getMeaningFromDeclaration(declaration)&ast.SemanticMeaningValue != 0 { - addReference(declaration) - } - } - } -} - -func climbPastPropertyAccess(node *ast.Node) *ast.Node { - if isRightSideOfPropertyAccess(node) { - return node.Parent - } - return node -} - -func isNewExpressionTarget(node *ast.Node) bool { - if node.Parent == nil { - return false - } - return node.Parent.Kind == ast.KindNewExpression && node.Parent.AsNewExpression().Expression == node -} - -func isCallExpressionTarget(node *ast.Node) bool { - if node.Parent == nil { - return false - } - return node.Parent.Kind == ast.KindCallExpression && node.Parent.AsCallExpression().Expression == node -} - -func isMethodOrAccessor(node *ast.Node) bool { - return node.Kind == ast.KindMethodDeclaration || node.Kind == ast.KindGetAccessor || node.Kind == ast.KindSetAccessor -} - -func tryGetClassByExtendingIdentifier(node *ast.Node) *ast.ClassLikeDeclaration { - return ast.TryGetClassExtendingExpressionWithTypeArguments(climbPastPropertyAccess(node).Parent) -} - -func getClassConstructorSymbol(classSymbol *ast.Symbol) *ast.Symbol { - if classSymbol.Members == nil { - return nil - } - return classSymbol.Members[ast.InternalSymbolNameConstructor] -} - -func hasOwnConstructor(classDeclaration *ast.ClassLikeDeclaration) bool { - return getClassConstructorSymbol(classDeclaration.Symbol()) != nil -} - -func findOwnConstructorReferences(classSymbol *ast.Symbol, sourceFile *ast.SourceFile, addNode func(*ast.Node)) { - constructorSymbol := getClassConstructorSymbol(classSymbol) - if constructorSymbol != nil && len(constructorSymbol.Declarations) > 0 { - for _, decl := range constructorSymbol.Declarations { - if decl.Kind == ast.KindConstructor { - if ctrKeyword := findChildOfKind(decl, ast.KindConstructorKeyword, sourceFile); ctrKeyword != nil { - addNode(ctrKeyword) - } - } - } - } - - if classSymbol.Exports != nil { - for _, member := range classSymbol.Exports { - decl := member.ValueDeclaration - if decl != nil && decl.Kind == ast.KindMethodDeclaration { - body := decl.Body() - if body != nil { - forEachDescendantOfKind(body, ast.KindThisKeyword, func(thisKeyword *ast.Node) { - if isNewExpressionTarget(thisKeyword) { - addNode(thisKeyword) - } - }) - } - } - } - } -} - -func findSuperConstructorAccesses(classDeclaration *ast.ClassLikeDeclaration, addNode func(*ast.Node)) { - constructorSymbol := getClassConstructorSymbol(classDeclaration.Symbol()) - if constructorSymbol == nil || len(constructorSymbol.Declarations) == 0 { - return - } - - for _, decl := range constructorSymbol.Declarations { - if decl.Kind == ast.KindConstructor { - body := decl.Body() - if body != nil { - forEachDescendantOfKind(body, ast.KindSuperKeyword, func(node *ast.Node) { - if isCallExpressionTarget(node) { - addNode(node) - } - }) - } - } - } -} - -func forEachDescendantOfKind(node *ast.Node, kind ast.Kind, action func(*ast.Node)) { - node.ForEachChild(func(child *ast.Node) bool { - if child.Kind == kind { - action(child) - } - forEachDescendantOfKind(child, kind, action) - return false - }) -} - -func (state *refState) addImplementationReferences(refNode *ast.Node, addRef func(*ast.Node)) { - // Check if we found a function/propertyAssignment/method with an implementation or initializer - if ast.IsDeclarationName(refNode) && isImplementation(refNode.Parent) { - addRef(refNode) - return - } - - if refNode.Kind != ast.KindIdentifier { - return - } - - if refNode.Parent.Kind == ast.KindShorthandPropertyAssignment { - // Go ahead and dereference the shorthand assignment by going to its definition - getReferenceEntriesForShorthandPropertyAssignment(refNode, state.checker, addRef) - } - - // Check if the node is within an extends or implements clause - - if containingNode := getContainingNodeIfInHeritageClause(refNode); containingNode != nil { - addRef(containingNode) - return - } - - // If we got a type reference, try and see if the reference applies to any expressions that can implement an interface - // Find the first node whose parent isn't a type node -- i.e., the highest type node. - typeNode := ast.FindAncestor(refNode, func(a *ast.Node) bool { - return !ast.IsQualifiedName(a.Parent) && !ast.IsTypeNode(a.Parent) && !ast.IsTypeElement(a.Parent) - }) - - if typeNode == nil || typeNode.Parent.Type() == nil { - return - } - - typeHavingNode := typeNode.Parent - if typeHavingNode.Type() == typeNode && !state.seenContainingTypeReferences.AddIfAbsent(typeHavingNode) { - addIfImplementation := func(e *ast.Expression) { - if isImplementationExpression(e) { - addRef(e) - } - } - if ast.HasInitializer(typeHavingNode) { - addIfImplementation(typeHavingNode.Initializer()) - } else if ast.IsFunctionLike(typeHavingNode) && typeHavingNode.Body() != nil { - body := typeHavingNode.Body() - if body.Kind == ast.KindBlock { - ast.ForEachReturnStatement(body, func(returnStatement *ast.Node) bool { - if expr := returnStatement.Expression(); expr != nil { - addIfImplementation(expr) - } - return false - }) - } else { - addIfImplementation(body) - } - } else if ast.IsAssertionExpression(typeHavingNode) || ast.IsSatisfiesExpression(typeHavingNode) { - addIfImplementation(typeHavingNode.Expression()) - } - } -} - -func (state *refState) getReferencesInContainerOrFiles(symbol *ast.Symbol, search *refSearch) { - // Try to get the smallest valid scope that we can limit our search to; - // otherwise we'll need to search globally (i.e. include each file). - if scope := getSymbolScope(symbol); scope != nil { - state.getReferencesInContainer(scope, ast.GetSourceFileOfNode(scope), search /*addReferencesHere*/, !(scope.Kind == ast.KindSourceFile && !slices.Contains(state.sourceFiles, scope.AsSourceFile()))) - } else { - // Global search - for _, sourceFile := range state.sourceFiles { - // state.cancellationToken.throwIfCancellationRequested(); - state.searchForName(sourceFile, search) - } - } -} - -func (state *refState) getReferencesInSourceFile(sourceFile *ast.SourceFile, search *refSearch, addReferencesHere bool) { - // state.cancellationToken.throwIfCancellationRequested(); - state.getReferencesInContainer(sourceFile.AsNode(), sourceFile, search, addReferencesHere) -} - -func (state *refState) getReferencesInContainer(container *ast.Node, sourceFile *ast.SourceFile, search *refSearch, addReferencesHere bool) { - // Search within node "container" for references for a search value, where the search value is defined as a - // tuple of (searchSymbol, searchText, searchLocation, and searchMeaning). - // searchLocation: a node where the search value - if !state.markSearchedSymbols(sourceFile, search.allSearchSymbols) { - return - } - - for _, position := range getPossibleSymbolReferencePositions(sourceFile, search.text, container) { - state.getReferencesAtLocation(sourceFile, position, search, addReferencesHere) - } -} - -func (state *refState) markSearchedSymbols(sourceFile *ast.SourceFile, symbols []*ast.Symbol) bool { - seenSymbols := state.sourceFileToSeenSymbols[sourceFile] - if seenSymbols == nil { - seenSymbols = &collections.Set[*ast.Symbol]{} - state.sourceFileToSeenSymbols[sourceFile] = seenSymbols - } - anyNewSymbols := false - for _, sym := range symbols { - if seenSymbols.AddIfAbsent(sym) { - anyNewSymbols = true - } - } - return anyNewSymbols -} - -func (state *refState) getReferencesAtLocation(sourceFile *ast.SourceFile, position int, search *refSearch, addReferencesHere bool) { - referenceLocation := astnav.GetTouchingPropertyName(sourceFile, position) - - if !isValidReferencePosition(referenceLocation, search.text) { - // This wasn't the start of a token. Check to see if it might be a - // match in a comment or string if that's what the caller is asking - // for. - - // !!! not implemented - // if (!state.options.implementations && (state.options.findInStrings && isInString(sourceFile, position) || state.options.findInComments && isInNonReferenceComment(sourceFile, position))) { - // // In the case where we're looking inside comments/strings, we don't have - // // an actual definition. So just use 'undefined' here. Features like - // // 'Rename' won't care (as they ignore the definitions), and features like - // // 'FindReferences' will just filter out these results. - // state.addStringOrCommentReference(sourceFile.FileName, createTextSpan(position, search.text.length)); - // } - - return - } - - if getMeaningFromLocation(referenceLocation)&state.searchMeaning == 0 { - return - } - - referenceSymbol := state.checker.GetSymbolAtLocation(referenceLocation) - if referenceSymbol == nil { - return - } - - parent := referenceLocation.Parent - if parent.Kind == ast.KindImportSpecifier && parent.PropertyName() == referenceLocation { - // This is added through `singleReferences` in ImportsResult. If we happen to see it again, don't add it again. - return - } - - if parent.Kind == ast.KindExportSpecifier { - // !!! not implemented - // debug.Assert(referenceLocation.Kind == ast.KindIdentifier || referenceLocation.Kind == ast.KindStringLiteral) - // state.getReferencesAtExportSpecifier(referenceLocation /* Identifier | StringLiteral*/, referenceSymbol, parent.AsExportSpecifier(), search, addReferencesHere, false /*alwaysGetReferences*/) - return - } - - relatedSymbol, relatedSymbolKind := state.getRelatedSymbol(search, referenceSymbol, referenceLocation) - if relatedSymbol == nil { - state.getReferenceForShorthandProperty(referenceSymbol, search) - return - } - - switch state.specialSearchKind { - case "none": - if addReferencesHere { - state.addReference(referenceLocation, relatedSymbol, relatedSymbolKind) - } - case "constructor": - state.addConstructorReferences(referenceLocation, relatedSymbol, search, addReferencesHere) - case "class": - state.addClassStaticThisReferences(referenceLocation, relatedSymbol, search, addReferencesHere) - } - - // Use the parent symbol if the location is commonjs require syntax on javascript files only. - if ast.IsInJSFile(referenceLocation) && referenceLocation.Parent.Kind == ast.KindBindingElement && - ast.IsVariableDeclarationInitializedToRequire(referenceLocation.Parent.Parent.Parent) { - referenceSymbol = referenceLocation.Parent.Symbol() - // The parent will not have a symbol if it's an ObjectBindingPattern (when destructuring is used). In - // this case, just skip it, since the bound identifiers are not an alias of the import. - if referenceSymbol == nil { - return - } - } - - state.getImportOrExportReferences(referenceLocation, referenceSymbol, search) -} - -func (state *refState) addConstructorReferences(referenceLocation *ast.Node, symbol *ast.Symbol, search *refSearch, addReferencesHere bool) { - if isNewExpressionTarget(referenceLocation) && addReferencesHere { - state.addReference(referenceLocation, symbol, entryKindNone) - } - - pusher := func() func(*ast.Node, entryKind) { - return state.referenceAdder(search.symbol) - } - - if ast.IsClassLike(referenceLocation.Parent) { - // This is the class declaration containing the constructor. - sourceFile := ast.GetSourceFileOfNode(referenceLocation) - findOwnConstructorReferences(search.symbol, sourceFile, func(n *ast.Node) { - pusher()(n, entryKindNone) - }) - } else { - // If this class appears in `extends C`, then the extending class' "super" calls are references. - if classExtending := tryGetClassByExtendingIdentifier(referenceLocation); classExtending != nil { - findSuperConstructorAccesses(classExtending, func(n *ast.Node) { - pusher()(n, entryKindNone) - }) - state.findInheritedConstructorReferences(classExtending) - } - } -} - -func (state *refState) addClassStaticThisReferences(referenceLocation *ast.Node, symbol *ast.Symbol, search *refSearch, addReferencesHere bool) { - if addReferencesHere { - state.addReference(referenceLocation, symbol, entryKindNone) - } - - classLike := referenceLocation.Parent - if state.options.use == referenceUseRename || !ast.IsClassLike(classLike) { - return - } - - addRef := state.referenceAdder(search.symbol) - members := classLike.Members() - if members == nil { - return - } - for _, member := range members { - if !(isMethodOrAccessor(member) && ast.HasStaticModifier(member)) { - continue - } - body := member.Body() - if body != nil { - var cb func(*ast.Node) - cb = func(node *ast.Node) { - if node.Kind == ast.KindThisKeyword { - addRef(node, entryKindNone) - } else if !ast.IsFunctionLike(node) && !ast.IsClassLike(node) { - node.ForEachChild(func(child *ast.Node) bool { - cb(child) - return false - }) - } - } - cb(body) - } - } -} - -func (state *refState) findInheritedConstructorReferences(classDeclaration *ast.ClassLikeDeclaration) { - if hasOwnConstructor(classDeclaration) { - return - } - classSymbol := classDeclaration.Symbol() - search := state.createSearch(nil, classSymbol, ImpExpKindUnknown, "", nil) - state.getReferencesInContainerOrFiles(classSymbol, search) -} - -func (state *refState) getImportOrExportReferences(referenceLocation *ast.Node, referenceSymbol *ast.Symbol, search *refSearch) { - importOrExport := getImportOrExportSymbol(referenceLocation, referenceSymbol, state.checker, search.comingFrom == ImpExpKindExport) - if importOrExport == nil { - return - } - if importOrExport.kind == ImpExpKindImport { - if !isForRenameWithPrefixAndSuffixText(state.options) { - state.searchForImportedSymbol(importOrExport.symbol) - } - } else { - state.searchForImportsOfExport(referenceLocation, importOrExport.symbol, importOrExport.exportInfo) - } -} - -// Go to the symbol we imported from and find references for it. -func (state *refState) searchForImportedSymbol(symbol *ast.Symbol) { - for _, declaration := range symbol.Declarations { - exportingFile := ast.GetSourceFileOfNode(declaration) - // Need to search in the file even if it's not in the search-file set, because it might export the symbol. - state.getReferencesInSourceFile(exportingFile, state.createSearch(declaration, symbol, ImpExpKindImport, "", nil), state.includesSourceFile(exportingFile)) - } -} - -// Search for all imports of a given exported symbol using `State.getImportSearches`. */ -func (state *refState) searchForImportsOfExport(exportLocation *ast.Node, exportSymbol *ast.Symbol, exportInfo *ExportInfo) { - r := state.getImportSearches(exportSymbol, exportInfo) - - // For `import { foo as bar }` just add the reference to `foo`, and don't otherwise search in the file. - if len(r.singleReferences) != 0 { - addRef := state.referenceAdder(exportSymbol) - for _, singleRef := range r.singleReferences { - if state.shouldAddSingleReference(singleRef) { - addRef(singleRef, entryKindNode) - } - } - } - - // For each import, find all references to that import in its source file. - for _, i := range r.importSearches { - state.getReferencesInSourceFile(ast.GetSourceFileOfNode(i.importLocation), state.createSearch(i.importLocation, i.importSymbol, ImpExpKindExport, "", nil), true /*addReferencesHere*/) - } - - if len(r.indirectUsers) != 0 { - var indirectSearch *refSearch - switch exportInfo.exportKind { - case ExportKindNamed: - indirectSearch = state.createSearch(exportLocation, exportSymbol, ImpExpKindExport, "", nil) - case ExportKindDefault: - // Search for a property access to '.default'. This can't be renamed. - if state.options.use != referenceUseRename { - indirectSearch = state.createSearch(exportLocation, exportSymbol, ImpExpKindExport, "default", nil) - } - } - if indirectSearch != nil { - for _, indirectUser := range r.indirectUsers { - state.searchForName(indirectUser, indirectSearch) - } - } - } -} - -func (state *refState) shouldAddSingleReference(singleRef *ast.Node) bool { - if !state.hasMatchingMeaning(singleRef) { - return false - } - if state.options.use != referenceUseRename { - return true - } - // Don't rename an import type `import("./module-name")` when renaming `name` in `export = name;` - if !ast.IsIdentifier(singleRef) && !ast.IsImportOrExportSpecifier(singleRef.Parent) { - return false - } - // At `default` in `import { default as x }` or `export { default as x }`, do add a reference, but do not rename. - return !(ast.IsImportOrExportSpecifier(singleRef.Parent) && ast.ModuleExportNameIsDefault(singleRef)) -} - -func (state *refState) hasMatchingMeaning(referenceLocation *ast.Node) bool { - return getMeaningFromLocation(referenceLocation)&state.searchMeaning != 0 -} - -func (state *refState) getReferenceForShorthandProperty(referenceSymbol *ast.Symbol, search *refSearch) { - if referenceSymbol.Flags&ast.SymbolFlagsTransient != 0 || referenceSymbol.ValueDeclaration == nil { - return - } - shorthandValueSymbol := state.checker.GetShorthandAssignmentValueSymbol(referenceSymbol.ValueDeclaration) - name := ast.GetNameOfDeclaration(referenceSymbol.ValueDeclaration) - - // Because in short-hand property assignment, an identifier which stored as name of the short-hand property assignment - // has two meanings: property name and property value. Therefore when we do findAllReference at the position where - // an identifier is declared, the language service should return the position of the variable declaration as well as - // the position in short-hand property assignment excluding property accessing. However, if we do findAllReference at the - // position of property accessing, the referenceEntry of such position will be handled in the first case. - if name != nil && search.includes(shorthandValueSymbol) { - state.addReference(name, shorthandValueSymbol, entryKindNone) - } -} - -// === search === -func (state *refState) populateSearchSymbolSet(symbol *ast.Symbol, location *ast.Node, isForRename, providePrefixAndSuffixText, implementations bool) []*ast.Symbol { - if location == nil { - return []*ast.Symbol{symbol} - } - result := []*ast.Symbol{} - state.forEachRelatedSymbol( - symbol, - location, - isForRename, - !(isForRename && providePrefixAndSuffixText), - func(sym *ast.Symbol, root *ast.Symbol, base *ast.Symbol) *ast.Symbol { - // static method/property and instance method/property might have the same name. Only include static or only include instance. - if base != nil { - if isStaticSymbol(symbol) != isStaticSymbol(base) { - base = nil - } - } - result = append(result, core.OrElse(base, core.OrElse(root, sym))) - return nil - }, // when try to find implementation, implementations is true, and not allowed to find base class - /*allowBaseTypes*/ func(_ *ast.Symbol) bool { return !implementations }, - ) - return result -} - -func (state *refState) getRelatedSymbol(search *refSearch, referenceSymbol *ast.Symbol, referenceLocation *ast.Node) (*ast.Symbol, entryKind) { - return state.forEachRelatedSymbol( - referenceSymbol, - referenceLocation, - false, /*isForRenamePopulateSearchSymbolSet*/ - state.options.use != referenceUseRename || state.options.useAliasesForRename, /*onlyIncludeBindingElementAtReferenceLocation*/ - func(sym *ast.Symbol, rootSymbol *ast.Symbol, baseSymbol *ast.Symbol) *ast.Symbol { - // check whether the symbol used to search itself is just the searched one. - if baseSymbol != nil { - // static method/property and instance method/property might have the same name. Only check static or only check instance. - if isStaticSymbol(referenceSymbol) != isStaticSymbol(baseSymbol) { - baseSymbol = nil - } - } - searchSym := core.Coalesce(baseSymbol, core.Coalesce(rootSymbol, sym)) - if searchSym != nil && search.includes(searchSym) { - if rootSymbol != nil && sym.CheckFlags&ast.CheckFlagsSynthetic == 0 { - return rootSymbol - } - return sym - } - // For a base type, use the symbol for the derived type. For a synthetic (e.g. union) property, use the union symbol. - return nil - }, - func(rootSymbol *ast.Symbol) bool { - return !(len(search.parents) != 0 && !core.Some(search.parents, func(parent *ast.Symbol) bool { - return state.explicitlyInheritsFrom(rootSymbol.Parent, parent) - })) - }, - ) -} - -func (state *refState) forEachRelatedSymbol( - symbol *ast.Symbol, - location *ast.Node, - isForRenamePopulateSearchSymbolSet, - onlyIncludeBindingElementAtReferenceLocation bool, - cbSymbol func(*ast.Symbol, *ast.Symbol, *ast.Symbol) *ast.Symbol, - allowBaseTypes func(*ast.Symbol) bool, -) (*ast.Symbol, entryKind) { - fromRoot := func(sym *ast.Symbol) *ast.Symbol { - // If this is a union property: - // - In populateSearchSymbolsSet we will add all the symbols from all its source symbols in all unioned types. - // - In findRelatedSymbol, we will just use the union symbol if any source symbol is included in the search. - // If the symbol is an instantiation from a another symbol (e.g. widened symbol): - // - In populateSearchSymbolsSet, add the root the list - // - In findRelatedSymbol, return the source symbol if that is in the search. (Do not return the instantiation symbol.) - for _, rootSymbol := range state.checker.GetRootSymbols(sym) { - if result := cbSymbol(sym, rootSymbol, nil /*baseSymbol*/); result != nil { - return result - } - // Add symbol of properties/methods of the same name in base classes and implemented interfaces definitions - if rootSymbol.Parent != nil && rootSymbol.Parent.Flags&(ast.SymbolFlagsClass|ast.SymbolFlagsInterface) != 0 && allowBaseTypes(rootSymbol) { - result := getPropertySymbolsFromBaseTypes(rootSymbol.Parent, rootSymbol.Name, state.checker, func(base *ast.Symbol) *ast.Symbol { - return cbSymbol(sym, rootSymbol, base) - }) - if result != nil { - return result - } - } - } - return nil - } - - if containingObjectLiteralElement := getContainingObjectLiteralElement(location); containingObjectLiteralElement != nil { - /* Because in short-hand property assignment, location has two meaning : property name and as value of the property - * When we do findAllReference at the position of the short-hand property assignment, we would want to have references to position of - * property name and variable declaration of the identifier. - * Like in below example, when querying for all references for an identifier 'name', of the property assignment, the language service - * should show both 'name' in 'obj' and 'name' in variable declaration - * const name = "Foo"; - * const obj = { name }; - * In order to do that, we will populate the search set with the value symbol of the identifier as a value of the property assignment - * so that when matching with potential reference symbol, both symbols from property declaration and variable declaration - * will be included correctly. - */ - shorthandValueSymbol := state.checker.GetShorthandAssignmentValueSymbol(location.Parent) - // gets the local symbol - if shorthandValueSymbol != nil && isForRenamePopulateSearchSymbolSet { - // When renaming 'x' in `const o = { x }`, just rename the local variable, not the property. - return cbSymbol(shorthandValueSymbol, nil /*rootSymbol*/, nil /*baseSymbol*/), entryKindSearchedLocalFoundProperty - } - // If the location is in a context sensitive location (i.e. in an object literal) try - // to get a contextual type for it, and add the property symbol from the contextual - // type to the search set - if contextualType := state.checker.GetContextualType(containingObjectLiteralElement.Parent, checker.ContextFlagsNone); contextualType != nil { - symbols := state.checker.GetPropertySymbolsFromContextualType(containingObjectLiteralElement, contextualType, true /*unionSymbolOk*/) - for _, sym := range symbols { - if res := fromRoot(sym); res != nil { - return res, entryKindSearchedPropertyFoundLocal - } - } - } - // If the location is name of property symbol from object literal destructuring pattern - // Search the property symbol - // for ( { property: p2 } of elems) { } - if propertySymbol := state.checker.GetPropertySymbolOfDestructuringAssignment(location); propertySymbol != nil { - if res := cbSymbol(propertySymbol, nil /*rootSymbol*/, nil /*baseSymbol*/); res != nil { - return res, entryKindSearchedPropertyFoundLocal - } - } - if shorthandValueSymbol != nil { - if res := cbSymbol(shorthandValueSymbol, nil /*rootSymbol*/, nil /*baseSymbol*/); res != nil { - return res, entryKindSearchedLocalFoundProperty - } - } - } - - if aliasedSymbol := getMergedAliasedSymbolOfNamespaceExportDeclaration(location, symbol, state.checker); aliasedSymbol != nil { - // In case of UMD module and global merging, search for global as well - if res := cbSymbol(aliasedSymbol, nil /*rootSymbol*/, nil /*baseSymbol*/); res != nil { - return res, entryKindNode - } - } - - if res := fromRoot(symbol); res != nil { - return res, entryKindNone - } - - if symbol.ValueDeclaration != nil && ast.IsParameterPropertyDeclaration(symbol.ValueDeclaration, symbol.ValueDeclaration.Parent) { - // For a parameter property, now try on the other symbol (property if this was a parameter, parameter if this was a property). - if symbol.ValueDeclaration == nil || symbol.ValueDeclaration.Kind != ast.KindParameter { - panic("expected symbol.ValueDeclaration to be a parameter") - } - paramProp1, paramProp2 := state.checker.GetSymbolsOfParameterPropertyDeclaration(symbol.ValueDeclaration, symbol.Name) - debug.Assert((paramProp1.Flags&ast.SymbolFlagsFunctionScopedVariable != 0) && (paramProp2.Flags&ast.SymbolFlagsProperty != 0)) // is [parameter, property] - if !(paramProp1.Flags&ast.SymbolFlagsFunctionScopedVariable != 0 && paramProp2.Flags&ast.SymbolFlagsProperty != 0) { - panic("Expected a parameter and a property") - } - return fromRoot(core.IfElse(symbol.Flags&ast.SymbolFlagsFunctionScopedVariable != 0, paramProp2, paramProp1)), entryKindNone - } - - if exportSpecifier := ast.GetDeclarationOfKind(symbol, ast.KindExportSpecifier); exportSpecifier != nil && (!isForRenamePopulateSearchSymbolSet || exportSpecifier.PropertyName() == nil) { - if localSymbol := state.checker.GetExportSpecifierLocalTargetSymbol(exportSpecifier); localSymbol != nil { - if res := cbSymbol(localSymbol, nil /*rootSymbol*/, nil /*baseSymbol*/); res != nil { - return res, entryKindNode - } - } - } - - // symbolAtLocation for a binding element is the local symbol. See if the search symbol is the property. - // Don't do this when populating search set for a rename when prefix and suffix text will be provided -- just rename the local. - if !isForRenamePopulateSearchSymbolSet { - var bindingElementPropertySymbol *ast.Symbol - if onlyIncludeBindingElementAtReferenceLocation { - if !isObjectBindingElementWithoutPropertyName(location.Parent) { - return nil, entryKindNone - } - bindingElementPropertySymbol = getPropertySymbolFromBindingElement(state.checker, location.Parent) - } else { - bindingElementPropertySymbol = getPropertySymbolOfObjectBindingPatternWithoutPropertyName(symbol, state.checker) - } - if bindingElementPropertySymbol == nil { - return nil, entryKindNone - } - return fromRoot(bindingElementPropertySymbol), entryKindSearchedPropertyFoundLocal - } - - debug.Assert(isForRenamePopulateSearchSymbolSet) - - // due to the above assert and the arguments at the uses of this function, - // (onlyIncludeBindingElementAtReferenceLocation <=> !providePrefixAndSuffixTextForRename) holds - includeOriginalSymbolOfBindingElement := onlyIncludeBindingElementAtReferenceLocation - - if includeOriginalSymbolOfBindingElement { - if bindingElementPropertySymbol := getPropertySymbolOfObjectBindingPatternWithoutPropertyName(symbol, state.checker); bindingElementPropertySymbol != nil { - return fromRoot(bindingElementPropertySymbol), entryKindSearchedPropertyFoundLocal - } - } - return nil, entryKindNone -} - -// Search for all occurrences of an identifier in a source file (and filter out the ones that match). -func (state *refState) searchForName(sourceFile *ast.SourceFile, search *refSearch) { - if _, ok := getNameTable(sourceFile)[search.escapedText]; ok { - state.getReferencesInSourceFile(sourceFile, search, true /*addReferencesHere*/) - } -} - -func (state *refState) explicitlyInheritsFrom(symbol *ast.Symbol, parent *ast.Symbol) bool { - if symbol == parent { - return true - } - - // Check cache first - key := inheritKey{symbol: symbol, parent: parent} - if cached, ok := state.inheritsFromCache[key]; ok { - return cached - } - - // Set to false initially to prevent infinite recursion - state.inheritsFromCache[key] = false - - if symbol.Declarations == nil { - return false - } - - inherits := core.Some(symbol.Declarations, func(declaration *ast.Node) bool { - superTypeNodes := getAllSuperTypeNodes(declaration) - return core.Some(superTypeNodes, func(typeReference *ast.TypeNode) bool { - typ := state.checker.GetTypeAtLocation(typeReference.AsNode()) - return typ != nil && typ.Symbol() != nil && state.explicitlyInheritsFrom(typ.Symbol(), parent) - }) - }) - - // Update cache with the actual result - state.inheritsFromCache[key] = inherits - return inherits -} diff --git a/kitcom/internal/tsgo/ls/format.go b/kitcom/internal/tsgo/ls/format.go deleted file mode 100644 index 779dff6..0000000 --- a/kitcom/internal/tsgo/ls/format.go +++ /dev/null @@ -1,181 +0,0 @@ -package ls - -import ( - "context" - "iter" - - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/format" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner" -) - -func toFormatCodeSettings(opt *lsproto.FormattingOptions, newLine string) *format.FormatCodeSettings { - initial := format.GetDefaultFormatCodeSettings(newLine) - initial.TabSize = int(opt.TabSize) - initial.IndentSize = int(opt.TabSize) - initial.ConvertTabsToSpaces = opt.InsertSpaces - if opt.TrimTrailingWhitespace != nil { - initial.TrimTrailingWhitespace = *opt.TrimTrailingWhitespace - } - - // !!! get format settings - // TODO: We support a _lot_ more options than this - return initial -} - -func (l *LanguageService) toLSProtoTextEdits(file *ast.SourceFile, changes []core.TextChange) []*lsproto.TextEdit { - result := make([]*lsproto.TextEdit, 0, len(changes)) - for _, c := range changes { - result = append(result, &lsproto.TextEdit{ - NewText: c.NewText, - Range: *l.createLspRangeFromBounds(c.Pos(), c.End(), file), - }) - } - return result -} - -func (l *LanguageService) ProvideFormatDocument( - ctx context.Context, - documentURI lsproto.DocumentUri, - options *lsproto.FormattingOptions, -) (lsproto.DocumentFormattingResponse, error) { - _, file := l.getProgramAndFile(documentURI) - edits := l.toLSProtoTextEdits(file, l.getFormattingEditsForDocument( - ctx, - file, - toFormatCodeSettings(options, l.GetProgram().Options().NewLine.GetNewLineCharacter()), - )) - return lsproto.TextEditsOrNull{TextEdits: &edits}, nil -} - -func (l *LanguageService) ProvideFormatDocumentRange( - ctx context.Context, - documentURI lsproto.DocumentUri, - options *lsproto.FormattingOptions, - r lsproto.Range, -) (lsproto.DocumentRangeFormattingResponse, error) { - _, file := l.getProgramAndFile(documentURI) - edits := l.toLSProtoTextEdits(file, l.getFormattingEditsForRange( - ctx, - file, - toFormatCodeSettings(options, l.GetProgram().Options().NewLine.GetNewLineCharacter()), - l.converters.FromLSPRange(file, r), - )) - return lsproto.TextEditsOrNull{TextEdits: &edits}, nil -} - -func (l *LanguageService) ProvideFormatDocumentOnType( - ctx context.Context, - documentURI lsproto.DocumentUri, - options *lsproto.FormattingOptions, - position lsproto.Position, - character string, -) (lsproto.DocumentOnTypeFormattingResponse, error) { - _, file := l.getProgramAndFile(documentURI) - edits := l.toLSProtoTextEdits(file, l.getFormattingEditsAfterKeystroke( - ctx, - file, - toFormatCodeSettings(options, l.GetProgram().Options().NewLine.GetNewLineCharacter()), - int(l.converters.LineAndCharacterToPosition(file, position)), - character, - )) - return lsproto.TextEditsOrNull{TextEdits: &edits}, nil -} - -func (l *LanguageService) getFormattingEditsForRange( - ctx context.Context, - file *ast.SourceFile, - options *format.FormatCodeSettings, - r core.TextRange, -) []core.TextChange { - ctx = format.WithFormatCodeSettings(ctx, options, options.NewLineCharacter) - return format.FormatSelection(ctx, file, r.Pos(), r.End()) -} - -func (l *LanguageService) getFormattingEditsForDocument( - ctx context.Context, - file *ast.SourceFile, - options *format.FormatCodeSettings, -) []core.TextChange { - ctx = format.WithFormatCodeSettings(ctx, options, options.NewLineCharacter) - return format.FormatDocument(ctx, file) -} - -func (l *LanguageService) getFormattingEditsAfterKeystroke( - ctx context.Context, - file *ast.SourceFile, - options *format.FormatCodeSettings, - position int, - key string, -) []core.TextChange { - ctx = format.WithFormatCodeSettings(ctx, options, options.NewLineCharacter) - - if isInComment(file, position, nil) == nil { - switch key { - case "{": - return format.FormatOnOpeningCurly(ctx, file, position) - case "}": - return format.FormatOnClosingCurly(ctx, file, position) - case ";": - return format.FormatOnSemicolon(ctx, file, position) - case "\n": - return format.FormatOnEnter(ctx, file, position) - default: - return nil - } - } - return nil -} - -// Unlike the TS implementation, this function *will not* compute default values for -// `precedingToken` and `tokenAtPosition`. -// It is the caller's responsibility to call `astnav.GetTokenAtPosition` to compute a default `tokenAtPosition`, -// or `astnav.FindPrecedingToken` to compute a default `precedingToken`. -func getRangeOfEnclosingComment( - file *ast.SourceFile, - position int, - precedingToken *ast.Node, - tokenAtPosition *ast.Node, -) *ast.CommentRange { - jsdoc := ast.FindAncestor(tokenAtPosition, (*ast.Node).IsJSDoc) - if jsdoc != nil { - tokenAtPosition = jsdoc.Parent - } - tokenStart := astnav.GetStartOfNode(tokenAtPosition, file, false /*includeJSDoc*/) - if tokenStart <= position && position < tokenAtPosition.End() { - return nil - } - - // Between two consecutive tokens, all comments are either trailing on the former - // or leading on the latter (and none are in both lists). - var trailingRangesOfPreviousToken iter.Seq[ast.CommentRange] - if precedingToken != nil { - trailingRangesOfPreviousToken = scanner.GetTrailingCommentRanges(&ast.NodeFactory{}, file.Text(), precedingToken.End()) - } - leadingRangesOfNextToken := getLeadingCommentRangesOfNode(tokenAtPosition, file) - commentRanges := core.ConcatenateSeq(trailingRangesOfPreviousToken, leadingRangesOfNextToken) - for commentRange := range commentRanges { - // The end marker of a single-line comment does not include the newline character. - // In the following case where the cursor is at `^`, we are inside a comment: - // - // // asdf ^\n - // - // But for closed multi-line comments, we don't want to be inside the comment in the following case: - // - // /* asdf */^ - // - // Internally, we represent the end of the comment prior to the newline and at the '/', respectively. - // - // However, unterminated multi-line comments lack a `/`, end at the end of the file, and *do* contain their end. - // - if commentRange.ContainsExclusive(position) || - position == commentRange.End() && - (commentRange.Kind == ast.KindSingleLineCommentTrivia || position == len(file.Text())) { - return &commentRange - } - } - return nil -} diff --git a/kitcom/internal/tsgo/ls/host.go b/kitcom/internal/tsgo/ls/host.go deleted file mode 100644 index 4951c8c..0000000 --- a/kitcom/internal/tsgo/ls/host.go +++ /dev/null @@ -1,10 +0,0 @@ -package ls - -import "efprojects.com/kitten-ipc/kitcom/internal/tsgo/sourcemap" - -type Host interface { - UseCaseSensitiveFileNames() bool - ReadFile(path string) (contents string, ok bool) - Converters() *Converters - GetECMALineInfo(fileName string) *sourcemap.ECMALineInfo -} diff --git a/kitcom/internal/tsgo/ls/hover.go b/kitcom/internal/tsgo/ls/hover.go deleted file mode 100644 index 9bb6144..0000000 --- a/kitcom/internal/tsgo/ls/hover.go +++ /dev/null @@ -1,488 +0,0 @@ -package ls - -import ( - "context" - "fmt" - "slices" - "strings" - - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/checker" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto" -) - -const ( - symbolFormatFlags = checker.SymbolFormatFlagsWriteTypeParametersOrArguments | checker.SymbolFormatFlagsUseOnlyExternalAliasing | checker.SymbolFormatFlagsAllowAnyNodeKind | checker.SymbolFormatFlagsUseAliasDefinedOutsideCurrentScope - typeFormatFlags = checker.TypeFormatFlagsUseAliasDefinedOutsideCurrentScope -) - -func (l *LanguageService) ProvideHover(ctx context.Context, documentURI lsproto.DocumentUri, position lsproto.Position) (lsproto.HoverResponse, error) { - program, file := l.getProgramAndFile(documentURI) - node := astnav.GetTouchingPropertyName(file, int(l.converters.LineAndCharacterToPosition(file, position))) - if node.Kind == ast.KindSourceFile { - // Avoid giving quickInfo for the sourceFile as a whole. - return lsproto.HoverOrNull{}, nil - } - c, done := program.GetTypeCheckerForFile(ctx, file) - defer done() - quickInfo, documentation := getQuickInfoAndDocumentation(c, node) - if quickInfo == "" { - return lsproto.HoverOrNull{}, nil - } - return lsproto.HoverOrNull{ - Hover: &lsproto.Hover{ - Contents: lsproto.MarkupContentOrStringOrMarkedStringWithLanguageOrMarkedStrings{ - MarkupContent: &lsproto.MarkupContent{ - Kind: lsproto.MarkupKindMarkdown, - Value: formatQuickInfo(quickInfo) + documentation, - }, - }, - }, - }, nil -} - -func getQuickInfoAndDocumentation(c *checker.Checker, node *ast.Node) (string, string) { - return getQuickInfoAndDocumentationForSymbol(c, c.GetSymbolAtLocation(node), getNodeForQuickInfo(node)) -} - -func getQuickInfoAndDocumentationForSymbol(c *checker.Checker, symbol *ast.Symbol, node *ast.Node) (string, string) { - quickInfo, declaration := getQuickInfoAndDeclarationAtLocation(c, symbol, node) - if quickInfo == "" { - return "", "" - } - var b strings.Builder - if declaration != nil { - if jsdoc := getJSDocOrTag(declaration); jsdoc != nil && !containsTypedefTag(jsdoc) { - writeComments(&b, jsdoc.Comments()) - if jsdoc.Kind == ast.KindJSDoc { - if tags := jsdoc.AsJSDoc().Tags; tags != nil { - for _, tag := range tags.Nodes { - if tag.Kind == ast.KindJSDocTypeTag { - continue - } - b.WriteString("\n\n*@") - b.WriteString(tag.TagName().Text()) - b.WriteString("*") - switch tag.Kind { - case ast.KindJSDocParameterTag, ast.KindJSDocPropertyTag: - writeOptionalEntityName(&b, tag.Name()) - case ast.KindJSDocAugmentsTag: - writeOptionalEntityName(&b, tag.AsJSDocAugmentsTag().ClassName) - case ast.KindJSDocSeeTag: - writeOptionalEntityName(&b, tag.AsJSDocSeeTag().NameExpression) - case ast.KindJSDocTemplateTag: - for i, tp := range tag.TypeParameters() { - if i != 0 { - b.WriteString(",") - } - writeOptionalEntityName(&b, tp.Name()) - } - } - comments := tag.Comments() - if len(comments) != 0 { - if commentHasPrefix(comments, "```") { - b.WriteString("\n") - } else { - b.WriteString(" ") - if !commentHasPrefix(comments, "-") { - b.WriteString("— ") - } - } - writeComments(&b, comments) - } - } - } - } - } - } - return quickInfo, b.String() -} - -func formatQuickInfo(quickInfo string) string { - var b strings.Builder - b.Grow(32) - writeCode(&b, "tsx", quickInfo) - return b.String() -} - -func getQuickInfoAndDeclarationAtLocation(c *checker.Checker, symbol *ast.Symbol, node *ast.Node) (string, *ast.Node) { - isAlias := symbol != nil && symbol.Flags&ast.SymbolFlagsAlias != 0 - if isAlias { - symbol = c.GetAliasedSymbol(symbol) - } - if symbol == nil || symbol == c.GetUnknownSymbol() { - return "", nil - } - declaration := symbol.ValueDeclaration - if symbol.Flags&ast.SymbolFlagsClass != 0 && inConstructorContext(node) { - if s := symbol.Members[ast.InternalSymbolNameConstructor]; s != nil { - symbol = s - declaration = core.Find(symbol.Declarations, func(d *ast.Node) bool { - return ast.IsConstructorDeclaration(d) || ast.IsConstructSignatureDeclaration(d) - }) - } - } - flags := symbol.Flags - if flags&ast.SymbolFlagsType != 0 && (ast.IsPartOfTypeNode(node) || ast.IsTypeDeclarationName(node)) { - // If the symbol has a type meaning and we're in a type context, remove value-only meanings - flags &^= ast.SymbolFlagsVariable | ast.SymbolFlagsFunction - } - container := getContainerNode(node) - var b strings.Builder - if isAlias { - b.WriteString("(alias) ") - } - switch { - case flags&(ast.SymbolFlagsVariable|ast.SymbolFlagsProperty|ast.SymbolFlagsAccessor) != 0: - switch { - case flags&ast.SymbolFlagsProperty != 0: - b.WriteString("(property) ") - case flags&ast.SymbolFlagsAccessor != 0: - b.WriteString("(accessor) ") - default: - decl := symbol.ValueDeclaration - if decl != nil { - switch { - case ast.IsParameter(decl): - b.WriteString("(parameter) ") - case ast.IsVarLet(decl): - b.WriteString("let ") - case ast.IsVarConst(decl): - b.WriteString("const ") - case ast.IsVarUsing(decl): - b.WriteString("using ") - case ast.IsVarAwaitUsing(decl): - b.WriteString("await using ") - default: - b.WriteString("var ") - } - } - } - b.WriteString(c.SymbolToStringEx(symbol, container, ast.SymbolFlagsNone, symbolFormatFlags)) - b.WriteString(": ") - b.WriteString(c.TypeToStringEx(c.GetTypeOfSymbolAtLocation(symbol, node), container, typeFormatFlags)) - case flags&ast.SymbolFlagsEnumMember != 0: - b.WriteString("(enum member) ") - t := c.GetTypeOfSymbol(symbol) - b.WriteString(c.TypeToStringEx(t, container, typeFormatFlags)) - if t.Flags()&checker.TypeFlagsLiteral != 0 { - b.WriteString(" = ") - b.WriteString(t.AsLiteralType().String()) - } - case flags&(ast.SymbolFlagsFunction|ast.SymbolFlagsMethod) != 0: - signatures := getSignaturesAtLocation(c, symbol, checker.SignatureKindCall, node) - if len(signatures) == 1 && signatures[0].Declaration() != nil { - declaration = signatures[0].Declaration() - } - prefix := core.IfElse(symbol.Flags&ast.SymbolFlagsMethod != 0, "(method) ", "function ") - writeSignatures(&b, c, signatures, container, prefix, symbol) - case flags&ast.SymbolFlagsConstructor != 0: - signatures := getSignaturesAtLocation(c, symbol.Parent, checker.SignatureKindConstruct, node) - if len(signatures) == 1 && signatures[0].Declaration() != nil { - declaration = signatures[0].Declaration() - } - writeSignatures(&b, c, signatures, container, "constructor ", symbol.Parent) - case flags&(ast.SymbolFlagsClass|ast.SymbolFlagsInterface) != 0: - if node.Kind == ast.KindThisKeyword || ast.IsThisInTypeQuery(node) { - b.WriteString("this") - } else { - b.WriteString(core.IfElse(symbol.Flags&ast.SymbolFlagsClass != 0, "class ", "interface ")) - b.WriteString(c.SymbolToStringEx(symbol, container, ast.SymbolFlagsNone, symbolFormatFlags)) - params := c.GetDeclaredTypeOfSymbol(symbol).AsInterfaceType().LocalTypeParameters() - writeTypeParams(&b, c, params) - } - if flags&ast.SymbolFlagsInterface != 0 { - declaration = core.Find(symbol.Declarations, ast.IsInterfaceDeclaration) - } - case flags&ast.SymbolFlagsEnum != 0: - b.WriteString("enum ") - b.WriteString(c.SymbolToStringEx(symbol, container, ast.SymbolFlagsNone, symbolFormatFlags)) - case flags&ast.SymbolFlagsModule != 0: - b.WriteString(core.IfElse(symbol.ValueDeclaration != nil && ast.IsSourceFile(symbol.ValueDeclaration), "module ", "namespace ")) - b.WriteString(c.SymbolToStringEx(symbol, container, ast.SymbolFlagsNone, symbolFormatFlags)) - case flags&ast.SymbolFlagsTypeParameter != 0: - b.WriteString("(type parameter) ") - tp := c.GetDeclaredTypeOfSymbol(symbol) - b.WriteString(c.SymbolToStringEx(symbol, container, ast.SymbolFlagsNone, symbolFormatFlags)) - cons := c.GetConstraintOfTypeParameter(tp) - if cons != nil { - b.WriteString(" extends ") - b.WriteString(c.TypeToStringEx(cons, container, typeFormatFlags)) - } - declaration = core.Find(symbol.Declarations, ast.IsTypeParameterDeclaration) - case flags&ast.SymbolFlagsTypeAlias != 0: - b.WriteString("type ") - b.WriteString(c.SymbolToStringEx(symbol, container, ast.SymbolFlagsNone, symbolFormatFlags)) - writeTypeParams(&b, c, c.GetTypeAliasTypeParameters(symbol)) - if len(symbol.Declarations) != 0 { - b.WriteString(" = ") - b.WriteString(c.TypeToStringEx(c.GetDeclaredTypeOfSymbol(symbol), container, typeFormatFlags|checker.TypeFormatFlagsInTypeAlias)) - } - declaration = core.Find(symbol.Declarations, ast.IsTypeAliasDeclaration) - case flags&ast.SymbolFlagsAlias != 0: - b.WriteString("import ") - b.WriteString(c.SymbolToStringEx(symbol, container, ast.SymbolFlagsNone, symbolFormatFlags)) - default: - b.WriteString(c.TypeToStringEx(c.GetTypeOfSymbol(symbol), container, typeFormatFlags)) - } - return b.String(), declaration -} - -func getNodeForQuickInfo(node *ast.Node) *ast.Node { - if node.Parent == nil { - return node - } - if ast.IsNewExpression(node.Parent) && node.Pos() == node.Parent.Pos() { - return node.Parent.Expression() - } - if ast.IsNamedTupleMember(node.Parent) && node.Pos() == node.Parent.Pos() { - return node.Parent - } - if ast.IsImportMeta(node.Parent) && node.Parent.Name() == node { - return node.Parent - } - if ast.IsJsxNamespacedName(node.Parent) { - return node.Parent - } - return node -} - -func inConstructorContext(node *ast.Node) bool { - if node.Kind == ast.KindConstructorKeyword { - return true - } - if ast.IsIdentifier(node) { - for ast.IsRightSideOfQualifiedNameOrPropertyAccess(node) { - node = node.Parent - } - if ast.IsNewExpression(node.Parent) { - return true - } - } - return false -} - -func getSignaturesAtLocation(c *checker.Checker, symbol *ast.Symbol, kind checker.SignatureKind, node *ast.Node) []*checker.Signature { - signatures := c.GetSignaturesOfType(c.GetTypeOfSymbol(symbol), kind) - if len(signatures) > 1 || len(signatures) == 1 && len(signatures[0].TypeParameters()) != 0 { - if callNode := getCallOrNewExpression(node); callNode != nil { - signature := c.GetResolvedSignature(callNode) - // If we have a resolved signature, make sure it isn't a synthetic signature - if signature != nil && (slices.Contains(signatures, signature) || signature.Target() != nil && slices.Contains(signatures, signature.Target())) { - return []*checker.Signature{signature} - } - } - } - return signatures -} - -func getCallOrNewExpression(node *ast.Node) *ast.Node { - if ast.IsSourceFile(node) { - return nil - } - if ast.IsPropertyAccessExpression(node.Parent) && node.Parent.Name() == node { - node = node.Parent - } - if ast.IsCallExpression(node.Parent) || ast.IsNewExpression(node.Parent) { - return node.Parent - } - return nil -} - -func writeTypeParams(b *strings.Builder, c *checker.Checker, params []*checker.Type) { - if len(params) > 0 { - b.WriteString("<") - for i, tp := range params { - if i != 0 { - b.WriteString(", ") - } - symbol := tp.Symbol() - b.WriteString(c.SymbolToStringEx(symbol, nil, ast.SymbolFlagsNone, symbolFormatFlags)) - cons := c.GetConstraintOfTypeParameter(tp) - if cons != nil { - b.WriteString(" extends ") - b.WriteString(c.TypeToStringEx(cons, nil, typeFormatFlags)) - } - } - b.WriteString(">") - } -} - -func writeSignatures(b *strings.Builder, c *checker.Checker, signatures []*checker.Signature, container *ast.Node, prefix string, symbol *ast.Symbol) { - for i, sig := range signatures { - if i != 0 { - b.WriteString("\n") - } - if i == 3 && len(signatures) >= 5 { - b.WriteString(fmt.Sprintf("// +%v more overloads", len(signatures)-3)) - break - } - b.WriteString(prefix) - b.WriteString(c.SymbolToStringEx(symbol, container, ast.SymbolFlagsNone, symbolFormatFlags)) - b.WriteString(c.SignatureToStringEx(sig, container, typeFormatFlags|checker.TypeFormatFlagsWriteCallStyleSignature|checker.TypeFormatFlagsWriteTypeArgumentsOfSignature)) - } -} - -func containsTypedefTag(jsdoc *ast.Node) bool { - if jsdoc.Kind == ast.KindJSDoc { - if tags := jsdoc.AsJSDoc().Tags; tags != nil { - for _, tag := range tags.Nodes { - if tag.Kind == ast.KindJSDocTypedefTag { - return true - } - } - } - } - return false -} - -func commentHasPrefix(comments []*ast.Node, prefix string) bool { - return comments[0].Kind == ast.KindJSDocText && strings.HasPrefix(comments[0].Text(), prefix) -} - -func getJSDoc(node *ast.Node) *ast.Node { - return core.LastOrNil(node.JSDoc(nil)) -} - -func getJSDocOrTag(node *ast.Node) *ast.Node { - if jsdoc := getJSDoc(node); jsdoc != nil { - return jsdoc - } - switch { - case ast.IsParameter(node): - return getMatchingJSDocTag(node.Parent, node.Name().Text(), isMatchingParameterTag) - case ast.IsTypeParameterDeclaration(node): - return getMatchingJSDocTag(node.Parent, node.Name().Text(), isMatchingTemplateTag) - case ast.IsVariableDeclaration(node) && ast.IsVariableDeclarationList(node.Parent) && core.FirstOrNil(node.Parent.AsVariableDeclarationList().Declarations.Nodes) == node: - return getJSDocOrTag(node.Parent.Parent) - case (ast.IsFunctionExpressionOrArrowFunction(node) || ast.IsClassExpression(node)) && - (ast.IsVariableDeclaration(node.Parent) || ast.IsPropertyDeclaration(node.Parent) || ast.IsPropertyAssignment(node.Parent)) && node.Parent.Initializer() == node: - return getJSDocOrTag(node.Parent) - } - return nil -} - -func getMatchingJSDocTag(node *ast.Node, name string, match func(*ast.Node, string) bool) *ast.Node { - if jsdoc := getJSDocOrTag(node); jsdoc != nil && jsdoc.Kind == ast.KindJSDoc { - if tags := jsdoc.AsJSDoc().Tags; tags != nil { - for _, tag := range tags.Nodes { - if match(tag, name) { - return tag - } - } - } - } - return nil -} - -func isMatchingParameterTag(tag *ast.Node, name string) bool { - return tag.Kind == ast.KindJSDocParameterTag && isNodeWithName(tag, name) -} - -func isMatchingTemplateTag(tag *ast.Node, name string) bool { - return tag.Kind == ast.KindJSDocTemplateTag && core.Some(tag.TypeParameters(), func(tp *ast.Node) bool { return isNodeWithName(tp, name) }) -} - -func isNodeWithName(node *ast.Node, name string) bool { - nodeName := node.Name() - return ast.IsIdentifier(nodeName) && nodeName.Text() == name -} - -func writeCode(b *strings.Builder, lang string, code string) { - if code == "" { - return - } - ticks := 3 - for strings.Contains(code, strings.Repeat("`", ticks)) { - ticks++ - } - for range ticks { - b.WriteByte('`') - } - b.WriteString(lang) - b.WriteByte('\n') - b.WriteString(code) - b.WriteByte('\n') - for range ticks { - b.WriteByte('`') - } - b.WriteByte('\n') -} - -func writeComments(b *strings.Builder, comments []*ast.Node) { - for _, comment := range comments { - switch comment.Kind { - case ast.KindJSDocText: - b.WriteString(comment.Text()) - case ast.KindJSDocLink: - name := comment.Name() - text := comment.AsJSDocLink().Text() - if name != nil { - if text == "" { - writeEntityName(b, name) - } else { - writeEntityNameParts(b, name) - } - } - b.WriteString(text) - case ast.KindJSDocLinkCode: - // !!! TODO: This is a temporary placeholder implementation that needs to be updated later - name := comment.Name() - text := comment.AsJSDocLinkCode().Text() - if name != nil { - if text == "" { - writeEntityName(b, name) - } else { - writeEntityNameParts(b, name) - } - } - b.WriteString(text) - case ast.KindJSDocLinkPlain: - // !!! TODO: This is a temporary placeholder implementation that needs to be updated later - name := comment.Name() - text := comment.AsJSDocLinkPlain().Text() - if name != nil { - if text == "" { - writeEntityName(b, name) - } else { - writeEntityNameParts(b, name) - } - } - b.WriteString(text) - } - } -} - -func writeOptionalEntityName(b *strings.Builder, name *ast.Node) { - if name != nil { - b.WriteString(" ") - writeEntityName(b, name) - } -} - -func writeEntityName(b *strings.Builder, name *ast.Node) { - b.WriteString("`") - writeEntityNameParts(b, name) - b.WriteString("`") -} - -func writeEntityNameParts(b *strings.Builder, node *ast.Node) { - switch node.Kind { - case ast.KindIdentifier: - b.WriteString(node.Text()) - case ast.KindQualifiedName: - writeEntityNameParts(b, node.AsQualifiedName().Left) - b.WriteByte('.') - writeEntityNameParts(b, node.AsQualifiedName().Right) - case ast.KindPropertyAccessExpression: - writeEntityNameParts(b, node.Expression()) - b.WriteByte('.') - writeEntityNameParts(b, node.Name()) - case ast.KindParenthesizedExpression, ast.KindExpressionWithTypeArguments: - writeEntityNameParts(b, node.Expression()) - case ast.KindJSDocNameReference: - writeEntityNameParts(b, node.Name()) - } -} diff --git a/kitcom/internal/tsgo/ls/importTracker.go b/kitcom/internal/tsgo/ls/importTracker.go deleted file mode 100644 index 7f710e1..0000000 --- a/kitcom/internal/tsgo/ls/importTracker.go +++ /dev/null @@ -1,728 +0,0 @@ -package ls - -import ( - "slices" - - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/checker" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/compiler" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/debug" -) - -type ImpExpKind int32 - -const ( - ImpExpKindUnknown ImpExpKind = iota - ImpExpKindImport - ImpExpKindExport -) - -type ImportExportSymbol struct { - kind ImpExpKind - symbol *ast.Symbol - exportInfo *ExportInfo -} - -type ExportInfo struct { - exportingModuleSymbol *ast.Symbol - exportKind ExportKind -} - -type LocationAndSymbol struct { - importLocation *ast.Node - importSymbol *ast.Symbol -} - -type ImportsResult struct { - importSearches []LocationAndSymbol - singleReferences []*ast.Node - indirectUsers []*ast.SourceFile -} - -type ImportTracker func(exportSymbol *ast.Symbol, exportInfo *ExportInfo, isForRename bool) *ImportsResult - -type ModuleReferenceKind int32 - -const ( - ModuleReferenceKindImport ModuleReferenceKind = iota - ModuleReferenceKindReference - ModuleReferenceKindImplicit -) - -// ModuleReference represents a reference to a module, either via import, , or implicit reference -type ModuleReference struct { - kind ModuleReferenceKind - literal *ast.Node // for import and implicit kinds (StringLiteralLike) - referencingFile *ast.SourceFile - ref *ast.FileReference // for reference kind -} - -// Creates the imports map and returns an ImportTracker that uses it. Call this lazily to avoid calling `getDirectImportsMap` unnecessarily. -func createImportTracker(sourceFiles []*ast.SourceFile, sourceFilesSet *collections.Set[string], checker *checker.Checker) ImportTracker { - allDirectImports := getDirectImportsMap(sourceFiles, checker) - return func(exportSymbol *ast.Symbol, exportInfo *ExportInfo, isForRename bool) *ImportsResult { - directImports, indirectUsers := getImportersForExport(sourceFiles, sourceFilesSet, allDirectImports, exportInfo, checker) - importSearches, singleReferences := getSearchesFromDirectImports(directImports, exportSymbol, exportInfo.exportKind, checker, isForRename) - return &ImportsResult{importSearches, singleReferences, indirectUsers} - } -} - -// Returns a map from a module symbol to all import statements that directly reference the module -func getDirectImportsMap(sourceFiles []*ast.SourceFile, checker *checker.Checker) map[*ast.Symbol][]*ast.Node { - result := make(map[*ast.Symbol][]*ast.Node) - for _, sourceFile := range sourceFiles { - // !!! cancellation - forEachImport(sourceFile, func(importDecl *ast.Node, moduleSpecifier *ast.Node) { - if moduleSymbol := checker.GetSymbolAtLocation(moduleSpecifier); moduleSymbol != nil { - result[moduleSymbol] = append(result[moduleSymbol], importDecl) - } - }) - } - return result -} - -// Calls `action` for each import, re-export, or require() in a file -func forEachImport(sourceFile *ast.SourceFile, action func(importStatement *ast.Node, imported *ast.Node)) { - if sourceFile.ExternalModuleIndicator != nil || len(sourceFile.Imports()) != 0 { - for _, i := range sourceFile.Imports() { - action(importFromModuleSpecifier(i), i) - } - } else { - forEachPossibleImportOrExportStatement(sourceFile.AsNode(), func(node *ast.Node) bool { - switch node.Kind { - case ast.KindExportDeclaration, ast.KindImportDeclaration, ast.KindJSImportDeclaration: - if specifier := node.ModuleSpecifier(); specifier != nil && ast.IsStringLiteral(specifier) { - action(node, specifier) - } - case ast.KindImportEqualsDeclaration: - if isExternalModuleImportEquals(node) { - action(node, node.AsImportEqualsDeclaration().ModuleReference.Expression()) - } - } - return false - }) - } -} - -func forEachPossibleImportOrExportStatement(sourceFileLike *ast.Node, action func(statement *ast.Node) bool) bool { - for _, statement := range getStatementsOfSourceFileLike(sourceFileLike) { - if action(statement) || isAmbientModuleDeclaration(statement) && forEachPossibleImportOrExportStatement(statement, action) { - return true - } - } - return false -} - -func getSourceFileLikeForImportDeclaration(node *ast.Node) *ast.Node { - if ast.IsCallExpression(node) || ast.IsJSDocImportTag(node) { - return ast.GetSourceFileOfNode(node).AsNode() - } - parent := node.Parent - if ast.IsSourceFile(parent) { - return parent - } - debug.Assert(ast.IsModuleBlock(parent) && isAmbientModuleDeclaration(parent.Parent)) - return parent.Parent -} - -func isAmbientModuleDeclaration(node *ast.Node) bool { - return ast.IsModuleDeclaration(node) && ast.IsStringLiteral(node.Name()) -} - -func getStatementsOfSourceFileLike(node *ast.Node) []*ast.Node { - if ast.IsSourceFile(node) { - return node.Statements() - } - if body := node.Body(); body != nil { - return body.Statements() - } - return nil -} - -func getImportersForExport( - sourceFiles []*ast.SourceFile, - sourceFilesSet *collections.Set[string], - allDirectImports map[*ast.Symbol][]*ast.Node, - exportInfo *ExportInfo, - checker *checker.Checker, -) ([]*ast.Node, []*ast.SourceFile) { - var directImports []*ast.Node - var indirectUserDeclarations []*ast.Node - markSeenDirectImport := nodeSeenTracker() - markSeenIndirectUser := nodeSeenTracker() - isAvailableThroughGlobal := exportInfo.exportingModuleSymbol.GlobalExports != nil - - getDirectImports := func(moduleSymbol *ast.Symbol) []*ast.Node { - return allDirectImports[moduleSymbol] - } - - // Adds a module and all of its transitive dependencies as possible indirect users - var addIndirectUser func(*ast.Node, bool) - addIndirectUser = func(sourceFileLike *ast.Node, addTransitiveDependencies bool) { - debug.Assert(!isAvailableThroughGlobal) - if !markSeenIndirectUser(sourceFileLike) { - return - } - indirectUserDeclarations = append(indirectUserDeclarations, sourceFileLike) - if !addTransitiveDependencies { - return - } - moduleSymbol := checker.GetMergedSymbol(sourceFileLike.Symbol()) - if moduleSymbol == nil { - return - } - debug.Assert(moduleSymbol.Flags&ast.SymbolFlagsModule != 0) - for _, directImport := range getDirectImports(moduleSymbol) { - if !ast.IsImportTypeNode(directImport) { - addIndirectUser(getSourceFileLikeForImportDeclaration(directImport), true /*addTransitiveDependencies*/) - } - } - } - - isExported := func(node *ast.Node, stopAtAmbientModule bool) bool { - for node != nil && !(stopAtAmbientModule && isAmbientModuleDeclaration(node)) { - if ast.HasSyntacticModifier(node, ast.ModifierFlagsExport) { - return true - } - node = node.Parent - } - return false - } - - handleImportCall := func(importCall *ast.Node) { - top := ast.FindAncestor(importCall, isAmbientModuleDeclaration) - if top == nil { - top = ast.GetSourceFileOfNode(importCall).AsNode() - } - addIndirectUser(top, isExported(importCall, true /*stopAtAmbientModule*/)) - } - - handleNamespaceImport := func(importDeclaration *ast.Node, name *ast.Node, isReExport bool, alreadyAddedDirect bool) { - if exportInfo.exportKind == ExportKindExportEquals { - // This is a direct import, not import-as-namespace. - if !alreadyAddedDirect { - directImports = append(directImports, importDeclaration) - } - } else if !isAvailableThroughGlobal { - sourceFileLike := getSourceFileLikeForImportDeclaration(importDeclaration) - debug.Assert(ast.IsSourceFile(sourceFileLike) || ast.IsModuleDeclaration(sourceFileLike)) - addIndirectUser(sourceFileLike, isReExport || findNamespaceReExports(sourceFileLike, name, checker)) - } - } - - var handleDirectImports func(*ast.Symbol) - handleDirectImports = func(exportingModuleSymbol *ast.Symbol) { - theseDirectImports := getDirectImports(exportingModuleSymbol) - for _, direct := range theseDirectImports { - if !markSeenDirectImport(direct) { - continue - } - // !!! cancellation - switch direct.Kind { - case ast.KindCallExpression: - if ast.IsImportCall(direct) { - handleImportCall(direct) - } else if !isAvailableThroughGlobal { - parent := direct.Parent - if exportInfo.exportKind == ExportKindExportEquals && ast.IsVariableDeclaration(parent) { - name := parent.Name() - if ast.IsIdentifier(name) { - directImports = append(directImports, name) - } - } - } - case ast.KindIdentifier: - // Nothing - case ast.KindImportEqualsDeclaration: - handleNamespaceImport(direct, direct.Name(), ast.HasSyntacticModifier(direct, ast.ModifierFlagsExport), false /*alreadyAddedDirect*/) - case ast.KindImportDeclaration, ast.KindJSImportDeclaration, ast.KindJSDocImportTag: - directImports = append(directImports, direct) - if importClause := direct.ImportClause(); importClause != nil { - if namedBindings := importClause.AsImportClause().NamedBindings; namedBindings != nil && ast.IsNamespaceImport(namedBindings) { - handleNamespaceImport(direct, namedBindings.Name(), false /*isReExport*/, true /*alreadyAddedDirect*/) - break - } - } - if !isAvailableThroughGlobal && ast.IsDefaultImport(direct) { - addIndirectUser(getSourceFileLikeForImportDeclaration(direct), false) - // Add a check for indirect uses to handle synthetic default imports - } - case ast.KindExportDeclaration: - exportClause := direct.AsExportDeclaration().ExportClause - if exportClause == nil { - // This is `export * from "foo"`, so imports of this module may import the export too. - handleDirectImports(getContainingModuleSymbol(direct, checker)) - } else if ast.IsNamespaceExport(exportClause) { - // `export * as foo from "foo"` add to indirect uses - addIndirectUser(getSourceFileLikeForImportDeclaration(direct), true /*addTransitiveDependencies*/) - } else { - // This is `export { foo } from "foo"` and creates an alias symbol, so recursive search will get handle re-exports. - directImports = append(directImports, direct) - } - case ast.KindImportType: - // Only check for typeof import('xyz') - if !isAvailableThroughGlobal && direct.AsImportTypeNode().IsTypeOf && direct.AsImportTypeNode().Qualifier == nil && isExported(direct, false) { - addIndirectUser(ast.GetSourceFileOfNode(direct).AsNode(), true /*addTransitiveDependencies*/) - } - directImports = append(directImports, direct) - default: - debug.FailBadSyntaxKind(direct, "Unexpected import kind.") - } - } - } - - getIndirectUsers := func() []*ast.SourceFile { - if isAvailableThroughGlobal { - // It has `export as namespace`, so anything could potentially use it. - return sourceFiles - } - // Module augmentations may use this module's exports without importing it. - for _, decl := range exportInfo.exportingModuleSymbol.Declarations { - if ast.IsExternalModuleAugmentation(decl) && sourceFilesSet.Has(ast.GetSourceFileOfNode(decl).FileName()) { - addIndirectUser(decl, false) - } - } - // This may return duplicates (if there are multiple module declarations in a single source file, all importing the same thing as a namespace), but `State.markSearchedSymbol` will handle that. - return core.Map(indirectUserDeclarations, ast.GetSourceFileOfNode) - } - - handleDirectImports(exportInfo.exportingModuleSymbol) - return directImports, getIndirectUsers() -} - -func getContainingModuleSymbol(importer *ast.Node, checker *checker.Checker) *ast.Symbol { - return checker.GetMergedSymbol(getSourceFileLikeForImportDeclaration(importer).Symbol()) -} - -// Returns 'true' is the namespace 'name' is re-exported from this module, and 'false' if it is only used locally -func findNamespaceReExports(sourceFileLike *ast.Node, name *ast.Node, checker *checker.Checker) bool { - namespaceImportSymbol := checker.GetSymbolAtLocation(name) - return forEachPossibleImportOrExportStatement(sourceFileLike, func(statement *ast.Node) bool { - if !ast.IsExportDeclaration(statement) { - return false - } - exportClause := statement.AsExportDeclaration().ExportClause - moduleSpecifier := statement.AsExportDeclaration().ModuleSpecifier - return moduleSpecifier == nil && exportClause != nil && ast.IsNamedExports(exportClause) && core.Some(exportClause.Elements(), func(element *ast.Node) bool { - return checker.GetExportSpecifierLocalTargetSymbol(element) == namespaceImportSymbol - }) - }) -} - -func getSearchesFromDirectImports( - directImports []*ast.Node, - exportSymbol *ast.Symbol, - exportKind ExportKind, - checker *checker.Checker, - isForRename bool, -) ([]LocationAndSymbol, []*ast.Node) { - var importSearches []LocationAndSymbol - var singleReferences []*ast.Node - - addSearch := func(location *ast.Node, symbol *ast.Symbol) { - importSearches = append(importSearches, LocationAndSymbol{location, symbol}) - } - - isNameMatch := func(name string) bool { - // Use name of "default" even in `export =` case because we may have allowSyntheticDefaultImports - return name == exportSymbol.Name || exportKind != ExportKindNamed && name == ast.InternalSymbolNameDefault - } - - // `import x = require("./x")` or `import * as x from "./x"`. - // An `export =` may be imported by this syntax, so it may be a direct import. - // If it's not a direct import, it will be in `indirectUsers`, so we don't have to do anything here. - handleNamespaceImportLike := func(importName *ast.Node) { - // Don't rename an import that already has a different name than the export. - if exportKind == ExportKindExportEquals && (!isForRename || isNameMatch(importName.Text())) { - addSearch(importName, checker.GetSymbolAtLocation(importName)) - } - } - - searchForNamedImport := func(namedBindings *ast.Node) { - if namedBindings == nil { - return - } - for _, element := range namedBindings.Elements() { - name := element.Name() - propertyName := element.PropertyName() - if !isNameMatch(core.OrElse(propertyName, name).Text()) { - continue - } - if propertyName != nil { - // This is `import { foo as bar } from "./a"` or `export { foo as bar } from "./a"`. `foo` isn't a local in the file, so just add it as a single reference. - singleReferences = append(singleReferences, propertyName) - // If renaming `{ foo as bar }`, don't touch `bar`, just `foo`. - // But do rename `foo` in ` { default as foo }` if that's the original export name. - if !isForRename || name.Text() == exportSymbol.Name { - // Search locally for `bar`. - addSearch(name, checker.GetSymbolAtLocation(name)) - } - } else { - var localSymbol *ast.Symbol - if ast.IsExportSpecifier(element) && element.PropertyName() != nil { - localSymbol = checker.GetExportSpecifierLocalTargetSymbol(element) - } else { - localSymbol = checker.GetSymbolAtLocation(name) - } - addSearch(name, localSymbol) - } - } - } - - handleImport := func(decl *ast.Node) { - if ast.IsImportEqualsDeclaration(decl) { - if isExternalModuleImportEquals(decl) { - handleNamespaceImportLike(decl.Name()) - } - return - } - if ast.IsIdentifier(decl) { - handleNamespaceImportLike(decl) - return - } - if ast.IsImportTypeNode(decl) { - if qualifier := decl.AsImportTypeNode().Qualifier; qualifier != nil { - firstIdentifier := ast.GetFirstIdentifier(qualifier) - if firstIdentifier.Text() == ast.SymbolName(exportSymbol) { - singleReferences = append(singleReferences, firstIdentifier) - } - } else if exportKind == ExportKindExportEquals { - singleReferences = append(singleReferences, decl.AsImportTypeNode().Argument.AsLiteralTypeNode().Literal) - } - return - } - // Ignore if there's a grammar error - if !ast.IsStringLiteral(decl.ModuleSpecifier()) { - return - } - if ast.IsExportDeclaration(decl) { - if exportClause := decl.AsExportDeclaration().ExportClause; exportClause != nil && ast.IsNamedExports(exportClause) { - searchForNamedImport(exportClause) - } - return - } - if importClause := decl.ImportClause(); importClause != nil { - if namedBindings := importClause.AsImportClause().NamedBindings; namedBindings != nil { - switch namedBindings.Kind { - case ast.KindNamespaceImport: - handleNamespaceImportLike(namedBindings.Name()) - case ast.KindNamedImports: - // 'default' might be accessed as a named import `{ default as foo }`. - if exportKind == ExportKindNamed || exportKind == ExportKindDefault { - searchForNamedImport(namedBindings) - } - } - } - // `export =` might be imported by a default import if `--allowSyntheticDefaultImports` is on, so this handles both ExportKind.Default and ExportKind.ExportEquals. - // If a default import has the same name as the default export, allow to rename it. - // Given `import f` and `export default function f`, we will rename both, but for `import g` we will rename just that. - if name := importClause.Name(); name != nil && (exportKind == ExportKindDefault || exportKind == ExportKindExportEquals) && (!isForRename || name.Text() == symbolNameNoDefault(exportSymbol)) { - defaultImportAlias := checker.GetSymbolAtLocation(name) - addSearch(name, defaultImportAlias) - } - } - } - for _, decl := range directImports { - handleImport(decl) - } - return importSearches, singleReferences -} - -func getImportOrExportSymbol(node *ast.Node, symbol *ast.Symbol, checker *checker.Checker, comingFromExport bool) *ImportExportSymbol { - exportInfo := func(symbol *ast.Symbol, kind ExportKind) *ImportExportSymbol { - if exportInfo := getExportInfo(symbol, kind, checker); exportInfo != nil { - return &ImportExportSymbol{ - kind: ImpExpKindExport, - symbol: symbol, - exportInfo: exportInfo, - } - } - return nil - } - - getExport := func() *ImportExportSymbol { - getExportAssignmentExport := func(ex *ast.Node) *ImportExportSymbol { - // Get the symbol for the `export =` node; its parent is the module it's the export of. - if ex.Symbol().Parent == nil { - return nil - } - exportKind := core.IfElse(ex.AsExportAssignment().IsExportEquals, ExportKindExportEquals, ExportKindDefault) - return &ImportExportSymbol{ - kind: ImpExpKindExport, - symbol: symbol, - exportInfo: &ExportInfo{ - exportingModuleSymbol: ex.Symbol().Parent, - exportKind: exportKind, - }, - } - } - - // Not meant for use with export specifiers or export assignment. - getExportKindForDeclaration := func(node *ast.Node) ExportKind { - if ast.HasSyntacticModifier(node, ast.ModifierFlagsDefault) { - return ExportKindDefault - } - return ExportKindNamed - } - - getSpecialPropertyExport := func(node *ast.Node, useLhsSymbol bool) *ImportExportSymbol { - var kind ExportKind - switch ast.GetAssignmentDeclarationKind(node.AsBinaryExpression()) { - case ast.JSDeclarationKindExportsProperty: - kind = ExportKindNamed - case ast.JSDeclarationKindModuleExports: - kind = ExportKindExportEquals - default: - return nil - } - sym := symbol - if useLhsSymbol { - sym = checker.GetSymbolAtLocation(ast.GetElementOrPropertyAccessName(node.AsBinaryExpression().Left)) - } - if sym == nil { - return nil - } - return exportInfo(sym, kind) - } - - parent := node.Parent - grandparent := parent.Parent - if symbol.ExportSymbol != nil { - if ast.IsPropertyAccessExpression(parent) { - // When accessing an export of a JS module, there's no alias. The symbol will still be flagged as an export even though we're at the use. - // So check that we are at the declaration. - if ast.IsBinaryExpression(grandparent) && slices.Contains(symbol.Declarations, parent) { - return getSpecialPropertyExport(grandparent, false /*useLhsSymbol*/) - } - return nil - } - return exportInfo(symbol.ExportSymbol, getExportKindForDeclaration(parent)) - } else { - exportNode := getExportNode(parent, node) - switch { - case exportNode != nil && ast.HasSyntacticModifier(exportNode, ast.ModifierFlagsExport): - if ast.IsImportEqualsDeclaration(exportNode) && exportNode.AsImportEqualsDeclaration().ModuleReference == node { - // We're at `Y` in `export import X = Y`. This is not the exported symbol, the left-hand-side is. So treat this as an import statement. - if comingFromExport { - return nil - } - lhsSymbol := checker.GetSymbolAtLocation(exportNode.Name()) - return &ImportExportSymbol{ - kind: ImpExpKindImport, - symbol: lhsSymbol, - } - } - return exportInfo(symbol, getExportKindForDeclaration(exportNode)) - case ast.IsNamespaceExport(parent): - return exportInfo(symbol, ExportKindNamed) - case ast.IsExportAssignment(parent): - return getExportAssignmentExport(parent) - case ast.IsExportAssignment(grandparent): - return getExportAssignmentExport(grandparent) - case ast.IsBinaryExpression(parent): - return getSpecialPropertyExport(parent, true /*useLhsSymbol*/) - case ast.IsBinaryExpression(grandparent): - return getSpecialPropertyExport(grandparent, true /*useLhsSymbol*/) - case ast.IsJSDocTypedefTag(parent) || ast.IsJSDocCallbackTag(parent): - return exportInfo(symbol, ExportKindNamed) - } - } - return nil - } - - getImport := func() *ImportExportSymbol { - if !isNodeImport(node) { - return nil - } - // A symbol being imported is always an alias. So get what that aliases to find the local symbol. - importedSymbol := checker.GetImmediateAliasedSymbol(symbol) - if importedSymbol == nil { - return nil - } - // Search on the local symbol in the exporting module, not the exported symbol. - importedSymbol = skipExportSpecifierSymbol(importedSymbol, checker) - // Similarly, skip past the symbol for 'export =' - if importedSymbol.Name == "export=" { - importedSymbol = getExportEqualsLocalSymbol(importedSymbol, checker) - if importedSymbol == nil { - return nil - } - } - // If the import has a different name than the export, do not continue searching. - // If `importedName` is undefined, do continue searching as the export is anonymous. - // (All imports returned from this function will be ignored anyway if we are in rename and this is a not a named export.) - importedName := symbolNameNoDefault(importedSymbol) - if importedName == "" || importedName == ast.InternalSymbolNameDefault || importedName == symbol.Name { - return &ImportExportSymbol{ - kind: ImpExpKindImport, - symbol: importedSymbol, - } - } - return nil - } - - result := getExport() - if result == nil && !comingFromExport { - result = getImport() - } - return result -} - -func getExportInfo(exportSymbol *ast.Symbol, exportKind ExportKind, c *checker.Checker) *ExportInfo { - // Parent can be nil if an `export` is not at the top-level (which is a compile error). - if exportSymbol.Parent != nil { - exportingModuleSymbol := c.GetMergedSymbol(exportSymbol.Parent) - // `export` may appear in a namespace. In that case, just rely on global search. - if checker.IsExternalModuleSymbol(exportingModuleSymbol) { - return &ExportInfo{ - exportingModuleSymbol: exportingModuleSymbol, - exportKind: exportKind, - } - } - } - return nil -} - -// If a reference is a class expression, the exported node would be its parent. -// If a reference is a variable declaration, the exported node would be the variable statement. -func getExportNode(parent *ast.Node, node *ast.Node) *ast.Node { - var declaration *ast.Node - switch { - case ast.IsVariableDeclaration(parent): - declaration = parent - case ast.IsBindingElement(parent): - declaration = ast.WalkUpBindingElementsAndPatterns(parent) - } - if declaration != nil { - if parent.Name() == node && !ast.IsCatchClause(declaration.Parent) && ast.IsVariableStatement(declaration.Parent.Parent) { - return declaration.Parent.Parent - } - return nil - } - return parent -} - -func isNodeImport(node *ast.Node) bool { - parent := node.Parent - switch parent.Kind { - case ast.KindImportEqualsDeclaration: - return parent.Name() == node && isExternalModuleImportEquals(parent) - case ast.KindImportSpecifier: - // For a rename import `{ foo as bar }`, don't search for the imported symbol. Just find local uses of `bar`. - return parent.PropertyName() == nil - case ast.KindImportClause, ast.KindNamespaceImport: - debug.Assert(parent.Name() == node) - return true - case ast.KindBindingElement: - return ast.IsInJSFile(node) && ast.IsVariableDeclarationInitializedToRequire(parent.Parent.Parent) - } - return false -} - -func isExternalModuleImportEquals(node *ast.Node) bool { - moduleReference := node.AsImportEqualsDeclaration().ModuleReference - return ast.IsExternalModuleReference(moduleReference) && moduleReference.Expression().Kind == ast.KindStringLiteral -} - -// If at an export specifier, go to the symbol it refers to. */ -func skipExportSpecifierSymbol(symbol *ast.Symbol, checker *checker.Checker) *ast.Symbol { - // For `export { foo } from './bar", there's nothing to skip, because it does not create a new alias. But `export { foo } does. - for _, declaration := range symbol.Declarations { - switch { - case ast.IsExportSpecifier(declaration) && declaration.PropertyName() == nil && declaration.Parent.Parent.ModuleSpecifier() == nil: - return core.OrElse(checker.GetExportSpecifierLocalTargetSymbol(declaration), symbol) - case ast.IsPropertyAccessExpression(declaration) && ast.IsModuleExportsAccessExpression(declaration.Expression()) && !ast.IsPrivateIdentifier(declaration.Name()): - // Export of form 'module.exports.propName = expr'; - return checker.GetSymbolAtLocation(declaration) - case ast.IsShorthandPropertyAssignment(declaration) && ast.IsBinaryExpression(declaration.Parent.Parent) && ast.GetAssignmentDeclarationKind(declaration.Parent.Parent.AsBinaryExpression()) == ast.JSDeclarationKindModuleExports: - return checker.GetExportSpecifierLocalTargetSymbol(declaration.Name()) - } - } - return symbol -} - -func getExportEqualsLocalSymbol(importedSymbol *ast.Symbol, checker *checker.Checker) *ast.Symbol { - if importedSymbol.Flags&ast.SymbolFlagsAlias != 0 { - return checker.GetImmediateAliasedSymbol(importedSymbol) - } - decl := debug.CheckDefined(importedSymbol.ValueDeclaration) - switch { - case ast.IsExportAssignment(decl): - return decl.Expression().Symbol() - case ast.IsBinaryExpression(decl): - return decl.AsBinaryExpression().Right.Symbol() - case ast.IsSourceFile(decl): - return decl.Symbol() - } - return nil -} - -func symbolNameNoDefault(symbol *ast.Symbol) string { - if symbol.Name != ast.InternalSymbolNameDefault { - return symbol.Name - } - for _, decl := range symbol.Declarations { - name := ast.GetNameOfDeclaration(decl) - if name != nil && ast.IsIdentifier(name) { - return name.Text() - } - } - return "" -} - -// findModuleReferences finds all references to a module symbol across the given source files. -// This includes import statements, directives, and implicit references (e.g., JSX runtime imports). -func findModuleReferences(program *compiler.Program, sourceFiles []*ast.SourceFile, searchModuleSymbol *ast.Symbol, checker *checker.Checker) []ModuleReference { - refs := []ModuleReference{} - - for _, referencingFile := range sourceFiles { - searchSourceFile := searchModuleSymbol.ValueDeclaration - if searchSourceFile != nil && searchSourceFile.Kind == ast.KindSourceFile { - // Check directives - for _, ref := range referencingFile.ReferencedFiles { - if program.GetSourceFileFromReference(referencingFile, ref) == searchSourceFile.AsSourceFile() { - refs = append(refs, ModuleReference{ - kind: ModuleReferenceKindReference, - referencingFile: referencingFile, - ref: ref, - }) - } - } - - // Check directives - for _, ref := range referencingFile.TypeReferenceDirectives { - referenced := program.GetResolvedTypeReferenceDirectiveFromTypeReferenceDirective(ref, referencingFile) - if referenced != nil && referenced.ResolvedFileName == searchSourceFile.AsSourceFile().FileName() { - refs = append(refs, ModuleReference{ - kind: ModuleReferenceKindReference, - referencingFile: referencingFile, - ref: ref, - }) - } - } - } - - // Check all imports (including require() calls) - forEachImport(referencingFile, func(importDecl *ast.Node, moduleSpecifier *ast.Node) { - moduleSymbol := checker.GetSymbolAtLocation(moduleSpecifier) - if moduleSymbol == searchModuleSymbol { - if ast.NodeIsSynthesized(importDecl) { - refs = append(refs, ModuleReference{ - kind: ModuleReferenceKindImplicit, - literal: moduleSpecifier, - referencingFile: referencingFile, - }) - } else { - refs = append(refs, ModuleReference{ - kind: ModuleReferenceKindImport, - literal: moduleSpecifier, - }) - } - } - }) - } - - return refs -} diff --git a/kitcom/internal/tsgo/ls/languageservice.go b/kitcom/internal/tsgo/ls/languageservice.go deleted file mode 100644 index f6288e6..0000000 --- a/kitcom/internal/tsgo/ls/languageservice.go +++ /dev/null @@ -1,67 +0,0 @@ -package ls - -import ( - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/compiler" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/sourcemap" -) - -type LanguageService struct { - host Host - program *compiler.Program - converters *Converters - documentPositionMappers map[string]*sourcemap.DocumentPositionMapper -} - -func NewLanguageService( - program *compiler.Program, - host Host, -) *LanguageService { - return &LanguageService{ - host: host, - program: program, - converters: host.Converters(), - documentPositionMappers: map[string]*sourcemap.DocumentPositionMapper{}, - } -} - -func (l *LanguageService) GetProgram() *compiler.Program { - return l.program -} - -func (l *LanguageService) tryGetProgramAndFile(fileName string) (*compiler.Program, *ast.SourceFile) { - program := l.GetProgram() - file := program.GetSourceFile(fileName) - return program, file -} - -func (l *LanguageService) getProgramAndFile(documentURI lsproto.DocumentUri) (*compiler.Program, *ast.SourceFile) { - fileName := documentURI.FileName() - program, file := l.tryGetProgramAndFile(fileName) - if file == nil { - panic("file not found: " + fileName) - } - return program, file -} - -func (l *LanguageService) GetDocumentPositionMapper(fileName string) *sourcemap.DocumentPositionMapper { - d, ok := l.documentPositionMappers[fileName] - if !ok { - d = sourcemap.GetDocumentPositionMapper(l, fileName) - l.documentPositionMappers[fileName] = d - } - return d -} - -func (l *LanguageService) ReadFile(fileName string) (string, bool) { - return l.host.ReadFile(fileName) -} - -func (l *LanguageService) UseCaseSensitiveFileNames() bool { - return l.host.UseCaseSensitiveFileNames() -} - -func (l *LanguageService) GetECMALineInfo(fileName string) *sourcemap.ECMALineInfo { - return l.host.GetECMALineInfo(fileName) -} diff --git a/kitcom/internal/tsgo/ls/linemap.go b/kitcom/internal/tsgo/ls/linemap.go deleted file mode 100644 index 3ce9327..0000000 --- a/kitcom/internal/tsgo/ls/linemap.go +++ /dev/null @@ -1,71 +0,0 @@ -package ls - -import ( - "cmp" - "slices" - "strings" - "unicode/utf8" - - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" -) - -type LSPLineStarts []core.TextPos - -type LSPLineMap struct { - LineStarts LSPLineStarts - AsciiOnly bool // TODO(jakebailey): collect ascii-only info per line -} - -func ComputeLSPLineStarts(text string) *LSPLineMap { - // This is like core.ComputeLineStarts, but only considers "\n", "\r", and "\r\n" as line breaks, - // and reports when the text is ASCII-only. - lineStarts := make([]core.TextPos, 0, strings.Count(text, "\n")+1) - asciiOnly := true - - textLen := core.TextPos(len(text)) - var pos core.TextPos - var lineStart core.TextPos - for pos < textLen { - b := text[pos] - if b < utf8.RuneSelf { - pos++ - switch b { - case '\r': - if pos < textLen && text[pos] == '\n' { - pos++ - } - fallthrough - case '\n': - lineStarts = append(lineStarts, lineStart) - lineStart = pos - } - } else { - _, size := utf8.DecodeRuneInString(text[pos:]) - pos += core.TextPos(size) - asciiOnly = false - } - } - lineStarts = append(lineStarts, lineStart) - - return &LSPLineMap{ - LineStarts: lineStarts, - AsciiOnly: asciiOnly, - } -} - -func (lm *LSPLineMap) ComputeIndexOfLineStart(targetPos core.TextPos) int { - // port of computeLineOfPosition(lineStarts: readonly number[], position: number, lowerBound?: number): number { - lineNumber, ok := slices.BinarySearchFunc(lm.LineStarts, targetPos, func(p, t core.TextPos) int { - return cmp.Compare(int(p), int(t)) - }) - if !ok && lineNumber > 0 { - // If the actual position was not found, the binary search returns where the target line start would be inserted - // if the target was in the slice. - // e.g. if the line starts at [5, 10, 23, 80] and the position requested was 20 - // then the search will return (3, false). - // - // We want the index of the previous line start, so we subtract 1. - lineNumber = lineNumber - 1 - } - return lineNumber -} diff --git a/kitcom/internal/tsgo/ls/organizeimports.go b/kitcom/internal/tsgo/ls/organizeimports.go deleted file mode 100644 index 26d89df..0000000 --- a/kitcom/internal/tsgo/ls/organizeimports.go +++ /dev/null @@ -1,128 +0,0 @@ -package ls - -import ( - "cmp" - "strings" - - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/compiler" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/modulespecifiers" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath" -) - -// statement = anyImportOrRequireStatement -func getImportDeclarationInsertIndex(sortedImports []*ast.Statement, newImport *ast.Statement, comparer func(a, b *ast.Statement) int) int { - // !!! - return len(sortedImports) -} - -// returns `-1` if `a` is better than `b` -// -// note: this sorts in descending order of preference; different than convention in other cmp-like functions -func compareModuleSpecifiers( - a *ImportFix, // !!! ImportFixWithModuleSpecifier - b *ImportFix, // !!! ImportFixWithModuleSpecifier - importingFile *ast.SourceFile, // | FutureSourceFile, - program *compiler.Program, - preferences UserPreferences, - allowsImportingSpecifier func(specifier string) bool, - toPath func(fileName string) tspath.Path, -) int { - if a.kind == ImportFixKindUseNamespace || b.kind == ImportFixKindUseNamespace { - return 0 - } - if comparison := compareBooleans( - b.moduleSpecifierKind != modulespecifiers.ResultKindNodeModules || allowsImportingSpecifier(b.moduleSpecifier), - a.moduleSpecifierKind != modulespecifiers.ResultKindNodeModules || allowsImportingSpecifier(a.moduleSpecifier), - ); comparison != 0 { - return comparison - } - if comparison := compareModuleSpecifierRelativity(a, b, preferences); comparison != 0 { - return comparison - } - if comparison := compareNodeCoreModuleSpecifiers(a.moduleSpecifier, b.moduleSpecifier, importingFile, program); comparison != 0 { - return comparison - } - if comparison := compareBooleans(isFixPossiblyReExportingImportingFile(a, importingFile.Path(), toPath), isFixPossiblyReExportingImportingFile(b, importingFile.Path(), toPath)); comparison != 0 { - return comparison - } - if comparison := compareNumberOfDirectorySeparators(a.moduleSpecifier, b.moduleSpecifier); comparison != 0 { - return comparison - } - return 0 -} - -// True > False -func compareBooleans(a, b bool) int { - if a && !b { - return -1 - } else if !a && b { - return 1 - } - return 0 -} - -// returns `-1` if `a` is better than `b` -func compareModuleSpecifierRelativity(a *ImportFix, b *ImportFix, preferences UserPreferences) int { - switch preferences.ImportModuleSpecifierPreference { - case modulespecifiers.ImportModuleSpecifierPreferenceNonRelative, modulespecifiers.ImportModuleSpecifierPreferenceProjectRelative: - return compareBooleans(a.moduleSpecifierKind == modulespecifiers.ResultKindRelative, b.moduleSpecifierKind == modulespecifiers.ResultKindRelative) - } - return 0 -} - -func compareNodeCoreModuleSpecifiers(a, b string, importingFile *ast.SourceFile, program *compiler.Program) int { - if strings.HasPrefix(a, "node:") && !strings.HasPrefix(b, "node:") { - if shouldUseUriStyleNodeCoreModules(importingFile, program) { - return -1 - } - return 1 - } - if strings.HasPrefix(b, "node:") && !strings.HasPrefix(a, "node:") { - if shouldUseUriStyleNodeCoreModules(importingFile, program) { - return 1 - } - return -1 - } - return 0 -} - -func shouldUseUriStyleNodeCoreModules(file *ast.SourceFile, program *compiler.Program) bool { - for _, node := range file.Imports() { - if core.NodeCoreModules()[node.Text()] && !core.ExclusivelyPrefixedNodeCoreModules[node.Text()] { - if strings.HasPrefix(node.Text(), "node:") { - return true - } else { - return false - } - } - } - - return program.UsesUriStyleNodeCoreModules() -} - -// This is a simple heuristic to try to avoid creating an import cycle with a barrel re-export. -// E.g., do not `import { Foo } from ".."` when you could `import { Foo } from "../Foo"`. -// This can produce false positives or negatives if re-exports cross into sibling directories -// (e.g. `export * from "../whatever"`) or are not named "index". -func isFixPossiblyReExportingImportingFile(fix *ImportFix, importingFilePath tspath.Path, toPath func(fileName string) tspath.Path) bool { - if fix.isReExport != nil && *(fix.isReExport) && - fix.exportInfo != nil && fix.exportInfo.moduleFileName != "" && isIndexFileName(fix.exportInfo.moduleFileName) { - reExportDir := toPath(tspath.GetDirectoryPath(fix.exportInfo.moduleFileName)) - return strings.HasPrefix(string(importingFilePath), string(reExportDir)) - } - return false -} - -func compareNumberOfDirectorySeparators(path1, path2 string) int { - return cmp.Compare(strings.Count(path1, "/"), strings.Count(path2, "/")) -} - -func isIndexFileName(fileName string) bool { - fileName = tspath.GetBaseFileName(fileName) - if tspath.FileExtensionIsOneOf(fileName, []string{".js", ".jsx", ".d.ts", ".ts", ".tsx"}) { - fileName = tspath.RemoveFileExtension(fileName) - } - return fileName == "index" -} diff --git a/kitcom/internal/tsgo/ls/signaturehelp.go b/kitcom/internal/tsgo/ls/signaturehelp.go deleted file mode 100644 index 705f052..0000000 --- a/kitcom/internal/tsgo/ls/signaturehelp.go +++ /dev/null @@ -1,1157 +0,0 @@ -package ls - -import ( - "context" - "fmt" - "strings" - - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/checker" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/compiler" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/debug" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/nodebuilder" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/printer" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner" -) - -type callInvocation struct { - node *ast.Node -} - -type typeArgsInvocation struct { - called *ast.Identifier -} - -type contextualInvocation struct { - signature *checker.Signature - node *ast.Node // Just for enclosingDeclaration for printing types - symbol *ast.Symbol -} - -type invocation struct { - callInvocation *callInvocation - typeArgsInvocation *typeArgsInvocation - contextualInvocation *contextualInvocation -} - -func (l *LanguageService) ProvideSignatureHelp( - ctx context.Context, - documentURI lsproto.DocumentUri, - position lsproto.Position, - context *lsproto.SignatureHelpContext, - clientOptions *lsproto.SignatureHelpClientCapabilities, - preferences *UserPreferences, -) (lsproto.SignatureHelpResponse, error) { - program, sourceFile := l.getProgramAndFile(documentURI) - items := l.GetSignatureHelpItems( - ctx, - int(l.converters.LineAndCharacterToPosition(sourceFile, position)), - program, - sourceFile, - context, - clientOptions, - preferences) - return lsproto.SignatureHelpOrNull{SignatureHelp: items}, nil -} - -func (l *LanguageService) GetSignatureHelpItems( - ctx context.Context, - position int, - program *compiler.Program, - sourceFile *ast.SourceFile, - context *lsproto.SignatureHelpContext, - clientOptions *lsproto.SignatureHelpClientCapabilities, - preferences *UserPreferences, -) *lsproto.SignatureHelp { - typeChecker, done := program.GetTypeCheckerForFile(ctx, sourceFile) - defer done() - - // Decide whether to show signature help - startingToken := astnav.FindPrecedingToken(sourceFile, position) - if startingToken == nil { - // We are at the beginning of the file - return nil - } - - type signatureHelpTriggerReasonKind int32 - - const ( - signatureHelpTriggerReasonKindNone signatureHelpTriggerReasonKind = 0 // was undefined - signatureHelpTriggerReasonKindInvoked signatureHelpTriggerReasonKind = iota // was "invoked" - signatureHelpTriggerReasonKindCharacterTyped // was "characterTyped" - signatureHelpTriggerReasonKindRetriggered // was "retrigger" - ) - - // Emulate VS Code's toTsTriggerReason. - triggerReasonKind := signatureHelpTriggerReasonKindNone - if context != nil { - switch context.TriggerKind { - case lsproto.SignatureHelpTriggerKindTriggerCharacter: - if context.TriggerCharacter != nil { - if context.IsRetrigger { - triggerReasonKind = signatureHelpTriggerReasonKindRetriggered - } else { - triggerReasonKind = signatureHelpTriggerReasonKindCharacterTyped - } - } else { - triggerReasonKind = signatureHelpTriggerReasonKindInvoked - } - case lsproto.SignatureHelpTriggerKindContentChange: - if context.IsRetrigger { - triggerReasonKind = signatureHelpTriggerReasonKindRetriggered - } else { - triggerReasonKind = signatureHelpTriggerReasonKindCharacterTyped - } - case lsproto.SignatureHelpTriggerKindInvoked: - triggerReasonKind = signatureHelpTriggerReasonKindInvoked - default: - triggerReasonKind = signatureHelpTriggerReasonKindInvoked - } - } - - // Only need to be careful if the user typed a character and signature help wasn't showing. - onlyUseSyntacticOwners := triggerReasonKind == signatureHelpTriggerReasonKindCharacterTyped - - // Bail out quickly in the middle of a string or comment, don't provide signature help unless the user explicitly requested it. - if onlyUseSyntacticOwners && IsInString(sourceFile, position, startingToken) { // isInComment(sourceFile, position) needs formatting implemented - return nil - } - - isManuallyInvoked := triggerReasonKind == signatureHelpTriggerReasonKindInvoked - argumentInfo := getContainingArgumentInfo(startingToken, sourceFile, typeChecker, isManuallyInvoked, position) - if argumentInfo == nil { - return nil - } - - // cancellationToken.throwIfCancellationRequested(); - - // Extra syntactic and semantic filtering of signature help - candidateInfo := getCandidateOrTypeInfo(argumentInfo, typeChecker, sourceFile, startingToken, onlyUseSyntacticOwners) - // cancellationToken.throwIfCancellationRequested(); - - if candidateInfo == nil { - // !!! - // // We didn't have any sig help items produced by the TS compiler. If this is a JS - // // file, then see if we can figure out anything better. - // return isSourceFileJS(sourceFile) ? createJSSignatureHelpItems(argumentInfo, program, cancellationToken) : undefined; - return nil - } - - // return typeChecker.runWithCancellationToken(cancellationToken, typeChecker => - if candidateInfo.candidateInfo != nil { - return createSignatureHelpItems(candidateInfo.candidateInfo.candidates, candidateInfo.candidateInfo.resolvedSignature, argumentInfo, sourceFile, typeChecker, onlyUseSyntacticOwners, clientOptions) - } - return createTypeHelpItems(candidateInfo.typeInfo, argumentInfo, sourceFile, clientOptions, typeChecker) -} - -func createTypeHelpItems(symbol *ast.Symbol, argumentInfo *argumentListInfo, sourceFile *ast.SourceFile, clientOptions *lsproto.SignatureHelpClientCapabilities, c *checker.Checker) *lsproto.SignatureHelp { - typeParameters := c.GetLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol) - if typeParameters == nil { - return nil - } - item := getTypeHelpItem(symbol, typeParameters, getEnclosingDeclarationFromInvocation(argumentInfo.invocation), sourceFile, c) - - // Converting signatureHelpParameter to *lsproto.ParameterInformation - parameters := make([]*lsproto.ParameterInformation, len(item.Parameters)) - for i, param := range item.Parameters { - parameters[i] = param.parameterInfo - } - signatureInformation := []*lsproto.SignatureInformation{ - { - Label: item.Label, - Documentation: nil, - Parameters: ¶meters, - }, - } - - return &lsproto.SignatureHelp{ - Signatures: signatureInformation, - ActiveSignature: ptrTo(uint32(0)), - ActiveParameter: &lsproto.UintegerOrNull{Uinteger: ptrTo(uint32(argumentInfo.argumentIndex))}, - } -} - -func getTypeHelpItem(symbol *ast.Symbol, typeParameter []*checker.Type, enclosingDeclaration *ast.Node, sourceFile *ast.SourceFile, c *checker.Checker) signatureInformation { - printer := printer.NewPrinter(printer.PrinterOptions{NewLine: core.NewLineKindLF}, printer.PrintHandlers{}, nil) - - parameters := make([]signatureHelpParameter, len(typeParameter)) - for i, typeParam := range typeParameter { - parameters[i] = createSignatureHelpParameterForTypeParameter(typeParam, sourceFile, enclosingDeclaration, c, printer) - } - - // Creating display label - var displayParts strings.Builder - displayParts.WriteString(c.SymbolToString(symbol)) - if len(parameters) != 0 { - displayParts.WriteString(scanner.TokenToString(ast.KindLessThanToken)) - for i, typeParameter := range parameters { - if i > 0 { - displayParts.WriteString(", ") - } - displayParts.WriteString(*typeParameter.parameterInfo.Label.String) - } - displayParts.WriteString(scanner.TokenToString(ast.KindGreaterThanToken)) - } - - return signatureInformation{ - Label: displayParts.String(), - Documentation: nil, - Parameters: parameters, - IsVariadic: false, - } -} - -func createSignatureHelpItems(candidates []*checker.Signature, resolvedSignature *checker.Signature, argumentInfo *argumentListInfo, sourceFile *ast.SourceFile, c *checker.Checker, useFullPrefix bool, clientOptions *lsproto.SignatureHelpClientCapabilities) *lsproto.SignatureHelp { - enclosingDeclaration := getEnclosingDeclarationFromInvocation(argumentInfo.invocation) - if enclosingDeclaration == nil { - return nil - } - var callTargetSymbol *ast.Symbol - if argumentInfo.invocation.contextualInvocation != nil { - callTargetSymbol = argumentInfo.invocation.contextualInvocation.symbol - } else { - callTargetSymbol = c.GetSymbolAtLocation(getExpressionFromInvocation(argumentInfo)) - if callTargetSymbol == nil && useFullPrefix && resolvedSignature.Declaration() != nil { - callTargetSymbol = resolvedSignature.Declaration().Symbol() - } - } - - var callTargetDisplayParts strings.Builder - if callTargetSymbol != nil { - callTargetDisplayParts.WriteString(c.SymbolToString(callTargetSymbol)) - } - items := make([][]signatureInformation, len(candidates)) - for i, candidateSignature := range candidates { - items[i] = getSignatureHelpItem(candidateSignature, argumentInfo.isTypeParameterList, callTargetDisplayParts.String(), enclosingDeclaration, sourceFile, c) - } - - selectedItemIndex := 0 - itemSeen := 0 - for i := range items { - item := items[i] - if (candidates)[i] == resolvedSignature { - selectedItemIndex = itemSeen - if len(item) > 1 { - count := 0 - for _, j := range item { - if j.IsVariadic || len(j.Parameters) >= argumentInfo.argumentCount { - selectedItemIndex = itemSeen + count - break - } - count++ - } - } - } - itemSeen = itemSeen + len(item) - } - - debug.Assert(selectedItemIndex != -1) - flattenedSignatures := []signatureInformation{} - for _, item := range items { - flattenedSignatures = append(flattenedSignatures, item...) - } - if len(flattenedSignatures) == 0 { - return nil - } - - // Converting []signatureInformation to []*lsproto.SignatureInformation - signatureInformation := make([]*lsproto.SignatureInformation, len(flattenedSignatures)) - for i, item := range flattenedSignatures { - parameters := make([]*lsproto.ParameterInformation, len(item.Parameters)) - for j, param := range item.Parameters { - parameters[j] = param.parameterInfo - } - signatureInformation[i] = &lsproto.SignatureInformation{ - Label: item.Label, - Documentation: nil, - Parameters: ¶meters, - } - } - - help := &lsproto.SignatureHelp{ - Signatures: signatureInformation, - ActiveSignature: ptrTo(uint32(selectedItemIndex)), - ActiveParameter: &lsproto.UintegerOrNull{Uinteger: ptrTo(uint32(argumentInfo.argumentIndex))}, - } - - activeSignature := flattenedSignatures[selectedItemIndex] - if activeSignature.IsVariadic { - firstRest := core.FindIndex(activeSignature.Parameters, func(p signatureHelpParameter) bool { - return p.isRest - }) - if -1 < firstRest && firstRest < len(activeSignature.Parameters)-1 { - // We don't have any code to get this correct; instead, don't highlight a current parameter AT ALL - help.ActiveParameter = &lsproto.UintegerOrNull{Uinteger: ptrTo(uint32(len(activeSignature.Parameters)))} - } - if help.ActiveParameter != nil && help.ActiveParameter.Uinteger != nil && *help.ActiveParameter.Uinteger > uint32(len(activeSignature.Parameters)-1) { - help.ActiveParameter = &lsproto.UintegerOrNull{Uinteger: ptrTo(uint32(len(activeSignature.Parameters) - 1))} - } - } - return help -} - -func getSignatureHelpItem(candidate *checker.Signature, isTypeParameterList bool, callTargetSymbol string, enclosingDeclaration *ast.Node, sourceFile *ast.SourceFile, c *checker.Checker) []signatureInformation { - var infos []*signatureHelpItemInfo - if isTypeParameterList { - infos = itemInfoForTypeParameters(candidate, c, enclosingDeclaration, sourceFile) - } else { - infos = itemInfoForParameters(candidate, c, enclosingDeclaration, sourceFile) - } - - suffixDisplayParts := returnTypeToDisplayParts(candidate, c) - - result := make([]signatureInformation, len(infos)) - for i, info := range infos { - var display strings.Builder - display.WriteString(callTargetSymbol) - display.WriteString(info.displayParts) - display.WriteString(suffixDisplayParts) - result[i] = signatureInformation{ - Label: display.String(), - Documentation: nil, - Parameters: info.parameters, - IsVariadic: info.isVariadic, - } - } - return result -} - -func returnTypeToDisplayParts(candidateSignature *checker.Signature, c *checker.Checker) string { - var returnType strings.Builder - returnType.WriteString(": ") - predicate := c.GetTypePredicateOfSignature(candidateSignature) - if predicate != nil { - returnType.WriteString(c.TypePredicateToString(predicate)) - } else { - returnType.WriteString(c.TypeToString(c.GetReturnTypeOfSignature(candidateSignature))) - } - return returnType.String() -} - -func itemInfoForTypeParameters(candidateSignature *checker.Signature, c *checker.Checker, enclosingDeclaration *ast.Node, sourceFile *ast.SourceFile) []*signatureHelpItemInfo { - printer := printer.NewPrinter(printer.PrinterOptions{NewLine: core.NewLineKindLF}, printer.PrintHandlers{}, nil) - - var typeParameters []*checker.Type - if candidateSignature.Target() != nil { - typeParameters = candidateSignature.Target().TypeParameters() - } else { - typeParameters = candidateSignature.TypeParameters() - } - signatureHelpTypeParameters := make([]signatureHelpParameter, len(typeParameters)) - for i, typeParameter := range typeParameters { - signatureHelpTypeParameters[i] = createSignatureHelpParameterForTypeParameter(typeParameter, sourceFile, enclosingDeclaration, c, printer) - } - - thisParameter := []signatureHelpParameter{} - if candidateSignature.ThisParameter() != nil { - thisParameter = []signatureHelpParameter{createSignatureHelpParameterForParameter(candidateSignature.ThisParameter(), enclosingDeclaration, printer, sourceFile, c)} - } - - // Creating type parameter display label - var displayParts strings.Builder - displayParts.WriteString(scanner.TokenToString(ast.KindLessThanToken)) - for i, typeParameter := range signatureHelpTypeParameters { - if i > 0 { - displayParts.WriteString(", ") - } - displayParts.WriteString(*typeParameter.parameterInfo.Label.String) - } - displayParts.WriteString(scanner.TokenToString(ast.KindGreaterThanToken)) - - // Creating display label for parameters like, (a: string, b: number) - lists := c.GetExpandedParameters(candidateSignature, false) - if len(lists) != 0 { - displayParts.WriteString(scanner.TokenToString(ast.KindOpenParenToken)) - } - - result := make([]*signatureHelpItemInfo, len(lists)) - for i, parameterList := range lists { - var displayParameters strings.Builder - displayParameters.WriteString(displayParts.String()) - parameters := thisParameter - for j, param := range parameterList { - parameter := createSignatureHelpParameterForParameter(param, enclosingDeclaration, printer, sourceFile, c) - parameters = append(parameters, parameter) - if j > 0 { - displayParameters.WriteString(", ") - } - displayParameters.WriteString(*parameter.parameterInfo.Label.String) - } - displayParameters.WriteString(scanner.TokenToString(ast.KindCloseParenToken)) - - result[i] = &signatureHelpItemInfo{ - isVariadic: false, - parameters: signatureHelpTypeParameters, - displayParts: displayParameters.String(), - } - } - return result -} - -func itemInfoForParameters(candidateSignature *checker.Signature, c *checker.Checker, enclosingDeclaratipn *ast.Node, sourceFile *ast.SourceFile) []*signatureHelpItemInfo { - printer := printer.NewPrinter(printer.PrinterOptions{NewLine: core.NewLineKindLF}, printer.PrintHandlers{}, nil) - - signatureHelpTypeParameters := make([]signatureHelpParameter, len(candidateSignature.TypeParameters())) - if len(candidateSignature.TypeParameters()) != 0 { - for i, typeParameter := range candidateSignature.TypeParameters() { - signatureHelpTypeParameters[i] = createSignatureHelpParameterForTypeParameter(typeParameter, sourceFile, enclosingDeclaratipn, c, printer) - } - } - - // Creating display label for type parameters like, - var displayParts strings.Builder - if len(signatureHelpTypeParameters) != 0 { - displayParts.WriteString(scanner.TokenToString(ast.KindLessThanToken)) - for _, typeParameter := range signatureHelpTypeParameters { - displayParts.WriteString(*typeParameter.parameterInfo.Label.String) - } - displayParts.WriteString(scanner.TokenToString(ast.KindGreaterThanToken)) - } - - // Creating display parts for parameters. For example, (a: string, b: number) - lists := c.GetExpandedParameters(candidateSignature, false) - if len(lists) != 0 { - displayParts.WriteString(scanner.TokenToString(ast.KindOpenParenToken)) - } - - isVariadic := func(parameterList []*ast.Symbol) bool { - if !c.HasEffectiveRestParameter(candidateSignature) { - return false - } - if len(lists) == 1 { - return true - } - return len(parameterList) != 0 && parameterList[len(parameterList)-1] != nil && (parameterList[len(parameterList)-1].CheckFlags&ast.CheckFlagsRestParameter != 0) - } - - result := make([]*signatureHelpItemInfo, len(lists)) - for i, parameterList := range lists { - parameters := make([]signatureHelpParameter, len(parameterList)) - var displayParameters strings.Builder - displayParameters.WriteString(displayParts.String()) - for j, param := range parameterList { - parameter := createSignatureHelpParameterForParameter(param, enclosingDeclaratipn, printer, sourceFile, c) - parameters[j] = parameter - if j > 0 { - displayParameters.WriteString(", ") - } - displayParameters.WriteString(*parameter.parameterInfo.Label.String) - } - displayParameters.WriteString(scanner.TokenToString(ast.KindCloseParenToken)) - - result[i] = &signatureHelpItemInfo{ - isVariadic: isVariadic(parameterList), - parameters: parameters, - displayParts: displayParameters.String(), - } - - } - return result -} - -const signatureHelpNodeBuilderFlags = nodebuilder.FlagsOmitParameterModifiers | nodebuilder.FlagsIgnoreErrors | nodebuilder.FlagsUseAliasDefinedOutsideCurrentScope - -func createSignatureHelpParameterForParameter(parameter *ast.Symbol, enclosingDeclaratipn *ast.Node, p *printer.Printer, sourceFile *ast.SourceFile, c *checker.Checker) signatureHelpParameter { - display := p.Emit(checker.NewNodeBuilder(c, printer.NewEmitContext()).SymbolToParameterDeclaration(parameter, enclosingDeclaratipn, signatureHelpNodeBuilderFlags, nodebuilder.InternalFlagsNone, nil), sourceFile) - isOptional := parameter.CheckFlags&ast.CheckFlagsOptionalParameter != 0 - isRest := parameter.CheckFlags&ast.CheckFlagsRestParameter != 0 - return signatureHelpParameter{ - parameterInfo: &lsproto.ParameterInformation{ - Label: lsproto.StringOrTuple{String: &display}, - Documentation: nil, - }, - isRest: isRest, - isOptional: isOptional, - } -} - -func createSignatureHelpParameterForTypeParameter(t *checker.Type, sourceFile *ast.SourceFile, enclosingDeclaration *ast.Node, c *checker.Checker, p *printer.Printer) signatureHelpParameter { - display := p.Emit(checker.NewNodeBuilder(c, printer.NewEmitContext()).TypeParameterToDeclaration(t, enclosingDeclaration, signatureHelpNodeBuilderFlags, nodebuilder.InternalFlagsNone, nil), sourceFile) - return signatureHelpParameter{ - parameterInfo: &lsproto.ParameterInformation{ - Label: lsproto.StringOrTuple{String: &display}, - }, - isRest: false, - isOptional: false, - } -} - -// Represents the signature of something callable. A signature -// can have a label, like a function-name, a doc-comment, and -// a set of parameters. -type signatureInformation struct { - // The Label of this signature. Will be shown in - // the UI. - Label string - // The human-readable doc-comment of this signature. Will be shown - // in the UI but can be omitted. - Documentation *string - // The Parameters of this signature. - Parameters []signatureHelpParameter - // Needed only here, not in lsp - IsVariadic bool -} - -type signatureHelpItemInfo struct { - isVariadic bool - parameters []signatureHelpParameter - displayParts string -} - -type signatureHelpParameter struct { - parameterInfo *lsproto.ParameterInformation - isRest bool - isOptional bool -} - -func getEnclosingDeclarationFromInvocation(invocation *invocation) *ast.Node { - if invocation.callInvocation != nil { - return invocation.callInvocation.node - } else if invocation.typeArgsInvocation != nil { - return invocation.typeArgsInvocation.called.AsNode() - } else { - return invocation.contextualInvocation.node - } -} - -func getExpressionFromInvocation(argumentInfo *argumentListInfo) *ast.Node { - if argumentInfo.invocation.callInvocation != nil { - return ast.GetInvokedExpression(argumentInfo.invocation.callInvocation.node) - } - return argumentInfo.invocation.typeArgsInvocation.called.AsNode() -} - -type candidateInfo struct { - candidates []*checker.Signature - resolvedSignature *checker.Signature -} - -type CandidateOrTypeInfo struct { - candidateInfo *candidateInfo - typeInfo *ast.Symbol -} - -func getCandidateOrTypeInfo(info *argumentListInfo, c *checker.Checker, sourceFile *ast.SourceFile, startingToken *ast.Node, onlyUseSyntacticOwners bool) *CandidateOrTypeInfo { - if info.invocation.callInvocation != nil { - if onlyUseSyntacticOwners && !isSyntacticOwner(startingToken, info.invocation.callInvocation.node, sourceFile) { - return nil - } - resolvedSignature, candidates := checker.GetResolvedSignatureForSignatureHelp(info.invocation.callInvocation.node, info.argumentCount, c) - return &CandidateOrTypeInfo{ - candidateInfo: &candidateInfo{ - candidates: candidates, - resolvedSignature: resolvedSignature, - }, - } - } - if info.invocation.typeArgsInvocation != nil { - called := info.invocation.typeArgsInvocation.called.AsNode() - container := called - if ast.IsIdentifier(called) { - container = called.Parent - } - if onlyUseSyntacticOwners && !containsPrecedingToken(startingToken, sourceFile, container) { - return nil - } - candidates := getPossibleGenericSignatures(called, info.argumentCount, c) - if len(candidates) != 0 { - return &CandidateOrTypeInfo{ - candidateInfo: &candidateInfo{ - candidates: candidates, - resolvedSignature: candidates[0], - }, - } - } - symbol := c.GetSymbolAtLocation(called) - return &CandidateOrTypeInfo{ - typeInfo: symbol, - } - } - if info.invocation.contextualInvocation != nil { - return &CandidateOrTypeInfo{ - candidateInfo: &candidateInfo{ - candidates: []*checker.Signature{info.invocation.contextualInvocation.signature}, - resolvedSignature: info.invocation.contextualInvocation.signature, - }, - } - } - debug.AssertNever(info.invocation) - return nil -} - -func isSyntacticOwner(startingToken *ast.Node, node *ast.CallLikeExpression, sourceFile *ast.SourceFile) bool { // !!! not tested - if !ast.IsCallOrNewExpression(node) { - return false - } - invocationChildren := getChildrenFromNonJSDocNode(node, sourceFile) - switch startingToken.Kind { - case ast.KindOpenParenToken, ast.KindCommaToken: - return containsNode(invocationChildren, startingToken) - case ast.KindLessThanToken: - return containsPrecedingToken(startingToken, sourceFile, node.AsCallExpression().Expression) - default: - return false - } -} - -func containsPrecedingToken(startingToken *ast.Node, sourceFile *ast.SourceFile, container *ast.Node) bool { - pos := startingToken.Pos() - // There's a possibility that `startingToken.parent` contains only `startingToken` and - // missing nodes, none of which are valid to be returned by `findPrecedingToken`. In that - // case, the preceding token we want is actually higher up the tree—almost definitely the - // next parent, but theoretically the situation with missing nodes might be happening on - // multiple nested levels. - currentParent := startingToken.Parent - for currentParent != nil { - precedingToken := astnav.FindPrecedingToken(sourceFile, pos) - if precedingToken != nil { - return RangeContainsRange(container.Loc, precedingToken.Loc) - } - currentParent = currentParent.Parent - } - // return Debug.fail("Could not find preceding token"); - return false -} - -func getContainingArgumentInfo(node *ast.Node, sourceFile *ast.SourceFile, checker *checker.Checker, isManuallyInvoked bool, position int) *argumentListInfo { - for n := node; !ast.IsSourceFile(n) && (isManuallyInvoked || !ast.IsBlock(n)); n = n.Parent { - // If the node is not a subspan of its parent, this is a big problem. - // There have been crashes that might be caused by this violation. - debug.Assert(RangeContainsRange(n.Parent.Loc, n.Loc), fmt.Sprintf("Not a subspan. Child: %s, parent: %s", n.KindString(), n.Parent.KindString())) - argumentInfo := getImmediatelyContainingArgumentOrContextualParameterInfo(n, position, sourceFile, checker) - if argumentInfo != nil { - return argumentInfo - } - } - return nil -} - -func getImmediatelyContainingArgumentOrContextualParameterInfo(node *ast.Node, position int, sourceFile *ast.SourceFile, checker *checker.Checker) *argumentListInfo { - result := tryGetParameterInfo(node, sourceFile, checker) - if result == nil { - return getImmediatelyContainingArgumentInfo(node, position, sourceFile, checker) - } - return result -} - -type argumentListInfo struct { - isTypeParameterList bool - invocation *invocation - argumentsSpan core.TextRange - argumentIndex int - /** argumentCount is the *apparent* number of arguments. */ - argumentCount int -} - -// Returns relevant information for the argument list and the current argument if we are -// in the argument of an invocation; returns undefined otherwise. -func getImmediatelyContainingArgumentInfo(node *ast.Node, position int, sourceFile *ast.SourceFile, c *checker.Checker) *argumentListInfo { - parent := node.Parent - if ast.IsCallOrNewExpression(parent) { - // There are 3 cases to handle: - // 1. The token introduces a list, and should begin a signature help session - // 2. The token is either not associated with a list, or ends a list, so the session should end - // 3. The token is buried inside a list, and should give signature help - // - // The following are examples of each: - // - // Case 1: - // foo<#T, U>(#a, b) -> The token introduces a list, and should begin a signature help session - // Case 2: - // fo#o#(a, b)# -> The token is either not associated with a list, or ends a list, so the session should end - // Case 3: - // foo(a#, #b#) -> The token is buried inside a list, and should give signature help - // Find out if 'node' is an argument, a type argument, or neither - info := getArgumentOrParameterListInfo(node, sourceFile, c) - if info == nil { - return nil - } - list := info.list - argumentIndex := info.argumentIndex - argumentCount := info.argumentCount - argumentsSpan := info.argumentsSpan - isTypeParameterList := false - parentTypeArgumentList := parent.TypeArgumentList() - if parentTypeArgumentList != nil { - if parentTypeArgumentList.Pos() == list.Pos() { - isTypeParameterList = true - } - } - return &argumentListInfo{ - isTypeParameterList: isTypeParameterList, - invocation: &invocation{callInvocation: &callInvocation{node: parent}}, - argumentsSpan: argumentsSpan, - argumentIndex: argumentIndex, - argumentCount: argumentCount, - } - } else if isNoSubstitutionTemplateLiteral(node) && isTaggedTemplateExpression(parent) { - // Check if we're actually inside the template; - // otherwise we'll fall out and return undefined. - if isInsideTemplateLiteral(node, position, sourceFile) { - return getArgumentListInfoForTemplate(parent.AsTaggedTemplateExpression(), 0, sourceFile) - } - return nil - } else if isTemplateHead(node) && parent.Parent.Kind == ast.KindTaggedTemplateExpression { - templateExpression := parent.AsTemplateExpression() - tagExpression := templateExpression.Parent.AsTaggedTemplateExpression() - - argumentIndex := 1 - if isInsideTemplateLiteral(node, position, sourceFile) { - argumentIndex = 0 - } - return getArgumentListInfoForTemplate(tagExpression, argumentIndex, sourceFile) - } else if ast.IsTemplateSpan(parent) && isTaggedTemplateExpression(parent.Parent.Parent) { - templateSpan := parent - tagExpression := parent.Parent.Parent - - // If we're just after a template tail, don't show signature help. - if isTemplateTail(node) && !isInsideTemplateLiteral(node, position, sourceFile) { - return nil - } - - spanIndex := ast.IndexOfNode(templateSpan.Parent.AsTemplateExpression().TemplateSpans.Nodes, templateSpan) - argumentIndex := getArgumentIndexForTemplatePiece(spanIndex, templateSpan, position, sourceFile) - - return getArgumentListInfoForTemplate(tagExpression.AsTaggedTemplateExpression(), argumentIndex, sourceFile) - } else if ast.IsJsxOpeningLikeElement(parent) { - // Provide a signature help for JSX opening element or JSX self-closing element. - // This is not guarantee that JSX tag-name is resolved into stateless function component. (that is done in "getSignatureHelpItems") - // i.e - // export function MainButton(props: ButtonProps, context: any): JSX.Element { ... } - // = node.Loc.Pos(), "Assumed 'position' could not occur before node.") - if ast.IsTemplateLiteralToken(node) { - if isInsideTemplateLiteral(node, position, sourceFile) { - return 0 - } - return spanIndex + 2 - } - return spanIndex + 1 -} - -func getAdjustedNode(node *ast.Node) *ast.Node { - switch node.Kind { - case ast.KindOpenParenToken, ast.KindCommaToken: - return node - default: - return ast.FindAncestor(node.Parent, func(n *ast.Node) bool { - if ast.IsParameter(n) { - return true - } else if ast.IsBindingElement(n) || ast.IsObjectBindingPattern(n) || ast.IsArrayBindingPattern(n) { - return false - } - return false - }) - } -} - -type contextualSignatureLocationInfo struct { - contextualType *checker.Type - argumentIndex int - argumentCount int - argumentsSpan core.TextRange -} - -func getSpreadElementCount(node *ast.SpreadElement, c *checker.Checker) int { - spreadType := c.GetTypeAtLocation(node.Expression) - if checker.IsTupleType(spreadType) { - tupleType := spreadType.Target().AsTupleType() - if tupleType == nil { - return 0 - } - elementFlags := tupleType.ElementFlags() - fixedLength := tupleType.FixedLength() - if fixedLength == 0 { - return 0 - } - - firstOptionalIndex := core.FindIndex(elementFlags, func(f checker.ElementFlags) bool { - return (f&checker.ElementFlagsRequired == 0) - }) - if firstOptionalIndex < 0 { - return fixedLength - } - return firstOptionalIndex - } - return 0 -} - -func getArgumentIndex(node *ast.Node, arguments *ast.NodeList, sourceFile *ast.SourceFile, c *checker.Checker) int { - return getArgumentIndexOrCount(getTokenFromNodeList(arguments, node.Parent, sourceFile), node, c) -} - -func getArgumentCount(node *ast.Node, arguments *ast.NodeList, sourceFile *ast.SourceFile, c *checker.Checker) int { - return getArgumentIndexOrCount(getTokenFromNodeList(arguments, node.Parent, sourceFile), nil, c) -} - -func getArgumentIndexOrCount(arguments []*ast.Node, node *ast.Node, c *checker.Checker) int { - argumentIndex := 0 - skipComma := false - for _, arg := range arguments { - if node != nil && arg == node { - if !skipComma && arg.Kind == ast.KindCommaToken { - argumentIndex++ - } - return argumentIndex - } - if ast.IsSpreadElement(arg) { - argumentIndex += getSpreadElementCount(arg.AsSpreadElement(), c) - skipComma = true - continue - } - if arg.Kind != ast.KindCommaToken { - argumentIndex++ - skipComma = true - continue - } - if skipComma { - skipComma = false - continue - } - argumentIndex++ - } - if node != nil { - return argumentIndex - } - // The argument count for a list is normally the number of non-comma children it has. - // For example, if you have "Foo(a,b)" then there will be three children of the arg - // list 'a' '' 'b'. So, in this case the arg count will be 2. However, there - // is a small subtlety. If you have "Foo(a,)", then the child list will just have - // 'a' ''. So, in the case where the last child is a comma, we increase the - // arg count by one to compensate. - argumentCount := argumentIndex - if len(arguments) > 0 && arguments[len(arguments)-1].Kind == ast.KindCommaToken { - argumentCount = argumentIndex + 1 - } - return argumentCount -} - -type argumentOrParameterListInfo struct { - list *ast.NodeList - argumentIndex int - argumentCount int - argumentsSpan core.TextRange -} - -func getArgumentOrParameterListInfo(node *ast.Node, sourceFile *ast.SourceFile, c *checker.Checker) *argumentOrParameterListInfo { - info := getArgumentOrParameterListAndIndex(node, sourceFile, c) - if info == nil { - return nil - } - list := info.list - argumentIndex := info.argumentIndex - argumentCount := getArgumentCount(node, list, sourceFile, c) - argumentsSpan := getApplicableSpanForArguments(list, node, sourceFile) - return &argumentOrParameterListInfo{ - list: list, - argumentIndex: argumentIndex, - argumentCount: argumentCount, - argumentsSpan: argumentsSpan, - } -} - -func getApplicableSpanForArguments(argumentList *ast.NodeList, node *ast.Node, sourceFile *ast.SourceFile) core.TextRange { - // We use full start and skip trivia on the end because we want to include trivia on - // both sides. For example, - // - // foo( /*comment */ a, b, c /*comment*/ ) - // | | - // - // The applicable span is from the first bar to the second bar (inclusive, - // but not including parentheses) - if argumentList == nil && node != nil { - // If the user has just opened a list, and there are no arguments. - // For example, foo( ) - // | | - return core.NewTextRange(node.End(), scanner.SkipTrivia(sourceFile.Text(), node.End())) - } - applicableSpanStart := argumentList.Pos() - applicableSpanEnd := scanner.SkipTrivia(sourceFile.Text(), argumentList.End()) - return core.NewTextRange(applicableSpanStart, applicableSpanEnd) -} - -type argumentOrParameterListAndIndex struct { - list *ast.NodeList - argumentIndex int -} - -func getArgumentOrParameterListAndIndex(node *ast.Node, sourceFile *ast.SourceFile, c *checker.Checker) *argumentOrParameterListAndIndex { - if node.Kind == ast.KindLessThanToken || node.Kind == ast.KindOpenParenToken { - // Find the list that starts right *after* the < or ( token. - // If the user has just opened a list, consider this item 0. - list := getChildListThatStartsWithOpenerToken(node.Parent, node) - return &argumentOrParameterListAndIndex{ - list: list, - argumentIndex: 0, - } - } else { - // findListItemInfo can return undefined if we are not in parent's argument list - // or type argument list. This includes cases where the cursor is: - // - To the right of the closing parenthesis, non-substitution template, or template tail. - // - Between the type arguments and the arguments (greater than token) - // - On the target of the call (parent.func) - // - On the 'new' keyword in a 'new' expression - list := findContainingList(node, sourceFile) - if list == nil { - return nil - } - return &argumentOrParameterListAndIndex{ - list: list, - // Find the index of the argument that contains the node. - argumentIndex: getArgumentIndex(node, list, sourceFile, c), - } - } -} - -func getChildListThatStartsWithOpenerToken(parent *ast.Node, openerToken *ast.Node) *ast.NodeList { //!!! - if ast.IsCallExpression(parent) { - parentCallExpression := parent.AsCallExpression() - if openerToken.Kind == ast.KindLessThanToken { - return parentCallExpression.TypeArgumentList() - } - return parentCallExpression.Arguments - } else if ast.IsNewExpression(parent) { - parentNewExpression := parent.AsNewExpression() - if openerToken.Kind == ast.KindLessThanToken { - return parentNewExpression.TypeArgumentList() - } - return parentNewExpression.Arguments - } - return nil -} - -func tryGetParameterInfo(startingToken *ast.Node, sourceFile *ast.SourceFile, c *checker.Checker) *argumentListInfo { - node := getAdjustedNode(startingToken) - if node == nil { - return nil - } - info := getContextualSignatureLocationInfo(node, sourceFile, c) - if info == nil { - return nil - } - - // for optional function condition - nonNullableContextualType := c.GetNonNullableType(info.contextualType) - if nonNullableContextualType == nil { - return nil - } - - symbol := nonNullableContextualType.Symbol() - if symbol == nil { - return nil - } - - signatures := c.GetSignaturesOfType(nonNullableContextualType, checker.SignatureKindCall) - if signatures == nil || signatures[len(signatures)-1] == nil { - return nil - } - signature := signatures[len(signatures)-1] - - contextualInvocation := &contextualInvocation{ - signature: signature, - node: startingToken, - symbol: chooseBetterSymbol(symbol), - } - return &argumentListInfo{ - isTypeParameterList: false, - invocation: &invocation{contextualInvocation: contextualInvocation}, - argumentsSpan: info.argumentsSpan, - argumentIndex: info.argumentIndex, - argumentCount: info.argumentCount, - } -} - -func chooseBetterSymbol(s *ast.Symbol) *ast.Symbol { - if s.Name == ast.InternalSymbolNameType { - for _, d := range s.Declarations { - if ast.IsFunctionTypeNode(d) && ast.CanHaveSymbol(d.Parent) { - return d.Parent.Symbol() - } - } - } - return s -} - -func getContextualSignatureLocationInfo(node *ast.Node, sourceFile *ast.SourceFile, c *checker.Checker) *contextualSignatureLocationInfo { - parent := node.Parent - switch parent.Kind { - case ast.KindParenthesizedExpression, ast.KindMethodDeclaration, ast.KindFunctionExpression, ast.KindArrowFunction: - info := getArgumentOrParameterListInfo(node, sourceFile, c) - if info == nil { - return nil - } - argumentIndex := info.argumentIndex - argumentCount := info.argumentCount - argumentsSpan := info.argumentsSpan - - var contextualType *checker.Type - if ast.IsMethodDeclaration(parent) { - contextualType = c.GetContextualTypeForObjectLiteralElement(parent, checker.ContextFlagsNone) - } else { - contextualType = c.GetContextualType(parent, checker.ContextFlagsNone) - } - if contextualType != nil { - return &contextualSignatureLocationInfo{ - contextualType: contextualType, - argumentIndex: argumentIndex, - argumentCount: argumentCount, - argumentsSpan: argumentsSpan, - } - } - return nil - case ast.KindBinaryExpression: - highestBinary := getHighestBinary(parent.AsBinaryExpression()) - contextualType := c.GetContextualType(highestBinary.AsNode(), checker.ContextFlagsNone) - argumentIndex := 0 - if node.Kind != ast.KindOpenParenToken { - argumentIndex = countBinaryExpressionParameters(parent.AsBinaryExpression()) - 1 - argumentCount := countBinaryExpressionParameters(highestBinary) - if contextualType != nil { - return &contextualSignatureLocationInfo{ - contextualType: contextualType, - argumentIndex: argumentIndex, - argumentCount: argumentCount, - argumentsSpan: core.NewTextRange(parent.Pos(), parent.End()), - } - } - return nil - } - } - return nil -} - -func getHighestBinary(b *ast.BinaryExpression) *ast.BinaryExpression { - if ast.IsBinaryExpression(b.Parent) { - return getHighestBinary(b.Parent.AsBinaryExpression()) - } - return b -} - -func countBinaryExpressionParameters(b *ast.BinaryExpression) int { - if ast.IsBinaryExpression(b.Left) { - return countBinaryExpressionParameters(b.Left.AsBinaryExpression()) + 1 - } - return 2 -} - -func getTokenFromNodeList(nodeList *ast.NodeList, nodeListParent *ast.Node, sourceFile *ast.SourceFile) []*ast.Node { - if nodeList == nil || nodeListParent == nil { - return nil - } - left := nodeList.Pos() - nodeListIndex := 0 - var tokens []*ast.Node - for left < nodeList.End() { - if len(nodeList.Nodes) > nodeListIndex && left == nodeList.Nodes[nodeListIndex].Pos() { - tokens = append(tokens, nodeList.Nodes[nodeListIndex]) - left = nodeList.Nodes[nodeListIndex].End() - nodeListIndex++ - } else { - scanner := scanner.GetScannerForSourceFile(sourceFile, left) - token := scanner.Token() - tokenFullStart := scanner.TokenFullStart() - tokenEnd := scanner.TokenEnd() - tokens = append(tokens, sourceFile.GetOrCreateToken(token, tokenFullStart, tokenEnd, nodeListParent)) - left = tokenEnd - } - } - return tokens -} - -func containsNode(nodes []*ast.Node, node *ast.Node) bool { - for i := range nodes { - if nodes[i] == node { - return true - } - } - return false -} - -func getArgumentListInfoForTemplate(tagExpression *ast.TaggedTemplateExpression, argumentIndex int, sourceFile *ast.SourceFile) *argumentListInfo { - // argumentCount is either 1 or (numSpans + 1) to account for the template strings array argument. - argumentCount := 1 - if !isNoSubstitutionTemplateLiteral(tagExpression.Template) { - argumentCount = len(tagExpression.Template.AsTemplateExpression().TemplateSpans.Nodes) + 1 - } - if argumentIndex != 0 { - debug.AssertLessThan(argumentIndex, argumentCount) - } - return &argumentListInfo{ - isTypeParameterList: false, - invocation: &invocation{callInvocation: &callInvocation{node: tagExpression.AsNode()}}, - argumentIndex: argumentIndex, - argumentCount: argumentCount, - argumentsSpan: getApplicableRangeForTaggedTemplate(tagExpression, sourceFile), - } -} - -func getApplicableRangeForTaggedTemplate(taggedTemplate *ast.TaggedTemplateExpression, sourceFile *ast.SourceFile) core.TextRange { - template := taggedTemplate.Template - applicableSpanStart := scanner.GetTokenPosOfNode(template, sourceFile, false) - applicableSpanEnd := template.End() - - // We need to adjust the end position for the case where the template does not have a tail. - // Otherwise, we will not show signature help past the expression. - // For example, - // - // ` ${ 1 + 1 foo(10) - // | | - // This is because a Missing node has no width. However, what we actually want is to include trivia - // leading up to the next token in case the user is about to type in a TemplateMiddle or TemplateTail. - if template.Kind == ast.KindTemplateExpression { - templateSpans := template.AsTemplateExpression().TemplateSpans - lastSpan := templateSpans.Nodes[len(templateSpans.Nodes)-1] - if lastSpan.AsTemplateSpan().Literal.End()-lastSpan.AsTemplateSpan().Literal.Pos() == 0 { - applicableSpanEnd = scanner.SkipTrivia(sourceFile.Text(), applicableSpanEnd) - } - } - - return core.NewTextRange(applicableSpanStart, applicableSpanEnd-applicableSpanStart) -} diff --git a/kitcom/internal/tsgo/ls/source_map.go b/kitcom/internal/tsgo/ls/source_map.go deleted file mode 100644 index 0fd10dc..0000000 --- a/kitcom/internal/tsgo/ls/source_map.go +++ /dev/null @@ -1,81 +0,0 @@ -package ls - -import ( - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/debug" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/sourcemap" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath" -) - -func (l *LanguageService) getMappedLocation(fileName string, fileRange core.TextRange) lsproto.Location { - startPos := l.tryGetSourcePosition(fileName, core.TextPos(fileRange.Pos())) - if startPos == nil { - lspRange := l.createLspRangeFromRange(fileRange, l.getScript(fileName)) - return lsproto.Location{ - Uri: FileNameToDocumentURI(fileName), - Range: *lspRange, - } - } - endPos := l.tryGetSourcePosition(fileName, core.TextPos(fileRange.End())) - debug.Assert(endPos.FileName == startPos.FileName, "start and end should be in same file") - newRange := core.NewTextRange(startPos.Pos, endPos.Pos) - lspRange := l.createLspRangeFromRange(newRange, l.getScript(startPos.FileName)) - return lsproto.Location{ - Uri: FileNameToDocumentURI(startPos.FileName), - Range: *lspRange, - } -} - -type script struct { - fileName string - text string -} - -func (s *script) FileName() string { - return s.fileName -} - -func (s *script) Text() string { - return s.text -} - -func (l *LanguageService) getScript(fileName string) *script { - text, ok := l.host.ReadFile(fileName) - if !ok { - return nil - } - return &script{fileName: fileName, text: text} -} - -func (l *LanguageService) tryGetSourcePosition( - fileName string, - position core.TextPos, -) *sourcemap.DocumentPosition { - newPos := l.tryGetSourcePositionWorker(fileName, position) - if newPos != nil { - if _, ok := l.ReadFile(newPos.FileName); !ok { // File doesn't exist - return nil - } - } - return newPos -} - -func (l *LanguageService) tryGetSourcePositionWorker( - fileName string, - position core.TextPos, -) *sourcemap.DocumentPosition { - if !tspath.IsDeclarationFileName(fileName) { - return nil - } - - positionMapper := l.GetDocumentPositionMapper(fileName) - documentPos := positionMapper.GetSourcePosition(&sourcemap.DocumentPosition{FileName: fileName, Pos: int(position)}) - if documentPos == nil { - return nil - } - if newPos := l.tryGetSourcePositionWorker(documentPos.FileName, core.TextPos(documentPos.Pos)); newPos != nil { - return newPos - } - return documentPos -} diff --git a/kitcom/internal/tsgo/ls/string_completions.go b/kitcom/internal/tsgo/ls/string_completions.go deleted file mode 100644 index e1afd8b..0000000 --- a/kitcom/internal/tsgo/ls/string_completions.go +++ /dev/null @@ -1,731 +0,0 @@ -package ls - -import ( - "context" - "fmt" - "slices" - "strings" - - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/checker" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/compiler" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/printer" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath" -) - -type completionsFromTypes struct { - types []*checker.StringLiteralType - isNewIdentifier bool -} - -type completionsFromProperties struct { - symbols []*ast.Symbol - hasIndexSignature bool -} - -type pathCompletion struct { - name string - // ScriptElementKindScriptElement | ScriptElementKindDirectory | ScriptElementKindExternalModuleName - kind ScriptElementKind - extension string - textRange *core.TextRange -} - -type stringLiteralCompletions struct { - fromTypes *completionsFromTypes - fromProperties *completionsFromProperties - fromPaths []*pathCompletion -} - -func (l *LanguageService) getStringLiteralCompletions( - ctx context.Context, - file *ast.SourceFile, - position int, - contextToken *ast.Node, - compilerOptions *core.CompilerOptions, - preferences *UserPreferences, - clientOptions *lsproto.CompletionClientCapabilities, -) *lsproto.CompletionList { - // !!! reference comment - if IsInString(file, position, contextToken) { - if contextToken == nil || !ast.IsStringLiteralLike(contextToken) { - return nil - } - entries := l.getStringLiteralCompletionEntries( - ctx, - file, - contextToken, - position, - preferences) - return l.convertStringLiteralCompletions( - ctx, - entries, - contextToken, - file, - position, - compilerOptions, - preferences, - clientOptions, - ) - } - return nil -} - -func (l *LanguageService) convertStringLiteralCompletions( - ctx context.Context, - completion *stringLiteralCompletions, - contextToken *ast.StringLiteralLike, - file *ast.SourceFile, - position int, - options *core.CompilerOptions, - preferences *UserPreferences, - clientOptions *lsproto.CompletionClientCapabilities, -) *lsproto.CompletionList { - if completion == nil { - return nil - } - - optionalReplacementRange := l.createRangeFromStringLiteralLikeContent(file, contextToken, position) - switch { - case completion.fromPaths != nil: - completion := completion.fromPaths - return l.convertPathCompletions(completion, file, position, clientOptions) - case completion.fromProperties != nil: - completion := completion.fromProperties - data := &completionDataData{ - symbols: completion.symbols, - completionKind: CompletionKindString, - isNewIdentifierLocation: completion.hasIndexSignature, - location: file.AsNode(), - contextToken: contextToken, - } - _, items := l.getCompletionEntriesFromSymbols( - ctx, - data, - contextToken, /*replacementToken*/ - position, - file, - preferences, - options, - clientOptions, - ) - defaultCommitCharacters := getDefaultCommitCharacters(completion.hasIndexSignature) - itemDefaults := l.setItemDefaults( - clientOptions, - position, - file, - items, - &defaultCommitCharacters, - optionalReplacementRange, - ) - return &lsproto.CompletionList{ - IsIncomplete: false, - ItemDefaults: itemDefaults, - Items: items, - } - case completion.fromTypes != nil: - completion := completion.fromTypes - var quoteChar printer.QuoteChar - if contextToken.Kind == ast.KindNoSubstitutionTemplateLiteral { - quoteChar = printer.QuoteCharBacktick - } else if strings.HasPrefix(contextToken.Text(), "'") { - quoteChar = printer.QuoteCharSingleQuote - } else { - quoteChar = printer.QuoteCharDoubleQuote - } - items := core.Map(completion.types, func(t *checker.StringLiteralType) *lsproto.CompletionItem { - name := printer.EscapeString(t.AsLiteralType().Value().(string), quoteChar) - return l.createLSPCompletionItem( - name, - "", /*insertText*/ - "", /*filterText*/ - SortTextLocationPriority, - ScriptElementKindString, - collections.Set[ScriptElementKindModifier]{}, - l.getReplacementRangeForContextToken(file, contextToken, position), - nil, /*commitCharacters*/ - nil, /*labelDetails*/ - file, - position, - clientOptions, - false, /*isMemberCompletion*/ - false, /*isSnippet*/ - false, /*hasAction*/ - false, /*preselect*/ - "", /*source*/ - nil, /*autoImportEntryData*/ - ) - }) - defaultCommitCharacters := getDefaultCommitCharacters(completion.isNewIdentifier) - itemDefaults := l.setItemDefaults( - clientOptions, - position, - file, - items, - &defaultCommitCharacters, - nil, /*optionalReplacementSpan*/ - ) - return &lsproto.CompletionList{ - IsIncomplete: false, - ItemDefaults: itemDefaults, - Items: items, - } - default: - return nil - } -} - -func (l *LanguageService) convertPathCompletions( - pathCompletions []*pathCompletion, - file *ast.SourceFile, - position int, - clientOptions *lsproto.CompletionClientCapabilities, -) *lsproto.CompletionList { - isNewIdentifierLocation := true // The user may type in a path that doesn't yet exist, creating a "new identifier" with respect to the collection of identifiers the server is aware of. - defaultCommitCharacters := getDefaultCommitCharacters(isNewIdentifierLocation) - items := core.Map(pathCompletions, func(pathCompletion *pathCompletion) *lsproto.CompletionItem { - replacementSpan := l.createLspRangeFromBounds(pathCompletion.textRange.Pos(), pathCompletion.textRange.End(), file) - return l.createLSPCompletionItem( - pathCompletion.name, - "", /*insertText*/ - "", /*filterText*/ - SortTextLocationPriority, - pathCompletion.kind, - *collections.NewSetFromItems(kindModifiersFromExtension(pathCompletion.extension)), - replacementSpan, - nil, /*commitCharacters*/ - nil, /*labelDetails*/ - file, - position, - clientOptions, - false, /*isMemberCompletion*/ - false, /*isSnippet*/ - false, /*hasAction*/ - false, /*preselect*/ - "", /*source*/ - nil, /*autoImportEntryData*/ - ) - }) - itemDefaults := l.setItemDefaults( - clientOptions, - position, - file, - items, - &defaultCommitCharacters, - nil, /*optionalReplacementSpan*/ - ) - return &lsproto.CompletionList{ - IsIncomplete: false, - ItemDefaults: itemDefaults, - Items: items, - } -} - -func (l *LanguageService) getStringLiteralCompletionEntries( - ctx context.Context, - file *ast.SourceFile, - node *ast.StringLiteralLike, - position int, - preferences *UserPreferences, -) *stringLiteralCompletions { - typeChecker, done := l.GetProgram().GetTypeCheckerForFile(ctx, file) - defer done() - parent := walkUpParentheses(node.Parent) - switch parent.Kind { - case ast.KindLiteralType: - grandparent := walkUpParentheses(parent.Parent) - if grandparent.Kind == ast.KindImportType { - return getStringLiteralCompletionsFromModuleNames( - file, - node, - l.GetProgram(), - preferences, - ) - } - return fromUnionableLiteralType(grandparent, parent, position, typeChecker) - case ast.KindPropertyAssignment: - if ast.IsObjectLiteralExpression(parent.Parent) && parent.Name() == node { - // Get quoted name of properties of the object literal expression - // i.e. interface ConfigFiles { - // 'jspm:dev': string - // } - // let files: ConfigFiles = { - // '/*completion position*/' - // } - // - // function foo(c: ConfigFiles) {} - // foo({ - // '/*completion position*/' - // }); - return &stringLiteralCompletions{ - fromProperties: stringLiteralCompletionsForObjectLiteral(typeChecker, parent.Parent), - } - } - result := fromContextualType(checker.ContextFlagsCompletions, node, typeChecker) - if result != nil { - return &stringLiteralCompletions{ - fromTypes: result, - } - } - return &stringLiteralCompletions{ - fromTypes: fromContextualType(checker.ContextFlagsNone, node, typeChecker), - } - case ast.KindElementAccessExpression: - expression := parent.Expression() - argumentExpression := parent.AsElementAccessExpression().ArgumentExpression - if node == ast.SkipParentheses(argumentExpression) { - // Get all names of properties on the expression - // i.e. interface A { - // 'prop1': string - // } - // let a: A; - // a['/*completion position*/'] - t := typeChecker.GetTypeAtLocation(expression) - return &stringLiteralCompletions{ - fromProperties: stringLiteralCompletionsFromProperties(t, typeChecker), - } - } - return nil - case ast.KindCallExpression, ast.KindNewExpression, ast.KindJsxAttribute: - if !isRequireCallArgument(node) && !ast.IsImportCall(parent) { - var argumentNode *ast.Node - if parent.Kind == ast.KindJsxAttribute { - argumentNode = parent.Parent - } else { - argumentNode = node - } - argumentInfo := getArgumentInfoForCompletions(argumentNode, position, file, typeChecker) - // Get string literal completions from specialized signatures of the target - // i.e. declare function f(a: 'A'); - // f("/*completion position*/") - if argumentInfo == nil { - return nil - } - - result := getStringLiteralCompletionsFromSignature(argumentInfo.invocation, node, argumentInfo, typeChecker) - if result != nil { - return &stringLiteralCompletions{ - fromTypes: result, - } - } - return &stringLiteralCompletions{ - fromTypes: fromContextualType(checker.ContextFlagsNone, node, typeChecker), - } - } - fallthrough // is `require("")` or `require(""` or `import("")` - case ast.KindImportDeclaration, ast.KindExportDeclaration, ast.KindExternalModuleReference, ast.KindJSDocImportTag: - // Get all known external module names or complete a path to a module - // i.e. import * as ns from "/*completion position*/"; - // var y = import("/*completion position*/"); - // import x = require("/*completion position*/"); - // var y = require("/*completion position*/"); - // export * from "/*completion position*/"; - return getStringLiteralCompletionsFromModuleNames(file, node, l.GetProgram(), preferences) - case ast.KindCaseClause: - tracker := newCaseClauseTracker(typeChecker, parent.Parent.AsCaseBlock().Clauses.Nodes) - contextualTypes := fromContextualType(checker.ContextFlagsCompletions, node, typeChecker) - if contextualTypes == nil { - return nil - } - literals := core.Filter(contextualTypes.types, func(t *checker.StringLiteralType) bool { - return !tracker.hasValue(t.AsLiteralType().Value()) - }) - return &stringLiteralCompletions{ - fromTypes: &completionsFromTypes{ - types: literals, - isNewIdentifier: false, - }, - } - case ast.KindImportSpecifier, ast.KindExportSpecifier: - // Complete string aliases in `import { "|" } from` and `export { "|" } from` - specifier := parent - if propertyName := specifier.PropertyName(); propertyName != nil && node != propertyName { - return nil // Don't complete in `export { "..." as "|" } from` - } - namedImportsOrExports := specifier.Parent - var moduleSpecifier *ast.Node - if namedImportsOrExports.Kind == ast.KindNamedImports { - moduleSpecifier = namedImportsOrExports.Parent.Parent - } else { - moduleSpecifier = namedImportsOrExports.Parent - } - if moduleSpecifier == nil { - return nil - } - moduleSpecifierSymbol := typeChecker.GetSymbolAtLocation(moduleSpecifier) - if moduleSpecifierSymbol == nil { - return nil - } - exports := typeChecker.GetExportsAndPropertiesOfModule(moduleSpecifierSymbol) - existing := collections.NewSetFromItems(core.Map(namedImportsOrExports.Elements(), func(n *ast.Node) string { - if n.PropertyName() != nil { - return n.PropertyName().Text() - } - return n.Name().Text() - })...) - uniques := core.Filter(exports, func(e *ast.Symbol) bool { - return e.Name != ast.InternalSymbolNameDefault && !existing.Has(e.Name) - }) - return &stringLiteralCompletions{ - fromProperties: &completionsFromProperties{ - symbols: uniques, - hasIndexSignature: false, - }, - } - case ast.KindBinaryExpression: - if parent.AsBinaryExpression().OperatorToken.Kind == ast.KindInKeyword { - t := typeChecker.GetTypeAtLocation(parent.AsBinaryExpression().Right) - properties := getPropertiesForCompletion(t, typeChecker) - return &stringLiteralCompletions{ - fromProperties: &completionsFromProperties{ - symbols: core.Filter(properties, func(s *ast.Symbol) bool { - return s.ValueDeclaration == nil || !ast.IsPrivateIdentifierClassElementDeclaration(s.ValueDeclaration) - }), - hasIndexSignature: false, - }, - } - } - return &stringLiteralCompletions{ - fromTypes: fromContextualType(checker.ContextFlagsNone, node, typeChecker), - } - default: - result := fromContextualType(checker.ContextFlagsCompletions, node, typeChecker) - if result != nil { - return &stringLiteralCompletions{ - fromTypes: result, - } - } - return &stringLiteralCompletions{ - fromTypes: fromContextualType(checker.ContextFlagsNone, node, typeChecker), - } - } -} - -func fromContextualType(contextFlags checker.ContextFlags, node *ast.Node, typeChecker *checker.Checker) *completionsFromTypes { - // Get completion for string literal from string literal type - // i.e. var x: "hi" | "hello" = "/*completion position*/" - types := getStringLiteralTypes(getContextualTypeFromParent(node, typeChecker, contextFlags), nil, typeChecker) - if len(types) == 0 { - return nil - } - return &completionsFromTypes{ - types: types, - isNewIdentifier: false, - } -} - -func fromUnionableLiteralType( - grandparent *ast.Node, - parent *ast.Node, - position int, - typeChecker *checker.Checker, -) *stringLiteralCompletions { - switch grandparent.Kind { - case ast.KindExpressionWithTypeArguments, ast.KindTypeReference: - typeArgument := ast.FindAncestor(parent, func(n *ast.Node) bool { return n.Parent == grandparent }) - if typeArgument != nil { - t := typeChecker.GetTypeArgumentConstraint(typeArgument) - return &stringLiteralCompletions{ - fromTypes: &completionsFromTypes{ - types: getStringLiteralTypes(t, nil, typeChecker), - isNewIdentifier: false, - }, - } - } - return nil - case ast.KindIndexedAccessType: - // Get all apparent property names - // i.e. interface Foo { - // foo: string; - // bar: string; - // } - // let x: Foo["/*completion position*/"] - indexType := grandparent.AsIndexedAccessTypeNode().IndexType - objectType := grandparent.AsIndexedAccessTypeNode().ObjectType - if !indexType.Loc.ContainsInclusive(position) { - return nil - } - t := typeChecker.GetTypeFromTypeNode(objectType) - return &stringLiteralCompletions{ - fromProperties: stringLiteralCompletionsFromProperties(t, typeChecker), - } - case ast.KindUnionType: - result := fromUnionableLiteralType( - walkUpParentheses(grandparent.Parent), - parent, - position, - typeChecker) - if result == nil { - return nil - } - alreadyUsedTypes := getAlreadyUsedTypesInStringLiteralUnion(grandparent, parent) - switch { - case result.fromProperties != nil: - result := result.fromProperties - return &stringLiteralCompletions{ - fromProperties: &completionsFromProperties{ - symbols: core.Filter( - result.symbols, - func(s *ast.Symbol) bool { return !slices.Contains(alreadyUsedTypes, s.Name) }, - ), - hasIndexSignature: result.hasIndexSignature, - }, - } - case result.fromTypes != nil: - result := result.fromTypes - return &stringLiteralCompletions{ - fromTypes: &completionsFromTypes{ - types: core.Filter(result.types, func(t *checker.StringLiteralType) bool { - return !slices.Contains(alreadyUsedTypes, t.AsLiteralType().Value().(string)) - }), - isNewIdentifier: false, - }, - } - default: - return nil - } - default: - return nil - } -} - -func stringLiteralCompletionsForObjectLiteral( - typeChecker *checker.Checker, - objectLiteralExpression *ast.ObjectLiteralExpressionNode, -) *completionsFromProperties { - contextualType := typeChecker.GetContextualType(objectLiteralExpression, checker.ContextFlagsNone) - if contextualType == nil { - return nil - } - - completionsType := typeChecker.GetContextualType(objectLiteralExpression, checker.ContextFlagsCompletions) - symbols := getPropertiesForObjectExpression( - contextualType, - completionsType, - objectLiteralExpression, - typeChecker) - - return &completionsFromProperties{ - symbols: symbols, - hasIndexSignature: hasIndexSignature(contextualType, typeChecker), - } -} - -func stringLiteralCompletionsFromProperties(t *checker.Type, typeChecker *checker.Checker) *completionsFromProperties { - return &completionsFromProperties{ - symbols: core.Filter(typeChecker.GetApparentProperties(t), func(s *ast.Symbol) bool { - return !(s.ValueDeclaration != nil && ast.IsPrivateIdentifierClassElementDeclaration(s.ValueDeclaration)) - }), - hasIndexSignature: hasIndexSignature(t, typeChecker), - } -} - -func getStringLiteralCompletionsFromModuleNames( - file *ast.SourceFile, - node *ast.LiteralExpression, - program *compiler.Program, - preferences *UserPreferences, -) *stringLiteralCompletions { - // !!! needs `getModeForUsageLocationWorker` - return nil -} - -func walkUpParentheses(node *ast.Node) *ast.Node { - switch node.Kind { - case ast.KindParenthesizedType: - return ast.WalkUpParenthesizedTypes(node) - case ast.KindParenthesizedExpression: - return ast.WalkUpParenthesizedExpressions(node) - default: - return node - } -} - -func getStringLiteralTypes(t *checker.Type, uniques *collections.Set[string], typeChecker *checker.Checker) []*checker.StringLiteralType { - if t == nil { - return nil - } - if uniques == nil { - uniques = &collections.Set[string]{} - } - t = skipConstraint(t, typeChecker) - if t.IsUnion() { - var types []*checker.StringLiteralType - for _, elementType := range t.Types() { - types = append(types, getStringLiteralTypes(elementType, uniques, typeChecker)...) - } - return types - } - if t.IsStringLiteral() && !t.IsEnumLiteral() && uniques.AddIfAbsent(t.AsLiteralType().Value().(string)) { - return []*checker.StringLiteralType{t} - } - return nil -} - -func getAlreadyUsedTypesInStringLiteralUnion(union *ast.UnionType, current *ast.LiteralType) []string { - typesList := union.AsUnionTypeNode().Types - if typesList == nil { - return nil - } - var values []string - for _, typeNode := range typesList.Nodes { - if typeNode != current && ast.IsLiteralTypeNode(typeNode) && - ast.IsStringLiteral(typeNode.AsLiteralTypeNode().Literal) { - values = append(values, typeNode.AsLiteralTypeNode().Literal.Text()) - } - } - return values -} - -func hasIndexSignature(t *checker.Type, typeChecker *checker.Checker) bool { - return typeChecker.GetStringIndexType(t) != nil || typeChecker.GetNumberIndexType(t) != nil -} - -// Matches -// -// require("" -// require("") -func isRequireCallArgument(node *ast.Node) bool { - return ast.IsCallExpression(node.Parent) && len(node.Parent.Arguments()) > 0 && node.Parent.Arguments()[0] == node && - ast.IsIdentifier(node.Parent.Expression()) && node.Parent.Expression().Text() == "require" -} - -func kindModifiersFromExtension(extension string) ScriptElementKindModifier { - switch extension { - case tspath.ExtensionDts: - return ScriptElementKindModifierDts - case tspath.ExtensionJs: - return ScriptElementKindModifierJs - case tspath.ExtensionJson: - return ScriptElementKindModifierJson - case tspath.ExtensionJsx: - return ScriptElementKindModifierJsx - case tspath.ExtensionTs: - return ScriptElementKindModifierTs - case tspath.ExtensionTsx: - return ScriptElementKindModifierTsx - case tspath.ExtensionDmts: - return ScriptElementKindModifierDmts - case tspath.ExtensionMjs: - return ScriptElementKindModifierMjs - case tspath.ExtensionMts: - return ScriptElementKindModifierMts - case tspath.ExtensionDcts: - return ScriptElementKindModifierDcts - case tspath.ExtensionCjs: - return ScriptElementKindModifierCjs - case tspath.ExtensionCts: - return ScriptElementKindModifierCts - case tspath.ExtensionTsBuildInfo: - panic(fmt.Sprintf("Extension %v is unsupported.", tspath.ExtensionTsBuildInfo)) - case "": - return ScriptElementKindModifierNone - default: - panic(fmt.Sprintf("Unexpected extension: %v", extension)) - } -} - -func getStringLiteralCompletionsFromSignature( - call *ast.CallLikeExpression, - arg *ast.StringLiteralLike, - argumentInfo *argumentInfoForCompletions, - typeChecker *checker.Checker, -) *completionsFromTypes { - isNewIdentifier := false - uniques := collections.Set[string]{} - var editingArgument *ast.Node - if ast.IsJsxOpeningLikeElement(call) { - editingArgument = ast.FindAncestor(arg.Parent, ast.IsJsxAttribute) - if editingArgument == nil { - panic("Expected jsx opening-like element to have a jsx attribute as ancestor.") - } - } else { - editingArgument = arg - } - candidates := typeChecker.GetCandidateSignaturesForStringLiteralCompletions(call, editingArgument) - var types []*checker.StringLiteralType - for _, candidate := range candidates { - if !candidate.HasRestParameter() && argumentInfo.argumentCount > len(candidate.Parameters()) { - continue - } - t := typeChecker.GetTypeParameterAtPosition(candidate, argumentInfo.argumentIndex) - if ast.IsJsxOpeningLikeElement(call) { - propType := typeChecker.GetTypeOfPropertyOfType(t, editingArgument.AsJsxAttribute().Name().Text()) - if propType != nil { - t = propType - } - } - isNewIdentifier = isNewIdentifier || t.IsString() - types = append(types, getStringLiteralTypes(t, &uniques, typeChecker)...) - } - if len(types) > 0 { - return &completionsFromTypes{ - types: types, - isNewIdentifier: isNewIdentifier, - } - } - return nil -} - -func (l *LanguageService) getStringLiteralCompletionDetails( - ctx context.Context, - checker *checker.Checker, - item *lsproto.CompletionItem, - name string, - file *ast.SourceFile, - position int, - contextToken *ast.Node, - preferences *UserPreferences, -) *lsproto.CompletionItem { - if contextToken == nil || !ast.IsStringLiteralLike(contextToken) { - return item - } - completions := l.getStringLiteralCompletionEntries( - ctx, - file, - contextToken, - position, - preferences, - ) - if completions == nil { - return item - } - return stringLiteralCompletionDetails(item, name, contextToken, completions, file, checker) -} - -func stringLiteralCompletionDetails( - item *lsproto.CompletionItem, - name string, - location *ast.Node, - completion *stringLiteralCompletions, - file *ast.SourceFile, - checker *checker.Checker, -) *lsproto.CompletionItem { - switch { - case completion.fromPaths != nil: - pathCompletions := completion.fromPaths - for _, pathCompletion := range pathCompletions { - if pathCompletion.name == name { - return createCompletionDetails(item, name, "" /*documentation*/) - } - } - case completion.fromProperties != nil: - properties := completion.fromProperties - for _, symbol := range properties.symbols { - if symbol.Name == name { - return createCompletionDetailsForSymbol(item, symbol, checker, location, nil /*actions*/) - } - } - case completion.fromTypes != nil: - types := completion.fromTypes - for _, t := range types.types { - if t.AsLiteralType().Value().(string) == name { - return createCompletionDetails(item, name, "" /*documentation*/) - } - } - } - return item -} diff --git a/kitcom/internal/tsgo/ls/symbol_display.go b/kitcom/internal/tsgo/ls/symbol_display.go deleted file mode 100644 index 541e2b2..0000000 --- a/kitcom/internal/tsgo/ls/symbol_display.go +++ /dev/null @@ -1,389 +0,0 @@ -package ls - -import ( - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/checker" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" -) - -type ScriptElementKind string - -const ( - ScriptElementKindUnknown ScriptElementKind = "" - ScriptElementKindWarning ScriptElementKind = "warning" - // predefined type (void) or keyword (class) - ScriptElementKindKeyword ScriptElementKind = "keyword" - // top level script node - ScriptElementKindScriptElement ScriptElementKind = "script" - // module foo {} - ScriptElementKindModuleElement ScriptElementKind = "module" - // class X {} - ScriptElementKindClassElement ScriptElementKind = "class" - // var x = class X {} - ScriptElementKindLocalClassElement ScriptElementKind = "local class" - // interface Y {} - ScriptElementKindInterfaceElement ScriptElementKind = "interface" - // type T = ... - ScriptElementKindTypeElement ScriptElementKind = "type" - // enum E {} - ScriptElementKindEnumElement ScriptElementKind = "enum" - ScriptElementKindEnumMemberElement ScriptElementKind = "enum member" - // Inside module and script only. - // const v = ... - ScriptElementKindVariableElement ScriptElementKind = "var" - // Inside function. - ScriptElementKindLocalVariableElement ScriptElementKind = "local var" - // using foo = ... - ScriptElementKindVariableUsingElement ScriptElementKind = "using" - // await using foo = ... - ScriptElementKindVariableAwaitUsingElement ScriptElementKind = "await using" - // Inside module and script only. - // function f() {} - ScriptElementKindFunctionElement ScriptElementKind = "function" - // Inside function. - ScriptElementKindLocalFunctionElement ScriptElementKind = "local function" - // class X { [public|private]* foo() {} } - ScriptElementKindMemberFunctionElement ScriptElementKind = "method" - // class X { [public|private]* [get|set] foo:number; } - ScriptElementKindMemberGetAccessorElement ScriptElementKind = "getter" - ScriptElementKindMemberSetAccessorElement ScriptElementKind = "setter" - // class X { [public|private]* foo:number; } - // interface Y { foo:number; } - ScriptElementKindMemberVariableElement ScriptElementKind = "property" - // class X { [public|private]* accessor foo: number; } - ScriptElementKindMemberAccessorVariableElement ScriptElementKind = "accessor" - // class X { constructor() { } } - // class X { static { } } - ScriptElementKindConstructorImplementationElement ScriptElementKind = "constructor" - // interface Y { ():number; } - ScriptElementKindCallSignatureElement ScriptElementKind = "call" - // interface Y { []:number; } - ScriptElementKindIndexSignatureElement ScriptElementKind = "index" - // interface Y { new():Y; } - ScriptElementKindConstructSignatureElement ScriptElementKind = "construct" - // function foo(*Y*: string) - ScriptElementKindParameterElement ScriptElementKind = "parameter" - ScriptElementKindTypeParameterElement ScriptElementKind = "type parameter" - ScriptElementKindPrimitiveType ScriptElementKind = "primitive type" - ScriptElementKindLabel ScriptElementKind = "label" - ScriptElementKindAlias ScriptElementKind = "alias" - ScriptElementKindConstElement ScriptElementKind = "const" - ScriptElementKindLetElement ScriptElementKind = "let" - ScriptElementKindDirectory ScriptElementKind = "directory" - ScriptElementKindExternalModuleName ScriptElementKind = "external module name" - // String literal - ScriptElementKindString ScriptElementKind = "string" - // Jsdoc @link: in `{@link C link text}`, the before and after text "{@link " and "}" - ScriptElementKindLink ScriptElementKind = "link" - // Jsdoc @link: in `{@link C link text}`, the entity name "C" - ScriptElementKindLinkName ScriptElementKind = "link name" - // Jsdoc @link: in `{@link C link text}`, the link text "link text" - ScriptElementKindLinkText ScriptElementKind = "link text" -) - -type ScriptElementKindModifier string - -const ( - ScriptElementKindModifierNone ScriptElementKindModifier = "" - ScriptElementKindModifierPublic ScriptElementKindModifier = "public" - ScriptElementKindModifierPrivate ScriptElementKindModifier = "private" - ScriptElementKindModifierProtected ScriptElementKindModifier = "protected" - ScriptElementKindModifierExported ScriptElementKindModifier = "export" - ScriptElementKindModifierAmbient ScriptElementKindModifier = "declare" - ScriptElementKindModifierStatic ScriptElementKindModifier = "static" - ScriptElementKindModifierAbstract ScriptElementKindModifier = "abstract" - ScriptElementKindModifierOptional ScriptElementKindModifier = "optional" - ScriptElementKindModifierDeprecated ScriptElementKindModifier = "deprecated" - ScriptElementKindModifierDts ScriptElementKindModifier = ".d.ts" - ScriptElementKindModifierTs ScriptElementKindModifier = ".ts" - ScriptElementKindModifierTsx ScriptElementKindModifier = ".tsx" - ScriptElementKindModifierJs ScriptElementKindModifier = ".js" - ScriptElementKindModifierJsx ScriptElementKindModifier = ".jsx" - ScriptElementKindModifierJson ScriptElementKindModifier = ".json" - ScriptElementKindModifierDmts ScriptElementKindModifier = ".d.mts" - ScriptElementKindModifierMts ScriptElementKindModifier = ".mts" - ScriptElementKindModifierMjs ScriptElementKindModifier = ".mjs" - ScriptElementKindModifierDcts ScriptElementKindModifier = ".d.cts" - ScriptElementKindModifierCts ScriptElementKindModifier = ".cts" - ScriptElementKindModifierCjs ScriptElementKindModifier = ".cjs" -) - -var fileExtensionKindModifiers = []ScriptElementKindModifier{ - ScriptElementKindModifierDts, - ScriptElementKindModifierTs, - ScriptElementKindModifierTsx, - ScriptElementKindModifierJs, - ScriptElementKindModifierJsx, - ScriptElementKindModifierJson, - ScriptElementKindModifierDmts, - ScriptElementKindModifierMts, - ScriptElementKindModifierMjs, - ScriptElementKindModifierDcts, - ScriptElementKindModifierCts, - ScriptElementKindModifierCjs, -} - -func getSymbolKind(typeChecker *checker.Checker, symbol *ast.Symbol, location *ast.Node) ScriptElementKind { - result := getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker, symbol, location) - if result != ScriptElementKindUnknown { - return result - } - - flags := checker.GetCombinedLocalAndExportSymbolFlags(symbol) - if flags&ast.SymbolFlagsClass != 0 { - decl := ast.GetDeclarationOfKind(symbol, ast.KindClassExpression) - if decl != nil { - return ScriptElementKindLocalClassElement - } - return ScriptElementKindClassElement - } - if flags&ast.SymbolFlagsEnum != 0 { - return ScriptElementKindEnumElement - } - if flags&ast.SymbolFlagsTypeAlias != 0 { - return ScriptElementKindTypeElement - } - if flags&ast.SymbolFlagsInterface != 0 { - return ScriptElementKindInterfaceElement - } - if flags&ast.SymbolFlagsTypeParameter != 0 { - return ScriptElementKindTypeParameterElement - } - if flags&ast.SymbolFlagsEnumMember != 0 { - return ScriptElementKindEnumMemberElement - } - if flags&ast.SymbolFlagsAlias != 0 { - return ScriptElementKindAlias - } - if flags&ast.SymbolFlagsModule != 0 { - return ScriptElementKindModuleElement - } - - return ScriptElementKindUnknown -} - -func getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker *checker.Checker, symbol *ast.Symbol, location *ast.Node) ScriptElementKind { - roots := typeChecker.GetRootSymbols(symbol) - // If this is a method from a mapped type, leave as a method so long as it still has a call signature, as opposed to e.g. - // `{ [K in keyof I]: number }`. - if len(roots) == 1 && - roots[0].Flags&ast.SymbolFlagsMethod != 0 && - len(typeChecker.GetCallSignatures(typeChecker.GetNonNullableType(typeChecker.GetTypeOfSymbolAtLocation(symbol, location)))) > 0 { - return ScriptElementKindMemberFunctionElement - } - - if typeChecker.IsUndefinedSymbol(symbol) { - return ScriptElementKindVariableElement - } - if typeChecker.IsArgumentsSymbol(symbol) { - return ScriptElementKindLocalVariableElement - } - if location.Kind == ast.KindThisKeyword && ast.IsExpression(location) || - ast.IsThisInTypeQuery(location) { - return ScriptElementKindParameterElement - } - - flags := checker.GetCombinedLocalAndExportSymbolFlags(symbol) - if flags&ast.SymbolFlagsVariable != 0 { - if isFirstDeclarationOfSymbolParameter(symbol) { - return ScriptElementKindParameterElement - } else if symbol.ValueDeclaration != nil && ast.IsVarConst(symbol.ValueDeclaration) { - return ScriptElementKindConstElement - } else if symbol.ValueDeclaration != nil && ast.IsVarUsing(symbol.ValueDeclaration) { - return ScriptElementKindVariableUsingElement - } else if symbol.ValueDeclaration != nil && ast.IsVarAwaitUsing(symbol.ValueDeclaration) { - return ScriptElementKindVariableAwaitUsingElement - } else if core.Some(symbol.Declarations, ast.IsLet) { - return ScriptElementKindLetElement - } - if isLocalVariableOrFunction(symbol) { - return ScriptElementKindLocalVariableElement - } - return ScriptElementKindVariableElement - } - if flags&ast.SymbolFlagsFunction != 0 { - if isLocalVariableOrFunction(symbol) { - return ScriptElementKindLocalFunctionElement - } - return ScriptElementKindFunctionElement - } - // FIXME: getter and setter use the same symbol. And it is rare to use only setter without getter, so in most cases the symbol always has getter flag. - // So, even when the location is just on the declaration of setter, this function returns getter. - if flags&ast.SymbolFlagsGetAccessor != 0 { - return ScriptElementKindMemberGetAccessorElement - } - if flags&ast.SymbolFlagsSetAccessor != 0 { - return ScriptElementKindMemberSetAccessorElement - } - if flags&ast.SymbolFlagsMethod != 0 { - return ScriptElementKindMemberFunctionElement - } - if flags&ast.SymbolFlagsConstructor != 0 { - return ScriptElementKindConstructorImplementationElement - } - if flags&ast.SymbolFlagsSignature != 0 { - return ScriptElementKindIndexSignatureElement - } - - if flags&ast.SymbolFlagsProperty != 0 { - if flags&ast.SymbolFlagsTransient != 0 && - symbol.CheckFlags&ast.CheckFlagsSynthetic != 0 { - // If union property is result of union of non method (property/accessors/variables), it is labeled as property - var unionPropertyKind ScriptElementKind - for _, rootSymbol := range roots { - if rootSymbol.Flags&(ast.SymbolFlagsPropertyOrAccessor|ast.SymbolFlagsVariable) != 0 { - unionPropertyKind = ScriptElementKindMemberVariableElement - break - } - } - if unionPropertyKind == ScriptElementKindUnknown { - // If this was union of all methods, - // make sure it has call signatures before we can label it as method. - typeOfUnionProperty := typeChecker.GetTypeOfSymbolAtLocation(symbol, location) - if len(typeChecker.GetCallSignatures(typeOfUnionProperty)) > 0 { - return ScriptElementKindMemberFunctionElement - } - return ScriptElementKindMemberVariableElement - } - return unionPropertyKind - } - - return ScriptElementKindMemberVariableElement - } - - return ScriptElementKindUnknown -} - -func isFirstDeclarationOfSymbolParameter(symbol *ast.Symbol) bool { - var declaration *ast.Node - if len(symbol.Declarations) > 0 { - declaration = symbol.Declarations[0] - } - result := ast.FindAncestorOrQuit(declaration, func(n *ast.Node) ast.FindAncestorResult { - if ast.IsParameter(n) { - return ast.FindAncestorTrue - } - if ast.IsBindingElement(n) || ast.IsObjectBindingPattern(n) || ast.IsArrayBindingPattern(n) { - return ast.FindAncestorFalse - } - return ast.FindAncestorQuit - }) - - return result != nil -} - -func isLocalVariableOrFunction(symbol *ast.Symbol) bool { - if symbol.Parent != nil { - return false // This is exported symbol - } - - for _, decl := range symbol.Declarations { - // Function expressions are local - if decl.Kind == ast.KindFunctionExpression { - return true - } - - if decl.Kind != ast.KindVariableDeclaration && decl.Kind != ast.KindFunctionDeclaration { - continue - } - - // If the parent is not source file or module block, it is a local variable. - parent := decl.Parent - for ; !ast.IsFunctionBlock(parent); parent = parent.Parent { - // Reached source file or module block - if parent.Kind == ast.KindSourceFile || parent.Kind == ast.KindModuleBlock { - break - } - } - - if ast.IsFunctionBlock(parent) { - // Parent is in function block. - return true - } - } - return false -} - -func getSymbolModifiers(typeChecker *checker.Checker, symbol *ast.Symbol) collections.Set[ScriptElementKindModifier] { - if symbol == nil { - return collections.Set[ScriptElementKindModifier]{} - } - - modifiers := getNormalizedSymbolModifiers(typeChecker, symbol) - if symbol.Flags&ast.SymbolFlagsAlias != 0 { - resolvedSymbol := typeChecker.GetAliasedSymbol(symbol) - if resolvedSymbol != symbol { - aliasModifiers := getNormalizedSymbolModifiers(typeChecker, resolvedSymbol) - for modifier := range aliasModifiers.Keys() { - modifiers.Add(modifier) - } - } - } - if symbol.Flags&ast.SymbolFlagsOptional != 0 { - modifiers.Add(ScriptElementKindModifierOptional) - } - - return modifiers -} - -func getNormalizedSymbolModifiers(typeChecker *checker.Checker, symbol *ast.Symbol) collections.Set[ScriptElementKindModifier] { - var modifierSet collections.Set[ScriptElementKindModifier] - if len(symbol.Declarations) > 0 { - declaration := symbol.Declarations[0] - declarations := symbol.Declarations[1:] - // omit deprecated flag if some declarations are not deprecated - var excludeFlags ast.ModifierFlags - if len(declarations) > 0 && - typeChecker.IsDeprecatedDeclaration(declaration) && // !!! include jsdoc node flags - core.Some(declarations, func(d *ast.Node) bool { return !typeChecker.IsDeprecatedDeclaration(d) }) { - excludeFlags = ast.ModifierFlagsDeprecated - } else { - excludeFlags = ast.ModifierFlagsNone - } - modifierSet = getNodeModifiers(declaration, excludeFlags) - } - - return modifierSet -} - -func getNodeModifiers(node *ast.Node, excludeFlags ast.ModifierFlags) collections.Set[ScriptElementKindModifier] { - var result collections.Set[ScriptElementKindModifier] - var flags ast.ModifierFlags - if ast.IsDeclaration(node) { - flags = ast.GetCombinedModifierFlags(node) & ^excludeFlags // !!! include jsdoc node flags - } - - if flags&ast.ModifierFlagsPrivate != 0 { - result.Add(ScriptElementKindModifierPrivate) - } - if flags&ast.ModifierFlagsProtected != 0 { - result.Add(ScriptElementKindModifierProtected) - } - if flags&ast.ModifierFlagsPublic != 0 { - result.Add(ScriptElementKindModifierPublic) - } - if flags&ast.ModifierFlagsStatic != 0 { - result.Add(ScriptElementKindModifierStatic) - } - if flags&ast.ModifierFlagsAbstract != 0 { - result.Add(ScriptElementKindModifierAbstract) - } - if flags&ast.ModifierFlagsExport != 0 { - result.Add(ScriptElementKindModifierExported) - } - if flags&ast.ModifierFlagsDeprecated != 0 { - result.Add(ScriptElementKindModifierDeprecated) - } - if flags&ast.ModifierFlagsAmbient != 0 { - result.Add(ScriptElementKindModifierAmbient) - } - if node.Flags&ast.NodeFlagsAmbient != 0 { - result.Add(ScriptElementKindModifierAmbient) - } - if node.Kind == ast.KindExportAssignment { - result.Add(ScriptElementKindModifierExported) - } - - return result -} diff --git a/kitcom/internal/tsgo/ls/symbols.go b/kitcom/internal/tsgo/ls/symbols.go deleted file mode 100644 index 74b54f7..0000000 --- a/kitcom/internal/tsgo/ls/symbols.go +++ /dev/null @@ -1,309 +0,0 @@ -package ls - -import ( - "context" - "slices" - "strings" - "unicode" - "unicode/utf8" - - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/compiler" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/printer" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/stringutil" -) - -func (l *LanguageService) ProvideDocumentSymbols(ctx context.Context, documentURI lsproto.DocumentUri) (lsproto.DocumentSymbolResponse, error) { - _, file := l.getProgramAndFile(documentURI) - symbols := l.getDocumentSymbolsForChildren(ctx, file.AsNode()) - return lsproto.SymbolInformationsOrDocumentSymbolsOrNull{DocumentSymbols: &symbols}, nil -} - -func (l *LanguageService) getDocumentSymbolsForChildren(ctx context.Context, node *ast.Node) []*lsproto.DocumentSymbol { - var symbols []*lsproto.DocumentSymbol - addSymbolForNode := func(node *ast.Node, children []*lsproto.DocumentSymbol) { - if node.Flags&ast.NodeFlagsReparsed == 0 { - symbol := l.newDocumentSymbol(node, children) - if symbol != nil { - symbols = append(symbols, symbol) - } - } - } - var visit func(*ast.Node) bool - getSymbolsForChildren := func(node *ast.Node) []*lsproto.DocumentSymbol { - var result []*lsproto.DocumentSymbol - if node != nil { - saveSymbols := symbols - symbols = nil - node.ForEachChild(visit) - result = symbols - symbols = saveSymbols - } - return result - } - visit = func(node *ast.Node) bool { - if ctx.Err() != nil { - return true - } - switch node.Kind { - case ast.KindClassDeclaration, ast.KindClassExpression, ast.KindInterfaceDeclaration, ast.KindEnumDeclaration: - addSymbolForNode(node, getSymbolsForChildren(node)) - case ast.KindModuleDeclaration: - addSymbolForNode(node, getSymbolsForChildren(getInteriorModule(node))) - case ast.KindFunctionDeclaration, ast.KindFunctionExpression, ast.KindArrowFunction, ast.KindMethodDeclaration, ast.KindGetAccessor, - ast.KindSetAccessor, ast.KindConstructor: - addSymbolForNode(node, getSymbolsForChildren(node.Body())) - case ast.KindVariableDeclaration, ast.KindBindingElement, ast.KindPropertyAssignment, ast.KindPropertyDeclaration: - name := node.Name() - if name != nil { - if ast.IsBindingPattern(name) { - visit(name) - } else { - addSymbolForNode(node, getSymbolsForChildren(node.Initializer())) - } - } - case ast.KindMethodSignature, ast.KindPropertySignature, ast.KindCallSignature, ast.KindConstructSignature, ast.KindIndexSignature, - ast.KindEnumMember, ast.KindShorthandPropertyAssignment, ast.KindTypeAliasDeclaration: - addSymbolForNode(node, nil) - default: - node.ForEachChild(visit) - } - return false - } - node.ForEachChild(visit) - return symbols -} - -func (l *LanguageService) newDocumentSymbol(node *ast.Node, children []*lsproto.DocumentSymbol) *lsproto.DocumentSymbol { - result := new(lsproto.DocumentSymbol) - file := ast.GetSourceFileOfNode(node) - nodeStartPos := scanner.SkipTrivia(file.Text(), node.Pos()) - name := ast.GetNameOfDeclaration(node) - var text string - var nameStartPos, nameEndPos int - if ast.IsModuleDeclaration(node) && !ast.IsAmbientModule(node) { - text = getModuleName(node) - nameStartPos = scanner.SkipTrivia(file.Text(), name.Pos()) - nameEndPos = getInteriorModule(node).Name().End() - } else if name != nil { - text = getTextOfName(name) - nameStartPos = max(scanner.SkipTrivia(file.Text(), name.Pos()), nodeStartPos) - nameEndPos = max(name.End(), nodeStartPos) - } else { - text = getUnnamedNodeLabel(node) - nameStartPos = nodeStartPos - nameEndPos = nodeStartPos - } - if text == "" { - return nil - } - result.Name = text - result.Kind = getSymbolKindFromNode(node) - result.Range = lsproto.Range{ - Start: l.converters.PositionToLineAndCharacter(file, core.TextPos(nodeStartPos)), - End: l.converters.PositionToLineAndCharacter(file, core.TextPos(node.End())), - } - result.SelectionRange = lsproto.Range{ - Start: l.converters.PositionToLineAndCharacter(file, core.TextPos(nameStartPos)), - End: l.converters.PositionToLineAndCharacter(file, core.TextPos(nameEndPos)), - } - if children == nil { - children = []*lsproto.DocumentSymbol{} - } - result.Children = &children - return result -} - -func getTextOfName(node *ast.Node) string { - switch node.Kind { - case ast.KindIdentifier, ast.KindPrivateIdentifier, ast.KindNumericLiteral: - return node.Text() - case ast.KindStringLiteral: - return "\"" + printer.EscapeString(node.Text(), '"') + "\"" - case ast.KindNoSubstitutionTemplateLiteral: - return "`" + printer.EscapeString(node.Text(), '`') + "`" - case ast.KindComputedPropertyName: - if ast.IsStringOrNumericLiteralLike(node.Expression()) { - return getTextOfName(node.Expression()) - } - } - return scanner.GetTextOfNode(node) -} - -func getUnnamedNodeLabel(node *ast.Node) string { - switch node.Kind { - case ast.KindFunctionExpression, ast.KindArrowFunction: - if ast.IsCallExpression(node.Parent) { - name := getCallExpressionName(node.Parent.Expression()) - if name != "" { - return name + "() callback" - } - } - return "" - case ast.KindClassExpression: - return "" - case ast.KindConstructor: - return "constructor" - case ast.KindCallSignature: - return "()" - case ast.KindConstructSignature: - return "new()" - case ast.KindIndexSignature: - return "[]" - } - return "" -} - -func getCallExpressionName(node *ast.Node) string { - switch node.Kind { - case ast.KindIdentifier, ast.KindPrivateIdentifier: - return node.Text() - case ast.KindPropertyAccessExpression: - left := getCallExpressionName(node.Expression()) - right := getCallExpressionName(node.Name()) - if left != "" { - return left + "." + right - } - return right - } - return "" -} - -func getInteriorModule(node *ast.Node) *ast.Node { - for node.Body() != nil && ast.IsModuleDeclaration(node.Body()) { - node = node.Body() - } - return node -} - -func getModuleName(node *ast.Node) string { - result := node.Name().Text() - for node.Body() != nil && ast.IsModuleDeclaration(node.Body()) { - node = node.Body() - result = result + "." + node.Name().Text() - } - return result -} - -type DeclarationInfo struct { - name string - declaration *ast.Node - matchScore int -} - -func ProvideWorkspaceSymbols(ctx context.Context, programs []*compiler.Program, converters *Converters, query string) (lsproto.WorkspaceSymbolResponse, error) { - // Obtain set of non-declaration source files from all active programs. - var sourceFiles collections.Set[*ast.SourceFile] - for _, program := range programs { - for _, sourceFile := range program.SourceFiles() { - if !sourceFile.IsDeclarationFile { - sourceFiles.Add(sourceFile) - } - } - } - // Create DeclarationInfos for all declarations in the source files. - var infos []DeclarationInfo - for sourceFile := range sourceFiles.Keys() { - if ctx.Err() != nil { - return lsproto.SymbolInformationsOrWorkspaceSymbolsOrNull{}, nil - } - declarationMap := sourceFile.GetDeclarationMap() - for name, declarations := range declarationMap { - score := getMatchScore(name, query) - if score >= 0 { - for _, declaration := range declarations { - infos = append(infos, DeclarationInfo{name, declaration, score}) - } - } - } - } - // Sort the DeclarationInfos and return the top 256 matches. - slices.SortFunc(infos, compareDeclarationInfos) - count := min(len(infos), 256) - symbols := make([]*lsproto.SymbolInformation, count) - for i, info := range infos[0:count] { - node := core.OrElse(ast.GetNameOfDeclaration(info.declaration), info.declaration) - sourceFile := ast.GetSourceFileOfNode(node) - pos := scanner.SkipTrivia(sourceFile.Text(), node.Pos()) - var symbol lsproto.SymbolInformation - symbol.Name = info.name - symbol.Kind = getSymbolKindFromNode(info.declaration) - symbol.Location = converters.ToLSPLocation(sourceFile, core.NewTextRange(pos, node.End())) - symbols[i] = &symbol - } - return lsproto.SymbolInformationsOrWorkspaceSymbolsOrNull{SymbolInformations: &symbols}, nil -} - -// Return a score for matching `s` against `pattern`. In order to match, `s` must contain each of the characters in -// `pattern` in the same order. Upper case characters in `pattern` must match exactly, whereas lower case characters -// in `pattern` match either case in `s`. If `s` doesn't match, -1 is returned. Otherwise, the returned score is the -// number of characters in `s` that weren't matched. Thus, zero represents an exact match, and higher values represent -// increasingly less specific partial matches. -func getMatchScore(s string, pattern string) int { - score := 0 - for _, p := range pattern { - exact := unicode.IsUpper(p) - for { - c, size := utf8.DecodeRuneInString(s) - if size == 0 { - return -1 - } - s = s[size:] - if exact && c == p || !exact && unicode.ToLower(c) == unicode.ToLower(p) { - break - } - score++ - } - } - return score -} - -// Sort DeclarationInfos by ascending match score, then ascending case insensitive name, then -// ascending case sensitive name, and finally by source file name and position. -func compareDeclarationInfos(d1, d2 DeclarationInfo) int { - if d1.matchScore != d2.matchScore { - return d1.matchScore - d2.matchScore - } - if c := stringutil.CompareStringsCaseInsensitive(d1.name, d2.name); c != 0 { - return c - } - if c := strings.Compare(d1.name, d2.name); c != 0 { - return c - } - s1 := ast.GetSourceFileOfNode(d1.declaration) - s2 := ast.GetSourceFileOfNode(d2.declaration) - if s1 != s2 { - return strings.Compare(string(s1.Path()), string(s2.Path())) - } - return d1.declaration.Pos() - d2.declaration.Pos() -} - -func getSymbolKindFromNode(node *ast.Node) lsproto.SymbolKind { - switch node.Kind { - case ast.KindModuleDeclaration: - return lsproto.SymbolKindNamespace - case ast.KindClassDeclaration, ast.KindClassExpression, ast.KindTypeAliasDeclaration: - return lsproto.SymbolKindClass - case ast.KindMethodDeclaration, ast.KindMethodSignature: - return lsproto.SymbolKindMethod - case ast.KindPropertyDeclaration, ast.KindPropertySignature, ast.KindGetAccessor, ast.KindSetAccessor: - return lsproto.SymbolKindProperty - case ast.KindConstructor, ast.KindConstructSignature: - return lsproto.SymbolKindConstructor - case ast.KindEnumDeclaration: - return lsproto.SymbolKindEnum - case ast.KindInterfaceDeclaration: - return lsproto.SymbolKindInterface - case ast.KindFunctionDeclaration, ast.KindFunctionExpression: - return lsproto.SymbolKindFunction - case ast.KindEnumMember: - return lsproto.SymbolKindEnumMember - case ast.KindTypeParameter: - return lsproto.SymbolKindTypeParameter - } - return lsproto.SymbolKindVariable -} diff --git a/kitcom/internal/tsgo/ls/userpreferences.go b/kitcom/internal/tsgo/ls/userpreferences.go deleted file mode 100644 index b1cfbfb..0000000 --- a/kitcom/internal/tsgo/ls/userpreferences.go +++ /dev/null @@ -1,211 +0,0 @@ -package ls - -import ( - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/modulespecifiers" -) - -type UserPreferences struct { - QuotePreference QuotePreference - LazyConfiguredProjectsFromExternalProject bool // !!! - - // A positive integer indicating the maximum length of a hover text before it is truncated. - // - // Default: `500` - MaximumHoverLength int // !!! - - // ------- Completions ------- - - // If enabled, TypeScript will search through all external modules' exports and add them to the completions list. - // This affects lone identifier completions but not completions on the right hand side of `obj.`. - IncludeCompletionsForModuleExports core.Tristate - // Enables auto-import-style completions on partially-typed import statements. E.g., allows - // `import write|` to be completed to `import { writeFile } from "fs"`. - IncludeCompletionsForImportStatements core.Tristate - // Unless this option is `false`, member completion lists triggered with `.` will include entries - // on potentially-null and potentially-undefined values, with insertion text to replace - // preceding `.` tokens with `?.`. - IncludeAutomaticOptionalChainCompletions core.Tristate - // Allows completions to be formatted with snippet text, indicated by `CompletionItem["isSnippet"]`. - IncludeCompletionsWithSnippetText core.Tristate // !!! - // If enabled, completions for class members (e.g. methods and properties) will include - // a whole declaration for the member. - // E.g., `class A { f| }` could be completed to `class A { foo(): number {} }`, instead of - // `class A { foo }`. - IncludeCompletionsWithClassMemberSnippets core.Tristate // !!! - // If enabled, object literal methods will have a method declaration completion entry in addition - // to the regular completion entry containing just the method name. - // E.g., `const objectLiteral: T = { f| }` could be completed to `const objectLiteral: T = { foo(): void {} }`, - // in addition to `const objectLiteral: T = { foo }`. - IncludeCompletionsWithObjectLiteralMethodSnippets core.Tristate // !!! - JsxAttributeCompletionStyle JsxAttributeCompletionStyle - - // ------- AutoImports -------- - - ImportModuleSpecifierPreference modulespecifiers.ImportModuleSpecifierPreference // !!! - // Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" - ImportModuleSpecifierEnding modulespecifiers.ImportModuleSpecifierEndingPreference // !!! - IncludePackageJsonAutoImports IncludePackageJsonAutoImports // !!! - AutoImportSpecifierExcludeRegexes []string // !!! - AutoImportFileExcludePatterns []string // !!! - PreferTypeOnlyAutoImports bool // !!! - - // ------- OrganizeImports ------- - - // Indicates whether imports should be organized in a case-insensitive manner. - // - // Default: TSUnknown ("auto" in strada), will perform detection - OrganizeImportsIgnoreCase core.Tristate // !!! - // Indicates whether imports should be organized via an "ordinal" (binary) comparison using the numeric value of their - // code points, or via "unicode" collation (via the Unicode Collation Algorithm (https://unicode.org/reports/tr10/#Scope)) - // - // using rules associated with the locale specified in organizeImportsCollationLocale. - // - // Default: Ordinal - OrganizeImportsCollation OrganizeImportsCollation // !!! - // Indicates the locale to use for "unicode" collation. If not specified, the locale `"en"` is used as an invariant - // for the sake of consistent sorting. Use `"auto"` to use the detected UI locale. - // - // This preference is ignored if organizeImportsCollation is not `unicode`. - // - // Default: `"en"` - OrganizeImportsLocale string // !!! - // Indicates whether numeric collation should be used for digit sequences in strings. When `true`, will collate - // strings such that `a1z < a2z < a100z`. When `false`, will collate strings such that `a1z < a100z < a2z`. - // - // This preference is ignored if organizeImportsCollation is not `unicode`. - // - // Default: `false` - OrganizeImportsNumericCollation bool // !!! - // Indicates whether accents and other diacritic marks are considered unequal for the purpose of collation. When - // `true`, characters with accents and other diacritics will be collated in the order defined by the locale specified - // in organizeImportsCollationLocale. - // - // This preference is ignored if organizeImportsCollation is not `unicode`. - // - // Default: `true` - OrganizeImportsAccentCollation OrganizeImportsAccentCollation // !!! - // Indicates whether upper case or lower case should sort first. When `false`, the default order for the locale - // specified in organizeImportsCollationLocale is used. - // - // This preference is ignored if: - // - organizeImportsCollation is not `unicode` - // - organizeImportsIgnoreCase is `true` - // - organizeImportsIgnoreCase is `auto` and the auto-detected case sensitivity is case-insensitive. - // - // Default: `false` - OrganizeImportsCaseFirst OrganizeImportsCaseFirst // !!! - // Indicates where named type-only imports should sort. "inline" sorts named imports without regard to if the import is type-only. - // - // Default: `last` - OrganizeImportsTypeOrder OrganizeImportsTypeOrder // !!! - - // ------- MoveToFile ------- - - AllowTextChangesInNewFiles bool // !!! - - // ------- Rename ------- - - // renamed from `providePrefixAndSuffixTextForRename` - UseAliasesForRename core.Tristate - AllowRenameOfImportPath bool // !!! - - // ------- CodeFixes/Refactors ------- - - ProvideRefactorNotApplicableReason bool // !!! - - // ------- InlayHints ------- - - IncludeInlayParameterNameHints IncludeInlayParameterNameHints - IncludeInlayParameterNameHintsWhenArgumentMatchesName bool - IncludeInlayFunctionParameterTypeHints bool - IncludeInlayVariableTypeHints bool - IncludeInlayVariableTypeHintsWhenTypeMatchesName bool - IncludeInlayPropertyDeclarationTypeHints bool - IncludeInlayFunctionLikeReturnTypeHints bool - IncludeInlayEnumMemberValueHints bool - InteractiveInlayHints bool - - // ------- Misc ------- - - ExcludeLibrarySymbolsInNavTo bool // !!! - DisableSuggestions bool // !!! - DisableLineTextInReferences bool // !!! - DisplayPartsForJSDoc bool // !!! -} - -type JsxAttributeCompletionStyle string - -const ( - JsxAttributeCompletionStyleUnknown JsxAttributeCompletionStyle = "" - JsxAttributeCompletionStyleAuto JsxAttributeCompletionStyle = "auto" - JsxAttributeCompletionStyleBraces JsxAttributeCompletionStyle = "braces" - JsxAttributeCompletionStyleNone JsxAttributeCompletionStyle = "none" -) - -type IncludeInlayParameterNameHints string - -const ( - IncludeInlayParameterNameHintsNone IncludeInlayParameterNameHints = "" - IncludeInlayParameterNameHintsAll IncludeInlayParameterNameHints = "all" - IncludeInlayParameterNameHintsLiterals IncludeInlayParameterNameHints = "literals" -) - -type IncludePackageJsonAutoImports string - -const ( - IncludePackageJsonAutoImportsUnknown IncludePackageJsonAutoImports = "" - IncludePackageJsonAutoImportsAuto IncludePackageJsonAutoImports = "auto" - IncludePackageJsonAutoImportsOn IncludePackageJsonAutoImports = "on" - IncludePackageJsonAutoImportsOff IncludePackageJsonAutoImports = "off" -) - -type OrganizeImportsCollation bool - -const ( - OrganizeImportsCollationOrdinal OrganizeImportsCollation = false - OrganizeImportsCollationUnicode OrganizeImportsCollation = true -) - -type OrganizeImportsAccentCollation int - -const ( - OrganizeImportsAccentCollationTrue OrganizeImportsAccentCollation = 0 - OrganizeImportsAccentCollationFalse OrganizeImportsAccentCollation = 1 -) - -type OrganizeImportsCaseFirst int - -const ( - OrganizeImportsCaseFirstFalse OrganizeImportsCaseFirst = 0 - OrganizeImportsCaseFirstLower OrganizeImportsCaseFirst = 1 - OrganizeImportsCaseFirstUpper OrganizeImportsCaseFirst = 2 -) - -type OrganizeImportsTypeOrder int - -const ( - OrganizeImportsTypeOrderLast OrganizeImportsTypeOrder = 0 - OrganizeImportsTypeOrderInline OrganizeImportsTypeOrder = 1 - OrganizeImportsTypeOrderFirst OrganizeImportsTypeOrder = 2 -) - -type QuotePreference string - -const ( - QuotePreferenceUnknown QuotePreference = "" - QuotePreferenceAuto QuotePreference = "auto" - QuotePreferenceDouble QuotePreference = "double" - QuotePreferenceSingle QuotePreference = "single" -) - -func (p *UserPreferences) Parse(config map[string]interface{}) { -} - -func (p *UserPreferences) ModuleSpecifierPreferences() modulespecifiers.UserPreferences { - return modulespecifiers.UserPreferences{ - ImportModuleSpecifierPreference: p.ImportModuleSpecifierPreference, - ImportModuleSpecifierEnding: p.ImportModuleSpecifierEnding, - AutoImportSpecifierExcludeRegexes: p.AutoImportSpecifierExcludeRegexes, - } -} diff --git a/kitcom/internal/tsgo/ls/utilities.go b/kitcom/internal/tsgo/ls/utilities.go deleted file mode 100644 index 1551897..0000000 --- a/kitcom/internal/tsgo/ls/utilities.go +++ /dev/null @@ -1,1722 +0,0 @@ -package ls - -import ( - "cmp" - "fmt" - "iter" - "slices" - "strings" - "unicode" - - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/checker" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/debug" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/jsnum" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsutil" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/stringutil" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath" -) - -// Implements a cmp.Compare like function for two lsproto.Position -// ComparePositions(pos, other) == cmp.Compare(pos, other) -func ComparePositions(pos, other lsproto.Position) int { - if lineComp := cmp.Compare(pos.Line, other.Line); lineComp != 0 { - return lineComp - } - return cmp.Compare(pos.Character, other.Character) -} - -// Implements a cmp.Compare like function for two *lsproto.Range -// CompareRanges(lsRange, other) == cmp.Compare(lsrange, other) -// -// Range.Start is compared before Range.End -func CompareRanges(lsRange, other *lsproto.Range) int { - if startComp := ComparePositions(lsRange.Start, other.Start); startComp != 0 { - return startComp - } - return ComparePositions(lsRange.End, other.End) -} - -var quoteReplacer = strings.NewReplacer("'", `\'`, `\"`, `"`) - -func IsInString(sourceFile *ast.SourceFile, position int, previousToken *ast.Node) bool { - if previousToken != nil && ast.IsStringTextContainingNode(previousToken) { - start := astnav.GetStartOfNode(previousToken, sourceFile, false /*includeJSDoc*/) - end := previousToken.End() - - // To be "in" one of these literals, the position has to be: - // 1. entirely within the token text. - // 2. at the end position of an unterminated token. - // 3. at the end of a regular expression (due to trailing flags like '/foo/g'). - if start < position && position < end { - return true - } - - if position == end { - return ast.IsUnterminatedLiteral(previousToken) - } - } - return false -} - -func importFromModuleSpecifier(node *ast.Node) *ast.Node { - if result := tryGetImportFromModuleSpecifier(node); result != nil { - return result - } - debug.FailBadSyntaxKind(node.Parent) - return nil -} - -func tryGetImportFromModuleSpecifier(node *ast.StringLiteralLike) *ast.Node { - switch node.Parent.Kind { - case ast.KindImportDeclaration, ast.KindJSImportDeclaration, ast.KindExportDeclaration: - return node.Parent - case ast.KindExternalModuleReference: - return node.Parent.Parent - case ast.KindCallExpression: - if ast.IsImportCall(node.Parent) || ast.IsRequireCall(node.Parent, false /*requireStringLiteralLikeArgument*/) { - return node.Parent - } - return nil - case ast.KindLiteralType: - if !ast.IsStringLiteral(node) { - return nil - } - if ast.IsImportTypeNode(node.Parent.Parent) { - return node.Parent.Parent - } - return nil - } - return nil -} - -func isModuleSpecifierLike(node *ast.Node) bool { - if !ast.IsStringLiteralLike(node) { - return false - } - - if ast.IsRequireCall(node.Parent, false /*requireStringLiteralLikeArgument*/) || ast.IsImportCall(node.Parent) { - return node.Parent.AsCallExpression().Arguments.Nodes[0] == node - } - - return node.Parent.Kind == ast.KindExternalModuleReference || - node.Parent.Kind == ast.KindImportDeclaration || - node.Parent.Kind == ast.KindJSImportDeclaration -} - -func getNonModuleSymbolOfMergedModuleSymbol(symbol *ast.Symbol) *ast.Symbol { - if len(symbol.Declarations) == 0 || (symbol.Flags&(ast.SymbolFlagsModule|ast.SymbolFlagsTransient)) == 0 { - return nil - } - - if decl := core.Find(symbol.Declarations, func(d *ast.Node) bool { return !ast.IsSourceFile(d) && !ast.IsModuleDeclaration(d) }); decl != nil { - return decl.Symbol() - } - return nil -} - -func moduleSymbolToValidIdentifier(moduleSymbol *ast.Symbol, target core.ScriptTarget, forceCapitalize bool) string { - return moduleSpecifierToValidIdentifier(stringutil.StripQuotes(moduleSymbol.Name), target, forceCapitalize) -} - -func moduleSpecifierToValidIdentifier(moduleSpecifier string, target core.ScriptTarget, forceCapitalize bool) string { - baseName := tspath.GetBaseFileName(strings.TrimSuffix(tspath.RemoveFileExtension(moduleSpecifier), "/index")) - res := []rune{} - lastCharWasValid := true - baseNameRunes := []rune(baseName) - if len(baseNameRunes) > 0 && scanner.IsIdentifierStart(baseNameRunes[0]) { - if forceCapitalize { - res = append(res, unicode.ToUpper(baseNameRunes[0])) - } else { - res = append(res, baseNameRunes[0]) - } - } else { - lastCharWasValid = false - } - - for i := 1; i < len(baseNameRunes); i++ { - isValid := scanner.IsIdentifierPart(baseNameRunes[i]) - if isValid { - if !lastCharWasValid { - res = append(res, unicode.ToUpper(baseNameRunes[i])) - } else { - res = append(res, baseNameRunes[i]) - } - } - lastCharWasValid = isValid - } - - // Need `"_"` to ensure result isn't empty. - resString := string(res) - if resString != "" && !isNonContextualKeyword(scanner.StringToToken(resString)) { - return resString - } - return "_" + resString -} - -func getLocalSymbolForExportSpecifier(referenceLocation *ast.Identifier, referenceSymbol *ast.Symbol, exportSpecifier *ast.ExportSpecifier, ch *checker.Checker) *ast.Symbol { - if isExportSpecifierAlias(referenceLocation, exportSpecifier) { - if symbol := ch.GetExportSpecifierLocalTargetSymbol(exportSpecifier.AsNode()); symbol != nil { - return symbol - } - } - return referenceSymbol -} - -func isExportSpecifierAlias(referenceLocation *ast.Identifier, exportSpecifier *ast.ExportSpecifier) bool { - debug.Assert(exportSpecifier.PropertyName == referenceLocation.AsNode() || exportSpecifier.Name() == referenceLocation.AsNode(), "referenceLocation is not export specifier name or property name") - propertyName := exportSpecifier.PropertyName - if propertyName != nil { - // Given `export { foo as bar } [from "someModule"]`: It's an alias at `foo`, but at `bar` it's a new symbol. - return propertyName == referenceLocation.AsNode() - } else { - // `export { foo } from "foo"` is a re-export. - // `export { foo };` is not a re-export, it creates an alias for the local variable `foo`. - return exportSpecifier.Parent.Parent.AsExportDeclaration().ModuleSpecifier == nil - } -} - -func isInComment(file *ast.SourceFile, position int, tokenAtPosition *ast.Node) *ast.CommentRange { - return getRangeOfEnclosingComment(file, position, astnav.FindPrecedingToken(file, position), tokenAtPosition) -} - -func hasChildOfKind(containingNode *ast.Node, kind ast.Kind, sourceFile *ast.SourceFile) bool { - return findChildOfKind(containingNode, kind, sourceFile) != nil -} - -func findChildOfKind(containingNode *ast.Node, kind ast.Kind, sourceFile *ast.SourceFile) *ast.Node { - lastNodePos := containingNode.Pos() - scanner := scanner.GetScannerForSourceFile(sourceFile, lastNodePos) - - var foundChild *ast.Node - visitNode := func(node *ast.Node) bool { - if node == nil || node.Flags&ast.NodeFlagsReparsed != 0 { - return false - } - // Look for child in preceding tokens. - startPos := lastNodePos - for startPos < node.Pos() { - tokenKind := scanner.Token() - tokenFullStart := scanner.TokenFullStart() - tokenEnd := scanner.TokenEnd() - token := sourceFile.GetOrCreateToken(tokenKind, tokenFullStart, tokenEnd, containingNode) - if tokenKind == kind { - foundChild = token - return true - } - startPos = tokenEnd - scanner.Scan() - } - if node.Kind == kind { - foundChild = node - return true - } - - lastNodePos = node.End() - scanner.ResetPos(lastNodePos) - return false - } - - ast.ForEachChildAndJSDoc(containingNode, sourceFile, visitNode) - - if foundChild != nil { - return foundChild - } - - // Look for child in trailing tokens. - startPos := lastNodePos - for startPos < containingNode.End() { - tokenKind := scanner.Token() - tokenFullStart := scanner.TokenFullStart() - tokenEnd := scanner.TokenEnd() - token := sourceFile.GetOrCreateToken(tokenKind, tokenFullStart, tokenEnd, containingNode) - if tokenKind == kind { - return token - } - startPos = tokenEnd - scanner.Scan() - } - return nil -} - -type PossibleTypeArgumentInfo struct { - called *ast.IdentifierNode - nTypeArguments int -} - -// Get info for an expression like `f <` that may be the start of type arguments. -func getPossibleTypeArgumentsInfo(tokenIn *ast.Node, sourceFile *ast.SourceFile) *PossibleTypeArgumentInfo { - // This is a rare case, but one that saves on a _lot_ of work if true - if the source file has _no_ `<` character, - // then there obviously can't be any type arguments - no expensive brace-matching backwards scanning required - if strings.LastIndexByte(sourceFile.Text(), '<') == -1 { - return nil - } - - token := tokenIn - // This function determines if the node could be a type argument position - // When editing, it is common to have an incomplete type argument list (e.g. missing ">"), - // so the tree can have any shape depending on the tokens before the current node. - // Instead, scanning for an identifier followed by a "<" before current node - // will typically give us better results than inspecting the tree. - // Note that we also balance out the already provided type arguments, arrays, object literals while doing so. - remainingLessThanTokens := 0 - nTypeArguments := 0 - for token != nil { - switch token.Kind { - case ast.KindLessThanToken: - // Found the beginning of the generic argument expression - token = astnav.FindPrecedingToken(sourceFile, token.Pos()) - if token != nil && token.Kind == ast.KindQuestionDotToken { - token = astnav.FindPrecedingToken(sourceFile, token.Pos()) - } - if token == nil || !ast.IsIdentifier(token) { - return nil - } - if remainingLessThanTokens == 0 { - if ast.IsDeclarationName(token) { - return nil - } - return &PossibleTypeArgumentInfo{ - called: token, - nTypeArguments: nTypeArguments, - } - } - remainingLessThanTokens-- - case ast.KindGreaterThanGreaterThanGreaterThanToken: - remainingLessThanTokens = +3 - case ast.KindGreaterThanGreaterThanToken: - remainingLessThanTokens = +2 - case ast.KindGreaterThanToken: - remainingLessThanTokens++ - case ast.KindCloseBraceToken: - // This can be object type, skip until we find the matching open brace token - // Skip until the matching open brace token - token = findPrecedingMatchingToken(token, ast.KindOpenBraceToken, sourceFile) - if token == nil { - return nil - } - case ast.KindCloseParenToken: - // This can be object type, skip until we find the matching open brace token - // Skip until the matching open brace token - token = findPrecedingMatchingToken(token, ast.KindOpenParenToken, sourceFile) - if token == nil { - return nil - } - case ast.KindCloseBracketToken: - // This can be object type, skip until we find the matching open brace token - // Skip until the matching open brace token - token = findPrecedingMatchingToken(token, ast.KindOpenBracketToken, sourceFile) - if token == nil { - return nil - } - case ast.KindCommaToken: - // Valid tokens in a type name. Skip. - nTypeArguments++ - case ast.KindEqualsGreaterThanToken, ast.KindIdentifier, ast.KindStringLiteral, ast.KindNumericLiteral, - ast.KindBigIntLiteral, ast.KindTrueKeyword, ast.KindFalseKeyword, ast.KindTypeOfKeyword, ast.KindExtendsKeyword, - ast.KindKeyOfKeyword, ast.KindDotToken, ast.KindBarToken, ast.KindQuestionToken, ast.KindColonToken: - // do nothing - default: - if !ast.IsTypeNode(token) { - // Invalid token in type - return nil - } - } - token = astnav.FindPrecedingToken(sourceFile, token.Pos()) - } - return nil -} - -func isNameOfModuleDeclaration(node *ast.Node) bool { - if node.Parent.Kind != ast.KindModuleDeclaration { - return false - } - return node.Parent.Name() == node -} - -func isExpressionOfExternalModuleImportEqualsDeclaration(node *ast.Node) bool { - return ast.IsExternalModuleImportEqualsDeclaration(node.Parent.Parent) && ast.GetExternalModuleImportEqualsDeclarationExpression(node.Parent.Parent) == node -} - -func isNamespaceReference(node *ast.Node) bool { - return isQualifiedNameNamespaceReference(node) || isPropertyAccessNamespaceReference(node) -} - -func isQualifiedNameNamespaceReference(node *ast.Node) bool { - root := node - isLastClause := true - if root.Parent.Kind == ast.KindQualifiedName { - for root.Parent != nil && root.Parent.Kind == ast.KindQualifiedName { - root = root.Parent - } - - isLastClause = root.AsQualifiedName().Right == node - } - - return root.Parent.Kind == ast.KindTypeReference && !isLastClause -} - -func isPropertyAccessNamespaceReference(node *ast.Node) bool { - root := node - isLastClause := true - if root.Parent.Kind == ast.KindPropertyAccessExpression { - for root.Parent != nil && root.Parent.Kind == ast.KindPropertyAccessExpression { - root = root.Parent - } - - isLastClause = root.Name() == node - } - - if !isLastClause && root.Parent.Kind == ast.KindExpressionWithTypeArguments && root.Parent.Parent.Kind == ast.KindHeritageClause { - decl := root.Parent.Parent.Parent - return (decl.Kind == ast.KindClassDeclaration && root.Parent.Parent.AsHeritageClause().Token == ast.KindImplementsKeyword) || - (decl.Kind == ast.KindInterfaceDeclaration && root.Parent.Parent.AsHeritageClause().Token == ast.KindExtendsKeyword) - } - - return false -} - -func isThis(node *ast.Node) bool { - switch node.Kind { - case ast.KindThisKeyword: - // case ast.KindThisType: TODO: GH#9267 - return true - case ast.KindIdentifier: - // 'this' as a parameter - return node.AsIdentifier().Text == "this" && node.Parent.Kind == ast.KindParameter - default: - return false - } -} - -func isTypeReference(node *ast.Node) bool { - if ast.IsRightSideOfQualifiedNameOrPropertyAccess(node) { - node = node.Parent - } - - switch node.Kind { - case ast.KindThisKeyword: - return !ast.IsExpressionNode(node) - case ast.KindThisType: - return true - } - - switch node.Parent.Kind { - case ast.KindTypeReference: - return true - case ast.KindImportType: - return !node.Parent.AsImportTypeNode().IsTypeOf - case ast.KindExpressionWithTypeArguments: - return ast.IsPartOfTypeNode(node.Parent) - } - - return false -} - -func isInRightSideOfInternalImportEqualsDeclaration(node *ast.Node) bool { - if node.Parent == nil { - return false - } - for node.Parent.Kind == ast.KindQualifiedName { - node = node.Parent - } - - return ast.IsInternalModuleImportEqualsDeclaration(node.Parent) && node.Parent.AsImportEqualsDeclaration().ModuleReference == node -} - -func (l *LanguageService) createLspRangeFromNode(node *ast.Node, file *ast.SourceFile) *lsproto.Range { - return l.createLspRangeFromBounds(scanner.GetTokenPosOfNode(node, file, false /*includeJSDoc*/), node.End(), file) -} - -func createRangeFromNode(node *ast.Node, file *ast.SourceFile) core.TextRange { - return core.NewTextRange(scanner.GetTokenPosOfNode(node, file, false /*includeJSDoc*/), node.End()) -} - -func (l *LanguageService) createLspRangeFromBounds(start, end int, file *ast.SourceFile) *lsproto.Range { - lspRange := l.converters.ToLSPRange(file, core.NewTextRange(start, end)) - return &lspRange -} - -func (l *LanguageService) createLspRangeFromRange(textRange core.TextRange, script Script) *lsproto.Range { - lspRange := l.converters.ToLSPRange(script, textRange) - return &lspRange -} - -func (l *LanguageService) createLspPosition(position int, file *ast.SourceFile) lsproto.Position { - return l.converters.PositionToLineAndCharacter(file, core.TextPos(position)) -} - -func quote(file *ast.SourceFile, preferences *UserPreferences, text string) string { - // Editors can pass in undefined or empty string - we want to infer the preference in those cases. - quotePreference := getQuotePreference(file, preferences) - quoted, _ := core.StringifyJson(text, "" /*prefix*/, "" /*indent*/) - if quotePreference == quotePreferenceSingle { - quoted = quoteReplacer.Replace(stringutil.StripQuotes(quoted)) - } - return quoted -} - -type quotePreference int - -const ( - quotePreferenceSingle quotePreference = iota - quotePreferenceDouble -) - -// !!! -func getQuotePreference(file *ast.SourceFile, preferences *UserPreferences) quotePreference { - return quotePreferenceDouble -} - -func isNonContextualKeyword(token ast.Kind) bool { - return ast.IsKeywordKind(token) && !ast.IsContextualKeyword(token) -} - -func probablyUsesSemicolons(file *ast.SourceFile) bool { - withSemicolon := 0 - withoutSemicolon := 0 - nStatementsToObserve := 5 - - var visit func(node *ast.Node) bool - visit = func(node *ast.Node) bool { - if node.Flags&ast.NodeFlagsReparsed != 0 { - return false - } - if lsutil.SyntaxRequiresTrailingSemicolonOrASI(node.Kind) { - lastToken := lsutil.GetLastToken(node, file) - if lastToken != nil && lastToken.Kind == ast.KindSemicolonToken { - withSemicolon++ - } else { - withoutSemicolon++ - } - } else if lsutil.SyntaxRequiresTrailingCommaOrSemicolonOrASI(node.Kind) { - lastToken := lsutil.GetLastToken(node, file) - if lastToken != nil && lastToken.Kind == ast.KindSemicolonToken { - withSemicolon++ - } else if lastToken != nil && lastToken.Kind != ast.KindCommaToken { - lastTokenLine, _ := scanner.GetECMALineAndCharacterOfPosition( - file, - astnav.GetStartOfNode(lastToken, file, false /*includeJSDoc*/)) - nextTokenLine, _ := scanner.GetECMALineAndCharacterOfPosition( - file, - scanner.GetRangeOfTokenAtPosition(file, lastToken.End()).Pos()) - // Avoid counting missing semicolon in single-line objects: - // `function f(p: { x: string /*no semicolon here is insignificant*/ }) {` - if lastTokenLine != nextTokenLine { - withoutSemicolon++ - } - } - } - - if withSemicolon+withoutSemicolon >= nStatementsToObserve { - return true - } - - return node.ForEachChild(visit) - } - - file.ForEachChild(visit) - - // One statement missing a semicolon isn't sufficient evidence to say the user - // doesn't want semicolons, because they may not even be done writing that statement. - if withSemicolon == 0 && withoutSemicolon <= 1 { - return true - } - - // If even 2/5 places have a semicolon, the user probably wants semicolons - if withoutSemicolon == 0 { - return true - } - return withSemicolon/withoutSemicolon > 1/nStatementsToObserve -} - -var typeKeywords *collections.Set[ast.Kind] = collections.NewSetFromItems( - ast.KindAnyKeyword, - ast.KindAssertsKeyword, - ast.KindBigIntKeyword, - ast.KindBooleanKeyword, - ast.KindFalseKeyword, - ast.KindInferKeyword, - ast.KindKeyOfKeyword, - ast.KindNeverKeyword, - ast.KindNullKeyword, - ast.KindNumberKeyword, - ast.KindObjectKeyword, - ast.KindReadonlyKeyword, - ast.KindStringKeyword, - ast.KindSymbolKeyword, - ast.KindTypeOfKeyword, - ast.KindTrueKeyword, - ast.KindVoidKeyword, - ast.KindUndefinedKeyword, - ast.KindUniqueKeyword, - ast.KindUnknownKeyword, -) - -func isTypeKeyword(kind ast.Kind) bool { - return typeKeywords.Has(kind) -} - -func isSeparator(node *ast.Node, candidate *ast.Node) bool { - return candidate != nil && node.Parent != nil && (candidate.Kind == ast.KindCommaToken || (candidate.Kind == ast.KindSemicolonToken && node.Parent.Kind == ast.KindObjectLiteralExpression)) -} - -// Returns a map of all names in the file to their positions. -// !!! cache this -func getNameTable(file *ast.SourceFile) map[string]int { - nameTable := make(map[string]int) - var walk func(node *ast.Node) bool - - walk = func(node *ast.Node) bool { - if ast.IsIdentifier(node) && !isTagName(node) && node.Text() != "" || - ast.IsStringOrNumericLiteralLike(node) && literalIsName(node) || - ast.IsPrivateIdentifier(node) { - text := node.Text() - if _, ok := nameTable[text]; ok { - nameTable[text] = -1 - } else { - nameTable[text] = node.Pos() - } - } - - node.ForEachChild(walk) - jsdocNodes := node.JSDoc(file) - for _, jsdoc := range jsdocNodes { - jsdoc.ForEachChild(walk) - } - return false - } - - file.ForEachChild(walk) - return nameTable -} - -// We want to store any numbers/strings if they were a name that could be -// related to a declaration. So, if we have 'import x = require("something")' -// then we want 'something' to be in the name table. Similarly, if we have -// "a['propname']" then we want to store "propname" in the name table. -func literalIsName(node *ast.NumericOrStringLikeLiteral) bool { - return ast.IsDeclarationName(node) || - node.Parent.Kind == ast.KindExternalModuleReference || - isArgumentOfElementAccessExpression(node) || - ast.IsLiteralComputedPropertyDeclarationName(node) -} - -func isLiteralNameOfPropertyDeclarationOrIndexAccess(node *ast.Node) bool { - // utilities - switch node.Parent.Kind { - case ast.KindPropertyDeclaration, - ast.KindPropertySignature, - ast.KindPropertyAssignment, - ast.KindEnumMember, - ast.KindMethodDeclaration, - ast.KindMethodSignature, - ast.KindGetAccessor, - ast.KindSetAccessor, - ast.KindModuleDeclaration: - return ast.GetNameOfDeclaration(node.Parent) == node - case ast.KindElementAccessExpression: - return node.Parent.AsElementAccessExpression().ArgumentExpression == node - case ast.KindComputedPropertyName: - return true - case ast.KindLiteralType: - return node.Parent.Parent.Kind == ast.KindIndexedAccessType - default: - return false - } -} - -func isObjectBindingElementWithoutPropertyName(bindingElement *ast.Node) bool { - return bindingElement.Kind == ast.KindBindingElement && - bindingElement.Parent.Kind == ast.KindObjectBindingPattern && - bindingElement.Name().Kind == ast.KindIdentifier && - bindingElement.PropertyName() == nil -} - -func isArgumentOfElementAccessExpression(node *ast.Node) bool { - return node != nil && node.Parent != nil && - node.Parent.Kind == ast.KindElementAccessExpression && - node.Parent.AsElementAccessExpression().ArgumentExpression == node -} - -func isRightSideOfPropertyAccess(node *ast.Node) bool { - return node.Parent.Kind == ast.KindPropertyAccessExpression && node.Parent.Name() == node -} - -func isStaticSymbol(symbol *ast.Symbol) bool { - if symbol.ValueDeclaration == nil { - return false - } - modifierFlags := symbol.ValueDeclaration.ModifierFlags() - return modifierFlags&ast.ModifierFlagsStatic != 0 -} - -func isImplementation(node *ast.Node) bool { - if node.Flags&ast.NodeFlagsAmbient != 0 { - return !(node.Kind == ast.KindInterfaceDeclaration || node.Kind == ast.KindTypeAliasDeclaration) - } - if ast.IsVariableLike(node) { - return ast.HasInitializer(node) - } - if ast.IsFunctionLikeDeclaration(node) { - return node.Body() != nil - } - return ast.IsClassLike(node) || ast.IsModuleOrEnumDeclaration(node) -} - -func isImplementationExpression(node *ast.Node) bool { - switch node.Kind { - case ast.KindParenthesizedExpression: - return isImplementationExpression(node.Expression()) - case ast.KindArrowFunction, ast.KindFunctionExpression, ast.KindObjectLiteralExpression, ast.KindClassExpression, ast.KindArrayLiteralExpression: - return true - default: - return false - } -} - -func isReadonlyTypeOperator(node *ast.Node) bool { - return node.Kind == ast.KindReadonlyKeyword && node.Parent.Kind == ast.KindTypeOperator && node.Parent.AsTypeOperatorNode().Operator == ast.KindReadonlyKeyword -} - -func isJumpStatementTarget(node *ast.Node) bool { - return node.Kind == ast.KindIdentifier && ast.IsBreakOrContinueStatement(node.Parent) && node.Parent.Label() == node -} - -func isLabelOfLabeledStatement(node *ast.Node) bool { - return node.Kind == ast.KindIdentifier && node.Parent.Kind == ast.KindLabeledStatement && node.Parent.Label() == node -} - -func findReferenceInPosition(refs []*ast.FileReference, pos int) *ast.FileReference { - return core.Find(refs, func(ref *ast.FileReference) bool { return ref.TextRange.ContainsInclusive(pos) }) -} - -func isTagName(node *ast.Node) bool { - return node.Parent != nil && ast.IsJSDocTag(node.Parent) && node.Parent.TagName() == node -} - -// Assumes `candidate.pos <= position` holds. -func positionBelongsToNode(candidate *ast.Node, position int, file *ast.SourceFile) bool { - if candidate.Pos() > position { - panic("Expected candidate.pos <= position") - } - return position < candidate.End() || !isCompletedNode(candidate, file) -} - -func isCompletedNode(n *ast.Node, sourceFile *ast.SourceFile) bool { - if n == nil || ast.NodeIsMissing(n) { - return false - } - - switch n.Kind { - case ast.KindClassDeclaration, - ast.KindInterfaceDeclaration, - ast.KindEnumDeclaration, - ast.KindObjectLiteralExpression, - ast.KindObjectBindingPattern, - ast.KindTypeLiteral, - ast.KindBlock, - ast.KindModuleBlock, - ast.KindCaseBlock, - ast.KindNamedImports, - ast.KindNamedExports: - return nodeEndsWith(n, ast.KindCloseBraceToken, sourceFile) - - case ast.KindCatchClause: - return isCompletedNode(n.AsCatchClause().Block, sourceFile) - - case ast.KindNewExpression: - if n.AsNewExpression().Arguments == nil { - return true - } - fallthrough - - case ast.KindCallExpression, - ast.KindParenthesizedExpression, - ast.KindParenthesizedType: - return nodeEndsWith(n, ast.KindCloseParenToken, sourceFile) - - case ast.KindFunctionType, - ast.KindConstructorType: - return isCompletedNode(n.Type(), sourceFile) - - case ast.KindConstructor, - ast.KindGetAccessor, - ast.KindSetAccessor, - ast.KindFunctionDeclaration, - ast.KindFunctionExpression, - ast.KindMethodDeclaration, - ast.KindMethodSignature, - ast.KindConstructSignature, - ast.KindCallSignature, - ast.KindArrowFunction: - if n.Body() != nil { - return isCompletedNode(n.Body(), sourceFile) - } - if n.Type() != nil { - return isCompletedNode(n.Type(), sourceFile) - } - // Even though type parameters can be unclosed, we can get away with - // having at least a closing paren. - return hasChildOfKind(n, ast.KindCloseParenToken, sourceFile) - - case ast.KindModuleDeclaration: - return n.AsModuleDeclaration().Body != nil && isCompletedNode(n.AsModuleDeclaration().Body, sourceFile) - - case ast.KindIfStatement: - if n.AsIfStatement().ElseStatement != nil { - return isCompletedNode(n.AsIfStatement().ElseStatement, sourceFile) - } - return isCompletedNode(n.AsIfStatement().ThenStatement, sourceFile) - - case ast.KindExpressionStatement: - return isCompletedNode(n.AsExpressionStatement().Expression, sourceFile) || - hasChildOfKind(n, ast.KindSemicolonToken, sourceFile) - - case ast.KindArrayLiteralExpression, - ast.KindArrayBindingPattern, - ast.KindElementAccessExpression, - ast.KindComputedPropertyName, - ast.KindTupleType: - return nodeEndsWith(n, ast.KindCloseBracketToken, sourceFile) - - case ast.KindIndexSignature: - if n.AsIndexSignatureDeclaration().Type != nil { - return isCompletedNode(n.AsIndexSignatureDeclaration().Type, sourceFile) - } - return hasChildOfKind(n, ast.KindCloseBracketToken, sourceFile) - - case ast.KindCaseClause, - ast.KindDefaultClause: - // there is no such thing as terminator token for CaseClause/DefaultClause so for simplicity always consider them non-completed - return false - - case ast.KindForStatement, - ast.KindForInStatement, - ast.KindForOfStatement, - ast.KindWhileStatement: - return isCompletedNode(n.Statement(), sourceFile) - case ast.KindDoStatement: - // rough approximation: if DoStatement has While keyword - then if node is completed is checking the presence of ')'; - if hasChildOfKind(n, ast.KindWhileKeyword, sourceFile) { - return nodeEndsWith(n, ast.KindCloseParenToken, sourceFile) - } - return isCompletedNode(n.AsDoStatement().Statement, sourceFile) - - case ast.KindTypeQuery: - return isCompletedNode(n.AsTypeQueryNode().ExprName, sourceFile) - - case ast.KindTypeOfExpression, - ast.KindDeleteExpression, - ast.KindVoidExpression, - ast.KindYieldExpression, - ast.KindSpreadElement: - return isCompletedNode(n.Expression(), sourceFile) - - case ast.KindTaggedTemplateExpression: - return isCompletedNode(n.AsTaggedTemplateExpression().Template, sourceFile) - - case ast.KindTemplateExpression: - if n.AsTemplateExpression().TemplateSpans == nil { - return false - } - lastSpan := core.LastOrNil(n.AsTemplateExpression().TemplateSpans.Nodes) - return isCompletedNode(lastSpan, sourceFile) - - case ast.KindTemplateSpan: - return ast.NodeIsPresent(n.AsTemplateSpan().Literal) - - case ast.KindExportDeclaration, - ast.KindImportDeclaration: - return ast.NodeIsPresent(n.ModuleSpecifier()) - - case ast.KindPrefixUnaryExpression: - return isCompletedNode(n.AsPrefixUnaryExpression().Operand, sourceFile) - - case ast.KindBinaryExpression: - return isCompletedNode(n.AsBinaryExpression().Right, sourceFile) - - case ast.KindConditionalExpression: - return isCompletedNode(n.AsConditionalExpression().WhenFalse, sourceFile) - - default: - return true - } -} - -// Checks if node ends with 'expectedLastToken'. -// If child at position 'length - 1' is 'SemicolonToken' it is skipped and 'expectedLastToken' is compared with child at position 'length - 2'. -func nodeEndsWith(n *ast.Node, expectedLastToken ast.Kind, sourceFile *ast.SourceFile) bool { - lastChildNode := lsutil.GetLastVisitedChild(n, sourceFile) - var lastNodeAndTokens []*ast.Node - var tokenStartPos int - if lastChildNode != nil { - lastNodeAndTokens = []*ast.Node{lastChildNode} - tokenStartPos = lastChildNode.End() - } else { - tokenStartPos = n.Pos() - } - scanner := scanner.GetScannerForSourceFile(sourceFile, tokenStartPos) - for startPos := tokenStartPos; startPos < n.End(); { - tokenKind := scanner.Token() - tokenFullStart := scanner.TokenFullStart() - tokenEnd := scanner.TokenEnd() - token := sourceFile.GetOrCreateToken(tokenKind, tokenFullStart, tokenEnd, n) - lastNodeAndTokens = append(lastNodeAndTokens, token) - startPos = tokenEnd - scanner.Scan() - } - if len(lastNodeAndTokens) == 0 { - return false - } - lastChild := lastNodeAndTokens[len(lastNodeAndTokens)-1] - if lastChild.Kind == expectedLastToken { - return true - } else if lastChild.Kind == ast.KindSemicolonToken && len(lastNodeAndTokens) > 1 { - return lastNodeAndTokens[len(lastNodeAndTokens)-2].Kind == expectedLastToken - } - return false -} - -func getContainingNodeIfInHeritageClause(node *ast.Node) *ast.Node { - if node.Kind == ast.KindIdentifier || node.Kind == ast.KindPropertyAccessExpression { - return getContainingNodeIfInHeritageClause(node.Parent) - } - if node.Kind == ast.KindExpressionWithTypeArguments && (ast.IsClassLike(node.Parent.Parent) || node.Parent.Parent.Kind == ast.KindInterfaceDeclaration) { - return node.Parent.Parent - } - return nil -} - -func getContainerNode(node *ast.Node) *ast.Node { - for parent := node.Parent; parent != nil; parent = parent.Parent { - switch parent.Kind { - case ast.KindSourceFile, ast.KindMethodDeclaration, ast.KindMethodSignature, ast.KindFunctionDeclaration, ast.KindFunctionExpression, - ast.KindGetAccessor, ast.KindSetAccessor, ast.KindClassDeclaration, ast.KindInterfaceDeclaration, ast.KindEnumDeclaration, ast.KindModuleDeclaration: - return parent - } - } - return nil -} - -func getAdjustedLocation(node *ast.Node, forRename bool, sourceFile *ast.SourceFile) *ast.Node { - // todo: check if this function needs to be changed for jsdoc updates - - parent := node.Parent - // /**/ [|name|] ... - // /**/ [|name|] ... - // /**/ [|name|] ... - // /**/import [|name|] = ... - // - // NOTE: If the node is a modifier, we don't adjust its location if it is the `default` modifier as that is handled - // specially by `getSymbolAtLocation`. - isModifier := func(node *ast.Node) bool { - if ast.IsModifier(node) && (forRename || node.Kind != ast.KindDefaultKeyword) { - return ast.CanHaveModifiers(parent) && slices.Contains(parent.Modifiers().NodeList.Nodes, node) - } - switch node.Kind { - case ast.KindClassKeyword: - return ast.IsClassDeclaration(parent) || ast.IsClassExpression(node) - case ast.KindFunctionKeyword: - return ast.IsFunctionDeclaration(parent) || ast.IsFunctionExpression(node) - case ast.KindInterfaceKeyword: - return ast.IsInterfaceDeclaration(parent) - case ast.KindEnumKeyword: - return ast.IsEnumDeclaration(parent) - case ast.KindTypeKeyword: - return ast.IsTypeAliasDeclaration(parent) - case ast.KindNamespaceKeyword, ast.KindModuleKeyword: - return ast.IsModuleDeclaration(parent) - case ast.KindImportKeyword: - return ast.IsImportEqualsDeclaration(parent) - case ast.KindGetKeyword: - return ast.IsGetAccessorDeclaration(parent) - case ast.KindSetKeyword: - return ast.IsSetAccessorDeclaration(parent) - } - return false - } - if isModifier(node) { - if sourceFile == nil { - sourceFile = ast.GetSourceFileOfNode(node) - } - if location := getAdjustedLocationForDeclaration(parent, forRename, sourceFile); location != nil { - return location - } - } - - // /**/ ... - if parent.Kind == ast.KindTypeParameter { - if constraint := parent.AsTypeParameter().Constraint; constraint != nil && constraint.Kind == ast.KindTypeReference { - return constraint.AsTypeReference().TypeName - } - } - // ... T /**/extends [|U|] ? ... - if parent.Kind == ast.KindConditionalType { - if extendsType := parent.AsConditionalTypeNode().ExtendsType; extendsType != nil && extendsType.Kind == ast.KindTypeReference { - return extendsType.AsTypeReference().TypeName - } - } - } - // ... T extends /**/infer [|U|] ? ... - if node.Kind == ast.KindInferKeyword && parent.Kind == ast.KindInferType { - return parent.AsInferTypeNode().TypeParameter.Name() - } - // { [ [|K|] /**/in keyof T]: ... } - if node.Kind == ast.KindInKeyword && parent.Kind == ast.KindTypeParameter && parent.Parent.Kind == ast.KindMappedType { - return parent.Name() - } - // /**/keyof [|T|] - if node.Kind == ast.KindKeyOfKeyword && parent.Kind == ast.KindTypeOperator && parent.AsTypeOperatorNode().Operator == ast.KindKeyOfKeyword { - if parentType := parent.Type(); parentType != nil && parentType.Kind == ast.KindTypeReference { - return parentType.AsTypeReferenceNode().TypeName - } - } - // /**/readonly [|name|][] - if node.Kind == ast.KindReadonlyKeyword && parent.Kind == ast.KindTypeOperator && parent.AsTypeOperatorNode().Operator == ast.KindReadonlyKeyword { - if parentType := parent.Type(); parentType != nil && parentType.Kind == ast.KindArrayType && parentType.AsArrayTypeNode().ElementType.Kind == ast.KindTypeReference { - return parentType.AsArrayTypeNode().ElementType.AsTypeReferenceNode().TypeName - } - } - - if !forRename { - // /**/new [|name|] - // /**/void [|name|] - // /**/void obj.[|name|] - // /**/typeof [|name|] - // /**/typeof obj.[|name|] - // /**/await [|name|] - // /**/await obj.[|name|] - // /**/yield [|name|] - // /**/yield obj.[|name|] - // /**/delete obj.[|name|] - if node.Kind == ast.KindNewKeyword && parent.Kind == ast.KindNewExpression || - node.Kind == ast.KindVoidKeyword && parent.Kind == ast.KindVoidExpression || - node.Kind == ast.KindTypeOfKeyword && parent.Kind == ast.KindTypeOfExpression || - node.Kind == ast.KindAwaitKeyword && parent.Kind == ast.KindAwaitExpression || - node.Kind == ast.KindYieldKeyword && parent.Kind == ast.KindYieldExpression || - node.Kind == ast.KindDeleteKeyword && parent.Kind == ast.KindDeleteExpression { - if expr := parent.Expression(); expr != nil { - return ast.SkipOuterExpressions(expr, ast.OEKAll) - } - } - - // left /**/in [|name|] - // left /**/instanceof [|name|] - if (node.Kind == ast.KindInKeyword || node.Kind == ast.KindInstanceOfKeyword) && parent.Kind == ast.KindBinaryExpression && parent.AsBinaryExpression().OperatorToken == node { - return ast.SkipOuterExpressions(parent.AsBinaryExpression().Right, ast.OEKAll) - } - - // left /**/as [|name|] - if node.Kind == ast.KindAsKeyword && parent.Kind == ast.KindAsExpression { - if asExprType := parent.Type(); asExprType != nil && asExprType.Kind == ast.KindTypeReference { - return asExprType.AsTypeReferenceNode().TypeName - } - } - - // for (... /**/in [|name|]) - // for (... /**/of [|name|]) - if node.Kind == ast.KindInKeyword && parent.Kind == ast.KindForInStatement || - node.Kind == ast.KindOfKeyword && parent.Kind == ast.KindForOfStatement { - return ast.SkipOuterExpressions(parent.AsForInOrOfStatement().Expression, ast.OEKAll) - } - } - - return node -} - -func getAdjustedLocationForDeclaration(node *ast.Node, forRename bool, sourceFile *ast.SourceFile) *ast.Node { - if node.Name() != nil { - return node.Name() - } - if forRename { - return nil - } - switch node.Kind { - case ast.KindClassDeclaration, ast.KindFunctionDeclaration: - // for class and function declarations, use the `default` modifier - // when the declaration is unnamed. - if node.Modifiers() != nil { - return core.Find(node.Modifiers().NodeList.Nodes, func(*ast.Node) bool { return node.Kind == ast.KindDefaultKeyword }) - } - case ast.KindClassExpression: - // for class expressions, use the `class` keyword when the class is unnamed - return findChildOfKind(node, ast.KindClassKeyword, sourceFile) - case ast.KindFunctionExpression: - // for function expressions, use the `function` keyword when the function is unnamed - return findChildOfKind(node, ast.KindFunctionKeyword, sourceFile) - case ast.KindConstructor: - return node - } - return nil -} - -func getAdjustedLocationForImportDeclaration(node *ast.ImportDeclaration, forRename bool) *ast.Node { - if node.ImportClause != nil { - if name := node.ImportClause.Name(); name != nil { - if node.ImportClause.AsImportClause().NamedBindings != nil { - // do not adjust if we have both a name and named bindings - return nil - } - // /**/import [|name|] from ...; - // import /**/type [|name|] from ...; - return node.ImportClause.Name() - } - - // /**/import { [|name|] } from ...; - // /**/import { propertyName as [|name|] } from ...; - // /**/import * as [|name|] from ...; - // import /**/type { [|name|] } from ...; - // import /**/type { propertyName as [|name|] } from ...; - // import /**/type * as [|name|] from ...; - if namedBindings := node.ImportClause.AsImportClause().NamedBindings; namedBindings != nil { - switch namedBindings.Kind { - case ast.KindNamedImports: - // do nothing if there is more than one binding - elements := namedBindings.AsNamedImports().Elements - if len(elements.Nodes) != 1 { - return nil - } - return elements.Nodes[0].Name() - - case ast.KindNamespaceImport: - return namedBindings.Name() - - } - } - } - if !forRename { - // /**/import "[|module|]"; - // /**/import ... from "[|module|]"; - // import /**/type ... from "[|module|]"; - return node.ModuleSpecifier - } - return nil -} - -func getAdjustedLocationForExportDeclaration(node *ast.ExportDeclaration, forRename bool) *ast.Node { - if node.ExportClause != nil { - // /**/export { [|name|] } ... - // /**/export { propertyName as [|name|] } ... - // /**/export * as [|name|] ... - // export /**/type { [|name|] } from ... - // export /**/type { propertyName as [|name|] } from ... - // export /**/type * as [|name|] ... - switch node.ExportClause.Kind { - case ast.KindNamedExports: - // do nothing if there is more than one binding - elements := node.ExportClause.AsNamedExports().Elements - if len(elements.Nodes) != 1 { - return nil - } - return elements.Nodes[0].Name() - case ast.KindNamespaceExport: - return node.ExportClause.Name() - } - } - if !forRename { - // /**/export * from "[|module|]"; - // export /**/type * from "[|module|]"; - return node.ModuleSpecifier - } - return nil -} - -func getMeaningFromLocation(node *ast.Node) ast.SemanticMeaning { - // todo: check if this function needs to be changed for jsdoc updates - node = getAdjustedLocation(node, false /*forRename*/, nil) - parent := node.Parent - switch { - case ast.IsSourceFile(node): - return ast.SemanticMeaningValue - case ast.NodeKindIs(node, ast.KindExportAssignment, ast.KindExportSpecifier, ast.KindExternalModuleReference, ast.KindImportSpecifier, ast.KindImportClause) || parent.Kind == ast.KindImportEqualsDeclaration && node == parent.Name(): - return ast.SemanticMeaningAll - case isInRightSideOfInternalImportEqualsDeclaration(node): - // import a = |b|; // Namespace - // import a = |b.c|; // Value, type, namespace - // import a = |b.c|.d; // Namespace - name := node - if node.Kind != ast.KindQualifiedName { - name = core.IfElse(node.Parent.Kind == ast.KindQualifiedName && node.Parent.AsQualifiedName().Right == node, node.Parent, nil) - } - if name == nil || name.Parent.Kind == ast.KindImportEqualsDeclaration { - return ast.SemanticMeaningNamespace - } - return ast.SemanticMeaningAll - case ast.IsDeclarationName(node): - return getMeaningFromDeclaration(parent) - case ast.IsEntityName(node) && ast.IsJSDocNameReferenceContext(node): - return ast.SemanticMeaningAll - case isTypeReference(node): - return ast.SemanticMeaningType - case isNamespaceReference(node): - return ast.SemanticMeaningNamespace - case ast.IsTypeParameterDeclaration(parent): - return ast.SemanticMeaningType - case ast.IsLiteralTypeNode(parent): - // This might be T["name"], which is actually referencing a property and not a type. So allow both meanings. - return ast.SemanticMeaningType | ast.SemanticMeaningValue - default: - return ast.SemanticMeaningValue - } -} - -func getMeaningFromDeclaration(node *ast.Node) ast.SemanticMeaning { - switch node.Kind { - case ast.KindVariableDeclaration, ast.KindCommonJSExport, ast.KindParameter, ast.KindBindingElement, - ast.KindPropertyDeclaration, ast.KindPropertySignature, ast.KindPropertyAssignment, ast.KindShorthandPropertyAssignment, - ast.KindMethodDeclaration, ast.KindMethodSignature, ast.KindConstructor, ast.KindGetAccessor, ast.KindSetAccessor, - ast.KindFunctionDeclaration, ast.KindFunctionExpression, ast.KindArrowFunction, ast.KindCatchClause, ast.KindJsxAttribute: - return ast.SemanticMeaningValue - - case ast.KindTypeParameter, ast.KindInterfaceDeclaration, ast.KindTypeAliasDeclaration, ast.KindJSTypeAliasDeclaration, ast.KindTypeLiteral: - return ast.SemanticMeaningType - - case ast.KindEnumMember, ast.KindClassDeclaration: - return ast.SemanticMeaningValue | ast.SemanticMeaningType - - case ast.KindModuleDeclaration: - if ast.IsAmbientModule(node) { - return ast.SemanticMeaningNamespace | ast.SemanticMeaningValue - } else if ast.GetModuleInstanceState(node) == ast.ModuleInstanceStateInstantiated { - return ast.SemanticMeaningNamespace | ast.SemanticMeaningValue - } else { - return ast.SemanticMeaningNamespace - } - - case ast.KindEnumDeclaration, ast.KindNamedImports, ast.KindImportSpecifier, ast.KindImportEqualsDeclaration, ast.KindImportDeclaration, - ast.KindJSImportDeclaration, ast.KindExportAssignment, ast.KindJSExportAssignment, ast.KindExportDeclaration: - return ast.SemanticMeaningAll - - // An external module can be a Value - case ast.KindSourceFile: - return ast.SemanticMeaningNamespace | ast.SemanticMeaningValue - } - - return ast.SemanticMeaningAll -} - -func getIntersectingMeaningFromDeclarations(node *ast.Node, symbol *ast.Symbol, defaultMeaning ast.SemanticMeaning) ast.SemanticMeaning { - if node == nil { - return defaultMeaning - } - - meaning := getMeaningFromLocation(node) - declarations := symbol.Declarations - if len(declarations) == 0 { - return meaning - } - - lastIterationMeaning := meaning - - // !!! TODO check if the port is correct and the for loop is needed - iteration := func(m ast.SemanticMeaning) ast.SemanticMeaning { - for _, declaration := range declarations { - declarationMeaning := getMeaningFromDeclaration(declaration) - - if declarationMeaning&m != 0 { - m |= declarationMeaning - } - } - return m - } - meaning = iteration(meaning) - - for meaning != lastIterationMeaning { - // The result is order-sensitive, for instance if initialMeaning == Namespace, and declarations = [class, instantiated module] - // we need to consider both as the initialMeaning intersects with the module in the namespace space, and the module - // intersects with the class in the value space. - // To achieve that we will keep iterating until the result stabilizes. - - // Remember the last meaning - lastIterationMeaning = meaning - meaning = iteration(meaning) - } - - return meaning -} - -// Returns the node in an `extends` or `implements` clause of a class or interface. -func getAllSuperTypeNodes(node *ast.Node) []*ast.TypeNode { - if ast.IsInterfaceDeclaration(node) { - return ast.GetHeritageElements(node, ast.KindExtendsKeyword) - } - if ast.IsClassLike(node) { - return append( - core.SingleElementSlice(ast.GetClassExtendsHeritageElement(node)), - ast.GetImplementsTypeNodes(node)..., - ) - } - return nil -} - -func getParentSymbolsOfPropertyAccess(location *ast.Node, symbol *ast.Symbol, ch *checker.Checker) []*ast.Symbol { - if !isRightSideOfPropertyAccess(location) { - return nil - } - lhsType := ch.GetTypeAtLocation(location.Parent.Expression()) - if lhsType == nil { - return nil - } - var possibleSymbols []*checker.Type - if lhsType.Flags()&checker.TypeFlagsUnionOrIntersection != 0 { - possibleSymbols = lhsType.Types() - } else if lhsType.Symbol() != symbol.Parent { - possibleSymbols = []*checker.Type{lhsType} - } - return core.MapNonNil(possibleSymbols, func(t *checker.Type) *ast.Symbol { - if t.Symbol() != nil && t.Symbol().Flags&(ast.SymbolFlagsClass|ast.SymbolFlagsInterface) != 0 { - return t.Symbol() - } - return nil - }) -} - -// Find symbol of the given property-name and add the symbol to the given result array -// @param symbol a symbol to start searching for the given propertyName -// @param propertyName a name of property to search for -// @param cb a cache of symbol from previous iterations of calling this function to prevent infinite revisiting of the same symbol. -// -// The value of previousIterationSymbol is undefined when the function is first called. -func getPropertySymbolsFromBaseTypes(symbol *ast.Symbol, propertyName string, checker *checker.Checker, cb func(base *ast.Symbol) *ast.Symbol) *ast.Symbol { - var seen collections.Set[*ast.Symbol] - var recur func(*ast.Symbol) *ast.Symbol - recur = func(symbol *ast.Symbol) *ast.Symbol { - // Use `addToSeen` to ensure we don't infinitely recurse in this situation: - // interface C extends C { - // /*findRef*/propName: string; - // } - if symbol.Flags&(ast.SymbolFlagsClass|ast.SymbolFlagsInterface) == 0 || !seen.AddIfAbsent(symbol) { - return nil - } - for _, declaration := range symbol.Declarations { - for _, typeReference := range getAllSuperTypeNodes(declaration) { - if propertyType := checker.GetTypeAtLocation(typeReference); propertyType != nil && propertyType.Symbol() != nil { - // Visit the typeReference as well to see if it directly or indirectly uses that property - if propertySymbol := checker.GetPropertyOfType(propertyType, propertyName); propertySymbol != nil { - for _, rootSymbol := range checker.GetRootSymbols(propertySymbol) { - if result := cb(rootSymbol); result != nil { - return result - } - } - } - if result := recur(propertyType.Symbol()); result != nil { - return result - } - } - } - } - return nil - } - return recur(symbol) -} - -func getPropertySymbolFromBindingElement(checker *checker.Checker, bindingElement *ast.Node) *ast.Symbol { - if typeOfPattern := checker.GetTypeAtLocation(bindingElement.Parent); typeOfPattern != nil { - return checker.GetPropertyOfType(typeOfPattern, bindingElement.Name().Text()) - } - return nil -} - -func getPropertySymbolOfObjectBindingPatternWithoutPropertyName(symbol *ast.Symbol, checker *checker.Checker) *ast.Symbol { - bindingElement := ast.GetDeclarationOfKind(symbol, ast.KindBindingElement) - if bindingElement != nil && isObjectBindingElementWithoutPropertyName(bindingElement) { - return getPropertySymbolFromBindingElement(checker, bindingElement) - } - return nil -} - -func getTargetLabel(referenceNode *ast.Node, labelName string) *ast.Node { - // todo: rewrite as `ast.FindAncestor` - for referenceNode != nil { - if referenceNode.Kind == ast.KindLabeledStatement && referenceNode.AsLabeledStatement().Label.Text() == labelName { - return referenceNode.AsLabeledStatement().Label - } - referenceNode = referenceNode.Parent - } - return nil -} - -func skipConstraint(t *checker.Type, typeChecker *checker.Checker) *checker.Type { - if t.IsTypeParameter() { - c := typeChecker.GetBaseConstraintOfType(t) - if c != nil { - return c - } - } - return t -} - -type caseClauseTrackerState struct { - existingStrings collections.Set[string] - existingNumbers collections.Set[jsnum.Number] - existingBigInts collections.Set[jsnum.PseudoBigInt] -} - -// string | jsnum.Number -type trackerAddValue = any - -// string | jsnum.Number | jsnum.PseudoBigInt -type trackerHasValue = any - -type caseClauseTracker interface { - addValue(value trackerAddValue) - hasValue(value trackerHasValue) bool -} - -func (c *caseClauseTrackerState) addValue(value trackerAddValue) { - switch v := value.(type) { - case string: - c.existingStrings.Add(v) - case jsnum.Number: - c.existingNumbers.Add(v) - default: - panic(fmt.Sprintf("Unsupported type: %T", v)) - } -} - -func (c *caseClauseTrackerState) hasValue(value trackerHasValue) bool { - switch v := value.(type) { - case string: - return c.existingStrings.Has(v) - case jsnum.Number: - return c.existingNumbers.Has(v) - case jsnum.PseudoBigInt: - return c.existingBigInts.Has(v) - default: - panic(fmt.Sprintf("Unsupported type: %T", v)) - } -} - -func newCaseClauseTracker(typeChecker *checker.Checker, clauses []*ast.CaseOrDefaultClauseNode) caseClauseTracker { - c := &caseClauseTrackerState{ - existingStrings: collections.Set[string]{}, - existingNumbers: collections.Set[jsnum.Number]{}, - existingBigInts: collections.Set[jsnum.PseudoBigInt]{}, - } - for _, clause := range clauses { - if !ast.IsDefaultClause(clause) { - expression := ast.SkipParentheses(clause.Expression()) - if ast.IsLiteralExpression(expression) { - switch expression.Kind { - case ast.KindNoSubstitutionTemplateLiteral, ast.KindStringLiteral: - c.existingStrings.Add(expression.Text()) - case ast.KindNumericLiteral: - c.existingNumbers.Add(jsnum.FromString(expression.Text())) - case ast.KindBigIntLiteral: - c.existingBigInts.Add(jsnum.ParseValidBigInt(expression.Text())) - } - } else { - symbol := typeChecker.GetSymbolAtLocation(clause.Expression()) - if symbol != nil && symbol.ValueDeclaration != nil && ast.IsEnumMember(symbol.ValueDeclaration) { - enumValue := typeChecker.GetConstantValue(symbol.ValueDeclaration) - if enumValue != nil { - c.addValue(enumValue) - } - } - } - } - } - return c -} - -func RangeContainsRange(r1 core.TextRange, r2 core.TextRange) bool { - return startEndContainsRange(r1.Pos(), r1.End(), r2) -} - -func startEndContainsRange(start int, end int, textRange core.TextRange) bool { - return start <= textRange.Pos() && end >= textRange.End() -} - -func getPossibleGenericSignatures(called *ast.Expression, typeArgumentCount int, c *checker.Checker) []*checker.Signature { - typeAtLocation := c.GetTypeAtLocation(called) - if ast.IsOptionalChain(called.Parent) { - typeAtLocation = removeOptionality(typeAtLocation, ast.IsOptionalChainRoot(called.Parent), true /*isOptionalChain*/, c) - } - var signatures []*checker.Signature - if ast.IsNewExpression(called.Parent) { - signatures = c.GetSignaturesOfType(typeAtLocation, checker.SignatureKindConstruct) - } else { - signatures = c.GetSignaturesOfType(typeAtLocation, checker.SignatureKindCall) - } - return core.Filter(signatures, func(s *checker.Signature) bool { - return s.TypeParameters() != nil && len(s.TypeParameters()) >= typeArgumentCount - }) -} - -func removeOptionality(t *checker.Type, isOptionalExpression bool, isOptionalChain bool, c *checker.Checker) *checker.Type { - if isOptionalExpression { - return c.GetNonNullableType(t) - } else if isOptionalChain { - return c.GetNonOptionalType(t) - } - return t -} - -func isNoSubstitutionTemplateLiteral(node *ast.Node) bool { - return node.Kind == ast.KindNoSubstitutionTemplateLiteral -} - -func isTaggedTemplateExpression(node *ast.Node) bool { - return node.Kind == ast.KindTaggedTemplateExpression -} - -func isInsideTemplateLiteral(node *ast.Node, position int, sourceFile *ast.SourceFile) bool { - return ast.IsTemplateLiteralKind(node.Kind) && (scanner.GetTokenPosOfNode(node, sourceFile, false) < position && position < node.End() || (ast.IsUnterminatedLiteral(node) && position == node.End())) -} - -// Pseudo-literals -func isTemplateHead(node *ast.Node) bool { - return node.Kind == ast.KindTemplateHead -} - -func isTemplateTail(node *ast.Node) bool { - return node.Kind == ast.KindTemplateTail -} - -func findPrecedingMatchingToken(token *ast.Node, matchingTokenKind ast.Kind, sourceFile *ast.SourceFile) *ast.Node { - closeTokenText := scanner.TokenToString(token.Kind) - matchingTokenText := scanner.TokenToString(matchingTokenKind) - // Text-scan based fast path - can be bamboozled by comments and other trivia, but often provides - // a good, fast approximation without too much extra work in the cases where it fails. - bestGuessIndex := strings.LastIndex(sourceFile.Text(), matchingTokenText) - if bestGuessIndex == -1 { - return nil // if the token text doesn't appear in the file, there can't be a match - super fast bail - } - // we can only use the textual result directly if we didn't have to count any close tokens within the range - if strings.LastIndex(sourceFile.Text(), closeTokenText) < bestGuessIndex { - nodeAtGuess := astnav.FindPrecedingToken(sourceFile, bestGuessIndex+1) - if nodeAtGuess != nil && nodeAtGuess.Kind == matchingTokenKind { - return nodeAtGuess - } - } - tokenKind := token.Kind - remainingMatchingTokens := 0 - for { - preceding := astnav.FindPrecedingToken(sourceFile, token.Pos()) - if preceding == nil { - return nil - } - token = preceding - switch token.Kind { - case matchingTokenKind: - if remainingMatchingTokens == 0 { - return token - } - remainingMatchingTokens-- - case tokenKind: - remainingMatchingTokens++ - } - } -} - -func findContainingList(node *ast.Node, file *ast.SourceFile) *ast.NodeList { - // The node might be a list element (nonsynthetic) or a comma (synthetic). Either way, it will - // be parented by the container of the SyntaxList, not the SyntaxList itself. - var list *ast.NodeList - visitNode := func(n *ast.Node, visitor *ast.NodeVisitor) *ast.Node { - return n - } - visitNodes := func(nodes *ast.NodeList, visitor *ast.NodeVisitor) *ast.NodeList { - if nodes != nil && RangeContainsRange(nodes.Loc, node.Loc) { - list = nodes - } - return nodes - } - astnav.VisitEachChildAndJSDoc(node.Parent, file, visitNode, visitNodes) - return list -} - -func getLeadingCommentRangesOfNode(node *ast.Node, file *ast.SourceFile) iter.Seq[ast.CommentRange] { - if node.Kind == ast.KindJsxText { - return nil - } - return scanner.GetLeadingCommentRanges(&ast.NodeFactory{}, file.Text(), node.Pos()) -} - -// Equivalent to Strada's `node.getChildren()` for non-JSDoc nodes. -func getChildrenFromNonJSDocNode(node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node { - var childNodes []*ast.Node - node.ForEachChild(func(child *ast.Node) bool { - childNodes = append(childNodes, child) - return false - }) - var children []*ast.Node - pos := node.Pos() - for _, child := range childNodes { - scanner := scanner.GetScannerForSourceFile(sourceFile, pos) - for pos < child.Pos() { - token := scanner.Token() - tokenFullStart := scanner.TokenFullStart() - tokenEnd := scanner.TokenEnd() - children = append(children, sourceFile.GetOrCreateToken(token, tokenFullStart, tokenEnd, node)) - pos = tokenEnd - scanner.Scan() - } - children = append(children, child) - pos = child.End() - } - scanner := scanner.GetScannerForSourceFile(sourceFile, pos) - for pos < node.End() { - token := scanner.Token() - tokenFullStart := scanner.TokenFullStart() - tokenEnd := scanner.TokenEnd() - children = append(children, sourceFile.GetOrCreateToken(token, tokenFullStart, tokenEnd, node)) - pos = tokenEnd - scanner.Scan() - } - return children -} - -// Returns the containing object literal property declaration given a possible name node, e.g. "a" in x = { "a": 1 } -func getContainingObjectLiteralElement(node *ast.Node) *ast.Node { - element := getContainingObjectLiteralElementWorker(node) - if element != nil && (ast.IsObjectLiteralExpression(element.Parent) || ast.IsJsxAttributes(element.Parent)) { - return element - } - return nil -} - -func getContainingObjectLiteralElementWorker(node *ast.Node) *ast.Node { - switch node.Kind { - case ast.KindStringLiteral, ast.KindNoSubstitutionTemplateLiteral, ast.KindNumericLiteral: - if node.Parent.Kind == ast.KindComputedPropertyName { - if ast.IsObjectLiteralElement(node.Parent.Parent) { - return node.Parent.Parent - } - return nil - } - fallthrough - case ast.KindIdentifier: - if ast.IsObjectLiteralElement(node.Parent) && (node.Parent.Parent.Kind == ast.KindObjectLiteralExpression || node.Parent.Parent.Kind == ast.KindJsxAttributes) && node.Parent.Name() == node { - return node.Parent - } - } - return nil -} - -// Return a function that returns true if the given node has not been seen -func nodeSeenTracker() func(*ast.Node) bool { - var seen collections.Set[*ast.Node] - return func(node *ast.Node) bool { - return seen.AddIfAbsent(node) - } -} diff --git a/kitcom/internal/tsgo/lsutil/asi.go b/kitcom/internal/tsgo/lsutil/asi.go deleted file mode 100644 index 93e3daf..0000000 --- a/kitcom/internal/tsgo/lsutil/asi.go +++ /dev/null @@ -1,104 +0,0 @@ -package lsutil - -import ( - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner" -) - -func PositionIsASICandidate(pos int, context *ast.Node, file *ast.SourceFile) bool { - contextAncestor := ast.FindAncestorOrQuit(context, func(ancestor *ast.Node) ast.FindAncestorResult { - if ancestor.End() != pos { - return ast.FindAncestorQuit - } - - return ast.ToFindAncestorResult(SyntaxMayBeASICandidate(ancestor.Kind)) - }) - - return contextAncestor != nil && NodeIsASICandidate(contextAncestor, file) -} - -func SyntaxMayBeASICandidate(kind ast.Kind) bool { - return SyntaxRequiresTrailingCommaOrSemicolonOrASI(kind) || - SyntaxRequiresTrailingFunctionBlockOrSemicolonOrASI(kind) || - SyntaxRequiresTrailingModuleBlockOrSemicolonOrASI(kind) || - SyntaxRequiresTrailingSemicolonOrASI(kind) -} - -func SyntaxRequiresTrailingCommaOrSemicolonOrASI(kind ast.Kind) bool { - return kind == ast.KindCallSignature || - kind == ast.KindConstructSignature || - kind == ast.KindIndexSignature || - kind == ast.KindPropertySignature || - kind == ast.KindMethodSignature -} - -func SyntaxRequiresTrailingFunctionBlockOrSemicolonOrASI(kind ast.Kind) bool { - return kind == ast.KindFunctionDeclaration || - kind == ast.KindConstructor || - kind == ast.KindMethodDeclaration || - kind == ast.KindGetAccessor || - kind == ast.KindSetAccessor -} - -func SyntaxRequiresTrailingModuleBlockOrSemicolonOrASI(kind ast.Kind) bool { - return kind == ast.KindModuleDeclaration -} - -func SyntaxRequiresTrailingSemicolonOrASI(kind ast.Kind) bool { - return kind == ast.KindVariableStatement || - kind == ast.KindExpressionStatement || - kind == ast.KindDoStatement || - kind == ast.KindContinueStatement || - kind == ast.KindBreakStatement || - kind == ast.KindReturnStatement || - kind == ast.KindThrowStatement || - kind == ast.KindDebuggerStatement || - kind == ast.KindPropertyDeclaration || - kind == ast.KindTypeAliasDeclaration || - kind == ast.KindImportDeclaration || - kind == ast.KindImportEqualsDeclaration || - kind == ast.KindExportDeclaration || - kind == ast.KindNamespaceExportDeclaration || - kind == ast.KindExportAssignment -} - -func NodeIsASICandidate(node *ast.Node, file *ast.SourceFile) bool { - lastToken := GetLastToken(node, file) - if lastToken != nil && lastToken.Kind == ast.KindSemicolonToken { - return false - } - - if SyntaxRequiresTrailingCommaOrSemicolonOrASI(node.Kind) { - if lastToken != nil && lastToken.Kind == ast.KindCommaToken { - return false - } - } else if SyntaxRequiresTrailingModuleBlockOrSemicolonOrASI(node.Kind) { - lastChild := GetLastChild(node, file) - if lastChild != nil && ast.IsModuleBlock(lastChild) { - return false - } - } else if SyntaxRequiresTrailingFunctionBlockOrSemicolonOrASI(node.Kind) { - lastChild := GetLastChild(node, file) - if lastChild != nil && ast.IsFunctionBlock(lastChild) { - return false - } - } else if !SyntaxRequiresTrailingSemicolonOrASI(node.Kind) { - return false - } - - // See comment in parser's `parseDoStatement` - if node.Kind == ast.KindDoStatement { - return true - } - - topNode := ast.FindAncestor(node, func(ancestor *ast.Node) bool { return ancestor.Parent == nil }) - nextToken := astnav.FindNextToken(node, topNode, file) - if nextToken == nil || nextToken.Kind == ast.KindCloseBraceToken { - return true - } - - startLine, _ := scanner.GetECMALineAndCharacterOfPosition(file, node.End()) - endLine, _ := scanner.GetECMALineAndCharacterOfPosition(file, astnav.GetStartOfNode(nextToken, file, false /*includeJSDoc*/)) - return startLine != endLine -} diff --git a/kitcom/internal/tsgo/lsutil/children.go b/kitcom/internal/tsgo/lsutil/children.go deleted file mode 100644 index f9dfa01..0000000 --- a/kitcom/internal/tsgo/lsutil/children.go +++ /dev/null @@ -1,130 +0,0 @@ -package lsutil - -import ( - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner" -) - -// Replaces last(node.getChildren(sourceFile)) -func GetLastChild(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node { - lastChildNode := GetLastVisitedChild(node, sourceFile) - if ast.IsJSDocSingleCommentNode(node) && lastChildNode == nil { - return nil - } - var tokenStartPos int - if lastChildNode != nil { - tokenStartPos = lastChildNode.End() - } else { - tokenStartPos = node.Pos() - } - var lastToken *ast.Node - scanner := scanner.GetScannerForSourceFile(sourceFile, tokenStartPos) - for startPos := tokenStartPos; startPos < node.End(); { - tokenKind := scanner.Token() - tokenFullStart := scanner.TokenFullStart() - tokenEnd := scanner.TokenEnd() - lastToken = sourceFile.GetOrCreateToken(tokenKind, tokenFullStart, tokenEnd, node) - startPos = tokenEnd - scanner.Scan() - } - return core.IfElse(lastToken != nil, lastToken, lastChildNode) -} - -func GetLastToken(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node { - if node == nil { - return nil - } - - if ast.IsTokenKind(node.Kind) || ast.IsIdentifier(node) { - return nil - } - - AssertHasRealPosition(node) - - lastChild := GetLastChild(node, sourceFile) - if lastChild == nil { - return nil - } - - if lastChild.Kind < ast.KindFirstNode { - return lastChild - } else { - return GetLastToken(lastChild, sourceFile) - } -} - -// Gets the last visited child of the given node. -// NOTE: This doesn't include unvisited tokens; for this, use `getLastChild` or `getLastToken`. -func GetLastVisitedChild(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node { - var lastChild *ast.Node - - visitNode := func(n *ast.Node, _ *ast.NodeVisitor) *ast.Node { - if !(n == nil || node.Flags&ast.NodeFlagsReparsed != 0) { - lastChild = n - } - return n - } - visitNodeList := func(nodeList *ast.NodeList, _ *ast.NodeVisitor) *ast.NodeList { - if nodeList != nil && len(nodeList.Nodes) > 0 { - for i := len(nodeList.Nodes) - 1; i >= 0; i-- { - if nodeList.Nodes[i].Flags&ast.NodeFlagsReparsed == 0 { - lastChild = nodeList.Nodes[i] - break - } - } - } - return nodeList - } - - astnav.VisitEachChildAndJSDoc(node, sourceFile, visitNode, visitNodeList) - return lastChild -} - -func GetFirstToken(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node { - if ast.IsIdentifier(node) || ast.IsTokenKind(node.Kind) { - return nil - } - AssertHasRealPosition(node) - var firstChild *ast.Node - node.ForEachChild(func(n *ast.Node) bool { - if n == nil || node.Flags&ast.NodeFlagsReparsed != 0 { - return false - } - firstChild = n - return true - }) - - var tokenEndPosition int - if firstChild != nil { - tokenEndPosition = firstChild.Pos() - } else { - tokenEndPosition = node.End() - } - scanner := scanner.GetScannerForSourceFile(sourceFile, node.Pos()) - var firstToken *ast.Node - if node.Pos() < tokenEndPosition { - tokenKind := scanner.Token() - tokenFullStart := scanner.TokenFullStart() - tokenEnd := scanner.TokenEnd() - firstToken = sourceFile.GetOrCreateToken(tokenKind, tokenFullStart, tokenEnd, node) - } - - if firstToken != nil { - return firstToken - } - if firstChild == nil { - return nil - } - if firstChild.Kind < ast.KindFirstNode { - return firstChild - } - return GetFirstToken(firstChild, sourceFile) -} - -func AssertHasRealPosition(node *ast.Node) { - if ast.PositionIsSynthesized(node.Pos()) || ast.PositionIsSynthesized(node.End()) { - panic("Node must have a real position for this operation.") - } -}