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 }