vendor tsgo
This commit is contained in:
954
tools/tsgo/internal/ls/organizeimports.go
Normal file
954
tools/tsgo/internal/ls/organizeimports.go
Normal file
@@ -0,0 +1,954 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user