955 lines
30 KiB
Go
955 lines
30 KiB
Go
package ls
|
|
|
|
import (
|
|
"context"
|
|
"slices"
|
|
"strings"
|
|
|
|
"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/lsp/lsproto"
|
|
"github.com/microsoft/typescript-go/internal/printer"
|
|
"github.com/microsoft/typescript-go/internal/scanner"
|
|
"github.com/microsoft/typescript-go/internal/stringutil"
|
|
)
|
|
|
|
// OrganizeImports organizes imports by:
|
|
// 1. Removing unused imports
|
|
// 2. Coalescing imports from the same module
|
|
// 3. Sorting imports
|
|
func (l *LanguageService) OrganizeImports(
|
|
ctx context.Context,
|
|
sourceFile *ast.SourceFile,
|
|
program *compiler.Program,
|
|
kind lsproto.CodeActionKind,
|
|
) map[string][]*lsproto.TextEdit {
|
|
changeTracker := change.NewTracker(ctx, program.Options(), l.FormatOptions(), l.converters)
|
|
shouldSort := kind == lsproto.CodeActionKindSourceSortImports || kind == lsproto.CodeActionKindSourceOrganizeImports
|
|
shouldCombine := shouldSort
|
|
shouldRemove := kind == lsproto.CodeActionKindSourceRemoveUnusedImports || kind == lsproto.CodeActionKindSourceOrganizeImports
|
|
topLevelImportDecls := lsutil.FilterImportDeclarations(sourceFile.Statements.Nodes)
|
|
topLevelImportGroupDecls := groupByNewlineContiguous(sourceFile, topLevelImportDecls)
|
|
|
|
preferences := l.UserPreferences()
|
|
comparersToTest, typeOrdersToTest := lsutil.GetDetectionLists(preferences)
|
|
defaultComparer := comparersToTest[0]
|
|
sort := lsutil.ResolveOrganizeImportsSort(preferences)
|
|
|
|
var moduleSpecifierComparer func(a, b string) int
|
|
var namedImportComparer func(a, b string) int
|
|
if sort != lsutil.OrganizeImportsSortAuto {
|
|
moduleSpecifierComparer = defaultComparer
|
|
namedImportComparer = defaultComparer
|
|
}
|
|
typeOrder := preferences.OrganizeImportsTypeOrder
|
|
|
|
if sort == lsutil.OrganizeImportsSortAuto {
|
|
result, _ := lsutil.DetectModuleSpecifierCaseBySort(topLevelImportGroupDecls, comparersToTest)
|
|
moduleSpecifierComparer = result
|
|
}
|
|
|
|
if typeOrder == lsutil.OrganizeImportsTypeOrderAuto || sort == lsutil.OrganizeImportsSortAuto {
|
|
namedImportComparer2, typeOrder2, found := lsutil.DetectNamedImportOrganizationBySort(topLevelImportDecls, comparersToTest, typeOrdersToTest)
|
|
if found {
|
|
if namedImportComparer == nil || sort == lsutil.OrganizeImportsSortAuto {
|
|
namedImportComparer = namedImportComparer2
|
|
}
|
|
if typeOrder == lsutil.OrganizeImportsTypeOrderAuto {
|
|
typeOrder = typeOrder2
|
|
}
|
|
}
|
|
}
|
|
|
|
comparer := organizeImportsComparerSettings{
|
|
moduleSpecifierComparer: moduleSpecifierComparer,
|
|
namedImportComparer: namedImportComparer,
|
|
typeOrder: typeOrder,
|
|
}
|
|
|
|
for _, importGroupDecl := range topLevelImportGroupDecls {
|
|
organizeImportsWorker(importGroupDecl, comparer, shouldSort, shouldCombine, shouldRemove, sourceFile, program, changeTracker, ctx)
|
|
}
|
|
|
|
if kind != lsproto.CodeActionKindSourceRemoveUnusedImports {
|
|
topLevelExportGroupDecls := getTopLevelExportGroups(sourceFile)
|
|
for _, exportGroupDecl := range topLevelExportGroupDecls {
|
|
organizeExportsWorker(exportGroupDecl, comparer, sourceFile, changeTracker)
|
|
}
|
|
}
|
|
|
|
for _, stmt := range sourceFile.Statements.Nodes {
|
|
if !ast.IsAmbientModule(stmt.AsNode()) {
|
|
continue
|
|
}
|
|
|
|
ambientModule := stmt.AsModuleDeclaration()
|
|
if ambientModule.Body == nil {
|
|
continue
|
|
}
|
|
|
|
moduleBody := ambientModule.Body.AsModuleBlock()
|
|
|
|
ambientModuleImportDecls := lsutil.FilterImportDeclarations(moduleBody.Statements.Nodes)
|
|
ambientModuleImportGroupDecls := groupByNewlineContiguous(sourceFile, ambientModuleImportDecls)
|
|
|
|
for _, importGroupDecl := range ambientModuleImportGroupDecls {
|
|
organizeImportsWorker(importGroupDecl, comparer, shouldSort, shouldCombine, shouldRemove, sourceFile, program, changeTracker, ctx)
|
|
}
|
|
|
|
if kind != lsproto.CodeActionKindSourceRemoveUnusedImports {
|
|
var ambientModuleExportDecls []*ast.Statement
|
|
for _, s := range moduleBody.Statements.Nodes {
|
|
if s.Kind == ast.KindExportDeclaration {
|
|
ambientModuleExportDecls = append(ambientModuleExportDecls, s)
|
|
}
|
|
}
|
|
organizeExportsWorker(ambientModuleExportDecls, comparer, sourceFile, changeTracker)
|
|
}
|
|
}
|
|
|
|
return changeTracker.GetChanges()
|
|
}
|
|
|
|
type organizeImportsComparerSettings struct {
|
|
moduleSpecifierComparer func(a, b string) int
|
|
namedImportComparer func(a, b string) int
|
|
typeOrder lsutil.OrganizeImportsTypeOrder
|
|
}
|
|
|
|
func organizeImportsWorker(
|
|
oldImportDecls []*ast.Statement,
|
|
comparer organizeImportsComparerSettings,
|
|
shouldSort bool,
|
|
shouldCombine bool,
|
|
shouldRemove bool,
|
|
sourceFile *ast.SourceFile,
|
|
program *compiler.Program,
|
|
changeTracker *change.Tracker,
|
|
ctx context.Context,
|
|
) {
|
|
if len(oldImportDecls) == 0 {
|
|
return
|
|
}
|
|
|
|
// Header comment preservation is handled via LeadingTriviaOptionExclude in the change tracker below
|
|
|
|
processedImports := slices.Clone(oldImportDecls)
|
|
if shouldRemove {
|
|
typeChecker, done := program.GetTypeCheckerForFile(ctx, sourceFile)
|
|
defer done()
|
|
processedImports = removeUnusedImports(processedImports, sourceFile, typeChecker, program, changeTracker)
|
|
}
|
|
|
|
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 lsutil.CompareModuleSpecifiers(
|
|
a[0].ModuleSpecifier(),
|
|
b[0].ModuleSpecifier(),
|
|
comparer.moduleSpecifierComparer,
|
|
)
|
|
})
|
|
}
|
|
|
|
specifierComparer := lsutil.GetNamedImportSpecifierComparer(
|
|
lsutil.UserPreferences{OrganizeImportsTypeOrder: comparer.typeOrder},
|
|
comparer.namedImportComparer,
|
|
)
|
|
|
|
for _, importGroup := range grouped {
|
|
coalesced := coalesceImportsWorker(importGroup, comparer.moduleSpecifierComparer, specifierComparer, sourceFile, changeTracker)
|
|
if shouldSort {
|
|
slices.SortFunc(coalesced, func(a, b *ast.Statement) int {
|
|
return lsutil.CompareImportsOrRequireStatements(a, b, comparer.moduleSpecifierComparer)
|
|
})
|
|
}
|
|
newImportDecls = append(newImportDecls, coalesced...)
|
|
}
|
|
} else {
|
|
newImportDecls = processedImports
|
|
}
|
|
|
|
if shouldSort && !shouldCombine {
|
|
slices.SortFunc(newImportDecls, func(a, b *ast.Statement) int {
|
|
return lsutil.CompareImportsOrRequireStatements(a, b, comparer.moduleSpecifierComparer)
|
|
})
|
|
}
|
|
|
|
if len(newImportDecls) == 0 {
|
|
changeTracker.DeleteNodeRange(
|
|
sourceFile,
|
|
oldImportDecls[0].AsNode(),
|
|
oldImportDecls[len(oldImportDecls)-1].AsNode(),
|
|
change.LeadingTriviaOptionExclude, // Preserve header comment
|
|
change.TrailingTriviaOptionInclude,
|
|
)
|
|
} else {
|
|
for _, imp := range newImportDecls {
|
|
changeTracker.SetEmitFlags(imp.AsNode(), printer.EFNoLeadingComments)
|
|
}
|
|
|
|
options := change.NodeOptions{
|
|
LeadingTriviaOption: change.LeadingTriviaOptionExclude, // Preserve header comment
|
|
TrailingTriviaOption: change.TrailingTriviaOptionInclude,
|
|
Suffix: "\n",
|
|
}
|
|
|
|
newNodes := core.Map(newImportDecls, func(s *ast.Statement) *ast.Node { return s.AsNode() })
|
|
changeTracker.ReplaceNodeWithNodes(sourceFile, oldImportDecls[0].AsNode(), newNodes, &options)
|
|
|
|
if len(oldImportDecls) > 1 {
|
|
for i := 1; i < len(oldImportDecls); i++ {
|
|
changeTracker.Delete(sourceFile, oldImportDecls[i].AsNode())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func groupByModuleSpecifier(imports []*ast.Statement) [][]*ast.Statement {
|
|
groups := make(map[string][]*ast.Statement)
|
|
var order []string
|
|
|
|
for _, imp := range imports {
|
|
specifier := lsutil.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, typeChecker *checker.Checker, program *compiler.Program, changeTracker *change.Tracker) []*ast.Statement {
|
|
compilerOptions := program.Options()
|
|
jsxElementsPresent := (sourceFile.AsNode().SubtreeFacts() & ast.SubtreeContainsJsx) != 0
|
|
jsxModeNeedsExplicitImport := compilerOptions.Jsx == core.JsxEmitReact || compilerOptions.Jsx == core.JsxEmitReactNative
|
|
|
|
factory := ast.NewNodeFactory(ast.NodeFactoryHooks{})
|
|
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 && !typeChecker.IsDeclarationUsed(sourceFile, name.AsIdentifier(), jsxElementsPresent, jsxModeNeedsExplicitImport) {
|
|
name = nil
|
|
}
|
|
|
|
if namedBindings != nil {
|
|
switch namedBindings.Kind {
|
|
case ast.KindNamespaceImport:
|
|
nsImport := namedBindings.AsNamespaceImport()
|
|
if !typeChecker.IsDeclarationUsed(sourceFile, nsImport.Name().AsIdentifier(), jsxElementsPresent, jsxModeNeedsExplicitImport) {
|
|
namedBindings = nil
|
|
}
|
|
case ast.KindNamedImports:
|
|
namedImports := namedBindings.AsNamedImports()
|
|
originalBindings := namedBindings
|
|
newElements := filterUsedImportSpecifiers(namedImports.Elements.Nodes, typeChecker, sourceFile, jsxElementsPresent, jsxModeNeedsExplicitImport)
|
|
if len(newElements) == 0 {
|
|
namedBindings = nil
|
|
} else if len(newElements) < len(namedImports.Elements.Nodes) {
|
|
newList := factory.NewNodeList(newElements)
|
|
updatedNamedImports := factory.UpdateNamedImports(namedImports, newList)
|
|
namedBindings = updatedNamedImports.AsNode()
|
|
}
|
|
if namedBindings != nil && !ast.NodeIsSynthesized(originalBindings.AsNode()) && !printer.RangeIsOnSingleLine(originalBindings.Loc, sourceFile) {
|
|
changeTracker.SetEmitFlags(namedBindings, printer.EFMultiLine)
|
|
}
|
|
}
|
|
}
|
|
|
|
if name != nil || namedBindings != nil {
|
|
importDeclNode := importDecl.AsImportDeclaration()
|
|
newClause := factory.UpdateImportClause(clause, clause.PhaseModifier, name, namedBindings)
|
|
newImportDecl := factory.UpdateImportDeclaration(
|
|
importDeclNode,
|
|
importDeclNode.Modifiers(),
|
|
newClause.AsNode(),
|
|
importDeclNode.ModuleSpecifier,
|
|
importDeclNode.Attributes,
|
|
)
|
|
usedImports = append(usedImports, newImportDecl)
|
|
} else {
|
|
moduleSpecifier := importDecl.ModuleSpecifier()
|
|
if hasModuleDeclarationMatchingSpecifier(sourceFile, moduleSpecifier) {
|
|
if sourceFile.IsDeclarationFile {
|
|
importDeclNode := importDecl.AsImportDeclaration()
|
|
newImportDecl := factory.UpdateImportDeclaration(
|
|
importDeclNode,
|
|
importDeclNode.Modifiers(),
|
|
nil, // no import clause
|
|
importDeclNode.ModuleSpecifier,
|
|
importDeclNode.Attributes,
|
|
)
|
|
usedImports = append(usedImports, newImportDecl)
|
|
} else {
|
|
usedImports = append(usedImports, importDecl)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return usedImports
|
|
}
|
|
|
|
func filterUsedImportSpecifiers(
|
|
elements []*ast.Statement,
|
|
typeChecker *checker.Checker,
|
|
sourceFile *ast.SourceFile,
|
|
jsxElementsPresent bool,
|
|
jsxModeNeedsExplicitImport bool,
|
|
) []*ast.Statement {
|
|
var result []*ast.Statement
|
|
for _, elem := range elements {
|
|
spec := elem.AsImportSpecifier()
|
|
if typeChecker.IsDeclarationUsed(sourceFile, spec.Name().AsIdentifier(), jsxElementsPresent, jsxModeNeedsExplicitImport) {
|
|
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()
|
|
|
|
for _, moduleName := range sourceFile.ModuleAugmentations {
|
|
if ast.IsStringLiteral(moduleName) && moduleName.Text() == moduleSpecifierText {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// getImportAttributesKey returns a key for grouping imports by their attributes.
|
|
func getImportAttributesKey(attributes *ast.ImportAttributesNode) string {
|
|
if attributes == nil {
|
|
return ""
|
|
}
|
|
|
|
importAttrs := attributes.AsImportAttributes()
|
|
var key strings.Builder
|
|
key.WriteString(importAttrs.Token.String())
|
|
key.WriteString(" ")
|
|
|
|
attrNodes := make([]*ast.Node, len(importAttrs.Attributes.Nodes))
|
|
copy(attrNodes, importAttrs.Attributes.Nodes)
|
|
slices.SortFunc(attrNodes, func(a, b *ast.Node) int {
|
|
aName := a.AsImportAttribute().Name().Text()
|
|
bName := b.AsImportAttribute().Name().Text()
|
|
return stringutil.CompareStringsCaseSensitive(aName, bName)
|
|
})
|
|
|
|
for _, attrNode := range attrNodes {
|
|
attr := attrNode.AsImportAttribute()
|
|
key.WriteString(attr.Name().Text())
|
|
key.WriteString(":")
|
|
if ast.IsStringLiteralLike(attr.Value.AsNode()) {
|
|
key.WriteString(`"`)
|
|
key.WriteString(attr.Value.Text())
|
|
key.WriteString(`"`)
|
|
} else {
|
|
key.WriteString(attr.Value.AsNode().Text())
|
|
}
|
|
key.WriteString(" ")
|
|
}
|
|
|
|
return key.String()
|
|
}
|
|
|
|
// groupByNewlineContiguous groups declarations by blank lines between them.
|
|
func groupByNewlineContiguous(sourceFile *ast.SourceFile, decls []*ast.Statement) [][]*ast.Statement {
|
|
s := scanner.NewScanner()
|
|
s.SetSkipTrivia(false) // Must not skip trivia to detect newlines
|
|
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 {
|
|
fullStart := decl.Pos()
|
|
if fullStart < 0 {
|
|
return false
|
|
}
|
|
|
|
text := sourceFile.Text()
|
|
textLen := len(text)
|
|
|
|
if fullStart >= textLen {
|
|
return false
|
|
}
|
|
|
|
startPos := scanner.SkipTrivia(text, fullStart)
|
|
if startPos <= fullStart {
|
|
return false
|
|
}
|
|
|
|
triviaLen := startPos - fullStart
|
|
s.SetText(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 coalesceImportsWorker(
|
|
importDecls []*ast.Statement,
|
|
comparer func(a, b string) int,
|
|
specifierComparer func(s1, s2 *ast.Node) int,
|
|
sourceFile *ast.SourceFile,
|
|
changeTracker *change.Tracker,
|
|
) []*ast.Statement {
|
|
if len(importDecls) == 0 {
|
|
return importDecls
|
|
}
|
|
|
|
importGroupsByAttributes := make(map[string][]*ast.Statement)
|
|
var attributeKeys []string
|
|
|
|
for _, importDecl := range importDecls {
|
|
key := getImportAttributesKey(importDecl.AsImportDeclaration().Attributes)
|
|
if _, exists := importGroupsByAttributes[key]; !exists {
|
|
attributeKeys = append(attributeKeys, key)
|
|
}
|
|
importGroupsByAttributes[key] = append(importGroupsByAttributes[key], importDecl)
|
|
}
|
|
|
|
coalescedImports := make([]*ast.Statement, 0)
|
|
|
|
for _, attributeKey := range attributeKeys {
|
|
importGroupSameAttrs := importGroupsByAttributes[attributeKey]
|
|
categorized := getCategorizedImports(importGroupSameAttrs)
|
|
|
|
if categorized.importWithoutClause != nil {
|
|
coalescedImports = append(coalescedImports, categorized.importWithoutClause)
|
|
}
|
|
|
|
factory := ast.NewNodeFactory(ast.NodeFactoryHooks{})
|
|
|
|
for i, group := range []importGroup{categorized.regularImports, categorized.typeOnlyImports} {
|
|
if group.isEmpty() {
|
|
continue
|
|
}
|
|
|
|
isTypeOnly := i == 1
|
|
|
|
if !isTypeOnly && len(group.defaultImports) == 1 && len(group.namespaceImports) == 1 && len(group.namedImports) == 0 {
|
|
defaultImport := group.defaultImports[0]
|
|
namespaceImport := group.namespaceImports[0]
|
|
|
|
defaultClause := defaultImport.AsImportDeclaration().ImportClause.AsImportClause()
|
|
namespaceBindings := namespaceImport.AsImportDeclaration().ImportClause.AsImportClause().NamedBindings
|
|
|
|
newClause := factory.UpdateImportClause(defaultClause, defaultClause.PhaseModifier, defaultClause.Name(), namespaceBindings)
|
|
defaultDeclNode := defaultImport.AsImportDeclaration()
|
|
newImportDecl := factory.UpdateImportDeclaration(
|
|
defaultDeclNode,
|
|
defaultDeclNode.Modifiers(),
|
|
newClause,
|
|
defaultDeclNode.ModuleSpecifier,
|
|
defaultDeclNode.Attributes,
|
|
)
|
|
coalescedImports = append(coalescedImports, newImportDecl)
|
|
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 {
|
|
nsImportDecl := nsImport.AsImportDeclaration()
|
|
clause := nsImportDecl.ImportClause.AsImportClause()
|
|
newClause := factory.UpdateImportClause(clause, clause.PhaseModifier, nil, clause.NamedBindings)
|
|
newImportDecl := factory.UpdateImportDeclaration(
|
|
nsImportDecl,
|
|
nsImportDecl.Modifiers(),
|
|
newClause,
|
|
nsImportDecl.ModuleSpecifier,
|
|
nsImportDecl.Attributes,
|
|
)
|
|
coalescedImports = append(coalescedImports, newImportDecl)
|
|
}
|
|
|
|
var firstDefaultImport *ast.Statement
|
|
var firstNamedImport *ast.Statement
|
|
|
|
if len(group.defaultImports) > 0 {
|
|
firstDefaultImport = group.defaultImports[0]
|
|
}
|
|
if len(group.namedImports) > 0 {
|
|
firstNamedImport = group.namedImports[0]
|
|
}
|
|
|
|
importDecl := firstDefaultImport
|
|
if importDecl == nil {
|
|
importDecl = firstNamedImport
|
|
}
|
|
if importDecl == nil {
|
|
continue
|
|
}
|
|
|
|
var newDefaultImport *ast.IdentifierNode
|
|
var newImportSpecifiers []*ast.Node
|
|
|
|
if len(group.defaultImports) == 1 {
|
|
newDefaultImport = group.defaultImports[0].AsImportDeclaration().ImportClause.AsImportClause().Name()
|
|
} else {
|
|
for _, defaultImport := range group.defaultImports {
|
|
defaultClause := defaultImport.AsImportDeclaration().ImportClause.AsImportClause()
|
|
defaultName := defaultClause.Name()
|
|
propertyName := factory.NewIdentifier("default")
|
|
importSpec := factory.NewImportSpecifier(false, propertyName, defaultName)
|
|
newImportSpecifiers = append(newImportSpecifiers, importSpec)
|
|
}
|
|
}
|
|
|
|
newImportSpecifiers = append(newImportSpecifiers, getNewImportSpecifiers(group.namedImports, factory)...)
|
|
slices.SortStableFunc(newImportSpecifiers, specifierComparer)
|
|
|
|
var newNamedImports *ast.NamedImportBindings
|
|
if len(newImportSpecifiers) == 0 {
|
|
if newDefaultImport != nil {
|
|
newNamedImports = nil
|
|
} else {
|
|
newNamedImports = factory.NewNamedImports(factory.NewNodeList(nil))
|
|
}
|
|
} else {
|
|
sortedList := factory.NewNodeList(newImportSpecifiers)
|
|
if firstNamedImport != nil {
|
|
firstNamedBindings := firstNamedImport.AsImportDeclaration().ImportClause.AsImportClause().NamedBindings.AsNamedImports()
|
|
originalElements := firstNamedBindings.Elements
|
|
if originalElements.HasTrailingComma() {
|
|
sortedList.Loc = originalElements.Loc
|
|
}
|
|
newNamedImports = factory.UpdateNamedImports(firstNamedBindings, sortedList).AsNode()
|
|
} else {
|
|
newNamedImports = factory.NewNamedImports(sortedList)
|
|
}
|
|
}
|
|
|
|
if sourceFile != nil && newNamedImports != nil && firstNamedImport != nil {
|
|
firstNamedBindings := firstNamedImport.AsImportDeclaration().ImportClause.AsImportClause().NamedBindings
|
|
if !ast.NodeIsSynthesized(firstNamedBindings.AsNode()) && !printer.RangeIsOnSingleLine(firstNamedBindings.Loc, sourceFile) {
|
|
changeTracker.SetEmitFlags(newNamedImports.AsNode(), printer.EFMultiLine)
|
|
}
|
|
}
|
|
|
|
if isTypeOnly && newDefaultImport != nil && newNamedImports != nil {
|
|
importDeclNode := importDecl.AsImportDeclaration()
|
|
|
|
defaultClause := factory.NewImportClause(importDeclNode.ImportClause.AsImportClause().PhaseModifier, newDefaultImport, nil)
|
|
defaultImportDecl := factory.UpdateImportDeclaration(
|
|
importDeclNode,
|
|
importDeclNode.Modifiers(),
|
|
defaultClause,
|
|
importDeclNode.ModuleSpecifier,
|
|
importDeclNode.Attributes,
|
|
)
|
|
coalescedImports = append(coalescedImports, defaultImportDecl)
|
|
|
|
namedDeclNode := firstNamedImport
|
|
if namedDeclNode == nil {
|
|
namedDeclNode = importDecl
|
|
}
|
|
namedImportDeclNode := namedDeclNode.AsImportDeclaration()
|
|
namedClause := factory.NewImportClause(namedImportDeclNode.ImportClause.AsImportClause().PhaseModifier, nil, newNamedImports)
|
|
namedImportDecl := factory.UpdateImportDeclaration(
|
|
namedImportDeclNode,
|
|
namedImportDeclNode.Modifiers(),
|
|
namedClause,
|
|
namedImportDeclNode.ModuleSpecifier,
|
|
namedImportDeclNode.Attributes,
|
|
)
|
|
coalescedImports = append(coalescedImports, namedImportDecl)
|
|
} else {
|
|
importDeclNode := importDecl.AsImportDeclaration()
|
|
clauseNode := importDeclNode.ImportClause.AsImportClause()
|
|
newClause := factory.UpdateImportClause(clauseNode, clauseNode.PhaseModifier, newDefaultImport, newNamedImports)
|
|
newImportDecl := factory.UpdateImportDeclaration(
|
|
importDeclNode,
|
|
importDeclNode.Modifiers(),
|
|
newClause,
|
|
importDeclNode.ModuleSpecifier,
|
|
importDeclNode.Attributes,
|
|
)
|
|
coalescedImports = append(coalescedImports, newImportDecl)
|
|
}
|
|
}
|
|
}
|
|
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.AsImportDeclaration().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 {
|
|
switch namedBindings.Kind {
|
|
case ast.KindNamespaceImport:
|
|
group.namespaceImports = append(group.namespaceImports, importDecl)
|
|
case ast.KindNamedImports:
|
|
group.namedImports = append(group.namedImports, importDecl)
|
|
}
|
|
}
|
|
}
|
|
|
|
return categorizedImports{
|
|
importWithoutClause: importWithoutClause,
|
|
typeOnlyImports: typeOnlyImports,
|
|
regularImports: regularImports,
|
|
}
|
|
}
|
|
|
|
func getNewImportSpecifiers(namedImports []*ast.Statement, factory *ast.NodeFactory) []*ast.Node {
|
|
var result []*ast.Node
|
|
|
|
for _, namedImport := range namedImports {
|
|
elements := tryGetNamedBindingElements(namedImport)
|
|
if elements == nil {
|
|
continue
|
|
}
|
|
|
|
for _, elem := range elements {
|
|
spec := elem.AsImportSpecifier()
|
|
|
|
if spec.PropertyName != nil && spec.Name() != nil {
|
|
propertyText := spec.PropertyName.Text()
|
|
nameText := spec.Name().Text()
|
|
|
|
if propertyText == nameText {
|
|
normalized := factory.UpdateImportSpecifier(spec, spec.IsTypeOnly, nil, spec.Name())
|
|
result = append(result, normalized)
|
|
continue
|
|
}
|
|
}
|
|
|
|
result = append(result, elem)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func tryGetNamedBindingElements(namedImport *ast.Statement) []*ast.Statement {
|
|
if namedImport.Kind != ast.KindImportDeclaration {
|
|
return nil
|
|
}
|
|
|
|
importDecl := namedImport.AsImportDeclaration()
|
|
if importDecl.ImportClause == nil {
|
|
return nil
|
|
}
|
|
|
|
clause := importDecl.ImportClause.AsImportClause()
|
|
namedBindings := clause.NamedBindings
|
|
|
|
if namedBindings != nil && namedBindings.Kind == ast.KindNamedImports {
|
|
namedImportsNode := namedBindings.AsNamedImports()
|
|
return namedImportsNode.Elements.Nodes
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getTopLevelExportGroups(sourceFile *ast.SourceFile) [][]*ast.Statement {
|
|
var topLevelExportGroups [][]*ast.Statement
|
|
statements := sourceFile.Statements.Nodes
|
|
statementsLen := len(statements)
|
|
|
|
i := 0
|
|
groupIndex := 0
|
|
for i < statementsLen {
|
|
if statements[i].Kind == ast.KindExportDeclaration {
|
|
if groupIndex >= len(topLevelExportGroups) {
|
|
topLevelExportGroups = append(topLevelExportGroups, []*ast.Statement{})
|
|
}
|
|
exportDecl := statements[i].AsExportDeclaration()
|
|
if exportDecl.ModuleSpecifier != nil {
|
|
topLevelExportGroups[groupIndex] = append(topLevelExportGroups[groupIndex], statements[i])
|
|
i++
|
|
} else {
|
|
for i < statementsLen && statements[i].Kind == ast.KindExportDeclaration {
|
|
topLevelExportGroups[groupIndex] = append(topLevelExportGroups[groupIndex], statements[i])
|
|
i++
|
|
}
|
|
groupIndex++
|
|
}
|
|
} else {
|
|
i++
|
|
if groupIndex < len(topLevelExportGroups) && len(topLevelExportGroups[groupIndex]) > 0 {
|
|
groupIndex++
|
|
}
|
|
}
|
|
}
|
|
|
|
var result [][]*ast.Statement
|
|
for _, exportGroup := range topLevelExportGroups {
|
|
subGroups := groupByNewlineContiguous(sourceFile, exportGroup)
|
|
result = append(result, subGroups...)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func organizeExportsWorker(
|
|
oldExportDecls []*ast.Statement,
|
|
comparer organizeImportsComparerSettings,
|
|
sourceFile *ast.SourceFile,
|
|
changeTracker *change.Tracker,
|
|
) {
|
|
if len(oldExportDecls) == 0 {
|
|
return
|
|
}
|
|
|
|
specifierComparerFunc := lsutil.GetNamedImportSpecifierComparer(
|
|
lsutil.UserPreferences{OrganizeImportsTypeOrder: comparer.typeOrder},
|
|
comparer.namedImportComparer,
|
|
)
|
|
|
|
newExportDecls := coalesceExportsWorker(oldExportDecls, specifierComparerFunc, comparer.moduleSpecifierComparer, sourceFile, changeTracker)
|
|
|
|
if len(oldExportDecls) > 0 {
|
|
if len(newExportDecls) == 0 {
|
|
changeTracker.DeleteNodeRange(
|
|
sourceFile,
|
|
oldExportDecls[0].AsNode(),
|
|
oldExportDecls[len(oldExportDecls)-1].AsNode(),
|
|
change.LeadingTriviaOptionExclude,
|
|
change.TrailingTriviaOptionInclude,
|
|
)
|
|
} else {
|
|
for _, exp := range newExportDecls {
|
|
changeTracker.AddEmitFlags(exp.AsNode(), printer.EFNoLeadingComments)
|
|
}
|
|
|
|
options := change.NodeOptions{
|
|
LeadingTriviaOption: change.LeadingTriviaOptionExclude,
|
|
TrailingTriviaOption: change.TrailingTriviaOptionInclude,
|
|
Suffix: "\n",
|
|
}
|
|
|
|
newNodes := core.Map(newExportDecls, func(s *ast.Statement) *ast.Node { return s.AsNode() })
|
|
changeTracker.ReplaceNodeWithNodes(sourceFile, oldExportDecls[0].AsNode(), newNodes, &options)
|
|
|
|
if len(oldExportDecls) > 1 {
|
|
for i := 1; i < len(oldExportDecls); i++ {
|
|
changeTracker.Delete(sourceFile, oldExportDecls[i].AsNode())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func coalesceExportsWorker(
|
|
exportGroup []*ast.Statement,
|
|
specifierComparer func(s1, s2 *ast.Node) int,
|
|
moduleSpecifierComparer func(a, b string) int,
|
|
sourceFile *ast.SourceFile,
|
|
changeTracker *change.Tracker,
|
|
) []*ast.Statement {
|
|
if len(exportGroup) == 0 {
|
|
return exportGroup
|
|
}
|
|
|
|
exportsByModuleSpecifier := make(map[string][]*ast.Statement)
|
|
var moduleSpecifierOrder []string
|
|
|
|
for _, exportDecl := range exportGroup {
|
|
export := exportDecl.AsExportDeclaration()
|
|
var moduleSpecifier string
|
|
if export.ModuleSpecifier != nil {
|
|
moduleSpecifier = export.ModuleSpecifier.Text()
|
|
}
|
|
if _, exists := exportsByModuleSpecifier[moduleSpecifier]; !exists {
|
|
moduleSpecifierOrder = append(moduleSpecifierOrder, moduleSpecifier)
|
|
}
|
|
exportsByModuleSpecifier[moduleSpecifier] = append(exportsByModuleSpecifier[moduleSpecifier], exportDecl)
|
|
}
|
|
|
|
slices.SortStableFunc(moduleSpecifierOrder, func(a, b string) int {
|
|
if a == "" && b != "" {
|
|
return 1
|
|
}
|
|
if a != "" && b == "" {
|
|
return -1
|
|
}
|
|
return moduleSpecifierComparer(a, b)
|
|
})
|
|
|
|
var coalescedExports []*ast.Statement
|
|
factory := ast.NewNodeFactory(ast.NodeFactoryHooks{})
|
|
|
|
for _, moduleSpecifier := range moduleSpecifierOrder {
|
|
group := exportsByModuleSpecifier[moduleSpecifier]
|
|
|
|
categorized := getCategorizedExports(group)
|
|
|
|
if categorized.exportWithoutClause != nil {
|
|
coalescedExports = append(coalescedExports, categorized.exportWithoutClause)
|
|
}
|
|
|
|
for _, subGroup := range [][]*ast.Statement{categorized.namedExports, categorized.typeOnlyExports} {
|
|
if len(subGroup) == 0 {
|
|
continue
|
|
}
|
|
|
|
var newExportSpecifiers []*ast.Node
|
|
for _, exportDecl := range subGroup {
|
|
exportClause := exportDecl.AsExportDeclaration().ExportClause
|
|
if exportClause != nil && exportClause.Kind == ast.KindNamedExports {
|
|
namedExports := exportClause.AsNamedExports()
|
|
newExportSpecifiers = append(newExportSpecifiers, namedExports.Elements.Nodes...)
|
|
}
|
|
}
|
|
|
|
slices.SortStableFunc(newExportSpecifiers, specifierComparer)
|
|
|
|
exportDecl := subGroup[0].AsExportDeclaration()
|
|
|
|
var updatedExportClause *ast.NamedExportBindings
|
|
if exportDecl.ExportClause != nil {
|
|
if exportDecl.ExportClause.Kind == ast.KindNamedExports {
|
|
namedExports := exportDecl.ExportClause.AsNamedExports()
|
|
sortedList := factory.NewNodeList(newExportSpecifiers)
|
|
updatedExportClause = factory.UpdateNamedExports(namedExports, sortedList)
|
|
|
|
if sourceFile != nil && !ast.NodeIsSynthesized(namedExports.AsNode()) && !printer.RangeIsOnSingleLine(namedExports.Loc, sourceFile) {
|
|
changeTracker.SetEmitFlags(updatedExportClause.AsNode(), printer.EFMultiLine)
|
|
}
|
|
} else {
|
|
updatedExportClause = exportDecl.ExportClause
|
|
}
|
|
}
|
|
|
|
newExportDecl := factory.UpdateExportDeclaration(
|
|
exportDecl,
|
|
exportDecl.Modifiers(),
|
|
exportDecl.IsTypeOnly,
|
|
updatedExportClause,
|
|
exportDecl.ModuleSpecifier,
|
|
exportDecl.Attributes,
|
|
)
|
|
coalescedExports = append(coalescedExports, newExportDecl)
|
|
}
|
|
}
|
|
|
|
return coalescedExports
|
|
}
|
|
|
|
type categorizedExports struct {
|
|
exportWithoutClause *ast.Statement
|
|
namedExports []*ast.Statement
|
|
typeOnlyExports []*ast.Statement
|
|
}
|
|
|
|
func getCategorizedExports(exportGroup []*ast.Statement) categorizedExports {
|
|
var exportWithoutClause *ast.Statement
|
|
var namedExports, typeOnlyExports []*ast.Statement
|
|
|
|
for _, exportDecl := range exportGroup {
|
|
export := exportDecl.AsExportDeclaration()
|
|
if export.ExportClause == nil {
|
|
if exportWithoutClause == nil {
|
|
exportWithoutClause = exportDecl
|
|
}
|
|
} else if export.IsTypeOnly {
|
|
typeOnlyExports = append(typeOnlyExports, exportDecl)
|
|
} else {
|
|
namedExports = append(namedExports, exportDecl)
|
|
}
|
|
}
|
|
|
|
return categorizedExports{
|
|
exportWithoutClause: exportWithoutClause,
|
|
namedExports: namedExports,
|
|
typeOnlyExports: typeOnlyExports,
|
|
}
|
|
}
|