internal/ls/autoimports.go (1,343 lines of code) (raw):

package ls import ( "context" "fmt" "strings" "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/astnav" "github.com/microsoft/typescript-go/internal/binder" "github.com/microsoft/typescript-go/internal/checker" "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/compiler" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/debug" "github.com/microsoft/typescript-go/internal/diagnostics" "github.com/microsoft/typescript-go/internal/locale" "github.com/microsoft/typescript-go/internal/ls/change" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/module" "github.com/microsoft/typescript-go/internal/modulespecifiers" "github.com/microsoft/typescript-go/internal/packagejson" "github.com/microsoft/typescript-go/internal/stringutil" "github.com/microsoft/typescript-go/internal/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 } func newExportInfoMapKey(importedName string, symbol *ast.Symbol, ambientModuleNameKey string, ch *checker.Checker) lsproto.ExportInfoMapKey { return lsproto.ExportInfoMapKey{ SymbolName: importedName, SymbolId: uint64(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.OrderedMap[lsproto.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.OrderedMap[lsproto.ExportInfoMapKey, []*CachedSymbolExportInfo]{} e.usableByFileName = "" } func (e *ExportInfoMap) get(importingFile tspath.Path, ch *checker.Checker, key lsproto.ExportInfoMapKey) []*SymbolExportInfo { if e.usableByFileName != importingFile { return nil } return core.Map(e.exportInfo.GetOrZero(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) e.exportInfoId++ id := e.exportInfoId 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() } key := newExportInfoMapKey(symbolName, symbol, moduleKey, ch) infos := e.exportInfo.GetOrZero(key) infos = append(infos, &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, }) e.exportInfo.Set(key, infos) } 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 lsproto.ExportInfoMapKey) []*SymbolExportInfo, ) []*SymbolExportInfo { if importingFile != e.usableByFileName { return nil } for key, info := range e.exportInfo.Entries() { 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 } func (l *LanguageService) getImportCompletionAction( ctx context.Context, ch *checker.Checker, targetSymbol *ast.Symbol, moduleSymbol *ast.Symbol, sourceFile *ast.SourceFile, position int, exportMapKey lsproto.ExportInfoMapKey, symbolName string, isJsxTagName bool, // formatContext *formattingContext, ) (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.getExportInfoMap(ctx, ch, sourceFile, 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)) 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) } func NewExportInfoMap(globalsTypingCacheLocation string) *ExportInfoMap { return &ExportInfoMap{ packages: map[string]string{}, symbols: map[int]symbolExportEntry{}, exportInfo: collections.OrderedMap[lsproto.ExportInfoMapKey, []*CachedSymbolExportInfo]{}, globalTypingsCacheLocation: globalsTypingCacheLocation, } } func (l *LanguageService) isImportable( fromFile *ast.SourceFile, toFile *ast.SourceFile, toModule *ast.Symbol, 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 := lsutil.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 *lsutil.UserPreferences l *LanguageService filter *packageJsonImportFilter } func (r *importSpecifierResolverForCompletions) packageJsonImportFilter() *packageJsonImportFilter { if r.filter == nil { r.filter = r.l.createPackageJsonImportFilter(r.SourceFile) } 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 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, false /* fromCacheOnly */) return i.l.getBestFix(fixes, i.SourceFile, packageJsonImportFilter.allowsImportingSpecifier) } func (l *LanguageService) getImportFixForSymbol( ch *checker.Checker, sourceFile *ast.SourceFile, exportInfos []*SymbolExportInfo, position int, isValidTypeOnlySite *bool, ) *ImportFix { if isValidTypeOnlySite == nil { isValidTypeOnlySite = ptrTo(ast.IsValidTypeOnlyAliasUseSite(astnav.GetTokenAtPosition(sourceFile, position))) } useRequire := shouldUseRequire(sourceFile, l.GetProgram()) packageJsonImportFilter := l.createPackageJsonImportFilter(sourceFile) _, fixes := l.getImportFixes(ch, exportInfos, ptrTo(l.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(position))), isValidTypeOnlySite, &useRequire, sourceFile, false /* fromCacheOnly */) return l.getBestFix(fixes, sourceFile, packageJsonImportFilter.allowsImportingSpecifier) } func (l *LanguageService) getBestFix(fixes []*ImportFix, sourceFile *ast.SourceFile, allowsImportingSpecifier func(moduleSpecifier string) bool) *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 l.compareModuleSpecifiers( fix, best, sourceFile, allowsImportingSpecifier, func(fileName string) tspath.Path { return tspath.ToPath(fileName, l.GetProgram().GetCurrentDirectory(), l.GetProgram().UseCaseSensitiveFileNames()) }, ) < 0 { best = fix } } return best } // 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 (l *LanguageService) compareModuleSpecifiers( a *ImportFix, // !!! ImportFixWithModuleSpecifier b *ImportFix, // !!! ImportFixWithModuleSpecifier importingFile *ast.SourceFile, // | FutureSourceFile, allowsImportingSpecifier func(specifier string) bool, toPath func(fileName string) tspath.Path, ) int { if a.kind != ImportFixKindUseNamespace && b.kind != ImportFixKindUseNamespace { if comparison := core.CompareBooleans( b.moduleSpecifierKind != modulespecifiers.ResultKindNodeModules || allowsImportingSpecifier(b.moduleSpecifier), a.moduleSpecifierKind != modulespecifiers.ResultKindNodeModules || allowsImportingSpecifier(a.moduleSpecifier), ); comparison != 0 { return comparison } if comparison := compareModuleSpecifierRelativity(a, b, l.UserPreferences()); comparison != 0 { return comparison } if comparison := compareNodeCoreModuleSpecifiers(a.moduleSpecifier, b.moduleSpecifier, importingFile, l.GetProgram()); comparison != 0 { return comparison } if comparison := core.CompareBooleans(isFixPossiblyReExportingImportingFile(a, importingFile.Path(), toPath), isFixPossiblyReExportingImportingFile(b, importingFile.Path(), toPath)); comparison != 0 { return comparison } if comparison := tspath.CompareNumberOfDirectorySeparators(a.moduleSpecifier, b.moduleSpecifier); comparison != 0 { return comparison } } return 0 } func compareNodeCoreModuleSpecifiers(a, b string, importingFile *ast.SourceFile, program *compiler.Program) int { if strings.HasPrefix(a, "node:") && !strings.HasPrefix(b, "node:") { if lsutil.ShouldUseUriStyleNodeCoreModules(importingFile, program) { return -1 } return 1 } if strings.HasPrefix(b, "node:") && !strings.HasPrefix(a, "node:") { if lsutil.ShouldUseUriStyleNodeCoreModules(importingFile, program) { return 1 } return -1 } return 0 } // 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 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" } // returns `-1` if `a` is better than `b` func compareModuleSpecifierRelativity(a *ImportFix, b *ImportFix, preferences *lsutil.UserPreferences) int { switch preferences.ImportModuleSpecifierPreference { case modulespecifiers.ImportModuleSpecifierPreferenceNonRelative, modulespecifiers.ImportModuleSpecifierPreferenceProjectRelative: return core.CompareBooleans(a.moduleSpecifierKind == modulespecifiers.ResultKindRelative, b.moduleSpecifierKind == modulespecifiers.ResultKindRelative) } return 0 } func (l *LanguageService) getImportFixes( ch *checker.Checker, exportInfos []*SymbolExportInfo, // | FutureSymbolExportInfo[], usagePosition *lsproto.Position, isValidTypeOnlyUseSite *bool, useRequire *bool, sourceFile *ast.SourceFile, // | FutureSourceFile, // 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, fromCacheOnly, ) computedWithoutCacheCount := 0 // if result.computedWithoutCacheCount != nil { // computedWithoutCacheCount = *result.computedWithoutCacheCount // } return computedWithoutCacheCount, append(useNamespace, result...) } func (l *LanguageService) createPackageJsonImportFilter(fromFile *ast.SourceFile) *packageJsonImportFilter { // !!! The program package.json cache may not have every relevant package.json. // This should eventually be integrated with the session. var packageJsons []*packagejson.PackageJson dir := tspath.GetDirectoryPath(fromFile.FileName()) for { packageJsonDir := l.GetProgram().GetNearestAncestorDirectoryWithPackageJson(dir) if packageJsonDir == "" { break } if packageJson := l.GetProgram().GetPackageJsonInfo(tspath.CombinePaths(packageJsonDir, "package.json")).GetContents(); packageJson != nil && packageJson.Parseable { packageJsons = append(packageJsons, packageJson) } dir = tspath.GetDirectoryPath(packageJsonDir) if dir == packageJsonDir { break } } 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.HasDependency(packageName) || packageJson.HasDependency(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, l.UserPreferences().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 } namespacePrefix := getNamespaceLikeImportText(existingImport.declaration) moduleSpecifier := checker.TryGetModuleSpecifierFromDeclaration(existingImport.declaration) if namespacePrefix != "" && moduleSpecifier != nil && moduleSpecifier.Text() != "" { return getUseNamespaceImport( moduleSpecifier.Text(), modulespecifiers.ResultKindNone, namespacePrefix, position, ) } } return nil } func getNamespaceLikeImportText(declaration *ast.Statement) string { switch declaration.Kind { case ast.KindVariableDeclaration: name := declaration.Name() if name != nil && name.Kind == ast.KindIdentifier { return name.Text() } return "" case ast.KindImportEqualsDeclaration: return declaration.Name().Text() case ast.KindJSDocImportTag, ast.KindImportDeclaration: importClause := declaration.ImportClause() if importClause != nil && importClause.AsImportClause().NamedBindings != nil && importClause.AsImportClause().NamedBindings.Kind == ast.KindNamespaceImport { return importClause.AsImportClause().NamedBindings.Name().Text() } return "" default: debug.AssertNever(declaration) return "" } } 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 := info.declaration.ImportClause() if importClause == nil || !ast.IsStringLiteralLike(info.declaration.ModuleSpecifier()) { return nil } namedBindings := importClause.AsImportClause().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, 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, fromCacheOnly) } func (l *LanguageService) getNewImportFixes( ch *checker.Checker, sourceFile *ast.SourceFile, // | FutureSourceFile, usagePosition *lsproto.Position, isValidTypeOnlyUseSite bool, useRequire bool, exportInfos []*SymbolExportInfo, // !!! (SymbolExportInfo | FutureSymbolExportInfo)[], 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(), l.UserPreferences().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 shouldUseRequire( 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, // 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, ) codeAction { tracker := change.NewTracker(ctx, l.GetProgram().Options(), l.FormatOptions(), l.converters) // !!! changetracker.with diag := l.codeActionForFixWorker(ctx, tracker, sourceFile, symbolName, fix, includeSymbolNameInDescription) changes := tracker.GetChanges()[sourceFile.FileName()] return codeAction{description: diag, changes: changes} } func (l *LanguageService) codeActionForFixWorker( ctx context.Context, changeTracker *change.Tracker, sourceFile *ast.SourceFile, symbolName string, fix *ImportFix, includeSymbolNameInDescription bool, ) string { locale := locale.FromContext(ctx) switch fix.kind { case ImportFixKindUseNamespace: addNamespaceQualifier(changeTracker, sourceFile, fix.qualification()) return diagnostics.Change_0_to_1.Localize(locale, symbolName, fmt.Sprintf("%s.%s", *fix.namespacePrefix, symbolName)) case ImportFixKindJsdocTypeImport: if fix.usagePosition == nil { return "" } quotePreference := getQuotePreference(sourceFile, l.UserPreferences()) quoteChar := "\"" if quotePreference == quotePreferenceSingle { quoteChar = "'" } importTypePrefix := fmt.Sprintf("import(%s%s%s).", quoteChar, fix.moduleSpecifier, quoteChar) changeTracker.InsertText(sourceFile, *fix.usagePosition, importTypePrefix) return diagnostics.Change_0_to_1.Localize(locale, symbolName, importTypePrefix+symbolName) case ImportFixKindAddToExisting: var defaultImport *Import var namedImports []*Import if fix.importKind == ImportKindDefault { defaultImport = &Import{name: symbolName, addAsTypeOnly: fix.addAsTypeOnly} } else if fix.importKind == ImportKindNamed { namedImports = []*Import{{name: symbolName, addAsTypeOnly: fix.addAsTypeOnly, propertyName: fix.propertyName}} } l.doAddExistingFix( changeTracker, sourceFile, fix.importClauseOrBindingPattern, defaultImport, namedImports, ) moduleSpecifierWithoutQuotes := stringutil.StripQuotes(fix.moduleSpecifier) if includeSymbolNameInDescription { return diagnostics.Import_0_from_1.Localize(locale, symbolName, moduleSpecifierWithoutQuotes) } return diagnostics.Update_import_from_0.Localize(locale, moduleSpecifierWithoutQuotes) case ImportFixKindAddNew: var declarations []*ast.Statement var defaultImport *Import var namedImports []*Import var namespaceLikeImport *Import if fix.importKind == ImportKindDefault { defaultImport = &Import{name: symbolName, addAsTypeOnly: fix.addAsTypeOnly} } else if fix.importKind == ImportKindNamed { namedImports = []*Import{{name: symbolName, addAsTypeOnly: fix.addAsTypeOnly, propertyName: fix.propertyName}} } 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 { declarations = getNewRequires(changeTracker, fix.moduleSpecifier, defaultImport, namedImports, namespaceLikeImport, l.GetProgram().Options()) } else { declarations = l.getNewImports(changeTracker, fix.moduleSpecifier, getQuotePreference(sourceFile, l.UserPreferences()), defaultImport, namedImports, namespaceLikeImport, l.GetProgram().Options()) } l.insertImports( changeTracker, sourceFile, declarations, /*blankLineBetween*/ true, ) if qualification != nil { addNamespaceQualifier(changeTracker, sourceFile, qualification) } if includeSymbolNameInDescription { return diagnostics.Import_0_from_1.Localize(locale, symbolName, fix.moduleSpecifier) } return diagnostics.Add_import_from_0.Localize(locale, fix.moduleSpecifier) case ImportFixKindPromoteTypeOnly: promotedDeclaration := promoteFromTypeOnly(changeTracker, fix.typeOnlyAliasDeclaration, l.GetProgram(), sourceFile, l) if promotedDeclaration.Kind == ast.KindImportSpecifier { moduleSpec := getModuleSpecifierText(promotedDeclaration.Parent.Parent) return diagnostics.Remove_type_from_import_of_0_from_1.Localize(locale, symbolName, moduleSpec) } moduleSpec := getModuleSpecifierText(promotedDeclaration) return diagnostics.Remove_type_from_import_declaration_from_0.Localize(locale, moduleSpec) default: panic(fmt.Sprintf(`Unexpected fix kind %v`, fix.kind)) } } func getNewRequires( changeTracker *change.Tracker, moduleSpecifier string, defaultImport *Import, namedImports []*Import, namespaceLikeImport *Import, compilerOptions *core.CompilerOptions, ) []*ast.Statement { quotedModuleSpecifier := changeTracker.NodeFactory.NewStringLiteral(moduleSpecifier) var statements []*ast.Statement // const { default: foo, bar, etc } = require('./mod'); if defaultImport != nil || len(namedImports) > 0 { bindingElements := []*ast.Node{} for _, namedImport := range namedImports { var propertyName *ast.Node if namedImport.propertyName != "" { propertyName = changeTracker.NodeFactory.NewIdentifier(namedImport.propertyName) } bindingElements = append(bindingElements, changeTracker.NodeFactory.NewBindingElement( /*dotDotDotToken*/ nil, propertyName, changeTracker.NodeFactory.NewIdentifier(namedImport.name), /*initializer*/ nil, )) } if defaultImport != nil { bindingElements = append([]*ast.Node{ changeTracker.NodeFactory.NewBindingElement( /*dotDotDotToken*/ nil, changeTracker.NodeFactory.NewIdentifier("default"), changeTracker.NodeFactory.NewIdentifier(defaultImport.name), /*initializer*/ nil, ), }, bindingElements...) } declaration := createConstEqualsRequireDeclaration( changeTracker, changeTracker.NodeFactory.NewBindingPattern( ast.KindObjectBindingPattern, changeTracker.NodeFactory.NewNodeList(bindingElements), ), quotedModuleSpecifier, ) statements = append(statements, declaration) } // const foo = require('./mod'); if namespaceLikeImport != nil { declaration := createConstEqualsRequireDeclaration( changeTracker, changeTracker.NodeFactory.NewIdentifier(namespaceLikeImport.name), quotedModuleSpecifier, ) statements = append(statements, declaration) } debug.AssertIsDefined(statements) return statements } func createConstEqualsRequireDeclaration(changeTracker *change.Tracker, name *ast.Node, quotedModuleSpecifier *ast.Node) *ast.Statement { return changeTracker.NodeFactory.NewVariableStatement( /*modifiers*/ nil, changeTracker.NodeFactory.NewVariableDeclarationList( ast.NodeFlagsConst, changeTracker.NodeFactory.NewNodeList([]*ast.Node{ changeTracker.NodeFactory.NewVariableDeclaration( name, /*exclamationToken*/ nil, /*type*/ nil, changeTracker.NodeFactory.NewCallExpression( changeTracker.NodeFactory.NewIdentifier("require"), /*questionDotToken*/ nil, /*typeArguments*/ nil, changeTracker.NodeFactory.NewNodeList([]*ast.Node{quotedModuleSpecifier}), ast.NodeFlagsNone, ), ), }), ), ) } func getModuleSpecifierText(promotedDeclaration *ast.Node) 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() } func ptrTo[T any](v T) *T { return &v }