package autoimport import ( "cmp" "context" "fmt" "slices" "strings" "unicode" "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/astnav" "github.com/microsoft/typescript-go/internal/checker" "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/compiler" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/debug" "github.com/microsoft/typescript-go/internal/diagnostics" "github.com/microsoft/typescript-go/internal/locale" "github.com/microsoft/typescript-go/internal/ls/change" "github.com/microsoft/typescript-go/internal/ls/lsconv" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/modulespecifiers" "github.com/microsoft/typescript-go/internal/scanner" "github.com/microsoft/typescript-go/internal/stringutil" "github.com/microsoft/typescript-go/internal/tspath" ) type newImportBinding struct { kind lsproto.ImportKind propertyName string name string addAsTypeOnly lsproto.AddAsTypeOnly } type Fix struct { *lsproto.AutoImportFix ModuleSpecifierKind modulespecifiers.ResultKind IsReExport bool ModuleFileName string TypeOnlyAliasDeclaration *ast.Declaration } type addToExistingImportFix struct { importClauseOrBindingPattern *ast.ImportClauseOrBindingPattern // One of `defaultImport` or `namedImports` will be present defaultImport *newImportBinding namedImport *newImportBinding } func (f *Fix) Edits( ctx context.Context, file *ast.SourceFile, compilerOptions *core.CompilerOptions, formatOptions lsutil.FormatCodeSettings, converters *lsconv.Converters, preferences lsutil.UserPreferences, ) ([]*lsproto.TextEdit, string) { locale := locale.FromContext(ctx) tracker := change.NewTracker(ctx, compilerOptions, formatOptions, converters) switch f.Kind { case lsproto.AutoImportFixKindUseNamespace: description := addNamespaceQualifier(f, tracker, file, locale) return tracker.GetChanges()[file.FileName()], description case lsproto.AutoImportFixKindAddToExisting: if len(file.Imports()) <= int(f.ImportIndex) { panic("import index out of range") } existingFix := getAddToExistingImportFix(file, f) addToExistingImport(tracker, file, existingFix.importClauseOrBindingPattern, existingFix.defaultImport, core.SingleElementSlice(existingFix.namedImport), preferences) return tracker.GetChanges()[file.FileName()], diagnostics.Update_import_from_0.Localize(locale, f.ModuleSpecifier) case lsproto.AutoImportFixKindAddNew: var declarations []*ast.Statement defaultImport := core.IfElse(f.ImportKind == lsproto.ImportKindDefault, &newImportBinding{name: f.Name, addAsTypeOnly: f.AddAsTypeOnly}, nil) namedImports := core.IfElse(f.ImportKind == lsproto.ImportKindNamed, []*newImportBinding{{name: f.Name, addAsTypeOnly: f.AddAsTypeOnly}}, nil) var namespaceLikeImport *newImportBinding // qualification := f.qualification() if f.ImportKind == lsproto.ImportKindNamespace || f.ImportKind == lsproto.ImportKindCommonJS { namespaceLikeImport = &newImportBinding{kind: f.ImportKind, name: f.Name} // if qualification != nil && qualification.namespacePref != "" { // namespaceLikeImport.name = qualification.namespacePref // } } quotePreference := lsutil.GetQuotePreference(file, preferences) if f.UseRequire { declarations = getNewRequires(tracker, f.ModuleSpecifier, quotePreference, defaultImport, namedImports, namespaceLikeImport, compilerOptions) } else { declarations = getNewImports(tracker, f.ModuleSpecifier, quotePreference, defaultImport, namedImports, namespaceLikeImport, compilerOptions, preferences) } insertImports( tracker, file, declarations, /*blankLineBetween*/ true, preferences, ) // if qualification != nil { // addNamespaceQualifier(tracker, file, qualification) // } return tracker.GetChanges()[file.FileName()], diagnostics.Add_import_from_0.Localize(locale, f.ModuleSpecifier) case lsproto.AutoImportFixKindPromoteTypeOnly: promotedDeclaration := promoteFromTypeOnly(tracker, f.TypeOnlyAliasDeclaration, compilerOptions, file, preferences) if promotedDeclaration.Kind == ast.KindImportSpecifier { moduleSpec := getModuleSpecifierText(promotedDeclaration.Parent.Parent) return tracker.GetChanges()[file.FileName()], diagnostics.Remove_type_from_import_of_0_from_1.Localize(locale, f.Name, moduleSpec) } moduleSpec := getModuleSpecifierText(promotedDeclaration) return tracker.GetChanges()[file.FileName()], diagnostics.Remove_type_from_import_declaration_from_0.Localize(locale, moduleSpec) case lsproto.AutoImportFixKindJsdocTypeImport: description := addImportType(f, file, preferences, tracker, locale) return tracker.GetChanges()[file.FileName()], description default: panic("unimplemented fix edit") } } func addImportType(f *Fix, file *ast.SourceFile, preferences lsutil.UserPreferences, tracker *change.Tracker, locale locale.Locale) string { if f.UsagePosition == nil { panic("UsagePosition must be set for JSDoc type import fix") } quotePreference := lsutil.GetQuotePreference(file, preferences) quoteChar := "\"" if quotePreference == lsutil.QuotePreferenceSingle { quoteChar = "'" } importTypePrefix := fmt.Sprintf("import(%s%s%s).", quoteChar, f.ModuleSpecifier, quoteChar) tracker.InsertText(file, *f.UsagePosition, importTypePrefix) return diagnostics.Change_0_to_1.Localize(locale, f.Name, importTypePrefix+f.Name) } func addNamespaceQualifier(f *Fix, tracker *change.Tracker, file *ast.SourceFile, locale locale.Locale) string { if f.UsagePosition == nil || f.NamespacePrefix == "" { panic("namespace fix requires usage position and prefix") } qualified := fmt.Sprintf("%s.%s", f.NamespacePrefix, f.Name) tracker.InsertText(file, *f.UsagePosition, f.NamespacePrefix+".") return diagnostics.Change_0_to_1.Localize(locale, f.Name, qualified) } func getAddToExistingImportFix(file *ast.SourceFile, fix *Fix) *addToExistingImportFix { if fix.Kind != lsproto.AutoImportFixKindAddToExisting { panic("expected add to existing import fix") } moduleSpecifier := file.Imports()[fix.ImportIndex] importNode := ast.TryGetImportFromModuleSpecifier(moduleSpecifier) if importNode == nil { panic("expected import declaration") } var importClauseOrBindingPattern *ast.Node switch importNode.Kind { case ast.KindImportDeclaration: importClauseOrBindingPattern = importNode.ImportClause() if importClauseOrBindingPattern == nil { panic("expected import clause") } case ast.KindCallExpression: if !ast.IsVariableDeclarationInitializedToRequire(importNode.Parent) { panic("expected require call expression to be in variable declaration") } importClauseOrBindingPattern = importNode.Parent.Name() if importClauseOrBindingPattern == nil || !ast.IsObjectBindingPattern(importClauseOrBindingPattern) { panic("expected object binding pattern in variable declaration") } default: panic("expected import declaration or require call expression") } defaultImport := core.IfElse(fix.ImportKind == lsproto.ImportKindDefault, &newImportBinding{kind: lsproto.ImportKindDefault, name: fix.Name, addAsTypeOnly: fix.AddAsTypeOnly}, nil) namedImports := core.IfElse(fix.ImportKind == lsproto.ImportKindNamed, &newImportBinding{kind: lsproto.ImportKindNamed, name: fix.Name, addAsTypeOnly: fix.AddAsTypeOnly}, nil) return &addToExistingImportFix{ importClauseOrBindingPattern: importClauseOrBindingPattern, defaultImport: defaultImport, namedImport: namedImports, } } func addToExistingImport( ct *change.Tracker, file *ast.SourceFile, importClauseOrBindingPattern *ast.Node, defaultImport *newImportBinding, namedImports []*newImportBinding, preferences lsutil.UserPreferences, ) { switch importClauseOrBindingPattern.Kind { case ast.KindObjectBindingPattern: bindingPattern := importClauseOrBindingPattern.AsBindingPattern() if defaultImport != nil { addElementToBindingPattern(ct, file, bindingPattern, defaultImport.name, "default") } for _, namedImport := range namedImports { addElementToBindingPattern(ct, file, bindingPattern, namedImport.name, "") } return case ast.KindImportClause: importClause := importClauseOrBindingPattern.AsImportClause() // promoteFromTypeOnly = true if we need to promote the entire original clause from type only promoteFromTypeOnly := importClause.IsTypeOnly() && core.Some(append(namedImports, defaultImport), func(i *newImportBinding) bool { if i == nil { return false } return i.addAsTypeOnly == lsproto.AddAsTypeOnlyNotAllowed }) var existingSpecifiers []*ast.Node if importClause.NamedBindings != nil && importClause.NamedBindings.Kind == ast.KindNamedImports { existingSpecifiers = importClause.NamedBindings.Elements() } if defaultImport != nil { debug.Assert(importClause.Name() == nil, "Cannot add a default import to an import clause that already has one") ct.InsertNodeAt(file, core.TextPos(astnav.GetStartOfNode(importClause.AsNode(), file, false)), ct.NodeFactory.NewIdentifier(defaultImport.name), change.NodeOptions{Suffix: ", "}) } if len(namedImports) > 0 { specifierComparer, isSorted := lsutil.GetNamedImportSpecifierComparerWithDetection(importClause.Parent, file, preferences) newSpecifiers := core.Map(namedImports, func(namedImport *newImportBinding) *ast.Node { var identifier *ast.Node if namedImport.propertyName != "" { identifier = ct.NodeFactory.NewIdentifier(namedImport.propertyName).AsIdentifier().AsNode() } return ct.NodeFactory.NewImportSpecifier( (!importClause.IsTypeOnly() || promoteFromTypeOnly) && shouldUseTypeOnly(namedImport.addAsTypeOnly, preferences), identifier, ct.NodeFactory.NewIdentifier(namedImport.name), ) }) slices.SortFunc(newSpecifiers, specifierComparer) if len(existingSpecifiers) > 0 && isSorted != core.TSFalse { // The sorting preference computed earlier may or may not have validated that these particular // import specifiers are sorted. If they aren't, `getImportSpecifierInsertionIndex` will return // nonsense. So if there are existing specifiers, even if we know the sorting preference, we // need to ensure that the existing specifiers are sorted according to the preference in order // to do a sorted insertion. // If we're promoting the clause from type-only, we need to transform the existing imports // before attempting to insert the new named imports (for comparison purposes only) specsToCompareAgainst := existingSpecifiers if promoteFromTypeOnly && len(existingSpecifiers) > 0 { specsToCompareAgainst = core.Map(existingSpecifiers, func(e *ast.Node) *ast.Node { spec := e.AsImportSpecifier() var propertyName *ast.Node if spec.PropertyName != nil { propertyName = spec.PropertyName } syntheticSpec := ct.NodeFactory.NewImportSpecifier( true, // isTypeOnly propertyName, spec.Name(), ) return syntheticSpec }) } for _, spec := range newSpecifiers { insertionIndex := lsutil.GetImportSpecifierInsertionIndex(specsToCompareAgainst, spec, specifierComparer) ct.InsertImportSpecifierAtIndex(file, spec, importClause.NamedBindings, insertionIndex) } } else if len(existingSpecifiers) > 0 { for _, spec := range newSpecifiers { ct.InsertNodeInListAfter(file, existingSpecifiers[len(existingSpecifiers)-1], spec.AsNode(), nil) } } else { if len(newSpecifiers) > 0 { namedImports := ct.NodeFactory.NewNamedImports(ct.NodeFactory.NewNodeList(newSpecifiers)) if importClause.NamedBindings != nil { ct.ReplaceNode(file, importClause.NamedBindings, namedImports, nil) } else { if importClause.Name() == nil { panic("Import clause must have either named imports or a default import") } ct.InsertNodeAfter(file, importClause.Name(), namedImports) } } } } if promoteFromTypeOnly { // Delete the 'type' keyword from the import clause typeKeyword := getTypeKeywordOfTypeOnlyImport(importClause, file) ct.Delete(file, typeKeyword) // Add 'type' modifier to existing specifiers (not newly added ones) // We preserve the type-onlyness of existing specifiers regardless of whether // it would make a difference in emit (user preference). if len(existingSpecifiers) > 0 { for _, specifier := range existingSpecifiers { if !specifier.AsImportSpecifier().IsTypeOnly { ct.InsertModifierBefore(file, ast.KindTypeKeyword, specifier) } } } } default: panic("Unsupported clause kind: " + importClauseOrBindingPattern.KindString() + " for addToExistingImport") } } func getTypeKeywordOfTypeOnlyImport(importClause *ast.ImportClause, sourceFile *ast.SourceFile) *ast.Node { debug.Assert(importClause.IsTypeOnly(), "import clause must be type-only") // The first child of a type-only import clause is the 'type' keyword // import type { foo } from './bar' // ^^^^ typeKeyword := astnav.FindChildOfKind(importClause.AsNode(), ast.KindTypeKeyword, sourceFile) debug.Assert(typeKeyword != nil, "type-only import clause should have a type keyword") return typeKeyword } func addElementToBindingPattern( ct *change.Tracker, file *ast.SourceFile, bindingPattern *ast.BindingPattern, name string, propertyName string, ) { element := ct.NodeFactory.NewBindingElement(nil, nil, ct.NodeFactory.NewIdentifier(name), core.IfElse(propertyName == "", nil, ct.NodeFactory.NewIdentifier(propertyName))) if len(bindingPattern.Elements.Nodes) > 0 { ct.InsertNodeInListAfter(file, bindingPattern.Elements.Nodes[len(bindingPattern.Elements.Nodes)-1], element, bindingPattern.Elements) } else { ct.ReplaceNode(file, bindingPattern.AsNode(), ct.NodeFactory.NewBindingPattern(ast.KindObjectBindingPattern, ct.AsNodeFactory().NewNodeList([]*ast.Node{element})), nil) } } func getNewImports( ct *change.Tracker, moduleSpecifier string, quotePreference lsutil.QuotePreference, defaultImport *newImportBinding, namedImports []*newImportBinding, namespaceLikeImport *newImportBinding, // { lsproto.importKind: lsproto.ImportKind.CommonJS | lsproto.ImportKind.Namespace; } compilerOptions *core.CompilerOptions, preferences lsutil.UserPreferences, ) []*ast.AnyImportSyntax { tokenFlags := core.IfElse(quotePreference == lsutil.QuotePreferenceSingle, ast.TokenFlagsSingleQuote, ast.TokenFlagsNone) moduleSpecifierStringLiteral := ct.NodeFactory.NewStringLiteral(moduleSpecifier, tokenFlags) var statements []*ast.AnyImportSyntax if defaultImport != nil || len(namedImports) > 0 { // `verbatimModuleSyntax` should prefer top-level `import type` - // even though it's not an error, it would add unnecessary runtime emit. topLevelTypeOnly := (defaultImport == nil || needsTypeOnly(defaultImport.addAsTypeOnly)) && core.Every(namedImports, func(i *newImportBinding) bool { return needsTypeOnly(i.addAsTypeOnly) }) || (compilerOptions.VerbatimModuleSyntax.IsTrue() || preferences.PreferTypeOnlyAutoImports.IsTrue()) && (defaultImport == nil || defaultImport.addAsTypeOnly != lsproto.AddAsTypeOnlyNotAllowed) && !core.Some(namedImports, func(i *newImportBinding) bool { return i.addAsTypeOnly == lsproto.AddAsTypeOnlyNotAllowed }) var defaultImportNode *ast.Node if defaultImport != nil { defaultImportNode = ct.NodeFactory.NewIdentifier(defaultImport.name) } statements = append(statements, makeImport(ct, defaultImportNode, core.Map(namedImports, func(namedImport *newImportBinding) *ast.Node { var namedImportPropertyName *ast.Node if namedImport.propertyName != "" { namedImportPropertyName = ct.NodeFactory.NewIdentifier(namedImport.propertyName) } return ct.NodeFactory.NewImportSpecifier( !topLevelTypeOnly && shouldUseTypeOnly(namedImport.addAsTypeOnly, preferences), namedImportPropertyName, ct.NodeFactory.NewIdentifier(namedImport.name), ) }), moduleSpecifierStringLiteral, topLevelTypeOnly)) } if namespaceLikeImport != nil { var declaration *ast.Statement if namespaceLikeImport.kind == lsproto.ImportKindCommonJS { declaration = ct.NodeFactory.NewImportEqualsDeclaration( /*modifiers*/ nil, shouldUseTypeOnly(namespaceLikeImport.addAsTypeOnly, preferences), ct.NodeFactory.NewIdentifier(namespaceLikeImport.name), ct.NodeFactory.NewExternalModuleReference(moduleSpecifierStringLiteral), ) } else { declaration = ct.NodeFactory.NewImportDeclaration( /*modifiers*/ nil, ct.NodeFactory.NewImportClause( /*phaseModifier*/ core.IfElse(shouldUseTypeOnly(namespaceLikeImport.addAsTypeOnly, preferences), ast.KindTypeKeyword, ast.KindUnknown), /*name*/ nil, ct.NodeFactory.NewNamespaceImport(ct.NodeFactory.NewIdentifier(namespaceLikeImport.name)), ), moduleSpecifierStringLiteral, /*attributes*/ nil, ) } statements = append(statements, declaration) } if len(statements) == 0 { panic("No statements to insert for new imports") } return statements } func getNewRequires( changeTracker *change.Tracker, moduleSpecifier string, quotePreference lsutil.QuotePreference, defaultImport *newImportBinding, namedImports []*newImportBinding, namespaceLikeImport *newImportBinding, compilerOptions *core.CompilerOptions, ) []*ast.Statement { quotedModuleSpecifier := changeTracker.NodeFactory.NewStringLiteral( moduleSpecifier, core.IfElse(quotePreference == lsutil.QuotePreferenceSingle, ast.TokenFlagsSingleQuote, ast.TokenFlagsNone), ) var statements []*ast.Statement // const { default: foo, bar, etc } = require('./mod'); if defaultImport != nil || len(namedImports) > 0 { bindingElements := []*ast.Node{} for _, namedImport := range namedImports { var propertyName *ast.Node if namedImport.propertyName != "" { propertyName = changeTracker.NodeFactory.NewIdentifier(namedImport.propertyName) } bindingElements = append(bindingElements, changeTracker.NodeFactory.NewBindingElement( /*dotDotDotToken*/ nil, propertyName, changeTracker.NodeFactory.NewIdentifier(namedImport.name), /*initializer*/ nil, )) } if defaultImport != nil { bindingElements = append([]*ast.Node{ changeTracker.NodeFactory.NewBindingElement( /*dotDotDotToken*/ nil, changeTracker.NodeFactory.NewIdentifier("default"), changeTracker.NodeFactory.NewIdentifier(defaultImport.name), /*initializer*/ nil, ), }, bindingElements...) } declaration := createConstEqualsRequireDeclaration( changeTracker, changeTracker.NodeFactory.NewBindingPattern( ast.KindObjectBindingPattern, changeTracker.NodeFactory.NewNodeList(bindingElements), ), quotedModuleSpecifier, ) statements = append(statements, declaration) } // const foo = require('./mod'); if namespaceLikeImport != nil { declaration := createConstEqualsRequireDeclaration( changeTracker, changeTracker.NodeFactory.NewIdentifier(namespaceLikeImport.name), quotedModuleSpecifier, ) statements = append(statements, declaration) } debug.Assert(statements != nil) return statements } func createConstEqualsRequireDeclaration(changeTracker *change.Tracker, name *ast.Node, quotedModuleSpecifier *ast.Node) *ast.Statement { return changeTracker.NodeFactory.NewVariableStatement( /*modifiers*/ nil, changeTracker.NodeFactory.NewVariableDeclarationList( changeTracker.NodeFactory.NewNodeList([]*ast.Node{ changeTracker.NodeFactory.NewVariableDeclaration( name, /*exclamationToken*/ nil, /*type*/ nil, changeTracker.NodeFactory.NewCallExpression( changeTracker.NodeFactory.NewIdentifier("require"), /*questionDotToken*/ nil, /*typeArguments*/ nil, changeTracker.NodeFactory.NewNodeList([]*ast.Node{quotedModuleSpecifier}), ast.NodeFlagsNone, ), ), }), ast.NodeFlagsConst, ), ) } func insertImports(ct *change.Tracker, sourceFile *ast.SourceFile, imports []*ast.AnyImportOrRequireStatement, blankLineBetween bool, preferences lsutil.UserPreferences) { var existingImportStatements []*ast.Statement if imports[0].Kind == ast.KindVariableStatement { existingImportStatements = core.Filter(sourceFile.Statements.Nodes, ast.IsRequireVariableStatement) } else { existingImportStatements = core.Filter(sourceFile.Statements.Nodes, ast.IsAnyImportSyntax) } comparer, isSorted := lsutil.GetOrganizeImportsStringComparerWithDetection(existingImportStatements, preferences) sortedNewImports := slices.Clone(imports) slices.SortFunc(sortedNewImports, func(a, b *ast.Statement) int { return lsutil.CompareImportsOrRequireStatements(a, b, comparer) }) if len(existingImportStatements) > 0 && isSorted { // Existing imports are sorted, insert each new import at the correct position for _, newImport := range sortedNewImports { insertionIndex := lsutil.GetImportDeclarationInsertIndex(existingImportStatements, newImport, func(a, b *ast.Statement) stringutil.Comparison { return lsutil.CompareImportsOrRequireStatements(a, b, comparer) }) if insertionIndex == 0 { // If the first import is top-of-file, insert after the leading comment which is likely the header. leadingTriviaOption := change.LeadingTriviaOptionNone if existingImportStatements[0] == sourceFile.Statements.Nodes[0] { leadingTriviaOption = change.LeadingTriviaOptionExclude } ct.InsertNodeBefore(sourceFile, existingImportStatements[0].AsNode(), newImport.AsNode(), false /*blankLineBetween*/, leadingTriviaOption) } else { prevImport := existingImportStatements[insertionIndex-1] ct.InsertNodeAfter(sourceFile, prevImport.AsNode(), newImport.AsNode()) } } } else if len(existingImportStatements) > 0 { ct.InsertNodesAfter(sourceFile, existingImportStatements[len(existingImportStatements)-1], sortedNewImports) } else { ct.InsertAtTopOfFile(sourceFile, sortedNewImports, blankLineBetween) } } func makeImport(ct *change.Tracker, defaultImport *ast.IdentifierNode, namedImports []*ast.Node, moduleSpecifier *ast.Expression, isTypeOnly bool) *ast.Statement { var newNamedImports *ast.Node if len(namedImports) > 0 { newNamedImports = ct.NodeFactory.NewNamedImports(ct.NodeFactory.NewNodeList(namedImports)) } var importClause *ast.Node if defaultImport != nil || newNamedImports != nil { importClause = ct.NodeFactory.NewImportClause(core.IfElse(isTypeOnly, ast.KindTypeKeyword, ast.KindUnknown), defaultImport, newNamedImports) } return ct.NodeFactory.NewImportDeclaration( /*modifiers*/ nil, importClause, moduleSpecifier, nil /*attributes*/) } func (v *View) GetFixes(ctx context.Context, export *Export, forJSX bool, isValidTypeOnlyUseSite bool, usagePosition *lsproto.Position) []*Fix { var fixes []*Fix if namespaceFix := v.tryUseExistingNamespaceImport(ctx, export, usagePosition); namespaceFix != nil { fixes = append(fixes, namespaceFix) } if fix := v.tryAddToExistingImport(ctx, export, isValidTypeOnlyUseSite); fix != nil { return append(fixes, fix) } // !!! getNewImportFromExistingSpecifier - even worth it? moduleSpecifier, moduleSpecifierKind := v.GetModuleSpecifier(export, v.preferences) if moduleSpecifier == "" { if len(fixes) > 0 { return fixes } return nil } // Check if we need a JSDoc import type fix (for JS files with type-only imports) isJs := tspath.HasJSFileExtension(v.importingFile.FileName()) importedSymbolHasValueMeaning := export.Flags&ast.SymbolFlagsValue != 0 || export.IsUnresolvedAlias() if !importedSymbolHasValueMeaning && isJs && usagePosition != nil { // For pure types in JS files, use JSDoc import type syntax return []*Fix{ { AutoImportFix: &lsproto.AutoImportFix{ Kind: lsproto.AutoImportFixKindJsdocTypeImport, ModuleSpecifier: moduleSpecifier, Name: export.Name(), UsagePosition: usagePosition, }, ModuleSpecifierKind: moduleSpecifierKind, IsReExport: export.Target.ModuleID != export.ModuleID, ModuleFileName: export.ModuleFileName, }, } } importKind := getImportKind(v.importingFile, export, v.program) addAsTypeOnly := getAddAsTypeOnly(isValidTypeOnlyUseSite, export, v.program.Options()) name := export.Name() startsWithUpper := unicode.IsUpper(rune(name[0])) if forJSX && !startsWithUpper { if export.IsRenameable() { name = fmt.Sprintf("%c%s", unicode.ToUpper(rune(name[0])), name[1:]) } else { return nil } } return append(fixes, &Fix{ AutoImportFix: &lsproto.AutoImportFix{ Kind: lsproto.AutoImportFixKindAddNew, ImportKind: importKind, ModuleSpecifier: moduleSpecifier, Name: name, UseRequire: v.shouldUseRequire(), AddAsTypeOnly: addAsTypeOnly, }, ModuleSpecifierKind: moduleSpecifierKind, IsReExport: export.Target.ModuleID != export.ModuleID, ModuleFileName: export.ModuleFileName, }) } // getAddAsTypeOnly determines if an import should be type-only based on usage context func getAddAsTypeOnly(isValidTypeOnlyUseSite bool, export *Export, compilerOptions *core.CompilerOptions) lsproto.AddAsTypeOnly { if !isValidTypeOnlyUseSite { // Can't use a type-only import if the usage is an emitting position return lsproto.AddAsTypeOnlyNotAllowed } if compilerOptions.VerbatimModuleSyntax.IsTrue() && (export.IsTypeOnly || export.Flags&ast.SymbolFlagsValue == 0) || export.IsTypeOnly && export.Flags&ast.SymbolFlagsValue != 0 { // A type-only import is required for this symbol if under verbatimModuleSyntax and it's purely a type return lsproto.AddAsTypeOnlyRequired } return lsproto.AddAsTypeOnlyAllowed } func (v *View) tryUseExistingNamespaceImport(ctx context.Context, export *Export, usagePosition *lsproto.Position) *Fix { if usagePosition == nil { return nil } if getImportKind(v.importingFile, export, v.program) != lsproto.ImportKindNamed { return nil } existingImports := v.getExistingImports(ctx) matchingDeclarations := existingImports.Get(export.ModuleID) for _, existingImport := range matchingDeclarations { namespacePrefix := getNamespaceLikeImportText(existingImport.node) if namespacePrefix == "" || existingImport.moduleSpecifier == "" { continue } return &Fix{ AutoImportFix: &lsproto.AutoImportFix{ Kind: lsproto.AutoImportFixKindUseNamespace, Name: export.Name(), ModuleSpecifier: existingImport.moduleSpecifier, ImportKind: lsproto.ImportKindNamespace, AddAsTypeOnly: lsproto.AddAsTypeOnlyAllowed, ImportIndex: int32(existingImport.index), UsagePosition: usagePosition, NamespacePrefix: namespacePrefix, }, } } return nil } func getNamespaceLikeImportText(declaration *ast.Node) string { switch declaration.Kind { case ast.KindVariableDeclaration: name := declaration.Name() if name != nil && name.Kind == ast.KindIdentifier { return name.Text() } return "" case ast.KindImportEqualsDeclaration: return declaration.Name().Text() case ast.KindJSDocImportTag, ast.KindImportDeclaration: importClause := declaration.ImportClause() if importClause != nil && importClause.AsImportClause().NamedBindings != nil && importClause.AsImportClause().NamedBindings.Kind == ast.KindNamespaceImport { return importClause.AsImportClause().NamedBindings.Name().Text() } return "" default: return "" } } func (v *View) tryAddToExistingImport( ctx context.Context, export *Export, isValidTypeOnlyUseSite bool, ) *Fix { existingImports := v.getExistingImports(ctx) matchingDeclarations := existingImports.Get(export.ModuleID) if len(matchingDeclarations) == 0 { return nil } // Can't use an es6 import for a type in JS. if ast.IsSourceFileJS(v.importingFile) && export.Flags&ast.SymbolFlagsValue == 0 && !core.Every(matchingDeclarations, func(i existingImport) bool { return ast.IsJSDocImportTag(i.node) }) { return nil } importKind := getImportKind(v.importingFile, export, v.program) if importKind == lsproto.ImportKindCommonJS || importKind == lsproto.ImportKindNamespace { return nil } addAsTypeOnly := getAddAsTypeOnly(isValidTypeOnlyUseSite, export, v.program.Options()) var best *Fix for _, existingImport := range matchingDeclarations { if existingImport.node.Kind == ast.KindImportEqualsDeclaration { continue } if existingImport.node.Kind == ast.KindVariableDeclaration { if (importKind == lsproto.ImportKindNamed || importKind == lsproto.ImportKindDefault) && existingImport.node.Name().Kind == ast.KindObjectBindingPattern { fix := &Fix{ AutoImportFix: &lsproto.AutoImportFix{ Kind: lsproto.AutoImportFixKindAddToExisting, Name: export.Name(), ImportKind: importKind, ImportIndex: int32(existingImport.index), ModuleSpecifier: existingImport.moduleSpecifier, AddAsTypeOnly: addAsTypeOnly, }, } // Variable declarations are never type-only. // Give preference to putting types in existing type-only imports and avoiding conversions // of import statements to/from type-only. if addAsTypeOnly == lsproto.AddAsTypeOnlyNotAllowed { return fix } if best == nil { best = fix } } continue } importClauseNode := existingImport.node.ImportClause() if importClauseNode == nil || !ast.IsStringLiteralLike(existingImport.node.ModuleSpecifier()) { // Side-effect import (no import clause) - can't add to it continue } importClause := importClauseNode.AsImportClause() namedBindings := importClause.NamedBindings // A type-only import may not have both a default and named imports, so the only way a name can // be added to an existing type-only import is adding a named import to existing named bindings. if importClause.IsTypeOnly() && !(importKind == lsproto.ImportKindNamed && namedBindings != nil) { continue } if importKind == lsproto.ImportKindDefault && (importClause.Name() != nil || // Cannot add a default import as type-only if the import already has named bindings addAsTypeOnly == lsproto.AddAsTypeOnlyRequired && namedBindings != nil) { continue } // Cannot add a named import to a declaration that has a namespace import if importKind == lsproto.ImportKindNamed && namedBindings != nil && namedBindings.Kind == ast.KindNamespaceImport { continue } fix := &Fix{ AutoImportFix: &lsproto.AutoImportFix{ Kind: lsproto.AutoImportFixKindAddToExisting, Name: export.Name(), ImportKind: importKind, ImportIndex: int32(existingImport.index), ModuleSpecifier: existingImport.moduleSpecifier, AddAsTypeOnly: addAsTypeOnly, }, } isTypeOnly := importClause.IsTypeOnly() // Give preference to putting types in existing type-only imports and avoiding conversions // of import statements to/from type-only. if (addAsTypeOnly != lsproto.AddAsTypeOnlyNotAllowed && isTypeOnly) || (addAsTypeOnly == lsproto.AddAsTypeOnlyNotAllowed && !isTypeOnly) { return fix } if best == nil { best = fix } } return best } func getImportKind(importingFile *ast.SourceFile, export *Export, program *compiler.Program) lsproto.ImportKind { if program.Options().VerbatimModuleSyntax.IsTrue() && program.GetEmitModuleFormatOfFile(importingFile) == core.ModuleKindCommonJS { return lsproto.ImportKindCommonJS } switch export.Syntax { case ExportSyntaxDefaultModifier, ExportSyntaxDefaultDeclaration: return lsproto.ImportKindDefault case ExportSyntaxNamed: if export.ExportName == ast.InternalSymbolNameDefault { return lsproto.ImportKindDefault } fallthrough case ExportSyntaxModifier, ExportSyntaxStar, ExportSyntaxCommonJSExportsProperty: return lsproto.ImportKindNamed case ExportSyntaxEquals, ExportSyntaxCommonJSModuleExports, ExportSyntaxUMD: // export.Syntax will be ExportSyntaxEquals for named exports/properties of an export='s target. if export.ExportName != ast.InternalSymbolNameExportEquals { return lsproto.ImportKindNamed } // !!! cache this? for _, statement := range importingFile.Statements.Nodes { // `import foo` parses as an ImportEqualsDeclaration even though it could be an ImportDeclaration if ast.IsImportEqualsDeclaration(statement) && !ast.NodeIsMissing(statement.AsImportEqualsDeclaration().ModuleReference) { return lsproto.ImportKindCommonJS } } // !!! this logic feels weird; we're basically trying to predict if shouldUseRequire is going to // be true. The meaning of "default import" is different depending on whether we write it as // a require or an es6 import. The latter, compiled to CJS, has interop built in that will // avoid accessing .default, but if we write a require directly and call it a default import, // we emit an unconditional .default access. if importingFile.ExternalModuleIndicator != nil || !ast.IsSourceFileJS(importingFile) { return lsproto.ImportKindDefault } return lsproto.ImportKindCommonJS default: panic("unhandled export syntax kind: " + export.Syntax.String()) } } type existingImport struct { node *ast.Node moduleSpecifier string index int } func (v *View) getExistingImports(ctx context.Context) *collections.MultiMap[ModuleID, existingImport] { if v.existingImports != nil { return v.existingImports } result := collections.NewMultiMapWithSizeHint[ModuleID, existingImport](len(v.importingFile.Imports())) ch, done := v.program.GetTypeChecker(ctx) defer done() for i, moduleSpecifier := range v.importingFile.Imports() { node := ast.TryGetImportFromModuleSpecifier(moduleSpecifier) if node == nil { panic("error: did not expect node kind " + moduleSpecifier.Kind.String()) } else if ast.IsVariableDeclarationInitializedToRequire(node.Parent) { if moduleSymbol := ch.ResolveExternalModuleName(moduleSpecifier); moduleSymbol != nil { if moduleID, _, ok := tryGetModuleIDAndFileNameOfModuleSymbol(moduleSymbol); ok { result.Add(moduleID, existingImport{node: node.Parent, moduleSpecifier: moduleSpecifier.Text(), index: i}) } } } else if node.Kind == ast.KindImportDeclaration || node.Kind == ast.KindImportEqualsDeclaration || node.Kind == ast.KindJSDocImportTag { if moduleSymbol := ch.GetSymbolAtLocation(moduleSpecifier); moduleSymbol != nil { if moduleID, _, ok := tryGetModuleIDAndFileNameOfModuleSymbol(moduleSymbol); ok { result.Add(moduleID, existingImport{node: node, moduleSpecifier: moduleSpecifier.Text(), index: i}) } } } } v.existingImports = result return result } func (v *View) shouldUseRequire() bool { if v.shouldUseRequireForFixes != nil { return *v.shouldUseRequireForFixes } shouldUseRequire := v.computeShouldUseRequire() v.shouldUseRequireForFixes = &shouldUseRequire return shouldUseRequire } // fileSyntaxKind represents the detected module syntax of a source file. type fileSyntaxKind int const ( fileSyntaxKindAmbiguous fileSyntaxKind = iota fileSyntaxKindESM fileSyntaxKindCJS ) // detectSyntax returns whether a source file has unambiguous ESM or CJS syntax. // When moduleDetection is "force", ExternalModuleIndicator may be set to the // source file node itself rather than a genuine syntax indicator, so we fall back // to inspecting the file's Imports() to find actual import/export declarations. func detectSyntax(file *ast.SourceFile, options *core.CompilerOptions) fileSyntaxKind { hasESM, hasCJS := detectSyntaxIndicators(file, options) switch { case hasCJS && !hasESM: return fileSyntaxKindCJS case hasESM && !hasCJS: return fileSyntaxKindESM default: return fileSyntaxKindAmbiguous } } // detectSyntaxIndicators checks whether a source file contains genuine ESM // and/or CJS syntax. Under moduleDetection "force", the cached // ExternalModuleIndicator may be the source file itself rather than a real // statement, so we look at Imports() for actual import/export declarations. func detectSyntaxIndicators(file *ast.SourceFile, options *core.CompilerOptions) (hasESM bool, hasCJS bool) { hasCJS = file.CommonJSModuleIndicator != nil if options.GetEmitModuleDetectionKind() != core.ModuleDetectionKindForce { // ExternalModuleIndicator is reliable when moduleDetection is not "force" hasESM = file.ExternalModuleIndicator != nil return hasESM, hasCJS } // Under moduleDetection "force", ExternalModuleIndicator is set to // file.AsNode() when there is no genuine ESM syntax, so only trust it // when it points to a real statement node. if file.ExternalModuleIndicator != nil && file.ExternalModuleIndicator != file.AsNode() { return true, hasCJS } // Fall back to scanning Imports() for actual import/export declarations // (not require() calls or dynamic imports). for _, imp := range file.Imports() { if imp.Flags&ast.NodeFlagsSynthesized != 0 { continue } parent := imp.Parent if parent == nil { continue } switch parent.Kind { case ast.KindImportDeclaration, ast.KindJSImportDeclaration, ast.KindExportDeclaration: return true, hasCJS case ast.KindExternalModuleReference: // import x = require("...") — this is ESM-ish syntax return true, hasCJS } } return hasESM, hasCJS } func (v *View) computeShouldUseRequire() bool { // 1. TypeScript files don't use require variable declarations if !tspath.HasJSFileExtension(v.importingFile.FileName()) { return false } // 2. If the current source file is unambiguously CJS or ESM, go with that switch detectSyntax(v.importingFile, v.program.Options()) { case fileSyntaxKindCJS: return true case fileSyntaxKindESM: return false } // 3. Use the implied node format to determine CJS vs ESM // TODO: consider removing `impliedNodeFormatForEmit` switch v.program.GetImpliedNodeFormatForEmit(v.importingFile) { case core.ModuleKindCommonJS: return true case core.ModuleKindESNext: return false } // 4. If there's a tsconfig/jsconfig, use its module setting if v.program.Options().ConfigFilePath != "" { return v.program.Options().GetEmitModuleKind() < core.ModuleKindES2015 } // 5. Match the first other JS file in the program that's unambiguously CJS or ESM for _, otherFile := range v.program.GetSourceFiles() { switch { case otherFile == v.importingFile, !ast.IsSourceFileJS(otherFile), v.program.IsSourceFileFromExternalLibrary(otherFile): continue } switch detectSyntax(otherFile, v.program.Options()) { case fileSyntaxKindCJS: return true case fileSyntaxKindESM: return false } } // 6. Literally nothing to go on return true } func needsTypeOnly(addAsTypeOnly lsproto.AddAsTypeOnly) bool { return addAsTypeOnly == lsproto.AddAsTypeOnlyRequired } func shouldUseTypeOnly(addAsTypeOnly lsproto.AddAsTypeOnly, preferences lsutil.UserPreferences) bool { return needsTypeOnly(addAsTypeOnly) || addAsTypeOnly != lsproto.AddAsTypeOnlyNotAllowed && preferences.PreferTypeOnlyAutoImports.IsTrue() } // CompareFixesForSorting returns negative if `a` is better than `b`. // Sorting with this comparator will place the best fix first. // After rank sorting, fixes will be sorted by arbitrary but stable criteria // to ensure a deterministic order. func (v *View) CompareFixesForSorting(a, b *Fix) int { if res := v.CompareFixesForRanking(a, b); res != 0 { return res } return v.compareModuleSpecifiersForSorting(a, b) } // CompareFixesForRanking returns negative if `a` is better than `b`. // Sorting with this comparator will place the best fix first. // Fixes of equal desirability will be considered equal. func (v *View) CompareFixesForRanking(a, b *Fix) int { if res := compareFixKinds(a.Kind, b.Kind); res != 0 { return res } return v.compareModuleSpecifiersForRanking(a, b) } func compareFixKinds(a, b lsproto.AutoImportFixKind) int { return int(a) - int(b) } func (v *View) compareModuleSpecifiersForRanking(a, b *Fix) int { if comparison := compareModuleSpecifierRelativity(a, b, v.preferences); comparison != 0 { return comparison } if a.ModuleSpecifierKind == modulespecifiers.ResultKindAmbient && b.ModuleSpecifierKind == modulespecifiers.ResultKindAmbient { if comparison := v.compareNodeCoreModuleSpecifiers(a.ModuleSpecifier, b.ModuleSpecifier, v.importingFile, v.program); comparison != 0 { return comparison } } if a.ModuleSpecifierKind == modulespecifiers.ResultKindRelative && b.ModuleSpecifierKind == modulespecifiers.ResultKindRelative { if comparison := core.CompareBooleans( isFixPossiblyReExportingImportingFile(a, v.importingFile.FileName()), isFixPossiblyReExportingImportingFile(b, v.importingFile.FileName()), ); comparison != 0 { return comparison } } if comparison := tspath.CompareNumberOfDirectorySeparators(a.ModuleSpecifier, b.ModuleSpecifier); comparison != 0 { return comparison } return 0 } func (v *View) compareModuleSpecifiersForSorting(a, b *Fix) int { if res := v.compareModuleSpecifiersForRanking(a, b); res != 0 { return res } // Sort ./foo before ../foo for equal-length specifiers if strings.HasPrefix(a.ModuleSpecifier, "./") && !strings.HasPrefix(b.ModuleSpecifier, "./") { return -1 } if strings.HasPrefix(b.ModuleSpecifier, "./") && !strings.HasPrefix(a.ModuleSpecifier, "./") { return 1 } if comparison := strings.Compare(a.ModuleSpecifier, b.ModuleSpecifier); comparison != 0 { return comparison } if comparison := cmp.Compare(a.ImportKind, b.ImportKind); comparison != 0 { return comparison } // !!! further tie-breakers? In practice this is only called on fixes with the same name return 0 } func (v *View) compareNodeCoreModuleSpecifiers(a, b string, importingFile *ast.SourceFile, program *compiler.Program) int { if strings.HasPrefix(a, "node:") && !strings.HasPrefix(b, "node:") { if v.shouldUseUriStyleNodeCoreModules.IsTrue() { return -1 } else if v.shouldUseUriStyleNodeCoreModules.IsFalse() { return 1 } return 0 } if strings.HasPrefix(b, "node:") && !strings.HasPrefix(a, "node:") { if v.shouldUseUriStyleNodeCoreModules.IsTrue() { return 1 } else if v.shouldUseUriStyleNodeCoreModules.IsFalse() { return -1 } } return 0 } // This is a simple heuristic to try to avoid creating an import cycle with a barrel re-export. // E.g., do not `import { Foo } from ".."` when you could `import { Foo } from "../Foo"`. // This can produce false positives or negatives if re-exports cross into sibling directories // (e.g. `export * from "../whatever"`) or are not named "index". Technically this should do // a tspath.Path comparison, but it's not worth it to run a heuristic in such a hot path. func isFixPossiblyReExportingImportingFile(fix *Fix, importingFileName string) bool { if fix.IsReExport && isIndexFileName(fix.ModuleFileName) { reExportDir := tspath.GetDirectoryPath(fix.ModuleFileName) return strings.HasPrefix(importingFileName, reExportDir) } return false } func isIndexFileName(fileName string) bool { lastSlash := strings.LastIndexByte(fileName, '/') if lastSlash < 0 || len(fileName) <= lastSlash+1 { return false } fileName = fileName[lastSlash+1:] switch fileName { case "index.js", "index.jsx", "index.d.ts", "index.ts", "index.tsx": return true } return false } func promoteFromTypeOnly( changes *change.Tracker, aliasDeclaration *ast.Declaration, compilerOptions *core.CompilerOptions, sourceFile *ast.SourceFile, preferences lsutil.UserPreferences, ) *ast.Declaration { // See comment in `doAddExistingFix` on constant with the same name. convertExistingToTypeOnly := compilerOptions.VerbatimModuleSyntax switch aliasDeclaration.Kind { case ast.KindImportSpecifier: spec := aliasDeclaration.AsImportSpecifier() if spec.IsTypeOnly { if spec.Parent != nil && spec.Parent.Kind == ast.KindNamedImports { namedImportsNode := spec.Parent.AsNamedImports() elements := namedImportsNode.Elements.Nodes if len(elements) > 1 { // Create a synthetic specifier with isTypeOnly=false to compute sorted position var propertyName *ast.Node if spec.PropertyName != nil { propertyName = changes.NodeFactory.NewIdentifier(spec.PropertyName.Text()).AsIdentifier().AsNode() } newSpecifier := changes.NodeFactory.NewImportSpecifier( false, // isTypeOnly = false propertyName, changes.NodeFactory.NewIdentifier(spec.Name().Text()), ) specifierComparer, _ := lsutil.GetNamedImportSpecifierComparerWithDetection( spec.Parent.Parent.Parent, // ImportDeclaration sourceFile, preferences, ) insertionIndex := lsutil.GetImportSpecifierInsertionIndex(elements, newSpecifier, specifierComparer) currentIndex := slices.Index(elements, aliasDeclaration) if insertionIndex != currentIndex { changes.Delete(sourceFile, aliasDeclaration) changes.InsertImportSpecifierAtIndex(sourceFile, newSpecifier, spec.Parent, insertionIndex) return aliasDeclaration } } // If no re-sorting needed, just remove the 'type' keyword firstToken := lsutil.GetFirstToken(aliasDeclaration, sourceFile) typeKeywordPos := scanner.GetTokenPosOfNode(firstToken, sourceFile, false) var targetNode *ast.DeclarationName if spec.PropertyName != nil { targetNode = spec.PropertyName } else { targetNode = spec.Name() } targetPos := scanner.GetTokenPosOfNode(targetNode.AsNode(), sourceFile, false) changes.DeleteRange(sourceFile, core.NewTextRange(typeKeywordPos, targetPos)) } return aliasDeclaration } else { // The parent import clause is type-only if spec.Parent == nil || spec.Parent.Kind != ast.KindNamedImports { panic("ImportSpecifier parent must be NamedImports") } if spec.Parent.Parent == nil || spec.Parent.Parent.Kind != ast.KindImportClause { panic("NamedImports parent must be ImportClause") } promoteImportClause(changes, spec.Parent.Parent.AsImportClause(), compilerOptions, sourceFile, preferences, convertExistingToTypeOnly, aliasDeclaration) return spec.Parent.Parent } case ast.KindImportClause: promoteImportClause(changes, aliasDeclaration.AsImportClause(), compilerOptions, sourceFile, preferences, convertExistingToTypeOnly, aliasDeclaration) return aliasDeclaration case ast.KindNamespaceImport: // Promote the parent import clause if aliasDeclaration.Parent == nil || aliasDeclaration.Parent.Kind != ast.KindImportClause { panic("NamespaceImport parent must be ImportClause") } promoteImportClause(changes, aliasDeclaration.Parent.AsImportClause(), compilerOptions, sourceFile, preferences, convertExistingToTypeOnly, aliasDeclaration) return aliasDeclaration.Parent case ast.KindImportEqualsDeclaration: // Remove the 'type' keyword (which is the second token: 'import' 'type' name '=' ...) importEqDecl := aliasDeclaration.AsImportEqualsDeclaration() // The type keyword is after 'import' and before the name scan := scanner.GetScannerForSourceFile(sourceFile, importEqDecl.Pos()) // Skip 'import' keyword to get to 'type' scan.Scan() deleteTypeKeyword(changes, sourceFile, scan.TokenStart()) return aliasDeclaration default: panic(fmt.Sprintf("Unexpected alias declaration kind: %v", aliasDeclaration.Kind)) } } // promoteImportClause removes the type keyword from an import clause func promoteImportClause( changes *change.Tracker, importClause *ast.ImportClause, compilerOptions *core.CompilerOptions, sourceFile *ast.SourceFile, preferences lsutil.UserPreferences, convertExistingToTypeOnly core.Tristate, aliasDeclaration *ast.Declaration, ) { // Delete the 'type' keyword if importClause.PhaseModifier == ast.KindTypeKeyword { deleteTypeKeyword(changes, sourceFile, importClause.Pos()) } // Handle .ts extension conversion to .js if necessary if compilerOptions.AllowImportingTsExtensions.IsFalse() { moduleSpecifier := checker.TryGetModuleSpecifierFromDeclaration(importClause.Parent) if moduleSpecifier != nil { // Note: We can't check ResolvedUsingTsExtension without program, so we'll skip this optimization // The fix will still work, just might not change .ts to .js extensions in all cases } } // Handle verbatimModuleSyntax conversion // If convertExistingToTypeOnly is true, we need to add 'type' to other specifiers // in the same import declaration if convertExistingToTypeOnly.IsTrue() { namedImports := importClause.NamedBindings if namedImports != nil && namedImports.Kind == ast.KindNamedImports { namedImportsData := namedImports.AsNamedImports() if len(namedImportsData.Elements.Nodes) > 1 { // Check if the list is sorted and if we need to reorder _, isSorted := lsutil.GetNamedImportSpecifierComparerWithDetection( importClause.Parent, sourceFile, preferences, ) // If the alias declaration is an ImportSpecifier and the list is sorted, // move it to index 0 (since it will be the only non-type-only import) if isSorted.IsFalse() == false && // isSorted !== false aliasDeclaration != nil && aliasDeclaration.Kind == ast.KindImportSpecifier { // Find the index of the alias declaration aliasIndex := -1 for i, element := range namedImportsData.Elements.Nodes { if element == aliasDeclaration { aliasIndex = i break } } // If not already at index 0, move it there if aliasIndex > 0 { // Delete the specifier from its current position changes.Delete(sourceFile, aliasDeclaration) // Insert it at index 0 changes.InsertImportSpecifierAtIndex(sourceFile, aliasDeclaration, namedImports, 0) } } // Add 'type' keyword to all other import specifiers that aren't already type-only for _, element := range namedImportsData.Elements.Nodes { spec := element.AsImportSpecifier() // Skip the specifier being promoted (if aliasDeclaration is an ImportSpecifier) if aliasDeclaration != nil && aliasDeclaration.Kind == ast.KindImportSpecifier { if element == aliasDeclaration { continue } } // Skip if already type-only if !spec.IsTypeOnly { changes.InsertModifierBefore(sourceFile, ast.KindTypeKeyword, element) } } } } } } // deleteTypeKeyword deletes the 'type' keyword token starting at the given position, // including any trailing whitespace. func deleteTypeKeyword(changes *change.Tracker, sourceFile *ast.SourceFile, startPos int) { scan := scanner.GetScannerForSourceFile(sourceFile, startPos) if scan.Token() != ast.KindTypeKeyword { return } typeStart := scan.TokenStart() typeEnd := scan.TokenEnd() // Skip trailing whitespace text := sourceFile.Text() for typeEnd < len(text) && (text[typeEnd] == ' ' || text[typeEnd] == '\t') { typeEnd++ } changes.DeleteRange(sourceFile, core.NewTextRange(typeStart, typeEnd)) } func getModuleSpecifierText(promotedDeclaration *ast.Node) string { if promotedDeclaration.Kind == ast.KindImportEqualsDeclaration { importEqualsDeclaration := promotedDeclaration.AsImportEqualsDeclaration() if ast.IsExternalModuleReference(importEqualsDeclaration.ModuleReference) { expr := importEqualsDeclaration.ModuleReference.Expression() if expr != nil { if ast.IsStringLiteralLike(expr) { return expr.Text() } return scanner.GetTextOfNode(expr) } } return scanner.GetTextOfNode(importEqualsDeclaration.ModuleReference) } moduleSpecifier := promotedDeclaration.Parent.ModuleSpecifier() if ast.IsStringLiteralLike(moduleSpecifier) { return moduleSpecifier.Text() } return scanner.GetTextOfNode(moduleSpecifier) } // returns `-1` if `a` is better than `b` func compareModuleSpecifierRelativity(a *Fix, b *Fix, preferences modulespecifiers.UserPreferences) int { switch preferences.ImportModuleSpecifierPreference { case modulespecifiers.ImportModuleSpecifierPreferenceNonRelative, modulespecifiers.ImportModuleSpecifierPreferenceProjectRelative: return core.CompareBooleans(a.ModuleSpecifierKind == modulespecifiers.ResultKindRelative, b.ModuleSpecifierKind == modulespecifiers.ResultKindRelative) } return 0 }