1315 lines
51 KiB
Go
1315 lines
51 KiB
Go
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
|
|
}
|