internal/ls/autoimportfixes.go (272 lines of code) (raw):
package ls
import (
"slices"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/astnav"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/debug"
"github.com/microsoft/typescript-go/internal/ls/change"
"github.com/microsoft/typescript-go/internal/ls/lsutil"
"github.com/microsoft/typescript-go/internal/ls/organizeimports"
"github.com/microsoft/typescript-go/internal/stringutil"
)
type Import struct {
name string
kind ImportKind // ImportKindCommonJS | ImportKindNamespace
addAsTypeOnly AddAsTypeOnly
propertyName string // Use when needing to generate an `ImportSpecifier with a `propertyName`; the name preceding "as" keyword (propertyName = "" when "as" is absent)
}
func addNamespaceQualifier(ct *change.Tracker, sourceFile *ast.SourceFile, qualification *Qualification) {
ct.InsertText(sourceFile, qualification.usagePosition, qualification.namespacePrefix+".")
}
func (ls *LanguageService) doAddExistingFix(
ct *change.Tracker,
sourceFile *ast.SourceFile,
clause *ast.Node, // ImportClause | ObjectBindingPattern,
defaultImport *Import,
namedImports []*Import,
// removeExistingImportSpecifiers *core.Set[ImportSpecifier | BindingElement] // !!! remove imports not implemented
) {
switch clause.Kind {
case ast.KindObjectBindingPattern:
if clause.Kind == ast.KindObjectBindingPattern {
// bindingPattern := clause.AsBindingPattern()
// !!! adding *and* removing imports not implemented
// if (removeExistingImportSpecifiers && core.Some(bindingPattern.Elements, func(e *ast.Node) bool {
// return removeExistingImportSpecifiers.Has(e)
// })) {
// If we're both adding and removing elements, just replace and reprint the whole
// node. The change tracker doesn't understand all the operations and can insert or
// leave behind stray commas.
// ct.replaceNode(
// sourceFile,
// bindingPattern,
// ct.NodeFactory.NewObjectBindingPattern([
// ...bindingPattern.Elements.Filter(func(e *ast.Node) bool {
// return !removeExistingImportSpecifiers.Has(e)
// }),
// ...defaultImport ? [ct.NodeFactory.createBindingElement(/*dotDotDotToken*/ nil, /*propertyName*/ "default", defaultImport.name)] : emptyArray,
// ...namedImports.map(i => ct.NodeFactory.createBindingElement(/*dotDotDotToken*/ nil, i.propertyName, i.name)),
// ]),
// )
// return
// }
if defaultImport != nil {
addElementToBindingPattern(ct, sourceFile, clause, defaultImport.name, ptrTo("default"))
}
for _, specifier := range namedImports {
addElementToBindingPattern(ct, sourceFile, clause, specifier.name, &specifier.propertyName)
}
return
}
case ast.KindImportClause:
importClause := clause.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 *Import) bool {
if i == nil {
return false
}
return i.addAsTypeOnly == AddAsTypeOnlyNotAllowed
})
existingSpecifiers := []*ast.Node{} // []*ast.ImportSpecifier
if importClause.NamedBindings != nil && importClause.NamedBindings.Kind == ast.KindNamedImports {
existingSpecifiers = importClause.NamedBindings.Elements()
}
if defaultImport != nil {
debug.Assert(clause.Name() == nil, "Cannot add a default import to an import clause that already has one")
ct.InsertNodeAt(sourceFile, core.TextPos(astnav.GetStartOfNode(clause, sourceFile, false)), ct.NodeFactory.NewIdentifier(defaultImport.name), change.NodeOptions{Suffix: ", "})
}
if len(namedImports) > 0 {
specifierComparer, isSorted := organizeimports.GetNamedImportSpecifierComparerWithDetection(importClause.Parent, sourceFile, ls.UserPreferences())
newSpecifiers := core.Map(namedImports, func(namedImport *Import) *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, ls.UserPreferences()),
identifier,
ct.NodeFactory.NewIdentifier(namedImport.name),
)
})
slices.SortFunc(newSpecifiers, specifierComparer)
// !!! remove imports not implemented
// if (removeExistingImportSpecifiers) {
// // If we're both adding and removing specifiers, just replace and reprint the whole
// // node. The change tracker doesn't understand all the operations and can insert or
// // leave behind stray commas.
// ct.replaceNode(
// sourceFile,
// importClause.NamedBindings,
// ct.NodeFactory.updateNamedImports(
// importClause.NamedBindings.AsNamedImports(),
// append(core.Filter(existingSpecifiers, func (s *ast.ImportSpecifier) bool {return !removeExistingImportSpecifiers.Has(s)}), newSpecifiers...), // !!! sort with 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 := organizeimports.GetImportSpecifierInsertionIndex(specsToCompareAgainst, spec, specifierComparer)
ct.InsertImportSpecifierAtIndex(sourceFile, spec, importClause.NamedBindings, insertionIndex)
}
} else if len(existingSpecifiers) > 0 && isSorted.IsTrue() {
// Existing specifiers are sorted, so insert each new specifier at the correct position
for _, spec := range newSpecifiers {
insertionIndex := organizeimports.GetImportSpecifierInsertionIndex(existingSpecifiers, spec, specifierComparer)
if insertionIndex >= len(existingSpecifiers) {
// Insert at the end
ct.InsertNodeInListAfter(sourceFile, existingSpecifiers[len(existingSpecifiers)-1], spec.AsNode(), existingSpecifiers)
} else {
// Insert before the element at insertionIndex
ct.InsertNodeInListAfter(sourceFile, existingSpecifiers[insertionIndex], spec.AsNode(), existingSpecifiers)
}
}
} else if len(existingSpecifiers) > 0 {
// Existing specifiers may not be sorted, append to the end
for _, spec := range newSpecifiers {
ct.InsertNodeInListAfter(sourceFile, existingSpecifiers[len(existingSpecifiers)-1], spec.AsNode(), existingSpecifiers)
}
} else {
if len(newSpecifiers) > 0 {
namedImports := ct.NodeFactory.NewNamedImports(ct.NodeFactory.NewNodeList(newSpecifiers))
if importClause.NamedBindings != nil {
ct.ReplaceNode(sourceFile, importClause.NamedBindings, namedImports, nil)
} else {
if clause.Name() == nil {
panic("Import clause must have either named imports or a default import")
}
ct.InsertNodeAfter(sourceFile, clause.Name(), namedImports)
}
}
}
}
if promoteFromTypeOnly {
// Delete the 'type' keyword from the import clause
typeKeyword := getTypeKeywordOfTypeOnlyImport(importClause, sourceFile)
ct.Delete(sourceFile, 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(sourceFile, ast.KindTypeKeyword, specifier)
}
}
}
}
default:
panic("Unsupported clause kind: " + clause.Kind.String() + "for doAddExistingFix")
}
}
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, sourceFile *ast.SourceFile, bindingPattern *ast.Node, name string, propertyName *string) {
element := newBindingElementFromNameAndPropertyName(ct, name, propertyName)
if len(bindingPattern.Elements()) > 0 {
ct.InsertNodeInListAfter(sourceFile, bindingPattern.Elements()[len(bindingPattern.Elements())-1], element, nil)
} else {
ct.ReplaceNode(sourceFile, bindingPattern, ct.NodeFactory.NewBindingPattern(
ast.KindObjectBindingPattern,
ct.NodeFactory.NewNodeList([]*ast.Node{element}),
), nil)
}
}
func newBindingElementFromNameAndPropertyName(ct *change.Tracker, name string, propertyName *string) *ast.Node {
var newPropertyNameIdentifier *ast.Node
if propertyName != nil {
newPropertyNameIdentifier = ct.NodeFactory.NewIdentifier(*propertyName)
}
return ct.NodeFactory.NewBindingElement(
nil, /*dotDotDotToken*/
newPropertyNameIdentifier,
ct.NodeFactory.NewIdentifier(name),
nil, /* initializer */
)
}
func (ls *LanguageService) insertImports(ct *change.Tracker, sourceFile *ast.SourceFile, imports []*ast.Statement, blankLineBetween bool) {
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 := organizeimports.GetOrganizeImportsStringComparerWithDetection(existingImportStatements, ls.UserPreferences())
sortedNewImports := slices.Clone(imports)
slices.SortFunc(sortedNewImports, func(a, b *ast.Statement) int {
return organizeimports.CompareImportsOrRequireStatements(a, b, comparer)
})
// !!! FutureSourceFile
// if !isFullSourceFile(sourceFile) {
// for _, newImport := range sortedNewImports {
// // Insert one at a time to send correct original source file for accurate text reuse
// // when some imports are cloned from existing ones in other files.
// ct.insertStatementsInNewFile(sourceFile.fileName, []*ast.Node{newImport}, ast.GetSourceFileOfNode(getOriginalNode(newImport)))
// }
// return;
// }
if len(existingImportStatements) > 0 && isSorted {
// Existing imports are sorted, insert each new import at the correct position
for _, newImport := range sortedNewImports {
insertionIndex := organizeimports.GetImportDeclarationInsertIndex(existingImportStatements, newImport, func(a, b *ast.Statement) stringutil.Comparison {
return organizeimports.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
ct.InsertNodeAt(sourceFile, core.TextPos(astnav.GetStartOfNode(existingImportStatements[0], sourceFile, false)), newImport.AsNode(), change.NodeOptions{})
} 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 (ls *LanguageService) getNewImports(
ct *change.Tracker,
moduleSpecifier string,
quotePreference quotePreference,
defaultImport *Import,
namedImports []*Import,
namespaceLikeImport *Import, // { importKind: ImportKind.CommonJS | ImportKind.Namespace; }
compilerOptions *core.CompilerOptions,
) []*ast.Statement {
moduleSpecifierStringLiteral := ct.NodeFactory.NewStringLiteral(moduleSpecifier)
if quotePreference == quotePreferenceSingle {
moduleSpecifierStringLiteral.AsStringLiteral().TokenFlags |= ast.TokenFlagsSingleQuote
}
var statements []*ast.Statement // []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 *Import) bool { return needsTypeOnly(i.addAsTypeOnly) }) ||
(compilerOptions.VerbatimModuleSyntax.IsTrue() || ls.UserPreferences().PreferTypeOnlyAutoImports) &&
(defaultImport == nil || defaultImport.addAsTypeOnly != AddAsTypeOnlyNotAllowed) &&
!core.Some(namedImports, func(i *Import) bool { return i.addAsTypeOnly == 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 *Import) *ast.Node {
var namedImportPropertyName *ast.Node
if namedImport.propertyName != "" {
namedImportPropertyName = ct.NodeFactory.NewIdentifier(namedImport.propertyName)
}
return ct.NodeFactory.NewImportSpecifier(
!topLevelTypeOnly && shouldUseTypeOnly(namedImport.addAsTypeOnly, ls.UserPreferences()),
namedImportPropertyName,
ct.NodeFactory.NewIdentifier(namedImport.name),
)
}), moduleSpecifierStringLiteral, topLevelTypeOnly))
}
if namespaceLikeImport != nil {
var declaration *ast.Statement
if namespaceLikeImport.kind == ImportKindCommonJS {
declaration = ct.NodeFactory.NewImportEqualsDeclaration(
/*modifiers*/ nil,
shouldUseTypeOnly(namespaceLikeImport.addAsTypeOnly, ls.UserPreferences()),
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, ls.UserPreferences()), 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 needsTypeOnly(addAsTypeOnly AddAsTypeOnly) bool {
return addAsTypeOnly == AddAsTypeOnlyRequired
}
func shouldUseTypeOnly(addAsTypeOnly AddAsTypeOnly, preferences *lsutil.UserPreferences) bool {
return needsTypeOnly(addAsTypeOnly) || addAsTypeOnly != AddAsTypeOnlyNotAllowed && preferences.PreferTypeOnlyAutoImports
}