From cfa6255c5f8b1d00c4f10558541b9bee7bc294c5 Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Wed, 10 Dec 2025 16:24:22 -0600 Subject: [PATCH] initial impl and todos --- .../ls/organizeimports/organizeimports.go | 578 +++++++++++++++++- 1 file changed, 574 insertions(+), 4 deletions(-) diff --git a/internal/ls/organizeimports/organizeimports.go b/internal/ls/organizeimports/organizeimports.go index 5b9e650aa3..fe12e27fa8 100644 --- a/internal/ls/organizeimports/organizeimports.go +++ b/internal/ls/organizeimports/organizeimports.go @@ -2,11 +2,17 @@ package organizeimports import ( "cmp" + "context" "math" + "slices" "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/checker" + "github.com/microsoft/typescript-go/internal/compiler" "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/ls/change" "github.com/microsoft/typescript-go/internal/ls/lsutil" + "github.com/microsoft/typescript-go/internal/scanner" "github.com/microsoft/typescript-go/internal/stringutil" "github.com/microsoft/typescript-go/internal/tspath" ) @@ -20,10 +26,567 @@ var ( } ) -// statement = anyImportOrRequireStatement -func GetImportDeclarationInsertIndex(sortedImports []*ast.Statement, newImport *ast.Statement, comparer func(a, b *ast.Statement) int) int { - // !!! - return len(sortedImports) +type OrganizeImportsMode int + +const ( + OrganizeImportsModeAll OrganizeImportsMode = 0 + OrganizeImportsModeSortAndCombine OrganizeImportsMode = 1 + OrganizeImportsModeRemoveUnused OrganizeImportsMode = 2 +) + +// OrganizeImports organizes imports by: +// 1. Removing unused imports +// 2. Coalescing imports from the same module +// 3. Sorting imports +func OrganizeImports( + ctx context.Context, + sourceFile *ast.SourceFile, + changeTracker *change.Tracker, + program *compiler.Program, + preferences *lsutil.UserPreferences, + mode OrganizeImportsMode, +) { + shouldSort := mode == OrganizeImportsModeSortAndCombine || mode == OrganizeImportsModeAll + shouldCombine := shouldSort + shouldRemove := mode == OrganizeImportsModeRemoveUnused || mode == OrganizeImportsModeAll + + topLevelImportDecls := filterImportDeclarations(sourceFile.Statements.Nodes) + topLevelImportGroupDecls := groupByNewlineContiguous(sourceFile, topLevelImportDecls) + + comparersToTest, typeOrdersToTest := getDetectionLists(preferences) + defaultComparer := comparersToTest[0] + + moduleSpecifierComparer := defaultComparer + namedImportComparer := defaultComparer + typeOrder := lsutil.OrganizeImportsTypeOrderLast + if preferences != nil && preferences.OrganizeImportsTypeOrder != lsutil.OrganizeImportsTypeOrderAuto { + typeOrder = preferences.OrganizeImportsTypeOrder + } + + if preferences == nil || preferences.OrganizeImportsIgnoreCase.IsUnknown() { + result := detectModuleSpecifierCaseBySort(topLevelImportGroupDecls, comparersToTest) + moduleSpecifierComparer = result.comparer + } + + if typeOrder == lsutil.OrganizeImportsTypeOrderAuto || (preferences != nil && preferences.OrganizeImportsIgnoreCase.IsUnknown()) { + namedImportSort := detectNamedImportOrganizationBySort(topLevelImportDecls, comparersToTest, typeOrdersToTest) + if namedImportSort != nil { + if namedImportComparer == nil || (preferences != nil && preferences.OrganizeImportsIgnoreCase.IsUnknown()) { + namedImportComparer = namedImportSort.namedImportComparer + } + if typeOrder == lsutil.OrganizeImportsTypeOrderAuto { + typeOrder = namedImportSort.typeOrder + } + } + } + + comparer := comparerSettings{ + moduleSpecifierComparer: moduleSpecifierComparer, + namedImportComparer: namedImportComparer, + typeOrder: typeOrder, + } + + for _, importGroupDecl := range topLevelImportGroupDecls { + organizeImportsWorker(importGroupDecl, comparer, shouldSort, shouldCombine, shouldRemove, sourceFile, program, changeTracker, ctx) + } + + // TODO: Handle exports when mode != RemoveUnused + + // TODO: Handle ambient modules +} + +type comparerSettings struct { + moduleSpecifierComparer func(a, b string) int + namedImportComparer func(a, b string) int + typeOrder lsutil.OrganizeImportsTypeOrder +} + +func organizeImportsWorker( + oldImportDecls []*ast.Statement, + comparer comparerSettings, + shouldSort bool, + shouldCombine bool, + shouldRemove bool, + sourceFile *ast.SourceFile, + program *compiler.Program, + changeTracker *change.Tracker, + ctx context.Context, +) { + if len(oldImportDecls) == 0 { + return + } + + // TODO: Set EmitFlags.NoLeadingComments on first import + + processedImports := oldImportDecls + if shouldRemove { + processedImports = removeUnusedImports(processedImports, sourceFile, program, ctx) + } + + var newImportDecls []*ast.Statement + if shouldCombine { + grouped := groupByModuleSpecifier(processedImports) + if shouldSort { + slices.SortFunc(grouped, func(a, b []*ast.Statement) int { + if len(a) == 0 || len(b) == 0 { + return 0 + } + return compareModuleSpecifiersWorker( + a[0].ModuleSpecifier(), + b[0].ModuleSpecifier(), + comparer.moduleSpecifierComparer, + ) + }) + } + + specifierComparer := GetNamedImportSpecifierComparer( + &lsutil.UserPreferences{OrganizeImportsTypeOrder: comparer.typeOrder}, + comparer.namedImportComparer, + ) + + for _, importGroup := range grouped { + coalesced := coalesceImportsWorker(importGroup, comparer.moduleSpecifierComparer, specifierComparer, sourceFile) + newImportDecls = append(newImportDecls, coalesced...) + } + } else { + newImportDecls = processedImports + } + + if shouldSort && !shouldCombine { + slices.SortFunc(newImportDecls, func(a, b *ast.Statement) int { + return CompareImportsOrRequireStatements(a, b, comparer.moduleSpecifierComparer) + }) + } + + // TODO: Apply changes using changeTracker + _ = newImportDecls +} + +func filterImportDeclarations(statements []*ast.Statement) []*ast.Statement { + var result []*ast.Statement + for _, stmt := range statements { + if stmt.Kind == ast.KindImportDeclaration { + result = append(result, stmt) + } + } + return result +} + +func groupByModuleSpecifier(imports []*ast.Statement) [][]*ast.Statement { + groups := make(map[string][]*ast.Statement) + var order []string + + for _, imp := range imports { + specifier := getExternalModuleName(imp.ModuleSpecifier()) + if _, exists := groups[specifier]; !exists { + order = append(order, specifier) + } + groups[specifier] = append(groups[specifier], imp) + } + + result := make([][]*ast.Statement, 0, len(order)) + for _, key := range order { + result = append(result, groups[key]) + } + return result +} + +func removeUnusedImports(oldImports []*ast.Statement, sourceFile *ast.SourceFile, program *compiler.Program, ctx context.Context) []*ast.Statement { + typeChecker, done := program.GetTypeCheckerForFile(ctx, sourceFile) + defer done() + + // TODO: Get JSX namespace and fragment factory + jsxNamespace := typeChecker.GetJsxNamespace(sourceFile.AsNode()) + _ = jsxNamespace + + // TODO: Check if JSX elements are present + jsxElementsPresent := false + _ = jsxElementsPresent + + usedImports := make([]*ast.Statement, 0, len(oldImports)) + + for _, importDecl := range oldImports { + importClause := importDecl.AsImportDeclaration().ImportClause + if importClause == nil { + usedImports = append(usedImports, importDecl) + continue + } + + clause := importClause.AsImportClause() + name := clause.Name() + namedBindings := clause.NamedBindings + + if name != nil && !isDeclarationUsed(name.AsIdentifier(), typeChecker, sourceFile) { + name = nil + } + + if namedBindings != nil { + if namedBindings.Kind == ast.KindNamespaceImport { + nsImport := namedBindings.AsNamespaceImport() + if !isDeclarationUsed(nsImport.Name().AsIdentifier(), typeChecker, sourceFile) { + namedBindings = nil + } + } else if namedBindings.Kind == ast.KindNamedImports { + namedImports := namedBindings.AsNamedImports() + newElements := filterUsedImportSpecifiers(namedImports.Elements.Nodes, typeChecker, sourceFile) + if len(newElements) == 0 { + namedBindings = nil + } else if len(newElements) < len(namedImports.Elements.Nodes) { + // TODO: Create updated named imports + _ = newElements + } + } + } + + if name != nil || namedBindings != nil { + // TODO: Create updated import declaration + usedImports = append(usedImports, importDecl) + } else { + moduleSpecifier := importDecl.ModuleSpecifier() + if hasModuleDeclarationMatchingSpecifier(sourceFile, moduleSpecifier) { + if sourceFile.IsDeclarationFile { + // TODO: Create import without clause + usedImports = append(usedImports, importDecl) + } else { + usedImports = append(usedImports, importDecl) + } + } + } + } + + return usedImports +} + +func isDeclarationUsed(identifier *ast.Identifier, typeChecker *checker.Checker, sourceFile *ast.SourceFile) bool { + // TODO: Check for JSX factory usage + // TODO: Implement isSymbolReferencedInFile from FindAllReferences + _ = identifier + _ = typeChecker + _ = sourceFile + return true +} + +func filterUsedImportSpecifiers(elements []*ast.Statement, typeChecker *checker.Checker, sourceFile *ast.SourceFile) []*ast.Statement { + var result []*ast.Statement + for _, elem := range elements { + spec := elem.AsImportSpecifier() + if isDeclarationUsed(spec.Name().AsIdentifier(), typeChecker, sourceFile) { + result = append(result, elem) + } + } + return result +} + +func hasModuleDeclarationMatchingSpecifier(sourceFile *ast.SourceFile, moduleSpecifier *ast.Expression) bool { + if moduleSpecifier == nil || !ast.IsStringLiteral(moduleSpecifier.AsNode()) { + return false + } + moduleSpecifierText := moduleSpecifier.Text() + + // TODO: Check source file module augmentations + _ = moduleSpecifierText + return false +} + +func coalesceImportsWorker( + importDecls []*ast.Statement, + comparer func(a, b string) int, + specifierComparer func(s1, s2 *ast.Node) int, + sourceFile *ast.SourceFile, +) []*ast.Statement { + if len(importDecls) == 0 { + return importDecls + } + + // TODO: Group by attributes + categorized := getCategorizedImports(importDecls) + + coalescedImports := make([]*ast.Statement, 0) + + if categorized.importWithoutClause != nil { + coalescedImports = append(coalescedImports, categorized.importWithoutClause) + } + + for _, group := range []importGroup{categorized.regularImports, categorized.typeOnlyImports} { + if group.isEmpty() { + continue + } + + slices.SortFunc(group.namespaceImports, func(a, b *ast.Statement) int { + n1 := a.AsImportDeclaration().ImportClause.AsImportClause().NamedBindings.AsNamespaceImport().Name() + n2 := b.AsImportDeclaration().ImportClause.AsImportClause().NamedBindings.AsNamespaceImport().Name() + return comparer(n1.Text(), n2.Text()) + }) + + for _, nsImport := range group.namespaceImports { + coalescedImports = append(coalescedImports, nsImport) + } + + // TODO: Handle default and named imports combination + _ = group.defaultImports + _ = group.namedImports + _ = specifierComparer + } + + return coalescedImports +} + +type categorizedImports struct { + importWithoutClause *ast.Statement + typeOnlyImports importGroup + regularImports importGroup +} + +type importGroup struct { + defaultImports []*ast.Statement + namespaceImports []*ast.Statement + namedImports []*ast.Statement +} + +func (g importGroup) isEmpty() bool { + return len(g.defaultImports) == 0 && len(g.namespaceImports) == 0 && len(g.namedImports) == 0 +} + +func getCategorizedImports(importDecls []*ast.Statement) categorizedImports { + var importWithoutClause *ast.Statement + var typeOnlyImports, regularImports importGroup + + for _, importDecl := range importDecls { + if importDecl.ImportClause == nil { + if importWithoutClause == nil { + importWithoutClause = importDecl + } + continue + } + + clause := importDecl.AsImportDeclaration().ImportClause.AsImportClause() + group := ®ularImports + if clause.IsTypeOnly() { + group = &typeOnlyImports + } + + name := clause.Name() + namedBindings := clause.NamedBindings + + if name != nil { + group.defaultImports = append(group.defaultImports, importDecl) + } + + if namedBindings != nil { + if namedBindings.Kind == ast.KindNamespaceImport { + group.namespaceImports = append(group.namespaceImports, importDecl) + } else if namedBindings.Kind == ast.KindNamedImports { + group.namedImports = append(group.namedImports, importDecl) + } + } + } + + return categorizedImports{ + importWithoutClause: importWithoutClause, + typeOnlyImports: typeOnlyImports, + regularImports: regularImports, + } +} + +func groupByNewlineContiguous(sourceFile *ast.SourceFile, decls []*ast.Statement) [][]*ast.Statement { + s := scanner.NewScanner() + var groups [][]*ast.Statement + var currentGroup []*ast.Statement + + for _, decl := range decls { + if len(currentGroup) > 0 && isNewGroup(sourceFile, decl, s) { + groups = append(groups, currentGroup) + currentGroup = nil + } + currentGroup = append(currentGroup, decl) + } + + if len(currentGroup) > 0 { + groups = append(groups, currentGroup) + } + + return groups +} + +func isNewGroup(sourceFile *ast.SourceFile, decl *ast.Statement, s *scanner.Scanner) bool { + startPos := scanner.SkipTrivia(sourceFile.Text(), decl.Pos()) + fullStart := decl.Pos() + triviaLen := startPos - fullStart + s.SetText(sourceFile.Text()[fullStart:startPos]) + + numberOfNewLines := 0 + for s.TokenStart() < triviaLen { + tokenKind := s.Scan() + if tokenKind == ast.KindNewLineTrivia { + numberOfNewLines++ + if numberOfNewLines >= 2 { + return true + } + } + } + + return false +} + +func getDetectionLists(preferences *lsutil.UserPreferences) (comparersToTest []func(a, b string) int, typeOrdersToTest []lsutil.OrganizeImportsTypeOrder) { + if preferences != nil && !preferences.OrganizeImportsIgnoreCase.IsUnknown() { + ignoreCase := preferences.OrganizeImportsIgnoreCase.IsTrue() + comparersToTest = []func(a, b string) int{getOrganizeImportsOrdinalStringComparer(ignoreCase)} + } else { + comparersToTest = []func(a, b string) int{ + getOrganizeImportsOrdinalStringComparer(true), + getOrganizeImportsOrdinalStringComparer(false), + } + } + + if preferences != nil && preferences.OrganizeImportsTypeOrder != lsutil.OrganizeImportsTypeOrderAuto { + typeOrdersToTest = []lsutil.OrganizeImportsTypeOrder{preferences.OrganizeImportsTypeOrder} + } else { + typeOrdersToTest = []lsutil.OrganizeImportsTypeOrder{ + lsutil.OrganizeImportsTypeOrderLast, + lsutil.OrganizeImportsTypeOrderInline, + lsutil.OrganizeImportsTypeOrderFirst, + } + } + + return comparersToTest, typeOrdersToTest +} + +type namedImportSortResult struct { + namedImportComparer func(a, b string) int + typeOrder lsutil.OrganizeImportsTypeOrder + isSorted bool +} + +func detectNamedImportOrganizationBySort( + originalGroups []*ast.Statement, + comparersToTest []func(a, b string) int, + typesToTest []lsutil.OrganizeImportsTypeOrder, +) *namedImportSortResult { + var bothNamedImports bool + var importDeclsWithNamed []*ast.Statement + + for _, imp := range originalGroups { + if imp.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 := lsutil.OrganizeImportsTypeOrderLast + if len(typesToTest) == 1 { + typeOrder = typesToTest[0] + } + return &namedImportSortResult{ + namedImportComparer: sortState.comparer, + typeOrder: typeOrder, + isSorted: sortState.isSorted, + } + } + + bestDiff := map[lsutil.OrganizeImportsTypeOrder]int{ + lsutil.OrganizeImportsTypeOrderFirst: math.MaxInt, + lsutil.OrganizeImportsTypeOrderLast: math.MaxInt, + lsutil.OrganizeImportsTypeOrderInline: math.MaxInt, + } + bestComparer := map[lsutil.OrganizeImportsTypeOrder]func(a, b string) int{ + lsutil.OrganizeImportsTypeOrderFirst: comparersToTest[0], + lsutil.OrganizeImportsTypeOrderLast: comparersToTest[0], + lsutil.OrganizeImportsTypeOrderInline: comparersToTest[0], + } + + for _, curComparer := range comparersToTest { + currDiff := map[lsutil.OrganizeImportsTypeOrder]int{ + lsutil.OrganizeImportsTypeOrderFirst: 0, + lsutil.OrganizeImportsTypeOrderLast: 0, + lsutil.OrganizeImportsTypeOrderInline: 0, + } + + for _, importDecl := range namedImportsByDecl { + for _, typeOrder := range typesToTest { + prefs := &lsutil.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[lsutil.OrganizeImportsTypeOrderLast], + typeOrder: lsutil.OrganizeImportsTypeOrderLast, + isSorted: bestDiff[lsutil.OrganizeImportsTypeOrderLast] == 0, + } } func getOrganizeImportsOrdinalStringComparer(ignoreCase bool) func(a, b string) int { @@ -179,6 +742,13 @@ func GetImportSpecifierInsertionIndex(sortedImports []*ast.Node, newImport *ast. })) } +// GetImportDeclarationInsertIndex finds the insertion index for 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 detects the string comparer to use based on existing imports func GetOrganizeImportsStringComparerWithDetection(originalImportDecls []*ast.Statement, preferences *lsutil.UserPreferences) (comparer func(a, b string) int, isSorted bool) { result := detectModuleSpecifierCaseBySort([][]*ast.Statement{originalImportDecls}, getComparers(preferences))