Files
kjol/tools/tsgo/internal/ls/lsutil/organizeimports.go
2026-07-09 16:50:43 -04:00

696 lines
22 KiB
Go

package lsutil
import (
"cmp"
"math"
"strings"
"unicode"
"unicode/utf8"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/stringutil"
"github.com/microsoft/typescript-go/internal/tspath"
"golang.org/x/text/unicode/norm"
)
// FilterImportDeclarations filters out non-import declarations from a list of statements.
func FilterImportDeclarations(statements []*ast.Statement) []*ast.Statement {
return core.Filter(statements, func(stmt *ast.Statement) bool {
return stmt.Kind == ast.KindImportDeclaration
})
}
// GetDetectionLists returns the lists of comparers and type orders to test for organize imports detection.
func GetDetectionLists(preferences UserPreferences) (comparersToTest []func(a, b string) int, typeOrdersToTest []OrganizeImportsTypeOrder) {
if preferences.OrganizeImportsSort != OrganizeImportsSortAuto {
comparersToTest = []func(a, b string) int{getOrganizeImportsPresetStringComparer(preferences.OrganizeImportsSort)}
} else if !preferences.OrganizeImportsIgnoreCase.IsUnknown() {
comparersToTest = []func(a, b string) int{getOrganizeImportsStringComparer(preferences, preferences.OrganizeImportsIgnoreCase.IsTrue())}
} else {
comparersToTest = []func(a, b string) int{
getOrganizeImportsStringComparer(preferences, true),
getOrganizeImportsStringComparer(preferences, false),
}
}
if preferences.OrganizeImportsTypeOrder != OrganizeImportsTypeOrderAuto {
typeOrdersToTest = []OrganizeImportsTypeOrder{preferences.OrganizeImportsTypeOrder}
} else {
typeOrdersToTest = []OrganizeImportsTypeOrder{
OrganizeImportsTypeOrderLast,
OrganizeImportsTypeOrderInline,
OrganizeImportsTypeOrderFirst,
}
}
return comparersToTest, typeOrdersToTest
}
func ResolveOrganizeImportsSort(preferences UserPreferences) OrganizeImportsSort {
if preferences.OrganizeImportsSort != OrganizeImportsSortAuto {
return preferences.OrganizeImportsSort
}
if preferences.OrganizeImportsCollation == OrganizeImportsCollationUnicode {
switch preferences.OrganizeImportsIgnoreCase {
case core.TSTrue:
return OrganizeImportsSortNaturalIgnoreCase
case core.TSFalse:
return OrganizeImportsSortNatural
default:
return OrganizeImportsSortAuto
}
}
switch preferences.OrganizeImportsIgnoreCase {
case core.TSTrue:
return OrganizeImportsSortOrdinalIgnoreCase
case core.TSFalse:
return OrganizeImportsSortOrdinal
default:
return OrganizeImportsSortAuto
}
}
func getOrganizeImportsOrdinalStringComparer(ignoreCase bool) func(a, b string) int {
if ignoreCase {
return stringutil.CompareStringsCaseInsensitiveEslintCompatible
}
return stringutil.CompareStringsCaseSensitive
}
func getOrganizeImportsNaturalStringComparer(caseSensitive bool) func(a, b string) int {
return func(a, b string) int {
return compareOrganizeImportsNaturalStrings(a, b, caseSensitive)
}
}
func getOrganizeImportsUnicodeStringComparer(ignoreCase bool, preferences UserPreferences) func(a, b string) int {
caseFirst := preferences.OrganizeImportsCaseFirst
numeric := preferences.OrganizeImportsNumericCollation.IsTrue()
accents := !preferences.OrganizeImportsAccentCollation.IsFalse()
return func(a, b string) int {
return compareOrganizeImportsUnicodeStrings(a, b, ignoreCase, caseFirst, numeric, accents)
}
}
func compareOrganizeImportsNaturalStrings(a string, b string, caseSensitive bool) int {
if cmp := compareStringsNumeric(naturalCollationKey(a), naturalCollationKey(b)); cmp != 0 {
return cmp
}
if caseSensitive {
if cmp := compareOrganizeImportsCaseUpperFirst(a, b); cmp != 0 {
return cmp
}
}
return strings.Compare(a, b)
}
func compareOrganizeImportsUnicodeStrings(a string, b string, ignoreCase bool, caseFirst OrganizeImportsCaseFirst, numeric bool, accents bool) int {
if cmp := compareOrganizeImportsUnicodeKeys(naturalCollationKey(a), naturalCollationKey(b), numeric); cmp != 0 {
return cmp
}
if accents {
if cmp := compareOrganizeImportsUnicodeKeys(strings.ToLower(a), strings.ToLower(b), numeric); cmp != 0 {
return cmp
}
}
if !ignoreCase {
if cmp := compareOrganizeImportsCase(a, b, caseFirst); cmp != 0 {
return cmp
}
}
return strings.Compare(a, b)
}
func naturalCollationKey(s string) string {
return strings.ToLower(removeDiacritics(s))
}
func removeDiacritics(s string) string {
return strings.Map(func(r rune) rune {
if unicode.Is(unicode.Mn, r) {
return -1
}
return r
}, norm.NFD.String(s))
}
func compareOrganizeImportsUnicodeKeys(a string, b string, numeric bool) int {
if numeric {
return compareStringsNumeric(a, b)
}
return strings.Compare(a, b)
}
func compareStringsNumeric(a string, b string) int {
for len(a) > 0 && len(b) > 0 {
if isASCIIDigit(a[0]) && isASCIIDigit(b[0]) {
aRunEnd := asciiDigitRunEnd(a)
bRunEnd := asciiDigitRunEnd(b)
if cmp := compareNumericText(a[:aRunEnd], b[:bRunEnd]); cmp != 0 {
return cmp
}
a = a[aRunEnd:]
b = b[bRunEnd:]
continue
}
aRune, aSize := utf8.DecodeRuneInString(a)
bRune, bSize := utf8.DecodeRuneInString(b)
if aRune != bRune {
return cmp.Compare(aRune, bRune)
}
a = a[aSize:]
b = b[bSize:]
}
return cmp.Compare(len(a), len(b))
}
func isASCIIDigit(ch byte) bool {
return ch >= '0' && ch <= '9'
}
func asciiDigitRunEnd(s string) int {
i := 0
for i < len(s) && isASCIIDigit(s[i]) {
i++
}
return i
}
func compareNumericText(a string, b string) int {
aDigits := strings.TrimLeft(a, "0")
bDigits := strings.TrimLeft(b, "0")
if aDigits == "" {
aDigits = "0"
}
if bDigits == "" {
bDigits = "0"
}
if len(aDigits) != len(bDigits) {
return cmp.Compare(len(aDigits), len(bDigits))
}
if cmp := strings.Compare(aDigits, bDigits); cmp != 0 {
return cmp
}
return strings.Compare(a, b)
}
func compareOrganizeImportsCaseUpperFirst(a string, b string) int {
return compareOrganizeImportsCase(a, b, OrganizeImportsCaseFirstUpper)
}
func compareOrganizeImportsCase(a string, b string, caseFirst OrganizeImportsCaseFirst) int {
aRunes := []rune(a)
bRunes := []rune(b)
minLen := min(len(aRunes), len(bRunes))
for i := range minLen {
aUpper := unicode.IsUpper(aRunes[i])
bUpper := unicode.IsUpper(bRunes[i])
if aUpper != bUpper {
switch caseFirst {
case OrganizeImportsCaseFirstUpper:
if aUpper {
return -1
}
return 1
case OrganizeImportsCaseFirstLower:
if !aUpper {
return -1
}
return 1
default:
if aUpper {
return 1
}
return -1
}
}
}
return cmp.Compare(len(aRunes), len(bRunes))
}
func getOrganizeImportsPresetStringComparer(sort OrganizeImportsSort) func(a, b string) int {
switch sort {
case OrganizeImportsSortOrdinalIgnoreCase:
return getOrganizeImportsOrdinalStringComparer(true)
case OrganizeImportsSortNatural:
return getOrganizeImportsNaturalStringComparer(true)
case OrganizeImportsSortNaturalIgnoreCase:
return getOrganizeImportsNaturalStringComparer(false)
default:
return getOrganizeImportsOrdinalStringComparer(false)
}
}
func getOrganizeImportsStringComparer(preferences UserPreferences, ignoreCase bool) func(a, b string) int {
if preferences.OrganizeImportsSort != OrganizeImportsSortAuto {
return getOrganizeImportsPresetStringComparer(preferences.OrganizeImportsSort)
}
if preferences.OrganizeImportsCollation == OrganizeImportsCollationUnicode {
return getOrganizeImportsUnicodeStringComparer(ignoreCase, preferences)
}
return getOrganizeImportsOrdinalStringComparer(ignoreCase)
}
func getModuleSpecifierExpression(declaration *ast.Statement) *ast.Expression {
switch declaration.Kind {
case ast.KindImportEqualsDeclaration:
importEquals := declaration.AsImportEqualsDeclaration()
if importEquals.ModuleReference.Kind == ast.KindExternalModuleReference {
return importEquals.ModuleReference.Expression()
}
return nil
case ast.KindImportDeclaration:
return declaration.ModuleSpecifier()
case ast.KindVariableStatement:
declarations := declaration.AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes
if len(declarations) > 0 {
initializer := declarations[0].Initializer()
if initializer != nil && initializer.Kind == ast.KindCallExpression {
callExpr := initializer.AsCallExpression()
if len(callExpr.Arguments.Nodes) > 0 {
return callExpr.Arguments.Nodes[0]
}
}
}
return nil
default:
return nil
}
}
// GetExternalModuleName returns the module name from a module specifier expression.
func GetExternalModuleName(specifier *ast.Expression) string {
if specifier != nil && ast.IsStringLiteralLike(specifier.AsNode()) {
return specifier.Text()
}
return ""
}
// CompareModuleSpecifiers compares two module specifiers using the given comparer.
func CompareModuleSpecifiers(m1 *ast.Expression, m2 *ast.Expression, comparer func(a, b string) int) int {
name1 := GetExternalModuleName(m1)
name2 := GetExternalModuleName(m2)
if cmp := core.CompareBooleans(name1 == "", name2 == ""); cmp != 0 {
return cmp
}
if cmp := core.CompareBooleans(tspath.IsExternalModuleNameRelative(name1), tspath.IsExternalModuleNameRelative(name2)); cmp != 0 {
return cmp
}
return comparer(name1, name2)
}
func compareImportKind(s1 *ast.Statement, s2 *ast.Statement) int {
return cmp.Compare(getImportKindOrder(s1), getImportKindOrder(s2))
}
// getImportKindOrder returns the sort order for different import kinds:
// 1. Side-effect imports
// 2. Type-only imports
// 3. Namespace imports
// 4. Default imports
// 5. Named imports
// 6. ImportEqualsDeclarations
// 7. Require variable statements
const (
importKindOrderSideEffect = 0
importKindOrderTypeOnly = 1
importKindOrderNamespace = 2
importKindOrderDefault = 3
importKindOrderNamed = 4
importKindOrderImportEquals = 5
importKindOrderRequire = 6
importKindOrderUnknown = 7
)
func getImportKindOrder(s1 *ast.Statement) int {
switch s1.Kind {
case ast.KindImportDeclaration:
importDecl := s1.AsImportDeclaration()
if importDecl.ImportClause == nil {
return importKindOrderSideEffect
}
importClause := importDecl.ImportClause.AsImportClause()
if importClause.IsTypeOnly() {
return importKindOrderTypeOnly
}
if importClause.NamedBindings != nil && importClause.NamedBindings.Kind == ast.KindNamespaceImport {
return importKindOrderNamespace
}
if importClause.Name() != nil {
return importKindOrderDefault
}
return importKindOrderNamed
case ast.KindImportEqualsDeclaration:
return importKindOrderImportEquals
case ast.KindVariableStatement:
return importKindOrderRequire
default:
return importKindOrderUnknown
}
}
// CompareImportsOrRequireStatements compares two import or require statements.
func CompareImportsOrRequireStatements(s1 *ast.Statement, s2 *ast.Statement, comparer func(a, b string) int) int {
if cmp := CompareModuleSpecifiers(getModuleSpecifierExpression(s1), getModuleSpecifierExpression(s2), comparer); cmp != 0 {
return cmp
}
return compareImportKind(s1, s2)
}
func compareImportOrExportSpecifiers(s1 *ast.Node, s2 *ast.Node, comparer func(a, b string) int, preferences UserPreferences) int {
typeOrder := preferences.OrganizeImportsTypeOrder
s1Name := s1.Name().Text()
s2Name := s2.Name().Text()
switch typeOrder {
case OrganizeImportsTypeOrderFirst:
if cmp := core.CompareBooleans(s2.IsTypeOnly(), s1.IsTypeOnly()); cmp != 0 {
return cmp
}
return comparer(s1Name, s2Name)
case OrganizeImportsTypeOrderInline:
return comparer(s1Name, s2Name)
default: // OrganizeImportsTypeOrderLast
if cmp := core.CompareBooleans(s1.IsTypeOnly(), s2.IsTypeOnly()); cmp != 0 {
return cmp
}
return comparer(s1Name, s2Name)
}
}
// GetNamedImportSpecifierComparer returns a comparer function for sorting import specifiers.
func GetNamedImportSpecifierComparer(preferences UserPreferences, comparer func(a, b string) int) func(s1, s2 *ast.Node) int {
if comparer == nil {
ignoreCase := false
if !preferences.OrganizeImportsIgnoreCase.IsUnknown() {
ignoreCase = preferences.OrganizeImportsIgnoreCase.IsTrue()
}
comparer = getOrganizeImportsStringComparer(preferences, ignoreCase)
}
return func(s1, s2 *ast.Node) int {
return compareImportOrExportSpecifiers(s1, s2, comparer, preferences)
}
}
// GetImportSpecifierInsertionIndex returns the index at which to insert a new import specifier.
func GetImportSpecifierInsertionIndex(sortedImports []*ast.Node, newImport *ast.Node, comparer func(s1, s2 *ast.Node) int) int {
return core.FirstResult(core.BinarySearchUniqueFunc(sortedImports, func(mid int, value *ast.Node) int {
return comparer(value, newImport)
}))
}
// GetImportDeclarationInsertIndex returns the index at which to insert a new import declaration.
func GetImportDeclarationInsertIndex(sortedImports []*ast.Statement, newImport *ast.Statement, comparer func(a, b *ast.Statement) int) int {
return core.FirstResult(core.BinarySearchUniqueFunc(sortedImports, func(mid int, value *ast.Statement) int {
return comparer(value, newImport)
}))
}
// GetOrganizeImportsStringComparerWithDetection returns a string comparer based on detecting the order of import statements by the module specifier
func GetOrganizeImportsStringComparerWithDetection(originalImportDecls []*ast.Statement, preferences UserPreferences) (comparer func(a, b string) int, isSorted bool) {
result, sorted := DetectModuleSpecifierCaseBySort([][]*ast.Statement{originalImportDecls}, getComparers(preferences))
return result, sorted
}
func getComparers(preferences UserPreferences) []func(a string, b string) int {
if preferences.OrganizeImportsSort != OrganizeImportsSortAuto || !preferences.OrganizeImportsIgnoreCase.IsUnknown() {
ignoreCase := false
if !preferences.OrganizeImportsIgnoreCase.IsUnknown() {
ignoreCase = preferences.OrganizeImportsIgnoreCase.IsTrue()
}
return []func(a, b string) int{getOrganizeImportsStringComparer(preferences, ignoreCase)}
}
return []func(a, b string) int{
getOrganizeImportsStringComparer(preferences, true),
getOrganizeImportsStringComparer(preferences, false),
}
}
type namedImportSortResult struct {
namedImportComparer func(a, b string) int
typeOrder OrganizeImportsTypeOrder
isSorted bool
}
// DetectNamedImportOrganizationBySort detects the order of named imports throughout the file by considering the named imports in each statement as a group
func DetectNamedImportOrganizationBySort(
originalGroups []*ast.Statement,
comparersToTest []func(a, b string) int,
typesToTest []OrganizeImportsTypeOrder,
) (comparer func(a, b string) int, typeOrder OrganizeImportsTypeOrder, found bool) {
result := detectNamedImportOrganizationBySort(originalGroups, comparersToTest, typesToTest)
if result == nil {
return nil, OrganizeImportsTypeOrderLast, false
}
return result.namedImportComparer, result.typeOrder, true
}
func detectNamedImportOrganizationBySort(
originalGroups []*ast.Statement,
comparersToTest []func(a, b string) int,
typesToTest []OrganizeImportsTypeOrder,
) *namedImportSortResult {
var bothNamedImports bool
var importDeclsWithNamed []*ast.Statement
for _, imp := range originalGroups {
if imp.AsImportDeclaration().ImportClause == nil {
continue
}
clause := imp.AsImportDeclaration().ImportClause.AsImportClause()
if clause.NamedBindings == nil || clause.NamedBindings.Kind != ast.KindNamedImports {
continue
}
namedImports := clause.NamedBindings.AsNamedImports()
if len(namedImports.Elements.Nodes) == 0 {
continue
}
if !bothNamedImports {
hasTypeOnly := false
hasRegular := false
for _, elem := range namedImports.Elements.Nodes {
if elem.IsTypeOnly() {
hasTypeOnly = true
} else {
hasRegular = true
}
}
if hasTypeOnly && hasRegular {
bothNamedImports = true
}
}
importDeclsWithNamed = append(importDeclsWithNamed, imp)
}
if len(importDeclsWithNamed) == 0 {
return nil
}
namedImportsByDecl := make([][]*ast.Statement, 0, len(importDeclsWithNamed))
for _, imp := range importDeclsWithNamed {
clause := imp.AsImportDeclaration().ImportClause.AsImportClause()
namedImports := clause.NamedBindings.AsNamedImports()
namedImportsByDecl = append(namedImportsByDecl, namedImports.Elements.Nodes)
}
if !bothNamedImports || len(typesToTest) == 0 {
namesList := make([][]string, len(namedImportsByDecl))
for i, imports := range namedImportsByDecl {
names := make([]string, len(imports))
for j, imp := range imports {
names[j] = imp.Name().Text()
}
namesList[i] = names
}
sortState := detectCaseSensitivityBySort(namesList, comparersToTest)
typeOrder := OrganizeImportsTypeOrderLast
if len(typesToTest) == 1 {
typeOrder = typesToTest[0]
}
return &namedImportSortResult{
namedImportComparer: sortState.comparer,
typeOrder: typeOrder,
isSorted: sortState.isSorted,
}
}
bestDiff := map[OrganizeImportsTypeOrder]int{
OrganizeImportsTypeOrderFirst: math.MaxInt,
OrganizeImportsTypeOrderLast: math.MaxInt,
OrganizeImportsTypeOrderInline: math.MaxInt,
}
bestComparer := map[OrganizeImportsTypeOrder]func(a, b string) int{
OrganizeImportsTypeOrderFirst: comparersToTest[0],
OrganizeImportsTypeOrderLast: comparersToTest[0],
OrganizeImportsTypeOrderInline: comparersToTest[0],
}
for _, curComparer := range comparersToTest {
currDiff := map[OrganizeImportsTypeOrder]int{
OrganizeImportsTypeOrderFirst: 0,
OrganizeImportsTypeOrderLast: 0,
OrganizeImportsTypeOrderInline: 0,
}
for _, importDecl := range namedImportsByDecl {
for _, typeOrder := range typesToTest {
prefs := UserPreferences{OrganizeImportsTypeOrder: typeOrder}
diff := measureSortedness(importDecl, func(n1, n2 *ast.Node) int {
return compareImportOrExportSpecifiers(n1, n2, curComparer, prefs)
})
currDiff[typeOrder] = currDiff[typeOrder] + diff
}
}
for _, typeOrder := range typesToTest {
if currDiff[typeOrder] < bestDiff[typeOrder] {
bestDiff[typeOrder] = currDiff[typeOrder]
bestComparer[typeOrder] = curComparer
}
}
}
for _, bestTypeOrder := range typesToTest {
isBest := true
for _, testTypeOrder := range typesToTest {
if bestDiff[testTypeOrder] < bestDiff[bestTypeOrder] {
isBest = false
break
}
}
if isBest {
return &namedImportSortResult{
namedImportComparer: bestComparer[bestTypeOrder],
typeOrder: bestTypeOrder,
isSorted: bestDiff[bestTypeOrder] == 0,
}
}
}
return &namedImportSortResult{
namedImportComparer: bestComparer[OrganizeImportsTypeOrderLast],
typeOrder: OrganizeImportsTypeOrderLast,
isSorted: bestDiff[OrganizeImportsTypeOrderLast] == 0,
}
}
type caseSensitivityDetectionResult struct {
comparer func(a, b string) int
isSorted bool
}
// DetectModuleSpecifierCaseBySort detects the order of module specifiers based on import statements throughout the module/file
func DetectModuleSpecifierCaseBySort(importDeclsByGroup [][]*ast.Statement, comparersToTest []func(a, b string) int) (comparer func(a, b string) int, isSorted bool) {
moduleSpecifiersByGroup := make([][]string, 0, len(importDeclsByGroup))
for _, importGroup := range importDeclsByGroup {
moduleNames := make([]string, 0, len(importGroup))
for _, decl := range importGroup {
if expr := getModuleSpecifierExpression(decl); expr != nil {
moduleNames = append(moduleNames, GetExternalModuleName(expr))
} else {
moduleNames = append(moduleNames, "")
}
}
moduleSpecifiersByGroup = append(moduleSpecifiersByGroup, moduleNames)
}
result := detectCaseSensitivityBySort(moduleSpecifiersByGroup, comparersToTest)
return result.comparer, result.isSorted
}
func detectCaseSensitivityBySort(originalGroups [][]string, comparersToTest []func(a, b string) int) caseSensitivityDetectionResult {
var bestComparer func(a, b string) int
bestDiff := math.MaxInt
for _, curComparer := range comparersToTest {
diffOfCurrentComparer := 0
for _, listToSort := range originalGroups {
if len(listToSort) <= 1 {
continue
}
diff := measureSortedness(listToSort, curComparer)
diffOfCurrentComparer += diff
}
if diffOfCurrentComparer < bestDiff {
bestDiff = diffOfCurrentComparer
bestComparer = curComparer
}
}
if bestComparer == nil && len(comparersToTest) > 0 {
bestComparer = comparersToTest[0]
}
return caseSensitivityDetectionResult{
comparer: bestComparer,
isSorted: bestDiff == 0,
}
}
func measureSortedness[T any](arr []T, comparer func(a, b T) int) int {
i := 0
for j := range len(arr) - 1 {
if comparer(arr[j], arr[j+1]) > 0 {
i++
}
}
return i
}
// GetNamedImportSpecifierComparerWithDetection returns a specifier comparer based on detecting the existing sort order within a single import statement
func GetNamedImportSpecifierComparerWithDetection(importDecl *ast.Node, sourceFile *ast.SourceFile, preferences UserPreferences) (specifierComparer func(s1, s2 *ast.Node) int, isSorted core.Tristate) {
comparersToTest, typeOrdersToTest := GetDetectionLists(preferences)
var importStmt *ast.Statement
if importDecl.Kind == ast.KindImportDeclaration {
importStmt = importDecl
}
specifierComparer = GetNamedImportSpecifierComparer(preferences, comparersToTest[0])
isSorted = core.TSUnknown
if (ResolveOrganizeImportsSort(preferences) == OrganizeImportsSortAuto || preferences.OrganizeImportsTypeOrder == OrganizeImportsTypeOrderAuto) && importStmt != nil {
detectFromDecl := detectNamedImportOrganizationBySort([]*ast.Statement{importStmt}, comparersToTest, typeOrdersToTest)
if detectFromDecl != nil {
isSorted = core.BoolToTristate(detectFromDecl.isSorted)
specifierComparer = GetNamedImportSpecifierComparer(
UserPreferences{OrganizeImportsTypeOrder: detectFromDecl.typeOrder},
detectFromDecl.namedImportComparer,
)
} else if sourceFile != nil {
allImports := FilterImportDeclarations(sourceFile.Statements.Nodes)
detectFromFile := detectNamedImportOrganizationBySort(allImports, comparersToTest, typeOrdersToTest)
if detectFromFile != nil {
isSorted = core.BoolToTristate(detectFromFile.isSorted)
specifierComparer = GetNamedImportSpecifierComparer(
UserPreferences{OrganizeImportsTypeOrder: detectFromFile.typeOrder},
detectFromFile.namedImportComparer,
)
}
}
}
return specifierComparer, isSorted
}