diff --git a/internal/ast/ast.go b/internal/ast/ast.go index 87e9c79c53..6b1b2aff66 100644 --- a/internal/ast/ast.go +++ b/internal/ast/ast.go @@ -991,10 +991,22 @@ func (n *Node) ModuleSpecifier() *Expression { return n.AsImportDeclaration().ModuleSpecifier case KindExportDeclaration: return n.AsExportDeclaration().ModuleSpecifier + case KindJSDocImportTag: + return n.AsJSDocImportTag().ModuleSpecifier } panic("Unhandled case in Node.ModuleSpecifier: " + n.Kind.String()) } +func (n *Node) ImportClause() *Node { + switch n.Kind { + case KindImportDeclaration: + return n.AsImportDeclaration().ImportClause + case KindJSDocImportTag: + return n.AsJSDocImportTag().ImportClause + } + panic("Unhandled case in Node.ImportClause: " + n.Kind.String()) +} + func (n *Node) Statement() *Statement { switch n.Kind { case KindDoStatement: diff --git a/internal/ast/utilities.go b/internal/ast/utilities.go index b34fa3d51f..ec7b602b95 100644 --- a/internal/ast/utilities.go +++ b/internal/ast/utilities.go @@ -1408,6 +1408,16 @@ func GetNameOfDeclaration(declaration *Node) *Node { return nil } +func GetImportClauseOfDeclaration(declaration *Declaration) *ImportClause { + switch declaration.Kind { + case KindImportDeclaration: + return declaration.AsImportDeclaration().ImportClause.AsImportClause() + case KindJSDocImportTag: + return declaration.AsJSDocImportTag().ImportClause.AsImportClause() + } + return nil +} + func GetNonAssignedNameOfDeclaration(declaration *Node) *Node { // !!! switch declaration.Kind { @@ -2658,6 +2668,10 @@ func nodeContainsPosition(node *Node, position int) bool { return node.Kind >= KindFirstNode && node.Pos() <= position && (position < node.End() || position == node.End() && node.Kind == KindEndOfFile) } +func NodeRangeContainsPosition(node *Node, pos int) bool { + return node.Pos() <= pos && pos <= node.End() +} + func findImportOrRequire(text string, start int) (index int, size int) { index = max(start, 0) n := len(text) @@ -2732,6 +2746,15 @@ func IsRequireCall(node *Node, requireStringLiteralLikeArgument bool) bool { return !requireStringLiteralLikeArgument || IsStringLiteralLike(call.Arguments.Nodes[0]) } +func IsRequireVariableStatement(node *Node) bool { + if IsVariableStatement(node) { + if declarations := node.AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes; len(declarations) > 0 { + return core.Every(declarations, IsVariableDeclarationInitializedToRequire) + } + } + return false +} + func GetJSXImplicitImportBase(compilerOptions *core.CompilerOptions, file *SourceFile) string { jsxImportSourcePragma := GetPragmaFromSourceFile(file, "jsximportsource") jsxRuntimePragma := GetPragmaFromSourceFile(file, "jsxruntime") diff --git a/internal/checker/exports.go b/internal/checker/exports.go index 5481b1f31c..dc61a4a547 100644 --- a/internal/checker/exports.go +++ b/internal/checker/exports.go @@ -26,6 +26,26 @@ func (c *Checker) GetMergedSymbol(symbol *ast.Symbol) *ast.Symbol { return c.getMergedSymbol(symbol) } +func (c *Checker) TryFindAmbientModule(moduleName string) *ast.Symbol { + return c.tryFindAmbientModule(moduleName, true /* withAugmentations */) +} + +func (c *Checker) GetImmediateAliasedSymbol(symbol *ast.Symbol) *ast.Symbol { + return c.getImmediateAliasedSymbol(symbol) +} + +func (c *Checker) GetTypeOnlyAliasDeclaration(symbol *ast.Symbol) *ast.Node { + return c.getTypeOnlyAliasDeclaration(symbol) +} + +func (c *Checker) ResolveExternalModuleName(moduleSpecifier *ast.Node) *ast.Symbol { + return c.resolveExternalModuleName(moduleSpecifier, moduleSpecifier, true /*ignoreErrors*/) +} + +func (c *Checker) ResolveExternalModuleSymbol(moduleSymbol *ast.Symbol) *ast.Symbol { + return c.resolveExternalModuleSymbol(moduleSymbol, false /*dontResolveAlias*/) +} + func (c *Checker) GetTypeFromTypeNode(node *ast.Node) *Type { return c.getTypeFromTypeNode(node) } diff --git a/internal/checker/nodebuilderimpl.go b/internal/checker/nodebuilderimpl.go index 4227c2fb8c..eb63f86d74 100644 --- a/internal/checker/nodebuilderimpl.go +++ b/internal/checker/nodebuilderimpl.go @@ -1079,7 +1079,7 @@ func canHaveModuleSpecifier(node *ast.Node) bool { return false } -func tryGetModuleSpecifierFromDeclaration(node *ast.Node) *ast.Node { +func TryGetModuleSpecifierFromDeclaration(node *ast.Node) *ast.Node { res := tryGetModuleSpecifierFromDeclarationWorker(node) if res == nil || !ast.IsStringLiteral(res) { return nil @@ -1165,7 +1165,7 @@ func (b *nodeBuilderImpl) getSpecifierForModuleSymbol(symbol *ast.Symbol, overri enclosingDeclaration := b.e.MostOriginal(b.ctx.enclosingDeclaration) var originalModuleSpecifier *ast.Node if canHaveModuleSpecifier(enclosingDeclaration) { - originalModuleSpecifier = tryGetModuleSpecifierFromDeclaration(enclosingDeclaration) + originalModuleSpecifier = TryGetModuleSpecifierFromDeclaration(enclosingDeclaration) } contextFile := b.ctx.enclosingFile resolutionMode := overrideImportMode @@ -1216,6 +1216,7 @@ func (b *nodeBuilderImpl) getSpecifierForModuleSymbol(symbol *ast.Symbol, overri modulespecifiers.ModuleSpecifierOptions{ OverrideImportMode: overrideImportMode, }, + false, /*forAutoImports*/ ) specifier := allSpecifiers[0] links.specifierCache[cacheKey] = specifier diff --git a/internal/checker/services.go b/internal/checker/services.go index ed7286203c..ffadc33ada 100644 --- a/internal/checker/services.go +++ b/internal/checker/services.go @@ -118,6 +118,35 @@ func (c *Checker) GetExportsOfModule(symbol *ast.Symbol) []*ast.Symbol { return symbolsToArray(c.getExportsOfModule(symbol)) } +func (c *Checker) ForEachExportAndPropertyOfModule(moduleSymbol *ast.Symbol, cb func(*ast.Symbol, string)) { + for key, exportedSymbol := range c.getExportsOfModule(moduleSymbol) { + if !isReservedMemberName(key) { + cb(exportedSymbol, key) + } + } + + exportEquals := c.resolveExternalModuleSymbol(moduleSymbol, false /*dontResolveAlias*/) + if exportEquals == moduleSymbol { + return + } + + typeOfSymbol := c.getTypeOfSymbol(exportEquals) + if !c.shouldTreatPropertiesOfExternalModuleAsExports(typeOfSymbol) { + return + } + + // forEachPropertyOfType + reducedType := c.getReducedApparentType(typeOfSymbol) + if reducedType.flags&TypeFlagsStructuredType == 0 { + return + } + for name, symbol := range c.resolveStructuredTypeMembers(reducedType).members { + if c.isNamedMember(symbol, name) { + cb(symbol, name) + } + } +} + func (c *Checker) IsValidPropertyAccess(node *ast.Node, propertyName string) bool { return c.isValidPropertyAccess(node, propertyName) } @@ -345,6 +374,13 @@ func runWithoutResolvedSignatureCaching[T any](c *Checker, node *ast.Node, fn fu return fn() } +func (c *Checker) SkipAlias(symbol *ast.Symbol) *ast.Symbol { + if symbol.Flags&ast.SymbolFlagsAlias != 0 { + return c.GetAliasedSymbol(symbol) + } + return symbol +} + func (c *Checker) GetRootSymbols(symbol *ast.Symbol) []*ast.Symbol { roots := c.getImmediateRootSymbols(symbol) if roots != nil { diff --git a/internal/compiler/program.go b/internal/compiler/program.go index 4c409a319f..752c4d8ab0 100644 --- a/internal/compiler/program.go +++ b/internal/compiler/program.go @@ -138,6 +138,10 @@ func (p *Program) UseCaseSensitiveFileNames() bool { return p.Host().FS().UseCaseSensitiveFileNames() } +func (p *Program) UsesUriStyleNodeCoreModules() bool { + return p.usesUriStyleNodeCoreModules.IsTrue() +} + var _ checker.Program = (*Program)(nil) /** This should have similar behavior to 'processSourceFile' without diagnostics or mutation. */ diff --git a/internal/core/compileroptions.go b/internal/core/compileroptions.go index 8ce79b2d02..62af25d37b 100644 --- a/internal/core/compileroptions.go +++ b/internal/core/compileroptions.go @@ -478,6 +478,17 @@ const ( NewLineKindLF NewLineKind = 2 ) +func GetNewLineKind(s string) NewLineKind { + switch s { + case "\r\n": + return NewLineKindCRLF + case "\n": + return NewLineKindLF + default: + return NewLineKindNone + } +} + func (newLine NewLineKind) GetNewLineCharacter() string { switch newLine { case NewLineKindCRLF: diff --git a/internal/core/core.go b/internal/core/core.go index a14fb98290..88dee0a260 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -174,6 +174,17 @@ func Every[T any](slice []T, f func(T) bool) bool { return true } +func Or[T any](funcs ...func(T) bool) func(T) bool { + return func(input T) bool { + for _, f := range funcs { + if f(input) { + return true + } + } + return false + } +} + func Find[T any](slice []T, f func(T) bool) T { for _, value := range slice { if f(value) { diff --git a/internal/core/nodemodules.go b/internal/core/nodemodules.go index c103e4f837..a3abda62b0 100644 --- a/internal/core/nodemodules.go +++ b/internal/core/nodemodules.go @@ -70,7 +70,7 @@ var ExclusivelyPrefixedNodeCoreModules = map[string]bool{ "node:test/reporters": true, } -var nodeCoreModules = sync.OnceValue(func() map[string]bool { +var NodeCoreModules = sync.OnceValue(func() map[string]bool { nodeCoreModules := make(map[string]bool, len(UnprefixedNodeCoreModules)*2+len(ExclusivelyPrefixedNodeCoreModules)) for unprefixed := range UnprefixedNodeCoreModules { nodeCoreModules[unprefixed] = true @@ -81,7 +81,7 @@ var nodeCoreModules = sync.OnceValue(func() map[string]bool { }) func NonRelativeModuleNameForTypingCache(moduleName string) string { - if nodeCoreModules()[moduleName] { + if NodeCoreModules()[moduleName] { return "node" } return moduleName diff --git a/internal/format/api.go b/internal/format/api.go index db223ef543..42fb7ee2b2 100644 --- a/internal/format/api.go +++ b/internal/format/api.go @@ -75,6 +75,25 @@ func FormatSpan(ctx context.Context, span core.TextRange, file *ast.SourceFile, ) } +func FormatNodeGivenIndentation(ctx context.Context, node *ast.Node, file *ast.SourceFile, languageVariant core.LanguageVariant, initialIndentation int, delta int) []core.TextChange { + textRange := core.NewTextRange(node.Pos(), node.End()) + return newFormattingScanner( + file.Text(), + languageVariant, + textRange.Pos(), + textRange.End(), + newFormatSpanWorker( + ctx, + textRange, + node, + initialIndentation, + delta, + FormatRequestKindFormatSelection, + func(core.TextRange) bool { return false }, // assume that node does not have any errors + file, + )) +} + func formatNodeLines(ctx context.Context, sourceFile *ast.SourceFile, node *ast.Node, requestKind FormatRequestKind) []core.TextChange { if node == nil { return nil diff --git a/internal/format/indent.go b/internal/format/indent.go index f7eab25fea..6a9f5908da 100644 --- a/internal/format/indent.go +++ b/internal/format/indent.go @@ -45,7 +45,7 @@ func getIndentationForNodeWorker( if useActualIndentation { // check if current node is a list item - if yes, take indentation from it var firstListChild *ast.Node - containerList := getContainingList(current, sourceFile) + containerList := GetContainingList(current, sourceFile) if containerList != nil { firstListChild = core.FirstOrNil(containerList.Nodes) } @@ -139,7 +139,7 @@ func getActualIndentationForListItem(node *ast.Node, sourceFile *ast.SourceFile, // VariableDeclarationList has no wrapping tokens return -1 } - containingList := getContainingList(node, sourceFile) + containingList := GetContainingList(node, sourceFile) if containingList != nil { index := core.FindIndex(containingList.Nodes, func(e *ast.Node) bool { return e == node }) if index != -1 { @@ -196,10 +196,10 @@ func deriveActualIndentationFromList(list *ast.NodeList, index int, sourceFile * func findColumnForFirstNonWhitespaceCharacterInLine(line int, char int, sourceFile *ast.SourceFile, options *FormatCodeSettings) int { lineStart := scanner.GetPositionOfLineAndCharacter(sourceFile, line, 0) - return findFirstNonWhitespaceColumn(lineStart, lineStart+char, sourceFile, options) + return FindFirstNonWhitespaceColumn(lineStart, lineStart+char, sourceFile, options) } -func findFirstNonWhitespaceColumn(startPos int, endPos int, sourceFile *ast.SourceFile, options *FormatCodeSettings) int { +func FindFirstNonWhitespaceColumn(startPos int, endPos int, sourceFile *ast.SourceFile, options *FormatCodeSettings) int { _, col := findFirstNonWhitespaceCharacterAndColumn(startPos, endPos, sourceFile, options) return col } @@ -249,7 +249,7 @@ func getStartLineAndCharacterForNode(n *ast.Node, sourceFile *ast.SourceFile) (l return scanner.GetLineAndCharacterOfPosition(sourceFile, scanner.GetTokenPosOfNode(n, sourceFile, false)) } -func getContainingList(node *ast.Node, sourceFile *ast.SourceFile) *ast.NodeList { +func GetContainingList(node *ast.Node, sourceFile *ast.SourceFile) *ast.NodeList { if node.Parent == nil { return nil } @@ -348,7 +348,7 @@ func getVisualListRange(node *ast.Node, list core.TextRange, sourceFile *ast.Sou } func getContainingListOrParentStart(parent *ast.Node, child *ast.Node, sourceFile *ast.SourceFile) (line int, character int) { - containingList := getContainingList(child, sourceFile) + containingList := GetContainingList(child, sourceFile) var startPos int if containingList != nil { startPos = containingList.Loc.Pos() diff --git a/internal/format/span.go b/internal/format/span.go index 87fd829daa..70a2ae5b5c 100644 --- a/internal/format/span.go +++ b/internal/format/span.go @@ -468,7 +468,7 @@ func (w *formatSpanWorker) processChildNodes( indentationOnListStartToken = w.indentationOnLastIndentedLine } else { startLinePosition := getLineStartPositionForPosition(tokenInfo.token.Loc.Pos(), w.sourceFile) - indentationOnListStartToken = findFirstNonWhitespaceColumn(startLinePosition, tokenInfo.token.Loc.Pos(), w.sourceFile, w.formattingContext.Options) + indentationOnListStartToken = FindFirstNonWhitespaceColumn(startLinePosition, tokenInfo.token.Loc.Pos(), w.sourceFile, w.formattingContext.Options) } listDynamicIndentation = w.getDynamicIndentation(parent, parentStartLine, indentationOnListStartToken, w.formattingContext.Options.IndentSize) @@ -578,7 +578,7 @@ func (w *formatSpanWorker) tryComputeIndentationForListItem(startPos int, endPos } else { startLine, _ := scanner.GetLineAndCharacterOfPosition(w.sourceFile, startPos) startLinePosition := getLineStartPositionForPosition(startPos, w.sourceFile) - column := findFirstNonWhitespaceColumn(startLinePosition, startPos, w.sourceFile, w.formattingContext.Options) + column := FindFirstNonWhitespaceColumn(startLinePosition, startPos, w.sourceFile, w.formattingContext.Options) if startLine != parentStartLine || startPos == column { // Use the base indent size if it is greater than // the indentation of the inherited predecessor. diff --git a/internal/fourslash/_scripts/failingTests.txt b/internal/fourslash/_scripts/failingTests.txt index 873155459d..6b462d6242 100644 --- a/internal/fourslash/_scripts/failingTests.txt +++ b/internal/fourslash/_scripts/failingTests.txt @@ -18,7 +18,6 @@ TestCompletionEntryForArrayElementConstrainedToString2 TestCompletionEntryForClassMembers_StaticWhenBaseTypeIsNotResolved TestCompletionEntryForUnionProperty TestCompletionEntryForUnionProperty2 -TestCompletionExportFrom TestCompletionForComputedStringProperties TestCompletionForMetaProperty TestCompletionForStringLiteral @@ -66,7 +65,6 @@ TestCompletionImportModuleSpecifierEndingUnsupportedExtension TestCompletionInFunctionLikeBody_includesPrimitiveTypes TestCompletionInJsDoc TestCompletionInJsDocQualifiedNames -TestCompletionInNamedImportLocation TestCompletionInUncheckedJSFile TestCompletionJSDocNamePath TestCompletionListBuilderLocations_VariableDeclarations @@ -95,7 +93,6 @@ TestCompletionListInUnclosedTaggedTemplate02 TestCompletionListInUnclosedTemplate01 TestCompletionListInUnclosedTemplate02 TestCompletionListInvalidMemberNames -TestCompletionListInvalidMemberNames2 TestCompletionListInvalidMemberNames_escapeQuote TestCompletionListInvalidMemberNames_startWithSpace TestCompletionListInvalidMemberNames_withExistingIdentifier @@ -108,12 +105,10 @@ TestCompletionListWithoutVariableinitializer TestCompletionListsStringLiteralTypeAsIndexedAccessTypeObject TestCompletionNoAutoInsertQuestionDotForThis TestCompletionNoAutoInsertQuestionDotForTypeParameter -TestCompletionNoAutoInsertQuestionDotWithUserPreferencesOff TestCompletionOfAwaitPromise1 TestCompletionOfAwaitPromise2 TestCompletionOfAwaitPromise3 TestCompletionOfAwaitPromise5 -TestCompletionOfAwaitPromise6 TestCompletionOfAwaitPromise7 TestCompletionOfInterfaceAndVar TestCompletionPreferredSuggestions1 @@ -123,7 +118,6 @@ TestCompletionsBeforeRestArg1 TestCompletionsElementAccessNumeric TestCompletionsExportImport TestCompletionsGenericTypeWithMultipleBases1 -TestCompletionsImportOrExportSpecifier TestCompletionsInExport TestCompletionsInExport_moduleBlock TestCompletionsInRequire @@ -230,9 +224,6 @@ TestImportCompletions_importsMap2 TestImportCompletions_importsMap3 TestImportCompletions_importsMap4 TestImportCompletions_importsMap5 -TestImportStatementCompletions4 -TestImportStatementCompletions_noPatternAmbient -TestImportStatementCompletions_pnpmTransitive TestIndexerReturnTypes1 TestIndirectClassInstantiation TestInstanceTypesForGenericType1 @@ -291,7 +282,6 @@ TestLetQuickInfoAndCompletionList TestLocalFunction TestLocalGetReferences TestMemberListInReopenedEnum -TestMemberListInWithBlock TestMemberListOfExportedClass TestMemberListOnContextualThis TestModuleReexportedIntoGlobalQuickInfo @@ -416,7 +406,6 @@ TestQuickInfoOnFunctionPropertyReturnedFromGenericFunction3 TestQuickInfoOnGenericWithConstraints1 TestQuickInfoOnInternalAliases TestQuickInfoOnMethodOfImportEquals -TestQuickInfoOnNarrowedType TestQuickInfoOnNarrowedTypeInModule TestQuickInfoOnNewKeyword01 TestQuickInfoOnObjectLiteralWithAccessors diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index a8f0f8c997..dc9c1206f0 100644 --- a/internal/fourslash/fourslash.go +++ b/internal/fourslash/fourslash.go @@ -731,7 +731,7 @@ func (f *FourslashTest) verifyCompletionsAreExactly(t *testing.T, prefix string, var completionIgnoreOpts = cmp.FilterPath( func(p cmp.Path) bool { switch p.Last().String() { - case ".Kind", ".SortText", ".Data": + case ".Kind", ".SortText", ".Data", ".FilterText": return true default: return false @@ -752,6 +752,10 @@ func (f *FourslashTest) verifyCompletionItem(t *testing.T, prefix string, actual actual = result } assertDeepEqual(t, actual, expected, prefix, completionIgnoreOpts) + // in strada, filtertext was generated on the extension side in most cases + if expected.FilterText != nil { + assertDeepEqual(t, actual.FilterText, expected.FilterText, prefix+" FilterText mismatch") + } if expected.Kind != nil { assertDeepEqual(t, actual.Kind, expected.Kind, prefix+" Kind mismatch") } diff --git a/internal/fourslash/tests/gen/completionExportFrom_test.go b/internal/fourslash/tests/gen/completionExportFrom_test.go index c19346f4cf..e648a2b101 100644 --- a/internal/fourslash/tests/gen/completionExportFrom_test.go +++ b/internal/fourslash/tests/gen/completionExportFrom_test.go @@ -12,7 +12,7 @@ import ( func TestCompletionExportFrom(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `export * /*1*/; export {} /*2*/;` diff --git a/internal/fourslash/tests/gen/importStatementCompletions4_test.go b/internal/fourslash/tests/gen/importStatementCompletions4_test.go index 30c586e479..274183401c 100644 --- a/internal/fourslash/tests/gen/importStatementCompletions4_test.go +++ b/internal/fourslash/tests/gen/importStatementCompletions4_test.go @@ -12,7 +12,7 @@ import ( func TestImportStatementCompletions4(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `// @allowJs: true // @Filename: /a.js diff --git a/internal/fourslash/tests/gen/importStatementCompletions_noPatternAmbient_test.go b/internal/fourslash/tests/gen/importStatementCompletions_noPatternAmbient_test.go index 09532666fa..4700a41492 100644 --- a/internal/fourslash/tests/gen/importStatementCompletions_noPatternAmbient_test.go +++ b/internal/fourslash/tests/gen/importStatementCompletions_noPatternAmbient_test.go @@ -12,7 +12,7 @@ import ( func TestImportStatementCompletions_noPatternAmbient(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `// @Filename: /types.d.ts declare module "*.css" { diff --git a/internal/fourslash/tests/gen/importStatementCompletions_pnpmTransitive_test.go b/internal/fourslash/tests/gen/importStatementCompletions_pnpmTransitive_test.go index becc9be3b2..297213158f 100644 --- a/internal/fourslash/tests/gen/importStatementCompletions_pnpmTransitive_test.go +++ b/internal/fourslash/tests/gen/importStatementCompletions_pnpmTransitive_test.go @@ -12,7 +12,7 @@ import ( func TestImportStatementCompletions_pnpmTransitive(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `// @Filename: /home/src/workspaces/project/tsconfig.json { "compilerOptions": { "module": "commonjs" } } diff --git a/internal/ls/autoimports.go b/internal/ls/autoimports.go new file mode 100644 index 0000000000..8ded952f16 --- /dev/null +++ b/internal/ls/autoimports.go @@ -0,0 +1,1651 @@ +package ls + +import ( + "context" + "fmt" + "strconv" + "strings" + + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/astnav" + "github.com/microsoft/typescript-go/internal/binder" + "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/diagnostics" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/module" + "github.com/microsoft/typescript-go/internal/modulespecifiers" + "github.com/microsoft/typescript-go/internal/stringutil" + "github.com/microsoft/typescript-go/internal/tspath" +) + +type SymbolExportInfo struct { + symbol *ast.Symbol + moduleSymbol *ast.Symbol + moduleFileName string + exportKind ExportKind + targetFlags ast.SymbolFlags + isFromPackageJson *bool +} + +type symbolExportEntry struct { + symbol *ast.Symbol + moduleSymbol *ast.Symbol +} + +type ExportMapInfoKey string + +func newExportMapInfoKey(importedName string, symbol *ast.Symbol, ambientModuleNameKey string, ch *checker.Checker) ExportMapInfoKey { + return ExportMapInfoKey(fmt.Sprintf("%s %d %s %s", + strconv.Itoa(len(importedName)), + ast.GetSymbolId(ch.SkipAlias(symbol)), + importedName, + ambientModuleNameKey, + )) +} + +func (key ExportMapInfoKey) parse() (string, string) { + parts := strings.SplitN(string(key), " ", 3) + symbolNameLen, _ := strconv.Atoi(parts[0]) + + symbolName := parts[2][:symbolNameLen] + moduleKey := parts[2][symbolNameLen+1:] + + return symbolName, moduleKey +} + +type CachedSymbolExportInfo struct { + // Used to rehydrate `symbol` and `moduleSymbol` when transient + id int + symbolTableKey string + symbolName string + capitalizedSymbolName string + moduleName string + moduleFile *ast.SourceFile // may be nil + packageName string + + symbol *ast.Symbol // may be nil + moduleSymbol *ast.Symbol // may be nil + moduleFileName string // may be "" + targetFlags ast.SymbolFlags + exportKind ExportKind + isFromPackageJson bool +} + +type exportInfoMap struct { + exportInfo collections.MultiMap[ExportMapInfoKey, CachedSymbolExportInfo] + symbols map[int]symbolExportEntry + exportInfoId int + usableByFileName *tspath.Path + packages map[string]string + + globalTypingsCacheLocation string + ls *LanguageService + + isUsableByFile func(importingFile tspath.Path) bool + get func(importingFile tspath.Path, key string) []*SymbolExportInfo + releaseSymbols func() + isEmpty func() bool + /** @returns Whether the change resulted in the cache being cleared */ + onFileChanged func(oldSourceFile *ast.SourceFile, newSourceFile *ast.SourceFile, typeAcquisitionEnabled bool) bool +} + +func (e *exportInfoMap) clear() { + e.symbols = map[int]symbolExportEntry{} + e.exportInfo = collections.MultiMap[ExportMapInfoKey, CachedSymbolExportInfo]{} + e.usableByFileName = nil +} + +func (e *exportInfoMap) add( + importingFile tspath.Path, + symbol *ast.Symbol, + symbolTableKey string, + moduleSymbol *ast.Symbol, + moduleFile *ast.SourceFile, + exportKind ExportKind, + isFromPackageJson bool, + ch *checker.Checker, +) { + if e.usableByFileName == nil || importingFile != *e.usableByFileName { + e.clear() + e.usableByFileName = &importingFile + } + + packageName := "" + if moduleFile != nil { + if nodeModulesPathParts := modulespecifiers.GetNodeModulePathParts(moduleFile.FileName()); nodeModulesPathParts != nil { + topLevelNodeModulesIndex := nodeModulesPathParts.TopLevelNodeModulesIndex + topLevelPackageNameIndex := nodeModulesPathParts.TopLevelPackageNameIndex + packageRootIndex := nodeModulesPathParts.PackageRootIndex + packageName = module.UnmangleScopedPackageName(modulespecifiers.GetPackageNameFromTypesPackageName(moduleFile.FileName()[topLevelPackageNameIndex+1 : packageRootIndex])) + if strings.HasPrefix(string(importingFile), string(moduleFile.Path())[0:topLevelNodeModulesIndex]) { + nodeModulesPath := moduleFile.FileName()[0 : topLevelPackageNameIndex+1] + if prevDeepestNodeModulesPath, ok := e.packages[packageName]; ok { + prevDeepestNodeModulesIndex := strings.Index(prevDeepestNodeModulesPath, "/node_modules/") + if topLevelNodeModulesIndex > prevDeepestNodeModulesIndex { + e.packages[packageName] = nodeModulesPath + } + } else { + e.packages[packageName] = nodeModulesPath + } + } + } + } + + isDefault := exportKind == ExportKindDefault + namedSymbol := symbol + if isDefault { + if s := binder.GetLocalSymbolForExportDefault(symbol); s != nil { + namedSymbol = s + } + } + // 1. A named export must be imported by its key in `moduleSymbol.exports` or `moduleSymbol.members`. + // 2. A re-export merged with an export from a module augmentation can result in `symbol` + // being an external module symbol; the name it is re-exported by will be `symbolTableKey` + // (which comes from the keys of `moduleSymbol.exports`.) + // 3. Otherwise, we have a default/namespace import that can be imported by any name, and + // `symbolTableKey` will be something undesirable like `export=` or `default`, so we try to + // get a better name. + names := []string{} + if exportKind == ExportKindNamed || checker.IsExternalModuleSymbol(namedSymbol) { + names = append(names, symbolTableKey) + } else { + names = getNamesForExportedSymbol(namedSymbol, ch, core.ScriptTargetNone) + } + + symbolName := names[0] + capitalizedSymbolName := "" + if len(names) > 1 { + capitalizedSymbolName = names[1] + } + + moduleName := stringutil.StripQuotes(moduleSymbol.Name) + id := e.exportInfoId + 1 + target := ch.SkipAlias(symbol) + + var storedSymbol, storedModuleSymbol *ast.Symbol + + if symbol.Flags&ast.SymbolFlagsTransient == 0 { + storedSymbol = symbol + } + if moduleSymbol.Flags&ast.SymbolFlagsTransient == 0 { + storedModuleSymbol = moduleSymbol + } + + if storedSymbol == nil || storedModuleSymbol == nil { + e.symbols[id] = symbolExportEntry{storedSymbol, storedModuleSymbol} + } + + moduleKey := "" + if !tspath.IsExternalModuleNameRelative(moduleName) { + moduleKey = moduleName + } + + e.exportInfo.Add(newExportMapInfoKey(symbolName, symbol, moduleKey, ch), CachedSymbolExportInfo{ + id: id, + symbolTableKey: symbolTableKey, + symbolName: symbolName, + capitalizedSymbolName: capitalizedSymbolName, + moduleName: moduleName, + moduleFile: moduleFile, + packageName: packageName, + + symbol: storedSymbol, + moduleSymbol: storedModuleSymbol, + exportKind: exportKind, + targetFlags: target.Flags, + isFromPackageJson: isFromPackageJson, + }) +} + +func (e *exportInfoMap) search( + ch *checker.Checker, + importingFile tspath.Path, + preferCapitalized bool, + matches func(name string, targetFlags ast.SymbolFlags) bool, + action func(info []*SymbolExportInfo, symbolName string, isFromAmbientModule bool, key ExportMapInfoKey) []*SymbolExportInfo, +) []*SymbolExportInfo { + if ptrTo(importingFile) != e.usableByFileName { + return nil + } + for key, info := range e.exportInfo.M { + symbolName, ambientModuleName := key.parse() + name := symbolName + if preferCapitalized && info[0].capitalizedSymbolName != "" { + name = info[0].capitalizedSymbolName + } + if matches(name, info[0].targetFlags) { + rehydrated := core.Map(info, func(info CachedSymbolExportInfo) *SymbolExportInfo { + return e.rehydrateCachedInfo(ch, info) + }) + filtered := core.FilterIndex(rehydrated, func(r *SymbolExportInfo, i int, _ []*SymbolExportInfo) bool { + return e.isNotShadowedByDeeperNodeModulesPackage(r, info[i].packageName) + }) + if len(filtered) > 0 { + if res := action(filtered, name, ambientModuleName != "", key); res != nil { + return res + } + } + } + } + return nil +} + +func (e *exportInfoMap) isNotShadowedByDeeperNodeModulesPackage(info *SymbolExportInfo, packageName string) bool { + if packageName == "" || info.moduleFileName == "" { + return true + } + if e.globalTypingsCacheLocation != "" && strings.HasPrefix(info.moduleFileName, e.globalTypingsCacheLocation) { + return true + } + packageDeepestNodeModulesPath, ok := e.packages[packageName] + return !ok || strings.HasPrefix(info.moduleFileName, packageDeepestNodeModulesPath) +} + +func (e *exportInfoMap) rehydrateCachedInfo(ch *checker.Checker, info CachedSymbolExportInfo) *SymbolExportInfo { + if info.symbol != nil && info.moduleSymbol != nil { + return &SymbolExportInfo{ + symbol: info.symbol, + moduleSymbol: info.moduleSymbol, + moduleFileName: info.moduleFileName, + exportKind: info.exportKind, + targetFlags: info.targetFlags, + isFromPackageJson: ptrTo(info.isFromPackageJson), + } + } + cached := e.symbols[info.id] + cachedSymbol, cachedModuleSymbol := cached.symbol, cached.moduleSymbol + if cachedSymbol != nil && cachedModuleSymbol != nil { + return &SymbolExportInfo{ + symbol: cachedSymbol, + moduleSymbol: cachedModuleSymbol, + moduleFileName: info.moduleFileName, + exportKind: info.exportKind, + targetFlags: info.targetFlags, + isFromPackageJson: ptrTo(info.isFromPackageJson), + } + } + + moduleSymbol := core.Coalesce(info.moduleSymbol, cachedModuleSymbol) + if moduleSymbol == nil { + if info.moduleFile != nil { + moduleSymbol = ch.GetMergedSymbol(info.moduleFile.Symbol) + } else { + moduleSymbol = ch.TryFindAmbientModule(info.moduleName) + } + } + if moduleSymbol == nil { + panic(fmt.Sprintf("Could not find module symbol for %s in exportInfoMap", info.moduleName)) + } + symbol := core.Coalesce(info.symbol, cachedSymbol) + if symbol == nil { + if info.exportKind == ExportKindExportEquals { + symbol = ch.ResolveExternalModuleSymbol(moduleSymbol) + } else { + symbol = ch.TryGetMemberInModuleExportsAndProperties(info.symbolTableKey, moduleSymbol) + } + } + + if symbol == nil { + panic(fmt.Sprintf("Could not find symbol '%s' by key '%s' in module %s", info.symbolName, info.symbolTableKey, moduleSymbol.Name)) + } + e.symbols[info.id] = symbolExportEntry{symbol, moduleSymbol} + return &SymbolExportInfo{ + symbol, + moduleSymbol, + info.moduleFileName, + info.exportKind, + info.targetFlags, + ptrTo(info.isFromPackageJson), + } +} + +func getNamesForExportedSymbol(defaultExport *ast.Symbol, ch *checker.Checker, scriptTarget core.ScriptTarget) []string { + var names []string + forEachNameOfDefaultExport(defaultExport, ch, scriptTarget, func(name, capitalizedName string) string { + if capitalizedName != "" { + names = []string{name, capitalizedName} + } else { + names = []string{name} + } + return name + }) + return names +} + +type packageJsonImportFilter struct { + allowsImportingAmbientModule func(moduleSymbol *ast.Symbol, host modulespecifiers.ModuleSpecifierGenerationHost) bool + getSourceFileInfo func(sourceFile *ast.SourceFile, host modulespecifiers.ModuleSpecifierGenerationHost) packageJsonFilterResult + /** + * Use for a specific module specifier that has already been resolved. + * Use `allowsImportingAmbientModule` or `allowsImportingSourceFile` to resolve + * the best module specifier for a given module _and_ determine if it's importable. + */ + allowsImportingSpecifier func(moduleSpecifier string) bool +} + +type packageJsonFilterResult struct { + importable bool + packageName string +} +type projectPackageJsonInfo struct { + fileName string + parseable bool + dependencies map[string]string + devDependencies map[string]string + peerDependencies map[string]string + optionalDependencies map[string]string +} + +func (info *projectPackageJsonInfo) has(dependencyName string) bool { + if _, ok := info.dependencies[dependencyName]; ok { + return true + } + if _, ok := info.devDependencies[dependencyName]; ok { + return true + } + + if _, ok := info.peerDependencies[dependencyName]; ok { + return true + } + if _, ok := info.optionalDependencies[dependencyName]; ok { + return true + } + + return false +} + +// type codeAction struct { +// /** Description of the code action to display in the UI of the editor */ +// description string +// /** Text changes to apply to each file as part of the code action */ +// changes []FileTextChanges +// /** +// * If the user accepts the code fix, the editor should send the action back in a `applyAction` request. +// * This allows the language service to have side effects (e.g. installing dependencies) upon a code fix. +// */ +// commands [] CodeActionCommand +// } + +type completionCodeAction struct { + description string + changes []*lsproto.TextEdit + commands *lsproto.Command +} + +func (l *LanguageService) getImportCompletionAction( + ctx context.Context, + ch *checker.Checker, + targetSymbol *ast.Symbol, + moduleSymbol *ast.Symbol, + sourceFile *ast.SourceFile, + position int, + exportMapKey string, // !!! needs *string ? + symbolName string, // !!! needs *string ? + isJsxTagName bool, + // formatContext *formattingContext, + preferences *UserPreferences, +) (string, codeAction) { + var exportInfos []*SymbolExportInfo + if exportMapKey != "" { + // The new way: `exportMapKey` should be in the `data` of each auto-import completion entry and + // sent back when asking for details. + exportInfos = l.getExportInfoMap(ch, sourceFile, preferences).get(sourceFile.Path(), exportMapKey) + if exportInfos == nil { + panic("Some exportInfo should match the specified exportMapKey") + } + } else { + // The old way, kept alive for super old editors that don't give us `data` back. + if modulespecifiers.PathIsBareSpecifier(stringutil.StripQuotes(moduleSymbol.Name)) { + exportInfos = []*SymbolExportInfo{l.getSingleExportInfoForSymbol(ch, targetSymbol, symbolName, moduleSymbol)} + } else { + exportInfos = l.getAllExportInfoForSymbol(ch, sourceFile, targetSymbol, symbolName, moduleSymbol, isJsxTagName, preferences) + } + if exportInfos == nil { + panic("Some exportInfo should match the specified exportMapKey") + } + } + + isValidTypeOnlyUseSite := ast.IsValidTypeOnlyAliasUseSite(astnav.GetTokenAtPosition(sourceFile, position)) + fix := l.getImportFixForSymbol(ch, sourceFile, exportInfos, position, ptrTo(isValidTypeOnlyUseSite), preferences) + if fix == nil { + lineAndChar := l.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(position)) + panic(fmt.Sprintf("expected importFix at %s: (%v,%v)", sourceFile.FileName(), lineAndChar.Line, lineAndChar.Character)) + } + return fix.Base().moduleSpecifier, l.codeActionForFix(ctx, sourceFile, symbolName, fix /*includeSymbolNameInDescription*/, false, preferences) +} + +func NewExportInfoMap(ls *LanguageService) *exportInfoMap { + return &exportInfoMap{ls: ls} +} + +func (l *LanguageService) getExportInfoMap( + ch *checker.Checker, + importingFile *ast.SourceFile, + preferences *UserPreferences, +) *exportInfoMap { + // Pulling the AutoImportProvider project will trigger its updateGraph if pending, + // which will invalidate the export map cache if things change, so pull it before + // checking the cache. + // l.GetPackageJsonAutoImportProvider?.(); + // cache := host.getCachedExportInfoMap() + // || createCacheableExportInfoMap({ + // getCurrentProgram: () => program, + // getPackageJsonAutoImportProvider: () => host.getPackageJsonAutoImportProvider?.(), + // getGlobalTypingsCacheLocation: () => host.getGlobalTypingsCacheLocation?.(), + // }); + + // if (cache.isUsableByFile(importingFile.path)) { + // host.log?.("getExportInfoMap: cache hit"); + // return cache; + // } + + // host.log?.("getExportInfoMap: expInfoMap miss or empty; calculating new results"); + expInfoMap := NewExportInfoMap(l) + moduleCount := 0 + l.forEachExternalModuleToImportFrom( + ch, + preferences, + // /*useAutoImportProvider*/ true, + func(moduleSymbol *ast.Symbol, moduleFile *ast.SourceFile, ch *checker.Checker, isFromPackageJson bool) { + if moduleCount = moduleCount + 1; moduleCount%100 == 0 { + return + } + seenExports := collections.Set[string]{} + defaultInfo := getDefaultLikeExportInfo(moduleSymbol, ch) + // Note: I think we shouldn't actually see resolved module symbols here, but weird merges + // can cause it to happen: see 'completionsImport_mergedReExport.ts' + if defaultInfo != nil && isImportableSymbol(defaultInfo.exportingModuleSymbol, ch) { + expInfoMap.add( + importingFile.Path(), + defaultInfo.exportingModuleSymbol, + core.IfElse(defaultInfo.exportKind == ExportKindDefault, ast.InternalSymbolNameDefault, ast.InternalSymbolNameExportEquals), + moduleSymbol, + moduleFile, + defaultInfo.exportKind, + isFromPackageJson, + ch, + ) + } + ch.ForEachExportAndPropertyOfModule(moduleSymbol, func(exported *ast.Symbol, key string) { + if defaultInfo == nil { + return + } + if exported != defaultInfo.exportingModuleSymbol && isImportableSymbol(exported, ch) && seenExports.Has(key) { + expInfoMap.add( + importingFile.Path(), + exported, + key, + moduleSymbol, + moduleFile, + ExportKindNamed, + isFromPackageJson, + ch, + ) + } + }) + }) + + // catch (err) { + // // Ensure cache is reset if operation is cancelled + // cache.clear(); + // throw err; + // } + + // host.log?.(`getExportInfoMap: done in ${timestamp() - start} ms`); + return expInfoMap +} + +func (l *LanguageService) getAllExportInfoForSymbol(ch *checker.Checker, importingFile *ast.SourceFile, symbol *ast.Symbol, symbolName string, moduleSymbol *ast.Symbol, preferCapitalized bool, preferences *UserPreferences) []*SymbolExportInfo { + // !!! isFileExcluded := len(preferences.AutoImportFileExcludePatterns) != 0 && getIsFileExcluded(host, preferences); + // mergedModuleSymbol := ch.GetMergedSymbol(moduleSymbol) + // moduleSourceFile := isFileExcluded && len(mergedModuleSymbol.Declarations) > 0 && ast.GetDeclarationOfKind(mergedModuleSymbol, SyntaxKind.SourceFile) + // moduleSymbolExcluded := moduleSourceFile && isFileExcluded(moduleSourceFile.AsSourceFile()); + moduleSymbolExcluded := false + return l.getExportInfoMap(ch, importingFile, preferences).search( + ch, + importingFile.Path(), + preferCapitalized, + func(name string, _ ast.SymbolFlags) bool { return name == symbolName }, + func(info []*SymbolExportInfo, symbolName string, isFromAmbientModule bool, key ExportMapInfoKey) []*SymbolExportInfo { + if ch.GetMergedSymbol(ch.SkipAlias(info[0].symbol)) == symbol && (moduleSymbolExcluded || core.Some(info, func(i *SymbolExportInfo) bool { + return ch.GetMergedSymbol(i.moduleSymbol) == moduleSymbol || i.symbol.Parent == moduleSymbol + })) { + return info + } + return nil + }, + ) +} + +func (l *LanguageService) getSingleExportInfoForSymbol(ch *checker.Checker, symbol *ast.Symbol, symbolName string, moduleSymbol *ast.Symbol) *SymbolExportInfo { + getInfoWithChecker := func(program *compiler.Program, isFromPackageJson bool) *SymbolExportInfo { + defaultInfo := getDefaultLikeExportInfo(moduleSymbol, ch) + if defaultInfo != nil && ch.SkipAlias(defaultInfo.exportingModuleSymbol) == symbol { + return &SymbolExportInfo{ + symbol: defaultInfo.exportingModuleSymbol, + moduleSymbol: moduleSymbol, + moduleFileName: "", + exportKind: defaultInfo.exportKind, + targetFlags: ch.SkipAlias(symbol).Flags, + isFromPackageJson: &isFromPackageJson, + } + } + if named := ch.TryGetMemberInModuleExportsAndProperties(symbolName, moduleSymbol); named != nil && ch.SkipAlias(named) == symbol { + return &SymbolExportInfo{ + symbol: named, + moduleSymbol: moduleSymbol, + moduleFileName: "", + exportKind: ExportKindNamed, + targetFlags: ch.SkipAlias(symbol).Flags, + isFromPackageJson: &isFromPackageJson, + } + } + return nil + } + + if mainProgramInfo := getInfoWithChecker(l.GetProgram() /*isFromPackageJson*/, false); mainProgramInfo != nil { + return mainProgramInfo + } + // !!! autoImportProvider := host.getPackageJsonAutoImportProvider?.()? + // return Debug.checkDefined(autoImportProvider && getInfoWithChecker(autoImportProvider, /*isFromPackageJson*/ true), `Could not find symbol in specified module for code actions`); + return nil +} + +func (l *LanguageService) isImportable( + fromFile *ast.SourceFile, + toFile *ast.SourceFile, + toModule *ast.Symbol, + preferences *UserPreferences, + packageJsonFilter *packageJsonImportFilter, + // moduleSpecifierResolutionHost ModuleSpecifierResolutionHost, + // moduleSpecifierCache ModuleSpecifierCache, +) bool { + // !!! moduleSpecifierResolutionHost := l.GetModuleSpecifierResolutionHost() + moduleSpecifierResolutionHost := l.GetProgram() + + // Ambient module + if toFile == nil { + moduleName := stringutil.StripQuotes(toModule.Name) + if _, ok := core.NodeCoreModules()[moduleName]; ok { + if useNodePrefix := shouldUseUriStyleNodeCoreModules(fromFile, l.GetProgram()); useNodePrefix { + return useNodePrefix == strings.HasPrefix(moduleName, "node:") + } + } + return packageJsonFilter == nil || + packageJsonFilter.allowsImportingAmbientModule(toModule, moduleSpecifierResolutionHost) || + fileContainsPackageImport(fromFile, moduleName) + } + + if fromFile == toFile { + return false + } + + // !!! moduleSpecifierCache + // cachedResult := moduleSpecifierCache?.get(fromFile.path, toFile.path, preferences, {}) + // if cachedResult?.isBlockedByPackageJsonDependencies != nil { + // return !cachedResult.isBlockedByPackageJsonDependencies || cachedResult.packageName != nil && fileContainsPackageImport(fromFile, cachedResult.packageName) + // } + + fromPath := fromFile.FileName() + useCaseSensitiveFileNames := moduleSpecifierResolutionHost.UseCaseSensitiveFileNames() + globalTypingsCache := l.GetProgram().GetGlobalTypingsCacheLocation() + modulePaths := modulespecifiers.GetEachFileNameOfModule( + fromPath, + toFile.FileName(), + moduleSpecifierResolutionHost, + /*preferSymlinks*/ false, + ) + hasImportablePath := false + for _, module := range modulePaths { + file := l.GetProgram().GetSourceFile(module.FileName) + + // Determine to import using toPath only if toPath is what we were looking at + // or there doesnt exist the file in the program by the symlink + if file == nil || file != toFile { + continue + } + + // If it's in a `node_modules` but is not reachable from here via a global import, don't bother. + toNodeModules := tspath.ForEachAncestorDirectoryStoppingAtGlobalCache( + globalTypingsCache, + module.FileName, + func(ancestor string) (string, bool) { + if tspath.GetBaseFileName(ancestor) == "node_modules" { + return ancestor, true + } else { + return "", false + } + }, + ) + toNodeModulesParent := "" + if toNodeModules != "" { + toNodeModulesParent = tspath.GetDirectoryPath(tspath.GetCanonicalFileName(toNodeModules, useCaseSensitiveFileNames)) + } + hasImportablePath = toNodeModulesParent != "" || + strings.HasPrefix(tspath.GetCanonicalFileName(fromPath, useCaseSensitiveFileNames), toNodeModulesParent) || + (globalTypingsCache != "" && strings.HasPrefix(tspath.GetCanonicalFileName(globalTypingsCache, useCaseSensitiveFileNames), toNodeModulesParent)) + if hasImportablePath { + break + } + } + + if packageJsonFilter != nil { + if hasImportablePath { + importInfo := packageJsonFilter.getSourceFileInfo(toFile, moduleSpecifierResolutionHost) + // moduleSpecifierCache?.setBlockedByPackageJsonDependencies(fromFile.path, toFile.path, preferences, {}, importInfo?.packageName, !importInfo?.importable) + return importInfo.importable || hasImportablePath && importInfo.packageName != "" && fileContainsPackageImport(fromFile, importInfo.packageName) + } + return false + } + + return hasImportablePath +} + +func fileContainsPackageImport(sourceFile *ast.SourceFile, packageName string) bool { + return core.Some(sourceFile.Imports(), func(i *ast.Node) bool { + text := i.Text() + return text == packageName || strings.HasPrefix(text, packageName+"/") + }) +} + +func isImportableSymbol(symbol *ast.Symbol, ch *checker.Checker) bool { + return !ch.IsUndefinedSymbol(symbol) && !ch.IsUnknownSymbol(symbol) && !checker.IsKnownSymbol(symbol) // !!! && !checker.IsPrivateIdentifierSymbol(symbol); +} + +func getDefaultLikeExportInfo(moduleSymbol *ast.Symbol, ch *checker.Checker) *ExportInfo { + exportEquals := ch.ResolveExternalModuleSymbol(moduleSymbol) + if exportEquals != moduleSymbol { + if defaultExport := ch.TryGetMemberInModuleExports(ast.InternalSymbolNameDefault, exportEquals); defaultExport != nil { + return &ExportInfo{defaultExport, ExportKindDefault} + } + return &ExportInfo{exportEquals, ExportKindExportEquals} + } + if defaultExport := ch.TryGetMemberInModuleExports(ast.InternalSymbolNameDefault, moduleSymbol); defaultExport != nil { + return &ExportInfo{defaultExport, ExportKindDefault} + } + return nil +} + +type importSpecifierResolverForCompletions struct { + *ast.SourceFile // importingFile + *UserPreferences + l *LanguageService + filter *packageJsonImportFilter +} + +func (r *importSpecifierResolverForCompletions) packageJsonImportFilter() *packageJsonImportFilter { + if r.filter == nil { + r.filter = r.l.createPackageJsonImportFilter(r.SourceFile, *r.UserPreferences) + } + return r.filter +} + +func (i *importSpecifierResolverForCompletions) getModuleSpecifierForBestExportInfo( + ch *checker.Checker, + exportInfo []*SymbolExportInfo, + position int, + isValidTypeOnlyUseSite bool, +) ImportFix { + // !!! caching + // used in completions, usually calculated once per `getCompletionData` call + var userPreferences UserPreferences + if i.UserPreferences == nil { + userPreferences = UserPreferences{} + } else { + userPreferences = *i.UserPreferences + } + packageJsonImportFilter := i.packageJsonImportFilter() + _, fixes := i.l.getImportFixes(ch, exportInfo, ptrTo(i.l.converters.PositionToLineAndCharacter(i.SourceFile, core.TextPos(position))), ptrTo(isValidTypeOnlyUseSite), ptrTo(false), i.SourceFile, userPreferences, false /* fromCacheOnly */) + return i.l.getBestFix(fixes, i.SourceFile, packageJsonImportFilter.allowsImportingSpecifier, userPreferences) +} + +func (l *LanguageService) getImportFixForSymbol( + ch *checker.Checker, + sourceFile *ast.SourceFile, + exportInfos []*SymbolExportInfo, + position int, + isValidTypeOnlySite *bool, + preferences *UserPreferences, +) ImportFix { + var userPreferences UserPreferences + if preferences != nil { + userPreferences = *preferences + } + + if isValidTypeOnlySite == nil { + isValidTypeOnlySite = ptrTo(ast.IsValidTypeOnlyAliasUseSite(astnav.GetTokenAtPosition(sourceFile, position))) + } + useRequire := getShouldUseRequire(sourceFile, l.GetProgram()) + packageJsonImportFilter := l.createPackageJsonImportFilter(sourceFile, userPreferences) + _, fixes := l.getImportFixes(ch, exportInfos, ptrTo(l.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(position))), isValidTypeOnlySite, &useRequire, sourceFile, userPreferences, false /* fromCacheOnly */) + return l.getBestFix(fixes, sourceFile, packageJsonImportFilter.allowsImportingSpecifier, userPreferences) +} + +func (l *LanguageService) getBestFix(fixes []ImportFix, sourceFile *ast.SourceFile, allowsImportingSpecifier func(moduleSpecifier string) bool, preferences UserPreferences) ImportFix { + if len(fixes) == 0 { + return nil + } + + // These will always be placed first if available, and are better than other kinds + if fixes[0].Kind() == ImportFixKindUseNamespace || fixes[0].Kind() == ImportFixKindAddToExisting { + return fixes[0] + } + + var best ImportFix + for _, fix := range fixes { + // Takes true branch of conditional if `fix` is better than `best` + if compareModuleSpecifiers( + fix, + best, + sourceFile, + l.GetProgram(), + preferences, + allowsImportingSpecifier, + func(fileName string) tspath.Path { + return tspath.ToPath(fileName, l.GetProgram().GetCurrentDirectory(), l.GetProgram().UseCaseSensitiveFileNames()) + }, + ) < 0 { + best = fix + } + } + + return best +} + +func (l *LanguageService) getImportFixes( + ch *checker.Checker, + exportInfos []*SymbolExportInfo, // | FutureSymbolExportInfo[], + usagePosition *lsproto.Position, + isValidTypeOnlyUseSite *bool, + useRequire *bool, + sourceFile *ast.SourceFile, // | FutureSourceFile, + preferences UserPreferences, + // importMap + fromCacheOnly bool, +) (int, []ImportFix) { + var importMap *importMap + if importMap == nil { // && isFullSourceFile(sourceFile) { + importMap = createExistingImportMap(sourceFile, l.GetProgram(), ch) + } + var existingImports []*FixAddToExistingImportInfo + if importMap != nil { + core.FlatMap(exportInfos, importMap.getImportsForExportInfo) + } + var useNamespace []ImportFix + if usagePosition != nil { + if namespaceImport := tryUseExistingNamespaceImport(existingImports, *usagePosition); namespaceImport != nil { + useNamespace = append(useNamespace, namespaceImport) + } + } + if addToExisting := tryAddToExistingImport(existingImports, isValidTypeOnlyUseSite, ch, l.GetProgram().Options()); addToExisting != nil { + // Don't bother providing an action to add a new import if we can add to an existing one. + return 0, append(useNamespace, addToExisting) + } + + result := l.getFixesForAddImport( + ch, + exportInfos, + existingImports, + sourceFile, + usagePosition, + *isValidTypeOnlyUseSite, + *useRequire, + preferences, + fromCacheOnly, + ) + computedWithoutCacheCount := 0 + // if result.computedWithoutCacheCount != nil { + // computedWithoutCacheCount = *result.computedWithoutCacheCount + // } + return computedWithoutCacheCount, append(useNamespace, result...) +} + +func (l *LanguageService) createPackageJsonImportFilter(fromFile *ast.SourceFile, preferences UserPreferences) *packageJsonImportFilter { + packageJsons := []*projectPackageJsonInfo{} + // packageJsons := ( + // (host.getPackageJsonsVisibleToFile && host.getPackageJsonsVisibleToFile(fromFile.fileName)) || getPackageJsonsVisibleToFile(fromFile.fileName, host) + // ).filter(p => p.parseable); + + var usesNodeCoreModules *bool + ambientModuleCache := map[*ast.Symbol]bool{} + sourceFileCache := map[*ast.SourceFile]packageJsonFilterResult{} + + getNodeModuleRootSpecifier := func(fullSpecifier string) string { + components := tspath.GetPathComponents(modulespecifiers.GetPackageNameFromTypesPackageName(fullSpecifier), "")[1:] + // Scoped packages + if strings.HasPrefix(components[0], "@") { + return fmt.Sprintf("%s/%s", components[0], components[1]) + } + return components[0] + } + + moduleSpecifierIsCoveredByPackageJson := func(specifier string) bool { + packageName := getNodeModuleRootSpecifier(specifier) + for _, packageJson := range packageJsons { + if packageJson.has(packageName) || packageJson.has(module.GetTypesPackageName(packageName)) { + return true + } + } + return false + } + + isAllowedCoreNodeModulesImport := func(moduleSpecifier string) bool { + // If we're in JavaScript, it can be difficult to tell whether the user wants to import + // from Node core modules or not. We can start by seeing if the user is actually using + // any node core modules, as opposed to simply having @types/node accidentally as a + // dependency of a dependency. + if /*isFullSourceFile(fromFile) &&*/ ast.IsSourceFileJS(fromFile) && core.NodeCoreModules()[moduleSpecifier] { + if usesNodeCoreModules == nil { + usesNodeCoreModules = ptrTo(consumesNodeCoreModules(fromFile)) + } + if *usesNodeCoreModules { + return true + } + } + return false + } + + getNodeModulesPackageNameFromFileName := func(importedFileName string, moduleSpecifierResolutionHost modulespecifiers.ModuleSpecifierGenerationHost) *string { + if !strings.Contains(importedFileName, "node_modules") { + return nil + } + specifier := modulespecifiers.GetNodeModulesPackageName( + l.host.GetProgram().Options(), + fromFile, + importedFileName, + moduleSpecifierResolutionHost, + preferences.ModuleSpecifierPreferences(), + modulespecifiers.ModuleSpecifierOptions{}, + ) + if specifier == "" { + return nil + } + // Paths here are not node_modules, so we don't care about them; + // returning anything will trigger a lookup in package.json. + if !tspath.PathIsRelative(specifier) && !tspath.IsRootedDiskPath(specifier) { + return ptrTo(getNodeModuleRootSpecifier(specifier)) + } + return nil + } + + allowsImportingAmbientModule := func(moduleSymbol *ast.Symbol, moduleSpecifierResolutionHost modulespecifiers.ModuleSpecifierGenerationHost) bool { + if len(packageJsons) > 0 || moduleSymbol.ValueDeclaration == nil { + return true + } + + if cached, ok := ambientModuleCache[moduleSymbol]; ok { + return cached + } + + declaredModuleSpecifier := stringutil.StripQuotes(moduleSymbol.Name) + if isAllowedCoreNodeModulesImport(declaredModuleSpecifier) { + ambientModuleCache[moduleSymbol] = true + return true + } + + declaringSourceFile := ast.GetSourceFileOfNode(moduleSymbol.ValueDeclaration) + declaringNodeModuleName := getNodeModulesPackageNameFromFileName(declaringSourceFile.FileName(), moduleSpecifierResolutionHost) + if declaringNodeModuleName == nil { + ambientModuleCache[moduleSymbol] = true + return true + } + + result := moduleSpecifierIsCoveredByPackageJson(*declaringNodeModuleName) + if !result { + result = moduleSpecifierIsCoveredByPackageJson(declaredModuleSpecifier) + } + ambientModuleCache[moduleSymbol] = result + return result + } + + getSourceFileInfo := func(sourceFile *ast.SourceFile, moduleSpecifierResolutionHost modulespecifiers.ModuleSpecifierGenerationHost) packageJsonFilterResult { + result := packageJsonFilterResult{ + importable: true, + packageName: "", + } + + if len(packageJsons) == 0 { + return result + } + if cached, ok := sourceFileCache[sourceFile]; ok { + return cached + } + + if packageName := getNodeModulesPackageNameFromFileName(sourceFile.FileName(), moduleSpecifierResolutionHost); packageName != nil { + result = packageJsonFilterResult{importable: moduleSpecifierIsCoveredByPackageJson(*packageName), packageName: *packageName} + } + sourceFileCache[sourceFile] = result + return result + } + + allowsImportingSpecifier := func(moduleSpecifier string) bool { + if len(packageJsons) == 0 || isAllowedCoreNodeModulesImport(moduleSpecifier) { + return true + } + if tspath.PathIsRelative(moduleSpecifier) || tspath.IsRootedDiskPath(moduleSpecifier) { + return true + } + return moduleSpecifierIsCoveredByPackageJson(moduleSpecifier) + } + + return &packageJsonImportFilter{ + allowsImportingAmbientModule, + getSourceFileInfo, + allowsImportingSpecifier, + } +} + +func consumesNodeCoreModules(sourceFile *ast.SourceFile) bool { + for _, importStatement := range sourceFile.Imports() { + if core.NodeCoreModules()[importStatement.Text()] { + return true + } + } + return false +} + +func createExistingImportMap(importingFile *ast.SourceFile, program *compiler.Program, ch *checker.Checker) *importMap { + m := collections.MultiMap[ast.SymbolId, *ast.Statement]{} + for _, moduleSpecifier := range importingFile.Imports() { + i := tryGetImportFromModuleSpecifier(moduleSpecifier) + if i == nil { + panic("error: did not expect node kind " + moduleSpecifier.Kind.String()) + } else if ast.IsVariableDeclarationInitializedToRequire(i.Parent) { + if moduleSymbol := ch.ResolveExternalModuleName(moduleSpecifier); moduleSymbol != nil { + m.Add(ast.GetSymbolId(moduleSymbol), i.Parent) + } + } else if i.Kind == ast.KindImportDeclaration || i.Kind == ast.KindImportEqualsDeclaration || i.Kind == ast.KindJSDocImportTag { + if moduleSymbol := ch.GetSymbolAtLocation(moduleSpecifier); moduleSymbol != nil { + m.Add(ast.GetSymbolId(moduleSymbol), i) + } + } + } + return &importMap{importingFile: importingFile, program: program, m: m} +} + +type importMap struct { + importingFile *ast.SourceFile + program *compiler.Program + m collections.MultiMap[ast.SymbolId, *ast.Statement] // !!! anyImportOrRequire +} + +func (i *importMap) getImportsForExportInfo(info *SymbolExportInfo /* | FutureSymbolExportInfo*/) []FixAddToExistingImportInfo { + matchingDeclarations := i.m.Get(ast.GetSymbolId(info.moduleSymbol)) + if len(matchingDeclarations) == 0 { + return []FixAddToExistingImportInfo{} + } + + // Can't use an es6 import for a type in JS. + if ast.IsSourceFileJS(i.importingFile) && info.targetFlags&ast.SymbolFlagsValue == 0 && !core.Every(matchingDeclarations, ast.IsJSDocImportTag) { + return []FixAddToExistingImportInfo{} + } + + importKind := getImportKind(i.importingFile, info.exportKind, i.program, false) + return core.Map(matchingDeclarations, func(d *ast.Statement) FixAddToExistingImportInfo { + return FixAddToExistingImportInfo{declaration: d, importKind: importKind, symbol: info.symbol, targetFlags: info.targetFlags} + }) +} + +func tryUseExistingNamespaceImport(existingImports []*FixAddToExistingImportInfo, position lsproto.Position) *FixUseNamespaceImport { + // It is possible that multiple import statements with the same specifier exist in the file. + // e.g. + // + // import * as ns from "foo"; + // import { member1, member2 } from "foo"; + // + // member3/**/ <-- cusor here + // + // in this case we should provie 2 actions: + // 1. change "member3" to "ns.member3" + // 2. add "member3" to the second import statement's import list + // and it is up to the user to decide which one fits best. + for _, existingImport := range existingImports { + if existingImport.importKind != ImportKindNamed { + continue + } + var namespacePrefix string + declaration := existingImport.declaration + switch declaration.Kind { + case ast.KindVariableDeclaration, ast.KindImportEqualsDeclaration: + if declaration.Kind == ast.KindVariableDeclaration && declaration.Name().Kind != ast.KindIdentifier { + continue + } + namespacePrefix = declaration.Name().Text() + case ast.KindJSDocImportTag, ast.KindImportDeclaration: + importClause := ast.GetImportClauseOfDeclaration(declaration) + if importClause == nil || importClause.NamedBindings.Kind != ast.KindNamespaceImport { + continue + } + namespacePrefix = importClause.NamedBindings.Name().Text() + default: + panic("never") + // Debug.assertNever(declaration); + } + if namespacePrefix == "" { + continue + } + moduleSpecifier := checker.TryGetModuleSpecifierFromDeclaration(declaration) + if moduleSpecifier != nil && moduleSpecifier.Text() != "" { + return getUseNamespaceImport( + moduleSpecifier.Text(), + modulespecifiers.ResultKindNone, + namespacePrefix, + position, + ) + } + } + return nil +} + +func tryAddToExistingImport(existingImports []*FixAddToExistingImportInfo, isValidTypeOnlyUseSite *bool, ch *checker.Checker, compilerOptions *core.CompilerOptions) *FixAddToExistingImport { + var best *FixAddToExistingImport + + typeOnly := false + if isValidTypeOnlyUseSite != nil { + typeOnly = *isValidTypeOnlyUseSite + } + + for _, existingImport := range existingImports { + fix := existingImport.getAddToExistingImportFix(typeOnly, ch, compilerOptions) + if fix == nil { + continue + } + isTypeOnly := ast.IsTypeOnlyImportDeclaration(fix.importClauseOrBindingPattern) + if (fix.addAsTypeOnly != AddAsTypeOnlyNotAllowed && isTypeOnly) || (fix.addAsTypeOnly == AddAsTypeOnlyNotAllowed && !isTypeOnly) { + // Give preference to putting types in existing type-only imports and avoiding conversions + // of import statements to/from type-only. + return fix + } + if best == nil { + best = fix + } + } + return best +} + +func (info *FixAddToExistingImportInfo) getAddToExistingImportFix(isValidTypeOnlyUseSite bool, ch *checker.Checker, compilerOptions *core.CompilerOptions) *FixAddToExistingImport { + if info.importKind == ImportKindCommonJS || info.importKind == ImportKindNamespace || info.declaration.Kind == ast.KindImportEqualsDeclaration { + // These kinds of imports are not combinable with anything + return nil + } + + if info.declaration.Kind == ast.KindVariableDeclaration { + if (info.importKind == ImportKindNamed || info.importKind == ImportKindDefault) && info.declaration.Name().Kind == ast.KindObjectBindingPattern { + return getAddToExistingImport( + info.declaration.Name(), + info.importKind, + info.declaration.Initializer().Arguments()[0].Text(), + modulespecifiers.ResultKindNone, + AddAsTypeOnlyNotAllowed, + ) + } + return nil + } + + importClause := ast.GetImportClauseOfDeclaration(info.declaration) + if importClause == nil || !ast.IsStringLiteralLike(info.declaration.ModuleSpecifier()) { + return nil + } + 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 && !(info.importKind == ImportKindNamed && namedBindings != nil) { + return nil + } + + // N.B. we don't have to figure out whether to use the main program checker + // or the AutoImportProvider checker because we're adding to an existing import; the existence of + // the import guarantees the symbol came from the main program. + addAsTypeOnly := getAddAsTypeOnly(isValidTypeOnlyUseSite, info.symbol, info.targetFlags, ch, compilerOptions) + + if info.importKind == ImportKindDefault && (importClause.Name() != nil || // Cannot add a default import to a declaration that already has one + addAsTypeOnly == AddAsTypeOnlyRequired && namedBindings != nil) { // Cannot add a default import as type-only if the import already has named bindings + + return nil + } + + // Cannot add a named import to a declaration that has a namespace import + if info.importKind == ImportKindNamed && namedBindings != nil && namedBindings.Kind == ast.KindNamespaceImport { + return nil + } + + return getAddToExistingImport( + importClause.AsNode(), + info.importKind, + info.declaration.ModuleSpecifier().Text(), + modulespecifiers.ResultKindNone, + addAsTypeOnly, + ) +} + +func (l *LanguageService) getFixesForAddImport( + ch *checker.Checker, + exportInfos []*SymbolExportInfo, // !!! | readonly FutureSymbolExportInfo[], + existingImports []*FixAddToExistingImportInfo, + sourceFile *ast.SourceFile, // !!! | FutureSourceFile, + usagePosition *lsproto.Position, + isValidTypeOnlyUseSite bool, + useRequire bool, + preferences UserPreferences, + fromCacheOnly bool, +) []ImportFix { + // tries to create a new import statement using an existing import specifier + var importWithExistingSpecifier *FixAddNewImport + + for _, existingImport := range existingImports { + if fix := existingImport.getNewImportFromExistingSpecifier(isValidTypeOnlyUseSite, useRequire, ch, l.GetProgram().Options()); fix != nil { + importWithExistingSpecifier = fix + break + } + } + + if importWithExistingSpecifier != nil { + return []ImportFix{importWithExistingSpecifier} + } + + return l.getNewImportFixes(ch, sourceFile, usagePosition, isValidTypeOnlyUseSite, useRequire, exportInfos, preferences, fromCacheOnly) +} + +func (l *LanguageService) getNewImportFixes( + ch *checker.Checker, + sourceFile *ast.SourceFile, // | FutureSourceFile, + usagePosition *lsproto.Position, + isValidTypeOnlyUseSite bool, + useRequire bool, + exportInfos []*SymbolExportInfo, // !!! (SymbolExportInfo | FutureSymbolExportInfo)[], + preferences UserPreferences, + fromCacheOnly bool, +) []ImportFix /* FixAddNewImport | FixAddJsdocTypeImport */ { + isJs := tspath.HasJSFileExtension(sourceFile.FileName()) + compilerOptions := l.GetProgram().Options() + // !!! packagejsonAutoimportProvider + // getChecker := createGetChecker(program, host)// memoized typechecker based on `isFromPackageJson` bool + + getModuleSpecifiers := func(moduleSymbol *ast.Symbol, checker *checker.Checker) ([]string, modulespecifiers.ResultKind) { + return modulespecifiers.GetModuleSpecifiersWithInfo(moduleSymbol, checker, compilerOptions, sourceFile, l.GetProgram(), preferences.ModuleSpecifierPreferences(), modulespecifiers.ModuleSpecifierOptions{}, true /*forAutoImport*/) + } + // fromCacheOnly + // ? (exportInfo: SymbolExportInfo | FutureSymbolExportInfo) => moduleSpecifiers.tryGetModuleSpecifiersFromCache(exportInfo.moduleSymbol, sourceFile, moduleSpecifierResolutionHost, preferences) + // : (exportInfo: SymbolExportInfo | FutureSymbolExportInfo, checker: TypeChecker) => moduleSpecifiers.getModuleSpecifiersWithCacheInfo(exportInfo.moduleSymbol, checker, compilerOptions, sourceFile, moduleSpecifierResolutionHost, preferences, /*options*/ nil, /*forAutoImport*/ true); + + // computedWithoutCacheCount = 0; + var fixes []ImportFix /* FixAddNewImport | FixAddJsdocTypeImport */ + for i, exportInfo := range exportInfos { + moduleSpecifiers, moduleSpecifierKind := getModuleSpecifiers(exportInfo.moduleSymbol, ch) + importedSymbolHasValueMeaning := exportInfo.targetFlags&ast.SymbolFlagsValue != 0 + addAsTypeOnly := getAddAsTypeOnly(isValidTypeOnlyUseSite, exportInfo.symbol, exportInfo.targetFlags, ch, compilerOptions) + // computedWithoutCacheCount += computedWithoutCache ? 1 : 0; + for _, moduleSpecifier := range moduleSpecifiers { + if modulespecifiers.ContainsNodeModules(moduleSpecifier) { + continue + } + if !importedSymbolHasValueMeaning && isJs && usagePosition != nil { + // `position` should only be undefined at a missing jsx namespace, in which case we shouldn't be looking for pure types. + fixes = append(fixes, getAddJsdocTypeImport( + moduleSpecifier, + moduleSpecifierKind, + usagePosition, + exportInfo, + ptrTo(i > 0)), // isReExport + ) + continue + } + importKind := getImportKind(sourceFile, exportInfo.exportKind, l.GetProgram(), false) + var qualification *Qualification + if usagePosition != nil && importKind == ImportKindCommonJS && exportInfo.exportKind == ExportKindNamed { + // Compiler options are restricting our import options to a require, but we need to access + // a named export or property of the exporting module. We need to import the entire module + // and insert a property access, e.g. `writeFile` becomes + // + // import fs = require("fs"); // or const in JS + // fs.writeFile + exportEquals := ch.ResolveExternalModuleSymbol(exportInfo.moduleSymbol) + var namespacePrefix *string + if exportEquals != exportInfo.moduleSymbol { + namespacePrefix = strPtrTo(forEachNameOfDefaultExport( + exportEquals, + ch, + compilerOptions.GetEmitScriptTarget(), + func(a, _ string) string { return a }, // Identity + )) + } + if namespacePrefix == nil { + namespacePrefix = ptrTo(moduleSymbolToValidIdentifier( + exportInfo.moduleSymbol, + compilerOptions.GetEmitScriptTarget(), + /*forceCapitalize*/ false, + )) + } + qualification = &Qualification{*usagePosition, *namespacePrefix} + } + fixes = append(fixes, getNewAddNewImport( + moduleSpecifier, + moduleSpecifierKind, + importKind, + useRequire, + addAsTypeOnly, + exportInfo, + ptrTo(i > 0), // isReExport + qualification, + )) + } + } + + return fixes +} + +func getAddAsTypeOnly( + isValidTypeOnlyUseSite bool, + symbol *ast.Symbol, + targetFlags ast.SymbolFlags, + ch *checker.Checker, + compilerOptions *core.CompilerOptions, +) AddAsTypeOnly { + if !isValidTypeOnlyUseSite { + // Can't use a type-only import if the usage is an emitting position + return AddAsTypeOnlyNotAllowed + } + if symbol != nil && compilerOptions.VerbatimModuleSyntax.IsTrue() && + (targetFlags&ast.SymbolFlagsValue == 0 || ch.GetTypeOnlyAliasDeclaration(symbol) != nil) { + // A type-only import is required for this symbol if under these settings if the symbol will + // be erased, which will happen if the target symbol is purely a type or if it was exported/imported + // as type-only already somewhere between this import and the target. + return AddAsTypeOnlyRequired + } + return AddAsTypeOnlyAllowed +} + +func getShouldUseRequire( + sourceFile *ast.SourceFile, // !!! | FutureSourceFile + program *compiler.Program, +) bool { + // 1. TypeScript files don't use require variable declarations + if !tspath.HasJSFileExtension(sourceFile.FileName()) { + return false + } + + // 2. If the current source file is unambiguously CJS or ESM, go with that + switch { + case sourceFile.CommonJSModuleIndicator != nil && sourceFile.ExternalModuleIndicator == nil: + return true + case sourceFile.ExternalModuleIndicator != nil && sourceFile.CommonJSModuleIndicator == nil: + return false + } + + // 3. If there's a tsconfig/jsconfig, use its module setting + if program.Options().ConfigFilePath != "" { + return program.Options().GetEmitModuleKind() < core.ModuleKindES2015 + } + + // 4. In --module nodenext, assume we're not emitting JS -> JS, so use + // whatever syntax Node expects based on the detected module kind + // TODO: consider removing `impliedNodeFormatForEmit` + switch program.GetImpliedNodeFormatForEmit(sourceFile) { + case core.ModuleKindCommonJS: + return true + case core.ModuleKindESNext: + return false + } + + // 5. Match the first other JS file in the program that's unambiguously CJS or ESM + for _, otherFile := range program.GetSourceFiles() { + switch { + case otherFile == sourceFile, !ast.IsSourceFileJS(otherFile), program.IsSourceFileFromExternalLibrary(otherFile): + continue + case otherFile.CommonJSModuleIndicator != nil && otherFile.ExternalModuleIndicator == nil: + return true + case otherFile.ExternalModuleIndicator != nil && otherFile.CommonJSModuleIndicator == nil: + return false + } + } + + // 6. Literally nothing to go on + return true +} + +/** + * @param forceImportKeyword Indicates that the user has already typed `import`, so the result must start with `import`. + * (In other words, do not allow `const x = require("...")` for JS files.) + * + * @internal + */ +func getImportKind(importingFile *ast.SourceFile /*| FutureSourceFile*/, exportKind ExportKind, program *compiler.Program, forceImportKeyword bool) ImportKind { + if program.Options().VerbatimModuleSyntax.IsTrue() && program.GetEmitModuleFormatOfFile(importingFile) == core.ModuleKindCommonJS { + // TODO: if the exporting file is ESM under nodenext, or `forceImport` is given in a JS file, this is impossible + return ImportKindCommonJS + } + switch exportKind { + case ExportKindNamed: + return ImportKindNamed + case ExportKindDefault: + return ImportKindDefault + case ExportKindExportEquals: + return getExportEqualsImportKind(importingFile, program.Options(), forceImportKeyword) + case ExportKindUMD: + return getUmdImportKind(importingFile, program, forceImportKeyword) + case ExportKindModule: + return ImportKindNamespace + } + panic("unexpected export kind: " + exportKind.String()) +} + +func getExportEqualsImportKind(importingFile *ast.SourceFile /* | FutureSourceFile*/, compilerOptions *core.CompilerOptions, forceImportKeyword bool) ImportKind { + allowSyntheticDefaults := compilerOptions.GetAllowSyntheticDefaultImports() + isJS := tspath.HasJSFileExtension(importingFile.FileName()) + // 1. 'import =' will not work in es2015+ TS files, so the decision is between a default + // and a namespace import, based on allowSyntheticDefaultImports/esModuleInterop. + if !isJS && compilerOptions.GetEmitModuleKind() >= core.ModuleKindES2015 { + if allowSyntheticDefaults { + return ImportKindDefault + } + return ImportKindNamespace + } + // 2. 'import =' will not work in JavaScript, so the decision is between a default import, + // a namespace import, and const/require. + if isJS { + if importingFile.ExternalModuleIndicator != nil || forceImportKeyword { + if allowSyntheticDefaults { + return ImportKindDefault + } + return ImportKindNamespace + } + return ImportKindCommonJS + } + // 3. At this point the most correct choice is probably 'import =', but people + // really hate that, so look to see if the importing file has any precedent + // on how to handle it. + 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 ImportKindCommonJS + } + } + // 4. We have no precedent to go on, so just use a default import if + // allowSyntheticDefaultImports/esModuleInterop is enabled. + if allowSyntheticDefaults { + return ImportKindDefault + } + return ImportKindCommonJS +} + +func getUmdImportKind(importingFile *ast.SourceFile /* | FutureSourceFile */, program *compiler.Program, forceImportKeyword bool) ImportKind { + // Import a synthetic `default` if enabled. + if program.Options().GetAllowSyntheticDefaultImports() { + return ImportKindDefault + } + + // When a synthetic `default` is unavailable, use `import..require` if the module kind supports it. + moduleKind := program.Options().GetEmitModuleKind() + switch moduleKind { + case core.ModuleKindAMD, core.ModuleKindCommonJS, core.ModuleKindUMD: + if tspath.HasJSFileExtension(importingFile.FileName()) && (importingFile.ExternalModuleIndicator != nil || forceImportKeyword) { + return ImportKindNamespace + } + return ImportKindCommonJS + case core.ModuleKindSystem, core.ModuleKindES2015, core.ModuleKindES2020, core.ModuleKindES2022, core.ModuleKindESNext, core.ModuleKindNone, core.ModuleKindPreserve: + // Fall back to the `import * as ns` style import. + return ImportKindNamespace + case core.ModuleKindNode16, core.ModuleKindNode18, core.ModuleKindNodeNext: + if program.GetImpliedNodeFormatForEmit(importingFile) == core.ModuleKindESNext { + return ImportKindNamespace + } + return ImportKindCommonJS + default: + panic(`Unexpected moduleKind :` + moduleKind.String()) + } +} + +/** + * May call `cb` multiple times with the same name. + * Terminates when `cb` returns a truthy value. + */ +func forEachNameOfDefaultExport(defaultExport *ast.Symbol, ch *checker.Checker, scriptTarget core.ScriptTarget, cb func(name string, capitalizedName string) string) string { + var chain []*ast.Symbol + current := defaultExport + seen := collections.Set[*ast.Symbol]{} + + for current != nil { + // The predecessor to this function also looked for a name on the `localSymbol` + // of default exports, but I think `getDefaultLikeExportNameFromDeclaration` + // accomplishes the same thing via syntax - no tests failed when I removed it. + fromDeclaration := getDefaultLikeExportNameFromDeclaration(current) + if fromDeclaration != "" { + final := cb(fromDeclaration, "") + if final != "" { + return final + } + } + + if current.Name != ast.InternalSymbolNameDefault && current.Name != ast.InternalSymbolNameExportEquals { + if final := cb(current.Name, ""); final != "" { + return final + } + } + + chain = append(chain, current) + if !seen.AddIfAbsent(current) { + break + } + if current.Flags&ast.SymbolFlagsAlias != 0 { + current = ch.GetImmediateAliasedSymbol(current) + } else { + current = nil + } + } + + for _, symbol := range chain { + if symbol.Parent != nil && checker.IsExternalModuleSymbol(symbol.Parent) { + final := cb( + moduleSymbolToValidIdentifier(symbol.Parent, scriptTarget /*forceCapitalize*/, false), + moduleSymbolToValidIdentifier(symbol.Parent, scriptTarget /*forceCapitalize*/, true), + ) + if final != "" { + return final + } + } + } + return "" +} + +func getDefaultLikeExportNameFromDeclaration(symbol *ast.Symbol) string { + for _, d := range symbol.Declarations { + // "export default" in this case. See `ExportAssignment`for more details. + if ast.IsExportAssignment(d) { + if innerExpression := ast.SkipOuterExpressions(d.Expression(), ast.OEKAll); ast.IsIdentifier(innerExpression) { + return innerExpression.Text() + } + continue + } + // "export { ~ as default }" + if ast.IsExportSpecifier(d) && d.Symbol().Flags == ast.SymbolFlagsAlias && d.PropertyName() != nil { + if d.PropertyName().Kind == ast.KindIdentifier { + return d.PropertyName().Text() + } + continue + } + // GH#52694 + if name := ast.GetNameOfDeclaration(d); name != nil && name.Kind == ast.KindIdentifier { + return name.Text() + } + if symbol.Parent != nil && !checker.IsExternalModuleSymbol(symbol.Parent) { + return symbol.Parent.Name + } + } + return "" +} + +func (l *LanguageService) forEachExternalModuleToImportFrom( + ch *checker.Checker, + preferences *UserPreferences, + // useAutoImportProvider bool, + cb func(module *ast.Symbol, moduleFile *ast.SourceFile, checker *checker.Checker, isFromPackageJson bool), +) { + // useCaseSensitiveFileNames := hostUsesCaseSensitiveFileNames(host) + // !!! excludePatterns + // excludePatterns := preferences.autoImportFileExcludePatterns && getIsExcludedPatterns(preferences, useCaseSensitiveFileNames) + + forEachExternalModule( + ch, + l.GetProgram().GetSourceFiles(), + // !!! excludePatterns, + func(module *ast.Symbol, file *ast.SourceFile) { + cb(module, file, ch, false) + }, + ) + + // !!! autoImportProvider + // if autoImportProvider := useAutoImportProvider && l.getPackageJsonAutoImportProvider(); autoImportProvider != nil { + // // start := timestamp(); + // forEachExternalModule(autoImportProvider.getTypeChecker(), autoImportProvider.getSourceFiles(), excludePatterns, host, func (module *ast.Symbol, file *ast.SourceFile) { + // if (file && !program.getSourceFile(file.FileName()) || !file && !checker.resolveName(module.Name, /*location*/ nil, ast.SymbolFlagsModule, /*excludeGlobals*/ false)) { + // // The AutoImportProvider filters files already in the main program out of its *root* files, + // // but non-root files can still be present in both programs, and already in the export info map + // // at this point. This doesn't create any incorrect behavior, but is a waste of time and memory, + // // so we filter them out here. + // cb(module, file, autoImportProvide.checker, /*isFromPackageJson*/ true); + // } + // }); + // // host.log?.(`forEachExternalModuleToImportFrom autoImportProvider: ${timestamp() - start}`); + // } +} + +func forEachExternalModule( + ch *checker.Checker, + allSourceFiles []*ast.SourceFile, + // excludePatterns []RegExp, + cb func(moduleSymbol *ast.Symbol, sourceFile *ast.SourceFile), +) { + // !!! excludePatterns + // isExcluded := excludePatterns && getIsExcluded(excludePatterns, host) + + for _, ambient := range ch.GetAmbientModules() { + if !strings.Contains(ambient.Name, "*") /* && !(excludePatterns && ambient.Declarations.every(func (d){ return isExcluded(d.getSourceFile())})) */ { + cb(ambient /*sourceFile*/, nil) + } + } + for _, sourceFile := range allSourceFiles { + if ast.IsExternalOrCommonJSModule(sourceFile) /* && !isExcluded(sourceFile) */ { + cb(ch.GetMergedSymbol(sourceFile.Symbol), sourceFile) + } + } +} + +// ======================== generate code actions ======================= +// !!! change tracker +// these functions use changetracker/should be updated to use the new lsproto.CodeAction + +func newCodeFixAction(description string, changes []*lsproto.TextEdit) codeAction { + return codeAction{description: description, changes: changes} +} + +func (l *LanguageService) codeActionForFix( + ctx context.Context, + sourceFile *ast.SourceFile, + symbolName string, + fix ImportFix, + includeSymbolNameInDescription bool, + preferences *UserPreferences, +) codeAction { + tracker := l.newChangeTracker(ctx) // !!! changetracker.with + diag := l.codeActionForFixWorker(tracker, sourceFile, symbolName, fix, includeSymbolNameInDescription, preferences) + changes := tracker.getChanges()[sourceFile.FileName()] + // !!! codeaction + return newCodeFixAction(diag.Message(), changes) +} + +func (l *LanguageService) codeActionForFixWorker( + changeTracker *changeTracker, + sourceFile *ast.SourceFile, + symbolName string, + fix ImportFix, + includeSymbolNameInDescription bool, + preferences *UserPreferences, +) *diagnostics.Message { + switch fix.Kind() { + case ImportFixKindUseNamespace: + namespaceFix := fix.(*FixUseNamespaceImport) + changeTracker.addNamespaceQualifier(sourceFile, namespaceFix.Qualification) + return diagnostics.FormatMessage(diagnostics.Change_0_to_1, symbolName, `${fix.namespacePrefix}.${symbolName}`) + case ImportFixKindJsdocTypeImport: + // !!! not implemented + // changeTracker.addImportType(changeTracker, sourceFile, fix, quotePreference); + // return diagnostics.FormatMessage(diagnostics.Change_0_to_1, symbolName, getImportTypePrefix(fix.Base().moduleSpecifier, quotePreference) + symbolName); + case ImportFixKindAddToExisting: + { + fixAddToExisting := fix.(*FixAddToExistingImport) + changeTracker.doAddExistingFix( + sourceFile, + fixAddToExisting.importClauseOrBindingPattern, + core.IfElse(fixAddToExisting.importKind == ImportKindDefault, &Import{name: symbolName, addAsTypeOnly: fixAddToExisting.addAsTypeOnly}, nil), + core.IfElse(fixAddToExisting.importKind == ImportKindNamed, []*Import{{name: symbolName, addAsTypeOnly: fixAddToExisting.addAsTypeOnly}}, nil), + // nil /*removeExistingImportSpecifiers*/, + preferences, + ) + moduleSpecifierWithoutQuotes := stringutil.StripQuotes(fix.Base().moduleSpecifier) + if includeSymbolNameInDescription { + return diagnostics.FormatMessage(diagnostics.Import_0_from_1, symbolName, moduleSpecifierWithoutQuotes) + } + return diagnostics.FormatMessage(diagnostics.Update_import_from_0, moduleSpecifierWithoutQuotes) + } + case ImportFixKindAddNew: + fixAddNew := fix.(*FixAddNewImport) + var declarations []*ast.Statement + defaultImport := core.IfElse(fixAddNew.importKind == ImportKindDefault, &Import{name: symbolName, addAsTypeOnly: fixAddNew.addAsTypeOnly}, nil) + namedImports := core.IfElse(fixAddNew.importKind == ImportKindNamed, []*Import{{name: symbolName, addAsTypeOnly: fixAddNew.addAsTypeOnly}}, nil) + var namespaceLikeImport *Import + if fixAddNew.importKind == ImportKindNamespace || fixAddNew.importKind == ImportKindCommonJS { + namespaceLikeImport = &Import{kind: fixAddNew.importKind, addAsTypeOnly: fixAddNew.addAsTypeOnly, name: symbolName} + if fixAddNew.Qualification != nil && fixAddNew.Qualification.namespacePrefix != "" { + namespaceLikeImport.name = fixAddNew.Qualification.namespacePrefix + } + } + + if fixAddNew.useRequire { + // !!! require + // declarations = getNewRequires(fixAddNew.moduleSpecifier, quotePreference, defaultImport, namedImports, namespaceLikeImport, l.GetProgram().Options(), preferences) + } else { + declarations = changeTracker.getNewImports(fixAddNew.moduleSpecifier, defaultImport, namedImports, namespaceLikeImport, l.GetProgram().Options(), preferences) + } + + changeTracker.insertImports( + sourceFile, + declarations, + /*blankLineBetween*/ true, + preferences, + ) + if fixAddNew.Qualification != nil { + changeTracker.addNamespaceQualifier(sourceFile, *fixAddNew.Qualification) + } + if includeSymbolNameInDescription { + return diagnostics.FormatMessage(diagnostics.Import_0_from_1, symbolName, fix.Base().moduleSpecifier) + } + return diagnostics.FormatMessage(diagnostics.Add_import_from_0, fix.Base().moduleSpecifier) + case ImportFixKindPromoteTypeOnly: + // promotedDeclaration := promoteFromTypeOnly(changes, fix.typeOnlyAliasDeclaration, program, sourceFile, preferences); + // if promotedDeclaration.Kind == ast.KindImportSpecifier { + // return diagnostics.FormatMessage(diagnostics.Remove_type_from_import_of_0_from_1, symbolName, getModuleSpecifierText(promotedDeclaration.parent.parent)) + // } + // return diagnostics.FormatMessage(diagnostics.Remove_type_from_import_declaration_from_0, getModuleSpecifierText(promotedDeclaration)); + default: + panic(fmt.Sprintf(`Unexpected fix kind %v`, fix.Kind())) + } + return nil +} + +func getModuleSpecifierText(promotedDeclaration *ast.ImportDeclaration) string { + if promotedDeclaration.Kind == ast.KindImportEqualsDeclaration { + importEqualsDeclaration := promotedDeclaration.AsImportEqualsDeclaration() + if ast.IsExternalModuleReference(importEqualsDeclaration.ModuleReference) { + expr := importEqualsDeclaration.ModuleReference.Expression() + if expr != nil && expr.Kind == ast.KindStringLiteral { + return expr.Text() + } + + } + return importEqualsDeclaration.ModuleReference.Text() + } + return promotedDeclaration.Parent.ModuleSpecifier().Text() +} diff --git a/internal/ls/autoimportstypes.go b/internal/ls/autoimportstypes.go new file mode 100644 index 0000000000..cf826b609b --- /dev/null +++ b/internal/ls/autoimportstypes.go @@ -0,0 +1,272 @@ +package ls + +import ( + "fmt" + + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/checker" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/modulespecifiers" +) + +type ImportKind int + +const ( + ImportKindNamed ImportKind = 0 + ImportKindDefault ImportKind = 1 + ImportKindNamespace ImportKind = 2 + ImportKindCommonJS ImportKind = 3 +) + +type ExportKind int + +const ( + ExportKindNamed ExportKind = 0 + ExportKindDefault ExportKind = 1 + ExportKindExportEquals ExportKind = 2 + ExportKindUMD ExportKind = 3 + ExportKindModule ExportKind = 4 +) + +func (k ExportKind) String() string { + switch k { + case ExportKindNamed: + return "Named" + case ExportKindDefault: + return "Default" + case ExportKindExportEquals: + return "ExportEquals" + case ExportKindUMD: + return "UMD" + case ExportKindModule: + return "Module" + } + panic(fmt.Sprintf("unexpected export kind: %d", k)) +} + +type ImportFix interface { + Kind() ImportFixKind + Base() *ImportFixBase +} + +type ImportFixKind int + +const ( + // Sorted with the preferred fix coming first. + ImportFixKindUseNamespace ImportFixKind = 0 + ImportFixKindJsdocTypeImport ImportFixKind = 1 + ImportFixKindAddToExisting ImportFixKind = 2 + ImportFixKindAddNew ImportFixKind = 3 + ImportFixKindPromoteTypeOnly ImportFixKind = 4 +) + +type AddAsTypeOnly int + +const ( + // These should not be combined as bitflags, but are given powers of 2 values to + // easily detect conflicts between `NotAllowed` and `Required` by giving them a unique sum. + // They're also ordered in terms of increasing priority for a fix-all scenario (see + // `reduceAddAsTypeOnlyValues`). + AddAsTypeOnlyAllowed AddAsTypeOnly = 1 << 0 + AddAsTypeOnlyRequired AddAsTypeOnly = 1 << 1 + AddAsTypeOnlyNotAllowed AddAsTypeOnly = 1 << 2 +) + +type ImportFixBase struct { + isReExport *bool + exportInfo *SymbolExportInfo // !!! | FutureSymbolExportInfo | undefined + moduleSpecifierKind modulespecifiers.ResultKind + moduleSpecifier string +} + +type Qualification struct { + usagePosition lsproto.Position + namespacePrefix string +} + +type FixUseNamespaceImport struct { + ImportFixBase + Qualification +} + +func (f *FixUseNamespaceImport) Kind() ImportFixKind { + return ImportFixKindUseNamespace +} + +func (f *FixUseNamespaceImport) Base() *ImportFixBase { + return &f.ImportFixBase +} + +func getUseNamespaceImport( + moduleSpecifier string, + moduleSpecifierKind modulespecifiers.ResultKind, + namespacePrefix string, + usagePosition lsproto.Position, +) *FixUseNamespaceImport { + return &FixUseNamespaceImport{ + ImportFixBase: ImportFixBase{ + moduleSpecifierKind: moduleSpecifierKind, + moduleSpecifier: moduleSpecifier, + }, + Qualification: Qualification{ + usagePosition: usagePosition, + namespacePrefix: namespacePrefix, + }, + } +} + +type FixAddJsdocTypeImport struct { + ImportFixBase + usagePosition *lsproto.Position +} + +func (f *FixAddJsdocTypeImport) Kind() ImportFixKind { + return ImportFixKindJsdocTypeImport +} + +func (f *FixAddJsdocTypeImport) Base() *ImportFixBase { + return &f.ImportFixBase +} + +func getAddJsdocTypeImport( + moduleSpecifier string, + moduleSpecifierKind modulespecifiers.ResultKind, + usagePosition *lsproto.Position, + exportInfo *SymbolExportInfo, + isReExport *bool, +) *FixAddJsdocTypeImport { + return &FixAddJsdocTypeImport{ + ImportFixBase: ImportFixBase{ + isReExport: isReExport, + exportInfo: exportInfo, + moduleSpecifierKind: moduleSpecifierKind, + moduleSpecifier: moduleSpecifier, + }, + usagePosition: usagePosition, + } +} + +type FixAddToExistingImport struct { + ImportFixBase + importClauseOrBindingPattern *ast.Node // ImportClause | ObjectBindingPattern + importKind ImportKind // ImportKindDefault | ImportKindNamed + addAsTypeOnly AddAsTypeOnly + propertyName string +} + +func (f *FixAddToExistingImport) Kind() ImportFixKind { + return ImportFixKindAddToExisting +} + +func (f *FixAddToExistingImport) Base() *ImportFixBase { + return &f.ImportFixBase +} + +func getAddToExistingImport( + importClauseOrBindingPattern *ast.Node, + importKind ImportKind, + moduleSpecifier string, + moduleSpecifierKind modulespecifiers.ResultKind, + addAsTypeOnly AddAsTypeOnly, +) *FixAddToExistingImport { + return &FixAddToExistingImport{ + ImportFixBase: ImportFixBase{ + moduleSpecifierKind: moduleSpecifierKind, + moduleSpecifier: moduleSpecifier, + }, + importClauseOrBindingPattern: importClauseOrBindingPattern, + importKind: importKind, + addAsTypeOnly: addAsTypeOnly, + } +} + +type FixAddNewImport struct { + ImportFixBase + *Qualification + importKind ImportKind + addAsTypeOnly AddAsTypeOnly + propertyName string + useRequire bool +} + +func (f *FixAddNewImport) Kind() ImportFixKind { + return ImportFixKindAddNew +} + +func (f *FixAddNewImport) Base() *ImportFixBase { + return &f.ImportFixBase +} + +func getNewAddNewImport( + moduleSpecifier string, + moduleSpecifierKind modulespecifiers.ResultKind, + importKind ImportKind, + useRequire bool, + addAsTypeOnly AddAsTypeOnly, + exportInfo *SymbolExportInfo, // !!! | FutureSymbolExportInfo + isReExport *bool, + qualification *Qualification, +) *FixAddNewImport { + return &FixAddNewImport{ + ImportFixBase: ImportFixBase{ + isReExport: isReExport, + exportInfo: exportInfo, + moduleSpecifierKind: modulespecifiers.ResultKindNone, + moduleSpecifier: moduleSpecifier, + }, + // Qualification: qualification, + importKind: importKind, + addAsTypeOnly: addAsTypeOnly, + useRequire: useRequire, + } +} + +type FixPromoteTypeOnlyImport struct { + ImportFixBase + typeOnlyAliasDeclaration *ast.Declaration // TypeOnlyAliasDeclaration +} + +func (f *FixPromoteTypeOnlyImport) Kind() ImportFixKind { + return ImportFixKindPromoteTypeOnly +} + +func (f *FixPromoteTypeOnlyImport) Base() *ImportFixBase { + return &f.ImportFixBase +} + +/** Information needed to augment an existing import declaration. */ +// rename all fixes to say fix at end +// rename to AddToExistingImportInfo +type FixAddToExistingImportInfo struct { + declaration *ast.Declaration + importKind ImportKind + targetFlags ast.SymbolFlags + symbol *ast.Symbol +} + +func (info *FixAddToExistingImportInfo) getNewImportFromExistingSpecifier( + isValidTypeOnlyUseSite bool, + useRequire bool, + ch *checker.Checker, + compilerOptions *core.CompilerOptions, +) *FixAddNewImport { + moduleSpecifier := checker.TryGetModuleSpecifierFromDeclaration(info.declaration) + if moduleSpecifier == nil || moduleSpecifier.Text() == "" { + return nil + } + addAsTypeOnly := AddAsTypeOnlyNotAllowed + if !useRequire { + addAsTypeOnly = getAddAsTypeOnly(isValidTypeOnlyUseSite, info.symbol, info.targetFlags, ch, compilerOptions) + } + return getNewAddNewImport( + moduleSpecifier.Text(), + modulespecifiers.ResultKindNone, + info.importKind, + useRequire, + addAsTypeOnly, + nil, // exportInfo + nil, // isReExport + nil, // qualification + ) +} diff --git a/internal/ls/changetracker.go b/internal/ls/changetracker.go new file mode 100644 index 0000000000..270837fcdb --- /dev/null +++ b/internal/ls/changetracker.go @@ -0,0 +1,324 @@ +package ls + +import ( + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/astnav" + "github.com/microsoft/typescript-go/internal/core" +) + +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 (ct *changeTracker) addNamespaceQualifier(sourceFile *ast.SourceFile, qualification Qualification) { + ct.insertText(sourceFile, qualification.usagePosition, qualification.namespacePrefix+".") +} + +func (ct *changeTracker) doAddExistingFix( + sourceFile *ast.SourceFile, + clause *ast.Node, // ImportClause | ObjectBindingPattern, + defaultImport *Import, + namedImports []*Import, + // removeExistingImportSpecifiers *core.Set[ImportSpecifier | BindingElement] // !!! remove imports not implemented + preferences *UserPreferences, +) { + 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 { + ct.addElementToBindingPattern(sourceFile, clause, defaultImport.name, ptrTo("default")) + } + for _, specifier := range namedImports { + ct.addElementToBindingPattern(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, "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), insertNodeOptions{suffix: ", "}) + } + + if len(namedImports) > 0 { + // !!! OrganizeImports not yet implemented + // specifierComparer, isSorted := OrganizeImports.getNamedImportSpecifierComparerWithDetection(importClause.Parent, preferences, sourceFile); + 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, preferences), + identifier, + ct.NodeFactory.NewIdentifier(namedImport.name), + ) + }) // !!! sort with 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 + // ), + // ); + // } else if (len(existingSpecifiers) > 0 && isSorted != false) { + // !!! OrganizeImports not implemented + // 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. + // changed to check if existing specifiers are sorted + // if we're promoting the clause from type-only, we need to transform the existing imports before attempting to insert the new named imports + // transformedExistingSpecifiers := existingSpecifiers + // if promoteFromTypeOnly && existingSpecifiers { + // transformedExistingSpecifiers = ct.NodeFactory.updateNamedImports( + // importClause.NamedBindings.AsNamedImports(), + // core.SameMap(existingSpecifiers, func(e *ast.ImportSpecifier) *ast.ImportSpecifier { + // return ct.NodeFactory.updateImportSpecifier(e, /*isTypeOnly*/ true, e.propertyName, e.name) + // }), + // ).elements + // } + // for _, spec := range newSpecifiers { + // insertionIndex := OrganizeImports.getImportSpecifierInsertionIndex(transformedExistingSpecifiers, spec, specifierComparer); + // ct.insertImportSpecifierAtIndex(sourceFile, spec, importClause.namedBindings as NamedImports, insertionIndex); + // } + // } else + if len(existingSpecifiers) > 0 { + 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 { + // !!! promote type-only imports not implemented + + // ct.delete(sourceFile, getTypeKeywordOfTypeOnlyImport(clause, sourceFile)); + // if (existingSpecifiers) { + // // We used to convert existing specifiers to type-only only if compiler options indicated that + // // would be meaningful (see the `importNameElisionDisabled` utility function), but user + // // feedback indicated a preference for preserving the type-onlyness of existing specifiers + // // regardless of whether it would make a difference in emit. + // for _, specifier := range existingSpecifiers { + // ct.insertModifierBefore(sourceFile, SyntaxKind.TypeKeyword, specifier); + // } + // } + } + default: + panic("Unsupported clause kind: " + clause.Kind.String() + "for doAddExistingFix") + } +} + +func (ct *changeTracker) addElementToBindingPattern(sourceFile *ast.SourceFile, bindingPattern *ast.Node, name string, propertyName *string) { + element := ct.newBindingElementFromNameAndPropertyName(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 (ct *changeTracker) newBindingElementFromNameAndPropertyName(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 (ct *changeTracker) insertImports(sourceFile *ast.SourceFile, imports []*ast.Statement, blankLineBetween bool, preferences *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) + } + // !!! OrganizeImports + // { comparer, isSorted } := OrganizeImports.getOrganizeImportsStringComparerWithDetection(existingImportStatements, preferences); + // sortedNewImports := isArray(imports) ? toSorted(imports, (a, b) => OrganizeImports.compareImportsOrRequireStatements(a, b, comparer)) : [imports]; + sortedNewImports := imports + // !!! 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 { + // for _, newImport := range sortedNewImports { + // insertionIndex := OrganizeImports.getImportDeclarationInsertionIndex(existingImportStatements, newImport, comparer) + // if insertionIndex == 0 { + // // If the first import is top-of-file, insert after the leading comment which is likely the header. + // options := existingImportStatements[0] == sourceFile.statements[0] ? { leadingTriviaOption: textchanges.LeadingTriviaOption.Exclude } : {}; + // ct.insertNodeBefore(sourceFile, existingImportStatements[0], newImport, /*blankLineBetween*/ false, options); + // } else { + // prevImport := existingImportStatements[insertionIndex - 1] + // ct.insertNodeAfter(sourceFile, prevImport, newImport); + // } + // } + // return + // } + + if len(existingImportStatements) > 0 { + ct.insertNodesAfter(sourceFile, existingImportStatements[len(existingImportStatements)-1], sortedNewImports) + } else { + ct.insertAtTopOfFile(sourceFile, sortedNewImports, blankLineBetween) + } +} + +func (ct *changeTracker) makeImport(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(isTypeOnly, defaultImport, newNamedImports) + } + return ct.NodeFactory.NewImportDeclaration( /*modifiers*/ nil, importClause, moduleSpecifier, nil /*attributes*/) +} + +func (ct *changeTracker) getNewImports( + moduleSpecifier string, + // quotePreference quotePreference, // !!! quotePreference + defaultImport *Import, + namedImports []*Import, + namespaceLikeImport *Import, // { importKind: ImportKind.CommonJS | ImportKind.Namespace; } + compilerOptions *core.CompilerOptions, + preferences *UserPreferences, +) []*ast.Statement { + moduleSpecifierStringLiteral := ct.NodeFactory.NewStringLiteral(moduleSpecifier) + 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() || ptrIsTrue(preferences.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, ct.makeImport(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, preferences), + 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, preferences), + ct.NodeFactory.NewIdentifier(namespaceLikeImport.name), + ct.NodeFactory.NewExternalModuleReference(moduleSpecifierStringLiteral), + ) + } else { + declaration = ct.NodeFactory.NewImportDeclaration( + /*modifiers*/ nil, + ct.NodeFactory.NewImportClause( + shouldUseTypeOnly(namespaceLikeImport.addAsTypeOnly, preferences), + /*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 *UserPreferences) bool { + return needsTypeOnly(addAsTypeOnly) || addAsTypeOnly != AddAsTypeOnlyNotAllowed && preferences.PreferTypeOnlyAutoImports != nil && *preferences.PreferTypeOnlyAutoImports +} diff --git a/internal/ls/changetrackerimpl.go b/internal/ls/changetrackerimpl.go new file mode 100644 index 0000000000..c07d8a108e --- /dev/null +++ b/internal/ls/changetrackerimpl.go @@ -0,0 +1,763 @@ +package ls + +import ( + "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/collections" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/format" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/parser" + "github.com/microsoft/typescript-go/internal/printer" + "github.com/microsoft/typescript-go/internal/scanner" + "github.com/microsoft/typescript-go/internal/stringutil" +) + +type changeNodeOptions struct { + insertNodeOptions + leadingTriviaOption + trailingTriviaOption + joiner string +} + +type leadingTriviaOption int + +const ( + leadingTriviaOptionNone leadingTriviaOption = 0 + leadingTriviaOptionExclude leadingTriviaOption = 1 + leadingTriviaOptionIncludeAll leadingTriviaOption = 2 + leadingTriviaOptionJSDoc leadingTriviaOption = 3 + leadingTriviaOptionStartLine leadingTriviaOption = 4 +) + +type trailingTriviaOption int + +const ( + trailingTriviaOptionNone trailingTriviaOption = 0 + trailingTriviaOptionExclude trailingTriviaOption = 1 + trailingTriviaOptionExcludeWhitespace trailingTriviaOption = 2 + trailingTriviaOptionInclude trailingTriviaOption = 3 +) + +type insertNodeOptions struct { + /** + * Text to be inserted before the new node + */ + prefix string + /** + * Text to be inserted after the new node + */ + suffix string + /** + * Text of inserted node will be formatted with this indentation, otherwise indentation will be inferred from the old node + */ + indentation *int + /** + * Text of inserted node will be formatted with this delta, otherwise delta will be inferred from the new node kind + */ + delta *int +} + +type trackerEditKind int + +const ( + trackerEditKindText trackerEditKind = 1 + trackerEditKindRemove trackerEditKind = 2 + trackerEditKindReplaceWithSingleNode trackerEditKind = 3 + trackerEditKindReplaceWithMultipleNodes trackerEditKind = 4 +) + +type trackerEdit interface { + Kind() trackerEditKind + getRange() lsproto.Range +} + +type trackerEditText struct { + // sourceFile *ast.SourceFile + *lsproto.TextEdit +} + +func (eText *trackerEditText) Kind() trackerEditKind { + return trackerEditKindText +} + +func (eText *trackerEditText) getRange() lsproto.Range { + return eText.TextEdit.Range +} + +type trackerEditRemove struct { + // sourceFile *ast.SourceFile + lsproto.Range +} + +func (eRemove *trackerEditRemove) Kind() trackerEditKind { + return trackerEditKindRemove +} + +func (eRemove *trackerEditRemove) getRange() lsproto.Range { + return eRemove.Range +} + +type trackerEditReplaceWithSingleNode struct { + // sourceFile *ast.SourceFile + lsproto.Range + *ast.Node + options insertNodeOptions +} + +func (re *trackerEditReplaceWithSingleNode) Kind() trackerEditKind { + return trackerEditKindRemove +} + +func (eSingle *trackerEditReplaceWithSingleNode) getRange() lsproto.Range { + return eSingle.Range +} + +type trackerEditReplaceWithMultipleNodes struct { + // sourceFile *ast.SourceFile + lsproto.Range + nodes []*ast.Node + options changeNodeOptions +} + +func (re *trackerEditReplaceWithMultipleNodes) Kind() trackerEditKind { + return trackerEditKindReplaceWithMultipleNodes +} + +func (eSingle *trackerEditReplaceWithMultipleNodes) getRange() lsproto.Range { + return eSingle.Range +} + +type changeTracker struct { + // initialized with + formatSettings *format.FormatCodeSettings + newLine string + ls *LanguageService + ctx context.Context + *printer.EmitContext + + *ast.NodeFactory + changes *collections.MultiMap[*ast.SourceFile, trackerEdit] + + // created during call to getChanges + writer *printer.ChangeTrackerWriter + // printer +} + +func (ls *LanguageService) newChangeTracker(ctx context.Context) *changeTracker { + emitContext := printer.NewEmitContext() + return &changeTracker{ + ls: ls, + EmitContext: emitContext, + NodeFactory: &emitContext.Factory.NodeFactory, + changes: &collections.MultiMap[*ast.SourceFile, trackerEdit]{}, + ctx: ctx, + formatSettings: format.GetFormatCodeSettingsFromContext(ctx), + newLine: format.GetNewLineOrDefaultFromContext(ctx), + } +} + +// !!! strada note +// - Note: after calling this, the TextChanges object must be discarded! +func (ct *changeTracker) getChanges() map[string][]*lsproto.TextEdit { + // !!! finishDeleteDeclarations + // !!! finishClassesWithNodesInsertedAtStart + changes := ct.getTextChangesFromChanges() + // !!! changes for new files + return changes +} + +func (ct *changeTracker) getTextChangesFromChanges() map[string][]*lsproto.TextEdit { + changes := map[string][]*lsproto.TextEdit{} + for sourceFile, changesInFile := range ct.changes.M { + // order changes by start position + // If the start position is the same, put the shorter range first, since an empty range (x, x) may precede (x, y) but not vice-versa. + slices.SortStableFunc(changesInFile, func(a, b trackerEdit) int { return CompareRanges(ptrTo(a.getRange()), ptrTo(b.getRange())) }) + // verify that change intervals do not overlap, except possibly at end points. + for i := range len(changesInFile) - 1 { + if ComparePositions(changesInFile[i].getRange().End, changesInFile[i+1].getRange().Start) > 0 { + // assert change[i].End <= change[i + 1].Start + panic(fmt.Sprintf("changes overlap: %v and %v", changesInFile[i].getRange(), changesInFile[i+1].getRange())) + } + } + + textChanges := core.MapNonNil(changesInFile, func(change trackerEdit) *lsproto.TextEdit { + // !!! targetSourceFile + + newText := ct.computeNewText(change, sourceFile, sourceFile) + // span := createTextSpanFromRange(c.getRange()) + // !!! + // Filter out redundant changes. + // if (span.length == newText.length && stringContainsAt(targetSourceFile.text, newText, span.start)) { return nil } + + return &lsproto.TextEdit{ + NewText: newText, + Range: change.getRange(), + } + }) + + if len(textChanges) > 0 { + changes[sourceFile.FileName()] = textChanges + } + } + return changes +} + +func (ct *changeTracker) computeNewText(change trackerEdit, targetSourceFile *ast.SourceFile, sourceFile *ast.SourceFile) string { + switch change.Kind() { + case trackerEditKindRemove: + return "" + case trackerEditKindText: + return change.(*trackerEditText).NewText + } + + var options changeNodeOptions + pos := int(ct.ls.converters.LineAndCharacterToPosition(sourceFile, change.getRange().Start)) + targetFileLineMap := targetSourceFile.LineMap() + format := func(n *ast.Node) string { + return ct.getFormattedTextOfNode(n, targetSourceFile, sourceFile, pos, targetFileLineMap, options.insertNodeOptions) + } + + var text string + switch change.Kind() { + + case trackerEditKindReplaceWithMultipleNodes: + changeEditMultiple := change.(*trackerEditReplaceWithMultipleNodes) + if options.joiner == "" { + options.joiner = ct.newLine + } + text = strings.Join(core.Map(changeEditMultiple.nodes, func(n *ast.Node) string { return strings.TrimSuffix(format(n), ct.newLine) }), options.joiner) + case trackerEditKindReplaceWithSingleNode: + text = format(change.(*trackerEditReplaceWithSingleNode).Node) + default: + panic(fmt.Sprintf("change kind %d should have been handled earlier", change.Kind())) + } + // strip initial indentation (spaces or tabs) if text will be inserted in the middle of the line + noIndent := text + if !(options.indentation != nil && *options.indentation != 0 || scanner.GetLineStartPositionForPosition(pos, targetFileLineMap) == pos) { + noIndent = strings.TrimLeftFunc(text, unicode.IsSpace) + } + return options.prefix + noIndent // !!! +((!options.suffix || endsWith(noIndent, options.suffix)) ? "" : options.suffix); +} + +/** Note: this may mutate `nodeIn`. */ +func (ct *changeTracker) getFormattedTextOfNode(nodeIn *ast.Node, targetSourceFile *ast.SourceFile, sourceFile *ast.SourceFile, pos int, targetFileLineMap []core.TextPos, options insertNodeOptions) string { + text, node := ct.getNonformattedText(nodeIn, targetSourceFile) + // !!! if (validate) validate(node, text); + formatOptions := getFormatCodeSettingsForWriting(ct.formatSettings, targetSourceFile) + + var initialIndentation, delta int + if options.indentation == nil { + // !!! indentation for position + // initialIndentation = format.GetIndentationForPos(pos, sourceFile, formatOptions, options.prefix == ct.newLine || scanner.GetLineStartPositionForPosition(pos, targetFileLineMap) == pos); + } else { + initialIndentation = *options.indentation + } + + if options.delta != nil { + delta = *options.delta + } else if formatOptions.IndentSize != 0 && format.ShouldIndentChildNode(formatOptions, nodeIn, nil, nil) { + delta = formatOptions.IndentSize + } + + changes := format.FormatNodeGivenIndentation(ct.ctx, node, targetSourceFile, targetSourceFile.LanguageVariant, initialIndentation, delta) + return applyTextChanges(text, changes) +} + +func getFormatCodeSettingsForWriting(options *format.FormatCodeSettings, sourceFile *ast.SourceFile) *format.FormatCodeSettings { + shouldAutoDetectSemicolonPreference := options.Semicolons == format.SemicolonPreferenceIgnore + shouldRemoveSemicolons := options.Semicolons == format.SemicolonPreferenceRemove || shouldAutoDetectSemicolonPreference && !probablyUsesSemicolons(sourceFile) + if shouldRemoveSemicolons { + options.Semicolons = format.SemicolonPreferenceRemove + } + + return options +} + +/** Note: output node may be mutated input node. */ +func (ct *changeTracker) getNonformattedText(node *ast.Node, sourceFile *ast.SourceFile) (string, *ast.Node) { + writer := printer.NewChangeTrackerWriter(ct.newLine) + + printer.NewPrinter( + printer.PrinterOptions{ + NewLine: core.GetNewLineKind(ct.newLine), + NeverAsciiEscape: true, + PreserveSourceNewlines: true, + TerminateUnterminatedLiterals: true, + }, + writer.GetPrintHandlers(), + ct.EmitContext, + ).Write(node, sourceFile, writer, nil) + + text := writer.String() + + return text, writer.AssignPositionsToNode(node, ct.NodeFactory) +} + +func (ct *changeTracker) Write(s string) { ct.writer.Write(s) } + +func (ct *changeTracker) replaceNode(sourceFile *ast.SourceFile, oldNode *ast.Node, newNode *ast.Node, options *changeNodeOptions) { + if options == nil { + // defaults to `useNonAdjustedPositions` + options = &changeNodeOptions{ + insertNodeOptions: insertNodeOptions{}, + leadingTriviaOption: leadingTriviaOptionExclude, + trailingTriviaOption: trailingTriviaOptionExclude, + } + } + ct.replaceRange(sourceFile, ct.getAdjustedRange(sourceFile, oldNode, oldNode, options.leadingTriviaOption, options.trailingTriviaOption), newNode, options.insertNodeOptions) +} + +func (ct *changeTracker) replaceRange(sourceFile *ast.SourceFile, lsprotoRange lsproto.Range, newNode *ast.Node, options insertNodeOptions) { + ct.changes.Add(sourceFile, &trackerEditReplaceWithSingleNode{Range: lsprotoRange, options: options, Node: newNode}) +} + +func (ct *changeTracker) replaceRangeWithText(sourceFile *ast.SourceFile, lsprotoRange lsproto.Range, text string) { + ct.changes.Add(sourceFile, &trackerEditText{&lsproto.TextEdit{Range: lsprotoRange, NewText: text}}) +} + +func (ct *changeTracker) replaceRangeWithNodes(sourceFile *ast.SourceFile, lsprotoRange lsproto.Range, newNodes []*ast.Node, options changeNodeOptions) { + if len(newNodes) == 1 { + ct.replaceRange(sourceFile, lsprotoRange, newNodes[0], options.insertNodeOptions) + return + } + ct.changes.Add(sourceFile, &trackerEditReplaceWithMultipleNodes{Range: lsprotoRange, nodes: newNodes, options: options}) +} + +func (ct *changeTracker) insertText(sourceFile *ast.SourceFile, pos lsproto.Position, text string) { + ct.replaceRangeWithText(sourceFile, lsproto.Range{Start: pos, End: pos}, text) +} + +func (ct *changeTracker) insertNodeAt(sourceFile *ast.SourceFile, pos core.TextPos, newNode *ast.Node, options insertNodeOptions) { + lsPos := ct.ls.converters.PositionToLineAndCharacter(sourceFile, pos) + ct.replaceRange(sourceFile, lsproto.Range{Start: lsPos, End: lsPos}, newNode, options) +} + +func (ct *changeTracker) insertNodesAt(sourceFile *ast.SourceFile, pos core.TextPos, newNodes []*ast.Node, options changeNodeOptions) { + lsPos := ct.ls.converters.PositionToLineAndCharacter(sourceFile, pos) + ct.replaceRangeWithNodes(sourceFile, lsproto.Range{Start: lsPos, End: lsPos}, newNodes, options) +} + +func (ct *changeTracker) insertNodeAfter(sourceFile *ast.SourceFile, after *ast.Node, newNode *ast.Node) { + endPosition := ct.endPosForInsertNodeAfter(sourceFile, after, newNode) + ct.insertNodeAt(sourceFile, endPosition, newNode, ct.getInsertNodeAfterOptions(sourceFile, after)) +} + +func (ct *changeTracker) insertNodesAfter(sourceFile *ast.SourceFile, after *ast.Node, newNodes []*ast.Node) { + endPosition := ct.endPosForInsertNodeAfter(sourceFile, after, newNodes[0]) + ct.insertNodesAt(sourceFile, endPosition, newNodes, changeNodeOptions{insertNodeOptions: ct.getInsertNodeAfterOptions(sourceFile, after)}) +} + +func (ct *changeTracker) endPosForInsertNodeAfter(sourceFile *ast.SourceFile, after *ast.Node, newNode *ast.Node) core.TextPos { + if (needSemicolonBetween(after, newNode)) && (rune(sourceFile.Text()[after.End()-1]) != ';') { + // check if previous statement ends with semicolon + // if not - insert semicolon to preserve the code from changing the meaning due to ASI + endPos := ct.ls.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(after.End())) + ct.replaceRange(sourceFile, + lsproto.Range{Start: endPos, End: endPos}, + sourceFile.GetOrCreateToken(ast.KindSemicolonToken, after.End(), after.End(), after.Parent), + insertNodeOptions{}, + ) + } + return core.TextPos(ct.getAdjustedEndPosition(sourceFile, after, trailingTriviaOptionNone)) +} + +func (ct *changeTracker) getInsertNodeAfterOptions(sourceFile *ast.SourceFile, node *ast.Node) insertNodeOptions { + newLineChar := ct.newLine + var options insertNodeOptions + switch node.Kind { + case ast.KindParameter: + // default opts + options = insertNodeOptions{} + case ast.KindClassDeclaration, ast.KindModuleDeclaration: + options = insertNodeOptions{prefix: newLineChar, suffix: newLineChar} + + case ast.KindVariableDeclaration, ast.KindStringLiteral, ast.KindIdentifier: + options = insertNodeOptions{prefix: ", "} + + case ast.KindPropertyAssignment: + options = insertNodeOptions{suffix: "," + newLineChar} + + case ast.KindExportKeyword: + options = insertNodeOptions{prefix: " "} + + default: + if !(ast.IsStatement(node) || ast.IsClassOrTypeElement(node)) { + // Else we haven't handled this kind of node yet -- add it + panic("unimplemented node type " + node.Kind.String() + " in changeTracker.getInsertNodeAfterOptions") + } + options = insertNodeOptions{suffix: newLineChar} + } + if node.End() == sourceFile.End() && ast.IsStatement(node) { + options.prefix = "\n" + options.prefix + } + + return options +} + +/** +* This function should be used to insert nodes in lists when nodes don't carry separators as the part of the node range, +* i.e. arguments in arguments lists, parameters in parameter lists etc. +* Note that separators are part of the node in statements and class elements. + */ +func (ct *changeTracker) insertNodeInListAfter(sourceFile *ast.SourceFile, after *ast.Node, newNode *ast.Node, containingList []*ast.Node) { + if len(containingList) == 0 { + containingList = format.GetContainingList(after, sourceFile).Nodes + } + index := slices.Index(containingList, after) + if index < 0 { + return + } + if index != len(containingList)-1 { + // any element except the last one + // use next sibling as an anchor + if nextToken := astnav.GetTokenAtPosition(sourceFile, after.End()); nextToken != nil && isSeparator(after, nextToken) { + // for list + // a, b, c + // create change for adding 'e' after 'a' as + // - find start of next element after a (it is b) + // - use next element start as start and end position in final change + // - build text of change by formatting the text of node + whitespace trivia of b + + // in multiline case it will work as + // a, + // b, + // c, + // result - '*' denotes leading trivia that will be inserted after new text (displayed as '#') + // a, + // insertedtext# + // ###b, + // c, + nextNode := containingList[index+1] + startPos := scanner.SkipTriviaEx(sourceFile.Text(), nextNode.Pos(), &scanner.SkipTriviaOptions{StopAfterLineBreak: true, StopAtComments: false}) + + // write separator and leading trivia of the next element as suffix + suffix := scanner.TokenToString(nextToken.Kind) + sourceFile.Text()[nextNode.End():startPos] + ct.insertNodeAt(sourceFile, core.TextPos(startPos), newNode, insertNodeOptions{suffix: suffix}) + } + return + } + + afterStart := astnav.GetStartOfNode(after, sourceFile, false) + lineMap := sourceFile.LineMap() + afterStartLinePosition := scanner.GetLineStartPositionForPosition(afterStart, lineMap) + + // insert element after the last element in the list that has more than one item + // pick the element preceding the after element to: + // - pick the separator + // - determine if list is a multiline + separator := ast.KindUnknown // SyntaxKind.CommaToken | SyntaxKind.SemicolonToken | undefined; + multilineList := false + + // if list has only one element then we'll format is as multiline if node has comment in trailing trivia, or as singleline otherwise + // i.e. var x = 1 // this is x + // | new element will be inserted at this position + if len(containingList) != 1 { + // otherwise, if list has more than one element, pick separator from the list + tokenBeforeInsertPosition := astnav.FindPrecedingToken(sourceFile, after.Pos()) + separator = core.IfElse(isSeparator(after, tokenBeforeInsertPosition), tokenBeforeInsertPosition.Kind, ast.KindCommaToken) + // determine if list is multiline by checking lines of after element and element that precedes it. + afterMinusOneStartLinePosition := scanner.GetLineStartPositionForPosition(astnav.GetStartOfNode(containingList[index-1], sourceFile, false), lineMap) + multilineList = afterMinusOneStartLinePosition != afterStartLinePosition + } + if hasCommentsBeforeLineBreak(sourceFile.Text(), after.End()) || printer.GetLinesBetweenPositions(sourceFile, containingList[0].Pos(), containingList[len(containingList)-1].End()) != 0 { + // in this case we'll always treat containing list as multiline + multilineList = true + } + + end := ct.ls.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(after.End())) + if !multilineList { + ct.replaceRange(sourceFile, lsproto.Range{Start: end, End: end}, newNode, insertNodeOptions{prefix: `${tokenToString(separator)} `}) + return + } + + // insert separator immediately following the 'after' node to preserve comments in trailing trivia + // !!! check GetOrCreateToken port + ct.replaceRange(sourceFile, lsproto.Range{Start: end, End: end}, sourceFile.GetOrCreateToken(separator, after.End(), after.End(), after.Parent), insertNodeOptions{}) + // use the same indentation as 'after' item + indentation := format.FindFirstNonWhitespaceColumn(afterStartLinePosition, afterStart, sourceFile, ct.formatSettings) + // insert element before the line break on the line that contains 'after' element + insertPos := scanner.SkipTriviaEx(sourceFile.Text(), after.End(), &scanner.SkipTriviaOptions{StopAfterLineBreak: true, StopAtComments: false}) + // find position before "\n" or "\r\n" + for insertPos != after.End() && stringutil.IsLineBreak(rune(sourceFile.Text()[insertPos-1])) { + insertPos-- + } + insertLSPos := ct.ls.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(insertPos)) + ct.replaceRange( + sourceFile, + lsproto.Range{Start: insertLSPos, End: insertLSPos}, + newNode, + insertNodeOptions{ + indentation: ptrTo(indentation), + prefix: ct.newLine, + }, + ) +} + +func (ct *changeTracker) insertAtTopOfFile(sourceFile *ast.SourceFile, insert []*ast.Statement, blankLineBetween bool) { + pos := ct.getInsertionPositionAtSourceFileTop(sourceFile) + options := insertNodeOptions{} + if pos != 0 { + options.prefix = ct.newLine + } + if !stringutil.IsLineBreak(rune(sourceFile.Text()[pos])) { + options.suffix = ct.newLine + } + if blankLineBetween { + options.suffix += ct.newLine + } + + if len(insert) == 0 { + ct.insertNodeAt(sourceFile, core.TextPos(pos), insert[0], options) + } else { + ct.insertNodesAt(sourceFile, core.TextPos(pos), insert, changeNodeOptions{insertNodeOptions: options}) + } +} + +// method on the changeTracker because use of converters +func (ct *changeTracker) getAdjustedRange(sourceFile *ast.SourceFile, startNode *ast.Node, endNode *ast.Node, leadingOption leadingTriviaOption, trailingOption trailingTriviaOption) lsproto.Range { + return *ct.ls.createLspRangeFromBounds( + ct.getAdjustedStartPosition(sourceFile, startNode, leadingOption, false), + ct.getAdjustedEndPosition(sourceFile, endNode, trailingOption), + sourceFile, + ) +} + +// method on the changeTracker because use of converters +func (ct *changeTracker) getAdjustedStartPosition(sourceFile *ast.SourceFile, node *ast.Node, leadingOption leadingTriviaOption, hasTrailingComment bool) int { + if leadingOption == leadingTriviaOptionJSDoc { + if JSDocComments := parser.GetJSDocCommentRanges(ct.NodeFactory, nil, node, sourceFile.Text()); len(JSDocComments) > 0 { + return scanner.GetLineStartPositionForPosition(JSDocComments[0].Pos(), sourceFile.LineMap()) + } + } + + start := astnav.GetStartOfNode(node, sourceFile, false) + lineStarts := sourceFile.LineMap() + startOfLinePos := scanner.GetLineStartPositionForPosition(start, lineStarts) + + switch leadingOption { + case leadingTriviaOptionExclude: + return start + case leadingTriviaOptionStartLine: + if ast.NodeRangeContainsPosition(node, startOfLinePos) { + return startOfLinePos + } + return start + } + + fullStart := node.Pos() + if fullStart == start { + return start + } + fullStartLineIndex := scanner.ComputeLineOfPosition(lineStarts, fullStart) + fullStartLinePos := scanner.GetLineStartPositionForPosition(fullStart, lineStarts) + if startOfLinePos == fullStartLinePos { + // full start and start of the node are on the same line + // a, b; + // ^ ^ + // | start + // fullstart + // when b is replaced - we usually want to keep the leading trvia + // when b is deleted - we delete it + if leadingOption == leadingTriviaOptionIncludeAll { + return fullStart + } + return start + } + + // if node has a trailing comments, use comment end position as the text has already been included. + if hasTrailingComment { + // Check first for leading comments as if the node is the first import, we want to exclude the trivia; + // otherwise we get the trailing comments. + comments := slices.Collect(scanner.GetLeadingCommentRanges(ct.NodeFactory, sourceFile.Text(), fullStart)) + if len(comments) == 0 { + comments = slices.Collect(scanner.GetTrailingCommentRanges(ct.NodeFactory, sourceFile.Text(), fullStart)) + } + if len(comments) > 0 { + return scanner.SkipTriviaEx(sourceFile.Text(), comments[0].End(), &scanner.SkipTriviaOptions{StopAfterLineBreak: true, StopAtComments: true}) + } + } + + // get start position of the line following the line that contains fullstart position + // (but only if the fullstart isn't the very beginning of the file) + nextLineStart := core.IfElse(fullStart > 0, 1, 0) + adjustedStartPosition := int(lineStarts[fullStartLineIndex+nextLineStart]) + // skip whitespaces/newlines + adjustedStartPosition = scanner.SkipTriviaEx(sourceFile.Text(), adjustedStartPosition, &scanner.SkipTriviaOptions{StopAtComments: true}) + return int(lineStarts[scanner.ComputeLineOfPosition(lineStarts, adjustedStartPosition)]) +} + +// method on the changeTracker because of converters +// Return the end position of a multiline comment of it is on another line; otherwise returns `undefined`; +func (ct *changeTracker) getEndPositionOfMultilineTrailingComment(sourceFile *ast.SourceFile, node *ast.Node, trailingOpt trailingTriviaOption) int { + if trailingOpt == trailingTriviaOptionInclude { + // If the trailing comment is a multiline comment that extends to the next lines, + // return the end of the comment and track it for the next nodes to adjust. + lineStarts := sourceFile.LineMap() + nodeEndLine := scanner.ComputeLineOfPosition(lineStarts, node.End()) + for comment := range scanner.GetTrailingCommentRanges(ct.NodeFactory, sourceFile.Text(), node.End()) { + // Single line can break the loop as trivia will only be this line. + // Comments on subsequest lines are also ignored. + if comment.Kind == ast.KindSingleLineCommentTrivia || scanner.ComputeLineOfPosition(lineStarts, comment.Pos()) > nodeEndLine { + break + } + + // Get the end line of the comment and compare against the end line of the node. + // If the comment end line position and the multiline comment extends to multiple lines, + // then is safe to return the end position. + if commentEndLine := scanner.ComputeLineOfPosition(lineStarts, comment.End()); commentEndLine > nodeEndLine { + return scanner.SkipTriviaEx(sourceFile.Text(), comment.End(), &scanner.SkipTriviaOptions{StopAfterLineBreak: true, StopAtComments: true}) + } + } + } + + return 0 +} + +// method on the changeTracker because of converters +func (ct *changeTracker) getAdjustedEndPosition(sourceFile *ast.SourceFile, node *ast.Node, trailingTriviaOption trailingTriviaOption) int { + if trailingTriviaOption == trailingTriviaOptionExclude { + return node.End() + } + if trailingTriviaOption == trailingTriviaOptionExcludeWhitespace { + if comments := slices.AppendSeq( + slices.Collect(scanner.GetTrailingCommentRanges(ct.NodeFactory, sourceFile.Text(), node.End())), + scanner.GetLeadingCommentRanges(ct.NodeFactory, sourceFile.Text(), node.End()), + ); len(comments) > 0 { + if realEnd := comments[len(comments)-1].End(); realEnd != 0 { + return realEnd + } + } + return node.End() + } + + if multilineEndPosition := ct.getEndPositionOfMultilineTrailingComment(sourceFile, node, trailingTriviaOption); multilineEndPosition != 0 { + return multilineEndPosition + } + + newEnd := scanner.SkipTriviaEx(sourceFile.Text(), node.End(), &scanner.SkipTriviaOptions{StopAfterLineBreak: true}) + + if newEnd != node.End() && (trailingTriviaOption == trailingTriviaOptionInclude || stringutil.IsLineBreak(rune(sourceFile.Text()[newEnd-1]))) { + return newEnd + } + return node.End() +} + +// ============= utilities ============= + +func hasCommentsBeforeLineBreak(text string, start int) bool { + for _, ch := range []rune(text[start:]) { + if !stringutil.IsWhiteSpaceSingleLine(ch) { + return ch == '/' + } + } + return false +} + +func needSemicolonBetween(a, b *ast.Node) bool { + return (ast.IsPropertySignatureDeclaration(a) || ast.IsPropertyDeclaration(a)) && + ast.IsClassOrTypeElement(b) && + b.Name().Kind == ast.KindComputedPropertyName || + ast.IsStatementButNotDeclaration(a) && + ast.IsStatementButNotDeclaration(b) // TODO: only if b would start with a `(` or `[` +} + +func (ct *changeTracker) getInsertionPositionAtSourceFileTop(sourceFile *ast.SourceFile) int { + var lastPrologue *ast.Node + for _, node := range sourceFile.Statements.Nodes { + if ast.IsPrologueDirective(node) { + lastPrologue = node + } else { + break + } + } + + position := 0 + text := sourceFile.Text() + advancePastLineBreak := func() { + if position >= len(text) { + return + } + if char := rune(text[position]); stringutil.IsLineBreak(char) { + position++ + if position < len(text) && char == '\r' && rune(text[position]) == '\n' { + position++ + } + } + } + if lastPrologue != nil { + position = lastPrologue.End() + advancePastLineBreak() + return position + } + + shebang := scanner.GetShebang(text) + if shebang != "" { + position = len(shebang) + advancePastLineBreak() + } + + ranges := slices.Collect(scanner.GetLeadingCommentRanges(ct.NodeFactory, text, position)) + if len(ranges) == 0 { + return position + } + // Find the first attached comment to the first node and add before it + var lastComment *ast.CommentRange + pinnedOrTripleSlash := false + firstNodeLine := -1 + + lenStatements := len(sourceFile.Statements.Nodes) + lineMap := sourceFile.LineMap() + for _, r := range ranges { + if r.Kind == ast.KindMultiLineCommentTrivia { + if printer.IsPinnedComment(text, r) { + lastComment = &r + pinnedOrTripleSlash = true + continue + } + } else if printer.IsRecognizedTripleSlashComment(text, r) { + lastComment = &r + pinnedOrTripleSlash = true + continue + } + + if lastComment != nil { + // Always insert after pinned or triple slash comments + if pinnedOrTripleSlash { + break + } + + // There was a blank line between the last comment and this comment. + // This comment is not part of the copyright comments + commentLine := scanner.ComputeLineOfPosition(lineMap, r.Pos()) + lastCommentEndLine := scanner.ComputeLineOfPosition(lineMap, lastComment.End()) + if commentLine >= lastCommentEndLine+2 { + break + } + } + + if lenStatements > 0 { + if firstNodeLine == -1 { + firstNodeLine = scanner.ComputeLineOfPosition(lineMap, astnav.GetStartOfNode(sourceFile.Statements.Nodes[0], sourceFile, false)) + } + commentEndLine := scanner.ComputeLineOfPosition(lineMap, r.End()) + if firstNodeLine < commentEndLine+2 { + break + } + } + lastComment = &r + pinnedOrTripleSlash = false + } + + if lastComment != nil { + position = lastComment.End() + advancePastLineBreak() + } + return position +} diff --git a/internal/ls/completions.go b/internal/ls/completions.go index 8a40ca0793..17230e3660 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -14,6 +14,7 @@ import ( "github.com/go-json-experiment/json" "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/astnav" + "github.com/microsoft/typescript-go/internal/binder" "github.com/microsoft/typescript-go/internal/checker" "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/compiler" @@ -21,8 +22,10 @@ import ( "github.com/microsoft/typescript-go/internal/jsnum" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/lsutil" + "github.com/microsoft/typescript-go/internal/printer" "github.com/microsoft/typescript-go/internal/scanner" "github.com/microsoft/typescript-go/internal/stringutil" + "github.com/microsoft/typescript-go/internal/tspath" "golang.org/x/text/collate" ) @@ -95,8 +98,8 @@ type completionDataData struct { isJsxIdentifierExpected bool isRightOfOpenTag bool isRightOfDotOrQuestionDot bool - importStatementCompletion any // !!! - hasUnresolvedAutoImports bool // !!! + importStatementCompletion *importStatementCompletionInfo // !!! + hasUnresolvedAutoImports bool // !!! // flags CompletionInfoFlags // !!! defaultCommitCharacters []string } @@ -107,7 +110,12 @@ type completionDataKeyword struct { } type importStatementCompletionInfo struct { - // !!! + isKeywordOnlyCompletion bool + keywordCompletion ast.Kind // TokenKind + isNewIdentifierLocation bool + isTopLevelTypeOnly bool + couldBeTypeOnlyImportSpecifier bool + replacementSpan *lsproto.Range } // If we're after the `=` sign but no identifier has been typed yet, @@ -132,6 +140,15 @@ const ( KeywordCompletionFiltersLast = KeywordCompletionFiltersTypeKeyword ) +func keywordFiltersFromSyntaxKind(keywordCompletion ast.Kind) KeywordCompletionFilters { + switch keywordCompletion { + case ast.KindTypeKeyword: + return KeywordCompletionFiltersTypeKeyword + default: + panic("Unknown mapping from ast.Kind `" + keywordCompletion.String() + "` to KeywordCompletionFilters") + } +} + type CompletionKind int const ( @@ -201,23 +218,69 @@ type symbolOriginInfo struct { data any } -func (s *symbolOriginInfo) symbolName() string { - switch s.data.(type) { +func (origin *symbolOriginInfo) symbolName() string { + switch origin.data.(type) { + case *symbolOriginInfoExport: + return origin.data.(*symbolOriginInfoExport).symbolName + case *symbolOriginInfoResolvedExport: + return origin.data.(*symbolOriginInfoResolvedExport).symbolName + default: + panic(fmt.Sprintf("symbolOriginInfo: unknown data type for symbolName(): %T", origin.data)) + } +} + +func (origin *symbolOriginInfo) moduleSymbol() *ast.Symbol { + switch origin.data.(type) { + case *symbolOriginInfoExport: + return origin.data.(*symbolOriginInfoExport).moduleSymbol + case *symbolOriginInfoResolvedExport: + return origin.data.(*symbolOriginInfoResolvedExport).moduleSymbol + default: + panic(fmt.Sprintf("symbolOriginInfo: unknown data type for moduleSymbol(): %T", origin.data)) + } +} + +func (origin *symbolOriginInfo) toCompletionEntryData() *completionEntryData { + var ambientModuleName *string + if origin.fileName != "" { + ambientModuleName = strPtrTo(stringutil.StripQuotes(origin.fileName)) + } + var isPackageJsonImport core.Tristate + if origin.isFromPackageJson { + isPackageJsonImport = core.TSTrue + } + switch origin.data.(type) { case *symbolOriginInfoExport: - return s.data.(*symbolOriginInfoExport).symbolName + data := origin.data.(*symbolOriginInfoExport) + return &completionEntryData{ + kind: completionEntryDataKindAutoImportUnresolved, + exportName: data.exportName, + exportMapKey: data.exportMapKey, + fileName: strPtrTo(origin.fileName), + ambientModuleName: ambientModuleName, + isPackageJsonImport: isPackageJsonImport, + } case *symbolOriginInfoResolvedExport: - return s.data.(*symbolOriginInfoResolvedExport).symbolName + data := origin.data.(*symbolOriginInfoResolvedExport) + return &completionEntryData{ + kind: completionEntryDataKindAutoImportResolved, + exportName: data.exportName, + exportMapKey: data.exportMapKey, + moduleSpecifier: data.moduleSpecifier, + ambientModuleName: ambientModuleName, + fileName: strPtrTo(origin.fileName), + isPackageJsonImport: isPackageJsonImport, + } default: - panic(fmt.Sprintf("symbolOriginInfo: unknown data type for symbolName(): %T", s.data)) + panic(fmt.Sprintf("completionEntryData is not generated for symbolOriginInfo of type %T", origin.data)) } } type symbolOriginInfoExport struct { - symbolName string - moduleSymbol *ast.Symbol - isDefaultExport bool - exporName string - // exportMapKey ExportMapInfoKey // !!! + symbolName string + moduleSymbol *ast.Symbol + exportName string + exportMapKey ExportMapInfoKey } func (s *symbolOriginInfo) asExport() *symbolOriginInfoExport { @@ -225,10 +288,10 @@ func (s *symbolOriginInfo) asExport() *symbolOriginInfoExport { } type symbolOriginInfoResolvedExport struct { - symbolName string - moduleSymbol *ast.Symbol - exportName string - // exportMapKey ExportMapInfoKey // !!! + symbolName string + moduleSymbol *ast.Symbol + exportName string + exportMapKey ExportMapInfoKey moduleSpecifier string } @@ -352,7 +415,7 @@ func (l *LanguageService) getCompletionsAtPosition( checker, done := program.GetTypeCheckerForFile(ctx, file) defer done() - data := getCompletionData(program, checker, file, position, preferences) + data := l.getCompletionData(program, checker, file, position, nil, preferences) if data == nil { return nil } @@ -389,7 +452,7 @@ func (l *LanguageService) getCompletionsAtPosition( } } -func getCompletionData(program *compiler.Program, typeChecker *checker.Checker, file *ast.SourceFile, position int, preferences *UserPreferences) completionData { +func (l *LanguageService) getCompletionData(program *compiler.Program, typeChecker *checker.Checker, file *ast.SourceFile, position int, itemData *itemData, preferences *UserPreferences) completionData { inCheckedFile := isCheckedFile(file, program.Options()) currentToken := astnav.GetTokenAtPosition(file, position) @@ -424,14 +487,34 @@ func getCompletionData(program *compiler.Program, typeChecker *checker.Checker, location := astnav.GetTouchingPropertyName(file, position) keywordFilters := KeywordCompletionFiltersNone isNewIdentifierLocation := false - // !!! - // flags := CompletionInfoFlagsNone + // !!! flags := CompletionInfoFlagsNone var defaultCommitCharacters []string if contextToken != nil { - // !!! import completions + importStatementCompletionInfo := l.getImportStatementCompletionInfo(contextToken, file) + if importStatementCompletionInfo.keywordCompletion != ast.KindUnknown { + if importStatementCompletionInfo.isKeywordOnlyCompletion { + return &completionDataKeyword{ + keywordCompletions: []*lsproto.CompletionItem{{ + Label: scanner.TokenToString(importStatementCompletionInfo.keywordCompletion), + Kind: ptrTo(lsproto.CompletionItemKindKeyword), + SortText: ptrTo(string(SortTextGlobalsOrKeywords)), + }}, + isNewIdentifierLocation: importStatementCompletionInfo.isNewIdentifierLocation, + } + } + keywordFilters = keywordFiltersFromSyntaxKind(importStatementCompletionInfo.keywordCompletion) + } + if importStatementCompletionInfo.replacementSpan != nil && ptrIsTrue(preferences.IncludeCompletionsForImportStatements) && ptrIsTrue(preferences.IncludeCompletionsWithInsertText) { + // Import statement completions use `insertText`, and also require the `data` property of `CompletionEntryIdentifier` + // added in TypeScript 4.3 to be sent back from the client during `getCompletionEntryDetails`. Since this feature + // is not backward compatible with older clients, the language service defaults to disabling it, allowing newer clients + // to opt in with the `includeCompletionsForImportStatements` user preference. + // !!! flags |= CompletionInfoFlags.IsImportStatementCompletion; + importStatementCompletion = &importStatementCompletionInfo + isNewIdentifierLocation = importStatementCompletionInfo.isNewIdentifierLocation + } // Bail out if this is a known invalid completion location. - // !!! if (!importStatementCompletionInfo.replacementSpan && ...) if isCompletionListBlocker(contextToken, previousToken, location, file, position, typeChecker) { if keywordFilters != KeywordCompletionFiltersNone { isNewIdentifierLocation, _ := computeCommitCharactersAndIsNewIdentifier(contextToken, file, position) @@ -553,15 +636,14 @@ func getCompletionData(program *compiler.Program, typeChecker *checker.Checker, var symbols []*ast.Symbol symbolToOriginInfoMap := map[ast.SymbolId]*symbolOriginInfo{} symbolToSortTextMap := map[ast.SymbolId]sortText{} - // var importSpecifierResolver any // !!! import var seenPropertySymbols collections.Set[ast.SymbolId] + importSpecifierResolver := &importSpecifierResolverForCompletions{SourceFile: file, UserPreferences: preferences, l: l} isTypeOnlyLocation := insideJSDocTagTypeExpression || insideJsDocImportTag || importStatementCompletion != nil && ast.IsTypeOnlyImportOrExportDeclaration(location.Parent) || !isContextTokenValueLocation(contextToken) && (isPossiblyTypeArgumentPosition(contextToken, file, typeChecker) || ast.IsPartOfTypeNode(location) || isContextTokenTypeLocation(contextToken)) - // var getModuleSpecifierResolutionHost any // !!! auto import addSymbolOriginInfo := func(symbol *ast.Symbol, insertQuestionDot bool, insertAwait bool) { symbolId := ast.GetSymbolId(symbol) @@ -614,38 +696,37 @@ func getCompletionData(program *compiler.Program, typeChecker *checker.Checker, typeChecker.TryGetMemberInModuleExportsAndProperties(firstAccessibleSymbol.Name, moduleSymbol) != firstAccessibleSymbol { symbolToOriginInfoMap[firstAccessibleSymbolId] = &symbolOriginInfo{kind: getNullableSymbolOriginInfoKind(symbolOriginInfoKindSymbolMemberNoExport, insertQuestionDot)} } else { - // !!! imports - // var fileName string - // if tspath.IsExternalModuleNameRelative(core.StripQuotes(moduleSymbol.Name)) { - // fileName = ast.GetSourceFileOfModule(moduleSymbol).FileName() - // } - // if importSpecifierResolver == nil { - // importSpecifierResolver ||= codefix.createImportSpecifierResolver(sourceFile, program, host, preferences)) - // } - // const { moduleSpecifier } = importSpecifier.getModuleSpecifierForBestExportInfo( - // [{ - // exportKind: ExportKind.Named, - // moduleFileName: fileName, - // isFromPackageJson: false, - // moduleSymbol, - // symbol: firstAccessibleSymbol, - // targetFlags: skipAlias(firstAccessibleSymbol, typeChecker).flags, - // }], - // position, - // isValidTypeOnlyAliasUseSite(location), - // ) || {}; - // if (moduleSpecifier) { - // const origin: SymbolOriginInfoResolvedExport = { - // kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.SymbolMemberExport), - // moduleSymbol, - // isDefaultExport: false, - // symbolName: firstAccessibleSymbol.name, - // exportName: firstAccessibleSymbol.name, - // fileName, - // moduleSpecifier, - // }; - // symbolToOriginInfoMap[index] = origin; - // } + var fileName string + if tspath.IsExternalModuleNameRelative(stringutil.StripQuotes(moduleSymbol.Name)) { + fileName = ast.GetSourceFileOfModule(moduleSymbol).FileName() + } + result := importSpecifierResolver.getModuleSpecifierForBestExportInfo( + typeChecker, + []*SymbolExportInfo{{ + exportKind: ExportKindNamed, + moduleFileName: fileName, + isFromPackageJson: ptrTo(false), + moduleSymbol: moduleSymbol, + symbol: firstAccessibleSymbol, + targetFlags: typeChecker.SkipAlias(firstAccessibleSymbol).Flags, + }}, + position, + ast.IsValidTypeOnlyAliasUseSite(location), + ) + + if result != nil { + symbolToOriginInfoMap[ast.GetSymbolId(symbol)] = &symbolOriginInfo{ + kind: getNullableSymbolOriginInfoKind(symbolOriginInfoKindSymbolMemberExport, insertQuestionDot), + isDefaultExport: false, + fileName: fileName, + data: symbolOriginInfoResolvedExport{ + moduleSymbol: moduleSymbol, + symbolName: firstAccessibleSymbol.Name, + exportName: firstAccessibleSymbol.Name, + moduleSpecifier: result.Base().moduleSpecifier, + }, + } + } } } else if firstAccessibleSymbolId == 0 || !seenPropertySymbols.Has(firstAccessibleSymbolId) { symbols = append(symbols, symbol) @@ -992,13 +1073,200 @@ func getCompletionData(program *compiler.Program, typeChecker *checker.Checker, return globalsSearchSuccess } + shouldOfferImportCompletions := func() bool { + // If already typing an import statement, provide completions for it. + if importStatementCompletion != nil { + return true + } + // If not already a module, must have modules enabled. + if !ptrIsTrue(preferences.IncludeCompletionsForModuleExports) { + return false + } + // If already using ES modules, OK to continue using them. + if file.ExternalModuleIndicator != nil || file.CommonJSModuleIndicator != nil { + return true + } + // If module transpilation is enabled or we're targeting es6 or above, or not emitting, OK. + if compilerOptionsIndicateEsModules(program.Options()) { + return true + } + + // If some file is using ES6 modules, assume that it's OK to add more. + return true || // !!! symlink program.getSymlinkCache?.().hasAnySymlinks() || + program.Options().Paths.Size() > 0 || + // programContainsModules + core.Some(program.GetSourceFiles(), func(file *ast.SourceFile) bool { + return !file.IsDeclarationFile && !program.IsSourceFileFromExternalLibrary(file) && (file.ExternalModuleIndicator != nil || file.CommonJSModuleIndicator != nil) + }) + } + // Mutates `symbols`, `symbolToOriginInfoMap`, and `symbolToSortTextMap` + collectAutoImports := func() { + if !shouldOfferImportCompletions() { + return + } + + if itemData != nil { + // Debug.assert(!detailsEntryId?.data, "Should not run 'collectAutoImports' when faster path is available via `data`"); + // Asking for completion details for an item that is not an auto-import + if itemData.AutoImport != nil { + panic("Should not run 'collectAutoImports' when faster path is available via `AutoImport`") + } + if itemData.Source == "" { + return + } + } + + // !!! CompletionInfoFlags + + // import { type | -> token text should be blank + var lowerCaseTokenText string + if previousToken != nil && ast.IsIdentifier(previousToken) && !(previousToken == contextToken && importStatementCompletion != nil) { + lowerCaseTokenText = strings.ToLower(previousToken.Text()) + } + + // !!! moduleSpecifierCache := host.getModuleSpecifierCache(); + // !!! packageJsonAutoImportProvider := host.getPackageJsonAutoImportProvider(); + exportInfo := l.getExportInfoMap(typeChecker, file, preferences) + + l.resolvingModuleSpecifiers( + "collectAutoImports", + importSpecifierResolver, + position, + importStatementCompletion != nil, + ast.IsValidTypeOnlyAliasUseSite(location), + func(context *resolvingModuleSpecifiersForCompletions) ImportFix { + exportInfo.search( + typeChecker, + file.Path(), + /*preferCapitalized*/ isRightOfOpenTag, + func(symbolName string, targetFlags ast.SymbolFlags) bool { + if !scanner.IsIdentifierText(symbolName, file.LanguageVariant) { + return false + } + if itemData == nil && isNonContextualKeyword(scanner.StringToToken(symbolName)) { + return false + } + if !isTypeOnlyLocation && importStatementCompletion == nil && (targetFlags&ast.SymbolFlagsValue) == 0 { + return false + } + if isTypeOnlyLocation && (targetFlags&(ast.SymbolFlagsModule|ast.SymbolFlagsType) == 0) { + return false + } + // Do not try to auto-import something with a lowercase first letter for a JSX tag + firstChar := rune(symbolName[0]) + if isRightOfOpenTag && (firstChar < 'A' || firstChar > 'Z') { + return false + } + + if itemData != nil { + return true + } + return charactersFuzzyMatchInString(symbolName, lowerCaseTokenText) + }, + func(info []*SymbolExportInfo, symbolName string, isFromAmbientModule bool, exportMapKey ExportMapInfoKey) []*SymbolExportInfo { + if itemData != nil && !core.Some(info, func(i *SymbolExportInfo) bool { + return stringutil.StripQuotes(i.moduleSymbol.Name) == itemData.Source + }) { + return nil + } + // Do a relatively cheap check to bail early if all re-exports are non-importable + // due to file location or package.json dependency filtering. For non-node16+ + // module resolution modes, getting past this point guarantees that we'll be + // able to generate a suitable module specifier, so we can safely show a completion, + // even if we defer computing the module specifier. + isImportableExportInfo := func(info *SymbolExportInfo) bool { + var toFile *ast.SourceFile + if ast.IsSourceFile(info.moduleSymbol.ValueDeclaration) { + toFile = info.moduleSymbol.ValueDeclaration.AsSourceFile() + } + return l.isImportable( + file, + toFile, + info.moduleSymbol, + preferences, + importSpecifierResolver.packageJsonImportFilter(), + ) + } + info = core.Filter(info, isImportableExportInfo) + if len(info) == 0 { + return nil + } + + // In node16+, module specifier resolution can fail due to modules being blocked + // by package.json `exports`. If that happens, don't show a completion item. + // N.B. in this resolution mode we always try to resolve module specifiers here, + // because we have to know now if it's going to fail so we can omit the completion + // from the list. + result, ok := context.tryResolve(typeChecker, info, isFromAmbientModule) + if ok == "failed" { + return nil + } + + // If we skipped resolving module specifiers, our selection of which ExportInfo + // to use here is arbitrary, since the info shown in the completion list derived from + // it should be identical regardless of which one is used. During the subsequent + // `CompletionEntryDetails` request, we'll get all the ExportInfos again and pick + // the best one based on the module specifier it produces. + exportInfo := info[0] + var moduleSpecifier string + if ok != "skipped" { + if result.Base().exportInfo != nil { + exportInfo = result.Base().exportInfo + } + moduleSpecifier = result.Base().moduleSpecifier + } + + isDefaultExport := exportInfo.exportKind == ExportKindDefault + if exportInfo.symbol == nil { + panic("should have handled `futureExportSymbolInfo` earlier") + } + symbol := exportInfo.symbol + if isDefaultExport { + symbol = binder.GetLocalSymbolForExportDefault(symbol) + } + + // pushAutoImportSymbol + symbolId := ast.GetSymbolId(symbol) + if symbolToSortTextMap[symbolId] != SortTextGlobalsOrKeywords { + // If an auto-importable symbol is available as a global, don't push the auto import + return nil + } + if moduleSpecifier != "" { + symbolToOriginInfoMap[symbolId] = &symbolOriginInfo{ + kind: symbolOriginInfoKindResolvedExport, + isDefaultExport: isDefaultExport, + isFromPackageJson: ptrIsTrue(exportInfo.isFromPackageJson), + fileName: exportInfo.moduleFileName, + data: symbolOriginInfoResolvedExport{ + symbolName: symbolName, + moduleSymbol: exportInfo.moduleSymbol, + exportName: core.IfElse(exportInfo.exportKind == ExportKindExportEquals, ast.InternalSymbolNameExportEquals, exportInfo.symbol.Name), + exportMapKey: exportMapKey, + moduleSpecifier: moduleSpecifier, + }, + } + } + symbolToSortTextMap[symbolId] = core.IfElse(importStatementCompletion != nil, SortTextLocationPriority, SortTextAutoImportSuggestions) + symbols = append(symbols, symbol) + return nil + }, + ) + + hasUnresolvedAutoImports = context.skippedAny + // !!! completionInfoFlags + // flags |= core.IfElse(context.resolvedCount > 0, completionInfoFlagsResolvedModuleSpecifiers, 0) + // flags |= core.IfElse(context.resolvedCount > moduleSpecifierResolutionLimit, completionInfoFlagsResolvedModuleSpecifiersBeyondLimit, 0) + return nil + }, + ) + } + tryGetImportCompletionSymbols := func() globalsSearch { if importStatementCompletion == nil { return globalsSearchContinue } isNewIdentifierLocation = true - // !!! auto imports - // collectAutoImports() + collectAutoImports() return globalsSearchSuccess } @@ -1393,9 +1661,7 @@ func getCompletionData(program *compiler.Program, typeChecker *checker.Checker, } } - // !!! auto imports - // collectAutoImports() - + collectAutoImports() if isTypeOnlyLocation { if contextToken != nil && ast.IsAssertionExpression(contextToken.Parent) { keywordFilters = KeywordCompletionFiltersTypeAssertionKeywords @@ -1528,6 +1794,92 @@ func keywordCompletionData( } } +type resolvingModuleSpecifiersForCompletions struct { + logPrefix string + resolver *importSpecifierResolverForCompletions + position int + isForImportStatementCompletion bool + isValidTypeOnlyUseSite bool + + needsFullResolution bool + skippedAny bool + ambientCount int + resolvedCount int + resolvedFromCacheCount int + cacheAttemptCount int +} + +func (r *resolvingModuleSpecifiersForCompletions) tryResolve(ch *checker.Checker, exportInfo []*SymbolExportInfo, isFromAmbientModule bool) (ImportFix, string) { + if isFromAmbientModule { + if result := r.resolver.getModuleSpecifierForBestExportInfo(ch, exportInfo, r.position, r.isValidTypeOnlyUseSite); result != nil { + r.ambientCount++ + return result, "" + } + return nil, "failed" + } + + allowIncompleteCompletions := ptrIsTrue(r.resolver.UserPreferences.AllowIncompleteCompletions) + shouldResolveModuleSpecifier := r.needsFullResolution || allowIncompleteCompletions && r.resolvedCount < moduleSpecifierResolutionLimit + shouldGetModuleSpecifierFromCache := !shouldResolveModuleSpecifier && allowIncompleteCompletions && r.cacheAttemptCount < moduleSpecifierResolutionCacheAttemptLimit + + var result ImportFix + if shouldResolveModuleSpecifier || shouldGetModuleSpecifierFromCache { + result = r.resolver.getModuleSpecifierForBestExportInfo(ch, exportInfo, r.position, r.isValidTypeOnlyUseSite) + } + + if !shouldResolveModuleSpecifier && !shouldGetModuleSpecifierFromCache || shouldGetModuleSpecifierFromCache && result == nil { + r.skippedAny = true + } + + // r.resolvedCount += result?.computedWithoutCacheCount || 0; + r.resolvedFromCacheCount += len(exportInfo) // - (result?.computedWithoutCacheCount || 0); + if shouldGetModuleSpecifierFromCache { + r.cacheAttemptCount++ + } + + if result != nil { + return result, "" + } + if r.needsFullResolution { + return nil, "failed" + } + return nil, "skipped" +} + +func (l *LanguageService) resolvingModuleSpecifiers( + logPrefix string, + resolver *importSpecifierResolverForCompletions, + position int, + isForImportStatementCompletion bool, + isValidTypeOnlyUseSite bool, + cb func(*resolvingModuleSpecifiersForCompletions) ImportFix, +) ImportFix { + // !!! timestamp + // Under `--moduleResolution nodenext` or `bundler`, we have to resolve module specifiers up front, because + // package.json exports can mean we *can't* resolve a module specifier (that doesn't include a + // relative path into node_modules), and we want to filter those completions out entirely. + // Import statement completions always need specifier resolution because the module specifier is + // part of their `insertText`, not the `codeActions` creating edits away from the cursor. + // Finally, `autoImportSpecifierExcludeRegexes` necessitates eagerly resolving module specifiers + // because completion items are being explcitly filtered out by module specifier. + r := &resolvingModuleSpecifiersForCompletions{ + logPrefix: logPrefix, + resolver: resolver, + position: position, + isForImportStatementCompletion: isForImportStatementCompletion, + isValidTypeOnlyUseSite: isValidTypeOnlyUseSite, + + needsFullResolution: isForImportStatementCompletion || + l.GetProgram().Options().GetResolvePackageJsonExports() || + len(resolver.UserPreferences.AutoImportSpecifierExcludeRegexes) > 0, + } + + result := cb(r) + // !!! logging + + return result +} + func getDefaultCommitCharacters(isNewIdentifierLocation bool) []string { if isNewIdentifierLocation { return []string{} @@ -1601,9 +1953,7 @@ func (l *LanguageService) completionInfoFromData( compareCompletionEntries := getCompareCompletionEntries(ctx) if data.keywordFilters != KeywordCompletionFiltersNone { - keywordCompletions := getKeywordCompletions( - data.keywordFilters, - !data.insideJSDocTagTypeExpression && ast.IsSourceFileJS(file)) + keywordCompletions := getKeywordCompletions(data.keywordFilters, !data.insideJSDocTagTypeExpression && ast.IsSourceFileJS(file)) for _, keywordEntry := range keywordCompletions { if data.isTypeOnlyLocation && isTypeKeyword(scanner.StringToToken(keywordEntry.Label)) || !data.isTypeOnlyLocation && isContextualKeywordInAutoImportableExpressionSpace(keywordEntry.Label) || @@ -1894,11 +2244,42 @@ func (l *LanguageService) createCompletionItem( } if originIsResolvedExport(origin) { + resolvedOrigin := origin.asResolvedExport() labelDetails = &lsproto.CompletionItemLabelDetails{ - Description: &origin.asResolvedExport().moduleSpecifier, // !!! vscode @link support + Description: &resolvedOrigin.moduleSpecifier, // !!! vscode @link support } if data.importStatementCompletion != nil { - // !!! auto-imports + quotedModuleSpecifier := escapeSnippetText(quote(file, preferences, resolvedOrigin.moduleSpecifier)) + exportKind := ExportKindNamed + if origin.isDefaultExport { + exportKind = ExportKindDefault + } else if resolvedOrigin.exportName == ast.InternalSymbolNameExportEquals { + exportKind = ExportKindExportEquals + } + + insertText = "import " + typeOnlyText := scanner.TokenToString(ast.KindTypeKeyword) + " " + if data.importStatementCompletion.isTopLevelTypeOnly { + insertText += typeOnlyText + } + tabStop := core.IfElse(ptrIsTrue(preferences.IncludeCompletionsWithSnippetText), "$1", "") + importKind := getImportKind(file, exportKind, program, true /*forceImportKeyword*/) + escapedSnippet := escapeSnippetText(name) + suffix := core.IfElse(useSemicolons, ";", "") + switch importKind { + case ImportKindCommonJS: + insertText += fmt.Sprintf(`%s%s = require(%s)%s`, escapedSnippet, tabStop, quotedModuleSpecifier, suffix) + case ImportKindDefault: + insertText += fmt.Sprintf(`%s%s from %s%s`, escapedSnippet, tabStop, quotedModuleSpecifier, suffix) + case ImportKindNamespace: + insertText += fmt.Sprintf(`* as %s from %s%s`, escapedSnippet, quotedModuleSpecifier, suffix) + case ImportKindNamed: + importSpecifierTypeOnlyText := core.IfElse(data.importStatementCompletion.couldBeTypeOnlyImportSpecifier, typeOnlyText, "") + insertText += fmt.Sprintf(`{ %s%s%s } from %s%s`, importSpecifierTypeOnlyText, escapedSnippet, tabStop, quotedModuleSpecifier, suffix) + } + + replacementSpan = data.importStatementCompletion.replacementSpan + isSnippet = ptrIsTrue(preferences.IncludeCompletionsWithSnippetText) } } @@ -1985,10 +2366,14 @@ func (l *LanguageService) createCompletionItem( } } + if insertText != "" && !ptrIsTrue(preferences.IncludeCompletionsWithInsertText) { + return nil + } + + var autoImportData *completionEntryData if originIsExport(origin) || originIsResolvedExport(origin) { - // !!! auto-imports - // data = originToCompletionEntryData(origin) - // hasAction = importStatementCompletion == nil + autoImportData = origin.toCompletionEntryData() + hasAction = data.importStatementCompletion == nil } parentNamedImportOrExport := ast.FindAncestor(data.location, isNamedImportsOrExports) @@ -2049,6 +2434,7 @@ func (l *LanguageService) createCompletionItem( hasAction, preselect, source, + autoImportData, ) } @@ -2908,9 +3294,9 @@ func generateIdentifierForArbitraryString(text string) string { if size > 0 && validChar { if needsUnderscore { identifier += "_" - identifier += string(ch) - needsUnderscore = false } + identifier += string(ch) + needsUnderscore = false } else { needsUnderscore = true } @@ -2994,23 +3380,68 @@ func getCompareCompletionEntries(ctx context.Context) func(entryInSlice *lsproto if result == stringutil.ComparisonEqual { result = compareStrings(entryInSlice.Label, entryToInsert.Label) } - // !!! auto-imports - // if (result === Comparison.EqualTo && entryInArray.data?.moduleSpecifier && entryToInsert.data?.moduleSpecifier) { - // // Sort same-named auto-imports by module specifier - // result = compareNumberOfDirectorySeparators( - // (entryInArray.data as CompletionEntryDataResolved).moduleSpecifier, - // (entryToInsert.data as CompletionEntryDataResolved).moduleSpecifier, - // ); - // } + if result == stringutil.ComparisonEqual && entryInSlice.Data != nil && entryToInsert.Data != nil { + sliceEntryData, ok1 := (*entryInSlice.Data).(*completionEntryData) + insertEntryData, ok2 := (*entryToInsert.Data).(*completionEntryData) + if ok1 && ok2 && sliceEntryData.moduleSpecifier != "" && insertEntryData.moduleSpecifier != "" { + // Sort same-named auto-imports by module specifier + result = compareNumberOfDirectorySeparators( + sliceEntryData.moduleSpecifier, + insertEntryData.moduleSpecifier, + ) + } + } if result == stringutil.ComparisonEqual { // Fall back to symbol order - if we return `EqualTo`, `insertSorted` will put later symbols first. return stringutil.ComparisonLessThan } - return result } } +// True if the first character of `lowercaseCharacters` is the first character +// of some "word" in `identiferString` (where the string is split into "words" +// by camelCase and snake_case segments), then if the remaining characters of +// `lowercaseCharacters` appear, in order, in the rest of `identifierString`.// +// True: +// 'state' in 'useState' +// 'sae' in 'useState' +// 'viable' in 'ENVIRONMENT_VARIABLE'// +// False: +// 'staet' in 'useState' +// 'tate' in 'useState' +// 'ment' in 'ENVIRONMENT_VARIABLE' +func charactersFuzzyMatchInString(identifierString string, lowercaseCharacters string) bool { + if lowercaseCharacters == "" { + return true + } + + var prevChar *rune + matchedFirstCharacter := false + characterIndex := 0 + length := len(identifierString) + for strIndex := 0; strIndex < length; strIndex++ { + strChar := rune(identifierString[strIndex]) + testChar := rune(lowercaseCharacters[strIndex]) + if strChar == testChar || strChar == unicode.ToUpper(testChar) { + willMatchFirstChar := prevChar == nil || // Beginning of word + 'a' <= *prevChar && *prevChar <= 'z' && 'A' <= strChar && strChar <= 'Z' || // camelCase transition + *prevChar == '_' && strChar != '_' // snake_case transition + matchedFirstCharacter = matchedFirstCharacter || willMatchFirstChar + if matchedFirstCharacter { + characterIndex++ + } + if characterIndex == len(lowercaseCharacters) { + return true + } + } + prevChar = ptrTo(strChar) + } + + // Did not find all characters + return false +} + var ( keywordCompletionsCache = collections.SyncMap[KeywordCompletionFilters, []*lsproto.CompletionItem]{} allKeywordCompletions = sync.OnceValue(func() []*lsproto.CompletionItem { @@ -4106,6 +4537,7 @@ func (l *LanguageService) getJsxClosingTagCompletion( false, /*hasAction*/ false, /*preselect*/ "", /*source*/ + nil, /*autoImportEntryData*/ // !!! jsx autoimports ) items := []*lsproto.CompletionItem{item} itemDefaults := l.setItemDefaults( @@ -4142,6 +4574,7 @@ func (l *LanguageService) createLSPCompletionItem( hasAction bool, preselect bool, source string, + autoImportEntryData *completionEntryData, ) *lsproto.CompletionItem { kind := getCompletionsSymbolKind(elementKind) var data any = &itemData{ @@ -4149,7 +4582,7 @@ func (l *LanguageService) createLSPCompletionItem( Position: position, Source: source, Name: name, - AutoImport: nil, // !!! auto-imports + AutoImport: autoImportEntryData, } // Text edit @@ -4290,6 +4723,7 @@ func (l *LanguageService) getLabelStatementCompletions( false, /*hasAction*/ false, /*preselect*/ "", /*source*/ + nil, /*autoImportEntryData*/ )) } } @@ -4527,6 +4961,11 @@ func hasCompletionItem(clientOptions *lsproto.CompletionClientCapabilities) bool return clientOptions != nil && clientOptions.CompletionItem != nil } +// strada TODO: this function is, at best, poorly named. Use sites are pretty suspicious. +func compilerOptionsIndicateEsModules(options *core.CompilerOptions) bool { + return options.Module == core.ModuleKindNone || options.GetEmitScriptTarget() >= core.ScriptTargetES2015 || options.NoEmit.IsTrue() +} + func clientSupportsItemLabelDetails(clientOptions *lsproto.CompletionClientCapabilities) bool { return hasCompletionItem(clientOptions) && ptrIsTrue(clientOptions.CompletionItem.LabelDetailsSupport) } @@ -4576,15 +5015,67 @@ func getArgumentInfoForCompletions(node *ast.Node, position int, file *ast.Sourc } type itemData struct { - FileName string `json:"fileName"` - Position int `json:"position"` - Source string `json:"source,omitzero"` - Name string `json:"name,omitzero"` - AutoImport *autoImportData `json:"autoImport,omitzero"` + FileName string `json:"fileName"` + Position int `json:"position"` + Source string `json:"source,omitempty"` + Name string `json:"name,omitempty"` + AutoImport *completionEntryData `json:"autoImport,omitempty"` +} + +type completionEntryDataKind int + +const ( + completionEntryDataKindNone completionEntryDataKind = 0 + + // Currently, only completions that use autoImports can have completionEntryData + completionEntryDataKindAutoImportResolved completionEntryDataKind = 1 + completionEntryDataKindAutoImportUnresolved completionEntryDataKind = 2 +) + +type completionEntryData struct { + kind completionEntryDataKind + + /** + * The name of the property or export in the module's symbol table. Differs from the completion name + * in the case of InternalSymbolName.ExportEquals and InternalSymbolName.Default. + */ + exportName string + exportMapKey ExportMapInfoKey // required if kind==unresolved + moduleSpecifier string // required if kind==resolved + + /** The file name declaring the export's module symbol, if it was an external module */ + fileName *string + /** The module name (with quotes stripped) of the export's module symbol, if it was an ambient module */ + ambientModuleName *string + + /** True if the export was found in the package.json AutoImportProvider */ + isPackageJsonImport core.Tristate +} + +func (d *completionEntryData) IsResolved() bool { + return d.moduleSpecifier != "" && d.kind == completionEntryDataKindAutoImportResolved +} + +func (d *completionEntryData) toSymbolOriginExport(symbolName string, moduleSymbol *ast.Symbol, isDefaultExport bool) *symbolOriginInfoExport { + // Unresolved Export Origin information + return &symbolOriginInfoExport{ + symbolName: symbolName, + moduleSymbol: moduleSymbol, + exportName: d.exportName, + exportMapKey: d.exportMapKey, + } } -// !!! CompletionEntryDataAutoImport -type autoImportData struct{} +func (d *completionEntryData) toSymbolOriginResolvedExport(symbolName string, moduleSymbol *ast.Symbol, isDefaultExport bool) *symbolOriginInfoResolvedExport { + // Resolved Export Origin information + return &symbolOriginInfoResolvedExport{ + symbolName: symbolName, + moduleSymbol: moduleSymbol, + exportName: d.exportName, + exportMapKey: d.exportMapKey, + moduleSpecifier: d.moduleSpecifier, + } +} // Special values for `CompletionInfo['source']` used to disambiguate // completion items with the same `name`. (Each completion item must @@ -4668,7 +5159,7 @@ func (l *LanguageService) getCompletionItemDetails( } // Compute all the completion symbols again. - symbolCompletion := getSymbolCompletionFromItemData( + symbolCompletion := l.getSymbolCompletionFromItemData( program, checker, file, @@ -4689,7 +5180,7 @@ func (l *LanguageService) getCompletionItemDetails( return nil case symbolCompletion.symbol != nil: symbolDetails := symbolCompletion.symbol - actions := getCompletionItemActions(symbolDetails.symbol) + actions := l.getCompletionItemActions(ctx, checker, file, position, itemData, symbolDetails, preferences) return createCompletionDetailsForSymbol( item, symbolDetails.symbol, @@ -4731,9 +5222,9 @@ type symbolDetails struct { isTypeOnlyLocation bool } -func getSymbolCompletionFromItemData( +func (l *LanguageService) getSymbolCompletionFromItemData( program *compiler.Program, - checker *checker.Checker, + ch *checker.Checker, file *ast.SourceFile, position int, itemData *itemData, @@ -4746,11 +5237,16 @@ func getSymbolCompletionFromItemData( } } if itemData.AutoImport != nil { - // !!! auto-import - return detailsData{} + if autoImportSymbolData := l.getAutoImportSymbolFromCompletionEntryData(ch, itemData.AutoImport.exportName, itemData.AutoImport); autoImportSymbolData != nil { + autoImportSymbolData.contextToken, autoImportSymbolData.previousToken = getRelevantTokens(position, file) + autoImportSymbolData.location = astnav.GetTouchingPropertyName(file, position) + autoImportSymbolData.jsxInitializer = jsxInitializer{false, nil} + autoImportSymbolData.isTypeOnlyLocation = false + return detailsData{symbol: autoImportSymbolData} + } } - completionData := getCompletionData(program, checker, file, position, preferences) + completionData := l.getCompletionData(program, ch, file, position, itemData, &UserPreferences{IncludeCompletionsForModuleExports: ptrTo(true), IncludeCompletionsWithInsertText: ptrTo(true)}) if completionData == nil { return detailsData{} } @@ -4805,6 +5301,54 @@ func getSymbolCompletionFromItemData( return detailsData{} } +func (l *LanguageService) getAutoImportSymbolFromCompletionEntryData(ch *checker.Checker, name string, autoImportData *completionEntryData) *symbolDetails { + containingProgram := l.GetPackageJsonAutoImportProvider() + var moduleSymbol *ast.Symbol + if autoImportData.ambientModuleName != nil { + moduleSymbol = ch.TryFindAmbientModule(*autoImportData.ambientModuleName) + } else if autoImportData.fileName != nil { + moduleSymbolSourceFile := containingProgram.GetSourceFile(*autoImportData.fileName) + if moduleSymbolSourceFile == nil { + panic("module sourceFile not found: " + *autoImportData.fileName) + } + moduleSymbol = ch.GetMergedSymbol(moduleSymbolSourceFile.Symbol) + } + if moduleSymbol == nil { + return nil + } + + var symbol *ast.Symbol + if autoImportData.exportName == ast.InternalSymbolNameExportEquals { + symbol = ch.ResolveExternalModuleSymbol(moduleSymbol) + } else { + symbol = ch.TryGetMemberInModuleExportsAndProperties(autoImportData.exportName, moduleSymbol) + } + if symbol == nil { + return nil + } + + isDefaultExport := autoImportData.exportName == ast.InternalSymbolNameDefault + if isDefaultExport { + if localSymbol := binder.GetLocalSymbolForExportDefault(symbol); localSymbol != nil { + symbol = localSymbol + } + } + origin := &symbolOriginInfo{ + isFromPackageJson: autoImportData.isPackageJsonImport.IsTrue(), + isDefaultExport: isDefaultExport, + } + if autoImportData.fileName != nil { + origin.fileName = *autoImportData.fileName + origin.kind = symbolOriginInfoKindResolvedExport + origin.data = autoImportData.toSymbolOriginResolvedExport(name, moduleSymbol, isDefaultExport) + } else { + origin.kind = symbolOriginInfoKindExport + origin.data = autoImportData.toSymbolOriginExport(name, moduleSymbol, isDefaultExport) + } + + return &symbolDetails{symbol: symbol, origin: origin} +} + func createSimpleDetails( item *lsproto.CompletionItem, name string, @@ -4855,8 +5399,226 @@ func createCompletionDetailsForSymbol( return createCompletionDetails(item, strings.Join(details, "\n\n"), documentation) } -// !!! auto-import // !!! snippets -func getCompletionItemActions(symbol *ast.Symbol) []codeAction { +func (l *LanguageService) getCompletionItemActions(ctx context.Context, ch *checker.Checker, file *ast.SourceFile, position int, itemData *itemData, symbolDetails *symbolDetails, preferences *UserPreferences) []codeAction { + if itemData.AutoImport.moduleSpecifier != "" && symbolDetails.previousToken != nil { + // Import statement completion: 'import c|' + if symbolDetails.contextToken != nil && l.getImportStatementCompletionInfo(symbolDetails.contextToken, file).replacementSpan != nil { + return nil + } else if l.getImportStatementCompletionInfo(symbolDetails.previousToken, file).replacementSpan != nil { + return nil // !!! sourceDisplay [textPart(data.moduleSpecifier)] + } + } + // !!! CompletionSource.ClassMemberSnippet + // !!! origin.isTypeOnlyAlias + // entryId.source == CompletionSourceObjectLiteralMemberWithComma && contextToken + + if symbolDetails.origin == nil { + return nil + } + + symbol := symbolDetails.symbol + if symbol.ExportSymbol != nil { + symbol = symbol.ExportSymbol + } + targetSymbol := ch.GetMergedSymbol(ch.SkipAlias(symbol)) + isJsxOpeningTagName := symbolDetails.contextToken.Kind == ast.KindLessThanToken && ast.IsJsxOpeningLikeElement(symbolDetails.contextToken.Parent) + if symbolDetails.previousToken != nil && ast.IsIdentifier(symbolDetails.previousToken) { + // If the previous token is an identifier, we can use its start position. + position = astnav.GetStartOfNode(symbolDetails.previousToken, file, false) + } + + moduleSymbol := symbolDetails.origin.moduleSymbol() + + _, importCompletionAction := l.getImportCompletionAction( + ctx, + ch, + targetSymbol, + moduleSymbol, + file, + position, + "", // entryId.data.exportMapKey + symbol.Name, // entryId.data.name, + isJsxOpeningTagName, + // formatContext, + preferences, + ) + // Debug.assert(!data.moduleSpecifier || moduleSpecifier == data.moduleSpecifier); + return []codeAction{importCompletionAction} +} + +func (l *LanguageService) getImportStatementCompletionInfo(contextToken *ast.Node, sourceFile *ast.SourceFile) importStatementCompletionInfo { + result := importStatementCompletionInfo{} + var candidate *ast.Node + parent := contextToken.Parent + switch { + case ast.IsImportEqualsDeclaration(parent): + // import Foo | + // import Foo f| + lastToken := lsutil.GetLastToken(parent, sourceFile) + if contextToken.Kind == ast.KindIdentifier && lastToken != contextToken { + result.keywordCompletion = ast.KindFromKeyword + result.isKeywordOnlyCompletion = true + } else { + if contextToken.Kind != ast.KindTypeKeyword { + result.keywordCompletion = ast.KindTypeKeyword + } + if isModuleSpecifierMissingOrEmpty(parent.AsImportEqualsDeclaration().ModuleReference) { + candidate = parent + } + } + + case couldBeTypeOnlyImportSpecifier(parent, contextToken) && canCompleteFromNamedBindings(parent.Parent): + candidate = parent + case ast.IsNamedImports(parent) || ast.IsNamespaceImport(parent): + if !parent.Parent.IsTypeOnly() && (contextToken.Kind == ast.KindOpenBraceToken || + contextToken.Kind == ast.KindImportKeyword || + contextToken.Kind == ast.KindCommaToken) { + result.keywordCompletion = ast.KindTypeKeyword + } + if canCompleteFromNamedBindings(parent) { + // At `import { ... } |` or `import * as Foo |`, the only possible completion is `from` + if contextToken.Kind == ast.KindCloseBraceToken || contextToken.Kind == ast.KindIdentifier { + result.isKeywordOnlyCompletion = true + result.keywordCompletion = ast.KindFromKeyword + } else { + candidate = parent.Parent.Parent + } + } + + case ast.IsExportDeclaration(parent) && contextToken.Kind == ast.KindAsteriskToken, + ast.IsNamedExports(parent) && contextToken.Kind == ast.KindCloseBraceToken: + result.isKeywordOnlyCompletion = true + result.keywordCompletion = ast.KindFromKeyword + + case contextToken.Kind == ast.KindImportKeyword: + if ast.IsSourceFile(parent) { + // A lone import keyword with nothing following it does not parse as a statement at all + result.keywordCompletion = ast.KindTypeKeyword + candidate = contextToken + } else if ast.IsImportDeclaration(parent) { + // `import s| from` + result.keywordCompletion = ast.KindTypeKeyword + if isModuleSpecifierMissingOrEmpty(parent.ModuleSpecifier()) { + candidate = parent + } + } + } + + if candidate != nil { + result.isNewIdentifierLocation = true + result.replacementSpan = l.getSingleLineReplacementSpanForImportCompletionNode(candidate) + result.couldBeTypeOnlyImportSpecifier = couldBeTypeOnlyImportSpecifier(candidate, contextToken) + if ast.IsImportDeclaration(candidate) { + result.isTopLevelTypeOnly = candidate.AsImportDeclaration().ImportClause.IsTypeOnly() + } else if candidate.Kind == ast.KindImportEqualsDeclaration { + result.isTopLevelTypeOnly = candidate.IsTypeOnly() + } + } else { + result.isNewIdentifierLocation = result.keywordCompletion == ast.KindTypeKeyword + } + return result +} + +func (l *LanguageService) getSingleLineReplacementSpanForImportCompletionNode(node *ast.Node) *lsproto.Range { + // node is ImportDeclaration | ImportEqualsDeclaration | ImportSpecifier | JSDocImportTag | Token + if ancestor := ast.FindAncestor(node, core.Or(ast.IsImportDeclaration, ast.IsImportEqualsDeclaration, ast.IsJSDocImportTag)); ancestor != nil { + node = ancestor + } + sourceFile := ast.GetSourceFileOfNode(node) + if printer.GetLinesBetweenPositions(sourceFile, node.Pos(), node.End()) == 0 { + return l.createLspRangeFromNode(node, sourceFile) + } + + if node.Kind == ast.KindImportKeyword || node.Kind == ast.KindImportSpecifier { + panic("ImportKeyword was necessarily on one line; ImportSpecifier was necessarily parented in an ImportDeclaration") + } + + // Guess which point in the import might actually be a later statement parsed as part of the import + // during parser recovery - either in the middle of named imports, or the module specifier. + var potentialSplitPoint *ast.Node + if node.Kind == ast.KindImportDeclaration || node.Kind == ast.KindJSDocImportTag { + var specifier *ast.Node + if importClause := node.ImportClause(); importClause != nil { + specifier = getPotentiallyInvalidImportSpecifier(importClause.AsImportClause().NamedBindings) + } + if specifier != nil { + potentialSplitPoint = specifier + } else { + potentialSplitPoint = node.ModuleSpecifier() + } + } else { + potentialSplitPoint = node.AsImportEqualsDeclaration().ModuleReference + } + + withoutModuleSpecifier := core.NewTextRange(scanner.GetTokenPosOfNode(lsutil.GetFirstToken(node, sourceFile), sourceFile, false), potentialSplitPoint.Pos()) + // The module specifier/reference was previously found to be missing, empty, or + // not a string literal - in this last case, it's likely that statement on a following + // line was parsed as the module specifier of a partially-typed import, e.g. + // import Foo| + // interface Blah {} + // This appears to be a multiline-import, and editors can't replace multiple lines. + // But if everything but the "module specifier" is on one line, by this point we can + // assume that the "module specifier" is actually just another statement, and return + // the single-line range of the import excluding that probable statement. + if printer.GetLinesBetweenPositions(sourceFile, withoutModuleSpecifier.Pos(), withoutModuleSpecifier.End()) == 0 { + return l.createLspRangeFromBounds(withoutModuleSpecifier.Pos(), withoutModuleSpecifier.End(), sourceFile) + } return nil } + +func couldBeTypeOnlyImportSpecifier(importSpecifier *ast.Node, contextToken *ast.Node) bool { + return ast.IsImportSpecifier(importSpecifier) && (importSpecifier.IsTypeOnly() || contextToken == importSpecifier.Name() && isTypeKeywordTokenOrIdentifier(contextToken)) +} + +func canCompleteFromNamedBindings(namedBindings *ast.NamedImportBindings) bool { + if !isModuleSpecifierMissingOrEmpty(namedBindings.Parent.Parent.ModuleSpecifier()) || namedBindings.Parent.Name() != nil { + return false + } + if ast.IsNamedImports(namedBindings) { + // We can only complete on named imports if there are no other named imports already, + // but parser recovery sometimes puts later statements in the named imports list, so + // we try to only consider the probably-valid ones. + invalidNamedImport := getPotentiallyInvalidImportSpecifier(namedBindings) + elements := namedBindings.Elements() + validImports := len(elements) + if invalidNamedImport != nil { + validImports = slices.Index(elements, invalidNamedImport) + } + + return validImports < 2 && validImports > -1 + } + return true +} + +// Tries to identify the first named import that is not really a named import, but rather +// just parser recovery for a situation like: +// +// import { Foo| +// interface Bar {} +// +// in which `Foo`, `interface`, and `Bar` are all parsed as import specifiers. The caller +// will also check if this token is on a separate line from the rest of the import. +func getPotentiallyInvalidImportSpecifier(namedBindings *ast.NamedImportBindings) *ast.Node { + if namedBindings.Kind != ast.KindNamedImports { + return nil + } + return core.Find(namedBindings.Elements(), func(e *ast.Node) bool { + return e.PropertyName() == nil && isNonContextualKeyword(scanner.StringToToken(e.Name().Text())) && + astnav.FindPrecedingToken(ast.GetSourceFileOfNode(namedBindings), e.Name().Pos()).Kind != ast.KindCommaToken + }) +} + +func isModuleSpecifierMissingOrEmpty(specifier *ast.Expression) bool { + if ast.NodeIsMissing(specifier) { + return true + } + node := specifier + if ast.IsExternalModuleReference(node) { + node = node.Expression() + } + if !ast.IsStringLiteralLike(node) { + return true + } + return node.Text() == "" +} diff --git a/internal/ls/constants.go b/internal/ls/constants.go new file mode 100644 index 0000000000..60f240b203 --- /dev/null +++ b/internal/ls/constants.go @@ -0,0 +1,6 @@ +package ls + +const ( + moduleSpecifierResolutionLimit = 100 + moduleSpecifierResolutionCacheAttemptLimit = 1000 +) diff --git a/internal/ls/findallreferences.go b/internal/ls/findallreferences.go index 51c2c48d50..962618ba46 100644 --- a/internal/ls/findallreferences.go +++ b/internal/ls/findallreferences.go @@ -987,14 +987,6 @@ func getReferencedSymbolsForSymbol(originalSymbol *ast.Symbol, node *ast.Node, s return state.result } -type ExportKind int - -const ( - ExportKindDefault ExportKind = 0 - ExportKindNamed ExportKind = 1 - ExportKindExportEquals ExportKind = 2 -) - type ExportInfo struct { exportingModuleSymbol *ast.Symbol exportKind ExportKind diff --git a/internal/ls/languageservice.go b/internal/ls/languageservice.go index 5d604eee3c..fccc3e69ac 100644 --- a/internal/ls/languageservice.go +++ b/internal/ls/languageservice.go @@ -23,6 +23,11 @@ func (l *LanguageService) GetProgram() *compiler.Program { return l.host.GetProgram() } +func (l *LanguageService) GetPackageJsonAutoImportProvider() *compiler.Program { + // !!! packageJsonAutoImportProvider + return l.GetProgram() +} + func (l *LanguageService) tryGetProgramAndFile(fileName string) (*compiler.Program, *ast.SourceFile) { program := l.GetProgram() file := program.GetSourceFile(fileName) diff --git a/internal/ls/organizeimports.go b/internal/ls/organizeimports.go new file mode 100644 index 0000000000..91dc11a3c4 --- /dev/null +++ b/internal/ls/organizeimports.go @@ -0,0 +1,129 @@ +package ls + +import ( + "cmp" + "strings" + + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/compiler" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/modulespecifiers" + "github.com/microsoft/typescript-go/internal/tspath" +) + +// statement = anyImportOrRequireStatement +func getImportDeclarationInsertIndex(sortedImports []*ast.Statement, newImport *ast.Statement, comparer func(a, b *ast.Statement) int) int { + // !!! + return len(sortedImports) +} + +// returns `-1` if `a` is better than `b` +// +// note: this sorts in descending order of preference; different than convention in other cmp-like functions +func compareModuleSpecifiers( + a ImportFix, // !!! ImportFixWithModuleSpecifier + b ImportFix, // !!! ImportFixWithModuleSpecifier + importingFile *ast.SourceFile, // | FutureSourceFile, + program *compiler.Program, + preferences UserPreferences, + allowsImportingSpecifier func(specifier string) bool, + toPath func(fileName string) tspath.Path, +) int { + if a.Kind() == ImportFixKindUseNamespace || b.Kind() == ImportFixKindUseNamespace { + return 0 + } + if comparison := compareBooleans( + b.Base().moduleSpecifierKind != modulespecifiers.ResultKindNodeModules || allowsImportingSpecifier(b.Base().moduleSpecifier), + a.Base().moduleSpecifierKind != modulespecifiers.ResultKindNodeModules || allowsImportingSpecifier(a.Base().moduleSpecifier), + ); comparison != 0 { + return comparison + } + if comparison := compareModuleSpecifierRelativity(a, b, preferences); comparison != 0 { + return comparison + } + if comparison := compareNodeCoreModuleSpecifiers(a.Base().moduleSpecifier, b.Base().moduleSpecifier, importingFile, program); comparison != 0 { + return comparison + } + if comparison := compareBooleans(isFixPossiblyReExportingImportingFile(a, importingFile.Path(), toPath), isFixPossiblyReExportingImportingFile(b, importingFile.Path(), toPath)); comparison != 0 { + return comparison + } + if comparison := compareNumberOfDirectorySeparators(a.Base().moduleSpecifier, b.Base().moduleSpecifier); comparison != 0 { + return comparison + } + return 0 +} + +// True > False +func compareBooleans(a, b bool) int { + if a && !b { + return -1 + } else if !a && b { + return 1 + } + return 0 +} + +// returns `-1` if `a` is better than `b` +func compareModuleSpecifierRelativity(a ImportFix, b ImportFix, preferences UserPreferences) int { + switch preferences.ImportModuleSpecifierPreference { + case modulespecifiers.ImportModuleSpecifierPreferenceNonRelative, modulespecifiers.ImportModuleSpecifierPreferenceProjectRelative: + return compareBooleans(a.Base().moduleSpecifierKind == modulespecifiers.ResultKindRelative, b.Base().moduleSpecifierKind == modulespecifiers.ResultKindRelative) + } + return 0 +} + +func compareNodeCoreModuleSpecifiers(a, b string, importingFile *ast.SourceFile, program *compiler.Program) int { + if strings.HasPrefix(a, "node:") && !strings.HasPrefix(b, "node:") { + if shouldUseUriStyleNodeCoreModules(importingFile, program) { + return -1 + } + return 1 + } + if strings.HasPrefix(b, "node:") && !strings.HasPrefix(a, "node:") { + if shouldUseUriStyleNodeCoreModules(importingFile, program) { + return 1 + } + return -1 + } + return 0 +} + +func shouldUseUriStyleNodeCoreModules(file *ast.SourceFile, program *compiler.Program) bool { + for _, node := range file.Imports() { + if core.NodeCoreModules()[node.Text()] && !core.ExclusivelyPrefixedNodeCoreModules[node.Text()] { + if strings.HasPrefix(node.Text(), "node:") { + return true + } else { + return false + } + } + } + + return program.UsesUriStyleNodeCoreModules() +} + +// 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". +func isFixPossiblyReExportingImportingFile(fix ImportFix, importingFilePath tspath.Path, toPath func(fileName string) tspath.Path) bool { + base := fix.Base() + if base.isReExport != nil && *(base.isReExport) && + base.exportInfo != nil && base.exportInfo.moduleFileName != "" && isIndexFileName(base.exportInfo.moduleFileName) { + reExportDir := toPath(tspath.GetDirectoryPath(base.exportInfo.moduleFileName)) + return strings.HasPrefix(string(importingFilePath), string(reExportDir)) + } + return false +} + +func compareNumberOfDirectorySeparators(path1, path2 string) int { + return cmp.Compare(strings.Count(path1, "/"), strings.Count(path2, "/")) +} + +func isIndexFileName(fileName string) bool { + fileName = tspath.GetBaseFileName(fileName) + if tspath.FileExtensionIsOneOf(fileName, []string{".js", ".jsx", ".d.ts", ".ts", ".tsx"}) { + fileName = tspath.RemoveFileExtension(fileName) + } + return fileName == "index" +} diff --git a/internal/ls/string_completions.go b/internal/ls/string_completions.go index 6af4212224..78ceae2a32 100644 --- a/internal/ls/string_completions.go +++ b/internal/ls/string_completions.go @@ -161,6 +161,7 @@ func (l *LanguageService) convertStringLiteralCompletions( false, /*hasAction*/ false, /*preselect*/ "", /*source*/ + nil, /*autoImportEntryData*/ ) }) defaultCommitCharacters := getDefaultCommitCharacters(completion.isNewIdentifier) @@ -210,6 +211,7 @@ func (l *LanguageService) convertPathCompletions( false, /*hasAction*/ false, /*preselect*/ "", /*source*/ + nil, /*autoImportEntryData*/ ) }) itemDefaults := l.setItemDefaults( diff --git a/internal/ls/types.go b/internal/ls/types.go index 226202d504..cf37bb8bd6 100644 --- a/internal/ls/types.go +++ b/internal/ls/types.go @@ -2,6 +2,7 @@ package ls import ( "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/modulespecifiers" ) type Location struct { @@ -18,15 +19,26 @@ const ( ) type UserPreferences struct { + // If enabled, TypeScript will search through all external modules' exports and add them to the completions list. + // This affects lone identifier completions but not completions on the right hand side of `obj.`. + IncludeCompletionsForModuleExports *bool + // Enables auto-import-style completions on partially-typed import statements. E.g., allows // `import write|` to be completed to `import { writeFile } from "fs"`. IncludeCompletionsForImportStatements *bool + // Allows completions to be formatted with snippet text, indicated by `CompletionItem["isSnippet"]`. + IncludeCompletionsWithSnippetText *bool + // Unless this option is `false`, member completion lists triggered with `.` will include entries // on potentially-null and potentially-undefined values, with insertion text to replace // preceding `.` tokens with `?.`. IncludeAutomaticOptionalChainCompletions *bool + // If enabled, the completion list will include completions with invalid identifier names. + // For those entries, The `insertText` and `replacementSpan` properties will be set to change from `.x` property access to `["x"]`. + IncludeCompletionsWithInsertText *bool + // If enabled, completions for class members (e.g. methods and properties) will include // a whole declaration for the member. // E.g., `class A { f| }` could be completed to `class A { foo(): number {} }`, instead of @@ -40,4 +52,19 @@ type UserPreferences struct { IncludeCompletionsWithObjectLiteralMethodSnippets *bool JsxAttributeCompletionStyle *JsxAttributeCompletionStyle + + ImportModuleSpecifierPreference modulespecifiers.ImportModuleSpecifierPreference + ImportModuleSpecifierEndingPreference modulespecifiers.ImportModuleSpecifierEndingPreference + PreferTypeOnlyAutoImports *bool + AllowIncompleteCompletions *bool + AutoImportSpecifierExcludeRegexes []string + AutoImportFileExcludePatterns []string +} + +func (p *UserPreferences) ModuleSpecifierPreferences() modulespecifiers.UserPreferences { + return modulespecifiers.UserPreferences{ + ImportModuleSpecifierPreference: p.ImportModuleSpecifierPreference, + ImportModuleSpecifierEndingPreference: p.ImportModuleSpecifierEndingPreference, + AutoImportSpecifierExcludeRegexes: p.AutoImportSpecifierExcludeRegexes, + } } diff --git a/internal/ls/utilities.go b/internal/ls/utilities.go index 3cd9ee217b..93f45c0f5f 100644 --- a/internal/ls/utilities.go +++ b/internal/ls/utilities.go @@ -6,6 +6,7 @@ import ( "iter" "slices" "strings" + "unicode" "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/astnav" @@ -17,6 +18,7 @@ import ( "github.com/microsoft/typescript-go/internal/lsutil" "github.com/microsoft/typescript-go/internal/scanner" "github.com/microsoft/typescript-go/internal/stringutil" + "github.com/microsoft/typescript-go/internal/tspath" ) // Implements a cmp.Compare like function for two lsproto.Position @@ -39,6 +41,13 @@ func CompareRanges(lsRange, other *lsproto.Range) int { return ComparePositions(lsRange.End, other.End) } +func applyTextChanges(text string, changes []core.TextChange) string { + for i := len(changes) - 1; i >= 0; i-- { + text = changes[i].ApplyTo(text) + } + return text +} + var quoteReplacer = strings.NewReplacer("'", `\'`, `\"`, `"`) func IsInString(sourceFile *ast.SourceFile, position int, previousToken *ast.Node) bool { @@ -109,6 +118,45 @@ func getNonModuleSymbolOfMergedModuleSymbol(symbol *ast.Symbol) *ast.Symbol { return nil } +func moduleSymbolToValidIdentifier(moduleSymbol *ast.Symbol, target core.ScriptTarget, forceCapitalize bool) string { + return moduleSpecifierToValidIdentifier(stringutil.StripQuotes(moduleSymbol.Name), target, forceCapitalize) +} + +func moduleSpecifierToValidIdentifier(moduleSpecifier string, target core.ScriptTarget, forceCapitalize bool) string { + baseName := tspath.GetBaseFileName(strings.TrimSuffix(tspath.RemoveFileExtension(moduleSpecifier), "/index")) + res := []rune{} + lastCharWasValid := true + baseNameRunes := []rune(baseName) + if scanner.IsIdentifierStart(baseNameRunes[0]) { + if forceCapitalize { + res = append(res, unicode.ToUpper(baseNameRunes[0])) + } else { + res = append(res, baseNameRunes[0]) + } + } else { + lastCharWasValid = false + } + + for i := 1; i < len(baseNameRunes); i++ { + isValid := scanner.IsIdentifierPart(baseNameRunes[i]) + if isValid { + if !lastCharWasValid { + res = append(res, unicode.ToUpper(baseNameRunes[i])) + } else { + res = append(res, baseNameRunes[i]) + } + } + lastCharWasValid = isValid + } + + // Need `"_"` to ensure result isn't empty. + resString := string(res) + if resString != "" && !isNonContextualKeyword(scanner.StringToToken(resString)) { + return resString + } + return "_" + resString +} + func getLocalSymbolForExportSpecifier(referenceLocation *ast.Identifier, referenceSymbol *ast.Symbol, exportSpecifier *ast.ExportSpecifier, ch *checker.Checker) *ast.Symbol { if isExportSpecifierAlias(referenceLocation, exportSpecifier) { if symbol := ch.GetExportSpecifierLocalTargetSymbol(exportSpecifier.AsNode()); symbol != nil { @@ -513,6 +561,10 @@ func isTypeKeyword(kind ast.Kind) bool { return typeKeywords.Has(kind) } +func isSeparator(node *ast.Node, candidate *ast.Node) bool { + return candidate != nil && node.Parent != nil && (candidate.Kind == ast.KindCommaToken || (candidate.Kind == ast.KindSemicolonToken && node.Parent.Kind == ast.KindObjectLiteralExpression)) +} + // Returns a map of all names in the file to their positions. // !!! cache this func getNameTable(file *ast.SourceFile) map[string]int { diff --git a/internal/module/resolver.go b/internal/module/resolver.go index 680a44d551..35c75f4df1 100644 --- a/internal/module/resolver.go +++ b/internal/module/resolver.go @@ -402,7 +402,7 @@ func (r *resolutionState) resolveNodeLike() *ResolvedModule { } r.diagnostics = r.diagnostics[:diagnosticsCount] } - return result + return result // !!! } func (r *resolutionState) resolveNodeLikeWorker() *ResolvedModule { diff --git a/internal/modulespecifiers/specifiers.go b/internal/modulespecifiers/specifiers.go index cb377869f1..72b5e3b3e9 100644 --- a/internal/modulespecifiers/specifiers.go +++ b/internal/modulespecifiers/specifiers.go @@ -23,16 +23,42 @@ func GetModuleSpecifiers( host ModuleSpecifierGenerationHost, userPreferences UserPreferences, options ModuleSpecifierOptions, + forAutoImports bool, ) []string { + result, _ := GetModuleSpecifiersWithInfo( + moduleSymbol, + checker, + compilerOptions, + importingSourceFile, + host, + userPreferences, + options, + forAutoImports, + ) + return result +} + +func GetModuleSpecifiersWithInfo( + moduleSymbol *ast.Symbol, + checker CheckerShape, + compilerOptions *core.CompilerOptions, + importingSourceFile SourceFileForSpecifierGeneration, + host ModuleSpecifierGenerationHost, + userPreferences UserPreferences, + options ModuleSpecifierOptions, + forAutoImports bool, +) ([]string, ResultKind) { ambient := tryGetModuleNameFromAmbientModule(moduleSymbol, checker) if len(ambient) > 0 { - return []string{ambient} + // !!! todo forAutoImport + return []string{ambient}, ResultKindAmbient } moduleSourceFile := ast.GetSourceFileOfModule(moduleSymbol) if moduleSourceFile == nil { - return nil + return nil, ResultKindNone } + modulePaths := getAllModulePathsWorker( getInfo(importingSourceFile.FileName(), host), moduleSourceFile.OriginalFileName(), @@ -41,17 +67,15 @@ func GetModuleSpecifiers( // options, ) - result := computeModuleSpecifiers( + return computeModuleSpecifiers( modulePaths, compilerOptions, importingSourceFile, host, userPreferences, options, - /*forAutoImport*/ false, + forAutoImports, ) - - return result } func tryGetModuleNameFromAmbientModule(moduleSymbol *ast.Symbol, checker CheckerShape) string { @@ -122,6 +146,29 @@ func getInfo( } } +func getAllModulePaths( + info Info, + importedFileName string, + host ModuleSpecifierGenerationHost, + compilerOptions *core.CompilerOptions, + preferences UserPreferences, + options ModuleSpecifierOptions, +) []ModulePath { + // !!! use new cache model + // importingFilePath := tspath.ToPath(info.ImportingSourceFileName, host.GetCurrentDirectory(), host.UseCaseSensitiveFileNames()); + // importedFilePath := tspath.ToPath(importedFileName, host.GetCurrentDirectory(), host.UseCaseSensitiveFileNames()); + // cache := host.getModuleSpecifierCache(); + // if (cache != nil) { + // cached := cache.get(importingFilePath, importedFilePath, preferences, options); + // if (cached.modulePaths) {return cached.modulePaths;} + // } + modulePaths := getAllModulePathsWorker(info, importedFileName, host) // , compilerOptions, options); + // if (cache != nil) { + // cache.setModulePaths(importingFilePath, importedFilePath, preferences, options, modulePaths); + // } + return modulePaths +} + func getAllModulePathsWorker( info Info, importedFileName string, @@ -149,7 +196,7 @@ func getAllModulePathsWorker( // } allFileNames := make(map[string]ModulePath) - paths := getEachFileNameOfModule(info.ImportingSourceFileName, importedFileName, host, true) + paths := GetEachFileNameOfModule(info.ImportingSourceFileName, importedFileName, host, true) for _, p := range paths { allFileNames[p.FileName] = p } @@ -189,11 +236,11 @@ func containsIgnoredPath(s string) bool { strings.Contains(s, "/.#") } -func containsNodeModules(s string) bool { +func ContainsNodeModules(s string) bool { return strings.Contains(s, "/node_modules/") } -func getEachFileNameOfModule( +func GetEachFileNameOfModule( importingFileName string, importedFileName string, host ModuleSpecifierGenerationHost, @@ -225,7 +272,7 @@ func getEachFileNameOfModule( if !(shouldFilterIgnoredPaths && containsIgnoredPath(p)) { results = append(results, ModulePath{ FileName: p, - IsInNodeModules: containsNodeModules(p), + IsInNodeModules: ContainsNodeModules(p), IsRedirect: referenceRedirect == p, }) } @@ -268,7 +315,7 @@ func getEachFileNameOfModule( if !(shouldFilterIgnoredPaths && containsIgnoredPath(p)) { results = append(results, ModulePath{ FileName: p, - IsInNodeModules: containsNodeModules(p), + IsInNodeModules: ContainsNodeModules(p), IsRedirect: referenceRedirect == p, }) } @@ -286,7 +333,7 @@ func computeModuleSpecifiers( userPreferences UserPreferences, options ModuleSpecifierOptions, forAutoImport bool, -) []string { +) ([]string, ResultKind) { info := getInfo(importingSourceFile.FileName(), host) preferences := getModuleSpecifierPreferences(userPreferences, host, compilerOptions, importingSourceFile, "") @@ -321,7 +368,7 @@ func computeModuleSpecifiers( } if existingSpecifier != "" { - return []string{existingSpecifier} + return []string{existingSpecifier}, ResultKindNone } importedFileIsInNodeModules := core.Some(modulePaths, func(p ModulePath) bool { return p.IsInNodeModules }) @@ -346,7 +393,7 @@ func computeModuleSpecifiers( if modulePath.IsRedirect { // If we got a specifier for a redirect, it was a bare package specifier (e.g. "@foo/bar", // not "@foo/bar/path/to/file"). No other specifier will be this good, so stop looking. - return nodeModulesSpecifiers + return nodeModulesSpecifiers, ResultKindNodeModules } } @@ -368,8 +415,8 @@ func computeModuleSpecifiers( } if modulePath.IsRedirect { redirectPathsSpecifiers = append(redirectPathsSpecifiers, local) - } else if pathIsBareSpecifier(local) { - if containsNodeModules(local) { + } else if PathIsBareSpecifier(local) { + if ContainsNodeModules(local) { // We could be in this branch due to inappropriate use of `baseUrl`, not intentional `paths` // usage. It's impossible to reason about where to prioritize baseUrl-generated module // specifiers, but if they contain `/node_modules/`, they're going to trigger a portability @@ -393,15 +440,15 @@ func computeModuleSpecifiers( } if len(pathsSpecifiers) > 0 { - return pathsSpecifiers + return pathsSpecifiers, ResultKindPaths } if len(redirectPathsSpecifiers) > 0 { - return redirectPathsSpecifiers + return redirectPathsSpecifiers, ResultKindRedirect } if len(nodeModulesSpecifiers) > 0 { - return nodeModulesSpecifiers + return nodeModulesSpecifiers, ResultKindNodeModules } - return relativeSpecifiers + return relativeSpecifiers, ResultKindRelative } func getLocalModuleSpecifier( @@ -643,7 +690,7 @@ func tryGetModuleNameAsNodeModule( packageNameOnly bool, overrideMode core.ResolutionMode, ) string { - parts := getNodeModulePathParts(pathObj.FileName) + parts := GetNodeModulePathParts(pathObj.FileName) if parts == nil { return "" } @@ -712,7 +759,7 @@ func tryGetModuleNameAsNodeModule( // If the module was found in @types, get the actual Node package name nodeModulesDirectoryName := moduleSpecifier[parts.TopLevelPackageNameIndex+1:] - return getPackageNameFromTypesPackageName(nodeModulesDirectoryName) + return GetPackageNameFromTypesPackageName(nodeModulesDirectoryName) } type pkgJsonDirAttemptResult struct { @@ -763,7 +810,7 @@ func tryDirectoryWithPackageJson( // name in the package.json content via url/filepath dependency specifiers. We need to // use the actual directory name, so don't look at `packageJsonContent.name` here. nodeModulesDirectoryName := packageRootPath[parts.TopLevelPackageNameIndex+1:] - packageName := getPackageNameFromTypesPackageName(nodeModulesDirectoryName) + packageName := GetPackageNameFromTypesPackageName(nodeModulesDirectoryName) conditions := module.GetConditions(options, importMode) var fromExports string @@ -1201,3 +1248,38 @@ func tryGetModuleNameFromExportsOrImports( } return "" } + +// `importingSourceFile` and `importingSourceFileName`? Why not just use `importingSourceFile.path`? +// Because when this is called by the declaration emitter, `importingSourceFile` is the implementation +// file, but `importingSourceFileName` and `toFileName` refer to declaration files (the former to the +// one currently being produced; the latter to the one being imported). We need an implementation file +// just to get its `impliedNodeFormat` and to detect certain preferences from existing import module +// specifiers. +func GetModuleSpecifier( + compilerOptions *core.CompilerOptions, + host ModuleSpecifierGenerationHost, + importingSourceFile *ast.SourceFile, // !!! | FutureSourceFile + importingSourceFileName string, + oldImportSpecifier string, // used only in updatingModuleSpecifier + toFileName string, + options ModuleSpecifierOptions, +) string { + userPreferences := UserPreferences{} + info := getInfo(importingSourceFileName, host) + modulePaths := getAllModulePaths(info, toFileName, host, compilerOptions, userPreferences, options) + preferences := getModuleSpecifierPreferences(userPreferences, host, compilerOptions, importingSourceFile, oldImportSpecifier) + + resolutionMode := options.OverrideImportMode + if resolutionMode == core.ResolutionModeNone { + resolutionMode = host.GetDefaultResolutionModeForFile(importingSourceFile) + } + + for _, modulePath := range modulePaths { + if firstDefined := tryGetModuleNameAsNodeModule(modulePath, info, importingSourceFile, host, compilerOptions, userPreferences, false /*packageNameOnly*/, options.OverrideImportMode); len(firstDefined) > 0 { + return firstDefined + } else if firstDefined := getLocalModuleSpecifier(toFileName, info, compilerOptions, host, resolutionMode, preferences, false); len(firstDefined) > 0 { + return firstDefined + } + } + return "" +} diff --git a/internal/modulespecifiers/util.go b/internal/modulespecifiers/util.go index 6abb7d3605..2203d23113 100644 --- a/internal/modulespecifiers/util.go +++ b/internal/modulespecifiers/util.go @@ -30,7 +30,7 @@ func comparePathsByRedirectAndNumberOfDirectorySeparators(a ModulePath, b Module return -1 } -func pathIsBareSpecifier(path string) bool { +func PathIsBareSpecifier(path string) bool { return !tspath.PathIsAbsolute(path) && !tspath.PathIsRelative(path) } @@ -61,7 +61,7 @@ func isExcludedByRegex(moduleSpecifier string, excludes []string) bool { * */ func ensurePathIsNonModuleName(path string) string { - if pathIsBareSpecifier(path) { + if PathIsBareSpecifier(path) { return "./" + path } return path @@ -234,7 +234,7 @@ const ( nodeModulesPathParseStatePackageContent ) -func getNodeModulePathParts(fullPath string) *NodeModulePathParts { +func GetNodeModulePathParts(fullPath string) *NodeModulePathParts { // If fullPath can't be valid module file within node_modules, returns undefined. // Example of expected pattern: /base/path/node_modules/[@scope/otherpackage/@otherscope/node_modules/]package/[subdirectory/]file.js // Returns indices: ^ ^ ^ ^ @@ -287,7 +287,25 @@ func getNodeModulePathParts(fullPath string) *NodeModulePathParts { return nil } -func getPackageNameFromTypesPackageName(mangledName string) string { +func GetNodeModulesPackageName( + compilerOptions *core.CompilerOptions, + importingSourceFile *ast.SourceFile, // !!! | FutureSourceFile + nodeModulesFileName string, + host ModuleSpecifierGenerationHost, + preferences UserPreferences, + options ModuleSpecifierOptions, +) string { + info := getInfo(importingSourceFile.FileName(), host) + modulePaths := getAllModulePaths(info, nodeModulesFileName, host, compilerOptions, preferences, options) + for _, modulePath := range modulePaths { + if result := tryGetModuleNameAsNodeModule(modulePath, info, importingSourceFile, host, compilerOptions, preferences, true /*packageNameOnly*/, options.OverrideImportMode); len(result) > 0 { + return result + } + } + return "" +} + +func GetPackageNameFromTypesPackageName(mangledName string) string { withoutAtTypePrefix := strings.TrimPrefix(mangledName, "@types/") if withoutAtTypePrefix != mangledName { return module.UnmangleScopedPackageName(withoutAtTypePrefix) diff --git a/internal/parser/jsdoc.go b/internal/parser/jsdoc.go index a2a630ac96..875ab54f61 100644 --- a/internal/parser/jsdoc.go +++ b/internal/parser/jsdoc.go @@ -39,7 +39,7 @@ func (p *Parser) withJSDoc(node *ast.Node, hasJSDoc bool) []*ast.Node { } // Should only be called once per node p.hasDeprecatedTag = false - ranges := getJSDocCommentRanges(&p.factory, p.jsdocCommentRangesSpace, node, p.sourceText) + ranges := GetJSDocCommentRanges(&p.factory, p.jsdocCommentRangesSpace, node, p.sourceText) p.jsdocCommentRangesSpace = ranges[:0] jsdoc := p.nodeSlicePool.NewSlice(len(ranges))[:0] pos := node.Pos() diff --git a/internal/parser/utilities.go b/internal/parser/utilities.go index ca9715f247..ee31ae4704 100644 --- a/internal/parser/utilities.go +++ b/internal/parser/utilities.go @@ -25,7 +25,7 @@ func tokenIsIdentifierOrKeywordOrGreaterThan(token ast.Kind) bool { return token == ast.KindGreaterThanToken || tokenIsIdentifierOrKeyword(token) } -func getJSDocCommentRanges(f *ast.NodeFactory, commentRanges []ast.CommentRange, node *ast.Node, text string) []ast.CommentRange { +func GetJSDocCommentRanges(f *ast.NodeFactory, commentRanges []ast.CommentRange, node *ast.Node, text string) []ast.CommentRange { switch node.Kind { case ast.KindParameter, ast.KindTypeParameter, ast.KindFunctionExpression, ast.KindArrowFunction, ast.KindParenthesizedExpression, ast.KindVariableDeclaration, ast.KindExportSpecifier: for commentRange := range scanner.GetTrailingCommentRanges(f, text, node.Pos()) { diff --git a/internal/printer/changetrackerwriter.go b/internal/printer/changetrackerwriter.go new file mode 100644 index 0000000000..f0acf78e02 --- /dev/null +++ b/internal/printer/changetrackerwriter.go @@ -0,0 +1,214 @@ +package printer + +import ( + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/scanner" + "github.com/microsoft/typescript-go/internal/stringutil" +) + +type ChangeTrackerWriter struct { + textWriter + lastNonTriviaPosition int + pos map[triviaPositionKey]int + end map[triviaPositionKey]int +} + +type triviaPositionKey interface { // *astNode | *ast.NodeList + Pos() int + End() int +} + +func NewChangeTrackerWriter(newline string) *ChangeTrackerWriter { + ctw := &ChangeTrackerWriter{ + textWriter: textWriter{newLine: newline}, + lastNonTriviaPosition: 0, + } + ctw.textWriter.Clear() + return ctw +} + +func (ct *ChangeTrackerWriter) GetPrintHandlers() PrintHandlers { + return PrintHandlers{ + OnBeforeEmitNode: func(nodeOpt *ast.Node) { + if nodeOpt != nil { + ct.setPos(nodeOpt) + } + }, + OnAfterEmitNode: func(nodeOpt *ast.Node) { + if nodeOpt != nil { + ct.setEnd(nodeOpt) + } + }, + OnBeforeEmitNodeList: func(nodesOpt *ast.NodeList) { + if nodesOpt != nil { + ct.setPos(nodesOpt) + } + }, + OnAfterEmitNodeList: func(nodesOpt *ast.NodeList) { + if nodesOpt != nil { + ct.setEnd(nodesOpt) + } + }, + OnBeforeEmitToken: func(nodeOpt *ast.TokenNode) { + if nodeOpt != nil { + ct.setPos(nodeOpt) + } + }, + OnAfterEmitToken: func(nodeOpt *ast.TokenNode) { + if nodeOpt != nil { + ct.setEnd(nodeOpt) + } + }, + } +} + +func (ct *ChangeTrackerWriter) setPos(node triviaPositionKey) { + ct.pos[node] = ct.lastNonTriviaPosition +} + +func (ct *ChangeTrackerWriter) setEnd(node triviaPositionKey) { + ct.end[node] = ct.lastNonTriviaPosition +} + +func (ct *ChangeTrackerWriter) getPos(node triviaPositionKey) int { + return ct.pos[node] +} + +func (ct *ChangeTrackerWriter) getEnd(node triviaPositionKey) int { + return ct.end[node] +} + +func (ct *ChangeTrackerWriter) setLastNonTriviaPosition(s string, force bool) { + if force || scanner.SkipTrivia(s, 0) != len(s) { + ct.lastNonTriviaPosition = ct.textWriter.GetTextPos() + i := 0 + for stringutil.IsWhiteSpaceLike(rune(s[len(s)-i-1])) { + i++ + } + // trim trailing whitespaces + ct.lastNonTriviaPosition -= i + } +} + +func (ct *ChangeTrackerWriter) AssignPositionsToNode(node *ast.Node, factory *ast.NodeFactory) *ast.Node { + var visitor *ast.NodeVisitor + visitor = &ast.NodeVisitor{ + Visit: func(n *ast.Node) *ast.Node { return ct.assignPositionsToNodeWorker(n, visitor) }, + Factory: factory, + Hooks: ast.NodeVisitorHooks{ + VisitNodes: ct.assignPositionsToNodeArray, + VisitToken: ct.assignPositionsToNodeWorker, + }, + } + return ct.assignPositionsToNodeWorker(node, visitor) +} + +func (ct *ChangeTrackerWriter) assignPositionsToNodeWorker( + node *ast.Node, + v *ast.NodeVisitor, +) *ast.Node { + visited := node.VisitEachChild(v) + // create proxy node for non synthesized nodes + newNode := visited + if !ast.NodeIsSynthesized(visited) { + newNode = visited.Clone(v.Factory) + } + newNode.Loc = core.NewTextRange(ct.getPos(node), ct.getEnd(node)) + return newNode +} + +func (ct *ChangeTrackerWriter) assignPositionsToNodeArray( + nodes *ast.NodeList, + v *ast.NodeVisitor, +) *ast.NodeList { + visited := v.VisitNodes(nodes) + if visited == nil { + return visited + } + if nodes == nil { + // Debug.assert(nodes); + panic("if nodes is nil, visited should not be nil") + } + // clone nodearray if necessary + nodeArray := visited + if visited == nodes { + nodeArray = visited.Clone(v.Factory) + } + nodeArray.Loc = core.NewTextRange(ct.getPos(nodes), ct.getEnd(nodes)) + return nodeArray +} + +func (ct *ChangeTrackerWriter) Write(text string) { + ct.textWriter.Write(text) + ct.setLastNonTriviaPosition(text, false) +} + +func (ct *ChangeTrackerWriter) WriteTrailingSemicolon(text string) { + ct.textWriter.WriteTrailingSemicolon(text) + ct.setLastNonTriviaPosition(text, false) +} +func (ct *ChangeTrackerWriter) WriteComment(text string) { ct.textWriter.WriteComment(text) } +func (ct *ChangeTrackerWriter) WriteKeyword(text string) { + ct.textWriter.WriteKeyword(text) + ct.setLastNonTriviaPosition(text, false) +} + +func (ct *ChangeTrackerWriter) WriteOperator(text string) { + ct.textWriter.WriteOperator(text) + ct.setLastNonTriviaPosition(text, false) +} + +func (ct *ChangeTrackerWriter) WritePunctuation(text string) { + ct.textWriter.WritePunctuation(text) + ct.setLastNonTriviaPosition(text, false) +} + +func (ct *ChangeTrackerWriter) WriteSpace(text string) { + ct.textWriter.WriteSpace(text) + ct.setLastNonTriviaPosition(text, false) +} + +func (ct *ChangeTrackerWriter) WriteStringLiteral(text string) { + ct.textWriter.WriteStringLiteral(text) + ct.setLastNonTriviaPosition(text, false) +} + +func (ct *ChangeTrackerWriter) WriteParameter(text string) { + ct.textWriter.WriteParameter(text) + ct.setLastNonTriviaPosition(text, false) +} + +func (ct *ChangeTrackerWriter) WriteProperty(text string) { + ct.textWriter.WriteProperty(text) + ct.setLastNonTriviaPosition(text, false) +} + +func (ct *ChangeTrackerWriter) WriteSymbol(text string, symbol *ast.Symbol) { + ct.textWriter.WriteSymbol(text, symbol) + ct.setLastNonTriviaPosition(text, false) +} +func (ct *ChangeTrackerWriter) WriteLine() { ct.textWriter.WriteLine() } +func (ct *ChangeTrackerWriter) WriteLineForce(force bool) { ct.textWriter.WriteLineForce(force) } +func (ct *ChangeTrackerWriter) IncreaseIndent() { ct.textWriter.IncreaseIndent() } +func (ct *ChangeTrackerWriter) DecreaseIndent() { ct.textWriter.DecreaseIndent() } +func (ct *ChangeTrackerWriter) Clear() { ct.textWriter.Clear(); ct.lastNonTriviaPosition = 0 } +func (ct *ChangeTrackerWriter) String() string { return ct.textWriter.String() } +func (ct *ChangeTrackerWriter) RawWrite(s string) { + ct.textWriter.RawWrite(s) + ct.setLastNonTriviaPosition(s, false) +} + +func (ct *ChangeTrackerWriter) WriteLiteral(s string) { + ct.textWriter.WriteLiteral(s) + ct.setLastNonTriviaPosition(s, true) +} +func (ct *ChangeTrackerWriter) GetTextPos() int { return ct.textWriter.GetTextPos() } +func (ct *ChangeTrackerWriter) GetLine() int { return ct.textWriter.GetLine() } +func (ct *ChangeTrackerWriter) GetColumn() int { return ct.textWriter.GetColumn() } +func (ct *ChangeTrackerWriter) GetIndent() int { return ct.textWriter.GetIndent() } +func (ct *ChangeTrackerWriter) IsAtStartOfLine() bool { return ct.textWriter.IsAtStartOfLine() } +func (ct *ChangeTrackerWriter) HasTrailingComment() bool { return ct.textWriter.HasTrailingComment() } +func (ct *ChangeTrackerWriter) HasTrailingWhitespace() bool { + return ct.textWriter.HasTrailingWhitespace() +} diff --git a/internal/printer/printer.go b/internal/printer/printer.go index abb3829c5f..25bec4b3c0 100644 --- a/internal/printer/printer.go +++ b/internal/printer/printer.go @@ -45,10 +45,10 @@ type PrinterOptions struct { OmitBraceSourceMapPositions bool // ExtendedDiagnostics bool OnlyPrintJSDocStyle bool - // NeverAsciiEscape bool + NeverAsciiEscape bool // !!! // StripInternal bool - PreserveSourceNewlines bool - // TerminateUnterminatedLiterals bool + PreserveSourceNewlines bool + TerminateUnterminatedLiterals bool // !!! } type PrintHandlers struct { @@ -727,7 +727,7 @@ func (p *Printer) shouldEmitComments(node *ast.Node) bool { func (p *Printer) shouldWriteComment(comment ast.CommentRange) bool { return !p.Options.OnlyPrintJSDocStyle || p.currentSourceFile != nil && isJSDocLikeText(p.currentSourceFile.Text(), comment) || - p.currentSourceFile != nil && isPinnedComment(p.currentSourceFile.Text(), comment) + p.currentSourceFile != nil && IsPinnedComment(p.currentSourceFile.Text(), comment) } func (p *Printer) shouldEmitIndented(node *ast.Node) bool { @@ -4108,7 +4108,7 @@ func (p *Printer) emitJsxAttributeLike(node *ast.JsxAttributeLike) { func (p *Printer) emitJsxExpression(node *ast.JsxExpression) { state := p.enterNode(node.AsNode()) if node.Expression != nil || !p.commentsDisabled && !ast.NodeIsSynthesized(node.AsNode()) && p.hasCommentsAtPosition(node.Pos()) { // preserve empty expressions if they contain comments! - indented := p.currentSourceFile != nil && !ast.NodeIsSynthesized(node.AsNode()) && getLinesBetweenPositions(p.currentSourceFile, node.Pos(), node.End()) != 0 + indented := p.currentSourceFile != nil && !ast.NodeIsSynthesized(node.AsNode()) && GetLinesBetweenPositions(p.currentSourceFile, node.Pos(), node.End()) != 0 p.increaseIndentIf(indented) end := p.emitToken(ast.KindOpenBraceToken, node.Pos(), WriteKindPunctuation, node.AsNode()) p.emitTokenNode(node.DotDotDotToken) @@ -5291,7 +5291,7 @@ func (p *Printer) emitDetachedComments(textRange core.TextRange) (result detache // var x = 10; if textRange.Pos() == 0 { for comment := range scanner.GetLeadingCommentRanges(p.emitContext.Factory.AsNodeFactory(), text, textRange.Pos()) { - if isPinnedComment(text, comment) { + if IsPinnedComment(text, comment) { leadingComments = append(leadingComments, comment) } } @@ -5394,7 +5394,7 @@ func (p *Printer) emitComment(comment ast.CommentRange) { func (p *Printer) isTripleSlashComment(comment ast.CommentRange) bool { return p.currentSourceFile != nil && - isRecognizedTripleSlashComment(p.currentSourceFile.Text(), comment) + IsRecognizedTripleSlashComment(p.currentSourceFile.Text(), comment) } // diff --git a/internal/printer/utilities.go b/internal/printer/utilities.go index 9eac7f1b89..0b28e865ab 100644 --- a/internal/printer/utilities.go +++ b/internal/printer/utilities.go @@ -362,10 +362,10 @@ func getStartPositionOfRange(r core.TextRange, sourceFile *ast.SourceFile, inclu } func positionsAreOnSameLine(pos1 int, pos2 int, sourceFile *ast.SourceFile) bool { - return getLinesBetweenPositions(sourceFile, pos1, pos2) == 0 + return GetLinesBetweenPositions(sourceFile, pos1, pos2) == 0 } -func getLinesBetweenPositions(sourceFile *ast.SourceFile, pos1 int, pos2 int) int { +func GetLinesBetweenPositions(sourceFile *ast.SourceFile, pos1 int, pos2 int) int { if pos1 == pos2 { return 0 } @@ -384,18 +384,18 @@ func getLinesBetweenPositions(sourceFile *ast.SourceFile, pos1 int, pos2 int) in func getLinesBetweenRangeEndAndRangeStart(range1 core.TextRange, range2 core.TextRange, sourceFile *ast.SourceFile, includeSecondRangeComments bool) int { range2Start := getStartPositionOfRange(range2, sourceFile, includeSecondRangeComments) - return getLinesBetweenPositions(sourceFile, range1.End(), range2Start) + return GetLinesBetweenPositions(sourceFile, range1.End(), range2Start) } func getLinesBetweenPositionAndPrecedingNonWhitespaceCharacter(pos int, stopPos int, sourceFile *ast.SourceFile, includeComments bool) int { startPos := scanner.SkipTriviaEx(sourceFile.Text(), pos, &scanner.SkipTriviaOptions{StopAtComments: includeComments}) prevPos := getPreviousNonWhitespacePosition(startPos, stopPos, sourceFile) - return getLinesBetweenPositions(sourceFile, core.IfElse(prevPos >= 0, prevPos, stopPos), startPos) + return GetLinesBetweenPositions(sourceFile, core.IfElse(prevPos >= 0, prevPos, stopPos), startPos) } func getLinesBetweenPositionAndNextNonWhitespaceCharacter(pos int, stopPos int, sourceFile *ast.SourceFile, includeComments bool) int { nextPos := scanner.SkipTriviaEx(sourceFile.Text(), pos, &scanner.SkipTriviaOptions{StopAtComments: includeComments}) - return getLinesBetweenPositions(sourceFile, pos, core.IfElse(stopPos < nextPos, stopPos, nextPos)) + return GetLinesBetweenPositions(sourceFile, pos, core.IfElse(stopPos < nextPos, stopPos, nextPos)) } func getPreviousNonWhitespacePosition(pos int, stopPos int, sourceFile *ast.SourceFile) int { @@ -817,7 +817,7 @@ func matchQuotedString(text string, pos *int) bool { // /// // /// // /// -func isRecognizedTripleSlashComment(text string, commentRange ast.CommentRange) bool { +func IsRecognizedTripleSlashComment(text string, commentRange ast.CommentRange) bool { if commentRange.Kind == ast.KindSingleLineCommentTrivia && commentRange.Len() > 2 && text[commentRange.Pos()+1] == '/' && @@ -881,7 +881,7 @@ func isJSDocLikeText(text string, comment ast.CommentRange) bool { text[comment.Pos()+3] != '/' } -func isPinnedComment(text string, comment ast.CommentRange) bool { +func IsPinnedComment(text string, comment ast.CommentRange) bool { return comment.Kind == ast.KindMultiLineCommentTrivia && comment.Len() > 5 && text[comment.Pos()+2] == '!' diff --git a/internal/printer/utilities_test.go b/internal/printer/utilities_test.go index 02281a0f8b..1d19afe4f1 100644 --- a/internal/printer/utilities_test.go +++ b/internal/printer/utilities_test.go @@ -146,7 +146,7 @@ func TestIsRecognizedTripleSlashComment(t *testing.T) { commentRange.Kind = ast.KindSingleLineCommentTrivia commentRange.TextRange = core.NewTextRange(0, len(rec.s)) } - actual := isRecognizedTripleSlashComment(rec.s, commentRange) + actual := IsRecognizedTripleSlashComment(rec.s, commentRange) assert.Equal(t, actual, rec.expected) }) } diff --git a/internal/scanner/scanner.go b/internal/scanner/scanner.go index fdad013ef1..7530b5d155 100644 --- a/internal/scanner/scanner.go +++ b/internal/scanner/scanner.go @@ -2428,6 +2428,10 @@ func GetLineStarts(sourceFile ast.SourceFileLike) []core.TextPos { return sourceFile.LineMap() } +func GetLineStartPositionForPosition(pos int, lineMap []core.TextPos) int { + return int(lineMap[ComputeLineOfPosition(lineMap, pos)]) +} + func GetLineAndCharacterOfPosition(sourceFile ast.SourceFileLike, pos int) (line int, character int) { lineMap := GetLineStarts(sourceFile) line = ComputeLineOfPosition(lineMap, pos) diff --git a/internal/tspath/path.go b/internal/tspath/path.go index a0d722b233..8e334f861d 100644 --- a/internal/tspath/path.go +++ b/internal/tspath/path.go @@ -894,6 +894,23 @@ func FileExtensionIs(path string, extension string) bool { return len(path) > len(extension) && strings.HasSuffix(path, extension) } +// Calls `callback` on `directory` and every ancestor directory it has, returning the first defined result. +// Stops at global cache location +func ForEachAncestorDirectoryStoppingAtGlobalCache[T any]( + globalCacheLocation string, + directory string, + callback func(directory string) (result T, stop bool), +) T { + result, _ := ForEachAncestorDirectory(directory, func(ancestorDirectory string) (T, bool) { + result, stop := callback(ancestorDirectory) + if stop || ancestorDirectory == globalCacheLocation { + return result, true + } + return result, false + }) + return result +} + func ForEachAncestorDirectory[T any](directory string, callback func(directory string) (result T, stop bool)) (result T, ok bool) { for { result, stop := callback(directory)