kittenipc/kitcom/internal/tsgo/ls/autoimports.go
2025-10-15 10:12:44 +03:00

1491 lines
55 KiB
Go

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