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 }