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() }