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

129 lines
4.7 KiB
Go

package ls
import (
"cmp"
"strings"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/compiler"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/modulespecifiers"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath"
)
// statement = anyImportOrRequireStatement
func getImportDeclarationInsertIndex(sortedImports []*ast.Statement, newImport *ast.Statement, comparer func(a, b *ast.Statement) int) int {
// !!!
return len(sortedImports)
}
// returns `-1` if `a` is better than `b`
//
// note: this sorts in descending order of preference; different than convention in other cmp-like functions
func compareModuleSpecifiers(
a *ImportFix, // !!! ImportFixWithModuleSpecifier
b *ImportFix, // !!! ImportFixWithModuleSpecifier
importingFile *ast.SourceFile, // | FutureSourceFile,
program *compiler.Program,
preferences UserPreferences,
allowsImportingSpecifier func(specifier string) bool,
toPath func(fileName string) tspath.Path,
) int {
if a.kind == ImportFixKindUseNamespace || b.kind == ImportFixKindUseNamespace {
return 0
}
if comparison := compareBooleans(
b.moduleSpecifierKind != modulespecifiers.ResultKindNodeModules || allowsImportingSpecifier(b.moduleSpecifier),
a.moduleSpecifierKind != modulespecifiers.ResultKindNodeModules || allowsImportingSpecifier(a.moduleSpecifier),
); comparison != 0 {
return comparison
}
if comparison := compareModuleSpecifierRelativity(a, b, preferences); comparison != 0 {
return comparison
}
if comparison := compareNodeCoreModuleSpecifiers(a.moduleSpecifier, b.moduleSpecifier, importingFile, program); comparison != 0 {
return comparison
}
if comparison := compareBooleans(isFixPossiblyReExportingImportingFile(a, importingFile.Path(), toPath), isFixPossiblyReExportingImportingFile(b, importingFile.Path(), toPath)); comparison != 0 {
return comparison
}
if comparison := compareNumberOfDirectorySeparators(a.moduleSpecifier, b.moduleSpecifier); comparison != 0 {
return comparison
}
return 0
}
// True > False
func compareBooleans(a, b bool) int {
if a && !b {
return -1
} else if !a && b {
return 1
}
return 0
}
// returns `-1` if `a` is better than `b`
func compareModuleSpecifierRelativity(a *ImportFix, b *ImportFix, preferences UserPreferences) int {
switch preferences.ImportModuleSpecifierPreference {
case modulespecifiers.ImportModuleSpecifierPreferenceNonRelative, modulespecifiers.ImportModuleSpecifierPreferenceProjectRelative:
return compareBooleans(a.moduleSpecifierKind == modulespecifiers.ResultKindRelative, b.moduleSpecifierKind == modulespecifiers.ResultKindRelative)
}
return 0
}
func compareNodeCoreModuleSpecifiers(a, b string, importingFile *ast.SourceFile, program *compiler.Program) int {
if strings.HasPrefix(a, "node:") && !strings.HasPrefix(b, "node:") {
if shouldUseUriStyleNodeCoreModules(importingFile, program) {
return -1
}
return 1
}
if strings.HasPrefix(b, "node:") && !strings.HasPrefix(a, "node:") {
if shouldUseUriStyleNodeCoreModules(importingFile, program) {
return 1
}
return -1
}
return 0
}
func shouldUseUriStyleNodeCoreModules(file *ast.SourceFile, program *compiler.Program) bool {
for _, node := range file.Imports() {
if core.NodeCoreModules()[node.Text()] && !core.ExclusivelyPrefixedNodeCoreModules[node.Text()] {
if strings.HasPrefix(node.Text(), "node:") {
return true
} else {
return false
}
}
}
return program.UsesUriStyleNodeCoreModules()
}
// This is a simple heuristic to try to avoid creating an import cycle with a barrel re-export.
// E.g., do not `import { Foo } from ".."` when you could `import { Foo } from "../Foo"`.
// This can produce false positives or negatives if re-exports cross into sibling directories
// (e.g. `export * from "../whatever"`) or are not named "index".
func isFixPossiblyReExportingImportingFile(fix *ImportFix, importingFilePath tspath.Path, toPath func(fileName string) tspath.Path) bool {
if fix.isReExport != nil && *(fix.isReExport) &&
fix.exportInfo != nil && fix.exportInfo.moduleFileName != "" && isIndexFileName(fix.exportInfo.moduleFileName) {
reExportDir := toPath(tspath.GetDirectoryPath(fix.exportInfo.moduleFileName))
return strings.HasPrefix(string(importingFilePath), string(reExportDir))
}
return false
}
func compareNumberOfDirectorySeparators(path1, path2 string) int {
return cmp.Compare(strings.Count(path1, "/"), strings.Count(path2, "/"))
}
func isIndexFileName(fileName string) bool {
fileName = tspath.GetBaseFileName(fileName)
if tspath.FileExtensionIsOneOf(fileName, []string{".js", ".jsx", ".d.ts", ".ts", ".tsx"}) {
fileName = tspath.RemoveFileExtension(fileName)
}
return fileName == "index"
}