diff --git a/cmd/tsgo/lsp.go b/cmd/tsgo/lsp.go index 0dd04c57fb..ae3d7ec768 100644 --- a/cmd/tsgo/lsp.go +++ b/cmd/tsgo/lsp.go @@ -64,6 +64,7 @@ func runLSP(args []string) int { defer stop() if err := s.Run(ctx); err != nil { + fmt.Fprintln(os.Stderr, err) return 1 } return 0 diff --git a/internal/api/api.go b/internal/api/api.go index 2c4fa079ac..a730713594 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -161,7 +161,7 @@ func (api *API) GetSymbolAtPosition(ctx context.Context, projectId Handle[projec return nil, errors.New("project not found") } - languageService := ls.NewLanguageService(project.GetProgram(), snapshot) + languageService := ls.NewLanguageService(project.ConfigFilePath(), project.GetProgram(), snapshot) symbol, err := languageService.GetSymbolAtPosition(ctx, fileName, position) if err != nil || symbol == nil { return nil, err @@ -203,7 +203,7 @@ func (api *API) GetSymbolAtLocation(ctx context.Context, projectId Handle[projec if node == nil { return nil, fmt.Errorf("node of kind %s not found at position %d in file %q", kind.String(), pos, sourceFile.FileName()) } - languageService := ls.NewLanguageService(project.GetProgram(), snapshot) + languageService := ls.NewLanguageService(project.ConfigFilePath(), project.GetProgram(), snapshot) symbol := languageService.GetSymbolAtLocation(ctx, node) if symbol == nil { return nil, nil @@ -233,7 +233,7 @@ func (api *API) GetTypeOfSymbol(ctx context.Context, projectId Handle[project.Pr if !ok { return nil, fmt.Errorf("symbol %q not found", symbolHandle) } - languageService := ls.NewLanguageService(project.GetProgram(), snapshot) + languageService := ls.NewLanguageService(project.ConfigFilePath(), project.GetProgram(), snapshot) t := languageService.GetTypeOfSymbol(ctx, symbol) if t == nil { return nil, nil diff --git a/internal/ast/symbol.go b/internal/ast/symbol.go index 7d0875adc0..f60ca0cee3 100644 --- a/internal/ast/symbol.go +++ b/internal/ast/symbol.go @@ -35,6 +35,14 @@ func (s *Symbol) IsStatic() bool { return modifierFlags&ModifierFlagsStatic != 0 } +// See comment on `declareModuleMember` in `binder.go`. +func (s *Symbol) CombinedLocalAndExportSymbolFlags() SymbolFlags { + if s.ExportSymbol != nil { + return s.Flags | s.ExportSymbol.Flags + } + return s.Flags +} + // SymbolTable type SymbolTable map[string]*Symbol diff --git a/internal/ast/utilities.go b/internal/ast/utilities.go index 6338dedc58..1c07b64715 100644 --- a/internal/ast/utilities.go +++ b/internal/ast/utilities.go @@ -1870,11 +1870,11 @@ func IsExpressionNode(node *Node) bool { for node.Parent.Kind == KindQualifiedName { node = node.Parent } - return IsTypeQueryNode(node.Parent) || IsJSDocLinkLike(node.Parent) || IsJSDocNameReference(node.Parent) || isJSXTagName(node) + return IsTypeQueryNode(node.Parent) || IsJSDocLinkLike(node.Parent) || IsJSDocNameReference(node.Parent) || IsJsxTagName(node) case KindPrivateIdentifier: return IsBinaryExpression(node.Parent) && node.Parent.AsBinaryExpression().Left == node && node.Parent.AsBinaryExpression().OperatorToken.Kind == KindInKeyword case KindIdentifier: - if IsTypeQueryNode(node.Parent) || IsJSDocLinkLike(node.Parent) || IsJSDocNameReference(node.Parent) || isJSXTagName(node) { + if IsTypeQueryNode(node.Parent) || IsJSDocLinkLike(node.Parent) || IsJSDocNameReference(node.Parent) || IsJsxTagName(node) { return true } fallthrough @@ -1991,15 +1991,6 @@ func IsJSDocTag(node *Node) bool { return node.Kind >= KindFirstJSDocTagNode && node.Kind <= KindLastJSDocTagNode } -func isJSXTagName(node *Node) bool { - parent := node.Parent - switch parent.Kind { - case KindJsxOpeningElement, KindJsxSelfClosingElement, KindJsxClosingElement: - return parent.TagName() == node - } - return false -} - func IsSuperCall(node *Node) bool { return IsCallExpression(node) && node.Expression().Kind == KindSuperKeyword } @@ -3407,12 +3398,12 @@ func IsExternalModuleAugmentation(node *Node) bool { func GetSourceFileOfModule(module *Symbol) *SourceFile { declaration := module.ValueDeclaration if declaration == nil { - declaration = getNonAugmentationDeclaration(module) + declaration = GetNonAugmentationDeclaration(module) } return GetSourceFileOfNode(declaration) } -func getNonAugmentationDeclaration(symbol *Symbol) *Node { +func GetNonAugmentationDeclaration(symbol *Symbol) *Node { return core.Find(symbol.Declarations, func(d *Node) bool { return !IsExternalModuleAugmentation(d) && !IsGlobalScopeAugmentation(d) }) @@ -3888,6 +3879,37 @@ func GetContainingFunction(node *Node) *Node { return FindAncestor(node.Parent, IsFunctionLike) } +func ImportFromModuleSpecifier(node *Node) *Node { + if result := TryGetImportFromModuleSpecifier(node); result != nil { + return result + } + debug.FailBadSyntaxKind(node.Parent) + return nil +} + +func TryGetImportFromModuleSpecifier(node *StringLiteralLike) *Node { + switch node.Parent.Kind { + case KindImportDeclaration, KindJSImportDeclaration, KindExportDeclaration: + return node.Parent + case KindExternalModuleReference: + return node.Parent.Parent + case KindCallExpression: + if IsImportCall(node.Parent) || IsRequireCall(node.Parent, false /*requireStringLiteralLikeArgument*/) { + return node.Parent + } + return nil + case KindLiteralType: + if !IsStringLiteral(node) { + return nil + } + if IsImportTypeNode(node.Parent.Parent) { + return node.Parent.Parent + } + return nil + } + return nil +} + func IsImplicitlyExportedJSTypeAlias(node *Node) bool { return IsJSTypeAliasDeclaration(node) && IsSourceFile(node.Parent) && IsExternalOrCommonJSModule(node.Parent.AsSourceFile()) } diff --git a/internal/checker/checker.go b/internal/checker/checker.go index 64e9f79f69..8c711c6325 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -14430,6 +14430,9 @@ func (c *Checker) getEmitSyntaxForModuleSpecifierExpression(usage *ast.Node) cor } func (c *Checker) errorNoModuleMemberSymbol(moduleSymbol *ast.Symbol, targetSymbol *ast.Symbol, node *ast.Node, name *ast.Node) { + if c.compilerOptions.NoCheck.IsTrue() { + return + } moduleName := c.getFullyQualifiedName(moduleSymbol, node) declarationName := scanner.DeclarationNameToString(name) var suggestion *ast.Symbol @@ -14641,6 +14644,7 @@ func (c *Checker) markSymbolOfAliasDeclarationIfTypeOnly(aliasDeclaration *ast.N func (c *Checker) resolveExternalModuleName(location *ast.Node, moduleReferenceExpression *ast.Node, ignoreErrors bool) *ast.Symbol { errorMessage := diagnostics.Cannot_find_module_0_or_its_corresponding_type_declarations + ignoreErrors = ignoreErrors || c.compilerOptions.NoCheck.IsTrue() return c.resolveExternalModuleNameWorker(location, moduleReferenceExpression, core.IfElse(ignoreErrors, nil, errorMessage), ignoreErrors, false /*isForAugmentation*/) } diff --git a/internal/checker/services.go b/internal/checker/services.go index ba0236d1b5..5d1f5f557f 100644 --- a/internal/checker/services.go +++ b/internal/checker/services.go @@ -26,7 +26,7 @@ func (c *Checker) getSymbolsInScope(location *ast.Node, meaning ast.SymbolFlags) // Copy the given symbol into symbol tables if the symbol has the given meaning // and it doesn't already exists in the symbol table. copySymbol := func(symbol *ast.Symbol, meaning ast.SymbolFlags) { - if GetCombinedLocalAndExportSymbolFlags(symbol)&meaning != 0 { + if symbol.CombinedLocalAndExportSymbolFlags()&meaning != 0 { id := symbol.Name // We will copy all symbol regardless of its reserved name because // symbolsToArray will check whether the key is a reserved name and @@ -393,6 +393,13 @@ func (c *Checker) GetRootSymbols(symbol *ast.Symbol) []*ast.Symbol { return result } +func (c *Checker) GetMappedTypeSymbolOfProperty(symbol *ast.Symbol) *ast.Symbol { + if valueLinks := c.valueSymbolLinks.TryGet(symbol); valueLinks != nil { + return valueLinks.containingType.symbol + } + return nil +} + func (c *Checker) getImmediateRootSymbols(symbol *ast.Symbol) []*ast.Symbol { if symbol.CheckFlags&ast.CheckFlagsSynthetic != 0 { return core.MapNonNil( diff --git a/internal/checker/utilities.go b/internal/checker/utilities.go index 6fdedd0a91..d1b852fe9e 100644 --- a/internal/checker/utilities.go +++ b/internal/checker/utilities.go @@ -1622,14 +1622,6 @@ func symbolsToArray(symbols ast.SymbolTable) []*ast.Symbol { return result } -// See comment on `declareModuleMember` in `binder.go`. -func GetCombinedLocalAndExportSymbolFlags(symbol *ast.Symbol) ast.SymbolFlags { - if symbol.ExportSymbol != nil { - return symbol.Flags | symbol.ExportSymbol.Flags - } - return symbol.Flags -} - func SkipAlias(symbol *ast.Symbol, checker *Checker) *ast.Symbol { if symbol.Flags&ast.SymbolFlagsAlias != 0 { return checker.GetAliasedSymbol(symbol) diff --git a/internal/collections/multimap.go b/internal/collections/multimap.go index e7df1ae5c7..a996917e21 100644 --- a/internal/collections/multimap.go +++ b/internal/collections/multimap.go @@ -10,6 +10,12 @@ type MultiMap[K comparable, V comparable] struct { M map[K][]V } +func NewMultiMapWithSizeHint[K comparable, V comparable](hint int) *MultiMap[K, V] { + return &MultiMap[K, V]{ + M: make(map[K][]V, hint), + } +} + func GroupBy[K comparable, V comparable](items []V, groupId func(V) K) *MultiMap[K, V] { m := &MultiMap[K, V]{} for _, item := range items { diff --git a/internal/collections/set.go b/internal/collections/set.go index 6dfd3c90d6..7b0aabff50 100644 --- a/internal/collections/set.go +++ b/internal/collections/set.go @@ -14,6 +14,9 @@ func NewSetWithSizeHint[T comparable](hint int) *Set[T] { } func (s *Set[T]) Has(key T) bool { + if s == nil { + return false + } _, ok := s.M[key] return ok } @@ -30,14 +33,23 @@ func (s *Set[T]) Delete(key T) { } func (s *Set[T]) Len() int { + if s == nil { + return 0 + } return len(s.M) } func (s *Set[T]) Keys() map[T]struct{} { + if s == nil { + return nil + } return s.M } func (s *Set[T]) Clear() { + if s == nil { + return + } clear(s.M) } @@ -58,6 +70,37 @@ func (s *Set[T]) Clone() *Set[T] { return clone } +func (s *Set[T]) Union(other *Set[T]) { + if s.Len() == 0 && other.Len() == 0 { + return + } + if s == nil { + panic("cannot modify nil Set") + } + if s.M == nil { + s.M = maps.Clone(other.M) + return + } + maps.Copy(s.M, other.M) +} + +func (s *Set[T]) UnionedWith(other *Set[T]) *Set[T] { + if s == nil && other == nil { + return nil + } + result := s.Clone() + if other != nil { + if result == nil { + result = &Set[T]{} + } + if result.M == nil { + result.M = make(map[T]struct{}, len(other.M)) + } + maps.Copy(result.M, other.M) + } + return result +} + func (s *Set[T]) Equals(other *Set[T]) bool { if s == other { return true @@ -68,6 +111,33 @@ func (s *Set[T]) Equals(other *Set[T]) bool { return maps.Equal(s.M, other.M) } +func (s *Set[T]) IsSubsetOf(other *Set[T]) bool { + if s == nil { + return true + } + if other == nil { + return false + } + for key := range s.M { + if !other.Has(key) { + return false + } + } + return true +} + +func (s *Set[T]) Intersects(other *Set[T]) bool { + if s == nil || other == nil { + return false + } + for key := range s.M { + if other.Has(key) { + return true + } + } + return false +} + func NewSetFromItems[T comparable](items ...T) *Set[T] { s := &Set[T]{} for _, item := range items { diff --git a/internal/compiler/program.go b/internal/compiler/program.go index 8527d018cf..834d842ab7 100644 --- a/internal/compiler/program.go +++ b/internal/compiler/program.go @@ -19,6 +19,7 @@ import ( "github.com/microsoft/typescript-go/internal/diagnostics" "github.com/microsoft/typescript-go/internal/locale" "github.com/microsoft/typescript-go/internal/module" + "github.com/microsoft/typescript-go/internal/modulespecifiers" "github.com/microsoft/typescript-go/internal/outputpaths" "github.com/microsoft/typescript-go/internal/packagejson" "github.com/microsoft/typescript-go/internal/parser" @@ -72,6 +73,11 @@ type Program struct { knownSymlinks *symlinks.KnownSymlinks knownSymlinksOnce sync.Once + // Used by auto-imports + packageNamesOnce sync.Once + resolvedPackageNames *collections.Set[string] + unresolvedPackageNames *collections.Set[string] + // Used by workspace/symbol hasTSFileOnce sync.Once hasTSFile bool @@ -89,7 +95,7 @@ func (p *Program) GetCurrentDirectory() string { // GetGlobalTypingsCacheLocation implements checker.Program. func (p *Program) GetGlobalTypingsCacheLocation() string { - return "" // !!! see src/tsserver/nodeServer.ts for strada's node-specific implementation + return p.opts.TypingsLocation } // GetNearestAncestorDirectoryWithPackageJson implements checker.Program. @@ -173,8 +179,8 @@ func (p *Program) UseCaseSensitiveFileNames() bool { return p.Host().FS().UseCaseSensitiveFileNames() } -func (p *Program) UsesUriStyleNodeCoreModules() bool { - return p.usesUriStyleNodeCoreModules.IsTrue() +func (p *Program) UsesUriStyleNodeCoreModules() core.Tristate { + return p.usesUriStyleNodeCoreModules } var _ checker.Program = (*Program)(nil) @@ -250,6 +256,8 @@ func (p *Program) UpdateProgram(changedFilePath tspath.Path, newHost CompilerHos programDiagnostics: p.programDiagnostics, hasEmitBlockingDiagnostics: p.hasEmitBlockingDiagnostics, unresolvedImports: p.unresolvedImports, + resolvedPackageNames: p.resolvedPackageNames, + unresolvedPackageNames: p.unresolvedPackageNames, knownSymlinks: p.knownSymlinks, } result.initCheckerPool() @@ -1309,6 +1317,13 @@ func (p *Program) IsSourceFileDefaultLibrary(path tspath.Path) bool { return ok } +func (p *Program) IsGlobalTypingsFile(fileName string) bool { + if !tspath.IsDeclarationFileName(fileName) { + return false + } + return tspath.ContainsPath(p.GetGlobalTypingsCacheLocation(), fileName, p.comparePathsOptions) +} + func (p *Program) GetDefaultLibFile(path tspath.Path) *LibFile { if libFile, ok := p.libFiles[path]; ok { return libFile @@ -1630,6 +1645,54 @@ func (p *Program) SourceFileMayBeEmitted(sourceFile *ast.SourceFile, forceDtsEmi return sourceFileMayBeEmitted(sourceFile, p, forceDtsEmit) } +func (p *Program) ResolvedPackageNames() *collections.Set[string] { + p.collectPackageNames() + return p.resolvedPackageNames +} + +func (p *Program) UnresolvedPackageNames() *collections.Set[string] { + p.collectPackageNames() + return p.unresolvedPackageNames +} + +func (p *Program) collectPackageNames() { + p.packageNamesOnce.Do(func() { + if p.resolvedPackageNames == nil { + p.resolvedPackageNames = &collections.Set[string]{} + p.unresolvedPackageNames = &collections.Set[string]{} + for _, file := range p.files { + if p.IsSourceFileDefaultLibrary(file.Path()) || p.IsSourceFileFromExternalLibrary(file) || strings.Contains(file.FileName(), "/node_modules/") { + // Checking for /node_modules/ is a little imprecise, but ATA treats locally installed typings + // as root files, which would not pass IsSourceFileFromExternalLibrary. + continue + } + for _, imp := range file.Imports() { + if tspath.IsExternalModuleNameRelative(imp.Text()) { + continue + } + if resolvedModules, ok := p.resolvedModules[file.Path()]; ok { + key := module.ModeAwareCacheKey{Name: imp.Text(), Mode: p.GetModeForUsageLocation(file, imp)} + if resolvedModule, ok := resolvedModules[key]; ok && resolvedModule.IsResolved() { + if !resolvedModule.IsExternalLibraryImport { + continue + } + name := resolvedModule.PackageId.Name + if name == "" { + // node_modules package, but no name in package.json - this can happen in a monorepo package, + // and unfortunately in lots of fourslash tests + name = modulespecifiers.GetPackageNameFromDirectory(resolvedModule.ResolvedFileName) + } + p.resolvedPackageNames.Add(name) + continue + } + } + p.unresolvedPackageNames.Add(imp.Text()) + } + } + } + }) +} + func (p *Program) IsLibFile(sourceFile *ast.SourceFile) bool { _, ok := p.libFiles[sourceFile.Path()] return ok diff --git a/internal/core/compileroptions.go b/internal/core/compileroptions.go index ad6a4ceeca..6fc84c84e1 100644 --- a/internal/core/compileroptions.go +++ b/internal/core/compileroptions.go @@ -166,6 +166,8 @@ type noCopy struct{} func (*noCopy) Lock() {} func (*noCopy) Unlock() {} +var EmptyCompilerOptions = &CompilerOptions{} + var optionsType = reflect.TypeFor[CompilerOptions]() // Clone creates a shallow copy of the CompilerOptions. diff --git a/internal/core/core.go b/internal/core/core.go index 345867fccc..c07284ea3d 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -266,6 +266,16 @@ func FirstNonNil[T any, U comparable](slice []T, f func(T) U) U { return *new(U) } +func FirstNonZero[T comparable](values ...T) T { + var zero T + for _, value := range values { + if value != zero { + return value + } + } + return zero +} + func Concatenate[T any](s1 []T, s2 []T) []T { if len(s2) == 0 { return s1 @@ -317,6 +327,30 @@ func InsertSorted[T any](slice []T, element T, cmp func(T, T) int) []T { return slices.Insert(slice, i, element) } +// MinAllFunc returns all minimum elements from xs according to the comparison function cmp. +func MinAllFunc[T any](xs []T, cmp func(a, b T) int) []T { + if len(xs) == 0 { + return nil + } + + m := xs[0] + mins := []T{m} + + for _, x := range xs[1:] { + c := cmp(x, m) + switch { + case c < 0: + m = x + mins = mins[:0] + mins = append(mins, x) + case c == 0: + mins = append(mins, x) + } + } + + return mins +} + func AppendIfUnique[T comparable](slice []T, element T) []T { if slices.Contains(slice, element) { return slice @@ -621,15 +655,20 @@ func DiffMaps[K comparable, V comparable](m1 map[K]V, m2 map[K]V, onAdded func(K DiffMapsFunc(m1, m2, comparableValuesEqual, onAdded, onRemoved, onChanged) } -func DiffMapsFunc[K comparable, V any](m1 map[K]V, m2 map[K]V, equalValues func(V, V) bool, onAdded func(K, V), onRemoved func(K, V), onChanged func(K, V, V)) { - for k, v2 := range m2 { - if _, ok := m1[k]; !ok { - onAdded(k, v2) +func DiffMapsFunc[K comparable, V1 any, V2 any](m1 map[K]V1, m2 map[K]V2, equalValues func(V1, V2) bool, onAdded func(K, V2), onRemoved func(K, V1), onChanged func(K, V1, V2)) { + if onAdded != nil { + for k, v2 := range m2 { + if _, ok := m1[k]; !ok { + onAdded(k, v2) + } } } + if onChanged == nil && onRemoved == nil { + return + } for k, v1 := range m1 { if v2, ok := m2[k]; ok { - if !equalValues(v1, v2) { + if onChanged != nil && !equalValues(v1, v2) { onChanged(k, v1, v2) } } else { @@ -648,6 +687,24 @@ func CopyMapInto[M1 ~map[K]V, M2 ~map[K]V, K comparable, V any](dst M1, src M2) return dst } +// UnorderedEqual returns true if s1 and s2 contain the same elements, regardless of order. +func UnorderedEqual[T comparable](s1 []T, s2 []T) bool { + if len(s1) != len(s2) { + return false + } + counts := make(map[T]int) + for _, v := range s1 { + counts[v]++ + } + for _, v := range s2 { + counts[v]-- + if counts[v] < 0 { + return false + } + } + return true +} + func Deduplicate[T comparable](slice []T) []T { if len(slice) > 1 { for i, value := range slice { diff --git a/internal/fourslash/_scripts/convertFourslash.mts b/internal/fourslash/_scripts/convertFourslash.mts index d958369d81..45b92ccc74 100644 --- a/internal/fourslash/_scripts/convertFourslash.mts +++ b/internal/fourslash/_scripts/convertFourslash.mts @@ -548,28 +548,9 @@ function parseVerifyApplyCodeActionArgs(arg: ts.Expression): string | undefined } dataProps.push(`ModuleSpecifier: ${getGoStringLiteral(moduleSpecifierInit.text)},`); break; - case "exportName": - const exportNameInit = getStringLiteralLike(dataProp.initializer); - if (!exportNameInit) { - console.error(`Expected string literal for exportName in verify.applyCodeActionFromCompletion data, got ${dataProp.initializer.getText()}`); - return undefined; - } - dataProps.push(`ExportName: ${getGoStringLiteral(exportNameInit.text)},`); - break; - case "fileName": - const fileNameInit = getStringLiteralLike(dataProp.initializer); - if (!fileNameInit) { - console.error(`Expected string literal for fileName in verify.applyCodeActionFromCompletion data, got ${dataProp.initializer.getText()}`); - return undefined; - } - dataProps.push(`FileName: ${getGoStringLiteral(fileNameInit.text)},`); - break; - default: - console.error(`Unrecognized property in verify.applyCodeActionFromCompletion data: ${dataProp.getText()}`); - return undefined; } } - props.push(`AutoImportData: &lsproto.AutoImportData{\n${dataProps.join("\n")}\n},`); + props.push(`AutoImportFix: &lsproto.AutoImportFix{\n${dataProps.join("\n")}\n},`); break; case "description": descInit = getStringLiteralLike(init); @@ -1060,7 +1041,7 @@ function parseExpectedCompletionItem(expr: ts.Expression, codeActionArgs?: Verif break; } itemProps.push(`Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: ${getGoStringLiteral(sourceInit.text)}, }, },`); diff --git a/internal/fourslash/_scripts/crashingTests.txt b/internal/fourslash/_scripts/crashingTests.txt index c8ceb4d30c..63c7a62466 100644 --- a/internal/fourslash/_scripts/crashingTests.txt +++ b/internal/fourslash/_scripts/crashingTests.txt @@ -1,6 +1,4 @@ TestCompletionsAfterJSDoc -TestCompletionsImport_require_addToExisting -TestCompletionsUniqueSymbol_import TestFindReferencesBindingPatternInJsdocNoCrash1 TestFindReferencesBindingPatternInJsdocNoCrash2 TestGetOccurrencesIfElseBroken diff --git a/internal/fourslash/_scripts/failingTests.txt b/internal/fourslash/_scripts/failingTests.txt index be20094672..5105853a8f 100644 --- a/internal/fourslash/_scripts/failingTests.txt +++ b/internal/fourslash/_scripts/failingTests.txt @@ -11,10 +11,8 @@ TestAutoImportCompletionExportListAugmentation1 TestAutoImportCompletionExportListAugmentation2 TestAutoImportCompletionExportListAugmentation3 TestAutoImportCompletionExportListAugmentation4 -TestAutoImportCrossPackage_pathsAndSymlink TestAutoImportCrossProject_symlinks_stripSrc TestAutoImportCrossProject_symlinks_toDist -TestAutoImportCrossProject_symlinks_toSrc TestAutoImportFileExcludePatterns2 TestAutoImportFileExcludePatterns3 TestAutoImportJsDocImport1 @@ -22,22 +20,12 @@ TestAutoImportModuleNone1 TestAutoImportNodeModuleSymlinkRenamed TestAutoImportNodeNextJSRequire TestAutoImportPackageJsonImportsCaseSensitivity -TestAutoImportProvider_exportMap1 +TestAutoImportPackageRootPath +TestAutoImportPathsNodeModules TestAutoImportProvider_exportMap2 -TestAutoImportProvider_exportMap3 -TestAutoImportProvider_exportMap4 TestAutoImportProvider_exportMap5 -TestAutoImportProvider_exportMap6 -TestAutoImportProvider_exportMap7 -TestAutoImportProvider_exportMap8 TestAutoImportProvider_exportMap9 TestAutoImportProvider_globalTypingsCache -TestAutoImportProvider_namespaceSameNameAsIntrinsic -TestAutoImportProvider_pnpm -TestAutoImportProvider_wildcardExports1 -TestAutoImportProvider_wildcardExports2 -TestAutoImportProvider_wildcardExports3 -TestAutoImportProvider4 TestAutoImportProvider9 TestAutoImportSortCaseSensitivity1 TestAutoImportTypeImport1 @@ -132,6 +120,7 @@ TestCompletionListInNamedFunctionExpression TestCompletionListInNamedFunctionExpression1 TestCompletionListInNamedFunctionExpressionWithShadowing TestCompletionListInScope +TestCompletionListInScope_doesNotIncludeAugmentations TestCompletionListInTemplateLiteralParts1 TestCompletionListInUnclosedCommaExpression01 TestCompletionListInUnclosedCommaExpression02 @@ -177,19 +166,19 @@ TestCompletionsImport_default_anonymous TestCompletionsImport_details_withMisspelledName TestCompletionsImport_exportEquals_global TestCompletionsImport_filteredByInvalidPackageJson_direct +TestCompletionsImport_filteredByPackageJson_ambient TestCompletionsImport_filteredByPackageJson_direct TestCompletionsImport_filteredByPackageJson_nested TestCompletionsImport_filteredByPackageJson_peerDependencies TestCompletionsImport_filteredByPackageJson_typesImplicit TestCompletionsImport_filteredByPackageJson_typesOnly -TestCompletionsImport_jsxOpeningTagImportDefault -TestCompletionsImport_mergedReExport +TestCompletionsImport_importType TestCompletionsImport_named_didNotExistBefore +TestCompletionsImport_named_namespaceImportExists TestCompletionsImport_noSemicolons TestCompletionsImport_packageJsonImportsPreference TestCompletionsImport_quoteStyle TestCompletionsImport_reExport_wrongName -TestCompletionsImport_reExportDefault2 TestCompletionsImport_require_addToExisting TestCompletionsImport_typeOnly TestCompletionsImport_umdDefaultNoCrash1 @@ -344,6 +333,8 @@ TestImportNameCodeFix_trailingComma TestImportNameCodeFix_uriStyleNodeCoreModules2 TestImportNameCodeFix_uriStyleNodeCoreModules3 TestImportNameCodeFix_withJson +TestImportNameCodeFixDefaultExport4 +TestImportNameCodeFixDefaultExport7 TestImportNameCodeFixExistingImport10 TestImportNameCodeFixExistingImport11 TestImportNameCodeFixExistingImport8 @@ -362,6 +353,7 @@ TestImportNameCodeFixNewImportFileQuoteStyle2 TestImportNameCodeFixNewImportFileQuoteStyleMixed0 TestImportNameCodeFixNewImportFileQuoteStyleMixed1 TestImportNameCodeFixNewImportTypeRoots1 +TestImportNameCodeFixOptionalImport0 TestImportTypeCompletions1 TestImportTypeCompletions3 TestImportTypeCompletions4 diff --git a/internal/fourslash/_scripts/manualTests.txt b/internal/fourslash/_scripts/manualTests.txt index 5373890ae0..3fda8e48fc 100644 --- a/internal/fourslash/_scripts/manualTests.txt +++ b/internal/fourslash/_scripts/manualTests.txt @@ -21,9 +21,18 @@ quickInfoForOverloadOnConst1 renameDefaultKeyword renameForDefaultExport01 tsxCompletion12 +completionsImport_reExportDefault +completionsImport_reexportTransient jsDocFunctionSignatures2 jsDocFunctionSignatures12 outliningHintSpansForFunction getOutliningSpans outliningForNonCompleteInterfaceDeclaration -incrementalParsingWithJsDoc \ No newline at end of file +incrementalParsingWithJsDoc +autoImportPackageRootPathTypeModule +completionListWithLabel +completionsImport_defaultAndNamedConflict +completionsWithStringReplacementMode1 +jsdocParameterNameCompletion +stringLiteralCompletionsInPositionTypedUsingRest +importNameCodeFix_uriStyleNodeCoreModules1 \ No newline at end of file diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index 488c0beff8..41254911ae 100644 --- a/internal/fourslash/fourslash.go +++ b/internal/fourslash/fourslash.go @@ -620,7 +620,7 @@ func sendRequest[Params, Resp any](t *testing.T, f *FourslashTest, info lsproto. t.Fatalf(prefix+"%s request returned error: %s", info.Method, resp.Error.String()) } if !resultOk { - t.Fatalf(prefix+"Unexpected %s response type: %T", info.Method, resp.Result) + t.Fatalf(prefix+"Unexpected %s response type: %T, error: %v", info.Method, resp.Result, resp.Error) } return result } @@ -928,6 +928,7 @@ type MarkerInput = any // !!! user preferences param // !!! completion context param func (f *FourslashTest) VerifyCompletions(t *testing.T, markerInput MarkerInput, expected *CompletionsExpectedList) VerifyCompletionsResult { + t.Helper() var list *lsproto.CompletionList switch marker := markerInput.(type) { case string: @@ -998,6 +999,12 @@ func (f *FourslashTest) getCompletions(t *testing.T, userPreferences *lsutil.Use defer reset() } result := sendRequest(t, f, lsproto.TextDocumentCompletionInfo, params) + // For performance, the server may return unsorted completion lists. + // The client is expected to sort them by SortText and then by Label. + // We are the client here. + if result.List != nil { + slices.SortStableFunc(result.List.Items, ls.CompareCompletionEntries) + } return result.List } @@ -1075,7 +1082,7 @@ func (f *FourslashTest) verifyCompletionsItems(t *testing.T, prefix string, actu t.Fatal(prefix + "Expected exact completion list but also specified 'unsorted'.") } if len(actual) != len(expected.Exact) { - t.Fatalf(prefix+"Expected %d exact completion items but got %d: %s", len(expected.Exact), len(actual), cmp.Diff(actual, expected.Exact)) + t.Fatalf(prefix+"Expected %d exact completion items but got %d.", len(expected.Exact), len(actual)) } if len(actual) > 0 { f.verifyCompletionsAreExactly(t, prefix, actual, expected.Exact) @@ -1098,13 +1105,13 @@ func (f *FourslashTest) verifyCompletionsItems(t *testing.T, prefix string, actu case string: _, ok := nameToActualItems[item] if !ok { - t.Fatalf("%sLabel '%s' not found in actual items. Actual items: %s", prefix, item, cmp.Diff(actual, nil)) + t.Fatalf("%sLabel '%s' not found in actual items.", prefix, item) } delete(nameToActualItems, item) case *lsproto.CompletionItem: actualItems, ok := nameToActualItems[item.Label] if !ok { - t.Fatalf("%sLabel '%s' not found in actual items. Actual items: %s", prefix, item.Label, cmp.Diff(actual, nil)) + t.Fatalf("%sLabel '%s' not found in actual items.", prefix, item.Label) } actualItem := actualItems[0] actualItems = actualItems[1:] @@ -1130,12 +1137,12 @@ func (f *FourslashTest) verifyCompletionsItems(t *testing.T, prefix string, actu case string: _, ok := nameToActualItems[item] if !ok { - t.Fatalf("%sLabel '%s' not found in actual items. Actual items: %s", prefix, item, cmp.Diff(actual, nil)) + t.Fatalf("%sLabel '%s' not found in actual items.", prefix, item) } case *lsproto.CompletionItem: actualItems, ok := nameToActualItems[item.Label] if !ok { - t.Fatalf("%sLabel '%s' not found in actual items. Actual items: %s", prefix, item.Label, cmp.Diff(actual, nil)) + t.Fatalf("%sLabel '%s' not found in actual items.", prefix, item.Label) } actualItem := actualItems[0] actualItems = actualItems[1:] @@ -1152,7 +1159,7 @@ func (f *FourslashTest) verifyCompletionsItems(t *testing.T, prefix string, actu } for _, exclude := range expected.Excludes { if _, ok := nameToActualItems[exclude]; ok { - t.Fatalf("%sLabel '%s' should not be in actual items but was found. Actual items: %s", prefix, exclude, cmp.Diff(actual, nil)) + t.Fatalf("%sLabel '%s' should not be in actual items but was found.", prefix, exclude) } } } @@ -1190,22 +1197,22 @@ var ( ) func (f *FourslashTest) verifyCompletionItem(t *testing.T, prefix string, actual *lsproto.CompletionItem, expected *lsproto.CompletionItem) { - var actualAutoImportData, expectedAutoImportData *lsproto.AutoImportData + var actualAutoImportFix, expectedAutoImportFix *lsproto.AutoImportFix if actual.Data != nil { - actualAutoImportData = actual.Data.AutoImport + actualAutoImportFix = actual.Data.AutoImport } if expected.Data != nil { - expectedAutoImportData = expected.Data.AutoImport + expectedAutoImportFix = expected.Data.AutoImport } - if (actualAutoImportData == nil) != (expectedAutoImportData == nil) { + if (actualAutoImportFix == nil) != (expectedAutoImportFix == nil) { t.Fatal(prefix + "Mismatch in auto-import data presence") } - if expected.Detail != nil || expected.Documentation != nil || actualAutoImportData != nil { + if expected.Detail != nil || expected.Documentation != nil || actualAutoImportFix != nil { actual = f.resolveCompletionItem(t, actual) } - if actualAutoImportData != nil { + if actualAutoImportFix != nil { assertDeepEqual(t, actual, expected, prefix, autoImportIgnoreOpts) if expected.AdditionalTextEdits == AnyTextEdits { assert.Check(t, actual.AdditionalTextEdits != nil && len(*actual.AdditionalTextEdits) > 0, prefix+" Expected non-nil AdditionalTextEdits for auto-import completion item") @@ -1214,7 +1221,7 @@ func (f *FourslashTest) verifyCompletionItem(t *testing.T, prefix string, actual assertDeepEqual(t, actual.LabelDetails, expected.LabelDetails, prefix+" LabelDetails mismatch") } - assert.Equal(t, actualAutoImportData.ModuleSpecifier, expectedAutoImportData.ModuleSpecifier, prefix+" ModuleSpecifier mismatch") + assert.Equal(t, actualAutoImportFix.ModuleSpecifier, expectedAutoImportFix.ModuleSpecifier, prefix+" ModuleSpecifier mismatch") } else { assertDeepEqual(t, actual, expected, prefix, completionIgnoreOpts) } @@ -1257,7 +1264,7 @@ func assertDeepEqual(t *testing.T, actual any, expected any, prefix string, opts type ApplyCodeActionFromCompletionOptions struct { Name string Source string - AutoImportData *lsproto.AutoImportData + AutoImportFix *lsproto.AutoImportFix Description string NewFileContent *string NewRangeContent *string @@ -1281,13 +1288,14 @@ func (f *FourslashTest) VerifyApplyCodeActionFromCompletion(t *testing.T, marker if item.Label != options.Name || item.Data == nil { return false } + data := item.Data - if options.AutoImportData != nil { - return data.AutoImport != nil && ((data.AutoImport.FileName == options.AutoImportData.FileName) && - (options.AutoImportData.ModuleSpecifier == "" || data.AutoImport.ModuleSpecifier == options.AutoImportData.ModuleSpecifier) && - (options.AutoImportData.ExportName == "" || data.AutoImport.ExportName == options.AutoImportData.ExportName) && - (options.AutoImportData.AmbientModuleName == "" || data.AutoImport.AmbientModuleName == options.AutoImportData.AmbientModuleName) && - data.AutoImport.IsPackageJsonImport == options.AutoImportData.IsPackageJsonImport) + if data == nil { + return false + } + if options.AutoImportFix != nil { + return data.AutoImport != nil && + (options.AutoImportFix.ModuleSpecifier == "" || data.AutoImport.ModuleSpecifier == options.AutoImportFix.ModuleSpecifier) } if data.AutoImport == nil && data.Source != "" && data.Source == options.Source { return true @@ -1314,6 +1322,7 @@ func (f *FourslashTest) VerifyApplyCodeActionFromCompletion(t *testing.T, marker } func (f *FourslashTest) VerifyImportFixAtPosition(t *testing.T, expectedTexts []string, preferences *lsutil.UserPreferences) { + t.Helper() fileName := f.activeFilename ranges := f.Ranges() var filteredRanges []*RangeMarker @@ -1445,6 +1454,7 @@ func (f *FourslashTest) VerifyImportFixModuleSpecifiers( expectedModuleSpecifiers []string, preferences *lsutil.UserPreferences, ) { + t.Helper() f.GoToMarker(t, markerName) if preferences != nil { diff --git a/internal/fourslash/statebaseline.go b/internal/fourslash/statebaseline.go index df2ac9189b..68fa153525 100644 --- a/internal/fourslash/statebaseline.go +++ b/internal/fourslash/statebaseline.go @@ -339,7 +339,7 @@ func (f *FourslashTest) printOpenFilesDiff(t *testing.T, snapshot *project.Snaps options := diffTableOptions{indent: " ", sortKeys: true} for fileName := range f.openFiles { path := tspath.ToPath(fileName, "/", f.vfs.UseCaseSensitiveFileNames()) - defaultProject := snapshot.ProjectCollection.GetDefaultProject(fileName, path) + defaultProject := snapshot.ProjectCollection.GetDefaultProject(path) newFileInfo := &openFileInfo{} if defaultProject != nil { newFileInfo.defaultProjectName = defaultProject.Name() diff --git a/internal/fourslash/tests/autoImportErrorMixedExportKinds_test.go b/internal/fourslash/tests/autoImportErrorMixedExportKinds_test.go new file mode 100644 index 0000000000..0c47738f08 --- /dev/null +++ b/internal/fourslash/tests/autoImportErrorMixedExportKinds_test.go @@ -0,0 +1,28 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestAutoImportErrorMixedExportKinds(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @Filename: a.ts +export function foo(): number { + return 10 +} + +const bar = 20; +export { bar as foo }; + +// @Filename: b.ts +foo/**/ +` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + // Verify we don't crash from the mixed exports + f.BaselineAutoImportsCompletions(t, []string{""}) +} diff --git a/internal/fourslash/tests/autoImportModuleAugmentation_test.go b/internal/fourslash/tests/autoImportModuleAugmentation_test.go new file mode 100644 index 0000000000..97472535b2 --- /dev/null +++ b/internal/fourslash/tests/autoImportModuleAugmentation_test.go @@ -0,0 +1,30 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestAutoImportModuleAugmentation(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @Filename: /a.ts +export interface Foo { + x: number; +} + +// @Filename: /b.ts +export {}; +declare module "./a" { + export const Foo: any; +} + +// @Filename: /c.ts +Foo/**/ +` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.BaselineAutoImportsCompletions(t, []string{""}) +} diff --git a/internal/fourslash/tests/gen/autoImportFileExcludePatterns2_test.go b/internal/fourslash/tests/gen/autoImportFileExcludePatterns2_test.go index eb26fe8703..d8b4d253a2 100644 --- a/internal/fourslash/tests/gen/autoImportFileExcludePatterns2_test.go +++ b/internal/fourslash/tests/gen/autoImportFileExcludePatterns2_test.go @@ -41,7 +41,7 @@ Button/**/` &lsproto.CompletionItem{ Label: "Button", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./lib/main", }, }, diff --git a/internal/fourslash/tests/gen/autoImportFileExcludePatterns3_test.go b/internal/fourslash/tests/gen/autoImportFileExcludePatterns3_test.go index 084d51881f..45f1c56285 100644 --- a/internal/fourslash/tests/gen/autoImportFileExcludePatterns3_test.go +++ b/internal/fourslash/tests/gen/autoImportFileExcludePatterns3_test.go @@ -39,7 +39,7 @@ declare module "foo" { &lsproto.CompletionItem{ Label: "x", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "foo", }, }, @@ -49,7 +49,7 @@ declare module "foo" { &lsproto.CompletionItem{ Label: "y", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "foo", }, }, diff --git a/internal/fourslash/tests/gen/autoImportModuleNone2_test.go b/internal/fourslash/tests/gen/autoImportModuleNone2_test.go index 1a60437e36..1bacc49bc9 100644 --- a/internal/fourslash/tests/gen/autoImportModuleNone2_test.go +++ b/internal/fourslash/tests/gen/autoImportModuleNone2_test.go @@ -34,7 +34,7 @@ export const x: number; &lsproto.CompletionItem{ Label: "x", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "dep", }, }, diff --git a/internal/fourslash/tests/gen/autoImportPathsAliasesAndBarrels_test.go b/internal/fourslash/tests/gen/autoImportPathsAliasesAndBarrels_test.go index 3cebefb1c4..44942de609 100644 --- a/internal/fourslash/tests/gen/autoImportPathsAliasesAndBarrels_test.go +++ b/internal/fourslash/tests/gen/autoImportPathsAliasesAndBarrels_test.go @@ -51,7 +51,7 @@ func TestAutoImportPathsAliasesAndBarrels(t *testing.T) { &lsproto.CompletionItem{ Label: "Thing2A", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./thing2A", }, }, @@ -61,7 +61,7 @@ func TestAutoImportPathsAliasesAndBarrels(t *testing.T) { &lsproto.CompletionItem{ Label: "Thing1B", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "~/dirB", }, }, @@ -71,7 +71,7 @@ func TestAutoImportPathsAliasesAndBarrels(t *testing.T) { &lsproto.CompletionItem{ Label: "Thing2B", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "~/dirB", }, }, diff --git a/internal/fourslash/tests/gen/autoImportProvider6_test.go b/internal/fourslash/tests/gen/autoImportProvider6_test.go index d520ba516e..96f2d7096d 100644 --- a/internal/fourslash/tests/gen/autoImportProvider6_test.go +++ b/internal/fourslash/tests/gen/autoImportProvider6_test.go @@ -40,7 +40,7 @@ Component/**/` Label: "Component", AdditionalTextEdits: fourslash.AnyTextEdits, Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "react", }, }, diff --git a/internal/fourslash/tests/gen/autoImportProvider_exportMap1_test.go b/internal/fourslash/tests/gen/autoImportProvider_exportMap1_test.go index 8bccceaf8d..f09e4ed228 100644 --- a/internal/fourslash/tests/gen/autoImportProvider_exportMap1_test.go +++ b/internal/fourslash/tests/gen/autoImportProvider_exportMap1_test.go @@ -62,7 +62,7 @@ fooFrom/**/` &lsproto.CompletionItem{ Label: "fooFromIndex", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "dependency", }, }, @@ -72,7 +72,7 @@ fooFrom/**/` &lsproto.CompletionItem{ Label: "fooFromLol", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "dependency/lol", }, }, diff --git a/internal/fourslash/tests/gen/autoImportProvider_exportMap2_test.go b/internal/fourslash/tests/gen/autoImportProvider_exportMap2_test.go index abab093258..3808434490 100644 --- a/internal/fourslash/tests/gen/autoImportProvider_exportMap2_test.go +++ b/internal/fourslash/tests/gen/autoImportProvider_exportMap2_test.go @@ -65,7 +65,7 @@ fooFrom/**/` &lsproto.CompletionItem{ Label: "fooFromIndex", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "dependency", }, }, diff --git a/internal/fourslash/tests/gen/autoImportProvider_exportMap3_test.go b/internal/fourslash/tests/gen/autoImportProvider_exportMap3_test.go index 9257feb475..faa80cfab3 100644 --- a/internal/fourslash/tests/gen/autoImportProvider_exportMap3_test.go +++ b/internal/fourslash/tests/gen/autoImportProvider_exportMap3_test.go @@ -55,7 +55,7 @@ fooFrom/**/` &lsproto.CompletionItem{ Label: "fooFromLol", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "dependency", }, }, diff --git a/internal/fourslash/tests/gen/autoImportProvider_exportMap4_test.go b/internal/fourslash/tests/gen/autoImportProvider_exportMap4_test.go index f3fd417586..fa1ba7871e 100644 --- a/internal/fourslash/tests/gen/autoImportProvider_exportMap4_test.go +++ b/internal/fourslash/tests/gen/autoImportProvider_exportMap4_test.go @@ -58,7 +58,7 @@ fooFrom/**/` &lsproto.CompletionItem{ Label: "fooFromIndex", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "dependency", }, }, diff --git a/internal/fourslash/tests/gen/autoImportProvider_exportMap5_test.go b/internal/fourslash/tests/gen/autoImportProvider_exportMap5_test.go index e407094ba9..a5f1037fcb 100644 --- a/internal/fourslash/tests/gen/autoImportProvider_exportMap5_test.go +++ b/internal/fourslash/tests/gen/autoImportProvider_exportMap5_test.go @@ -73,7 +73,7 @@ fooFrom/**/` &lsproto.CompletionItem{ Label: "fooFromIndex", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "dependency", }, }, @@ -83,7 +83,7 @@ fooFrom/**/` &lsproto.CompletionItem{ Label: "fooFromLol", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "dependency/lol", }, }, diff --git a/internal/fourslash/tests/gen/autoImportProvider_exportMap6_test.go b/internal/fourslash/tests/gen/autoImportProvider_exportMap6_test.go index 43a8389555..be9f2dd8c5 100644 --- a/internal/fourslash/tests/gen/autoImportProvider_exportMap6_test.go +++ b/internal/fourslash/tests/gen/autoImportProvider_exportMap6_test.go @@ -80,7 +80,7 @@ fooFrom/**/` &lsproto.CompletionItem{ Label: "fooFromIndex", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "dependency", }, }, @@ -90,7 +90,7 @@ fooFrom/**/` &lsproto.CompletionItem{ Label: "fooFromLol", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "dependency/lol", }, }, diff --git a/internal/fourslash/tests/gen/autoImportProvider_exportMap7_test.go b/internal/fourslash/tests/gen/autoImportProvider_exportMap7_test.go index 03661a308e..2d15900d22 100644 --- a/internal/fourslash/tests/gen/autoImportProvider_exportMap7_test.go +++ b/internal/fourslash/tests/gen/autoImportProvider_exportMap7_test.go @@ -64,7 +64,7 @@ fooFrom/**/` &lsproto.CompletionItem{ Label: "fooFromIndex", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "dependency", }, }, @@ -74,7 +74,7 @@ fooFrom/**/` &lsproto.CompletionItem{ Label: "fooFromLol", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "dependency/lol", }, }, diff --git a/internal/fourslash/tests/gen/autoImportProvider_exportMap8_test.go b/internal/fourslash/tests/gen/autoImportProvider_exportMap8_test.go index b274b1a89a..3fa0ee44cd 100644 --- a/internal/fourslash/tests/gen/autoImportProvider_exportMap8_test.go +++ b/internal/fourslash/tests/gen/autoImportProvider_exportMap8_test.go @@ -64,7 +64,7 @@ fooFrom/*mts*/` &lsproto.CompletionItem{ Label: "fooFromLol", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "dependency/lol", }, }, @@ -89,7 +89,7 @@ fooFrom/*mts*/` &lsproto.CompletionItem{ Label: "fooFromIndex", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "dependency/lol", }, }, diff --git a/internal/fourslash/tests/gen/autoImportProvider_exportMap9_test.go b/internal/fourslash/tests/gen/autoImportProvider_exportMap9_test.go index b04572f70e..079596b583 100644 --- a/internal/fourslash/tests/gen/autoImportProvider_exportMap9_test.go +++ b/internal/fourslash/tests/gen/autoImportProvider_exportMap9_test.go @@ -59,7 +59,7 @@ fooFrom/**/` &lsproto.CompletionItem{ Label: "fooFromIndex", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "dependency/lol", }, }, diff --git a/internal/fourslash/tests/gen/autoImportProvider_globalTypingsCache_test.go b/internal/fourslash/tests/gen/autoImportProvider_globalTypingsCache_test.go index 2f3de96fcf..5231db7143 100644 --- a/internal/fourslash/tests/gen/autoImportProvider_globalTypingsCache_test.go +++ b/internal/fourslash/tests/gen/autoImportProvider_globalTypingsCache_test.go @@ -46,7 +46,7 @@ BrowserRouter/**/` &lsproto.CompletionItem{ Label: "BrowserRouterFromDts", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "react-router-dom", }, }, diff --git a/internal/fourslash/tests/gen/autoImportProvider_namespaceSameNameAsIntrinsic_test.go b/internal/fourslash/tests/gen/autoImportProvider_namespaceSameNameAsIntrinsic_test.go index 9e93cfb541..13ed373b8e 100644 --- a/internal/fourslash/tests/gen/autoImportProvider_namespaceSameNameAsIntrinsic_test.go +++ b/internal/fourslash/tests/gen/autoImportProvider_namespaceSameNameAsIntrinsic_test.go @@ -47,7 +47,7 @@ type A = { name: string/**/ }` Label: "string", SortText: PtrTo(string(ls.SortTextAutoImportSuggestions)), Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "fp-ts", }, }, diff --git a/internal/fourslash/tests/gen/autoImportProvider_wildcardExports1_test.go b/internal/fourslash/tests/gen/autoImportProvider_wildcardExports1_test.go index c191718112..e725877fce 100644 --- a/internal/fourslash/tests/gen/autoImportProvider_wildcardExports1_test.go +++ b/internal/fourslash/tests/gen/autoImportProvider_wildcardExports1_test.go @@ -68,7 +68,7 @@ export const d1: number; &lsproto.CompletionItem{ Label: "a1", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "pkg/a1", }, }, @@ -78,7 +78,7 @@ export const d1: number; &lsproto.CompletionItem{ Label: "b1", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "pkg/b/b1.js", }, }, @@ -88,7 +88,7 @@ export const d1: number; &lsproto.CompletionItem{ Label: "c1", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "pkg/c/c1.js", }, }, @@ -98,7 +98,7 @@ export const d1: number; &lsproto.CompletionItem{ Label: "c2", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "pkg/c/subfolder/c2.mjs", }, }, @@ -108,7 +108,7 @@ export const d1: number; &lsproto.CompletionItem{ Label: "d1", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "pkg/d/d1", }, }, diff --git a/internal/fourslash/tests/gen/autoImportProvider_wildcardExports2_test.go b/internal/fourslash/tests/gen/autoImportProvider_wildcardExports2_test.go index 213223ca54..b8aa2d3a94 100644 --- a/internal/fourslash/tests/gen/autoImportProvider_wildcardExports2_test.go +++ b/internal/fourslash/tests/gen/autoImportProvider_wildcardExports2_test.go @@ -56,7 +56,7 @@ export function test(): void; &lsproto.CompletionItem{ Label: "test", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "pkg/core/test", }, }, diff --git a/internal/fourslash/tests/gen/autoImportProvider_wildcardExports3_test.go b/internal/fourslash/tests/gen/autoImportProvider_wildcardExports3_test.go index 576947151d..f2df0bee2b 100644 --- a/internal/fourslash/tests/gen/autoImportProvider_wildcardExports3_test.go +++ b/internal/fourslash/tests/gen/autoImportProvider_wildcardExports3_test.go @@ -60,7 +60,7 @@ export const Card = () => null; &lsproto.CompletionItem{ Label: "Card", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "@repo/ui/Card", }, }, diff --git a/internal/fourslash/tests/gen/autoImportReExportFromAmbientModule_test.go b/internal/fourslash/tests/gen/autoImportReExportFromAmbientModule_test.go index ccbc98af20..7bc0cd8167 100644 --- a/internal/fourslash/tests/gen/autoImportReExportFromAmbientModule_test.go +++ b/internal/fourslash/tests/gen/autoImportReExportFromAmbientModule_test.go @@ -42,7 +42,7 @@ access/**/` &lsproto.CompletionItem{ Label: "accessSync", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "fs", }, }, @@ -52,7 +52,7 @@ access/**/` &lsproto.CompletionItem{ Label: "accessSync", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "fs-extra", }, }, @@ -69,9 +69,7 @@ access/**/` NewFileContent: PtrTo(`import { accessSync } from "fs-extra"; access`), - AutoImportData: &lsproto.AutoImportData{ - ExportName: "accessSync", - FileName: "/home/src/workspaces/project/node_modules/@types/fs-extra/index.d.ts", + AutoImportFix: &lsproto.AutoImportFix{ ModuleSpecifier: "fs-extra", }, }) diff --git a/internal/fourslash/tests/gen/autoImportSameNameDefaultExported_test.go b/internal/fourslash/tests/gen/autoImportSameNameDefaultExported_test.go index e913cbc444..4c1ab7852e 100644 --- a/internal/fourslash/tests/gen/autoImportSameNameDefaultExported_test.go +++ b/internal/fourslash/tests/gen/autoImportSameNameDefaultExported_test.go @@ -37,7 +37,7 @@ Table/**/` &lsproto.CompletionItem{ Label: "Table", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "antd", }, }, @@ -47,7 +47,7 @@ Table/**/` &lsproto.CompletionItem{ Label: "Table", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "rc-table", }, }, diff --git a/internal/fourslash/tests/gen/autoImportSortCaseSensitivity2_test.go b/internal/fourslash/tests/gen/autoImportSortCaseSensitivity2_test.go index a731fa3a69..51a0ea271c 100644 --- a/internal/fourslash/tests/gen/autoImportSortCaseSensitivity2_test.go +++ b/internal/fourslash/tests/gen/autoImportSortCaseSensitivity2_test.go @@ -35,7 +35,7 @@ f/**/;` &lsproto.CompletionItem{ Label: "foo", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, diff --git a/internal/fourslash/tests/gen/autoImportSpecifierExcludeRegexes1_test.go b/internal/fourslash/tests/gen/autoImportSpecifierExcludeRegexes1_test.go index b5f6ad8dbb..da57ab8751 100644 --- a/internal/fourslash/tests/gen/autoImportSpecifierExcludeRegexes1_test.go +++ b/internal/fourslash/tests/gen/autoImportSpecifierExcludeRegexes1_test.go @@ -48,7 +48,7 @@ x/**/` &lsproto.CompletionItem{ Label: "x", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "ambient", }, }, @@ -58,7 +58,7 @@ x/**/` &lsproto.CompletionItem{ Label: "x", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "ambient/utils", }, }, diff --git a/internal/fourslash/tests/gen/autoImportTypeOnlyPreferred1_test.go b/internal/fourslash/tests/gen/autoImportTypeOnlyPreferred1_test.go index ce2a39b446..2efa76c718 100644 --- a/internal/fourslash/tests/gen/autoImportTypeOnlyPreferred1_test.go +++ b/internal/fourslash/tests/gen/autoImportTypeOnlyPreferred1_test.go @@ -42,7 +42,7 @@ export interface VFS { &lsproto.CompletionItem{ Label: "ts", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./ts", }, }, diff --git a/internal/fourslash/tests/gen/autoImportVerbatimTypeOnly1_test.go b/internal/fourslash/tests/gen/autoImportVerbatimTypeOnly1_test.go index 8ae8ea3969..1a1e8ab6d8 100644 --- a/internal/fourslash/tests/gen/autoImportVerbatimTypeOnly1_test.go +++ b/internal/fourslash/tests/gen/autoImportVerbatimTypeOnly1_test.go @@ -27,9 +27,7 @@ const x: /**/` Name: "I", Source: "./mod", Description: "Add import from \"./mod.js\"", - AutoImportData: &lsproto.AutoImportData{ - ExportName: "I", - FileName: "/mod.ts", + AutoImportFix: &lsproto.AutoImportFix{ ModuleSpecifier: "./mod.js", }, NewFileContent: PtrTo(`import type { I } from "./mod.js"; @@ -41,9 +39,7 @@ const x: `), Name: "C", Source: "./mod", Description: "Update import from \"./mod.js\"", - AutoImportData: &lsproto.AutoImportData{ - ExportName: "C", - FileName: "/mod.ts", + AutoImportFix: &lsproto.AutoImportFix{ ModuleSpecifier: "./mod.js", }, NewFileContent: PtrTo(`import { C, type I } from "./mod.js"; diff --git a/internal/fourslash/tests/gen/completionForObjectProperty_test.go b/internal/fourslash/tests/gen/completionForObjectProperty_test.go index 53139f0e03..be41873772 100644 --- a/internal/fourslash/tests/gen/completionForObjectProperty_test.go +++ b/internal/fourslash/tests/gen/completionForObjectProperty_test.go @@ -45,7 +45,7 @@ const test8: { foo: string } = { foo/*8*/ }` &lsproto.CompletionItem{ Label: "foo", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, @@ -66,7 +66,7 @@ const test8: { foo: string } = { foo/*8*/ }` &lsproto.CompletionItem{ Label: "foo", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, @@ -87,7 +87,7 @@ const test8: { foo: string } = { foo/*8*/ }` &lsproto.CompletionItem{ Label: "foo", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, @@ -108,7 +108,7 @@ const test8: { foo: string } = { foo/*8*/ }` &lsproto.CompletionItem{ Label: "foo", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, @@ -129,7 +129,7 @@ const test8: { foo: string } = { foo/*8*/ }` &lsproto.CompletionItem{ Label: "foo", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, @@ -150,7 +150,7 @@ const test8: { foo: string } = { foo/*8*/ }` &lsproto.CompletionItem{ Label: "foo", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, diff --git a/internal/fourslash/tests/gen/completionPropertyShorthandForObjectLiteral5_test.go b/internal/fourslash/tests/gen/completionPropertyShorthandForObjectLiteral5_test.go index 88327b3e1e..f28d7e3307 100644 --- a/internal/fourslash/tests/gen/completionPropertyShorthandForObjectLiteral5_test.go +++ b/internal/fourslash/tests/gen/completionPropertyShorthandForObjectLiteral5_test.go @@ -33,7 +33,7 @@ const obj = { exp/**/` &lsproto.CompletionItem{ Label: "exportedConstant", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, diff --git a/internal/fourslash/tests/gen/completionsImportBaseUrl_test.go b/internal/fourslash/tests/gen/completionsImportBaseUrl_test.go index 9153cfd690..3942239797 100644 --- a/internal/fourslash/tests/gen/completionsImportBaseUrl_test.go +++ b/internal/fourslash/tests/gen/completionsImportBaseUrl_test.go @@ -38,7 +38,7 @@ fo/**/` &lsproto.CompletionItem{ Label: "foo", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, diff --git a/internal/fourslash/tests/gen/completionsImportDefaultExportCrash2_test.go b/internal/fourslash/tests/gen/completionsImportDefaultExportCrash2_test.go index b24c1842ca..de13c022b8 100644 --- a/internal/fourslash/tests/gen/completionsImportDefaultExportCrash2_test.go +++ b/internal/fourslash/tests/gen/completionsImportDefaultExportCrash2_test.go @@ -56,7 +56,7 @@ export default methods.$; Label: "$", AdditionalTextEdits: fourslash.AnyTextEdits, Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "dom7", }, }, @@ -66,7 +66,7 @@ export default methods.$; Label: "Dom7", AdditionalTextEdits: fourslash.AnyTextEdits, Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./dom7", }, }, diff --git a/internal/fourslash/tests/gen/completionsImportPathsConflict_test.go b/internal/fourslash/tests/gen/completionsImportPathsConflict_test.go index 9c763b8b63..75b9061bfe 100644 --- a/internal/fourslash/tests/gen/completionsImportPathsConflict_test.go +++ b/internal/fourslash/tests/gen/completionsImportPathsConflict_test.go @@ -44,7 +44,7 @@ import {} from "@reduxjs/toolkit"; &lsproto.CompletionItem{ Label: "configureStore", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "@reduxjs/toolkit", }, }, @@ -57,9 +57,7 @@ import {} from "@reduxjs/toolkit"; f.VerifyApplyCodeActionFromCompletion(t, PtrTo(""), &fourslash.ApplyCodeActionFromCompletionOptions{ Name: "configureStore", Source: "@reduxjs/toolkit", - AutoImportData: &lsproto.AutoImportData{ - ExportName: "configureStore", - FileName: "/src/configureStore.ts", + AutoImportFix: &lsproto.AutoImportFix{ ModuleSpecifier: "@reduxjs/toolkit", }, Description: "Update import from \"@reduxjs/toolkit\"", diff --git a/internal/fourslash/tests/gen/completionsImportTypeKeyword_test.go b/internal/fourslash/tests/gen/completionsImportTypeKeyword_test.go index f8b0c34309..8090966cdc 100644 --- a/internal/fourslash/tests/gen/completionsImportTypeKeyword_test.go +++ b/internal/fourslash/tests/gen/completionsImportTypeKeyword_test.go @@ -38,7 +38,7 @@ type/**/` &lsproto.CompletionItem{ Label: "type", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "os", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_46332_test.go b/internal/fourslash/tests/gen/completionsImport_46332_test.go index 1387d19c84..d05556844d 100644 --- a/internal/fourslash/tests/gen/completionsImport_46332_test.go +++ b/internal/fourslash/tests/gen/completionsImport_46332_test.go @@ -78,7 +78,7 @@ ref/**/` &lsproto.CompletionItem{ Label: "ref", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "vue", }, }, @@ -89,13 +89,10 @@ ref/**/` }, }) f.VerifyApplyCodeActionFromCompletion(t, PtrTo(""), &fourslash.ApplyCodeActionFromCompletionOptions{ - Name: "ref", - Source: "vue", - Description: "Update import from \"vue\"", - AutoImportData: &lsproto.AutoImportData{ - ExportName: "ref", - FileName: "/node_modules/vue/dist/vue.d.ts", - }, + Name: "ref", + Source: "vue", + Description: "Update import from \"vue\"", + AutoImportFix: &lsproto.AutoImportFix{}, NewFileContent: PtrTo(`import { ref } from "vue"; ref`), }) diff --git a/internal/fourslash/tests/gen/completionsImport_ambient_test.go b/internal/fourslash/tests/gen/completionsImport_ambient_test.go index 7671627582..f671b2f52f 100644 --- a/internal/fourslash/tests/gen/completionsImport_ambient_test.go +++ b/internal/fourslash/tests/gen/completionsImport_ambient_test.go @@ -46,7 +46,7 @@ Ba/**/` &lsproto.CompletionItem{ Label: "Bar", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "path1", }, }, @@ -56,7 +56,7 @@ Ba/**/` &lsproto.CompletionItem{ Label: "Bar", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "path2longer", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_augmentation_test.go b/internal/fourslash/tests/gen/completionsImport_augmentation_test.go index 164dcb2a29..84020d3a06 100644 --- a/internal/fourslash/tests/gen/completionsImport_augmentation_test.go +++ b/internal/fourslash/tests/gen/completionsImport_augmentation_test.go @@ -37,7 +37,7 @@ declare module "./a" { Label: "foo", Detail: PtrTo("const foo: 0"), Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, @@ -48,7 +48,7 @@ declare module "./a" { Label: "bar", Detail: PtrTo("const bar: 0"), Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_compilerOptionsModule_test.go b/internal/fourslash/tests/gen/completionsImport_compilerOptionsModule_test.go index ed7c0aef83..6fb86812cc 100644 --- a/internal/fourslash/tests/gen/completionsImport_compilerOptionsModule_test.go +++ b/internal/fourslash/tests/gen/completionsImport_compilerOptionsModule_test.go @@ -51,7 +51,7 @@ fo/*dts*/` &lsproto.CompletionItem{ Label: "foo", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "a", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_defaultFalsePositive_test.go b/internal/fourslash/tests/gen/completionsImport_defaultFalsePositive_test.go index 96f09c062b..d802463651 100644 --- a/internal/fourslash/tests/gen/completionsImport_defaultFalsePositive_test.go +++ b/internal/fourslash/tests/gen/completionsImport_defaultFalsePositive_test.go @@ -35,7 +35,7 @@ conca/**/` &lsproto.CompletionItem{ Label: "concat", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "bar/concat", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_default_addToNamedImports_test.go b/internal/fourslash/tests/gen/completionsImport_default_addToNamedImports_test.go index 48cc77bc86..c26168bde7 100644 --- a/internal/fourslash/tests/gen/completionsImport_default_addToNamedImports_test.go +++ b/internal/fourslash/tests/gen/completionsImport_default_addToNamedImports_test.go @@ -33,7 +33,7 @@ f/**/;` &lsproto.CompletionItem{ Label: "foo", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_default_addToNamespaceImport_test.go b/internal/fourslash/tests/gen/completionsImport_default_addToNamespaceImport_test.go index aa29c29cc7..822b42bd86 100644 --- a/internal/fourslash/tests/gen/completionsImport_default_addToNamespaceImport_test.go +++ b/internal/fourslash/tests/gen/completionsImport_default_addToNamespaceImport_test.go @@ -32,7 +32,7 @@ f/**/;` &lsproto.CompletionItem{ Label: "foo", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_default_alreadyExistedWithRename_test.go b/internal/fourslash/tests/gen/completionsImport_default_alreadyExistedWithRename_test.go index 45ab15e30c..02bf02d012 100644 --- a/internal/fourslash/tests/gen/completionsImport_default_alreadyExistedWithRename_test.go +++ b/internal/fourslash/tests/gen/completionsImport_default_alreadyExistedWithRename_test.go @@ -32,7 +32,7 @@ f/**/;` &lsproto.CompletionItem{ Label: "foo", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_default_anonymous_test.go b/internal/fourslash/tests/gen/completionsImport_default_anonymous_test.go index a424df0f8e..10b5d808f4 100644 --- a/internal/fourslash/tests/gen/completionsImport_default_anonymous_test.go +++ b/internal/fourslash/tests/gen/completionsImport_default_anonymous_test.go @@ -46,7 +46,7 @@ fooB/*1*/` &lsproto.CompletionItem{ Label: "fooBar", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./foo-bar", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_default_didNotExistBefore_test.go b/internal/fourslash/tests/gen/completionsImport_default_didNotExistBefore_test.go index 572707ec34..e6a7fa7531 100644 --- a/internal/fourslash/tests/gen/completionsImport_default_didNotExistBefore_test.go +++ b/internal/fourslash/tests/gen/completionsImport_default_didNotExistBefore_test.go @@ -32,7 +32,7 @@ f/**/;` &lsproto.CompletionItem{ Label: "foo", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_default_exportDefaultIdentifier_test.go b/internal/fourslash/tests/gen/completionsImport_default_exportDefaultIdentifier_test.go index 0e8bd33cb9..a3f255dda0 100644 --- a/internal/fourslash/tests/gen/completionsImport_default_exportDefaultIdentifier_test.go +++ b/internal/fourslash/tests/gen/completionsImport_default_exportDefaultIdentifier_test.go @@ -34,7 +34,7 @@ f/**/;` &lsproto.CompletionItem{ Label: "foo", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_default_fromMergedDeclarations_test.go b/internal/fourslash/tests/gen/completionsImport_default_fromMergedDeclarations_test.go index ffe65452de..4d1979efce 100644 --- a/internal/fourslash/tests/gen/completionsImport_default_fromMergedDeclarations_test.go +++ b/internal/fourslash/tests/gen/completionsImport_default_fromMergedDeclarations_test.go @@ -38,7 +38,7 @@ declare module "m" { &lsproto.CompletionItem{ Label: "M", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "m", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_default_reExport_test.go b/internal/fourslash/tests/gen/completionsImport_default_reExport_test.go index 73aee66f6c..0a93ac1d2c 100644 --- a/internal/fourslash/tests/gen/completionsImport_default_reExport_test.go +++ b/internal/fourslash/tests/gen/completionsImport_default_reExport_test.go @@ -42,7 +42,7 @@ export default foo.b;` &lsproto.CompletionItem{ Label: "a", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./file1", }, }, @@ -52,7 +52,7 @@ export default foo.b;` &lsproto.CompletionItem{ Label: "b", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./file1", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_default_symbolName_test.go b/internal/fourslash/tests/gen/completionsImport_default_symbolName_test.go index 0ee79308ec..0248f4d554 100644 --- a/internal/fourslash/tests/gen/completionsImport_default_symbolName_test.go +++ b/internal/fourslash/tests/gen/completionsImport_default_symbolName_test.go @@ -41,7 +41,7 @@ R/*0*/` Label: "RangeParser", Kind: PtrTo(lsproto.CompletionItemKindFunction), Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "range-parser", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_details_withMisspelledName_test.go b/internal/fourslash/tests/gen/completionsImport_details_withMisspelledName_test.go index b6489903c0..4a22ecd20b 100644 --- a/internal/fourslash/tests/gen/completionsImport_details_withMisspelledName_test.go +++ b/internal/fourslash/tests/gen/completionsImport_details_withMisspelledName_test.go @@ -34,9 +34,7 @@ acb;`), f.VerifyApplyCodeActionFromCompletion(t, PtrTo("2"), &fourslash.ApplyCodeActionFromCompletionOptions{ Name: "abc", Source: "./a", - AutoImportData: &lsproto.AutoImportData{ - ExportName: "abc", - FileName: "/a.ts", + AutoImportFix: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, Description: "Add import from \"./a\"", diff --git a/internal/fourslash/tests/gen/completionsImport_duplicatePackages_scopedTypesAndNotTypes_test.go b/internal/fourslash/tests/gen/completionsImport_duplicatePackages_scopedTypesAndNotTypes_test.go index 5f0b5a1739..b429f47723 100644 --- a/internal/fourslash/tests/gen/completionsImport_duplicatePackages_scopedTypesAndNotTypes_test.go +++ b/internal/fourslash/tests/gen/completionsImport_duplicatePackages_scopedTypesAndNotTypes_test.go @@ -52,7 +52,7 @@ import "react"; &lsproto.CompletionItem{ Label: "render", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "@scope/react-dom", }, }, @@ -62,7 +62,7 @@ import "react"; &lsproto.CompletionItem{ Label: "useState", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "@scope/react", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_duplicatePackages_scopedTypes_test.go b/internal/fourslash/tests/gen/completionsImport_duplicatePackages_scopedTypes_test.go index bce8f0f932..2a18d36022 100644 --- a/internal/fourslash/tests/gen/completionsImport_duplicatePackages_scopedTypes_test.go +++ b/internal/fourslash/tests/gen/completionsImport_duplicatePackages_scopedTypes_test.go @@ -52,7 +52,7 @@ import "@scope/react"; &lsproto.CompletionItem{ Label: "render", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "@scope/react-dom", }, }, @@ -62,7 +62,7 @@ import "@scope/react"; &lsproto.CompletionItem{ Label: "useState", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "@scope/react", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_duplicatePackages_scoped_test.go b/internal/fourslash/tests/gen/completionsImport_duplicatePackages_scoped_test.go index ac34c3da44..7d9a607244 100644 --- a/internal/fourslash/tests/gen/completionsImport_duplicatePackages_scoped_test.go +++ b/internal/fourslash/tests/gen/completionsImport_duplicatePackages_scoped_test.go @@ -52,7 +52,7 @@ import "@scope/react"; &lsproto.CompletionItem{ Label: "render", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "@scope/react-dom", }, }, @@ -62,7 +62,7 @@ import "@scope/react"; &lsproto.CompletionItem{ Label: "useState", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "@scope/react", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_duplicatePackages_typesAndNotTypes_test.go b/internal/fourslash/tests/gen/completionsImport_duplicatePackages_typesAndNotTypes_test.go index b61bd1e826..6eaadee5fc 100644 --- a/internal/fourslash/tests/gen/completionsImport_duplicatePackages_typesAndNotTypes_test.go +++ b/internal/fourslash/tests/gen/completionsImport_duplicatePackages_typesAndNotTypes_test.go @@ -52,7 +52,7 @@ useState/**/` &lsproto.CompletionItem{ Label: "useState", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "react", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_duplicatePackages_types_test.go b/internal/fourslash/tests/gen/completionsImport_duplicatePackages_types_test.go index 682619b529..aa075ea68b 100644 --- a/internal/fourslash/tests/gen/completionsImport_duplicatePackages_types_test.go +++ b/internal/fourslash/tests/gen/completionsImport_duplicatePackages_types_test.go @@ -52,7 +52,7 @@ import "react"; &lsproto.CompletionItem{ Label: "render", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "react-dom", }, }, @@ -62,7 +62,7 @@ import "react"; &lsproto.CompletionItem{ Label: "useState", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "react", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_exportEqualsNamespace_noDuplicate_test.go b/internal/fourslash/tests/gen/completionsImport_exportEqualsNamespace_noDuplicate_test.go index f88c4048eb..fbffa4dcaf 100644 --- a/internal/fourslash/tests/gen/completionsImport_exportEqualsNamespace_noDuplicate_test.go +++ b/internal/fourslash/tests/gen/completionsImport_exportEqualsNamespace_noDuplicate_test.go @@ -40,7 +40,7 @@ import * as a from "a"; &lsproto.CompletionItem{ Label: "foo", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "a", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_exportEquals_anonymous_test.go b/internal/fourslash/tests/gen/completionsImport_exportEquals_anonymous_test.go index db5eff488b..6fe56b7721 100644 --- a/internal/fourslash/tests/gen/completionsImport_exportEquals_anonymous_test.go +++ b/internal/fourslash/tests/gen/completionsImport_exportEquals_anonymous_test.go @@ -49,7 +49,7 @@ fooB/*1*/` &lsproto.CompletionItem{ Label: "fooBar", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./foo-bar", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_exportEquals_test.go b/internal/fourslash/tests/gen/completionsImport_exportEquals_test.go index 4ef13ee441..cb18fea642 100644 --- a/internal/fourslash/tests/gen/completionsImport_exportEquals_test.go +++ b/internal/fourslash/tests/gen/completionsImport_exportEquals_test.go @@ -39,7 +39,7 @@ let x: b/*1*/;` &lsproto.CompletionItem{ Label: "a", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, @@ -60,7 +60,7 @@ let x: b/*1*/;` &lsproto.CompletionItem{ Label: "b", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_filteredByInvalidPackageJson_direct_test.go b/internal/fourslash/tests/gen/completionsImport_filteredByInvalidPackageJson_direct_test.go index a8eeaf953a..0ed45545ff 100644 --- a/internal/fourslash/tests/gen/completionsImport_filteredByInvalidPackageJson_direct_test.go +++ b/internal/fourslash/tests/gen/completionsImport_filteredByInvalidPackageJson_direct_test.go @@ -52,7 +52,7 @@ const x = Re/**/` Label: "React", AdditionalTextEdits: fourslash.AnyTextEdits, Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "react", }, }, @@ -62,7 +62,7 @@ const x = Re/**/` Label: "ReactFake", AdditionalTextEdits: fourslash.AnyTextEdits, Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "fake-react", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_filteredByPackageJson_@typesImplicit_test.go b/internal/fourslash/tests/gen/completionsImport_filteredByPackageJson_@typesImplicit_test.go index 93cf19b77d..c39288af98 100644 --- a/internal/fourslash/tests/gen/completionsImport_filteredByPackageJson_@typesImplicit_test.go +++ b/internal/fourslash/tests/gen/completionsImport_filteredByPackageJson_@typesImplicit_test.go @@ -49,7 +49,7 @@ const x = Re/**/` Label: "React", AdditionalTextEdits: fourslash.AnyTextEdits, Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "react", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_filteredByPackageJson_@typesOnly_test.go b/internal/fourslash/tests/gen/completionsImport_filteredByPackageJson_@typesOnly_test.go index 06e6dd10ae..a009352c97 100644 --- a/internal/fourslash/tests/gen/completionsImport_filteredByPackageJson_@typesOnly_test.go +++ b/internal/fourslash/tests/gen/completionsImport_filteredByPackageJson_@typesOnly_test.go @@ -49,7 +49,7 @@ const x = Re/**/` Label: "React", AdditionalTextEdits: fourslash.AnyTextEdits, Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "react", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_filteredByPackageJson_ambient_test.go b/internal/fourslash/tests/gen/completionsImport_filteredByPackageJson_ambient_test.go index deb5424f10..a014c63301 100644 --- a/internal/fourslash/tests/gen/completionsImport_filteredByPackageJson_ambient_test.go +++ b/internal/fourslash/tests/gen/completionsImport_filteredByPackageJson_ambient_test.go @@ -78,7 +78,7 @@ loca/*5*/` &lsproto.CompletionItem{ Label: "agate", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "react-syntax-highlighter/sub", }, }, @@ -99,7 +99,7 @@ loca/*5*/` &lsproto.CompletionItem{ Label: "somethingElse", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "something-else", }, }, @@ -120,7 +120,7 @@ loca/*5*/` &lsproto.CompletionItem{ Label: "declaredBySomethingNotInPackageJson", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "declared-by-foo", }, }, @@ -141,7 +141,7 @@ loca/*5*/` &lsproto.CompletionItem{ Label: "local", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "local", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_filteredByPackageJson_direct_test.go b/internal/fourslash/tests/gen/completionsImport_filteredByPackageJson_direct_test.go index 556c0ca835..f63f2af248 100644 --- a/internal/fourslash/tests/gen/completionsImport_filteredByPackageJson_direct_test.go +++ b/internal/fourslash/tests/gen/completionsImport_filteredByPackageJson_direct_test.go @@ -51,7 +51,7 @@ const x = Re/**/` Label: "React", AdditionalTextEdits: fourslash.AnyTextEdits, Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "react", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_filteredByPackageJson_nested_test.go b/internal/fourslash/tests/gen/completionsImport_filteredByPackageJson_nested_test.go index 2fd771c5ba..b7f194b91e 100644 --- a/internal/fourslash/tests/gen/completionsImport_filteredByPackageJson_nested_test.go +++ b/internal/fourslash/tests/gen/completionsImport_filteredByPackageJson_nested_test.go @@ -57,7 +57,7 @@ const x = Re/**/` Label: "React", AdditionalTextEdits: fourslash.AnyTextEdits, Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "react", }, }, @@ -78,7 +78,7 @@ const x = Re/**/` Label: "Redux", AdditionalTextEdits: fourslash.AnyTextEdits, Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "redux", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_filteredByPackageJson_peerDependencies_test.go b/internal/fourslash/tests/gen/completionsImport_filteredByPackageJson_peerDependencies_test.go index 3ffefa8b3a..10ab87d1aa 100644 --- a/internal/fourslash/tests/gen/completionsImport_filteredByPackageJson_peerDependencies_test.go +++ b/internal/fourslash/tests/gen/completionsImport_filteredByPackageJson_peerDependencies_test.go @@ -51,7 +51,7 @@ const x = Re/**/` Label: "React", AdditionalTextEdits: fourslash.AnyTextEdits, Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "react", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_importType_test.go b/internal/fourslash/tests/gen/completionsImport_importType_test.go index 0cc686fe96..1a18540fd0 100644 --- a/internal/fourslash/tests/gen/completionsImport_importType_test.go +++ b/internal/fourslash/tests/gen/completionsImport_importType_test.go @@ -36,7 +36,7 @@ export const m = 0; &lsproto.CompletionItem{ Label: "C", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, @@ -47,7 +47,7 @@ export const m = 0; &lsproto.CompletionItem{ Label: "T", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_jsxOpeningTagImportDefault_test.go b/internal/fourslash/tests/gen/completionsImport_jsxOpeningTagImportDefault_test.go index a0d3f40ac5..a13ece3e5d 100644 --- a/internal/fourslash/tests/gen/completionsImport_jsxOpeningTagImportDefault_test.go +++ b/internal/fourslash/tests/gen/completionsImport_jsxOpeningTagImportDefault_test.go @@ -36,7 +36,7 @@ export function Index() { &lsproto.CompletionItem{ Label: "Component", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./component", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_mergedReExport_test.go b/internal/fourslash/tests/gen/completionsImport_mergedReExport_test.go index 830373ab69..1519327ca3 100644 --- a/internal/fourslash/tests/gen/completionsImport_mergedReExport_test.go +++ b/internal/fourslash/tests/gen/completionsImport_mergedReExport_test.go @@ -52,7 +52,7 @@ C/**/` &lsproto.CompletionItem{ Label: "Config", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "@jest/types", }, }, @@ -74,7 +74,7 @@ C/**/` &lsproto.CompletionItem{ Label: "Config", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "@jest/types", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_multipleWithSameName_test.go b/internal/fourslash/tests/gen/completionsImport_multipleWithSameName_test.go index 2ee272e38e..e41b898c32 100644 --- a/internal/fourslash/tests/gen/completionsImport_multipleWithSameName_test.go +++ b/internal/fourslash/tests/gen/completionsImport_multipleWithSameName_test.go @@ -45,7 +45,7 @@ fo/**/` &lsproto.CompletionItem{ Label: "foo", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, @@ -57,7 +57,7 @@ fo/**/` &lsproto.CompletionItem{ Label: "foo", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./b", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_named_addToNamedImports_test.go b/internal/fourslash/tests/gen/completionsImport_named_addToNamedImports_test.go index 34735e25a5..ddc37acd9c 100644 --- a/internal/fourslash/tests/gen/completionsImport_named_addToNamedImports_test.go +++ b/internal/fourslash/tests/gen/completionsImport_named_addToNamedImports_test.go @@ -33,7 +33,7 @@ f/**/;` &lsproto.CompletionItem{ Label: "foo", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_named_didNotExistBefore_test.go b/internal/fourslash/tests/gen/completionsImport_named_didNotExistBefore_test.go index fa0f514eb9..627697c77e 100644 --- a/internal/fourslash/tests/gen/completionsImport_named_didNotExistBefore_test.go +++ b/internal/fourslash/tests/gen/completionsImport_named_didNotExistBefore_test.go @@ -40,7 +40,7 @@ t/**/` &lsproto.CompletionItem{ Label: "Test1", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_named_exportEqualsNamespace_merged_test.go b/internal/fourslash/tests/gen/completionsImport_named_exportEqualsNamespace_merged_test.go index b647b87b7a..6102590a0a 100644 --- a/internal/fourslash/tests/gen/completionsImport_named_exportEqualsNamespace_merged_test.go +++ b/internal/fourslash/tests/gen/completionsImport_named_exportEqualsNamespace_merged_test.go @@ -39,7 +39,7 @@ fo/**/` &lsproto.CompletionItem{ Label: "foo", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "n", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_named_exportEqualsNamespace_test.go b/internal/fourslash/tests/gen/completionsImport_named_exportEqualsNamespace_test.go index 5129c1c3d9..936c25ed3c 100644 --- a/internal/fourslash/tests/gen/completionsImport_named_exportEqualsNamespace_test.go +++ b/internal/fourslash/tests/gen/completionsImport_named_exportEqualsNamespace_test.go @@ -35,7 +35,7 @@ f/**/;` &lsproto.CompletionItem{ Label: "foo", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_named_fromMergedDeclarations_test.go b/internal/fourslash/tests/gen/completionsImport_named_fromMergedDeclarations_test.go index aa8c399f5a..7826368e8f 100644 --- a/internal/fourslash/tests/gen/completionsImport_named_fromMergedDeclarations_test.go +++ b/internal/fourslash/tests/gen/completionsImport_named_fromMergedDeclarations_test.go @@ -38,7 +38,7 @@ declare module "m" { &lsproto.CompletionItem{ Label: "M", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "m", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_named_namespaceImportExists_test.go b/internal/fourslash/tests/gen/completionsImport_named_namespaceImportExists_test.go index bc0e0ef490..4f9b816218 100644 --- a/internal/fourslash/tests/gen/completionsImport_named_namespaceImportExists_test.go +++ b/internal/fourslash/tests/gen/completionsImport_named_namespaceImportExists_test.go @@ -32,7 +32,7 @@ f/**/;` &lsproto.CompletionItem{ Label: "foo", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_ofAlias_preferShortPath_test.go b/internal/fourslash/tests/gen/completionsImport_ofAlias_preferShortPath_test.go index f21c68a358..6126dd31f2 100644 --- a/internal/fourslash/tests/gen/completionsImport_ofAlias_preferShortPath_test.go +++ b/internal/fourslash/tests/gen/completionsImport_ofAlias_preferShortPath_test.go @@ -36,7 +36,7 @@ fo/**/` &lsproto.CompletionItem{ Label: "foo", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./foo", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_packageJsonImportsPreference_test.go b/internal/fourslash/tests/gen/completionsImport_packageJsonImportsPreference_test.go index da5ce3eb18..599a61bb0e 100644 --- a/internal/fourslash/tests/gen/completionsImport_packageJsonImportsPreference_test.go +++ b/internal/fourslash/tests/gen/completionsImport_packageJsonImportsPreference_test.go @@ -44,7 +44,7 @@ internalFoo/**/` &lsproto.CompletionItem{ Label: "internalFoo", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "#internal/foo", }, }, @@ -65,7 +65,7 @@ internalFoo/**/` &lsproto.CompletionItem{ Label: "internalFoo", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./other", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_preferUpdatingExistingImport_test.go b/internal/fourslash/tests/gen/completionsImport_preferUpdatingExistingImport_test.go index 5239412e41..eaa18d529e 100644 --- a/internal/fourslash/tests/gen/completionsImport_preferUpdatingExistingImport_test.go +++ b/internal/fourslash/tests/gen/completionsImport_preferUpdatingExistingImport_test.go @@ -40,7 +40,7 @@ y/**/` &lsproto.CompletionItem{ Label: "y", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./deep/module/why/you/want/this/path", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_previousTokenIsSemicolon_test.go b/internal/fourslash/tests/gen/completionsImport_previousTokenIsSemicolon_test.go index 6dbd4f5ed9..999142b2f8 100644 --- a/internal/fourslash/tests/gen/completionsImport_previousTokenIsSemicolon_test.go +++ b/internal/fourslash/tests/gen/completionsImport_previousTokenIsSemicolon_test.go @@ -32,7 +32,7 @@ import * as a from 'a'; &lsproto.CompletionItem{ Label: "foo", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_reExportDefault2_test.go b/internal/fourslash/tests/gen/completionsImport_reExportDefault2_test.go index a77af5ca53..54b1ef9b25 100644 --- a/internal/fourslash/tests/gen/completionsImport_reExportDefault2_test.go +++ b/internal/fourslash/tests/gen/completionsImport_reExportDefault2_test.go @@ -44,7 +44,7 @@ defaultExp/**/` &lsproto.CompletionItem{ Label: "defaultExport", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "example", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_reExport_wrongName_test.go b/internal/fourslash/tests/gen/completionsImport_reExport_wrongName_test.go index 9ef283a691..78d7156713 100644 --- a/internal/fourslash/tests/gen/completionsImport_reExport_wrongName_test.go +++ b/internal/fourslash/tests/gen/completionsImport_reExport_wrongName_test.go @@ -35,7 +35,7 @@ export { x as y } from "./a"; &lsproto.CompletionItem{ Label: "x", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, @@ -46,7 +46,7 @@ export { x as y } from "./a"; &lsproto.CompletionItem{ Label: "y", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: ".", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_require_addNew_test.go b/internal/fourslash/tests/gen/completionsImport_require_addNew_test.go index 4e76c0f239..dcabb1123b 100644 --- a/internal/fourslash/tests/gen/completionsImport_require_addNew_test.go +++ b/internal/fourslash/tests/gen/completionsImport_require_addNew_test.go @@ -33,7 +33,7 @@ x/**/` &lsproto.CompletionItem{ Label: "x", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_require_addToExisting_test.go b/internal/fourslash/tests/gen/completionsImport_require_addToExisting_test.go index 9c6c0337be..3c9bbd672e 100644 --- a/internal/fourslash/tests/gen/completionsImport_require_addToExisting_test.go +++ b/internal/fourslash/tests/gen/completionsImport_require_addToExisting_test.go @@ -36,7 +36,7 @@ x/**/` &lsproto.CompletionItem{ Label: "x", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_require_test.go b/internal/fourslash/tests/gen/completionsImport_require_test.go index 4e66ec4d9e..fa9558d893 100644 --- a/internal/fourslash/tests/gen/completionsImport_require_test.go +++ b/internal/fourslash/tests/gen/completionsImport_require_test.go @@ -33,7 +33,7 @@ fo/*b*/` &lsproto.CompletionItem{ Label: "foo", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_sortingModuleSpecifiers_test.go b/internal/fourslash/tests/gen/completionsImport_sortingModuleSpecifiers_test.go index e476f2f4a0..bedcf95cc6 100644 --- a/internal/fourslash/tests/gen/completionsImport_sortingModuleSpecifiers_test.go +++ b/internal/fourslash/tests/gen/completionsImport_sortingModuleSpecifiers_test.go @@ -43,7 +43,7 @@ normalize/**/` &lsproto.CompletionItem{ Label: "normalize", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "path", }, }, @@ -53,7 +53,7 @@ normalize/**/` &lsproto.CompletionItem{ Label: "normalize", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "path/posix", }, }, @@ -63,7 +63,7 @@ normalize/**/` &lsproto.CompletionItem{ Label: "normalize", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "path/win32", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_tsx_test.go b/internal/fourslash/tests/gen/completionsImport_tsx_test.go index 2839a597b8..3b0031c21c 100644 --- a/internal/fourslash/tests/gen/completionsImport_tsx_test.go +++ b/internal/fourslash/tests/gen/completionsImport_tsx_test.go @@ -34,7 +34,7 @@ export default function Foo() {}; &lsproto.CompletionItem{ Label: "Foo", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_umdDefaultNoCrash1_test.go b/internal/fourslash/tests/gen/completionsImport_umdDefaultNoCrash1_test.go index 6379c8cef9..e89e279cb4 100644 --- a/internal/fourslash/tests/gen/completionsImport_umdDefaultNoCrash1_test.go +++ b/internal/fourslash/tests/gen/completionsImport_umdDefaultNoCrash1_test.go @@ -59,7 +59,7 @@ func TestCompletionsImport_umdDefaultNoCrash1(t *testing.T) { Label: "Dottie", AdditionalTextEdits: fourslash.AnyTextEdits, Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "dottie", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_umdModules2_moduleExports_test.go b/internal/fourslash/tests/gen/completionsImport_umdModules2_moduleExports_test.go index 8503f171f0..22b8914502 100644 --- a/internal/fourslash/tests/gen/completionsImport_umdModules2_moduleExports_test.go +++ b/internal/fourslash/tests/gen/completionsImport_umdModules2_moduleExports_test.go @@ -43,7 +43,7 @@ const el1 =
foo
;` Label: "classNames", AdditionalTextEdits: fourslash.AnyTextEdits, Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "classnames", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_uriStyleNodeCoreModules1_test.go b/internal/fourslash/tests/gen/completionsImport_uriStyleNodeCoreModules1_test.go index bb495c2c45..a9d32aa576 100644 --- a/internal/fourslash/tests/gen/completionsImport_uriStyleNodeCoreModules1_test.go +++ b/internal/fourslash/tests/gen/completionsImport_uriStyleNodeCoreModules1_test.go @@ -36,7 +36,7 @@ write/**/` &lsproto.CompletionItem{ Label: "writeFile", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "fs", }, }, @@ -46,7 +46,7 @@ write/**/` &lsproto.CompletionItem{ Label: "writeFile", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "node:fs", }, }, @@ -56,7 +56,7 @@ write/**/` &lsproto.CompletionItem{ Label: "writeFile", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "fs/promises", }, }, @@ -66,7 +66,7 @@ write/**/` &lsproto.CompletionItem{ Label: "writeFile", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "node:fs/promises", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_uriStyleNodeCoreModules2_test.go b/internal/fourslash/tests/gen/completionsImport_uriStyleNodeCoreModules2_test.go index 967646a233..3d2e52cfdb 100644 --- a/internal/fourslash/tests/gen/completionsImport_uriStyleNodeCoreModules2_test.go +++ b/internal/fourslash/tests/gen/completionsImport_uriStyleNodeCoreModules2_test.go @@ -38,7 +38,7 @@ write/**/` &lsproto.CompletionItem{ Label: "writeFile", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "node:fs", }, }, @@ -48,7 +48,7 @@ write/**/` &lsproto.CompletionItem{ Label: "writeFile", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "node:fs/promises", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_uriStyleNodeCoreModules3_test.go b/internal/fourslash/tests/gen/completionsImport_uriStyleNodeCoreModules3_test.go index dc3ebf1cda..6118a9b8f5 100644 --- a/internal/fourslash/tests/gen/completionsImport_uriStyleNodeCoreModules3_test.go +++ b/internal/fourslash/tests/gen/completionsImport_uriStyleNodeCoreModules3_test.go @@ -59,7 +59,7 @@ writeFile/*test2*/` &lsproto.CompletionItem{ Label: "writeFile", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "fs", }, }, @@ -69,7 +69,7 @@ writeFile/*test2*/` &lsproto.CompletionItem{ Label: "writeFile", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "fs/promises", }, }, @@ -91,7 +91,7 @@ writeFile/*test2*/` &lsproto.CompletionItem{ Label: "writeFile", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "node:fs", }, }, @@ -101,7 +101,7 @@ writeFile/*test2*/` &lsproto.CompletionItem{ Label: "writeFile", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "node:fs/promises", }, }, @@ -123,7 +123,7 @@ writeFile/*test2*/` &lsproto.CompletionItem{ Label: "writeFile", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "node:fs", }, }, @@ -133,7 +133,7 @@ writeFile/*test2*/` &lsproto.CompletionItem{ Label: "writeFile", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "node:fs/promises", }, }, @@ -155,7 +155,7 @@ writeFile/*test2*/` &lsproto.CompletionItem{ Label: "writeFile", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "node:fs", }, }, @@ -165,7 +165,7 @@ writeFile/*test2*/` &lsproto.CompletionItem{ Label: "writeFile", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "node:fs/promises", }, }, @@ -189,7 +189,7 @@ writeFile/*test2*/` &lsproto.CompletionItem{ Label: "writeFile", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "fs", }, }, @@ -199,7 +199,7 @@ writeFile/*test2*/` &lsproto.CompletionItem{ Label: "writeFile", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "fs/promises", }, }, @@ -221,7 +221,7 @@ writeFile/*test2*/` &lsproto.CompletionItem{ Label: "writeFile", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "node:fs", }, }, @@ -231,7 +231,7 @@ writeFile/*test2*/` &lsproto.CompletionItem{ Label: "writeFile", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "node:fs/promises", }, }, diff --git a/internal/fourslash/tests/gen/completionsImport_windowsPathsProjectRelative_test.go b/internal/fourslash/tests/gen/completionsImport_windowsPathsProjectRelative_test.go index 6a9f90f7c1..d7225a151c 100644 --- a/internal/fourslash/tests/gen/completionsImport_windowsPathsProjectRelative_test.go +++ b/internal/fourslash/tests/gen/completionsImport_windowsPathsProjectRelative_test.go @@ -47,7 +47,7 @@ myFunction/**/` &lsproto.CompletionItem{ Label: "myFunctionA", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "~/noIndex/a", }, }, @@ -57,7 +57,7 @@ myFunction/**/` &lsproto.CompletionItem{ Label: "myFunctionB", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "~/withIndex", }, }, @@ -78,7 +78,7 @@ myFunction/**/` &lsproto.CompletionItem{ Label: "myFunctionA", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "../noIndex/a", }, }, @@ -88,7 +88,7 @@ myFunction/**/` &lsproto.CompletionItem{ Label: "myFunctionB", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "../withIndex", }, }, @@ -109,7 +109,7 @@ myFunction/**/` &lsproto.CompletionItem{ Label: "myFunctionA", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "../noIndex/a", }, }, @@ -119,7 +119,7 @@ myFunction/**/` &lsproto.CompletionItem{ Label: "myFunctionB", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "../withIndex", }, }, diff --git a/internal/fourslash/tests/gen/completionsRecommended_namespace_test.go b/internal/fourslash/tests/gen/completionsRecommended_namespace_test.go index 3586dec9db..b68431e6f1 100644 --- a/internal/fourslash/tests/gen/completionsRecommended_namespace_test.go +++ b/internal/fourslash/tests/gen/completionsRecommended_namespace_test.go @@ -60,7 +60,7 @@ alpha.f(new /*c1*/);` &lsproto.CompletionItem{ Label: "Name", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, diff --git a/internal/fourslash/tests/gen/completionsUniqueSymbol_import_test.go b/internal/fourslash/tests/gen/completionsUniqueSymbol_import_test.go index e913fa8634..287c869a39 100644 --- a/internal/fourslash/tests/gen/completionsUniqueSymbol_import_test.go +++ b/internal/fourslash/tests/gen/completionsUniqueSymbol_import_test.go @@ -45,7 +45,7 @@ i[|./**/|];` Label: "publicSym", InsertText: PtrTo("[publicSym]"), Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, diff --git a/internal/fourslash/tests/gen/completionsWithDeprecatedTag10_test.go b/internal/fourslash/tests/gen/completionsWithDeprecatedTag10_test.go index a052bfffe2..bf61441fd7 100644 --- a/internal/fourslash/tests/gen/completionsWithDeprecatedTag10_test.go +++ b/internal/fourslash/tests/gen/completionsWithDeprecatedTag10_test.go @@ -32,7 +32,7 @@ export const foo = 0; &lsproto.CompletionItem{ Label: "foo", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./foo", }, }, diff --git a/internal/fourslash/tests/gen/importSuggestionsCache_exportUndefined_test.go b/internal/fourslash/tests/gen/importSuggestionsCache_exportUndefined_test.go index c980419bb3..6264b5e445 100644 --- a/internal/fourslash/tests/gen/importSuggestionsCache_exportUndefined_test.go +++ b/internal/fourslash/tests/gen/importSuggestionsCache_exportUndefined_test.go @@ -40,7 +40,7 @@ export = x; AdditionalTextEdits: fourslash.AnyTextEdits, SortText: PtrTo(string(ls.SortTextAutoImportSuggestions)), Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./undefinedAlias", }, }, @@ -61,7 +61,7 @@ export = x; AdditionalTextEdits: fourslash.AnyTextEdits, SortText: PtrTo(string(ls.SortTextAutoImportSuggestions)), Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./undefinedAlias", }, }, diff --git a/internal/fourslash/tests/gen/importSuggestionsCache_invalidPackageJson_test.go b/internal/fourslash/tests/gen/importSuggestionsCache_invalidPackageJson_test.go index 90d01cb704..8c8210c7fc 100644 --- a/internal/fourslash/tests/gen/importSuggestionsCache_invalidPackageJson_test.go +++ b/internal/fourslash/tests/gen/importSuggestionsCache_invalidPackageJson_test.go @@ -47,7 +47,7 @@ readF/**/` &lsproto.CompletionItem{ Label: "readFile", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "fs", }, }, diff --git a/internal/fourslash/tests/gen/importTypeCompletions1_test.go b/internal/fourslash/tests/gen/importTypeCompletions1_test.go index bbf5095b8c..f464ab31cc 100644 --- a/internal/fourslash/tests/gen/importTypeCompletions1_test.go +++ b/internal/fourslash/tests/gen/importTypeCompletions1_test.go @@ -33,7 +33,7 @@ export interface Foo {} Label: "Foo", InsertText: PtrTo("import type { Foo } from \"./foo\";"), Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./foo", }, }, diff --git a/internal/fourslash/tests/gen/importTypeCompletions3_test.go b/internal/fourslash/tests/gen/importTypeCompletions3_test.go index 0453d3c7f4..3b4d63f4e3 100644 --- a/internal/fourslash/tests/gen/importTypeCompletions3_test.go +++ b/internal/fourslash/tests/gen/importTypeCompletions3_test.go @@ -33,7 +33,7 @@ export interface Foo {} Label: "Foo", InsertText: PtrTo("import type { Foo } from \"./foo\";"), Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./foo", }, }, diff --git a/internal/fourslash/tests/gen/importTypeCompletions4_test.go b/internal/fourslash/tests/gen/importTypeCompletions4_test.go index c9b8c87468..24d433e677 100644 --- a/internal/fourslash/tests/gen/importTypeCompletions4_test.go +++ b/internal/fourslash/tests/gen/importTypeCompletions4_test.go @@ -34,7 +34,7 @@ export = Foo; Label: "Foo", InsertText: PtrTo("import type Foo from \"./foo\";"), Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./foo", }, }, diff --git a/internal/fourslash/tests/gen/importTypeCompletions5_test.go b/internal/fourslash/tests/gen/importTypeCompletions5_test.go index d883ce0645..32156191ff 100644 --- a/internal/fourslash/tests/gen/importTypeCompletions5_test.go +++ b/internal/fourslash/tests/gen/importTypeCompletions5_test.go @@ -35,7 +35,7 @@ export = Foo; Label: "Foo", InsertText: PtrTo("import type Foo = require(\"./foo\");"), Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./foo", }, }, diff --git a/internal/fourslash/tests/gen/importTypeCompletions6_test.go b/internal/fourslash/tests/gen/importTypeCompletions6_test.go index 50e0a1b5a8..2173ef5dbe 100644 --- a/internal/fourslash/tests/gen/importTypeCompletions6_test.go +++ b/internal/fourslash/tests/gen/importTypeCompletions6_test.go @@ -34,7 +34,7 @@ export interface Foo { }; Label: "Foo", InsertText: PtrTo("import type { Foo } from \"./foo\";"), Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./foo", }, }, diff --git a/internal/fourslash/tests/gen/importTypeCompletions7_test.go b/internal/fourslash/tests/gen/importTypeCompletions7_test.go index 8ab0c4d458..bc038a7d46 100644 --- a/internal/fourslash/tests/gen/importTypeCompletions7_test.go +++ b/internal/fourslash/tests/gen/importTypeCompletions7_test.go @@ -36,7 +36,7 @@ export = Foo; Label: "Foo", InsertText: PtrTo("import Foo from \"./foo\";"), Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./foo", }, }, diff --git a/internal/fourslash/tests/gen/importTypeCompletions8_test.go b/internal/fourslash/tests/gen/importTypeCompletions8_test.go index d7e63483ee..cdff4b9e7b 100644 --- a/internal/fourslash/tests/gen/importTypeCompletions8_test.go +++ b/internal/fourslash/tests/gen/importTypeCompletions8_test.go @@ -33,7 +33,7 @@ export interface Foo {} Label: "Foo", InsertText: PtrTo("import { type Foo } from \"./foo\";"), Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./foo", }, }, diff --git a/internal/fourslash/tests/gen/importTypeCompletions9_test.go b/internal/fourslash/tests/gen/importTypeCompletions9_test.go index 90cc09ef5b..c38bfa7338 100644 --- a/internal/fourslash/tests/gen/importTypeCompletions9_test.go +++ b/internal/fourslash/tests/gen/importTypeCompletions9_test.go @@ -33,7 +33,7 @@ export interface Foo {} Label: "Foo", InsertText: PtrTo("import { type Foo } from \"./foo\";"), Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./foo", }, }, diff --git a/internal/fourslash/tests/gen/jsFileImportNoTypes2_test.go b/internal/fourslash/tests/gen/jsFileImportNoTypes2_test.go index d9ea22f264..03bdb5cb8c 100644 --- a/internal/fourslash/tests/gen/jsFileImportNoTypes2_test.go +++ b/internal/fourslash/tests/gen/jsFileImportNoTypes2_test.go @@ -46,7 +46,7 @@ import /**/` Label: "TestClassBaseline", InsertText: PtrTo("import { TestClassBaseline } from \"./baseline\";"), Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./baseline", }, }, @@ -55,7 +55,7 @@ import /**/` Label: "TestClassExportList", InsertText: PtrTo("import { TestClassExportList } from \"./exportList\";"), Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./exportList", }, }, @@ -64,7 +64,7 @@ import /**/` Label: "TestClassReExport", InsertText: PtrTo("import { TestClassReExport } from \"./reExport\";"), Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./reExport", }, }, @@ -73,7 +73,7 @@ import /**/` Label: "TestDefaultClass", InsertText: PtrTo("import TestDefaultClass from \"./default\";"), Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./default", }, }, diff --git a/internal/fourslash/tests/gen/autoImportPackageRootPathTypeModule_test.go b/internal/fourslash/tests/manual/autoImportPackageRootPathTypeModule_test.go similarity index 86% rename from internal/fourslash/tests/gen/autoImportPackageRootPathTypeModule_test.go rename to internal/fourslash/tests/manual/autoImportPackageRootPathTypeModule_test.go index f574821933..ca68c52c4e 100644 --- a/internal/fourslash/tests/gen/autoImportPackageRootPathTypeModule_test.go +++ b/internal/fourslash/tests/manual/autoImportPackageRootPathTypeModule_test.go @@ -8,7 +8,7 @@ import ( ) func TestAutoImportPackageRootPathTypeModule(t *testing.T) { - fourslash.SkipIfFailing(t) + t.Skip() t.Parallel() defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `// @allowJs: true @@ -31,5 +31,5 @@ export function foo() {}; foo/**/` f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) defer done() - f.VerifyImportFixModuleSpecifiers(t, "", []string{"pkg/lib"}, nil /*preferences*/) + f.VerifyImportFixModuleSpecifiers(t, "", []string{"pkg"}, nil /*preferences*/) } diff --git a/internal/fourslash/tests/gen/completionListWithLabel_test.go b/internal/fourslash/tests/manual/completionListWithLabel_test.go similarity index 100% rename from internal/fourslash/tests/gen/completionListWithLabel_test.go rename to internal/fourslash/tests/manual/completionListWithLabel_test.go index dde0947f86..2c3807e31d 100644 --- a/internal/fourslash/tests/gen/completionListWithLabel_test.go +++ b/internal/fourslash/tests/manual/completionListWithLabel_test.go @@ -46,8 +46,8 @@ func TestCompletionListWithLabel(t *testing.T) { }, Items: &fourslash.CompletionsExpectedItems{ Exact: []fourslash.CompletionsExpectedItem{ - "testlabel", "label", + "testlabel", }, }, }) diff --git a/internal/fourslash/tests/gen/completionsImport_defaultAndNamedConflict_test.go b/internal/fourslash/tests/manual/completionsImport_defaultAndNamedConflict_test.go similarity index 84% rename from internal/fourslash/tests/gen/completionsImport_defaultAndNamedConflict_test.go rename to internal/fourslash/tests/manual/completionsImport_defaultAndNamedConflict_test.go index 737ad8d659..779a4ce9cf 100644 --- a/internal/fourslash/tests/gen/completionsImport_defaultAndNamedConflict_test.go +++ b/internal/fourslash/tests/manual/completionsImport_defaultAndNamedConflict_test.go @@ -34,39 +34,38 @@ someMo/**/` &lsproto.CompletionItem{ Label: "someModule", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./someModule", }, }, - Detail: PtrTo("(property) default: 1"), - Kind: PtrTo(lsproto.CompletionItemKindField), + Detail: PtrTo("const someModule: 0"), + Kind: PtrTo(lsproto.CompletionItemKindVariable), AdditionalTextEdits: fourslash.AnyTextEdits, SortText: PtrTo(string(ls.SortTextAutoImportSuggestions)), }, &lsproto.CompletionItem{ Label: "someModule", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./someModule", }, }, - Detail: PtrTo("const someModule: 0"), - Kind: PtrTo(lsproto.CompletionItemKindVariable), + Detail: PtrTo("(property) default: 1"), + Kind: PtrTo(lsproto.CompletionItemKindField), AdditionalTextEdits: fourslash.AnyTextEdits, SortText: PtrTo(string(ls.SortTextAutoImportSuggestions)), }, - }, true), + }, + true, + ), }, }) f.VerifyApplyCodeActionFromCompletion(t, PtrTo(""), &fourslash.ApplyCodeActionFromCompletionOptions{ - Name: "someModule", - Source: "./someModule", - AutoImportData: &lsproto.AutoImportData{ - ExportName: "default", - FileName: "/someModule.ts", - }, - Description: "Add import from \"./someModule\"", - NewFileContent: PtrTo(`import someModule from "./someModule"; + Name: "someModule", + Source: "./someModule", + AutoImportFix: &lsproto.AutoImportFix{}, + Description: "Add import from \"./someModule\"", + NewFileContent: PtrTo(`import { someModule } from "./someModule"; someMo`), }) diff --git a/internal/fourslash/tests/gen/completionsImport_reExportDefault_test.go b/internal/fourslash/tests/manual/completionsImport_reExportDefault_test.go similarity index 93% rename from internal/fourslash/tests/gen/completionsImport_reExportDefault_test.go rename to internal/fourslash/tests/manual/completionsImport_reExportDefault_test.go index fb776c0766..6641d8f53a 100644 --- a/internal/fourslash/tests/gen/completionsImport_reExportDefault_test.go +++ b/internal/fourslash/tests/manual/completionsImport_reExportDefault_test.go @@ -35,12 +35,12 @@ fo/**/` &lsproto.CompletionItem{ Label: "foo", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ + AutoImport: &lsproto.AutoImportFix{ ModuleSpecifier: "./a", }, }, Detail: PtrTo("(alias) function foo(): void\nexport foo"), - Kind: PtrTo(lsproto.CompletionItemKindVariable), + Kind: PtrTo(lsproto.CompletionItemKindFunction), AdditionalTextEdits: fourslash.AnyTextEdits, SortText: PtrTo(string(ls.SortTextAutoImportSuggestions)), }, diff --git a/internal/fourslash/tests/gen/completionsImport_reexportTransient_test.go b/internal/fourslash/tests/manual/completionsImport_reexportTransient_test.go similarity index 79% rename from internal/fourslash/tests/gen/completionsImport_reexportTransient_test.go rename to internal/fourslash/tests/manual/completionsImport_reexportTransient_test.go index 95900a857f..db75c54981 100644 --- a/internal/fourslash/tests/gen/completionsImport_reexportTransient_test.go +++ b/internal/fourslash/tests/manual/completionsImport_reexportTransient_test.go @@ -39,8 +39,18 @@ one/**/` &lsproto.CompletionItem{ Label: "one", Data: &lsproto.CompletionItemData{ - AutoImport: &lsproto.AutoImportData{ - ModuleSpecifier: "./transient", + AutoImport: &lsproto.AutoImportFix{ + ModuleSpecifier: "./r1", + }, + }, + AdditionalTextEdits: fourslash.AnyTextEdits, + SortText: PtrTo(string(ls.SortTextAutoImportSuggestions)), + }, + &lsproto.CompletionItem{ + Label: "one", + Data: &lsproto.CompletionItemData{ + AutoImport: &lsproto.AutoImportFix{ + ModuleSpecifier: "./r2", }, }, AdditionalTextEdits: fourslash.AnyTextEdits, diff --git a/internal/fourslash/tests/gen/completionsWithStringReplacementMode1_test.go b/internal/fourslash/tests/manual/completionsWithStringReplacementMode1_test.go similarity index 100% rename from internal/fourslash/tests/gen/completionsWithStringReplacementMode1_test.go rename to internal/fourslash/tests/manual/completionsWithStringReplacementMode1_test.go index 79e186a910..731833bb77 100644 --- a/internal/fourslash/tests/gen/completionsWithStringReplacementMode1_test.go +++ b/internal/fourslash/tests/manual/completionsWithStringReplacementMode1_test.go @@ -44,145 +44,145 @@ f('[|login./**/|]')` Items: &fourslash.CompletionsExpectedItems{ Exact: []fourslash.CompletionsExpectedItem{ &lsproto.CompletionItem{ - Label: "login.title", + Label: "login.description", TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ TextEdit: &lsproto.TextEdit{ - NewText: "login.title", + NewText: "login.description", Range: f.Ranges()[0].LSRange, }, }, }, &lsproto.CompletionItem{ - Label: "login.description", + Label: "login.emailInputPlaceholder", TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ TextEdit: &lsproto.TextEdit{ - NewText: "login.description", + NewText: "login.emailInputPlaceholder", Range: f.Ranges()[0].LSRange, }, }, }, &lsproto.CompletionItem{ - Label: "login.sendEmailAgree", + Label: "login.errorGeneralEmailDescription", TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ TextEdit: &lsproto.TextEdit{ - NewText: "login.sendEmailAgree", + NewText: "login.errorGeneralEmailDescription", Range: f.Ranges()[0].LSRange, }, }, }, &lsproto.CompletionItem{ - Label: "login.termsOfUse", + Label: "login.errorGeneralEmailTitle", TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ TextEdit: &lsproto.TextEdit{ - NewText: "login.termsOfUse", + NewText: "login.errorGeneralEmailTitle", Range: f.Ranges()[0].LSRange, }, }, }, &lsproto.CompletionItem{ - Label: "login.privacyPolicy", + Label: "login.errorWrongEmailDescription", TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ TextEdit: &lsproto.TextEdit{ - NewText: "login.privacyPolicy", + NewText: "login.errorWrongEmailDescription", Range: f.Ranges()[0].LSRange, }, }, }, &lsproto.CompletionItem{ - Label: "login.sendEmailButton", + Label: "login.errorWrongEmailTitle", TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ TextEdit: &lsproto.TextEdit{ - NewText: "login.sendEmailButton", + NewText: "login.errorWrongEmailTitle", Range: f.Ranges()[0].LSRange, }, }, }, &lsproto.CompletionItem{ - Label: "login.emailInputPlaceholder", + Label: "login.loginErrorDescription", TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ TextEdit: &lsproto.TextEdit{ - NewText: "login.emailInputPlaceholder", + NewText: "login.loginErrorDescription", Range: f.Ranges()[0].LSRange, }, }, }, &lsproto.CompletionItem{ - Label: "login.errorWrongEmailTitle", + Label: "login.loginErrorTitle", TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ TextEdit: &lsproto.TextEdit{ - NewText: "login.errorWrongEmailTitle", + NewText: "login.loginErrorTitle", Range: f.Ranges()[0].LSRange, }, }, }, &lsproto.CompletionItem{ - Label: "login.errorWrongEmailDescription", + Label: "login.openEmailAppErrorConfirm", TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ TextEdit: &lsproto.TextEdit{ - NewText: "login.errorWrongEmailDescription", + NewText: "login.openEmailAppErrorConfirm", Range: f.Ranges()[0].LSRange, }, }, }, &lsproto.CompletionItem{ - Label: "login.errorGeneralEmailTitle", + Label: "login.openEmailAppErrorDescription", TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ TextEdit: &lsproto.TextEdit{ - NewText: "login.errorGeneralEmailTitle", + NewText: "login.openEmailAppErrorDescription", Range: f.Ranges()[0].LSRange, }, }, }, &lsproto.CompletionItem{ - Label: "login.errorGeneralEmailDescription", + Label: "login.openEmailAppErrorTitle", TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ TextEdit: &lsproto.TextEdit{ - NewText: "login.errorGeneralEmailDescription", + NewText: "login.openEmailAppErrorTitle", Range: f.Ranges()[0].LSRange, }, }, }, &lsproto.CompletionItem{ - Label: "login.loginErrorTitle", + Label: "login.privacyPolicy", TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ TextEdit: &lsproto.TextEdit{ - NewText: "login.loginErrorTitle", + NewText: "login.privacyPolicy", Range: f.Ranges()[0].LSRange, }, }, }, &lsproto.CompletionItem{ - Label: "login.loginErrorDescription", + Label: "login.sendEmailAgree", TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ TextEdit: &lsproto.TextEdit{ - NewText: "login.loginErrorDescription", + NewText: "login.sendEmailAgree", Range: f.Ranges()[0].LSRange, }, }, }, &lsproto.CompletionItem{ - Label: "login.openEmailAppErrorTitle", + Label: "login.sendEmailButton", TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ TextEdit: &lsproto.TextEdit{ - NewText: "login.openEmailAppErrorTitle", + NewText: "login.sendEmailButton", Range: f.Ranges()[0].LSRange, }, }, }, &lsproto.CompletionItem{ - Label: "login.openEmailAppErrorDescription", + Label: "login.termsOfUse", TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ TextEdit: &lsproto.TextEdit{ - NewText: "login.openEmailAppErrorDescription", + NewText: "login.termsOfUse", Range: f.Ranges()[0].LSRange, }, }, }, &lsproto.CompletionItem{ - Label: "login.openEmailAppErrorConfirm", + Label: "login.title", TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ TextEdit: &lsproto.TextEdit{ - NewText: "login.openEmailAppErrorConfirm", + NewText: "login.title", Range: f.Ranges()[0].LSRange, }, }, diff --git a/internal/fourslash/tests/gen/importNameCodeFix_uriStyleNodeCoreModules1_test.go b/internal/fourslash/tests/manual/importNameCodeFix_uriStyleNodeCoreModules1_test.go similarity index 83% rename from internal/fourslash/tests/gen/importNameCodeFix_uriStyleNodeCoreModules1_test.go rename to internal/fourslash/tests/manual/importNameCodeFix_uriStyleNodeCoreModules1_test.go index badec23586..24043e2252 100644 --- a/internal/fourslash/tests/gen/importNameCodeFix_uriStyleNodeCoreModules1_test.go +++ b/internal/fourslash/tests/manual/importNameCodeFix_uriStyleNodeCoreModules1_test.go @@ -8,7 +8,6 @@ import ( ) func TestImportNameCodeFix_uriStyleNodeCoreModules1(t *testing.T) { - fourslash.SkipIfFailing(t) t.Parallel() defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `// @module: commonjs @@ -21,5 +20,5 @@ declare module "node:fs/promises" { export * from "fs/promises"; } writeFile/**/` f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) defer done() - f.VerifyImportFixModuleSpecifiers(t, "", []string{"fs", "fs/promises", "node:fs", "node:fs/promises"}, nil /*preferences*/) + f.VerifyImportFixModuleSpecifiers(t, "", []string{"fs", "node:fs", "fs/promises", "node:fs/promises"}, nil /*preferences*/) } diff --git a/internal/fourslash/tests/gen/jsdocParameterNameCompletion_test.go b/internal/fourslash/tests/manual/jsdocParameterNameCompletion_test.go similarity index 100% rename from internal/fourslash/tests/gen/jsdocParameterNameCompletion_test.go rename to internal/fourslash/tests/manual/jsdocParameterNameCompletion_test.go index 6501937352..aa78f2494c 100644 --- a/internal/fourslash/tests/gen/jsdocParameterNameCompletion_test.go +++ b/internal/fourslash/tests/manual/jsdocParameterNameCompletion_test.go @@ -40,8 +40,8 @@ function i(foo, bar) {}` }, Items: &fourslash.CompletionsExpectedItems{ Exact: []fourslash.CompletionsExpectedItem{ - "foo", "bar", + "foo", }, }, }) diff --git a/internal/fourslash/tests/gen/stringLiteralCompletionsInPositionTypedUsingRest_test.go b/internal/fourslash/tests/manual/stringLiteralCompletionsInPositionTypedUsingRest_test.go similarity index 100% rename from internal/fourslash/tests/gen/stringLiteralCompletionsInPositionTypedUsingRest_test.go rename to internal/fourslash/tests/manual/stringLiteralCompletionsInPositionTypedUsingRest_test.go index be7d962ce7..4a68f319de 100644 --- a/internal/fourslash/tests/gen/stringLiteralCompletionsInPositionTypedUsingRest_test.go +++ b/internal/fourslash/tests/manual/stringLiteralCompletionsInPositionTypedUsingRest_test.go @@ -47,8 +47,8 @@ new Q<{ id: string; name: string }>().select("name", "/*ts3*/");` }, Items: &fourslash.CompletionsExpectedItems{ Exact: []fourslash.CompletionsExpectedItem{ - "name", "id", + "name", }, }, }) diff --git a/internal/ls/autoImports_stringer_generated.go b/internal/ls/autoImports_stringer_generated.go deleted file mode 100644 index 99243828a3..0000000000 --- a/internal/ls/autoImports_stringer_generated.go +++ /dev/null @@ -1,28 +0,0 @@ -// Code generated by "stringer -type=ExportKind -output=autoImports_stringer_generated.go"; DO NOT EDIT. - -package ls - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[ExportKindNamed-0] - _ = x[ExportKindDefault-1] - _ = x[ExportKindExportEquals-2] - _ = x[ExportKindUMD-3] - _ = x[ExportKindModule-4] -} - -const _ExportKind_name = "ExportKindNamedExportKindDefaultExportKindExportEqualsExportKindUMDExportKindModule" - -var _ExportKind_index = [...]uint8{0, 15, 32, 54, 67, 83} - -func (i ExportKind) String() string { - idx := int(i) - 0 - if i < 0 || idx >= len(_ExportKind_index)-1 { - return "ExportKind(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _ExportKind_name[_ExportKind_index[idx]:_ExportKind_index[idx+1]] -} diff --git a/internal/ls/autoimport/aliasresolver.go b/internal/ls/autoimport/aliasresolver.go new file mode 100644 index 0000000000..2351c6c4a9 --- /dev/null +++ b/internal/ls/autoimport/aliasresolver.go @@ -0,0 +1,219 @@ +package autoimport + +import ( + "github.com/microsoft/typescript-go/internal/ast" + "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/core" + "github.com/microsoft/typescript-go/internal/module" + "github.com/microsoft/typescript-go/internal/packagejson" + "github.com/microsoft/typescript-go/internal/symlinks" + "github.com/microsoft/typescript-go/internal/tsoptions" + "github.com/microsoft/typescript-go/internal/tspath" +) + +type aliasResolver struct { + toPath func(fileName string) tspath.Path + host RegistryCloneHost + moduleResolver *module.Resolver + + rootFiles []*ast.SourceFile + onFailedAmbientModuleLookup func(source ast.HasFileName, moduleName string) + resolvedModules collections.SyncMap[tspath.Path, *collections.SyncMap[module.ModeAwareCacheKey, *module.ResolvedModule]] +} + +func newAliasResolver( + rootFiles []*ast.SourceFile, + host RegistryCloneHost, + moduleResolver *module.Resolver, + toPath func(fileName string) tspath.Path, + onFailedAmbientModuleLookup func(source ast.HasFileName, moduleName string), +) *aliasResolver { + r := &aliasResolver{ + toPath: toPath, + host: host, + moduleResolver: moduleResolver, + rootFiles: rootFiles, + onFailedAmbientModuleLookup: onFailedAmbientModuleLookup, + } + return r +} + +// BindSourceFiles implements checker.Program. +func (r *aliasResolver) BindSourceFiles() { + // We will bind as we parse +} + +// SourceFiles implements checker.Program. +func (r *aliasResolver) SourceFiles() []*ast.SourceFile { + return r.rootFiles +} + +// Options implements checker.Program. +func (r *aliasResolver) Options() *core.CompilerOptions { + return &core.CompilerOptions{ + NoCheck: core.TSTrue, + } +} + +// GetCurrentDirectory implements checker.Program. +func (r *aliasResolver) GetCurrentDirectory() string { + return r.host.GetCurrentDirectory() +} + +// UseCaseSensitiveFileNames implements checker.Program. +func (r *aliasResolver) UseCaseSensitiveFileNames() bool { + return r.host.FS().UseCaseSensitiveFileNames() +} + +// GetSourceFile implements checker.Program. +func (r *aliasResolver) GetSourceFile(fileName string) *ast.SourceFile { + file := r.host.GetSourceFile(fileName, r.toPath(fileName)) + binder.BindSourceFile(file) + return file +} + +// GetDefaultResolutionModeForFile implements checker.Program. +func (r *aliasResolver) GetDefaultResolutionModeForFile(file ast.HasFileName) core.ResolutionMode { + return core.ModuleKindESNext +} + +// GetEmitModuleFormatOfFile implements checker.Program. +func (r *aliasResolver) GetEmitModuleFormatOfFile(sourceFile ast.HasFileName) core.ModuleKind { + return core.ModuleKindESNext +} + +// GetEmitSyntaxForUsageLocation implements checker.Program. +func (r *aliasResolver) GetEmitSyntaxForUsageLocation(sourceFile ast.HasFileName, usageLocation *ast.StringLiteralLike) core.ResolutionMode { + return core.ModuleKindESNext +} + +// GetImpliedNodeFormatForEmit implements checker.Program. +func (r *aliasResolver) GetImpliedNodeFormatForEmit(sourceFile ast.HasFileName) core.ModuleKind { + return core.ModuleKindESNext +} + +// GetModeForUsageLocation implements checker.Program. +func (r *aliasResolver) GetModeForUsageLocation(file ast.HasFileName, moduleSpecifier *ast.StringLiteralLike) core.ResolutionMode { + return core.ModuleKindESNext +} + +// GetResolvedModule implements checker.Program. +func (r *aliasResolver) GetResolvedModule(currentSourceFile ast.HasFileName, moduleReference string, mode core.ResolutionMode) *module.ResolvedModule { + cache, _ := r.resolvedModules.LoadOrStore(currentSourceFile.Path(), &collections.SyncMap[module.ModeAwareCacheKey, *module.ResolvedModule]{}) + if resolved, ok := cache.Load(module.ModeAwareCacheKey{Name: moduleReference, Mode: mode}); ok { + return resolved + } + resolved, _ := r.moduleResolver.ResolveModuleName(moduleReference, currentSourceFile.FileName(), mode, nil) + resolved, _ = cache.LoadOrStore(module.ModeAwareCacheKey{Name: moduleReference, Mode: mode}, resolved) + if !resolved.IsResolved() && !tspath.PathIsRelative(moduleReference) { + r.onFailedAmbientModuleLookup(currentSourceFile, moduleReference) + } + return resolved +} + +// GetSourceFileForResolvedModule implements checker.Program. +func (r *aliasResolver) GetSourceFileForResolvedModule(fileName string) *ast.SourceFile { + return r.GetSourceFile(fileName) +} + +// GetResolvedModules implements checker.Program. +func (r *aliasResolver) GetResolvedModules() map[tspath.Path]module.ModeAwareCache[*module.ResolvedModule] { + // only used when producing diagnostics, which hopefully the checker won't do + return nil +} + +// --- + +// GetSymlinkCache implements checker.Program. +func (r *aliasResolver) GetSymlinkCache() *symlinks.KnownSymlinks { + panic("unimplemented") +} + +// GetSourceFileMetaData implements checker.Program. +func (r *aliasResolver) GetSourceFileMetaData(path tspath.Path) ast.SourceFileMetaData { + panic("unimplemented") +} + +// CommonSourceDirectory implements checker.Program. +func (r *aliasResolver) CommonSourceDirectory() string { + panic("unimplemented") +} + +// FileExists implements checker.Program. +func (r *aliasResolver) FileExists(fileName string) bool { + panic("unimplemented") +} + +// GetGlobalTypingsCacheLocation implements checker.Program. +func (r *aliasResolver) GetGlobalTypingsCacheLocation() string { + panic("unimplemented") +} + +// GetImportHelpersImportSpecifier implements checker.Program. +func (r *aliasResolver) GetImportHelpersImportSpecifier(path tspath.Path) *ast.Node { + panic("unimplemented") +} + +// GetJSXRuntimeImportSpecifier implements checker.Program. +func (r *aliasResolver) GetJSXRuntimeImportSpecifier(path tspath.Path) (moduleReference string, specifier *ast.Node) { + panic("unimplemented") +} + +// GetNearestAncestorDirectoryWithPackageJson implements checker.Program. +func (r *aliasResolver) GetNearestAncestorDirectoryWithPackageJson(dirname string) string { + panic("unimplemented") +} + +// GetPackageJsonInfo implements checker.Program. +func (r *aliasResolver) GetPackageJsonInfo(pkgJsonPath string) *packagejson.InfoCacheEntry { + panic("unimplemented") +} + +// GetProjectReferenceFromOutputDts implements checker.Program. +func (r *aliasResolver) GetProjectReferenceFromOutputDts(path tspath.Path) *tsoptions.SourceOutputAndProjectReference { + panic("unimplemented") +} + +// GetProjectReferenceFromSource implements checker.Program. +func (r *aliasResolver) GetProjectReferenceFromSource(path tspath.Path) *tsoptions.SourceOutputAndProjectReference { + panic("unimplemented") +} + +// GetRedirectForResolution implements checker.Program. +func (r *aliasResolver) GetRedirectForResolution(file ast.HasFileName) *tsoptions.ParsedCommandLine { + panic("unimplemented") +} + +// GetRedirectTargets implements checker.Program. +func (r *aliasResolver) GetRedirectTargets(path tspath.Path) []string { + panic("unimplemented") +} + +// GetResolvedModuleFromModuleSpecifier implements checker.Program. +func (r *aliasResolver) GetResolvedModuleFromModuleSpecifier(file ast.HasFileName, moduleSpecifier *ast.StringLiteralLike) *module.ResolvedModule { + panic("unimplemented") +} + +// GetSourceOfProjectReferenceIfOutputIncluded implements checker.Program. +func (r *aliasResolver) GetSourceOfProjectReferenceIfOutputIncluded(file ast.HasFileName) string { + panic("unimplemented") +} + +// IsSourceFileDefaultLibrary implements checker.Program. +func (r *aliasResolver) IsSourceFileDefaultLibrary(path tspath.Path) bool { + panic("unimplemented") +} + +// IsSourceFromProjectReference implements checker.Program. +func (r *aliasResolver) IsSourceFromProjectReference(path tspath.Path) bool { + panic("unimplemented") +} + +// SourceFileMayBeEmitted implements checker.Program. +func (r *aliasResolver) SourceFileMayBeEmitted(sourceFile *ast.SourceFile, forceDtsEmit bool) bool { + panic("unimplemented") +} + +var _ checker.Program = (*aliasResolver)(nil) diff --git a/internal/ls/autoimport/export.go b/internal/ls/autoimport/export.go new file mode 100644 index 0000000000..a338e0936d --- /dev/null +++ b/internal/ls/autoimport/export.go @@ -0,0 +1,116 @@ +package autoimport + +import ( + "strings" + + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/checker" + "github.com/microsoft/typescript-go/internal/collections" + "github.com/microsoft/typescript-go/internal/ls/lsutil" + "github.com/microsoft/typescript-go/internal/tspath" +) + +//go:generate go tool golang.org/x/tools/cmd/stringer -type=ExportSyntax -output=export_stringer_generated.go +//go:generate go tool mvdan.cc/gofumpt -w export_stringer_generated.go + +// ModuleID uniquely identifies a module across multiple declarations. +// If the export is from an ambient module declaration, this is the module name. +// If the export is from a module augmentation, this is the Path() of the resolved module file. +// Otherwise this is the Path() of the exporting source file. +type ModuleID string + +type ExportID struct { + ModuleID ModuleID + ExportName string +} + +type ExportSyntax int + +const ( + ExportSyntaxNone ExportSyntax = iota + // export const x = {} + ExportSyntaxModifier + // export { x } + ExportSyntaxNamed + // export default function f() {} + ExportSyntaxDefaultModifier + // export default f + ExportSyntaxDefaultDeclaration + // export = x + ExportSyntaxEquals + // export as namespace x + ExportSyntaxUMD + // export * from "module" + ExportSyntaxStar + // module.exports = {} + ExportSyntaxCommonJSModuleExports + // exports.x = {} + ExportSyntaxCommonJSExportsProperty +) + +type Export struct { + ExportID + ModuleFileName string + Syntax ExportSyntax + Flags ast.SymbolFlags + localName string + // through is the name of the module symbol's export that this export was found on, + // either 'export=', InternalSymbolNameExportStar, or empty string. + through string + + // Checker-set fields + + Target ExportID + IsTypeOnly bool + ScriptElementKind lsutil.ScriptElementKind + ScriptElementKindModifiers collections.Set[lsutil.ScriptElementKindModifier] + + // The file where the export was found. + Path tspath.Path + + NodeModulesDirectory tspath.Path + PackageName string +} + +func (e *Export) Name() string { + if e.localName != "" { + return e.localName + } + if e.ExportName == ast.InternalSymbolNameExportEquals { + return e.Target.ExportName + } + if strings.HasPrefix(e.ExportName, ast.InternalSymbolNamePrefix) { + panic("unexpected internal symbol name in export") + } + return e.ExportName +} + +func (e *Export) IsRenameable() bool { + return e.ExportName == ast.InternalSymbolNameExportEquals || e.ExportName == ast.InternalSymbolNameDefault +} + +func (e *Export) AmbientModuleName() string { + if !tspath.IsExternalModuleNameRelative(string(e.ModuleID)) { + return string(e.ModuleID) + } + return "" +} + +func (e *Export) IsUnresolvedAlias() bool { + return e.Flags == ast.SymbolFlagsAlias +} + +func SymbolToExport(symbol *ast.Symbol, ch *checker.Checker) *Export { + if symbol.Parent == nil || !checker.IsExternalModuleSymbol(symbol.Parent) { + return nil + } + moduleID, moduleFileName := getModuleIDAndFileNameOfModuleSymbol(symbol.Parent) + extractor := newSymbolExtractor("", "", ch) + + var exports []*Export + extractor.extractFromSymbol(symbol.Name, symbol, moduleID, moduleFileName, ast.GetSourceFileOfModule(symbol.Parent), &exports) + if len(exports) > 0 { + return exports[0] + } + return nil +} diff --git a/internal/ls/autoimport/export_stringer_generated.go b/internal/ls/autoimport/export_stringer_generated.go new file mode 100644 index 0000000000..ac00ae761b --- /dev/null +++ b/internal/ls/autoimport/export_stringer_generated.go @@ -0,0 +1,33 @@ +// Code generated by "stringer -type=ExportSyntax -output=export_stringer_generated.go"; DO NOT EDIT. + +package autoimport + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[ExportSyntaxNone-0] + _ = x[ExportSyntaxModifier-1] + _ = x[ExportSyntaxNamed-2] + _ = x[ExportSyntaxDefaultModifier-3] + _ = x[ExportSyntaxDefaultDeclaration-4] + _ = x[ExportSyntaxEquals-5] + _ = x[ExportSyntaxUMD-6] + _ = x[ExportSyntaxStar-7] + _ = x[ExportSyntaxCommonJSModuleExports-8] + _ = x[ExportSyntaxCommonJSExportsProperty-9] +} + +const _ExportSyntax_name = "ExportSyntaxNoneExportSyntaxModifierExportSyntaxNamedExportSyntaxDefaultModifierExportSyntaxDefaultDeclarationExportSyntaxEqualsExportSyntaxUMDExportSyntaxStarExportSyntaxCommonJSModuleExportsExportSyntaxCommonJSExportsProperty" + +var _ExportSyntax_index = [...]uint8{0, 16, 36, 53, 80, 110, 128, 143, 159, 192, 227} + +func (i ExportSyntax) String() string { + idx := int(i) - 0 + if i < 0 || idx >= len(_ExportSyntax_index)-1 { + return "ExportSyntax(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _ExportSyntax_name[_ExportSyntax_index[idx]:_ExportSyntax_index[idx+1]] +} diff --git a/internal/ls/autoimport/extract.go b/internal/ls/autoimport/extract.go new file mode 100644 index 0000000000..568d1ef391 --- /dev/null +++ b/internal/ls/autoimport/extract.go @@ -0,0 +1,401 @@ +package autoimport + +import ( + "slices" + "sync/atomic" + + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/binder" + "github.com/microsoft/typescript-go/internal/checker" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/ls/lsutil" + "github.com/microsoft/typescript-go/internal/module" + "github.com/microsoft/typescript-go/internal/tspath" +) + +type symbolExtractor struct { + nodeModulesDirectory tspath.Path + packageName string + stats *extractorStats + + localNameResolver *binder.NameResolver + checker *checker.Checker +} + +type exportExtractor struct { + *symbolExtractor + moduleResolver *module.Resolver + toPath func(fileName string) tspath.Path +} + +type extractorStats struct { + exports atomic.Int32 + usedChecker atomic.Int32 +} + +func (e *exportExtractor) Stats() *extractorStats { + return e.stats +} + +type checkerLease struct { + used bool + checker *checker.Checker +} + +func (l *checkerLease) GetChecker() *checker.Checker { + l.used = true + return l.checker +} + +func (l *checkerLease) TryChecker() *checker.Checker { + if l.used { + return l.checker + } + return nil +} + +func newSymbolExtractor(nodeModulesDirectory tspath.Path, packageName string, checker *checker.Checker) *symbolExtractor { + return &symbolExtractor{ + nodeModulesDirectory: nodeModulesDirectory, + packageName: packageName, + checker: checker, + localNameResolver: &binder.NameResolver{ + CompilerOptions: core.EmptyCompilerOptions, + }, + stats: &extractorStats{}, + } +} + +func (b *registryBuilder) newExportExtractor(nodeModulesDirectory tspath.Path, packageName string, checker *checker.Checker) *exportExtractor { + return &exportExtractor{ + symbolExtractor: newSymbolExtractor(nodeModulesDirectory, packageName, checker), + moduleResolver: b.resolver, + toPath: b.base.toPath, + } +} + +func (e *exportExtractor) extractFromFile(file *ast.SourceFile) []*Export { + if file.Symbol != nil { + return e.extractFromModule(file) + } + if len(file.AmbientModuleNames) > 0 { + moduleDeclarations := core.Filter(file.Statements.Nodes, ast.IsModuleWithStringLiteralName) + var exportCount int + for _, decl := range moduleDeclarations { + exportCount += len(decl.AsModuleDeclaration().Symbol.Exports) + } + exports := make([]*Export, 0, exportCount) + for _, decl := range moduleDeclarations { + e.extractFromModuleDeclaration(decl.AsModuleDeclaration(), file, ModuleID(decl.Name().Text()), "", &exports) + } + return exports + } + return nil +} + +func (e *exportExtractor) extractFromModule(file *ast.SourceFile) []*Export { + moduleAugmentations := core.MapNonNil(file.ModuleAugmentations, func(name *ast.ModuleName) *ast.ModuleDeclaration { + decl := name.Parent + if ast.IsGlobalScopeAugmentation(decl) { + return nil + } + return decl.AsModuleDeclaration() + }) + var augmentationExportCount int + for _, decl := range moduleAugmentations { + augmentationExportCount += len(decl.Symbol.Exports) + } + exports := make([]*Export, 0, len(file.Symbol.Exports)+augmentationExportCount) + for name, symbol := range file.Symbol.Exports { + e.extractFromSymbol(name, symbol, ModuleID(file.Path()), file.FileName(), file, &exports) + } + for _, decl := range moduleAugmentations { + name := decl.Name().AsStringLiteral().Text + moduleID := ModuleID(name) + var moduleFileName string + if tspath.IsExternalModuleNameRelative(name) { + if resolved, _ := e.moduleResolver.ResolveModuleName(name, file.FileName(), core.ModuleKindCommonJS, nil); resolved.IsResolved() { + moduleFileName = resolved.ResolvedFileName + moduleID = ModuleID(e.toPath(moduleFileName)) + } else { + // :shrug: + moduleFileName = tspath.ResolvePath(tspath.GetDirectoryPath(file.FileName()), name) + moduleID = ModuleID(e.toPath(moduleFileName)) + } + } + e.extractFromModuleDeclaration(decl, file, moduleID, moduleFileName, &exports) + } + return exports +} + +func (e *exportExtractor) extractFromModuleDeclaration(decl *ast.ModuleDeclaration, file *ast.SourceFile, moduleID ModuleID, moduleFileName string, exports *[]*Export) { + for name, symbol := range decl.Symbol.Exports { + e.extractFromSymbol(name, symbol, moduleID, moduleFileName, file, exports) + } +} + +func (e *symbolExtractor) extractFromSymbol(name string, symbol *ast.Symbol, moduleID ModuleID, moduleFileName string, file *ast.SourceFile, exports *[]*Export) { + if shouldIgnoreSymbol(symbol) { + return + } + + if name == ast.InternalSymbolNameExportStar { + checkerLease := &checkerLease{checker: e.checker} + allExports := e.checker.GetExportsOfModule(symbol.Parent) + // allExports includes named exports from the file that will be processed separately; + // we want to add only the ones that come from the star + for name, namedExport := range symbol.Parent.Exports { + if name != ast.InternalSymbolNameExportStar { + idx := slices.Index(allExports, namedExport) + if idx >= 0 || shouldIgnoreSymbol(namedExport) { + allExports = slices.Delete(allExports, idx, idx+1) + } + } + } + + *exports = slices.Grow(*exports, len(allExports)) + for _, reexportedSymbol := range allExports { + export, _ := e.createExport(reexportedSymbol, moduleID, moduleFileName, ExportSyntaxStar, file, checkerLease) + if export != nil { + parent := reexportedSymbol.Parent + if parent != nil && parent.IsExternalModule() { + targetModuleID, _ := getModuleIDAndFileNameOfModuleSymbol(parent) + export.Target = ExportID{ + ExportName: reexportedSymbol.Name, + ModuleID: targetModuleID, + } + } + export.through = ast.InternalSymbolNameExportStar + *exports = append(*exports, export) + } + } + return + } + + syntax := getSyntax(symbol) + checkerLease := &checkerLease{checker: e.checker} + export, target := e.createExport(symbol, moduleID, moduleFileName, syntax, file, checkerLease) + if export == nil { + return + } + + if symbol.Name == ast.InternalSymbolNameDefault || symbol.Name == ast.InternalSymbolNameExportEquals { + namedSymbol := symbol + if s := binder.GetLocalSymbolForExportDefault(symbol); s != nil { + namedSymbol = s + } + export.localName = getDefaultLikeExportNameFromDeclaration(namedSymbol) + if isUnusableName(export.localName) { + export.localName = export.Target.ExportName + } + if isUnusableName(export.localName) { + if target != nil { + namedSymbol = target + if s := binder.GetLocalSymbolForExportDefault(target); s != nil { + namedSymbol = s + } + export.localName = getDefaultLikeExportNameFromDeclaration(namedSymbol) + if isUnusableName(export.localName) { + export.localName = lsutil.ModuleSpecifierToValidIdentifier(string(export.Target.ModuleID), core.ScriptTargetESNext, false) + } + } else { + export.localName = lsutil.ModuleSpecifierToValidIdentifier(string(moduleID), core.ScriptTargetESNext, false) + } + } + } + + *exports = append(*exports, export) + + if target != nil { + if syntax == ExportSyntaxEquals && target.Flags&ast.SymbolFlagsNamespace != 0 { + *exports = slices.Grow(*exports, len(target.Exports)) + for _, namedExport := range target.Exports { + export, _ := e.createExport(namedExport, moduleID, moduleFileName, syntax, file, checkerLease) + if export != nil { + export.through = name + *exports = append(*exports, export) + } + } + } + } else if syntax == ExportSyntaxCommonJSModuleExports { + expression := symbol.Declarations[0].AsExportAssignment().Expression + if expression.Kind == ast.KindObjectLiteralExpression { + // what is actually desirable here? I think it would be reasonable to only treat these as exports + // if *every* property is a shorthand property or identifier: identifier + // At least, it would be sketchy if there were any methods, computed properties... + *exports = slices.Grow(*exports, len(expression.AsObjectLiteralExpression().Properties.Nodes)) + for _, prop := range expression.AsObjectLiteralExpression().Properties.Nodes { + if ast.IsShorthandPropertyAssignment(prop) || ast.IsPropertyAssignment(prop) && prop.AsPropertyAssignment().Name().Kind == ast.KindIdentifier { + export, _ := e.createExport(expression.Symbol().Members[prop.Name().Text()], moduleID, moduleFileName, syntax, file, checkerLease) + if export != nil { + export.through = name + *exports = append(*exports, export) + } + } + } + } + } +} + +// createExport creates an Export for the given symbol, returning the Export and the target symbol if the export is an alias. +func (e *symbolExtractor) createExport(symbol *ast.Symbol, moduleID ModuleID, moduleFileName string, syntax ExportSyntax, file *ast.SourceFile, checkerLease *checkerLease) (*Export, *ast.Symbol) { + if shouldIgnoreSymbol(symbol) { + return nil, nil + } + + export := &Export{ + ExportID: ExportID{ + ExportName: symbol.Name, + ModuleID: moduleID, + }, + ModuleFileName: moduleFileName, + Syntax: syntax, + Flags: symbol.CombinedLocalAndExportSymbolFlags(), + Path: file.Path(), + NodeModulesDirectory: e.nodeModulesDirectory, + PackageName: e.packageName, + } + + if syntax == ExportSyntaxUMD { + export.ExportName = ast.InternalSymbolNameExportEquals + export.localName = symbol.Name + } + + var targetSymbol *ast.Symbol + if symbol.Flags&ast.SymbolFlagsAlias != 0 { + targetSymbol = e.tryResolveSymbol(symbol, syntax, checkerLease) + if targetSymbol != nil { + var decl *ast.Node + if len(targetSymbol.Declarations) > 0 { + decl = targetSymbol.Declarations[0] + } else if targetSymbol.CheckFlags&ast.CheckFlagsMapped != 0 { + if mappedDecl := checkerLease.GetChecker().GetMappedTypeSymbolOfProperty(targetSymbol); mappedDecl != nil && len(mappedDecl.Declarations) > 0 { + decl = mappedDecl.Declarations[0] + } + } + if decl == nil { + // !!! consider GetImmediateAliasedSymbol to go as far as we can + decl = symbol.Declarations[0] + } + if decl == nil { + panic("no declaration for aliased symbol") + } + + if checker := checkerLease.TryChecker(); checker != nil { + export.Flags = checker.GetSymbolFlags(targetSymbol) + export.IsTypeOnly = checker.GetTypeOnlyAliasDeclaration(symbol) != nil + } else { + export.Flags = targetSymbol.Flags + export.IsTypeOnly = core.Some(symbol.Declarations, ast.IsPartOfTypeOnlyImportOrExportDeclaration) + } + export.ScriptElementKind = lsutil.GetSymbolKind(checkerLease.TryChecker(), targetSymbol, decl) + export.ScriptElementKindModifiers = lsutil.GetSymbolModifiers(checkerLease.TryChecker(), targetSymbol) + moduleID := ModuleID(ast.GetSourceFileOfNode(decl).Path()) + parent := targetSymbol.Parent + if parent != nil && parent.IsExternalModule() { + moduleID, _ = getModuleIDAndFileNameOfModuleSymbol(parent) + } + export.Target = ExportID{ + ExportName: targetSymbol.Name, + ModuleID: moduleID, + } + } + } else { + export.ScriptElementKind = lsutil.GetSymbolKind(checkerLease.TryChecker(), symbol, symbol.Declarations[0]) + export.ScriptElementKindModifiers = lsutil.GetSymbolModifiers(checkerLease.TryChecker(), symbol) + } + + e.stats.exports.Add(1) + if checkerLease.TryChecker() != nil { + e.stats.usedChecker.Add(1) + } + + return export, targetSymbol +} + +func (e *symbolExtractor) tryResolveSymbol(symbol *ast.Symbol, syntax ExportSyntax, checkerLease *checkerLease) *ast.Symbol { + if !ast.IsNonLocalAlias(symbol, ast.SymbolFlagsNone) { + return symbol + } + + var loc *ast.Node + var name string + switch syntax { + case ExportSyntaxNamed: + decl := ast.GetDeclarationOfKind(symbol, ast.KindExportSpecifier) + if decl.Parent.Parent.AsExportDeclaration().ModuleSpecifier == nil { + if n := core.FirstNonZero(decl.Name(), decl.PropertyName()); n.Kind == ast.KindIdentifier { + loc = n + name = n.Text() + } + } + // !!! check if module.exports = foo is marked as an alias + case ExportSyntaxEquals: + if symbol.Name != ast.InternalSymbolNameExportEquals { + break + } + fallthrough + case ExportSyntaxDefaultDeclaration: + decl := ast.GetDeclarationOfKind(symbol, ast.KindExportAssignment) + if decl.Expression().Kind == ast.KindIdentifier { + loc = decl.Expression() + name = loc.Text() + } + } + + if loc != nil { + local := e.localNameResolver.Resolve(loc, name, ast.SymbolFlagsAll, nil, false, false) + if local != nil && !ast.IsNonLocalAlias(local, ast.SymbolFlagsNone) { + return local + } + } + + checker := checkerLease.GetChecker() + if resolved := checker.GetAliasedSymbol(symbol); !checker.IsUnknownSymbol(resolved) { + return resolved + } + return nil +} + +func shouldIgnoreSymbol(symbol *ast.Symbol) bool { + if symbol.Flags&ast.SymbolFlagsPrototype != 0 { + return true + } + return false +} + +func getSyntax(symbol *ast.Symbol) ExportSyntax { + for _, decl := range symbol.Declarations { + switch decl.Kind { + case ast.KindExportSpecifier: + return ExportSyntaxNamed + case ast.KindExportAssignment: + return core.IfElse( + decl.AsExportAssignment().IsExportEquals, + ExportSyntaxEquals, + ExportSyntaxDefaultDeclaration, + ) + case ast.KindNamespaceExportDeclaration: + return ExportSyntaxUMD + case ast.KindJSExportAssignment: + return ExportSyntaxCommonJSModuleExports + case ast.KindCommonJSExport: + return ExportSyntaxCommonJSExportsProperty + default: + if ast.GetCombinedModifierFlags(decl)&ast.ModifierFlagsDefault != 0 { + return ExportSyntaxDefaultModifier + } else { + return ExportSyntaxModifier + } + } + } + return ExportSyntaxNone +} + +func isUnusableName(name string) bool { + return name == "" || + name == "_default" || + name == ast.InternalSymbolNameExportStar || + name == ast.InternalSymbolNameDefault || + name == ast.InternalSymbolNameExportEquals +} diff --git a/internal/ls/autoimport/fix.go b/internal/ls/autoimport/fix.go new file mode 100644 index 0000000000..32e7667d63 --- /dev/null +++ b/internal/ls/autoimport/fix.go @@ -0,0 +1,1175 @@ +package autoimport + +import ( + "cmp" + "context" + "fmt" + "slices" + "strings" + "unicode" + + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/astnav" + "github.com/microsoft/typescript-go/internal/checker" + "github.com/microsoft/typescript-go/internal/collections" + "github.com/microsoft/typescript-go/internal/compiler" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/debug" + "github.com/microsoft/typescript-go/internal/diagnostics" + "github.com/microsoft/typescript-go/internal/format" + "github.com/microsoft/typescript-go/internal/locale" + "github.com/microsoft/typescript-go/internal/ls/change" + "github.com/microsoft/typescript-go/internal/ls/lsconv" + "github.com/microsoft/typescript-go/internal/ls/lsutil" + "github.com/microsoft/typescript-go/internal/ls/organizeimports" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/modulespecifiers" + "github.com/microsoft/typescript-go/internal/scanner" + "github.com/microsoft/typescript-go/internal/stringutil" + "github.com/microsoft/typescript-go/internal/tspath" +) + +type newImportBinding struct { + kind lsproto.ImportKind + propertyName string + name string + addAsTypeOnly lsproto.AddAsTypeOnly +} + +type Fix struct { + *lsproto.AutoImportFix + + ModuleSpecifierKind modulespecifiers.ResultKind + IsReExport bool + ModuleFileName string + TypeOnlyAliasDeclaration *ast.Declaration +} + +func (f *Fix) Edits( + ctx context.Context, + file *ast.SourceFile, + compilerOptions *core.CompilerOptions, + formatOptions *format.FormatCodeSettings, + converters *lsconv.Converters, + preferences *lsutil.UserPreferences, +) ([]*lsproto.TextEdit, string) { + locale := locale.FromContext(ctx) + tracker := change.NewTracker(ctx, compilerOptions, formatOptions, converters) + switch f.Kind { + case lsproto.AutoImportFixKindUseNamespace: + if f.UsagePosition == nil || f.NamespacePrefix == "" { + panic("namespace fix requires usage position and prefix") + } + qualified := fmt.Sprintf("%s.%s", f.NamespacePrefix, f.Name) + tracker.InsertText(file, *f.UsagePosition, f.NamespacePrefix+".") + return tracker.GetChanges()[file.FileName()], diagnostics.Change_0_to_1.Localize(locale, f.Name, qualified) + case lsproto.AutoImportFixKindAddToExisting: + if len(file.Imports()) <= int(f.ImportIndex) { + panic("import index out of range") + } + moduleSpecifier := file.Imports()[f.ImportIndex] + importNode := ast.TryGetImportFromModuleSpecifier(moduleSpecifier) + if importNode == nil { + panic("expected import declaration") + } + var importClauseOrBindingPattern *ast.Node + switch importNode.Kind { + case ast.KindImportDeclaration: + importClauseOrBindingPattern = importNode.ImportClause() + if importClauseOrBindingPattern == nil { + panic("expected import clause") + } + case ast.KindCallExpression: + if !ast.IsVariableDeclarationInitializedToRequire(importNode.Parent) { + panic("expected require call expression to be in variable declaration") + } + importClauseOrBindingPattern = importNode.Parent.Name() + if importClauseOrBindingPattern == nil || !ast.IsObjectBindingPattern(importClauseOrBindingPattern) { + panic("expected object binding pattern in variable declaration") + } + default: + panic("expected import declaration or require call expression") + } + + defaultImport := core.IfElse(f.ImportKind == lsproto.ImportKindDefault, &newImportBinding{kind: lsproto.ImportKindDefault, name: f.Name, addAsTypeOnly: f.AddAsTypeOnly}, nil) + namedImports := core.IfElse(f.ImportKind == lsproto.ImportKindNamed, []*newImportBinding{{kind: lsproto.ImportKindNamed, name: f.Name, addAsTypeOnly: f.AddAsTypeOnly}}, nil) + addToExistingImport(tracker, file, importClauseOrBindingPattern, defaultImport, namedImports, preferences) + return tracker.GetChanges()[file.FileName()], diagnostics.Update_import_from_0.Localize(locale, f.ModuleSpecifier) + case lsproto.AutoImportFixKindAddNew: + var declarations []*ast.Statement + defaultImport := core.IfElse(f.ImportKind == lsproto.ImportKindDefault, &newImportBinding{name: f.Name, addAsTypeOnly: f.AddAsTypeOnly}, nil) + namedImports := core.IfElse(f.ImportKind == lsproto.ImportKindNamed, []*newImportBinding{{name: f.Name, addAsTypeOnly: f.AddAsTypeOnly}}, nil) + var namespaceLikeImport *newImportBinding + // qualification := f.qualification() + if f.ImportKind == lsproto.ImportKindNamespace || f.ImportKind == lsproto.ImportKindCommonJS { + namespaceLikeImport = &newImportBinding{kind: f.ImportKind, name: f.Name} + // if qualification != nil && qualification.namespacePref != "" { + // namespaceLikeImport.name = qualification.namespacePref + // } + } + + quotePreference := lsutil.GetQuotePreference(file, preferences) + if f.UseRequire { + declarations = getNewRequires(tracker, f.ModuleSpecifier, quotePreference, defaultImport, namedImports, namespaceLikeImport, compilerOptions) + } else { + declarations = getNewImports(tracker, f.ModuleSpecifier, quotePreference, defaultImport, namedImports, namespaceLikeImport, compilerOptions, preferences) + } + + insertImports( + tracker, + file, + declarations, + /*blankLineBetween*/ true, + preferences, + ) + // if qualification != nil { + // addNamespaceQualifier(tracker, file, qualification) + // } + return tracker.GetChanges()[file.FileName()], diagnostics.Add_import_from_0.Localize(locale, f.ModuleSpecifier) + case lsproto.AutoImportFixKindPromoteTypeOnly: + promotedDeclaration := promoteFromTypeOnly(tracker, f.TypeOnlyAliasDeclaration, compilerOptions, file, preferences) + if promotedDeclaration.Kind == ast.KindImportSpecifier { + moduleSpec := getModuleSpecifierText(promotedDeclaration.Parent.Parent) + return tracker.GetChanges()[file.FileName()], diagnostics.Remove_type_from_import_of_0_from_1.Localize(locale, f.Name, moduleSpec) + } + moduleSpec := getModuleSpecifierText(promotedDeclaration) + return tracker.GetChanges()[file.FileName()], diagnostics.Remove_type_from_import_declaration_from_0.Localize(locale, moduleSpec) + case lsproto.AutoImportFixKindJsdocTypeImport: + if f.UsagePosition == nil { + panic("UsagePosition must be set for JSDoc type import fix") + } + quotePreference := lsutil.GetQuotePreference(file, preferences) + quoteChar := "\"" + if quotePreference == lsutil.QuotePreferenceSingle { + quoteChar = "'" + } + importTypePrefix := fmt.Sprintf("import(%s%s%s).", quoteChar, f.ModuleSpecifier, quoteChar) + tracker.InsertText(file, *f.UsagePosition, importTypePrefix) + return tracker.GetChanges()[file.FileName()], diagnostics.Change_0_to_1.Localize(locale, f.Name, importTypePrefix+f.Name) + default: + panic("unimplemented fix edit") + } +} + +func addToExistingImport( + ct *change.Tracker, + file *ast.SourceFile, + importClauseOrBindingPattern *ast.Node, + defaultImport *newImportBinding, + namedImports []*newImportBinding, + preferences *lsutil.UserPreferences, +) { + switch importClauseOrBindingPattern.Kind { + case ast.KindObjectBindingPattern: + bindingPattern := importClauseOrBindingPattern.AsBindingPattern() + if defaultImport != nil { + addElementToBindingPattern(ct, file, bindingPattern, defaultImport.name, "default") + } + for _, namedImport := range namedImports { + addElementToBindingPattern(ct, file, bindingPattern, namedImport.name, "") + } + return + case ast.KindImportClause: + importClause := importClauseOrBindingPattern.AsImportClause() + + // promoteFromTypeOnly = true if we need to promote the entire original clause from type only + promoteFromTypeOnly := importClause.IsTypeOnly() && core.Some(append(namedImports, defaultImport), func(i *newImportBinding) bool { + if i == nil { + return false + } + return i.addAsTypeOnly == lsproto.AddAsTypeOnlyNotAllowed + }) + + var existingSpecifiers []*ast.Node + if importClause.NamedBindings != nil && importClause.NamedBindings.Kind == ast.KindNamedImports { + existingSpecifiers = importClause.NamedBindings.Elements() + } + + if defaultImport != nil { + debug.Assert(importClause.Name() == nil, "Cannot add a default import to an import clause that already has one") + ct.InsertNodeAt(file, core.TextPos(astnav.GetStartOfNode(importClause.AsNode(), file, false)), ct.NodeFactory.NewIdentifier(defaultImport.name), change.NodeOptions{Suffix: ", "}) + } + + if len(namedImports) > 0 { + specifierComparer, isSorted := organizeimports.GetNamedImportSpecifierComparerWithDetection(importClause.Parent, file, preferences) + newSpecifiers := core.Map(namedImports, func(namedImport *newImportBinding) *ast.Node { + var identifier *ast.Node + if namedImport.propertyName != "" { + identifier = ct.NodeFactory.NewIdentifier(namedImport.propertyName).AsIdentifier().AsNode() + } + return ct.NodeFactory.NewImportSpecifier( + shouldUseTypeOnly(namedImport.addAsTypeOnly, preferences), + identifier, + ct.NodeFactory.NewIdentifier(namedImport.name), + ) + }) + slices.SortFunc(newSpecifiers, specifierComparer) + if len(existingSpecifiers) > 0 && isSorted != core.TSFalse { + // The sorting preference computed earlier may or may not have validated that these particular + // import specifiers are sorted. If they aren't, `getImportSpecifierInsertionIndex` will return + // nonsense. So if there are existing specifiers, even if we know the sorting preference, we + // need to ensure that the existing specifiers are sorted according to the preference in order + // to do a sorted insertion. + + // If we're promoting the clause from type-only, we need to transform the existing imports + // before attempting to insert the new named imports (for comparison purposes only) + specsToCompareAgainst := existingSpecifiers + if promoteFromTypeOnly && len(existingSpecifiers) > 0 { + specsToCompareAgainst = core.Map(existingSpecifiers, func(e *ast.Node) *ast.Node { + spec := e.AsImportSpecifier() + var propertyName *ast.Node + if spec.PropertyName != nil { + propertyName = spec.PropertyName + } + syntheticSpec := ct.NodeFactory.NewImportSpecifier( + true, // isTypeOnly + propertyName, + spec.Name(), + ) + return syntheticSpec + }) + } + + for _, spec := range newSpecifiers { + insertionIndex := organizeimports.GetImportSpecifierInsertionIndex(specsToCompareAgainst, spec, specifierComparer) + ct.InsertImportSpecifierAtIndex(file, spec, importClause.NamedBindings, insertionIndex) + } + } else if len(existingSpecifiers) > 0 && isSorted.IsTrue() { + // Existing specifiers are sorted, so insert each new specifier at the correct position + for _, spec := range newSpecifiers { + insertionIndex := organizeimports.GetImportSpecifierInsertionIndex(existingSpecifiers, spec, specifierComparer) + if insertionIndex >= len(existingSpecifiers) { + // Insert at the end + ct.InsertNodeInListAfter(file, existingSpecifiers[len(existingSpecifiers)-1], spec.AsNode(), existingSpecifiers) + } else { + // Insert before the element at insertionIndex + ct.InsertNodeInListAfter(file, existingSpecifiers[insertionIndex], spec.AsNode(), existingSpecifiers) + } + } + } else if len(existingSpecifiers) > 0 { + // Existing specifiers may not be sorted, append to the end + for _, spec := range newSpecifiers { + ct.InsertNodeInListAfter(file, 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(file, importClause.NamedBindings, namedImports, nil) + } else { + if importClause.Name() == nil { + panic("Import clause must have either named imports or a default import") + } + ct.InsertNodeAfter(file, importClause.Name(), namedImports) + } + } + } + } + + if promoteFromTypeOnly { + // Delete the 'type' keyword from the import clause + typeKeyword := getTypeKeywordOfTypeOnlyImport(importClause, file) + ct.Delete(file, typeKeyword) + + // Add 'type' modifier to existing specifiers (not newly added ones) + // We preserve the type-onlyness of existing specifiers regardless of whether + // it would make a difference in emit (user preference). + if len(existingSpecifiers) > 0 { + for _, specifier := range existingSpecifiers { + if !specifier.AsImportSpecifier().IsTypeOnly { + ct.InsertModifierBefore(file, ast.KindTypeKeyword, specifier) + } + } + } + } + default: + panic("Unsupported clause kind: " + importClauseOrBindingPattern.KindString() + " for addToExistingImport") + } +} + +func getTypeKeywordOfTypeOnlyImport(importClause *ast.ImportClause, sourceFile *ast.SourceFile) *ast.Node { + debug.Assert(importClause.IsTypeOnly(), "import clause must be type-only") + // The first child of a type-only import clause is the 'type' keyword + // import type { foo } from './bar' + // ^^^^ + typeKeyword := astnav.FindChildOfKind(importClause.AsNode(), ast.KindTypeKeyword, sourceFile) + debug.Assert(typeKeyword != nil, "type-only import clause should have a type keyword") + return typeKeyword +} + +func addElementToBindingPattern( + ct *change.Tracker, + file *ast.SourceFile, + bindingPattern *ast.BindingPattern, + name string, + propertyName string, +) { + element := ct.NodeFactory.NewBindingElement(nil, nil, ct.NodeFactory.NewIdentifier(name), core.IfElse(propertyName == "", nil, ct.NodeFactory.NewIdentifier(propertyName))) + if len(bindingPattern.Elements.Nodes) > 0 { + ct.InsertNodeInListAfter(file, bindingPattern.Elements.Nodes[len(bindingPattern.Elements.Nodes)-1], element, bindingPattern.Elements.Nodes) + } else { + ct.ReplaceNode(file, bindingPattern.AsNode(), ct.NodeFactory.NewBindingPattern(ast.KindObjectBindingPattern, ct.AsNodeFactory().NewNodeList([]*ast.Node{element})), nil) + } +} + +func getNewImports( + ct *change.Tracker, + moduleSpecifier string, + quotePreference lsutil.QuotePreference, + defaultImport *newImportBinding, + namedImports []*newImportBinding, + namespaceLikeImport *newImportBinding, // { lsproto.importKind: lsproto.ImportKind.CommonJS | lsproto.ImportKind.Namespace; } + compilerOptions *core.CompilerOptions, + preferences *lsutil.UserPreferences, +) []*ast.Statement { + tokenFlags := core.IfElse(quotePreference == lsutil.QuotePreferenceSingle, ast.TokenFlagsSingleQuote, ast.TokenFlagsNone) + moduleSpecifierStringLiteral := ct.NodeFactory.NewStringLiteral(moduleSpecifier, tokenFlags) + 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 *newImportBinding) bool { return needsTypeOnly(i.addAsTypeOnly) }) || + (compilerOptions.VerbatimModuleSyntax.IsTrue() || preferences.PreferTypeOnlyAutoImports.IsTrue()) && + (defaultImport == nil || defaultImport.addAsTypeOnly != lsproto.AddAsTypeOnlyNotAllowed) && + !core.Some(namedImports, func(i *newImportBinding) bool { return i.addAsTypeOnly == lsproto.AddAsTypeOnlyNotAllowed }) + + var defaultImportNode *ast.Node + if defaultImport != nil { + defaultImportNode = ct.NodeFactory.NewIdentifier(defaultImport.name) + } + + statements = append(statements, makeImport(ct, defaultImportNode, core.Map(namedImports, func(namedImport *newImportBinding) *ast.Node { + var namedImportPropertyName *ast.Node + if namedImport.propertyName != "" { + namedImportPropertyName = ct.NodeFactory.NewIdentifier(namedImport.propertyName) + } + return ct.NodeFactory.NewImportSpecifier( + !topLevelTypeOnly && shouldUseTypeOnly(namedImport.addAsTypeOnly, preferences), + namedImportPropertyName, + ct.NodeFactory.NewIdentifier(namedImport.name), + ) + }), moduleSpecifierStringLiteral, topLevelTypeOnly)) + } + + if namespaceLikeImport != nil { + var declaration *ast.Statement + if namespaceLikeImport.kind == lsproto.ImportKindCommonJS { + declaration = ct.NodeFactory.NewImportEqualsDeclaration( + /*modifiers*/ nil, + shouldUseTypeOnly(namespaceLikeImport.addAsTypeOnly, preferences), + ct.NodeFactory.NewIdentifier(namespaceLikeImport.name), + ct.NodeFactory.NewExternalModuleReference(moduleSpecifierStringLiteral), + ) + } else { + declaration = ct.NodeFactory.NewImportDeclaration( + /*modifiers*/ nil, + ct.NodeFactory.NewImportClause( + /*phaseModifier*/ core.IfElse(shouldUseTypeOnly(namespaceLikeImport.addAsTypeOnly, preferences), ast.KindTypeKeyword, ast.KindUnknown), + /*name*/ nil, + ct.NodeFactory.NewNamespaceImport(ct.NodeFactory.NewIdentifier(namespaceLikeImport.name)), + ), + moduleSpecifierStringLiteral, + /*attributes*/ nil, + ) + } + statements = append(statements, declaration) + } + if len(statements) == 0 { + panic("No statements to insert for new imports") + } + return statements +} + +func getNewRequires( + changeTracker *change.Tracker, + moduleSpecifier string, + quotePreference lsutil.QuotePreference, + defaultImport *newImportBinding, + namedImports []*newImportBinding, + namespaceLikeImport *newImportBinding, + compilerOptions *core.CompilerOptions, +) []*ast.Statement { + quotedModuleSpecifier := changeTracker.NodeFactory.NewStringLiteral( + moduleSpecifier, + core.IfElse(quotePreference == lsutil.QuotePreferenceSingle, ast.TokenFlagsSingleQuote, ast.TokenFlagsNone), + ) + var statements []*ast.Statement + + // const { default: foo, bar, etc } = require('./mod'); + if defaultImport != nil || len(namedImports) > 0 { + bindingElements := []*ast.Node{} + for _, namedImport := range namedImports { + var propertyName *ast.Node + if namedImport.propertyName != "" { + propertyName = changeTracker.NodeFactory.NewIdentifier(namedImport.propertyName) + } + bindingElements = append(bindingElements, changeTracker.NodeFactory.NewBindingElement( + /*dotDotDotToken*/ nil, + propertyName, + changeTracker.NodeFactory.NewIdentifier(namedImport.name), + /*initializer*/ nil, + )) + } + if defaultImport != nil { + bindingElements = append([]*ast.Node{ + changeTracker.NodeFactory.NewBindingElement( + /*dotDotDotToken*/ nil, + changeTracker.NodeFactory.NewIdentifier("default"), + changeTracker.NodeFactory.NewIdentifier(defaultImport.name), + /*initializer*/ nil, + ), + }, bindingElements...) + } + declaration := createConstEqualsRequireDeclaration( + changeTracker, + changeTracker.NodeFactory.NewBindingPattern( + ast.KindObjectBindingPattern, + changeTracker.NodeFactory.NewNodeList(bindingElements), + ), + quotedModuleSpecifier, + ) + statements = append(statements, declaration) + } + + // const foo = require('./mod'); + if namespaceLikeImport != nil { + declaration := createConstEqualsRequireDeclaration( + changeTracker, + changeTracker.NodeFactory.NewIdentifier(namespaceLikeImport.name), + quotedModuleSpecifier, + ) + statements = append(statements, declaration) + } + + debug.AssertIsDefined(statements) + return statements +} + +func createConstEqualsRequireDeclaration(changeTracker *change.Tracker, name *ast.Node, quotedModuleSpecifier *ast.Node) *ast.Statement { + return changeTracker.NodeFactory.NewVariableStatement( + /*modifiers*/ nil, + changeTracker.NodeFactory.NewVariableDeclarationList( + ast.NodeFlagsConst, + changeTracker.NodeFactory.NewNodeList([]*ast.Node{ + changeTracker.NodeFactory.NewVariableDeclaration( + name, + /*exclamationToken*/ nil, + /*type*/ nil, + changeTracker.NodeFactory.NewCallExpression( + changeTracker.NodeFactory.NewIdentifier("require"), + /*questionDotToken*/ nil, + /*typeArguments*/ nil, + changeTracker.NodeFactory.NewNodeList([]*ast.Node{quotedModuleSpecifier}), + ast.NodeFlagsNone, + ), + ), + }), + ), + ) +} + +func insertImports(ct *change.Tracker, sourceFile *ast.SourceFile, imports []*ast.Statement, blankLineBetween bool, preferences *lsutil.UserPreferences) { + var existingImportStatements []*ast.Statement + + if imports[0].Kind == ast.KindVariableStatement { + existingImportStatements = core.Filter(sourceFile.Statements.Nodes, ast.IsRequireVariableStatement) + } else { + existingImportStatements = core.Filter(sourceFile.Statements.Nodes, ast.IsAnyImportSyntax) + } + comparer, isSorted := organizeimports.GetOrganizeImportsStringComparerWithDetection(existingImportStatements, preferences) + sortedNewImports := slices.Clone(imports) + slices.SortFunc(sortedNewImports, func(a, b *ast.Statement) int { + return organizeimports.CompareImportsOrRequireStatements(a, b, comparer) + }) + + if len(existingImportStatements) > 0 && isSorted { + // Existing imports are sorted, insert each new import at the correct position + for _, newImport := range sortedNewImports { + insertionIndex := organizeimports.GetImportDeclarationInsertIndex(existingImportStatements, newImport, func(a, b *ast.Statement) stringutil.Comparison { + return organizeimports.CompareImportsOrRequireStatements(a, b, comparer) + }) + if insertionIndex == 0 { + // If the first import is top-of-file, insert after the leading comment which is likely the header + ct.InsertNodeAt(sourceFile, core.TextPos(astnav.GetStartOfNode(existingImportStatements[0], sourceFile, false)), newImport.AsNode(), change.NodeOptions{}) + } else { + prevImport := existingImportStatements[insertionIndex-1] + ct.InsertNodeAfter(sourceFile, prevImport.AsNode(), newImport.AsNode()) + } + } + } else if len(existingImportStatements) > 0 { + ct.InsertNodesAfter(sourceFile, existingImportStatements[len(existingImportStatements)-1], sortedNewImports) + } else { + ct.InsertAtTopOfFile(sourceFile, sortedNewImports, blankLineBetween) + } +} + +func makeImport(ct *change.Tracker, defaultImport *ast.IdentifierNode, namedImports []*ast.Node, moduleSpecifier *ast.Expression, isTypeOnly bool) *ast.Statement { + var newNamedImports *ast.Node + if len(namedImports) > 0 { + newNamedImports = ct.NodeFactory.NewNamedImports(ct.NodeFactory.NewNodeList(namedImports)) + } + var importClause *ast.Node + if defaultImport != nil || newNamedImports != nil { + importClause = ct.NodeFactory.NewImportClause(core.IfElse(isTypeOnly, ast.KindTypeKeyword, ast.KindUnknown), defaultImport, newNamedImports) + } + return ct.NodeFactory.NewImportDeclaration( /*modifiers*/ nil, importClause, moduleSpecifier, nil /*attributes*/) +} + +func (v *View) GetFixes(ctx context.Context, export *Export, forJSX bool, isValidTypeOnlyUseSite bool, usagePosition *lsproto.Position) []*Fix { + var fixes []*Fix + if namespaceFix := v.tryUseExistingNamespaceImport(ctx, export, usagePosition); namespaceFix != nil { + fixes = append(fixes, namespaceFix) + } + + if fix := v.tryAddToExistingImport(ctx, export, isValidTypeOnlyUseSite); fix != nil { + return append(fixes, fix) + } + + // !!! getNewImportFromExistingSpecifier - even worth it? + + moduleSpecifier, moduleSpecifierKind := v.GetModuleSpecifier(export, v.preferences) + if moduleSpecifier == "" { + if len(fixes) > 0 { + return fixes + } + return nil + } + + // Check if we need a JSDoc import type fix (for JS files with type-only imports) + isJs := tspath.HasJSFileExtension(v.importingFile.FileName()) + importedSymbolHasValueMeaning := export.Flags&ast.SymbolFlagsValue != 0 || export.IsUnresolvedAlias() + if !importedSymbolHasValueMeaning && isJs && usagePosition != nil { + // For pure types in JS files, use JSDoc import type syntax + return []*Fix{ + { + AutoImportFix: &lsproto.AutoImportFix{ + Kind: lsproto.AutoImportFixKindJsdocTypeImport, + ModuleSpecifier: moduleSpecifier, + Name: export.Name(), + UsagePosition: usagePosition, + }, + ModuleSpecifierKind: moduleSpecifierKind, + IsReExport: export.Target.ModuleID != export.ModuleID, + ModuleFileName: export.ModuleFileName, + }, + } + } + + importKind := getImportKind(v.importingFile, export, v.program) + addAsTypeOnly := getAddAsTypeOnly(isValidTypeOnlyUseSite, export, v.program.Options()) + + name := export.Name() + startsWithUpper := unicode.IsUpper(rune(name[0])) + if forJSX && !startsWithUpper { + if export.IsRenameable() { + name = fmt.Sprintf("%c%s", unicode.ToUpper(rune(name[0])), name[1:]) + } else { + return nil + } + } + + return append(fixes, &Fix{ + AutoImportFix: &lsproto.AutoImportFix{ + Kind: lsproto.AutoImportFixKindAddNew, + ImportKind: importKind, + ModuleSpecifier: moduleSpecifier, + Name: name, + UseRequire: v.shouldUseRequire(), + AddAsTypeOnly: addAsTypeOnly, + }, + ModuleSpecifierKind: moduleSpecifierKind, + IsReExport: export.Target.ModuleID != export.ModuleID, + ModuleFileName: export.ModuleFileName, + }) +} + +// getAddAsTypeOnly determines if an import should be type-only based on usage context +func getAddAsTypeOnly(isValidTypeOnlyUseSite bool, export *Export, compilerOptions *core.CompilerOptions) lsproto.AddAsTypeOnly { + if !isValidTypeOnlyUseSite { + // Can't use a type-only import if the usage is an emitting position + return lsproto.AddAsTypeOnlyNotAllowed + } + if compilerOptions.VerbatimModuleSyntax.IsTrue() && (export.IsTypeOnly || export.Flags&ast.SymbolFlagsValue == 0) || + export.IsTypeOnly && export.Flags&ast.SymbolFlagsValue != 0 { + // A type-only import is required for this symbol if under verbatimModuleSyntax and it's purely a type + return lsproto.AddAsTypeOnlyRequired + } + return lsproto.AddAsTypeOnlyAllowed +} + +func (v *View) tryUseExistingNamespaceImport(ctx context.Context, export *Export, usagePosition *lsproto.Position) *Fix { + if usagePosition == nil { + return nil + } + + if getImportKind(v.importingFile, export, v.program) != lsproto.ImportKindNamed { + return nil + } + + existingImports := v.getExistingImports(ctx) + matchingDeclarations := existingImports.Get(export.ModuleID) + for _, existingImport := range matchingDeclarations { + namespacePrefix := getNamespaceLikeImportText(existingImport.node) + if namespacePrefix == "" || existingImport.moduleSpecifier == "" { + continue + } + return &Fix{ + AutoImportFix: &lsproto.AutoImportFix{ + Kind: lsproto.AutoImportFixKindUseNamespace, + Name: export.Name(), + ModuleSpecifier: existingImport.moduleSpecifier, + ImportKind: lsproto.ImportKindNamespace, + AddAsTypeOnly: lsproto.AddAsTypeOnlyAllowed, + ImportIndex: int32(existingImport.index), + UsagePosition: usagePosition, + NamespacePrefix: namespacePrefix, + }, + } + } + + return nil +} + +func getNamespaceLikeImportText(declaration *ast.Node) string { + switch declaration.Kind { + case ast.KindVariableDeclaration: + name := declaration.Name() + if name != nil && name.Kind == ast.KindIdentifier { + return name.Text() + } + return "" + case ast.KindImportEqualsDeclaration: + return declaration.Name().Text() + case ast.KindJSDocImportTag, ast.KindImportDeclaration: + importClause := declaration.ImportClause() + if importClause != nil && importClause.AsImportClause().NamedBindings != nil && importClause.AsImportClause().NamedBindings.Kind == ast.KindNamespaceImport { + return importClause.AsImportClause().NamedBindings.Name().Text() + } + return "" + default: + return "" + } +} + +func (v *View) tryAddToExistingImport( + ctx context.Context, + export *Export, + isValidTypeOnlyUseSite bool, +) *Fix { + existingImports := v.getExistingImports(ctx) + matchingDeclarations := existingImports.Get(export.ModuleID) + if len(matchingDeclarations) == 0 { + return nil + } + + // Can't use an es6 import for a type in JS. + if ast.IsSourceFileJS(v.importingFile) && export.Flags&ast.SymbolFlagsValue == 0 && !core.Every(matchingDeclarations, func(i existingImport) bool { + return ast.IsJSDocImportTag(i.node) + }) { + return nil + } + + importKind := getImportKind(v.importingFile, export, v.program) + if importKind == lsproto.ImportKindCommonJS || importKind == lsproto.ImportKindNamespace { + return nil + } + + addAsTypeOnly := getAddAsTypeOnly(isValidTypeOnlyUseSite, export, v.program.Options()) + + for _, existingImport := range matchingDeclarations { + if existingImport.node.Kind == ast.KindImportEqualsDeclaration { + continue + } + + if existingImport.node.Kind == ast.KindVariableDeclaration { + if (importKind == lsproto.ImportKindNamed || importKind == lsproto.ImportKindDefault) && existingImport.node.Name().Kind == ast.KindObjectBindingPattern { + return &Fix{ + AutoImportFix: &lsproto.AutoImportFix{ + Kind: lsproto.AutoImportFixKindAddToExisting, + Name: export.Name(), + ImportKind: importKind, + ImportIndex: int32(existingImport.index), + ModuleSpecifier: existingImport.moduleSpecifier, + AddAsTypeOnly: addAsTypeOnly, + }, + } + } + continue + } + + importClauseNode := existingImport.node.ImportClause() + if importClauseNode == nil || !ast.IsStringLiteralLike(existingImport.node.ModuleSpecifier()) { + // Side-effect import (no import clause) - can't add to it + continue + } + importClause := importClauseNode.AsImportClause() + + namedBindings := importClause.NamedBindings + // A type-only import may not have both a default and named imports, so the only way a name can + // be added to an existing type-only import is adding a named import to existing named bindings. + if importClause.IsTypeOnly() && !(importKind == lsproto.ImportKindNamed && namedBindings != nil) { + continue + } + + if importKind == lsproto.ImportKindDefault && importClause.Name() != nil { + // Cannot add a default import to a declaration that already has one + continue + } + + // Cannot add a named import to a declaration that has a namespace import + if importKind == lsproto.ImportKindNamed && namedBindings != nil && namedBindings.Kind == ast.KindNamespaceImport { + continue + } + + return &Fix{ + AutoImportFix: &lsproto.AutoImportFix{ + Kind: lsproto.AutoImportFixKindAddToExisting, + Name: export.Name(), + ImportKind: importKind, + ImportIndex: int32(existingImport.index), + ModuleSpecifier: existingImport.moduleSpecifier, + AddAsTypeOnly: addAsTypeOnly, + }, + } + } + + return nil +} + +func getImportKind(importingFile *ast.SourceFile, export *Export, program *compiler.Program) lsproto.ImportKind { + if program.Options().VerbatimModuleSyntax.IsTrue() && program.GetEmitModuleFormatOfFile(importingFile) == core.ModuleKindCommonJS { + return lsproto.ImportKindCommonJS + } + switch export.Syntax { + case ExportSyntaxDefaultModifier, ExportSyntaxDefaultDeclaration: + return lsproto.ImportKindDefault + case ExportSyntaxNamed: + if export.ExportName == ast.InternalSymbolNameDefault { + return lsproto.ImportKindDefault + } + fallthrough + case ExportSyntaxModifier, ExportSyntaxStar, ExportSyntaxCommonJSExportsProperty: + return lsproto.ImportKindNamed + case ExportSyntaxEquals, ExportSyntaxCommonJSModuleExports, ExportSyntaxUMD: + // export.Syntax will be ExportSyntaxEquals for named exports/properties of an export='s target. + if export.ExportName != ast.InternalSymbolNameExportEquals { + return lsproto.ImportKindNamed + } + // !!! cache this? + for _, statement := range importingFile.Statements.Nodes { + // `import foo` parses as an ImportEqualsDeclaration even though it could be an ImportDeclaration + if ast.IsImportEqualsDeclaration(statement) && !ast.NodeIsMissing(statement.AsImportEqualsDeclaration().ModuleReference) { + return lsproto.ImportKindCommonJS + } + } + // !!! this logic feels weird; we're basically trying to predict if shouldUseRequire is going to + // be true. The meaning of "default import" is different depending on whether we write it as + // a require or an es6 import. The latter, compiled to CJS, has interop built in that will + // avoid accessing .default, but if we write a require directly and call it a default import, + // we emit an unconditional .default access. + if importingFile.ExternalModuleIndicator != nil || !ast.IsSourceFileJS(importingFile) { + return lsproto.ImportKindDefault + } + return lsproto.ImportKindCommonJS + default: + panic("unhandled export syntax kind: " + export.Syntax.String()) + } +} + +type existingImport struct { + node *ast.Node + moduleSpecifier string + index int +} + +func (v *View) getExistingImports(ctx context.Context) *collections.MultiMap[ModuleID, existingImport] { + if v.existingImports != nil { + return v.existingImports + } + + result := collections.NewMultiMapWithSizeHint[ModuleID, existingImport](len(v.importingFile.Imports())) + ch, done := v.program.GetTypeChecker(ctx) + defer done() + for i, moduleSpecifier := range v.importingFile.Imports() { + node := ast.TryGetImportFromModuleSpecifier(moduleSpecifier) + if node == nil { + panic("error: did not expect node kind " + moduleSpecifier.Kind.String()) + } else if ast.IsVariableDeclarationInitializedToRequire(node.Parent) { + if moduleSymbol := ch.ResolveExternalModuleName(moduleSpecifier); moduleSymbol != nil { + result.Add(core.FirstResult(getModuleIDAndFileNameOfModuleSymbol(moduleSymbol)), existingImport{node: node.Parent, moduleSpecifier: moduleSpecifier.Text(), index: i}) + } + } else if node.Kind == ast.KindImportDeclaration || node.Kind == ast.KindImportEqualsDeclaration || node.Kind == ast.KindJSDocImportTag { + if moduleSymbol := ch.GetSymbolAtLocation(moduleSpecifier); moduleSymbol != nil { + result.Add(core.FirstResult(getModuleIDAndFileNameOfModuleSymbol(moduleSymbol)), existingImport{node: node, moduleSpecifier: moduleSpecifier.Text(), index: i}) + } + } + } + v.existingImports = result + return result +} + +func (v *View) shouldUseRequire() bool { + if v.shouldUseRequireForFixes != nil { + return *v.shouldUseRequireForFixes + } + shouldUseRequire := v.computeShouldUseRequire() + v.shouldUseRequireForFixes = &shouldUseRequire + return shouldUseRequire +} + +func (v *View) computeShouldUseRequire() bool { + // 1. TypeScript files don't use require variable declarations + if !tspath.HasJSFileExtension(v.importingFile.FileName()) { + return false + } + + // 2. If the current source file is unambiguously CJS or ESM, go with that + switch { + case v.importingFile.CommonJSModuleIndicator != nil && v.importingFile.ExternalModuleIndicator == nil: + return true + case v.importingFile.ExternalModuleIndicator != nil && v.importingFile.CommonJSModuleIndicator == nil: + return false + } + + // 3. If there's a tsconfig/jsconfig, use its module setting + if v.program.Options().ConfigFilePath != "" { + return v.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 v.program.GetImpliedNodeFormatForEmit(v.importingFile) { + 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 v.program.GetSourceFiles() { + switch { + case otherFile == v.importingFile, !ast.IsSourceFileJS(otherFile), v.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 +} + +func needsTypeOnly(addAsTypeOnly lsproto.AddAsTypeOnly) bool { + return addAsTypeOnly == lsproto.AddAsTypeOnlyRequired +} + +func shouldUseTypeOnly(addAsTypeOnly lsproto.AddAsTypeOnly, preferences *lsutil.UserPreferences) bool { + return needsTypeOnly(addAsTypeOnly) || addAsTypeOnly != lsproto.AddAsTypeOnlyNotAllowed && preferences.PreferTypeOnlyAutoImports.IsTrue() +} + +// CompareFixesForSorting returns negative if `a` is better than `b`. +// Sorting with this comparator will place the best fix first. +// After rank sorting, fixes will be sorted by arbitrary but stable criteria +// to ensure a deterministic order. +func (v *View) CompareFixesForSorting(a, b *Fix) int { + if res := v.CompareFixesForRanking(a, b); res != 0 { + return res + } + return v.compareModuleSpecifiersForSorting(a, b) +} + +// CompareFixesForRanking returns negative if `a` is better than `b`. +// Sorting with this comparator will place the best fix first. +// Fixes of equal desirability will be considered equal. +func (v *View) CompareFixesForRanking(a, b *Fix) int { + if res := compareFixKinds(a.Kind, b.Kind); res != 0 { + return res + } + return v.compareModuleSpecifiersForRanking(a, b) +} + +func compareFixKinds(a, b lsproto.AutoImportFixKind) int { + return int(a) - int(b) +} + +func (v *View) compareModuleSpecifiersForRanking(a, b *Fix) int { + if comparison := compareModuleSpecifierRelativity(a, b, v.preferences); comparison != 0 { + return comparison + } + if a.ModuleSpecifierKind == modulespecifiers.ResultKindAmbient && b.ModuleSpecifierKind == modulespecifiers.ResultKindAmbient { + if comparison := v.compareNodeCoreModuleSpecifiers(a.ModuleSpecifier, b.ModuleSpecifier, v.importingFile, v.program); comparison != 0 { + return comparison + } + } + if a.ModuleSpecifierKind == modulespecifiers.ResultKindRelative && b.ModuleSpecifierKind == modulespecifiers.ResultKindRelative { + if comparison := core.CompareBooleans( + isFixPossiblyReExportingImportingFile(a, v.importingFile.FileName()), + isFixPossiblyReExportingImportingFile(b, v.importingFile.FileName()), + ); comparison != 0 { + return comparison + } + } + if comparison := tspath.CompareNumberOfDirectorySeparators(a.ModuleSpecifier, b.ModuleSpecifier); comparison != 0 { + return comparison + } + return 0 +} + +func (v *View) compareModuleSpecifiersForSorting(a, b *Fix) int { + if res := v.compareModuleSpecifiersForRanking(a, b); res != 0 { + return res + } + // Sort ./foo before ../foo for equal-length specifiers + if strings.HasPrefix(a.ModuleSpecifier, "./") && !strings.HasPrefix(b.ModuleSpecifier, "./") { + return -1 + } + if strings.HasPrefix(b.ModuleSpecifier, "./") && !strings.HasPrefix(a.ModuleSpecifier, "./") { + return 1 + } + if comparison := strings.Compare(a.ModuleSpecifier, b.ModuleSpecifier); comparison != 0 { + return comparison + } + if comparison := cmp.Compare(a.ImportKind, b.ImportKind); comparison != 0 { + return comparison + } + // !!! further tie-breakers? In practice this is only called on fixes with the same name + return 0 +} + +func (v *View) compareNodeCoreModuleSpecifiers(a, b string, importingFile *ast.SourceFile, program *compiler.Program) int { + if strings.HasPrefix(a, "node:") && !strings.HasPrefix(b, "node:") { + if v.shouldUseUriStyleNodeCoreModules.IsTrue() { + return -1 + } else if v.shouldUseUriStyleNodeCoreModules.IsFalse() { + return 1 + } + return 0 + } + if strings.HasPrefix(b, "node:") && !strings.HasPrefix(a, "node:") { + if v.shouldUseUriStyleNodeCoreModules.IsTrue() { + return 1 + } else if v.shouldUseUriStyleNodeCoreModules.IsFalse() { + return -1 + } + } + return 0 +} + +// This is a simple heuristic to try to avoid creating an import cycle with a barrel re-export. +// E.g., do not `import { Foo } from ".."` when you could `import { Foo } from "../Foo"`. +// This can produce false positives or negatives if re-exports cross into sibling directories +// (e.g. `export * from "../whatever"`) or are not named "index". Technically this should do +// a tspath.Path comparison, but it's not worth it to run a heuristic in such a hot path. +func isFixPossiblyReExportingImportingFile(fix *Fix, importingFileName string) bool { + if fix.IsReExport && isIndexFileName(fix.ModuleFileName) { + reExportDir := tspath.GetDirectoryPath(fix.ModuleFileName) + return strings.HasPrefix(importingFileName, reExportDir) + } + return false +} + +func isIndexFileName(fileName string) bool { + lastSlash := strings.LastIndexByte(fileName, '/') + if lastSlash < 0 || len(fileName) <= lastSlash+1 { + return false + } + fileName = fileName[lastSlash+1:] + switch fileName { + case "index.js", "index.jsx", "index.d.ts", "index.ts", "index.tsx": + return true + } + return false +} + +func promoteFromTypeOnly( + changes *change.Tracker, + aliasDeclaration *ast.Declaration, + compilerOptions *core.CompilerOptions, + sourceFile *ast.SourceFile, + preferences *lsutil.UserPreferences, +) *ast.Declaration { + // See comment in `doAddExistingFix` on constant with the same name. + convertExistingToTypeOnly := compilerOptions.VerbatimModuleSyntax + + switch aliasDeclaration.Kind { + case ast.KindImportSpecifier: + spec := aliasDeclaration.AsImportSpecifier() + if spec.IsTypeOnly { + if spec.Parent != nil && spec.Parent.Kind == ast.KindNamedImports { + // TypeScript creates a new specifier with isTypeOnly=false, computes insertion index, + // and if different from current position, deletes and re-inserts at new position. + // For now, we just delete the range from the first token (type keyword) to the property name or name. + firstToken := lsutil.GetFirstToken(aliasDeclaration, sourceFile) + typeKeywordPos := scanner.GetTokenPosOfNode(firstToken, sourceFile, false) + var targetNode *ast.DeclarationName + if spec.PropertyName != nil { + targetNode = spec.PropertyName + } else { + targetNode = spec.Name() + } + targetPos := scanner.GetTokenPosOfNode(targetNode.AsNode(), sourceFile, false) + changes.DeleteRange(sourceFile, core.NewTextRange(typeKeywordPos, targetPos)) + } + return aliasDeclaration + } else { + // The parent import clause is type-only + if spec.Parent == nil || spec.Parent.Kind != ast.KindNamedImports { + panic("ImportSpecifier parent must be NamedImports") + } + if spec.Parent.Parent == nil || spec.Parent.Parent.Kind != ast.KindImportClause { + panic("NamedImports parent must be ImportClause") + } + promoteImportClause(changes, spec.Parent.Parent.AsImportClause(), compilerOptions, sourceFile, preferences, convertExistingToTypeOnly, aliasDeclaration) + return spec.Parent.Parent + } + + case ast.KindImportClause: + promoteImportClause(changes, aliasDeclaration.AsImportClause(), compilerOptions, sourceFile, preferences, convertExistingToTypeOnly, aliasDeclaration) + return aliasDeclaration + + case ast.KindNamespaceImport: + // Promote the parent import clause + if aliasDeclaration.Parent == nil || aliasDeclaration.Parent.Kind != ast.KindImportClause { + panic("NamespaceImport parent must be ImportClause") + } + promoteImportClause(changes, aliasDeclaration.Parent.AsImportClause(), compilerOptions, sourceFile, preferences, convertExistingToTypeOnly, aliasDeclaration) + return aliasDeclaration.Parent + + case ast.KindImportEqualsDeclaration: + // Remove the 'type' keyword (which is the second token: 'import' 'type' name '=' ...) + importEqDecl := aliasDeclaration.AsImportEqualsDeclaration() + // The type keyword is after 'import' and before the name + scan := scanner.GetScannerForSourceFile(sourceFile, importEqDecl.Pos()) + // Skip 'import' keyword to get to 'type' + scan.Scan() + deleteTypeKeyword(changes, sourceFile, scan.TokenStart()) + return aliasDeclaration + default: + panic(fmt.Sprintf("Unexpected alias declaration kind: %v", aliasDeclaration.Kind)) + } +} + +// promoteImportClause removes the type keyword from an import clause +func promoteImportClause( + changes *change.Tracker, + importClause *ast.ImportClause, + compilerOptions *core.CompilerOptions, + sourceFile *ast.SourceFile, + preferences *lsutil.UserPreferences, + convertExistingToTypeOnly core.Tristate, + aliasDeclaration *ast.Declaration, +) { + // Delete the 'type' keyword + if importClause.PhaseModifier == ast.KindTypeKeyword { + deleteTypeKeyword(changes, sourceFile, importClause.Pos()) + } + + // Handle .ts extension conversion to .js if necessary + if compilerOptions.AllowImportingTsExtensions.IsFalse() { + moduleSpecifier := checker.TryGetModuleSpecifierFromDeclaration(importClause.Parent) + if moduleSpecifier != nil { + // Note: We can't check ResolvedUsingTsExtension without program, so we'll skip this optimization + // The fix will still work, just might not change .ts to .js extensions in all cases + } + } + + // Handle verbatimModuleSyntax conversion + // If convertExistingToTypeOnly is true, we need to add 'type' to other specifiers + // in the same import declaration + if convertExistingToTypeOnly.IsTrue() { + namedImports := importClause.NamedBindings + if namedImports != nil && namedImports.Kind == ast.KindNamedImports { + namedImportsData := namedImports.AsNamedImports() + if len(namedImportsData.Elements.Nodes) > 1 { + // Check if the list is sorted and if we need to reorder + _, isSorted := organizeimports.GetNamedImportSpecifierComparerWithDetection( + importClause.Parent, + sourceFile, + preferences, + ) + + // If the alias declaration is an ImportSpecifier and the list is sorted, + // move it to index 0 (since it will be the only non-type-only import) + if isSorted.IsFalse() == false && // isSorted !== false + aliasDeclaration != nil && + aliasDeclaration.Kind == ast.KindImportSpecifier { + // Find the index of the alias declaration + aliasIndex := -1 + for i, element := range namedImportsData.Elements.Nodes { + if element == aliasDeclaration { + aliasIndex = i + break + } + } + // If not already at index 0, move it there + if aliasIndex > 0 { + // Delete the specifier from its current position + changes.Delete(sourceFile, aliasDeclaration) + // Insert it at index 0 + changes.InsertImportSpecifierAtIndex(sourceFile, aliasDeclaration, namedImports, 0) + } + } + + // Add 'type' keyword to all other import specifiers that aren't already type-only + for _, element := range namedImportsData.Elements.Nodes { + spec := element.AsImportSpecifier() + // Skip the specifier being promoted (if aliasDeclaration is an ImportSpecifier) + if aliasDeclaration != nil && aliasDeclaration.Kind == ast.KindImportSpecifier { + if element == aliasDeclaration { + continue + } + } + // Skip if already type-only + if !spec.IsTypeOnly { + changes.InsertModifierBefore(sourceFile, ast.KindTypeKeyword, element) + } + } + } + } + } +} + +// deleteTypeKeyword deletes the 'type' keyword token starting at the given position, +// including any trailing whitespace. +func deleteTypeKeyword(changes *change.Tracker, sourceFile *ast.SourceFile, startPos int) { + scan := scanner.GetScannerForSourceFile(sourceFile, startPos) + if scan.Token() != ast.KindTypeKeyword { + return + } + typeStart := scan.TokenStart() + typeEnd := scan.TokenEnd() + // Skip trailing whitespace + text := sourceFile.Text() + for typeEnd < len(text) && (text[typeEnd] == ' ' || text[typeEnd] == '\t') { + typeEnd++ + } + changes.DeleteRange(sourceFile, core.NewTextRange(typeStart, typeEnd)) +} + +func getModuleSpecifierText(promotedDeclaration *ast.Node) string { + if promotedDeclaration.Kind == ast.KindImportEqualsDeclaration { + importEqualsDeclaration := promotedDeclaration.AsImportEqualsDeclaration() + if ast.IsExternalModuleReference(importEqualsDeclaration.ModuleReference) { + expr := importEqualsDeclaration.ModuleReference.Expression() + if expr != nil && expr.Kind == ast.KindStringLiteral { + return expr.Text() + } + + } + return importEqualsDeclaration.ModuleReference.Text() + } + return promotedDeclaration.Parent.ModuleSpecifier().Text() +} + +// returns `-1` if `a` is better than `b` +func compareModuleSpecifierRelativity(a *Fix, b *Fix, preferences modulespecifiers.UserPreferences) int { + switch preferences.ImportModuleSpecifierPreference { + case modulespecifiers.ImportModuleSpecifierPreferenceNonRelative, modulespecifiers.ImportModuleSpecifierPreferenceProjectRelative: + return core.CompareBooleans(a.ModuleSpecifierKind == modulespecifiers.ResultKindRelative, b.ModuleSpecifierKind == modulespecifiers.ResultKindRelative) + } + return 0 +} diff --git a/internal/ls/autoimport/index.go b/internal/ls/autoimport/index.go new file mode 100644 index 0000000000..081d1b7abe --- /dev/null +++ b/internal/ls/autoimport/index.go @@ -0,0 +1,149 @@ +package autoimport + +import ( + "strings" + "unicode" + "unicode/utf8" + + "github.com/microsoft/typescript-go/internal/core" +) + +// Named is a constraint for types that can provide their name. +type Named interface { + Name() string +} + +// Index stores entries with an index mapping uppercase letters to entries whose name +// starts with that letter, and lowercase letters to entries whose name contains a +// word starting with that letter. +type Index[T Named] struct { + entries []T + index map[rune][]int +} + +func (idx *Index[T]) Find(name string, caseSensitive bool) []T { + if len(idx.entries) == 0 || len(name) == 0 { + return nil + } + firstRune := core.FirstResult(utf8.DecodeRuneInString(name)) + if firstRune == utf8.RuneError { + return nil + } + firstRuneUpper := unicode.ToUpper(firstRune) + candidates, ok := idx.index[firstRuneUpper] + if !ok { + return nil + } + + var results []T + for _, entryIndex := range candidates { + entry := idx.entries[entryIndex] + entryName := entry.Name() + if (caseSensitive && entryName == name) || (!caseSensitive && strings.EqualFold(entryName, name)) { + results = append(results, entry) + } + } + + return results +} + +// SearchWordPrefix returns each entry whose name contains a word beginning with +// the first character of 'prefix', and whose name contains all characters +// of 'prefix' in order (case-insensitive). If 'filter' is provided, only entries +// for which filter(entry) returns true are included. +func (idx *Index[T]) SearchWordPrefix(prefix string) []T { + if len(idx.entries) == 0 { + return nil + } + + if len(prefix) == 0 { + return idx.entries + } + + prefix = strings.ToLower(prefix) + firstRune, _ := utf8.DecodeRuneInString(prefix) + if firstRune == utf8.RuneError { + return nil + } + + firstRuneUpper := unicode.ToUpper(firstRune) + firstRuneLower := unicode.ToLower(firstRune) + + // Look up entries that have words starting with this letter + var wordStarts []int + nameStarts, _ := idx.index[firstRuneUpper] + if firstRuneUpper != firstRuneLower { + wordStarts, _ = idx.index[firstRuneLower] + } + count := len(nameStarts) + len(wordStarts) + if count == 0 { + return nil + } + + // Filter entries by checking if they contain all characters in order + results := make([]T, 0, count) + for _, starts := range [][]int{nameStarts, wordStarts} { + for _, i := range starts { + entry := idx.entries[i] + if containsCharsInOrder(entry.Name(), prefix) { + results = append(results, entry) + } + } + } + return results +} + +// containsCharsInOrder checks if str contains all characters from pattern in order (case-insensitive). +func containsCharsInOrder(str, pattern string) bool { + str = strings.ToLower(str) + pattern = strings.ToLower(pattern) + + patternIdx := 0 + for _, ch := range str { + if patternIdx < len(pattern) { + patternRune, size := utf8.DecodeRuneInString(pattern[patternIdx:]) + if ch == patternRune { + patternIdx += size + } + } + } + return patternIdx == len(pattern) +} + +// insertAsWords adds a value to the index keyed by the first letter of each word in its name. +func (idx *Index[T]) insertAsWords(value T) { + if idx.index == nil { + idx.index = make(map[rune][]int) + } + + name := value.Name() + if len(name) == 0 { + panic("Cannot index entry with empty name") + } + entryIndex := len(idx.entries) + idx.entries = append(idx.entries, value) + + indices := wordIndices(name) + seenRunes := make(map[rune]bool) + + for i, start := range indices { + substr := name[start:] + firstRune, _ := utf8.DecodeRuneInString(substr) + if firstRune == utf8.RuneError { + continue + } + if i == 0 { + // Name start keyed by uppercase + firstRune = unicode.ToUpper(firstRune) + idx.index[firstRune] = append(idx.index[firstRune], entryIndex) + seenRunes[firstRune] = true // (Still set seenRunes in case first character is non-alphabetic) + } else { + // Subsequent word starts keyed by lowercase + firstRune = unicode.ToLower(firstRune) + if !seenRunes[firstRune] { + idx.index[firstRune] = append(idx.index[firstRune], entryIndex) + seenRunes[firstRune] = true + } + } + } +} diff --git a/internal/ls/autoimport/registry.go b/internal/ls/autoimport/registry.go new file mode 100644 index 0000000000..b22b6d68f6 --- /dev/null +++ b/internal/ls/autoimport/registry.go @@ -0,0 +1,1059 @@ +package autoimport + +import ( + "cmp" + "context" + "maps" + "slices" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/microsoft/typescript-go/internal/ast" + "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/ls/lsconv" + "github.com/microsoft/typescript-go/internal/ls/lsutil" + "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/packagejson" + "github.com/microsoft/typescript-go/internal/project/dirty" + "github.com/microsoft/typescript-go/internal/project/logging" + "github.com/microsoft/typescript-go/internal/tspath" + "github.com/microsoft/typescript-go/internal/vfs" +) + +type newProgramStructure int + +const ( + newProgramStructureFalse newProgramStructure = iota + newProgramStructureSameFileNames + newProgramStructureDifferentFileNames +) + +// BucketState represents the dirty state of a bucket. +// In general, a bucket can be used for an auto-imports request if it is clean +// or if the only edited file is the one that was requested for auto-imports. +// Most edits within a file will not change the imports available to that file. +// However, two exceptions cause the bucket to be rebuilt after a change to a +// single file: +// +// 1. Local files are newly added to the project by a manual import +// 2. A node_modules dependency normally filtered out by package.json dependencies +// is added to the project by a manual import +// +// Both of these cases take a bit of work to determine, but can only happen after +// a full (non-clone) program update. When this happens, the `newProgramStructure` +// flag is set until the next time the bucket is rebuilt, when those conditions +// will be checked. +type BucketState struct { + // dirtyFile is the file that was edited last, if any. It does not necessarily + // indicate that no other files have been edited, so it should be ignored if + // `multipleFilesDirty` is set. + dirtyFile tspath.Path + multipleFilesDirty bool + newProgramStructure newProgramStructure + // fileExcludePatterns is the value of the corresponding user preference when + // the bucket was built. If changed, the bucket should be rebuilt. + fileExcludePatterns []string +} + +func (b BucketState) Dirty() bool { + return b.multipleFilesDirty || b.dirtyFile != "" || b.newProgramStructure > 0 +} + +func (b BucketState) DirtyFile() tspath.Path { + if b.multipleFilesDirty { + return "" + } + return b.dirtyFile +} + +func (b BucketState) possiblyNeedsRebuildForFile(file tspath.Path, preferences *lsutil.UserPreferences) bool { + return b.newProgramStructure > 0 || b.hasDirtyFileBesides(file) || !core.UnorderedEqual(b.fileExcludePatterns, preferences.AutoImportFileExcludePatterns) +} + +func (b BucketState) hasDirtyFileBesides(file tspath.Path) bool { + return b.multipleFilesDirty || b.dirtyFile != "" && b.dirtyFile != file +} + +type RegistryBucket struct { + state BucketState + + Paths collections.Set[tspath.Path] + // IgnoredPackageNames is only defined for project buckets. It is the set of + // package names that were present in the project's program, and not included + // in a node_modules bucket, and ultimately not included in the project bucket + // because they were only imported transitively. If an updated program's + // ResolvedPackageNames contains one of these, the bucket should be rebuilt + // because that package will be included. + IgnoredPackageNames *collections.Set[string] + // PackageNames is only defined for node_modules buckets. It is the full set of + // package directory names in the node_modules directory (but not necessarily + // inclued in the bucket). + PackageNames *collections.Set[string] + // DependencyNames is only defined for node_modules buckets. It is the set of + // package names that will be included in the bucket if present in the directory, + // computed from package.json dependencies. If nil, all packages are included + // because at least one open file has access to this node_modules directory without + // being filtered by a package.json. + DependencyNames *collections.Set[string] + // AmbientModuleNames is only defined for node_modules buckets. It is the set of + // ambient module names found while extracting exports in the bucket. + AmbientModuleNames map[string][]string + // Entrypoints is only defined for node_modules buckets. Keys are package entrypoint + // file paths, and values describe the ways of importing the package that would resolve + // to that file. + Entrypoints map[tspath.Path][]*module.ResolvedEntrypoint + Index *Index[*Export] +} + +func newRegistryBucket() *RegistryBucket { + return &RegistryBucket{ + state: BucketState{ + multipleFilesDirty: true, + newProgramStructure: newProgramStructureDifferentFileNames, + }, + } +} + +func (b *RegistryBucket) Clone() *RegistryBucket { + return &RegistryBucket{ + state: b.state, + Paths: b.Paths, + IgnoredPackageNames: b.IgnoredPackageNames, + PackageNames: b.PackageNames, + DependencyNames: b.DependencyNames, + AmbientModuleNames: b.AmbientModuleNames, + Entrypoints: b.Entrypoints, + Index: b.Index, + } +} + +// markFileDirty should only be called within a Change call on the dirty map. +// Buckets are considered immutable once in a finalized registry. +func (b *RegistryBucket) markFileDirty(file tspath.Path) { + if b.state.hasDirtyFileBesides(file) { + b.state.multipleFilesDirty = true + } else { + b.state.dirtyFile = file + } +} + +type directory struct { + name string + packageJson *packagejson.InfoCacheEntry + hasNodeModules bool +} + +func (d *directory) Clone() *directory { + return &directory{ + name: d.name, + packageJson: d.packageJson, + hasNodeModules: d.hasNodeModules, + } +} + +type Registry struct { + toPath func(fileName string) tspath.Path + userPreferences *lsutil.UserPreferences + + // exports map[tspath.Path][]*RawExport + directories map[tspath.Path]*directory + + nodeModules map[tspath.Path]*RegistryBucket + projects map[tspath.Path]*RegistryBucket + + // specifierCache maps from importing file to target file to specifier. + specifierCache map[tspath.Path]*collections.SyncMap[tspath.Path, string] +} + +func NewRegistry(toPath func(fileName string) tspath.Path) *Registry { + return &Registry{ + toPath: toPath, + directories: make(map[tspath.Path]*directory), + } +} + +func (r *Registry) IsPreparedForImportingFile(fileName string, projectPath tspath.Path, preferences *lsutil.UserPreferences) bool { + if r == nil { + return false + } + projectBucket, ok := r.projects[projectPath] + if !ok { + panic("project bucket missing") + } + path := r.toPath(fileName) + if projectBucket.state.possiblyNeedsRebuildForFile(path, preferences) { + return false + } + + dirPath := path.GetDirectoryPath() + for { + if dirBucket, ok := r.nodeModules[dirPath]; ok { + if dirBucket.state.possiblyNeedsRebuildForFile(path, preferences) { + return false + } + } + parent := dirPath.GetDirectoryPath() + if parent == dirPath { + break + } + dirPath = parent + } + return true +} + +func (r *Registry) NodeModulesDirectories() map[tspath.Path]string { + dirs := make(map[tspath.Path]string) + for dirPath, dir := range r.directories { + if dir.hasNodeModules { + dirs[tspath.Path(tspath.CombinePaths(string(dirPath), "node_modules"))] = tspath.CombinePaths(dir.name, "node_modules") + } + } + return dirs +} + +func (r *Registry) Clone(ctx context.Context, change RegistryChange, host RegistryCloneHost, logger *logging.LogTree) (*Registry, error) { + start := time.Now() + if logger != nil { + logger = logger.Fork("Building autoimport registry") + } + builder := newRegistryBuilder(r, host) + if change.UserPreferences != nil { + builder.userPreferences = change.UserPreferences + if !core.UnorderedEqual(builder.userPreferences.AutoImportSpecifierExcludeRegexes, r.userPreferences.AutoImportSpecifierExcludeRegexes) { + builder.specifierCache.Clear() + } + } + builder.updateBucketAndDirectoryExistence(change, logger) + builder.markBucketsDirty(change, logger) + if change.RequestedFile != "" { + builder.updateIndexes(ctx, change, logger) + } + if logger != nil { + logger.Logf("Built autoimport registry in %v", time.Since(start)) + } + registry := builder.Build() + builder.host.Dispose() + return registry, nil +} + +type BucketStats struct { + Path tspath.Path + ExportCount int + FileCount int + State BucketState + DependencyNames *collections.Set[string] + PackageNames *collections.Set[string] +} + +type CacheStats struct { + ProjectBuckets []BucketStats + NodeModulesBuckets []BucketStats +} + +func (r *Registry) GetCacheStats() *CacheStats { + stats := &CacheStats{} + + for path, bucket := range r.projects { + exportCount := 0 + if bucket.Index != nil { + exportCount = len(bucket.Index.entries) + } + stats.ProjectBuckets = append(stats.ProjectBuckets, BucketStats{ + Path: path, + ExportCount: exportCount, + FileCount: bucket.Paths.Len(), + State: bucket.state, + DependencyNames: bucket.DependencyNames, + PackageNames: bucket.PackageNames, + }) + } + + for path, bucket := range r.nodeModules { + exportCount := 0 + if bucket.Index != nil { + exportCount = len(bucket.Index.entries) + } + stats.NodeModulesBuckets = append(stats.NodeModulesBuckets, BucketStats{ + Path: path, + ExportCount: exportCount, + FileCount: bucket.Paths.Len(), + State: bucket.state, + DependencyNames: bucket.DependencyNames, + PackageNames: bucket.PackageNames, + }) + } + + slices.SortFunc(stats.ProjectBuckets, func(a, b BucketStats) int { + return cmp.Compare(a.Path, b.Path) + }) + slices.SortFunc(stats.NodeModulesBuckets, func(a, b BucketStats) int { + return cmp.Compare(a.Path, b.Path) + }) + + return stats +} + +type RegistryChange struct { + RequestedFile tspath.Path + OpenFiles map[tspath.Path]string + Changed collections.Set[lsproto.DocumentUri] + Created collections.Set[lsproto.DocumentUri] + Deleted collections.Set[lsproto.DocumentUri] + // RebuiltPrograms maps from project path to: + // - true: the program was rebuilt with a different set of file names + // - false: the program was rebuilt but the set of file names is unchanged + RebuiltPrograms map[tspath.Path]bool + UserPreferences *lsutil.UserPreferences +} + +type RegistryCloneHost interface { + module.ResolutionHost + FS() vfs.FS + GetDefaultProject(path tspath.Path) (tspath.Path, *compiler.Program) + GetProgramForProject(projectPath tspath.Path) *compiler.Program + GetPackageJson(fileName string) *packagejson.InfoCacheEntry + GetSourceFile(fileName string, path tspath.Path) *ast.SourceFile + Dispose() +} + +type registryBuilder struct { + host RegistryCloneHost + resolver *module.Resolver + base *Registry + + userPreferences *lsutil.UserPreferences + directories *dirty.Map[tspath.Path, *directory] + nodeModules *dirty.Map[tspath.Path, *RegistryBucket] + projects *dirty.Map[tspath.Path, *RegistryBucket] + specifierCache *dirty.MapBuilder[tspath.Path, *collections.SyncMap[tspath.Path, string], *collections.SyncMap[tspath.Path, string]] +} + +func newRegistryBuilder(registry *Registry, host RegistryCloneHost) *registryBuilder { + return ®istryBuilder{ + host: host, + resolver: module.NewResolver(host, core.EmptyCompilerOptions, "", ""), + base: registry, + + userPreferences: registry.userPreferences.OrDefault(), + directories: dirty.NewMap(registry.directories), + nodeModules: dirty.NewMap(registry.nodeModules), + projects: dirty.NewMap(registry.projects), + specifierCache: dirty.NewMapBuilder(registry.specifierCache, core.Identity, core.Identity), + } +} + +func (b *registryBuilder) Build() *Registry { + return &Registry{ + toPath: b.base.toPath, + userPreferences: b.userPreferences, + directories: core.FirstResult(b.directories.Finalize()), + nodeModules: core.FirstResult(b.nodeModules.Finalize()), + projects: core.FirstResult(b.projects.Finalize()), + specifierCache: core.FirstResult(b.specifierCache.Build()), + } +} + +func (b *registryBuilder) updateBucketAndDirectoryExistence(change RegistryChange, logger *logging.LogTree) { + start := time.Now() + neededProjects := make(map[tspath.Path]struct{}) + neededDirectories := make(map[tspath.Path]string) + for path, fileName := range change.OpenFiles { + neededProjects[core.FirstResult(b.host.GetDefaultProject(path))] = struct{}{} + if strings.HasPrefix(fileName, "^/") { + continue + } + dir := fileName + dirPath := path + for { + dir = tspath.GetDirectoryPath(dir) + lastDirPath := dirPath + dirPath = dirPath.GetDirectoryPath() + if dirPath == lastDirPath { + break + } + if _, ok := neededDirectories[dirPath]; ok { + break + } + neededDirectories[dirPath] = dir + } + + if !b.specifierCache.Has(path) { + b.specifierCache.Set(path, &collections.SyncMap[tspath.Path, string]{}) + } + } + + for path := range b.base.specifierCache { + if _, ok := change.OpenFiles[path]; !ok { + b.specifierCache.Delete(path) + } + } + + var addedProjects, removedProjects []tspath.Path + core.DiffMapsFunc( + b.base.projects, + neededProjects, + func(_ *RegistryBucket, _ struct{}) bool { + panic("never called because onChanged is nil") + }, + func(projectPath tspath.Path, _ struct{}) { + // Need and don't have + b.projects.Add(projectPath, newRegistryBucket()) + addedProjects = append(addedProjects, projectPath) + }, + func(projectPath tspath.Path, _ *RegistryBucket) { + // Have and don't need + b.projects.Delete(projectPath) + removedProjects = append(removedProjects, projectPath) + }, + nil, + ) + if logger != nil { + for _, projectPath := range addedProjects { + logger.Logf("Added project: %s", projectPath) + } + for _, projectPath := range removedProjects { + logger.Logf("Removed project: %s", projectPath) + } + } + + updateDirectory := func(dirPath tspath.Path, dirName string, packageJsonChanged bool) { + packageJsonFileName := tspath.CombinePaths(dirName, "package.json") + hasNodeModules := b.host.FS().DirectoryExists(tspath.CombinePaths(dirName, "node_modules")) + if entry, ok := b.directories.Get(dirPath); ok { + entry.ChangeIf(func(dir *directory) bool { + return packageJsonChanged || dir.hasNodeModules != hasNodeModules + }, func(dir *directory) { + dir.packageJson = b.host.GetPackageJson(packageJsonFileName) + dir.hasNodeModules = hasNodeModules + }) + } else { + b.directories.Add(dirPath, &directory{ + name: dirName, + packageJson: b.host.GetPackageJson(packageJsonFileName), + hasNodeModules: hasNodeModules, + }) + } + + if packageJsonChanged { + // package.json changes affecting node_modules are handled by comparing dependencies in updateIndexes + return + } + + if hasNodeModules { + if _, ok := b.nodeModules.Get(dirPath); !ok { + b.nodeModules.Add(dirPath, newRegistryBucket()) + } + } else { + b.nodeModules.TryDelete(dirPath) + } + } + + var addedNodeModulesDirs, removedNodeModulesDirs []tspath.Path + core.DiffMapsFunc( + b.base.directories, + neededDirectories, + func(dir *directory, dirName string) bool { + packageJsonUri := lsconv.FileNameToDocumentURI(tspath.CombinePaths(dirName, "package.json")) + return !change.Changed.Has(packageJsonUri) && !change.Deleted.Has(packageJsonUri) && !change.Created.Has(packageJsonUri) + }, + func(dirPath tspath.Path, dirName string) { + // Need and don't have + hadNodeModules := b.base.nodeModules[dirPath] != nil + updateDirectory(dirPath, dirName, false) + if logger != nil { + logger.Logf("Added directory: %s", dirPath) + } + if _, hasNow := b.nodeModules.Get(dirPath); hasNow && !hadNodeModules { + addedNodeModulesDirs = append(addedNodeModulesDirs, dirPath) + } + }, + func(dirPath tspath.Path, dir *directory) { + // Have and don't need + hadNodeModules := b.base.nodeModules[dirPath] != nil + b.directories.Delete(dirPath) + b.nodeModules.TryDelete(dirPath) + if logger != nil { + logger.Logf("Removed directory: %s", dirPath) + } + if hadNodeModules { + removedNodeModulesDirs = append(removedNodeModulesDirs, dirPath) + } + }, + func(dirPath tspath.Path, dir *directory, dirName string) { + // package.json may have changed + updateDirectory(dirPath, dirName, true) + if logger != nil { + logger.Logf("Changed directory: %s", dirPath) + } + }, + ) + if logger != nil { + for _, dirPath := range addedNodeModulesDirs { + logger.Logf("Added node_modules bucket: %s", dirPath) + } + for _, dirPath := range removedNodeModulesDirs { + logger.Logf("Removed node_modules bucket: %s", dirPath) + } + logger.Logf("Updated buckets and directories in %v", time.Since(start)) + } +} + +func (b *registryBuilder) markBucketsDirty(change RegistryChange, logger *logging.LogTree) { + // Mark new program structures + for projectPath, newFileNames := range change.RebuiltPrograms { + if bucket, ok := b.projects.Get(projectPath); ok { + bucket.Change(func(bucket *RegistryBucket) { + bucket.state.newProgramStructure = core.IfElse(newFileNames, newProgramStructureDifferentFileNames, newProgramStructureSameFileNames) + }) + } + } + + // Mark files dirty, bailing out if all buckets already have multiple files dirty + cleanNodeModulesBuckets := make(map[tspath.Path]struct{}) + cleanProjectBuckets := make(map[tspath.Path]struct{}) + b.nodeModules.Range(func(entry *dirty.MapEntry[tspath.Path, *RegistryBucket]) bool { + if !entry.Value().state.multipleFilesDirty { + cleanNodeModulesBuckets[entry.Key()] = struct{}{} + } + return true + }) + b.projects.Range(func(entry *dirty.MapEntry[tspath.Path, *RegistryBucket]) bool { + if !entry.Value().state.multipleFilesDirty { + cleanProjectBuckets[entry.Key()] = struct{}{} + } + return true + }) + + markFilesDirty := func(uris map[lsproto.DocumentUri]struct{}) { + if len(cleanNodeModulesBuckets) == 0 && len(cleanProjectBuckets) == 0 { + return + } + for uri := range uris { + path := b.base.toPath(uri.FileName()) + if len(cleanNodeModulesBuckets) > 0 { + // For node_modules, mark the bucket dirty if anything changes in the directory + if nodeModulesIndex := strings.Index(string(path), "/node_modules/"); nodeModulesIndex != -1 { + dirPath := path[:nodeModulesIndex] + if _, ok := cleanNodeModulesBuckets[dirPath]; ok { + entry := core.FirstResult(b.nodeModules.Get(dirPath)) + entry.Change(func(bucket *RegistryBucket) { bucket.markFileDirty(path) }) + if !entry.Value().state.multipleFilesDirty { + delete(cleanNodeModulesBuckets, dirPath) + } + } + } + } + // For projects, mark the bucket dirty if the bucket contains the file directly. + // Any other significant change, like a created failed lookup location, is + // handled by newProgramStructure. + for projectDirPath := range cleanProjectBuckets { + entry, _ := b.projects.Get(projectDirPath) + if entry.Value().Paths.Has(path) { + entry.Change(func(bucket *RegistryBucket) { bucket.markFileDirty(path) }) + if !entry.Value().state.multipleFilesDirty { + delete(cleanProjectBuckets, projectDirPath) + } + } + } + } + } + + markFilesDirty(change.Created.Keys()) + markFilesDirty(change.Deleted.Keys()) + markFilesDirty(change.Changed.Keys()) +} + +func (b *registryBuilder) updateIndexes(ctx context.Context, change RegistryChange, logger *logging.LogTree) { + type task struct { + entry *dirty.MapEntry[tspath.Path, *RegistryBucket] + dependencyNames *collections.Set[string] + result *bucketBuildResult + err error + } + + projectPath, _ := b.host.GetDefaultProject(change.RequestedFile) + if projectPath == "" { + return + } + + var tasks []*task + var wg sync.WaitGroup + + tspath.ForEachAncestorDirectoryPath(change.RequestedFile, func(dirPath tspath.Path) (any, bool) { + if nodeModulesBucket, ok := b.nodeModules.Get(dirPath); ok { + dirName := core.FirstResult(b.directories.Get(dirPath)).Value().name + dependencies := b.computeDependenciesForNodeModulesDirectory(change, dirName, dirPath) + if nodeModulesBucket.Value().state.hasDirtyFileBesides(change.RequestedFile) || !nodeModulesBucket.Value().DependencyNames.Equals(dependencies) { + task := &task{entry: nodeModulesBucket, dependencyNames: dependencies} + tasks = append(tasks, task) + wg.Go(func() { + result, err := b.buildNodeModulesBucket(ctx, dependencies, dirName, dirPath, logger.Fork("Building node_modules bucket "+dirName)) + task.result = result + task.err = err + }) + } + } + return nil, false + }) + + nodeModulesContainsDependency := func(nodeModulesDir tspath.Path, packageName string) bool { + for _, task := range tasks { + if task.entry.Key() == nodeModulesDir { + return task.dependencyNames == nil || task.dependencyNames.Has(packageName) + } + } + if bucket, ok := b.base.nodeModules[nodeModulesDir]; ok { + return bucket.DependencyNames == nil || bucket.DependencyNames.Has(packageName) + } + return false + } + + if project, ok := b.projects.Get(projectPath); ok { + program := b.host.GetProgramForProject(projectPath) + resolvedPackageNames := core.Memoize(func() *collections.Set[string] { + return getResolvedPackageNames(ctx, program) + }) + shouldRebuild := project.Value().state.hasDirtyFileBesides(change.RequestedFile) + if !shouldRebuild && project.Value().state.newProgramStructure > 0 { + // Exceptions from BucketState comment - check if new program's resolved package names include any + // previously ignored, or if there are new non-node_modules files. + // If not, we can skip rebuilding the project bucket. + if project.Value().IgnoredPackageNames.Intersects(resolvedPackageNames()) || hasNewNonNodeModulesFiles(program, project.Value()) { + shouldRebuild = true + } else { + project.Change(func(b *RegistryBucket) { b.state.newProgramStructure = newProgramStructureFalse }) + } + } + if shouldRebuild { + task := &task{entry: project} + tasks = append(tasks, task) + wg.Go(func() { + index, err := b.buildProjectBucket( + ctx, + projectPath, + resolvedPackageNames(), + nodeModulesContainsDependency, + logger.Fork("Building project bucket "+string(projectPath)), + ) + task.result = index + task.err = err + }) + } + } + + start := time.Now() + wg.Wait() + + for _, t := range tasks { + if t.err != nil { + continue + } + t.entry.Replace(t.result.bucket) + } + + // If we failed to resolve any alias exports by ending up at a non-relative module specifier + // that didn't resolve to another package, it's probably an ambient module declared in another package. + // We recorded these failures, along with the name of every ambient module declared elsewhere, so we + // can do a second pass on the failed files, this time including the ambient modules declarations that + // were missing the first time. Example: node_modules/fs-extra/index.d.ts is simply `export * from "fs"`, + // but when trying to resolve the `export *`, we don't know where "fs" is declared. The aliasResolver + // tries to find packages named "fs" on the file system, but after failing, records "fs" as a failure + // for fs-extra/index.d.ts. Meanwhile, if we also processed node_modules/@types/node/fs.d.ts, we + // recorded that file as declaring the ambient module "fs". In the second pass, we combine those two + // files and reprocess fs-extra/index.d.ts, this time finding "fs" declared in @types/node. + secondPassStart := time.Now() + var secondPassFileCount int + for _, t := range tasks { + if t.err != nil { + continue + } + if t.result.possibleFailedAmbientModuleLookupTargets == nil { + continue + } + rootFiles := make(map[string]*ast.SourceFile) + for target := range t.result.possibleFailedAmbientModuleLookupTargets.Keys() { + for _, fileName := range b.resolveAmbientModuleName(target, t.entry.Key()) { + if _, exists := rootFiles[fileName]; exists { + continue + } + rootFiles[fileName] = b.host.GetSourceFile(fileName, b.base.toPath(fileName)) + secondPassFileCount++ + } + } + if len(rootFiles) > 0 { + aliasResolver := newAliasResolver(slices.Collect(maps.Values(rootFiles)), b.host, b.resolver, b.base.toPath, func(source ast.HasFileName, moduleName string) { + // no-op + }) + ch, _ := checker.NewChecker(aliasResolver) + t.result.possibleFailedAmbientModuleLookupSources.Range(func(path tspath.Path, source *failedAmbientModuleLookupSource) bool { + sourceFile := aliasResolver.GetSourceFile(source.fileName) + extractor := b.newExportExtractor(t.entry.Key(), source.packageName, ch) + fileExports := extractor.extractFromFile(sourceFile) + t.result.bucket.Paths.Add(path) + for _, exp := range fileExports { + t.result.bucket.Index.insertAsWords(exp) + } + return true + }) + } + } + + if logger != nil && len(tasks) > 0 { + if secondPassFileCount > 0 { + logger.Logf("%d files required second pass, took %v", secondPassFileCount, time.Since(secondPassStart)) + } + logger.Logf("Built %d indexes in %v", len(tasks), time.Since(start)) + } +} + +func hasNewNonNodeModulesFiles(program *compiler.Program, bucket *RegistryBucket) bool { + if bucket.state.newProgramStructure != newProgramStructureDifferentFileNames { + return false + } + for _, file := range program.GetSourceFiles() { + if strings.Contains(file.FileName(), "/node_modules/") || isIgnoredFile(program, file) { + continue + } + if !bucket.Paths.Has(file.Path()) { + return true + } + } + return false +} + +func isIgnoredFile(program *compiler.Program, file *ast.SourceFile) bool { + return program.IsSourceFileDefaultLibrary(file.Path()) || program.IsGlobalTypingsFile(file.FileName()) +} + +type failedAmbientModuleLookupSource struct { + mu sync.Mutex + fileName string + packageName string +} + +type bucketBuildResult struct { + bucket *RegistryBucket + // File path to filename and package name + possibleFailedAmbientModuleLookupSources *collections.SyncMap[tspath.Path, *failedAmbientModuleLookupSource] + // Likely ambient module name + possibleFailedAmbientModuleLookupTargets *collections.SyncSet[string] +} + +func (b *registryBuilder) buildProjectBucket( + ctx context.Context, + projectPath tspath.Path, + resolvedPackageNames *collections.Set[string], + nodeModulesContainsDependency func(nodeModulesDir tspath.Path, packageName string) bool, + logger *logging.LogTree, +) (*bucketBuildResult, error) { + if ctx.Err() != nil { + return nil, ctx.Err() + } + + start := time.Now() + var mu sync.Mutex + fileExcludePatterns := b.userPreferences.ParsedAutoImportFileExcludePatterns(b.host.FS().UseCaseSensitiveFileNames()) + result := &bucketBuildResult{bucket: &RegistryBucket{}} + program := b.host.GetProgramForProject(projectPath) + getChecker, closePool, checkerCount := createCheckerPool(program) + defer closePool() + exports := make(map[tspath.Path][]*Export) + var wg sync.WaitGroup + var ignoredPackageNames collections.Set[string] + var skippedFileCount int + var combinedStats extractorStats + +outer: + for _, file := range program.GetSourceFiles() { + if isIgnoredFile(program, file) { + continue + } + for _, excludePattern := range fileExcludePatterns { + if matched, _ := excludePattern.MatchString(file.FileName()); matched { + skippedFileCount++ + continue outer + } + } + if packageName := modulespecifiers.GetPackageNameFromDirectory(file.FileName()); packageName != "" { + // Only process this file if it is not going to be processed as part of a node_modules bucket + // *and* if it was imported directly (not transitively) by a project file (i.e., this is part + // of a package not listed in package.json, but imported anyway). + pathComponents := tspath.GetPathComponents(string(file.Path()), "") + nodeModulesDir := tspath.GetPathFromPathComponents(pathComponents[:slices.Index(pathComponents, "node_modules")]) + if nodeModulesContainsDependency(tspath.Path(nodeModulesDir), packageName) { + continue + } + if !resolvedPackageNames.Has(packageName) { + ignoredPackageNames.Add(packageName) + continue + } + } + wg.Go(func() { + if ctx.Err() == nil { + checker, done := getChecker() + defer done() + extractor := b.newExportExtractor("", "", checker) + fileExports := extractor.extractFromFile(file) + mu.Lock() + exports[file.Path()] = fileExports + mu.Unlock() + stats := extractor.Stats() + combinedStats.exports.Add(stats.exports.Load()) + combinedStats.usedChecker.Add(stats.usedChecker.Load()) + } + }) + } + + wg.Wait() + + indexStart := time.Now() + idx := &Index[*Export]{} + for path, fileExports := range exports { + result.bucket.Paths.Add(path) + for _, exp := range fileExports { + idx.insertAsWords(exp) + } + } + + result.bucket.Index = idx + result.bucket.IgnoredPackageNames = &ignoredPackageNames + result.bucket.state.fileExcludePatterns = b.userPreferences.AutoImportFileExcludePatterns + + if logger != nil { + logger.Logf("Extracted exports: %v (%d exports, %d used checker, %d created checkers)", indexStart.Sub(start), combinedStats.exports.Load(), combinedStats.usedChecker.Load(), checkerCount()) + if skippedFileCount > 0 { + logger.Logf("Skipped %d files due to exclude patterns", skippedFileCount) + } + logger.Logf("Built index: %v", time.Since(indexStart)) + logger.Logf("Bucket total: %v", time.Since(start)) + } + return result, nil +} + +func (b *registryBuilder) computeDependenciesForNodeModulesDirectory(change RegistryChange, dirName string, dirPath tspath.Path) *collections.Set[string] { + // If any open files are in scope of this directory but not in scope of any package.json, + // we need to add all packages in this node_modules directory. + for path := range change.OpenFiles { + if dirPath.ContainsPath(path) && b.getNearestAncestorDirectoryWithValidPackageJson(path) == nil { + return nil + } + } + + // Get all package.jsons that have this node_modules directory in their spine + dependencies := &collections.Set[string]{} + b.directories.Range(func(entry *dirty.MapEntry[tspath.Path, *directory]) bool { + if entry.Value().packageJson.Exists() && dirPath.ContainsPath(entry.Key()) { + entry.Value().packageJson.Contents.RangeDependencies(func(name, _, field string) bool { + if field == "dependencies" || field == "peerDendencies" { + dependencies.Add(module.GetPackageNameFromTypesPackageName(name)) + } + return true + }) + } + return true + }) + return dependencies +} + +func (b *registryBuilder) buildNodeModulesBucket( + ctx context.Context, + dependencies *collections.Set[string], + dirName string, + dirPath tspath.Path, + logger *logging.LogTree, +) (*bucketBuildResult, error) { + if ctx.Err() != nil { + return nil, ctx.Err() + } + + start := time.Now() + fileExcludePatterns := b.userPreferences.ParsedAutoImportFileExcludePatterns(b.host.FS().UseCaseSensitiveFileNames()) + directoryPackageNames, err := getPackageNamesInNodeModules(tspath.CombinePaths(dirName, "node_modules"), b.host.FS()) + if err != nil { + return nil, err + } + + extractorStart := time.Now() + packageNames := core.Coalesce(dependencies, directoryPackageNames) + + var exportsMu sync.Mutex + exports := make(map[tspath.Path][]*Export) + ambientModuleNames := make(map[string][]string) + + var entrypointsMu sync.Mutex + var entrypoints []*module.ResolvedEntrypoints + var skippedEntrypointsCount int32 + var combinedStats extractorStats + var possibleFailedAmbientModuleLookupTargets collections.SyncSet[string] + var possibleFailedAmbientModuleLookupSources collections.SyncMap[tspath.Path, *failedAmbientModuleLookupSource] + + createAliasResolver := func(packageName string, entrypoints []*module.ResolvedEntrypoint) *aliasResolver { + rootFiles := make([]*ast.SourceFile, len(entrypoints)) + var wg sync.WaitGroup + for i, entrypoint := range entrypoints { + wg.Go(func() { + file := b.host.GetSourceFile(entrypoint.ResolvedFileName, b.base.toPath(entrypoint.ResolvedFileName)) + binder.BindSourceFile(file) + rootFiles[i] = file + }) + } + wg.Wait() + + rootFiles = slices.DeleteFunc(rootFiles, func(f *ast.SourceFile) bool { + return f == nil + }) + + return newAliasResolver(rootFiles, b.host, b.resolver, b.base.toPath, func(source ast.HasFileName, moduleName string) { + possibleFailedAmbientModuleLookupTargets.Add(moduleName) + possibleFailedAmbientModuleLookupSources.LoadOrStore(source.Path(), &failedAmbientModuleLookupSource{ + fileName: source.FileName(), + }) + }) + } + + indexStart := time.Now() + var wg sync.WaitGroup + for packageName := range packageNames.Keys() { + wg.Go(func() { + if ctx.Err() != nil { + return + } + + typesPackageName := module.GetTypesPackageName(packageName) + var packageJson *packagejson.InfoCacheEntry + packageJson = b.host.GetPackageJson(tspath.CombinePaths(dirName, "node_modules", packageName, "package.json")) + if !packageJson.DirectoryExists { + packageJson = b.host.GetPackageJson(tspath.CombinePaths(dirName, "node_modules", typesPackageName, "package.json")) + } + packageEntrypoints := b.resolver.GetEntrypointsFromPackageJsonInfo(packageJson, packageName) + if packageEntrypoints == nil { + return + } + if len(fileExcludePatterns) > 0 { + count := int32(len(packageEntrypoints.Entrypoints)) + packageEntrypoints.Entrypoints = slices.DeleteFunc(packageEntrypoints.Entrypoints, func(entrypoint *module.ResolvedEntrypoint) bool { + for _, excludePattern := range fileExcludePatterns { + if matched, _ := excludePattern.MatchString(entrypoint.ResolvedFileName); matched { + return true + } + } + return false + }) + atomic.AddInt32(&skippedEntrypointsCount, count-int32(len(packageEntrypoints.Entrypoints))) + } + if len(packageEntrypoints.Entrypoints) == 0 { + return + } + + entrypointsMu.Lock() + entrypoints = append(entrypoints, packageEntrypoints) + entrypointsMu.Unlock() + + aliasResolver := createAliasResolver(packageName, packageEntrypoints.Entrypoints) + checker, _ := checker.NewChecker(aliasResolver) + extractor := b.newExportExtractor(dirPath, packageName, checker) + seenFiles := collections.NewSetWithSizeHint[tspath.Path](len(packageEntrypoints.Entrypoints)) + for _, entrypoint := range aliasResolver.rootFiles { + if !seenFiles.AddIfAbsent(entrypoint.Path()) { + continue + } + + if ctx.Err() != nil { + return + } + + fileExports := extractor.extractFromFile(entrypoint) + exportsMu.Lock() + for _, name := range entrypoint.AmbientModuleNames { + ambientModuleNames[name] = append(ambientModuleNames[name], entrypoint.FileName()) + } + if source, ok := possibleFailedAmbientModuleLookupSources.Load(entrypoint.Path()); !ok { + // If we failed to resolve any ambient modules from this file, we'll try the + // whole file again later, so don't add anything now. + exports[entrypoint.Path()] = fileExports + } else { + // Record the package name so we can use it later during the second pass + source.mu.Lock() + source.packageName = packageName + source.mu.Unlock() + } + exportsMu.Unlock() + } + if logger != nil { + stats := extractor.Stats() + combinedStats.exports.Add(stats.exports.Load()) + combinedStats.usedChecker.Add(stats.usedChecker.Load()) + } + }) + } + + wg.Wait() + + result := &bucketBuildResult{ + bucket: &RegistryBucket{ + Index: &Index[*Export]{}, + DependencyNames: dependencies, + PackageNames: directoryPackageNames, + AmbientModuleNames: ambientModuleNames, + Paths: *collections.NewSetWithSizeHint[tspath.Path](len(exports)), + Entrypoints: make(map[tspath.Path][]*module.ResolvedEntrypoint, len(exports)), + state: BucketState{ + fileExcludePatterns: b.userPreferences.AutoImportFileExcludePatterns, + }, + }, + possibleFailedAmbientModuleLookupSources: &possibleFailedAmbientModuleLookupSources, + possibleFailedAmbientModuleLookupTargets: &possibleFailedAmbientModuleLookupTargets, + } + for path, fileExports := range exports { + result.bucket.Paths.Add(path) + for _, exp := range fileExports { + result.bucket.Index.insertAsWords(exp) + } + } + for _, entrypointSet := range entrypoints { + for _, entrypoint := range entrypointSet.Entrypoints { + path := b.base.toPath(entrypoint.ResolvedFileName) + result.bucket.Entrypoints[path] = append(result.bucket.Entrypoints[path], entrypoint) + } + } + + if logger != nil { + logger.Logf("Determined dependencies and package names: %v", extractorStart.Sub(start)) + logger.Logf("Extracted exports: %v (%d exports, %d used checker)", indexStart.Sub(extractorStart), combinedStats.exports.Load(), combinedStats.usedChecker.Load()) + if skippedEntrypointsCount > 0 { + logger.Logf("Skipped %d entrypoints due to exclude patterns", skippedEntrypointsCount) + } + logger.Logf("Built index: %v", time.Since(indexStart)) + logger.Logf("Bucket total: %v", time.Since(start)) + } + + return result, ctx.Err() +} + +func (b *registryBuilder) getNearestAncestorDirectoryWithValidPackageJson(filePath tspath.Path) *directory { + return core.FirstResult(tspath.ForEachAncestorDirectoryPath(filePath.GetDirectoryPath(), func(dirPath tspath.Path) (result *directory, stop bool) { + if dirEntry, ok := b.directories.Get(dirPath); ok && dirEntry.Value().packageJson.Exists() && dirEntry.Value().packageJson.Contents.Parseable { + return dirEntry.Value(), true + } + return nil, false + })) +} + +func (b *registryBuilder) resolveAmbientModuleName(moduleName string, fromPath tspath.Path) []string { + return core.FirstResult(tspath.ForEachAncestorDirectoryPath(fromPath, func(dirPath tspath.Path) (result []string, stop bool) { + if bucket, ok := b.nodeModules.Get(dirPath); ok { + if fileNames, ok := bucket.Value().AmbientModuleNames[moduleName]; ok { + return fileNames, true + } + } + return nil, false + })) +} diff --git a/internal/ls/autoimport/registry_test.go b/internal/ls/autoimport/registry_test.go new file mode 100644 index 0000000000..840f3e4dda --- /dev/null +++ b/internal/ls/autoimport/registry_test.go @@ -0,0 +1,312 @@ +package autoimport_test + +import ( + "context" + "fmt" + "testing" + + "github.com/microsoft/typescript-go/internal/collections" + "github.com/microsoft/typescript-go/internal/ls/autoimport" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/project" + "github.com/microsoft/typescript-go/internal/testutil/autoimporttestutil" + "github.com/microsoft/typescript-go/internal/testutil/projecttestutil" + "github.com/microsoft/typescript-go/internal/tspath" + "gotest.tools/v3/assert" +) + +func TestRegistryLifecycle(t *testing.T) { + t.Parallel() + t.Run("builds project and node_modules buckets", func(t *testing.T) { + t.Parallel() + fixture := autoimporttestutil.SetupLifecycleSession(t, lifecycleProjectRoot, 1) + session := fixture.Session() + project := fixture.SingleProject() + mainFile := project.File(0) + session.DidOpenFile(context.Background(), mainFile.URI(), 1, mainFile.Content(), lsproto.LanguageKindTypeScript) + + stats := autoImportStats(t, session) + projectBucket := singleBucket(t, stats.ProjectBuckets) + nodeModulesBucket := singleBucket(t, stats.NodeModulesBuckets) + assert.Equal(t, true, projectBucket.State.Dirty()) + assert.Equal(t, 0, projectBucket.FileCount) + assert.Equal(t, true, nodeModulesBucket.State.Dirty()) + assert.Equal(t, 0, nodeModulesBucket.FileCount) + + _, err := session.GetLanguageServiceWithAutoImports(context.Background(), mainFile.URI()) + assert.NilError(t, err) + + stats = autoImportStats(t, session) + projectBucket = singleBucket(t, stats.ProjectBuckets) + nodeModulesBucket = singleBucket(t, stats.NodeModulesBuckets) + assert.Equal(t, false, projectBucket.State.Dirty()) + assert.Assert(t, projectBucket.ExportCount > 0) + assert.Equal(t, false, nodeModulesBucket.State.Dirty()) + assert.Assert(t, nodeModulesBucket.ExportCount > 0) + }) + + t.Run("bucket does not rebuild on same-file change", func(t *testing.T) { + t.Parallel() + fixture := autoimporttestutil.SetupLifecycleSession(t, lifecycleProjectRoot, 2) + session := fixture.Session() + utils := fixture.Utils() + project := fixture.SingleProject() + mainFile := project.File(0) + secondaryFile := project.File(1) + session.DidOpenFile(context.Background(), mainFile.URI(), 1, mainFile.Content(), lsproto.LanguageKindTypeScript) + session.DidOpenFile(context.Background(), secondaryFile.URI(), 1, secondaryFile.Content(), lsproto.LanguageKindTypeScript) + _, err := session.GetLanguageServiceWithAutoImports(context.Background(), mainFile.URI()) + assert.NilError(t, err) + + updatedContent := mainFile.Content() + "// change\n" + session.DidChangeFile(context.Background(), mainFile.URI(), 2, []lsproto.TextDocumentContentChangePartialOrWholeDocument{ + {WholeDocument: &lsproto.TextDocumentContentChangeWholeDocument{Text: updatedContent}}, + }) + + _, err = session.GetLanguageService(context.Background(), mainFile.URI()) + assert.NilError(t, err) + + stats := autoImportStats(t, session) + projectBucket := singleBucket(t, stats.ProjectBuckets) + nodeModulesBucket := singleBucket(t, stats.NodeModulesBuckets) + assert.Equal(t, projectBucket.State.Dirty(), true) + assert.Equal(t, projectBucket.State.DirtyFile(), utils.ToPath(mainFile.FileName())) + assert.Equal(t, nodeModulesBucket.State.Dirty(), false) + assert.Equal(t, nodeModulesBucket.State.DirtyFile(), tspath.Path("")) + + // Bucket should not recompute when requesting same file changed + _, err = session.GetLanguageServiceWithAutoImports(context.Background(), mainFile.URI()) + assert.NilError(t, err) + stats = autoImportStats(t, session) + projectBucket = singleBucket(t, stats.ProjectBuckets) + assert.Equal(t, projectBucket.State.Dirty(), true) + assert.Equal(t, projectBucket.State.DirtyFile(), utils.ToPath(mainFile.FileName())) + + // Bucket should recompute when other file has changed + session.DidChangeFile(context.Background(), secondaryFile.URI(), 1, []lsproto.TextDocumentContentChangePartialOrWholeDocument{ + {WholeDocument: &lsproto.TextDocumentContentChangeWholeDocument{Text: "// new content"}}, + }) + _, err = session.GetLanguageServiceWithAutoImports(context.Background(), mainFile.URI()) + assert.NilError(t, err) + stats = autoImportStats(t, session) + projectBucket = singleBucket(t, stats.ProjectBuckets) + assert.Equal(t, projectBucket.State.Dirty(), false) + }) + + t.Run("bucket updates on same-file change when new files added to the program", func(t *testing.T) { + t.Parallel() + projectRoot := "/home/src/explicit-files-project" + files := map[string]any{ + projectRoot + "/tsconfig.json": `{ + "compilerOptions": { + "module": "esnext", + "target": "esnext", + "strict": true + }, + "files": ["index.ts"] + }`, + projectRoot + "/index.ts": "", + projectRoot + "/utils.ts": `export const foo = 1; +export const bar = 2;`, + } + session, _ := projecttestutil.Setup(files) + t.Cleanup(session.Close) + + ctx := context.Background() + indexURI := lsproto.DocumentUri("file://" + projectRoot + "/index.ts") + + // Open the index.ts file + session.DidOpenFile(ctx, indexURI, 1, "", lsproto.LanguageKindTypeScript) + _, err := session.GetLanguageServiceWithAutoImports(ctx, indexURI) + assert.NilError(t, err) + stats := autoImportStats(t, session) + projectBucket := singleBucket(t, stats.ProjectBuckets) + assert.Equal(t, 1, projectBucket.FileCount) + + // Edit index.ts to import foo from utils.ts + newContent := `import { foo } from "./utils";` + session.DidChangeFile(ctx, indexURI, 2, []lsproto.TextDocumentContentChangePartialOrWholeDocument{ + {WholeDocument: &lsproto.TextDocumentContentChangeWholeDocument{Text: newContent}}, + }) + + // Bucket should be rebuilt because new files were added + _, err = session.GetLanguageServiceWithAutoImports(ctx, indexURI) + assert.NilError(t, err) + stats = autoImportStats(t, session) + projectBucket = singleBucket(t, stats.ProjectBuckets) + assert.Equal(t, 2, projectBucket.FileCount) + }) + + t.Run("package.json dependency changes invalidate node_modules buckets", func(t *testing.T) { + t.Parallel() + fixture := autoimporttestutil.SetupLifecycleSession(t, lifecycleProjectRoot, 1) + session := fixture.Session() + sessionUtils := fixture.Utils() + project := fixture.SingleProject() + mainFile := project.File(0) + nodePackage := project.NodeModules()[0] + packageJSON := project.PackageJSONFile() + ctx := context.Background() + + session.DidOpenFile(ctx, mainFile.URI(), 1, mainFile.Content(), lsproto.LanguageKindTypeScript) + _, err := session.GetLanguageServiceWithAutoImports(ctx, mainFile.URI()) + assert.NilError(t, err) + stats := autoImportStats(t, session) + nodeModulesBucket := singleBucket(t, stats.NodeModulesBuckets) + assert.Equal(t, nodeModulesBucket.State.Dirty(), false) + + fs := sessionUtils.FS() + updatePackageJSON := func(content string) { + assert.NilError(t, fs.WriteFile(packageJSON.FileName(), content, false)) + session.DidChangeWatchedFiles(ctx, []*lsproto.FileEvent{ + {Type: lsproto.FileChangeTypeChanged, Uri: packageJSON.URI()}, + }) + } + + sameDepsContent := fmt.Sprintf("{\n \"name\": \"local-project-stable\",\n \"dependencies\": {\n \"%s\": \"*\"\n }\n}\n", nodePackage.Name) + updatePackageJSON(sameDepsContent) + _, err = session.GetLanguageService(ctx, mainFile.URI()) + assert.NilError(t, err) + stats = autoImportStats(t, session) + nodeModulesBucket = singleBucket(t, stats.NodeModulesBuckets) + assert.Equal(t, nodeModulesBucket.State.Dirty(), false) + + differentDepsContent := fmt.Sprintf("{\n \"name\": \"local-project-stable\",\n \"dependencies\": {\n \"%s\": \"*\",\n \"newpkg\": \"*\"\n }\n}\n", nodePackage.Name) + updatePackageJSON(differentDepsContent) + _, err = session.GetLanguageServiceWithAutoImports(ctx, mainFile.URI()) + assert.NilError(t, err) + stats = autoImportStats(t, session) + assert.Check(t, singleBucket(t, stats.NodeModulesBuckets).DependencyNames.Has("newpkg")) + }) + + t.Run("node_modules buckets get deleted when no open files can reference them", func(t *testing.T) { + t.Parallel() + fixture := autoimporttestutil.SetupMonorepoLifecycleSession(t, autoimporttestutil.MonorepoSetupConfig{ + Root: monorepoProjectRoot, + MonorepoPackageTemplate: autoimporttestutil.MonorepoPackageTemplate{ + Name: "monorepo", + NodeModuleNames: []string{"pkg-root"}, + }, + Packages: []autoimporttestutil.MonorepoPackageConfig{ + {FileCount: 1, MonorepoPackageTemplate: autoimporttestutil.MonorepoPackageTemplate{Name: "package-a", NodeModuleNames: []string{"pkg-a"}}}, + {FileCount: 1, MonorepoPackageTemplate: autoimporttestutil.MonorepoPackageTemplate{Name: "package-b", NodeModuleNames: []string{"pkg-b"}}}, + }, + }) + session := fixture.Session() + monorepo := fixture.Monorepo() + pkgA := monorepo.Package(0) + pkgB := monorepo.Package(1) + fileA := pkgA.File(0) + fileB := pkgB.File(0) + ctx := context.Background() + + // Open file in package-a, should create buckets for root and package-a node_modules + session.DidOpenFile(ctx, fileA.URI(), 1, fileA.Content(), lsproto.LanguageKindTypeScript) + _, err := session.GetLanguageServiceWithAutoImports(ctx, fileA.URI()) + assert.NilError(t, err) + + // Open file in package-b, should also create buckets for package-b + session.DidOpenFile(ctx, fileB.URI(), 1, fileB.Content(), lsproto.LanguageKindTypeScript) + _, err = session.GetLanguageServiceWithAutoImports(ctx, fileB.URI()) + assert.NilError(t, err) + stats := autoImportStats(t, session) + assert.Equal(t, len(stats.NodeModulesBuckets), 3) + assert.Equal(t, len(stats.ProjectBuckets), 2) + + // Close file in package-a, package-a's node_modules bucket and project bucket should be removed + session.DidCloseFile(ctx, fileA.URI()) + _, err = session.GetLanguageServiceWithAutoImports(ctx, fileB.URI()) + assert.NilError(t, err) + stats = autoImportStats(t, session) + assert.Equal(t, len(stats.NodeModulesBuckets), 2) + assert.Equal(t, len(stats.ProjectBuckets), 1) + }) + + t.Run("node_modules bucket dependency selection changes with open files", func(t *testing.T) { + t.Parallel() + monorepoRoot := "/home/src/monorepo" + packageADir := tspath.CombinePaths(monorepoRoot, "packages", "a") + monorepoIndex := tspath.CombinePaths(monorepoRoot, "index.js") + packageAIndex := tspath.CombinePaths(packageADir, "index.js") + + fixture := autoimporttestutil.SetupMonorepoLifecycleSession(t, autoimporttestutil.MonorepoSetupConfig{ + Root: monorepoRoot, + MonorepoPackageTemplate: autoimporttestutil.MonorepoPackageTemplate{ + Name: "monorepo", + NodeModuleNames: []string{"pkg1", "pkg2", "pkg3"}, + DependencyNames: []string{"pkg1"}, + }, + Packages: []autoimporttestutil.MonorepoPackageConfig{ + { + FileCount: 0, + MonorepoPackageTemplate: autoimporttestutil.MonorepoPackageTemplate{ + Name: "a", + DependencyNames: []string{"pkg1", "pkg2"}, + }, + }, + }, + ExtraFiles: []autoimporttestutil.TextFileSpec{ + {Path: monorepoIndex, Content: "export const monorepoIndex = 1;\n"}, + {Path: packageAIndex, Content: "export const pkgA = 2;\n"}, + }, + }) + session := fixture.Session() + monorepoHandle := fixture.ExtraFile(monorepoIndex) + packageAHandle := fixture.ExtraFile(packageAIndex) + + ctx := context.Background() + + // Open monorepo root file: expect dependencies restricted to pkg1 + session.DidOpenFile(ctx, monorepoHandle.URI(), 1, monorepoHandle.Content(), lsproto.LanguageKindJavaScript) + _, err := session.GetLanguageServiceWithAutoImports(ctx, monorepoHandle.URI()) + assert.NilError(t, err) + stats := autoImportStats(t, session) + assert.Assert(t, singleBucket(t, stats.NodeModulesBuckets).DependencyNames.Equals(collections.NewSetFromItems("pkg1"))) + + // Open package-a file: pkg2 should be added to existing bucket + session.DidOpenFile(ctx, packageAHandle.URI(), 1, packageAHandle.Content(), lsproto.LanguageKindJavaScript) + _, err = session.GetLanguageServiceWithAutoImports(ctx, packageAHandle.URI()) + assert.NilError(t, err) + stats = autoImportStats(t, session) + assert.Assert(t, singleBucket(t, stats.NodeModulesBuckets).DependencyNames.Equals(collections.NewSetFromItems("pkg1", "pkg2"))) + + // Close package-a file; only monorepo bucket should remain + session.DidCloseFile(ctx, packageAHandle.URI()) + _, err = session.GetLanguageServiceWithAutoImports(ctx, monorepoHandle.URI()) + assert.NilError(t, err) + stats = autoImportStats(t, session) + assert.Assert(t, singleBucket(t, stats.NodeModulesBuckets).DependencyNames.Equals(collections.NewSetFromItems("pkg1"))) + + // Close monorepo file; no node_modules buckets should remain + session.DidCloseFile(ctx, monorepoHandle.URI()) + session.DidOpenFile(ctx, "untitled:Untitled-1", 0, "", lsproto.LanguageKindTypeScript) + _, err = session.GetLanguageService(ctx, "untitled:Untitled-1") + assert.NilError(t, err) + stats = autoImportStats(t, session) + assert.Equal(t, len(stats.NodeModulesBuckets), 0) + }) +} + +const ( + lifecycleProjectRoot = "/home/src/autoimport-lifecycle" + monorepoProjectRoot = "/home/src/autoimport-monorepo" +) + +func autoImportStats(t *testing.T, session *project.Session) *autoimport.CacheStats { + t.Helper() + snapshot, release := session.Snapshot() + defer release() + registry := snapshot.AutoImportRegistry() + if registry == nil { + t.Fatal("auto import registry not initialized") + } + return registry.GetCacheStats() +} + +func singleBucket(t *testing.T, buckets []autoimport.BucketStats) autoimport.BucketStats { + t.Helper() + if len(buckets) != 1 { + t.Fatalf("expected 1 bucket, got %d", len(buckets)) + } + return buckets[0] +} diff --git a/internal/ls/autoimport/specifiers.go b/internal/ls/autoimport/specifiers.go new file mode 100644 index 0000000000..f53ad3cbb6 --- /dev/null +++ b/internal/ls/autoimport/specifiers.go @@ -0,0 +1,75 @@ +package autoimport + +import ( + "strings" + + "github.com/microsoft/typescript-go/internal/modulespecifiers" +) + +func (v *View) GetModuleSpecifier( + export *Export, + userPreferences modulespecifiers.UserPreferences, +) (string, modulespecifiers.ResultKind) { + // Ambient module + if modulespecifiers.PathIsBareSpecifier(string(export.ModuleID)) { + specifier := string(export.ModuleID) + if modulespecifiers.IsExcludedByRegex(specifier, userPreferences.AutoImportSpecifierExcludeRegexes) { + return "", modulespecifiers.ResultKindNone + } + return string(export.ModuleID), modulespecifiers.ResultKindAmbient + } + + if export.NodeModulesDirectory != "" { + if entrypoints, ok := v.registry.nodeModules[export.NodeModulesDirectory].Entrypoints[export.Path]; ok { + for _, entrypoint := range entrypoints { + if entrypoint.IncludeConditions.IsSubsetOf(v.conditions) && !v.conditions.Intersects(entrypoint.ExcludeConditions) { + specifier := modulespecifiers.ProcessEntrypointEnding( + entrypoint, + userPreferences, + v.program, + v.program.Options(), + v.importingFile, + v.getAllowedEndings(), + ) + + if !modulespecifiers.IsExcludedByRegex(specifier, userPreferences.AutoImportSpecifierExcludeRegexes) { + return specifier, modulespecifiers.ResultKindNodeModules + } + } + } + return "", modulespecifiers.ResultKindNone + } + } + + cache := v.registry.specifierCache[v.importingFile.Path()] + if export.NodeModulesDirectory == "" { + if specifier, ok := cache.Load(export.Path); ok { + if specifier == "" { + return "", modulespecifiers.ResultKindNone + } + return specifier, modulespecifiers.ResultKindRelative + } + } + + specifiers, kind := modulespecifiers.GetModuleSpecifiersForFileWithInfo( + v.importingFile, + export.ModuleFileName, + v.program.Options(), + v.program, + userPreferences, + modulespecifiers.ModuleSpecifierOptions{}, + true, + ) + // !!! unsure when this could return multiple specifiers combined with the + // new node_modules code. Possibly with local symlinks, which should be + // very rare. + for _, specifier := range specifiers { + if strings.Contains(specifier, "/node_modules/") { + continue + } + cache.Store(export.Path, specifier) + return specifier, kind + } + cache.Store(export.Path, "") + return "", modulespecifiers.ResultKindNone +} diff --git a/internal/ls/autoimport/util.go b/internal/ls/autoimport/util.go new file mode 100644 index 0000000000..7b02be30d8 --- /dev/null +++ b/internal/ls/autoimport/util.go @@ -0,0 +1,174 @@ +package autoimport + +import ( + "context" + "runtime" + "sync/atomic" + "unicode" + "unicode/utf8" + + "github.com/microsoft/typescript-go/internal/ast" + "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/module" + "github.com/microsoft/typescript-go/internal/modulespecifiers" + "github.com/microsoft/typescript-go/internal/tspath" + "github.com/microsoft/typescript-go/internal/vfs" +) + +func getModuleIDAndFileNameOfModuleSymbol(symbol *ast.Symbol) (ModuleID, string) { + if !symbol.IsExternalModule() { + panic("symbol is not an external module") + } + decl := ast.GetNonAugmentationDeclaration(symbol) + if decl == nil { + panic("module symbol has no non-augmentation declaration") + } + if decl.Kind == ast.KindSourceFile { + return ModuleID(decl.AsSourceFile().Path()), decl.AsSourceFile().FileName() + } + if ast.IsModuleWithStringLiteralName(decl) { + return ModuleID(decl.Name().Text()), "" + } + panic("could not determine module ID of module symbol") +} + +// wordIndices splits an identifier into its constituent words based on camelCase and snake_case conventions +// by returning the starting byte indices of each word. The first index is always 0. +// - CamelCase +// ^ ^ +// - snake_case +// ^ ^ +// - ParseURL +// ^ ^ +// - __proto__ +// ^ +func wordIndices(s string) []int { + var indices []int + for byteIndex, runeValue := range s { + if byteIndex == 0 { + indices = append(indices, byteIndex) + continue + } + if runeValue == '_' { + if byteIndex+1 < len(s) && s[byteIndex+1] != '_' { + indices = append(indices, byteIndex+1) + } + continue + } + if unicode.IsUpper(runeValue) && (unicode.IsLower(core.FirstResult(utf8.DecodeLastRuneInString(s[:byteIndex]))) || (byteIndex+1 < len(s) && unicode.IsLower(core.FirstResult(utf8.DecodeRuneInString(s[byteIndex+1:]))))) { + indices = append(indices, byteIndex) + } + } + return indices +} + +func getPackageNamesInNodeModules(nodeModulesDir string, fs vfs.FS) (*collections.Set[string], error) { + packageNames := &collections.Set[string]{} + if tspath.GetBaseFileName(nodeModulesDir) != "node_modules" { + panic("nodeModulesDir is not a node_modules directory") + } + if !fs.DirectoryExists(nodeModulesDir) { + return nil, vfs.ErrNotExist + } + entries := fs.GetAccessibleEntries(nodeModulesDir) + for _, baseName := range entries.Directories { + if baseName[0] == '.' { + continue + } + if baseName[0] == '@' { + scopedDirPath := tspath.CombinePaths(nodeModulesDir, baseName) + for _, scopedPackageDirName := range fs.GetAccessibleEntries(scopedDirPath).Directories { + scopedBaseName := tspath.GetBaseFileName(scopedPackageDirName) + if baseName == "@types" { + packageNames.Add(module.GetPackageNameFromTypesPackageName(tspath.CombinePaths("@types", scopedBaseName))) + } else { + packageNames.Add(tspath.CombinePaths(baseName, scopedBaseName)) + } + } + continue + } + packageNames.Add(baseName) + } + return packageNames, nil +} + +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 getResolvedPackageNames(ctx context.Context, program *compiler.Program) *collections.Set[string] { + resolvedPackageNames := program.ResolvedPackageNames().Clone() + unresolvedPackageNames := program.UnresolvedPackageNames() + if unresolvedPackageNames.Len() > 0 { + checker, done := program.GetTypeChecker(ctx) + defer done() + for name := range unresolvedPackageNames.Keys() { + if symbol := checker.TryFindAmbientModule(name); symbol != nil { + declaringFile := ast.GetSourceFileOfModule(symbol) + if packageName := modulespecifiers.GetPackageNameFromDirectory(declaringFile.FileName()); packageName != "" { + resolvedPackageNames.Add(packageName) + } + } + } + } + return resolvedPackageNames +} + +func createCheckerPool(program checker.Program) (getChecker func() (*checker.Checker, func()), closePool func(), getCreatedCount func() int32) { + maxSize := int32(runtime.GOMAXPROCS(0)) + pool := make(chan *checker.Checker, maxSize) + var created atomic.Int32 + + return func() (*checker.Checker, func()) { + // Try to get an existing checker + select { + case ch := <-pool: + return ch, func() { pool <- ch } + default: + break + } + // Try to create a new one if under limit + for { + current := created.Load() + if current >= maxSize { + // At limit, wait for one to become available + ch := <-pool + return ch, func() { pool <- ch } + } + if created.CompareAndSwap(current, current+1) { + ch := core.FirstResult(checker.NewChecker(program)) + return ch, func() { pool <- ch } + } + } + }, func() { + close(pool) + }, func() int32 { + return created.Load() + } +} diff --git a/internal/ls/autoimport/util_test.go b/internal/ls/autoimport/util_test.go new file mode 100644 index 0000000000..1dde71efd1 --- /dev/null +++ b/internal/ls/autoimport/util_test.go @@ -0,0 +1,90 @@ +package autoimport + +import ( + "reflect" + "testing" +) + +func TestWordIndices(t *testing.T) { + t.Parallel() + tests := []struct { + input string + expectedWords []string + }{ + // Basic camelCase + { + input: "camelCase", + expectedWords: []string{"camelCase", "Case"}, + }, + // snake_case + { + input: "snake_case", + expectedWords: []string{"snake_case", "case"}, + }, + // ParseURL - uppercase sequence followed by lowercase + { + input: "ParseURL", + expectedWords: []string{"ParseURL", "URL"}, + }, + // XMLHttpRequest - multiple uppercase sequences + { + input: "XMLHttpRequest", + expectedWords: []string{"XMLHttpRequest", "HttpRequest", "Request"}, + }, + // Single word lowercase + { + input: "hello", + expectedWords: []string{"hello"}, + }, + // Single word uppercase + { + input: "HELLO", + expectedWords: []string{"HELLO"}, + }, + // Mixed with numbers + { + input: "parseHTML5Parser", + expectedWords: []string{"parseHTML5Parser", "HTML5Parser", "Parser"}, + }, + // Underscore variations + { + input: "__proto__", + expectedWords: []string{"__proto__", "proto__"}, + }, + { + input: "_private_member", + expectedWords: []string{"_private_member", "member"}, + }, + // Single character + { + input: "a", + expectedWords: []string{"a"}, + }, + { + input: "A", + expectedWords: []string{"A"}, + }, + // Consecutive underscores + { + input: "test__double__underscore", + expectedWords: []string{"test__double__underscore", "double__underscore", "underscore"}, + }, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + t.Parallel() + indices := wordIndices(tt.input) + + // Convert indices to actual word slices for comparison + var actualWords []string + for _, idx := range indices { + actualWords = append(actualWords, tt.input[idx:]) + } + + if !reflect.DeepEqual(actualWords, tt.expectedWords) { + t.Errorf("wordIndices(%q) produced words %v, want %v", tt.input, actualWords, tt.expectedWords) + } + }) + } +} diff --git a/internal/ls/autoimport/view.go b/internal/ls/autoimport/view.go new file mode 100644 index 0000000000..46f5e8a900 --- /dev/null +++ b/internal/ls/autoimport/view.go @@ -0,0 +1,206 @@ +package autoimport + +import ( + "context" + "slices" + "strings" + "unicode" + + "github.com/microsoft/typescript-go/internal/ast" + "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/ls/lsutil" + "github.com/microsoft/typescript-go/internal/module" + "github.com/microsoft/typescript-go/internal/modulespecifiers" + "github.com/microsoft/typescript-go/internal/tspath" +) + +type View struct { + registry *Registry + importingFile *ast.SourceFile + program *compiler.Program + preferences modulespecifiers.UserPreferences + projectKey tspath.Path + + allowedEndings []modulespecifiers.ModuleSpecifierEnding + conditions *collections.Set[string] + shouldUseUriStyleNodeCoreModules core.Tristate + existingImports *collections.MultiMap[ModuleID, existingImport] + shouldUseRequireForFixes *bool +} + +func NewView(registry *Registry, importingFile *ast.SourceFile, projectKey tspath.Path, program *compiler.Program, preferences modulespecifiers.UserPreferences) *View { + return &View{ + registry: registry, + importingFile: importingFile, + program: program, + projectKey: projectKey, + preferences: preferences, + conditions: collections.NewSetFromItems( + module.GetConditions(program.Options(), + program.GetDefaultResolutionModeForFile(importingFile))..., + ), + shouldUseUriStyleNodeCoreModules: lsutil.ShouldUseUriStyleNodeCoreModules(importingFile, program), + } +} + +func (v *View) getAllowedEndings() []modulespecifiers.ModuleSpecifierEnding { + if v.allowedEndings == nil { + resolutionMode := v.program.GetDefaultResolutionModeForFile(v.importingFile) + v.allowedEndings = modulespecifiers.GetAllowedEndingsInPreferredOrder( + v.preferences, + v.program, + v.program.Options(), + v.importingFile, + "", + resolutionMode, + ) + } + return v.allowedEndings +} + +type QueryKind int + +const ( + QueryKindWordPrefix QueryKind = iota + QueryKindExactMatch + QueryKindCaseInsensitiveMatch +) + +func (v *View) Search(query string, kind QueryKind) []*Export { + var results []*Export + search := func(bucket *RegistryBucket) []*Export { + switch kind { + case QueryKindWordPrefix: + return bucket.Index.SearchWordPrefix(query) + case QueryKindExactMatch: + return bucket.Index.Find(query, true) + case QueryKindCaseInsensitiveMatch: + return bucket.Index.Find(query, false) + default: + panic("unreachable") + } + } + + if bucket, ok := v.registry.projects[v.projectKey]; ok { + exports := search(bucket) + results = slices.Grow(results, len(exports)) + for _, e := range exports { + if string(e.ModuleID) == string(v.importingFile.Path()) { + // Don't auto-import from the importing file itself + continue + } + results = append(results, e) + } + } + + var excludePackages *collections.Set[string] + tspath.ForEachAncestorDirectoryPath(v.importingFile.Path().GetDirectoryPath(), func(dirPath tspath.Path) (result any, stop bool) { + if nodeModulesBucket, ok := v.registry.nodeModules[dirPath]; ok { + exports := search(nodeModulesBucket) + if excludePackages.Len() > 0 { + results = slices.Grow(results, len(exports)) + for _, e := range exports { + if !excludePackages.Has(e.PackageName) { + results = append(results, e) + } + } + } else { + results = append(results, exports...) + } + + // As we go up the directory tree, exclude packages found in lower node_modules + excludePackages = excludePackages.UnionedWith(nodeModulesBucket.PackageNames) + } + return nil, false + }) + return results +} + +type FixAndExport struct { + Fix *Fix + Export *Export +} + +func (v *View) GetCompletions(ctx context.Context, prefix string, forJSX bool, isTypeOnlyLocation bool) []*FixAndExport { + results := v.Search(prefix, QueryKindWordPrefix) + + type exportGroupKey struct { + target ExportID + name string + ambientModuleOrPackageName string + } + grouped := make(map[exportGroupKey][]*Export, len(results)) +outer: + for _, e := range results { + name := e.Name() + if forJSX && !(unicode.IsUpper(rune(name[0])) || e.IsRenameable()) { + continue + } + target := e.ExportID + if e.Target != (ExportID{}) { + target = e.Target + } + key := exportGroupKey{ + target: target, + name: name, + ambientModuleOrPackageName: core.FirstNonZero(e.AmbientModuleName(), e.PackageName), + } + if e.PackageName == "@types/node" || strings.Contains(string(e.Path), "/node_modules/@types/node/") { + if _, ok := core.UnprefixedNodeCoreModules[key.ambientModuleOrPackageName]; ok { + // Group URI-style and non-URI style node core modules together so the ranking logic + // is allowed to drop one if an explicit preference is detected. + key.ambientModuleOrPackageName = "node:" + key.ambientModuleOrPackageName + } + } + if existing, ok := grouped[key]; ok { + for i, ex := range existing { + if e.ExportID == ex.ExportID { + grouped[key] = slices.Replace(existing, i, i+1, &Export{ + ExportID: e.ExportID, + ModuleFileName: e.ModuleFileName, + Syntax: min(e.Syntax, ex.Syntax), + Flags: e.Flags | ex.Flags, + ScriptElementKind: min(e.ScriptElementKind, ex.ScriptElementKind), + ScriptElementKindModifiers: *e.ScriptElementKindModifiers.UnionedWith(&ex.ScriptElementKindModifiers), + localName: e.localName, + Target: e.Target, + Path: e.Path, + NodeModulesDirectory: e.NodeModulesDirectory, + }) + continue outer + } + } + } + grouped[key] = append(grouped[key], e) + } + + fixes := make([]*FixAndExport, 0, len(results)) + compareFixes := func(a, b *FixAndExport) int { + return v.CompareFixesForRanking(a.Fix, b.Fix) + } + + for _, exps := range grouped { + fixesForGroup := make([]*FixAndExport, 0, len(exps)) + for _, e := range exps { + for _, fix := range v.GetFixes(ctx, e, forJSX, isTypeOnlyLocation, nil) { + fixesForGroup = append(fixesForGroup, &FixAndExport{ + Fix: fix, + Export: e, + }) + } + } + fixes = append(fixes, core.MinAllFunc(fixesForGroup, compareFixes)...) + } + + // The client will do additional sorting by SortText and Label, so we don't + // need to consider the name in our sorting here; we only need to produce a + // stable relative ordering between completions that the client will consider + // equivalent. + slices.SortFunc(fixes, func(a, b *FixAndExport) int { + return v.CompareFixesForSorting(a.Fix, b.Fix) + }) + + return fixes +} diff --git a/internal/ls/autoimportfixes.go b/internal/ls/autoimportfixes.go deleted file mode 100644 index 7383474f07..0000000000 --- a/internal/ls/autoimportfixes.go +++ /dev/null @@ -1,368 +0,0 @@ -package ls - -import ( - "slices" - - "github.com/microsoft/typescript-go/internal/ast" - "github.com/microsoft/typescript-go/internal/astnav" - "github.com/microsoft/typescript-go/internal/core" - "github.com/microsoft/typescript-go/internal/debug" - "github.com/microsoft/typescript-go/internal/ls/change" - "github.com/microsoft/typescript-go/internal/ls/lsutil" - "github.com/microsoft/typescript-go/internal/ls/organizeimports" - "github.com/microsoft/typescript-go/internal/stringutil" -) - -type Import struct { - name string - kind ImportKind // ImportKindCommonJS | ImportKindNamespace - addAsTypeOnly AddAsTypeOnly - propertyName string // Use when needing to generate an `ImportSpecifier with a `propertyName`; the name preceding "as" keyword (propertyName = "" when "as" is absent) -} - -func addNamespaceQualifier(ct *change.Tracker, sourceFile *ast.SourceFile, qualification *Qualification) { - ct.InsertText(sourceFile, qualification.usagePosition, qualification.namespacePrefix+".") -} - -func (ls *LanguageService) doAddExistingFix( - ct *change.Tracker, - sourceFile *ast.SourceFile, - clause *ast.Node, // ImportClause | ObjectBindingPattern, - defaultImport *Import, - namedImports []*Import, - // removeExistingImportSpecifiers *core.Set[ImportSpecifier | BindingElement] // !!! remove imports not implemented -) { - switch clause.Kind { - case ast.KindObjectBindingPattern: - if clause.Kind == ast.KindObjectBindingPattern { - // bindingPattern := clause.AsBindingPattern() - // !!! adding *and* removing imports not implemented - // if (removeExistingImportSpecifiers && core.Some(bindingPattern.Elements, func(e *ast.Node) bool { - // return removeExistingImportSpecifiers.Has(e) - // })) { - // If we're both adding and removing elements, just replace and reprint the whole - // node. The change tracker doesn't understand all the operations and can insert or - // leave behind stray commas. - // ct.replaceNode( - // sourceFile, - // bindingPattern, - // ct.NodeFactory.NewObjectBindingPattern([ - // ...bindingPattern.Elements.Filter(func(e *ast.Node) bool { - // return !removeExistingImportSpecifiers.Has(e) - // }), - // ...defaultImport ? [ct.NodeFactory.createBindingElement(/*dotDotDotToken*/ nil, /*propertyName*/ "default", defaultImport.name)] : emptyArray, - // ...namedImports.map(i => ct.NodeFactory.createBindingElement(/*dotDotDotToken*/ nil, i.propertyName, i.name)), - // ]), - // ) - // return - // } - if defaultImport != nil { - addElementToBindingPattern(ct, sourceFile, clause, defaultImport.name, ptrTo("default")) - } - for _, specifier := range namedImports { - addElementToBindingPattern(ct, sourceFile, clause, specifier.name, &specifier.propertyName) - } - return - } - case ast.KindImportClause: - - importClause := clause.AsImportClause() - - // promoteFromTypeOnly = true if we need to promote the entire original clause from type only - promoteFromTypeOnly := importClause.IsTypeOnly() && core.Some(append(namedImports, defaultImport), func(i *Import) bool { - if i == nil { - return false - } - return i.addAsTypeOnly == AddAsTypeOnlyNotAllowed - }) - - existingSpecifiers := []*ast.Node{} // []*ast.ImportSpecifier - if importClause.NamedBindings != nil && importClause.NamedBindings.Kind == ast.KindNamedImports { - existingSpecifiers = importClause.NamedBindings.Elements() - } - - if defaultImport != nil { - debug.Assert(clause.Name() == nil, "Cannot add a default import to an import clause that already has one") - ct.InsertNodeAt(sourceFile, core.TextPos(astnav.GetStartOfNode(clause, sourceFile, false)), ct.NodeFactory.NewIdentifier(defaultImport.name), change.NodeOptions{Suffix: ", "}) - } - - if len(namedImports) > 0 { - specifierComparer, isSorted := organizeimports.GetNamedImportSpecifierComparerWithDetection(importClause.Parent, sourceFile, ls.UserPreferences()) - newSpecifiers := core.Map(namedImports, func(namedImport *Import) *ast.Node { - var identifier *ast.Node - if namedImport.propertyName != "" { - identifier = ct.NodeFactory.NewIdentifier(namedImport.propertyName).AsIdentifier().AsNode() - } - return ct.NodeFactory.NewImportSpecifier( - (!importClause.IsTypeOnly() || promoteFromTypeOnly) && shouldUseTypeOnly(namedImport.addAsTypeOnly, ls.UserPreferences()), - identifier, - ct.NodeFactory.NewIdentifier(namedImport.name), - ) - }) - slices.SortFunc(newSpecifiers, specifierComparer) - - // !!! remove imports not implemented - // if (removeExistingImportSpecifiers) { - // // If we're both adding and removing specifiers, just replace and reprint the whole - // // node. The change tracker doesn't understand all the operations and can insert or - // // leave behind stray commas. - // ct.replaceNode( - // sourceFile, - // importClause.NamedBindings, - // ct.NodeFactory.updateNamedImports( - // importClause.NamedBindings.AsNamedImports(), - // append(core.Filter(existingSpecifiers, func (s *ast.ImportSpecifier) bool {return !removeExistingImportSpecifiers.Has(s)}), newSpecifiers...), // !!! sort with specifierComparer - // ), - // ); - // - if len(existingSpecifiers) > 0 && isSorted != core.TSFalse { - // The sorting preference computed earlier may or may not have validated that these particular - // import specifiers are sorted. If they aren't, `getImportSpecifierInsertionIndex` will return - // nonsense. So if there are existing specifiers, even if we know the sorting preference, we - // need to ensure that the existing specifiers are sorted according to the preference in order - // to do a sorted insertion. - - // If we're promoting the clause from type-only, we need to transform the existing imports - // before attempting to insert the new named imports (for comparison purposes only) - specsToCompareAgainst := existingSpecifiers - if promoteFromTypeOnly && len(existingSpecifiers) > 0 { - specsToCompareAgainst = core.Map(existingSpecifiers, func(e *ast.Node) *ast.Node { - spec := e.AsImportSpecifier() - var propertyName *ast.Node - if spec.PropertyName != nil { - propertyName = spec.PropertyName - } - syntheticSpec := ct.NodeFactory.NewImportSpecifier( - true, // isTypeOnly - propertyName, - spec.Name(), - ) - return syntheticSpec - }) - } - - for _, spec := range newSpecifiers { - insertionIndex := organizeimports.GetImportSpecifierInsertionIndex(specsToCompareAgainst, spec, specifierComparer) - ct.InsertImportSpecifierAtIndex(sourceFile, spec, importClause.NamedBindings, insertionIndex) - } - } else if len(existingSpecifiers) > 0 && isSorted.IsTrue() { - // Existing specifiers are sorted, so insert each new specifier at the correct position - for _, spec := range newSpecifiers { - insertionIndex := organizeimports.GetImportSpecifierInsertionIndex(existingSpecifiers, spec, specifierComparer) - if insertionIndex >= len(existingSpecifiers) { - // Insert at the end - ct.InsertNodeInListAfter(sourceFile, existingSpecifiers[len(existingSpecifiers)-1], spec.AsNode(), existingSpecifiers) - } else { - // Insert before the element at insertionIndex - ct.InsertNodeInListAfter(sourceFile, existingSpecifiers[insertionIndex], spec.AsNode(), existingSpecifiers) - } - } - } else if len(existingSpecifiers) > 0 { - // Existing specifiers may not be sorted, append to the end - for _, spec := range newSpecifiers { - ct.InsertNodeInListAfter(sourceFile, existingSpecifiers[len(existingSpecifiers)-1], spec.AsNode(), existingSpecifiers) - } - } else { - if len(newSpecifiers) > 0 { - namedImports := ct.NodeFactory.NewNamedImports(ct.NodeFactory.NewNodeList(newSpecifiers)) - if importClause.NamedBindings != nil { - ct.ReplaceNode(sourceFile, importClause.NamedBindings, namedImports, nil) - } else { - if clause.Name() == nil { - panic("Import clause must have either named imports or a default import") - } - ct.InsertNodeAfter(sourceFile, clause.Name(), namedImports) - } - } - } - } - - if promoteFromTypeOnly { - // Delete the 'type' keyword from the import clause - typeKeyword := getTypeKeywordOfTypeOnlyImport(importClause, sourceFile) - ct.Delete(sourceFile, typeKeyword) - - // Add 'type' modifier to existing specifiers (not newly added ones) - // We preserve the type-onlyness of existing specifiers regardless of whether - // it would make a difference in emit (user preference). - if len(existingSpecifiers) > 0 { - for _, specifier := range existingSpecifiers { - if !specifier.AsImportSpecifier().IsTypeOnly { - ct.InsertModifierBefore(sourceFile, ast.KindTypeKeyword, specifier) - } - } - } - } - default: - panic("Unsupported clause kind: " + clause.Kind.String() + "for doAddExistingFix") - } -} - -func getTypeKeywordOfTypeOnlyImport(importClause *ast.ImportClause, sourceFile *ast.SourceFile) *ast.Node { - debug.Assert(importClause.IsTypeOnly(), "import clause must be type-only") - // The first child of a type-only import clause is the 'type' keyword - // import type { foo } from './bar' - // ^^^^ - typeKeyword := astnav.FindChildOfKind(importClause.AsNode(), ast.KindTypeKeyword, sourceFile) - debug.Assert(typeKeyword != nil, "type-only import clause should have a type keyword") - return typeKeyword -} - -func addElementToBindingPattern(ct *change.Tracker, sourceFile *ast.SourceFile, bindingPattern *ast.Node, name string, propertyName *string) { - element := newBindingElementFromNameAndPropertyName(ct, name, propertyName) - if len(bindingPattern.Elements()) > 0 { - ct.InsertNodeInListAfter(sourceFile, bindingPattern.Elements()[len(bindingPattern.Elements())-1], element, nil) - } else { - ct.ReplaceNode(sourceFile, bindingPattern, ct.NodeFactory.NewBindingPattern( - ast.KindObjectBindingPattern, - ct.NodeFactory.NewNodeList([]*ast.Node{element}), - ), nil) - } -} - -func newBindingElementFromNameAndPropertyName(ct *change.Tracker, name string, propertyName *string) *ast.Node { - var newPropertyNameIdentifier *ast.Node - if propertyName != nil { - newPropertyNameIdentifier = ct.NodeFactory.NewIdentifier(*propertyName) - } - return ct.NodeFactory.NewBindingElement( - nil, /*dotDotDotToken*/ - newPropertyNameIdentifier, - ct.NodeFactory.NewIdentifier(name), - nil, /* initializer */ - ) -} - -func (ls *LanguageService) insertImports(ct *change.Tracker, sourceFile *ast.SourceFile, imports []*ast.Statement, blankLineBetween bool) { - var existingImportStatements []*ast.Statement - - if imports[0].Kind == ast.KindVariableStatement { - existingImportStatements = core.Filter(sourceFile.Statements.Nodes, ast.IsRequireVariableStatement) - } else { - existingImportStatements = core.Filter(sourceFile.Statements.Nodes, ast.IsAnyImportSyntax) - } - comparer, isSorted := organizeimports.GetOrganizeImportsStringComparerWithDetection(existingImportStatements, ls.UserPreferences()) - sortedNewImports := slices.Clone(imports) - slices.SortFunc(sortedNewImports, func(a, b *ast.Statement) int { - return organizeimports.CompareImportsOrRequireStatements(a, b, comparer) - }) - // !!! FutureSourceFile - // if !isFullSourceFile(sourceFile) { - // for _, newImport := range sortedNewImports { - // // Insert one at a time to send correct original source file for accurate text reuse - // // when some imports are cloned from existing ones in other files. - // ct.insertStatementsInNewFile(sourceFile.fileName, []*ast.Node{newImport}, ast.GetSourceFileOfNode(getOriginalNode(newImport))) - // } - // return; - // } - - if len(existingImportStatements) > 0 && isSorted { - // Existing imports are sorted, insert each new import at the correct position - for _, newImport := range sortedNewImports { - insertionIndex := organizeimports.GetImportDeclarationInsertIndex(existingImportStatements, newImport, func(a, b *ast.Statement) stringutil.Comparison { - return organizeimports.CompareImportsOrRequireStatements(a, b, comparer) - }) - if insertionIndex == 0 { - // If the first import is top-of-file, insert after the leading comment which is likely the header - ct.InsertNodeAt(sourceFile, core.TextPos(astnav.GetStartOfNode(existingImportStatements[0], sourceFile, false)), newImport.AsNode(), change.NodeOptions{}) - } else { - prevImport := existingImportStatements[insertionIndex-1] - ct.InsertNodeAfter(sourceFile, prevImport.AsNode(), newImport.AsNode()) - } - } - } else if len(existingImportStatements) > 0 { - ct.InsertNodesAfter(sourceFile, existingImportStatements[len(existingImportStatements)-1], sortedNewImports) - } else { - ct.InsertAtTopOfFile(sourceFile, sortedNewImports, blankLineBetween) - } -} - -func makeImport(ct *change.Tracker, defaultImport *ast.IdentifierNode, namedImports []*ast.Node, moduleSpecifier *ast.Expression, isTypeOnly bool) *ast.Statement { - var newNamedImports *ast.Node - if len(namedImports) > 0 { - newNamedImports = ct.NodeFactory.NewNamedImports(ct.NodeFactory.NewNodeList(namedImports)) - } - var importClause *ast.Node - if defaultImport != nil || newNamedImports != nil { - importClause = ct.NodeFactory.NewImportClause(core.IfElse(isTypeOnly, ast.KindTypeKeyword, ast.KindUnknown), defaultImport, newNamedImports) - } - return ct.NodeFactory.NewImportDeclaration( /*modifiers*/ nil, importClause, moduleSpecifier, nil /*attributes*/) -} - -func (ls *LanguageService) getNewImports( - ct *change.Tracker, - moduleSpecifier string, - quotePreference quotePreference, - defaultImport *Import, - namedImports []*Import, - namespaceLikeImport *Import, // { importKind: ImportKind.CommonJS | ImportKind.Namespace; } - compilerOptions *core.CompilerOptions, -) []*ast.Statement { - moduleSpecifierStringLiteral := ct.NodeFactory.NewStringLiteral( - moduleSpecifier, - core.IfElse(quotePreference == quotePreferenceSingle, ast.TokenFlagsSingleQuote, ast.TokenFlagsNone), - ) - var statements []*ast.Statement // []AnyImportSyntax - if defaultImport != nil || len(namedImports) > 0 { - // `verbatimModuleSyntax` should prefer top-level `import type` - - // even though it's not an error, it would add unnecessary runtime emit. - topLevelTypeOnly := (defaultImport == nil || needsTypeOnly(defaultImport.addAsTypeOnly)) && - core.Every(namedImports, func(i *Import) bool { return needsTypeOnly(i.addAsTypeOnly) }) || - (compilerOptions.VerbatimModuleSyntax.IsTrue() || ls.UserPreferences().PreferTypeOnlyAutoImports == core.TSTrue) && - (defaultImport == nil || defaultImport.addAsTypeOnly != AddAsTypeOnlyNotAllowed) && - !core.Some(namedImports, func(i *Import) bool { return i.addAsTypeOnly == AddAsTypeOnlyNotAllowed }) - - var defaultImportNode *ast.Node - if defaultImport != nil { - defaultImportNode = ct.NodeFactory.NewIdentifier(defaultImport.name) - } - - statements = append(statements, makeImport(ct, defaultImportNode, core.Map(namedImports, func(namedImport *Import) *ast.Node { - var namedImportPropertyName *ast.Node - if namedImport.propertyName != "" { - namedImportPropertyName = ct.NodeFactory.NewIdentifier(namedImport.propertyName) - } - return ct.NodeFactory.NewImportSpecifier( - !topLevelTypeOnly && shouldUseTypeOnly(namedImport.addAsTypeOnly, ls.UserPreferences()), - namedImportPropertyName, - ct.NodeFactory.NewIdentifier(namedImport.name), - ) - }), moduleSpecifierStringLiteral, topLevelTypeOnly)) - } - - if namespaceLikeImport != nil { - var declaration *ast.Statement - if namespaceLikeImport.kind == ImportKindCommonJS { - declaration = ct.NodeFactory.NewImportEqualsDeclaration( - /*modifiers*/ nil, - shouldUseTypeOnly(namespaceLikeImport.addAsTypeOnly, ls.UserPreferences()), - ct.NodeFactory.NewIdentifier(namespaceLikeImport.name), - ct.NodeFactory.NewExternalModuleReference(moduleSpecifierStringLiteral), - ) - } else { - declaration = ct.NodeFactory.NewImportDeclaration( - /*modifiers*/ nil, - ct.NodeFactory.NewImportClause( - /*phaseModifier*/ core.IfElse(shouldUseTypeOnly(namespaceLikeImport.addAsTypeOnly, ls.UserPreferences()), ast.KindTypeKeyword, ast.KindUnknown), - /*name*/ nil, - ct.NodeFactory.NewNamespaceImport(ct.NodeFactory.NewIdentifier(namespaceLikeImport.name)), - ), - moduleSpecifierStringLiteral, - /*attributes*/ nil, - ) - } - statements = append(statements, declaration) - } - if len(statements) == 0 { - panic("No statements to insert for new imports") - } - return statements -} - -func needsTypeOnly(addAsTypeOnly AddAsTypeOnly) bool { - return addAsTypeOnly == AddAsTypeOnlyRequired -} - -func shouldUseTypeOnly(addAsTypeOnly AddAsTypeOnly, preferences *lsutil.UserPreferences) bool { - return needsTypeOnly(addAsTypeOnly) || addAsTypeOnly != AddAsTypeOnlyNotAllowed && preferences.PreferTypeOnlyAutoImports == core.TSTrue -} diff --git a/internal/ls/autoimports.go b/internal/ls/autoimports.go deleted file mode 100644 index c88f426880..0000000000 --- a/internal/ls/autoimports.go +++ /dev/null @@ -1,1703 +0,0 @@ -package ls - -import ( - "context" - "fmt" - "strings" - - "github.com/dlclark/regexp2" - "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/debug" - "github.com/microsoft/typescript-go/internal/diagnostics" - "github.com/microsoft/typescript-go/internal/locale" - "github.com/microsoft/typescript-go/internal/ls/change" - "github.com/microsoft/typescript-go/internal/ls/lsutil" - "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/packagejson" - "github.com/microsoft/typescript-go/internal/stringutil" - "github.com/microsoft/typescript-go/internal/tspath" - "github.com/microsoft/typescript-go/internal/vfs" -) - -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 -} - -func newExportInfoMapKey(importedName string, symbol *ast.Symbol, ambientModuleNameKey string, ch *checker.Checker) lsproto.ExportInfoMapKey { - return lsproto.ExportInfoMapKey{ - SymbolName: importedName, - SymbolId: uint64(ast.GetSymbolId(ch.SkipAlias(symbol))), - AmbientModuleName: ambientModuleNameKey, - } -} - -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.OrderedMap[lsproto.ExportInfoMapKey, []*CachedSymbolExportInfo] - symbols map[int]symbolExportEntry - exportInfoId int - usableByFileName tspath.Path - packages map[string]string - - globalTypingsCacheLocation string - - // !!! releaseSymbols func() - // !!! onFileChanged func(oldSourceFile *ast.SourceFile, newSourceFile *ast.SourceFile, typeAcquisitionEnabled bool) bool -} - -func (e *ExportInfoMap) clear() { - e.symbols = map[int]symbolExportEntry{} - e.exportInfo = collections.OrderedMap[lsproto.ExportInfoMapKey, []*CachedSymbolExportInfo]{} - e.usableByFileName = "" -} - -func (e *ExportInfoMap) get(importingFile tspath.Path, ch *checker.Checker, key lsproto.ExportInfoMapKey) []*SymbolExportInfo { - if e.usableByFileName != importingFile { - return nil - } - return core.Map(e.exportInfo.GetOrZero(key), func(info *CachedSymbolExportInfo) *SymbolExportInfo { return e.rehydrateCachedInfo(ch, info) }) -} - -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, - symbolNameMatch func(string) bool, - flagMatch func(ast.SymbolFlags) bool, -) { - if 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] - if symbolNameMatch != nil && !symbolNameMatch(symbolName) { - return - } - - capitalizedSymbolName := "" - if len(names) > 1 { - capitalizedSymbolName = names[1] - } - - moduleName := stringutil.StripQuotes(moduleSymbol.Name) - e.exportInfoId++ - id := e.exportInfoId - target := ch.SkipAlias(symbol) - - if flagMatch != nil && !flagMatch(target.Flags) { - return - } - - 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 - } - - moduleFileName := "" - if moduleFile != nil { - moduleFileName = moduleFile.FileName() - } - key := newExportInfoMapKey(symbolName, symbol, moduleKey, ch) - infos := e.exportInfo.GetOrZero(key) - infos = append(infos, &CachedSymbolExportInfo{ - id: id, - symbolTableKey: symbolTableKey, - symbolName: symbolName, - capitalizedSymbolName: capitalizedSymbolName, - moduleName: moduleName, - moduleFile: moduleFile, - moduleFileName: moduleFileName, - packageName: packageName, - - symbol: storedSymbol, - moduleSymbol: storedModuleSymbol, - exportKind: exportKind, - targetFlags: target.Flags, - isFromPackageJson: isFromPackageJson, - }) - e.exportInfo.Set(key, infos) -} - -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 lsproto.ExportInfoMapKey) []*SymbolExportInfo, -) []*SymbolExportInfo { - if importingFile != e.usableByFileName { - return nil - } - for key, info := range e.exportInfo.Entries() { - symbolName, ambientModuleName := key.SymbolName, key.AmbientModuleName - if preferCapitalized && info[0].capitalizedSymbolName != "" { - symbolName = info[0].capitalizedSymbolName - } - if matches(symbolName, 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, symbolName, 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: 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: 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, - 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 -} - -func (l *LanguageService) getImportCompletionAction( - ctx context.Context, - ch *checker.Checker, - targetSymbol *ast.Symbol, - moduleSymbol *ast.Symbol, - sourceFile *ast.SourceFile, - position int, - exportMapKey lsproto.ExportInfoMapKey, - symbolName string, - isJsxTagName bool, - // formatContext *formattingContext, -) (string, codeAction) { - var exportInfos []*SymbolExportInfo - // `exportMapKey` should be in the `itemData` of each auto-import completion entry and sent in resolving completion entry requests - exportInfos = l.getExportInfoMap(ctx, ch, sourceFile, exportMapKey) - if len(exportInfos) == 0 { - panic("Some exportInfo should match the specified exportMapKey") - } - - isValidTypeOnlyUseSite := ast.IsValidTypeOnlyAliasUseSite(astnav.GetTokenAtPosition(sourceFile, position)) - fix := l.getImportFixForSymbol(ch, sourceFile, exportInfos, position, ptrTo(isValidTypeOnlyUseSite)) - 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.moduleSpecifier, l.codeActionForFix(ctx, sourceFile, symbolName, fix /*includeSymbolNameInDescription*/, false) -} - -func NewExportInfoMap(globalsTypingCacheLocation string) *ExportInfoMap { - return &ExportInfoMap{ - packages: map[string]string{}, - symbols: map[int]symbolExportEntry{}, - exportInfo: collections.OrderedMap[lsproto.ExportInfoMapKey, []*CachedSymbolExportInfo]{}, - globalTypingsCacheLocation: globalsTypingCacheLocation, - } -} - -func (l *LanguageService) isImportable( - fromFile *ast.SourceFile, - toFile *ast.SourceFile, - toModule *ast.Symbol, - 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 := lsutil.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 - *lsutil.UserPreferences - l *LanguageService - filter *packageJsonImportFilter -} - -func (r *importSpecifierResolverForCompletions) packageJsonImportFilter() *packageJsonImportFilter { - if r.filter == nil { - r.filter = r.l.createPackageJsonImportFilter(r.SourceFile) - } - 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 - 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, false /* fromCacheOnly */) - return i.l.getBestFix(fixes, i.SourceFile, packageJsonImportFilter.allowsImportingSpecifier) -} - -func (l *LanguageService) getImportFixForSymbol( - ch *checker.Checker, - sourceFile *ast.SourceFile, - exportInfos []*SymbolExportInfo, - position int, - isValidTypeOnlySite *bool, -) *ImportFix { - if isValidTypeOnlySite == nil { - isValidTypeOnlySite = ptrTo(ast.IsValidTypeOnlyAliasUseSite(astnav.GetTokenAtPosition(sourceFile, position))) - } - useRequire := shouldUseRequire(sourceFile, l.GetProgram()) - packageJsonImportFilter := l.createPackageJsonImportFilter(sourceFile) - _, fixes := l.getImportFixes(ch, exportInfos, ptrTo(l.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(position))), isValidTypeOnlySite, &useRequire, sourceFile, false /* fromCacheOnly */) - return l.getBestFix(fixes, sourceFile, packageJsonImportFilter.allowsImportingSpecifier) -} - -func (l *LanguageService) getBestFix(fixes []*ImportFix, sourceFile *ast.SourceFile, allowsImportingSpecifier func(moduleSpecifier string) bool) *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] - } - - best := fixes[0] - for _, fix := range fixes { - // Takes true branch of conditional if `fix` is better than `best` - if l.compareModuleSpecifiers( - fix, - best, - sourceFile, - allowsImportingSpecifier, - func(fileName string) tspath.Path { - return tspath.ToPath(fileName, l.GetProgram().GetCurrentDirectory(), l.GetProgram().UseCaseSensitiveFileNames()) - }, - ) < 0 { - best = fix - } - } - - return best -} - -// 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 (l *LanguageService) compareModuleSpecifiers( - a *ImportFix, // !!! ImportFixWithModuleSpecifier - b *ImportFix, // !!! ImportFixWithModuleSpecifier - importingFile *ast.SourceFile, // | FutureSourceFile, - allowsImportingSpecifier func(specifier string) bool, - toPath func(fileName string) tspath.Path, -) int { - if a.kind != ImportFixKindUseNamespace && b.kind != ImportFixKindUseNamespace { - if comparison := core.CompareBooleans( - b.moduleSpecifierKind != modulespecifiers.ResultKindNodeModules || allowsImportingSpecifier(b.moduleSpecifier), - a.moduleSpecifierKind != modulespecifiers.ResultKindNodeModules || allowsImportingSpecifier(a.moduleSpecifier), - ); comparison != 0 { - return comparison - } - if comparison := compareModuleSpecifierRelativity(a, b, l.UserPreferences()); comparison != 0 { - return comparison - } - if comparison := compareNodeCoreModuleSpecifiers(a.moduleSpecifier, b.moduleSpecifier, importingFile, l.GetProgram()); comparison != 0 { - return comparison - } - if comparison := core.CompareBooleans(isFixPossiblyReExportingImportingFile(a, importingFile.Path(), toPath), isFixPossiblyReExportingImportingFile(b, importingFile.Path(), toPath)); comparison != 0 { - return comparison - } - if comparison := tspath.CompareNumberOfDirectorySeparators(a.moduleSpecifier, b.moduleSpecifier); comparison != 0 { - return comparison - } - } - return 0 -} - -func compareNodeCoreModuleSpecifiers(a, b string, importingFile *ast.SourceFile, program *compiler.Program) int { - if strings.HasPrefix(a, "node:") && !strings.HasPrefix(b, "node:") { - if lsutil.ShouldUseUriStyleNodeCoreModules(importingFile, program) { - return -1 - } - return 1 - } - if strings.HasPrefix(b, "node:") && !strings.HasPrefix(a, "node:") { - if lsutil.ShouldUseUriStyleNodeCoreModules(importingFile, program) { - return 1 - } - return -1 - } - return 0 -} - -// This is a simple heuristic to try to avoid creating an import cycle with a barrel re-export. -// E.g., do not `import { Foo } from ".."` when you could `import { Foo } from "../Foo"`. -// This can produce false positives or negatives if re-exports cross into sibling directories -// (e.g. `export * from "../whatever"`) or are not named "index". -func isFixPossiblyReExportingImportingFile(fix *ImportFix, importingFilePath tspath.Path, toPath func(fileName string) tspath.Path) bool { - if fix.isReExport != nil && *(fix.isReExport) && - fix.exportInfo != nil && fix.exportInfo.moduleFileName != "" && isIndexFileName(fix.exportInfo.moduleFileName) { - reExportDir := toPath(tspath.GetDirectoryPath(fix.exportInfo.moduleFileName)) - return strings.HasPrefix(string(importingFilePath), string(reExportDir)) - } - return false -} - -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" -} - -// returns `-1` if `a` is better than `b` -func compareModuleSpecifierRelativity(a *ImportFix, b *ImportFix, preferences *lsutil.UserPreferences) int { - switch preferences.ImportModuleSpecifierPreference { - case modulespecifiers.ImportModuleSpecifierPreferenceNonRelative, modulespecifiers.ImportModuleSpecifierPreferenceProjectRelative: - return core.CompareBooleans(a.moduleSpecifierKind == modulespecifiers.ResultKindRelative, b.moduleSpecifierKind == modulespecifiers.ResultKindRelative) - } - return 0 -} - -func (l *LanguageService) getImportFixes( - ch *checker.Checker, - exportInfos []*SymbolExportInfo, // | FutureSymbolExportInfo[], - usagePosition *lsproto.Position, - isValidTypeOnlyUseSite *bool, - useRequire *bool, - sourceFile *ast.SourceFile, // | FutureSourceFile, - // importMap *importMap, - fromCacheOnly bool, -) (int, []*ImportFix) { - // if importMap == nil { && !!! isFullSourceFile(sourceFile) - importMap := createExistingImportMap(sourceFile, l.GetProgram(), ch) - var existingImports []*FixAddToExistingImportInfo - if importMap != nil { - existingImports = 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, - fromCacheOnly, - ) - computedWithoutCacheCount := 0 - // if result.computedWithoutCacheCount != nil { - // computedWithoutCacheCount = *result.computedWithoutCacheCount - // } - return computedWithoutCacheCount, append(useNamespace, result...) -} - -func (l *LanguageService) createPackageJsonImportFilter(fromFile *ast.SourceFile) *packageJsonImportFilter { - // !!! The program package.json cache may not have every relevant package.json. - // This should eventually be integrated with the session. - var packageJsons []*packagejson.PackageJson - dir := tspath.GetDirectoryPath(fromFile.FileName()) - for { - packageJsonDir := l.GetProgram().GetNearestAncestorDirectoryWithPackageJson(dir) - if packageJsonDir == "" { - break - } - if packageJson := l.GetProgram().GetPackageJsonInfo(tspath.CombinePaths(packageJsonDir, "package.json")).GetContents(); packageJson != nil && packageJson.Parseable { - packageJsons = append(packageJsons, packageJson) - } - dir = tspath.GetDirectoryPath(packageJsonDir) - if dir == packageJsonDir { - break - } - } - - 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.HasDependency(packageName) || packageJson.HasDependency(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.program.Options(), - fromFile, - importedFileName, - moduleSpecifierResolutionHost, - l.UserPreferences().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 nil - } - - // 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 nil - } - - 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) *ImportFix { - // 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 - } - namespacePrefix := getNamespaceLikeImportText(existingImport.declaration) - moduleSpecifier := checker.TryGetModuleSpecifierFromDeclaration(existingImport.declaration) - if namespacePrefix != "" && moduleSpecifier != nil && moduleSpecifier.Text() != "" { - return getUseNamespaceImport( - moduleSpecifier.Text(), - modulespecifiers.ResultKindNone, - namespacePrefix, - position, - ) - } - } - return nil -} - -func getNamespaceLikeImportText(declaration *ast.Statement) string { - switch declaration.Kind { - case ast.KindVariableDeclaration: - name := declaration.Name() - if name != nil && name.Kind == ast.KindIdentifier { - return name.Text() - } - return "" - case ast.KindImportEqualsDeclaration: - return declaration.Name().Text() - case ast.KindJSDocImportTag, ast.KindImportDeclaration: - importClause := declaration.ImportClause() - if importClause != nil && importClause.AsImportClause().NamedBindings != nil && importClause.AsImportClause().NamedBindings.Kind == ast.KindNamespaceImport { - return importClause.AsImportClause().NamedBindings.Name().Text() - } - return "" - default: - debug.AssertNever(declaration) - return "" - } -} - -func tryAddToExistingImport(existingImports []*FixAddToExistingImportInfo, isValidTypeOnlyUseSite *bool, ch *checker.Checker, compilerOptions *core.CompilerOptions) *ImportFix { - var best *ImportFix - - 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) *ImportFix { - 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 := info.declaration.ImportClause() - if importClause == nil || !ast.IsStringLiteralLike(info.declaration.ModuleSpecifier()) { - return nil - } - namedBindings := importClause.AsImportClause().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, - fromCacheOnly bool, -) []*ImportFix { - // tries to create a new import statement using an existing import specifier - var importWithExistingSpecifier *ImportFix - - 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, fromCacheOnly) -} - -func (l *LanguageService) getNewImportFixes( - ch *checker.Checker, - sourceFile *ast.SourceFile, // | FutureSourceFile, - usagePosition *lsproto.Position, - isValidTypeOnlyUseSite bool, - useRequire bool, - exportInfos []*SymbolExportInfo, // !!! (SymbolExportInfo | FutureSymbolExportInfo)[], - 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(), l.UserPreferences().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 shouldUseRequire( - 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.ModuleKindCommonJS: - if tspath.HasJSFileExtension(importingFile.FileName()) && (importingFile.ExternalModuleIndicator != nil || forceImportKeyword) { - return ImportKindNamespace - } - return ImportKindCommonJS - case 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.ModuleKindNode20, 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 forEachExternalModuleToImportFrom( - ch *checker.Checker, - program *compiler.Program, - preferences *lsutil.UserPreferences, - // useAutoImportProvider bool, - cb func(module *ast.Symbol, moduleFile *ast.SourceFile, checker *checker.Checker, isFromPackageJson bool), -) { - var excludePatterns []*regexp2.Regexp - if preferences.AutoImportFileExcludePatterns != nil { - excludePatterns = getIsExcludedPatterns(preferences, program.UseCaseSensitiveFileNames()) - } - - forEachExternalModule( - ch, - program.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 getIsExcludedPatterns(preferences *lsutil.UserPreferences, useCaseSensitiveFileNames bool) []*regexp2.Regexp { - if preferences.AutoImportFileExcludePatterns == nil { - return nil - } - var patterns []*regexp2.Regexp - for _, spec := range preferences.AutoImportFileExcludePatterns { - pattern := vfs.GetSubPatternFromSpec(spec, "", vfs.UsageExclude, vfs.WildcardMatcher{}) - if pattern != "" { - if re := vfs.GetRegexFromPattern(pattern, useCaseSensitiveFileNames); re != nil { - patterns = append(patterns, re) - } - } - } - return patterns -} - -func forEachExternalModule( - ch *checker.Checker, - allSourceFiles []*ast.SourceFile, - excludePatterns []*regexp2.Regexp, - cb func(moduleSymbol *ast.Symbol, sourceFile *ast.SourceFile), -) { - var isExcluded func(*ast.SourceFile) bool = func(_ *ast.SourceFile) bool { return false } - if excludePatterns != nil { - isExcluded = getIsExcluded(excludePatterns) - } - - for _, ambient := range ch.GetAmbientModules() { - if !strings.Contains(ambient.Name, "*") && !(excludePatterns != nil && core.Every(ambient.Declarations, func(d *ast.Node) bool { - return isExcluded(ast.GetSourceFileOfNode(d)) - })) { - cb(ambient, nil /*sourceFile*/) - } - } - for _, sourceFile := range allSourceFiles { - if ast.IsExternalOrCommonJSModule(sourceFile) && !isExcluded(sourceFile) { - cb(ch.GetMergedSymbol(sourceFile.Symbol), sourceFile) - } - } -} - -func getIsExcluded(excludePatterns []*regexp2.Regexp) func(sourceFile *ast.SourceFile) bool { - // !!! SymlinkCache - // const realpathsWithSymlinks = host.getSymlinkCache?.().getSymlinkedDirectoriesByRealpath(); - return func(sourceFile *ast.SourceFile) bool { - fileName := sourceFile.FileName() - for _, p := range excludePatterns { - if matched, _ := p.MatchString(fileName); matched { - return true - } - } - // !! SymlinkCache - // if (realpathsWithSymlinks?.size && pathContainsNodeModules(fileName)) { - // let dir = getDirectoryPath(fileName); - // return forEachAncestorDirectoryStoppingAtGlobalCache( - // host, - // getDirectoryPath(path), - // dirPath => { - // const symlinks = realpathsWithSymlinks.get(ensureTrailingDirectorySeparator(dirPath)); - // if (symlinks) { - // return symlinks.some(s => excludePatterns.some(p => p.test(fileName.replace(dir, s)))); - // } - // dir = getDirectoryPath(dir); - // }, - // ) ?? false; - // } - return false - } -} - -// ======================== generate code actions ======================= - -func (l *LanguageService) codeActionForFix( - ctx context.Context, - sourceFile *ast.SourceFile, - symbolName string, - fix *ImportFix, - includeSymbolNameInDescription bool, -) codeAction { - tracker := change.NewTracker(ctx, l.GetProgram().Options(), l.FormatOptions(), l.converters) // !!! changetracker.with - diag := l.codeActionForFixWorker(ctx, tracker, sourceFile, symbolName, fix, includeSymbolNameInDescription) - changes := tracker.GetChanges()[sourceFile.FileName()] - return codeAction{description: diag, changes: changes} -} - -func (l *LanguageService) codeActionForFixWorker( - ctx context.Context, - changeTracker *change.Tracker, - sourceFile *ast.SourceFile, - symbolName string, - fix *ImportFix, - includeSymbolNameInDescription bool, -) string { - locale := locale.FromContext(ctx) - - switch fix.kind { - case ImportFixKindUseNamespace: - addNamespaceQualifier(changeTracker, sourceFile, fix.qualification()) - return diagnostics.Change_0_to_1.Localize(locale, symbolName, fmt.Sprintf("%s.%s", *fix.namespacePrefix, symbolName)) - case ImportFixKindJsdocTypeImport: - if fix.usagePosition == nil { - return "" - } - quotePreference := getQuotePreference(sourceFile, l.UserPreferences()) - quoteChar := "\"" - if quotePreference == quotePreferenceSingle { - quoteChar = "'" - } - importTypePrefix := fmt.Sprintf("import(%s%s%s).", quoteChar, fix.moduleSpecifier, quoteChar) - changeTracker.InsertText(sourceFile, *fix.usagePosition, importTypePrefix) - return diagnostics.Change_0_to_1.Localize(locale, symbolName, importTypePrefix+symbolName) - case ImportFixKindAddToExisting: - var defaultImport *Import - var namedImports []*Import - if fix.importKind == ImportKindDefault { - defaultImport = &Import{name: symbolName, addAsTypeOnly: fix.addAsTypeOnly} - } else if fix.importKind == ImportKindNamed { - namedImports = []*Import{{name: symbolName, addAsTypeOnly: fix.addAsTypeOnly, propertyName: fix.propertyName}} - } - l.doAddExistingFix( - changeTracker, - sourceFile, - fix.importClauseOrBindingPattern, - defaultImport, - namedImports, - ) - moduleSpecifierWithoutQuotes := stringutil.StripQuotes(fix.moduleSpecifier) - if includeSymbolNameInDescription { - return diagnostics.Import_0_from_1.Localize(locale, symbolName, moduleSpecifierWithoutQuotes) - } - return diagnostics.Update_import_from_0.Localize(locale, moduleSpecifierWithoutQuotes) - case ImportFixKindAddNew: - var declarations []*ast.Statement - var defaultImport *Import - var namedImports []*Import - var namespaceLikeImport *Import - if fix.importKind == ImportKindDefault { - defaultImport = &Import{name: symbolName, addAsTypeOnly: fix.addAsTypeOnly} - } else if fix.importKind == ImportKindNamed { - namedImports = []*Import{{name: symbolName, addAsTypeOnly: fix.addAsTypeOnly, propertyName: fix.propertyName}} - } - qualification := fix.qualification() - if fix.importKind == ImportKindNamespace || fix.importKind == ImportKindCommonJS { - namespaceLikeImport = &Import{kind: fix.importKind, addAsTypeOnly: fix.addAsTypeOnly, name: symbolName} - if qualification != nil && qualification.namespacePrefix != "" { - namespaceLikeImport.name = qualification.namespacePrefix - } - } - - if fix.useRequire { - declarations = getNewRequires(changeTracker, fix.moduleSpecifier, getQuotePreference(sourceFile, l.UserPreferences()), defaultImport, namedImports, namespaceLikeImport, l.GetProgram().Options()) - } else { - declarations = l.getNewImports(changeTracker, fix.moduleSpecifier, getQuotePreference(sourceFile, l.UserPreferences()), defaultImport, namedImports, namespaceLikeImport, l.GetProgram().Options()) - } - - l.insertImports( - changeTracker, - sourceFile, - declarations, - /*blankLineBetween*/ true, - ) - if qualification != nil { - addNamespaceQualifier(changeTracker, sourceFile, qualification) - } - if includeSymbolNameInDescription { - return diagnostics.Import_0_from_1.Localize(locale, symbolName, fix.moduleSpecifier) - } - return diagnostics.Add_import_from_0.Localize(locale, fix.moduleSpecifier) - case ImportFixKindPromoteTypeOnly: - promotedDeclaration := promoteFromTypeOnly(changeTracker, fix.typeOnlyAliasDeclaration, l.GetProgram(), sourceFile, l) - if promotedDeclaration.Kind == ast.KindImportSpecifier { - moduleSpec := getModuleSpecifierText(promotedDeclaration.Parent.Parent) - return diagnostics.Remove_type_from_import_of_0_from_1.Localize(locale, symbolName, moduleSpec) - } - moduleSpec := getModuleSpecifierText(promotedDeclaration) - return diagnostics.Remove_type_from_import_declaration_from_0.Localize(locale, moduleSpec) - default: - panic(fmt.Sprintf(`Unexpected fix kind %v`, fix.kind)) - } -} - -func getNewRequires( - changeTracker *change.Tracker, - moduleSpecifier string, - quotePreference quotePreference, - defaultImport *Import, - namedImports []*Import, - namespaceLikeImport *Import, - compilerOptions *core.CompilerOptions, -) []*ast.Statement { - quotedModuleSpecifier := changeTracker.NodeFactory.NewStringLiteral( - moduleSpecifier, - core.IfElse(quotePreference == quotePreferenceSingle, ast.TokenFlagsSingleQuote, ast.TokenFlagsNone), - ) - var statements []*ast.Statement - - // const { default: foo, bar, etc } = require('./mod'); - if defaultImport != nil || len(namedImports) > 0 { - bindingElements := []*ast.Node{} - for _, namedImport := range namedImports { - var propertyName *ast.Node - if namedImport.propertyName != "" { - propertyName = changeTracker.NodeFactory.NewIdentifier(namedImport.propertyName) - } - bindingElements = append(bindingElements, changeTracker.NodeFactory.NewBindingElement( - /*dotDotDotToken*/ nil, - propertyName, - changeTracker.NodeFactory.NewIdentifier(namedImport.name), - /*initializer*/ nil, - )) - } - if defaultImport != nil { - bindingElements = append([]*ast.Node{ - changeTracker.NodeFactory.NewBindingElement( - /*dotDotDotToken*/ nil, - changeTracker.NodeFactory.NewIdentifier("default"), - changeTracker.NodeFactory.NewIdentifier(defaultImport.name), - /*initializer*/ nil, - ), - }, bindingElements...) - } - declaration := createConstEqualsRequireDeclaration( - changeTracker, - changeTracker.NodeFactory.NewBindingPattern( - ast.KindObjectBindingPattern, - changeTracker.NodeFactory.NewNodeList(bindingElements), - ), - quotedModuleSpecifier, - ) - statements = append(statements, declaration) - } - - // const foo = require('./mod'); - if namespaceLikeImport != nil { - declaration := createConstEqualsRequireDeclaration( - changeTracker, - changeTracker.NodeFactory.NewIdentifier(namespaceLikeImport.name), - quotedModuleSpecifier, - ) - statements = append(statements, declaration) - } - - debug.AssertIsDefined(statements) - return statements -} - -func createConstEqualsRequireDeclaration(changeTracker *change.Tracker, name *ast.Node, quotedModuleSpecifier *ast.Node) *ast.Statement { - return changeTracker.NodeFactory.NewVariableStatement( - /*modifiers*/ nil, - changeTracker.NodeFactory.NewVariableDeclarationList( - ast.NodeFlagsConst, - changeTracker.NodeFactory.NewNodeList([]*ast.Node{ - changeTracker.NodeFactory.NewVariableDeclaration( - name, - /*exclamationToken*/ nil, - /*type*/ nil, - changeTracker.NodeFactory.NewCallExpression( - changeTracker.NodeFactory.NewIdentifier("require"), - /*questionDotToken*/ nil, - /*typeArguments*/ nil, - changeTracker.NodeFactory.NewNodeList([]*ast.Node{quotedModuleSpecifier}), - ast.NodeFlagsNone, - ), - ), - }), - ), - ) -} - -func getModuleSpecifierText(promotedDeclaration *ast.Node) string { - if promotedDeclaration.Kind == ast.KindImportEqualsDeclaration { - importEqualsDeclaration := promotedDeclaration.AsImportEqualsDeclaration() - if ast.IsExternalModuleReference(importEqualsDeclaration.ModuleReference) { - expr := importEqualsDeclaration.ModuleReference.Expression() - if expr != nil && expr.Kind == ast.KindStringLiteral { - return expr.Text() - } - - } - return importEqualsDeclaration.ModuleReference.Text() - } - return promotedDeclaration.Parent.ModuleSpecifier().Text() -} - -func ptrTo[T any](v T) *T { - return &v -} diff --git a/internal/ls/autoimportsexportinfo.go b/internal/ls/autoimportsexportinfo.go deleted file mode 100644 index ab3fe4a8c3..0000000000 --- a/internal/ls/autoimportsexportinfo.go +++ /dev/null @@ -1,181 +0,0 @@ -package ls - -import ( - "context" - - "github.com/microsoft/typescript-go/internal/ast" - "github.com/microsoft/typescript-go/internal/checker" - "github.com/microsoft/typescript-go/internal/collections" - "github.com/microsoft/typescript-go/internal/core" - "github.com/microsoft/typescript-go/internal/lsp/lsproto" - "github.com/microsoft/typescript-go/internal/scanner" - "github.com/microsoft/typescript-go/internal/stringutil" -) - -func (l *LanguageService) getExportInfoMap( - ctx context.Context, - ch *checker.Checker, - importingFile *ast.SourceFile, - exportMapKey lsproto.ExportInfoMapKey, -) []*SymbolExportInfo { - expInfoMap := NewExportInfoMap(l.GetProgram().GetGlobalTypingsCacheLocation()) - moduleCount := 0 - symbolNameMatch := func(symbolName string) bool { - return symbolName == exportMapKey.SymbolName - } - forEachExternalModuleToImportFrom( - ch, - l.GetProgram(), - l.UserPreferences(), - // /*useAutoImportProvider*/ true, - func(moduleSymbol *ast.Symbol, moduleFile *ast.SourceFile, ch *checker.Checker, isFromPackageJson bool) { - if moduleCount = moduleCount + 1; moduleCount%100 == 0 && ctx.Err() != nil { - return - } - if moduleFile == nil && stringutil.StripQuotes(moduleSymbol.Name) != exportMapKey.AmbientModuleName { - return - } - seenExports := collections.Set[string]{} - defaultInfo := getDefaultLikeExportInfo(moduleSymbol, ch) - var exportingModuleSymbol *ast.Symbol - if defaultInfo != nil { - exportingModuleSymbol = defaultInfo.exportingModuleSymbol - // 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 isImportableSymbol(exportingModuleSymbol, ch) { - expInfoMap.add( - importingFile.Path(), - exportingModuleSymbol, - core.IfElse(defaultInfo.exportKind == ExportKindDefault, ast.InternalSymbolNameDefault, ast.InternalSymbolNameExportEquals), - moduleSymbol, - moduleFile, - defaultInfo.exportKind, - isFromPackageJson, - ch, - symbolNameMatch, - nil, - ) - } - } - ch.ForEachExportAndPropertyOfModule(moduleSymbol, func(exported *ast.Symbol, key string) { - if exported != exportingModuleSymbol && isImportableSymbol(exported, ch) && seenExports.AddIfAbsent(key) { - expInfoMap.add( - importingFile.Path(), - exported, - key, - moduleSymbol, - moduleFile, - ExportKindNamed, - isFromPackageJson, - ch, - symbolNameMatch, - nil, - ) - } - }) - }) - return expInfoMap.get(importingFile.Path(), ch, exportMapKey) -} - -func (l *LanguageService) searchExportInfosForCompletions( - ctx context.Context, - ch *checker.Checker, - importingFile *ast.SourceFile, - isForImportStatementCompletion bool, - isRightOfOpenTag bool, - isTypeOnlyLocation bool, - lowerCaseTokenText string, - action func([]*SymbolExportInfo, string, bool, lsproto.ExportInfoMapKey) []*SymbolExportInfo, -) { - symbolNameMatches := map[string]bool{} - symbolNameMatch := func(symbolName string) bool { - if !scanner.IsIdentifierText(symbolName, importingFile.LanguageVariant) { - return false - } - if b, ok := symbolNameMatches[symbolName]; ok { - return b - } - if isNonContextualKeyword(scanner.StringToToken(symbolName)) { - symbolNameMatches[symbolName] = false - 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') { - symbolNameMatches[symbolName] = false - return false - } - - symbolNameMatches[symbolName] = charactersFuzzyMatchInString(symbolName, lowerCaseTokenText) - return symbolNameMatches[symbolName] - } - flagMatch := func(targetFlags ast.SymbolFlags) bool { - if !isTypeOnlyLocation && !isForImportStatementCompletion && (targetFlags&ast.SymbolFlagsValue) == 0 { - return false - } - if isTypeOnlyLocation && (targetFlags&(ast.SymbolFlagsModule|ast.SymbolFlagsType) == 0) { - return false - } - return true - } - - expInfoMap := NewExportInfoMap(l.GetProgram().GetGlobalTypingsCacheLocation()) - moduleCount := 0 - forEachExternalModuleToImportFrom( - ch, - l.GetProgram(), - l.UserPreferences(), - // /*useAutoImportProvider*/ true, - func(moduleSymbol *ast.Symbol, moduleFile *ast.SourceFile, ch *checker.Checker, isFromPackageJson bool) { - if moduleCount = moduleCount + 1; moduleCount%100 == 0 && ctx.Err() != nil { - 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, - symbolNameMatch, - flagMatch, - ) - } - var exportingModuleSymbol *ast.Symbol - if defaultInfo != nil { - exportingModuleSymbol = defaultInfo.exportingModuleSymbol - } - ch.ForEachExportAndPropertyOfModule(moduleSymbol, func(exported *ast.Symbol, key string) { - if exported != exportingModuleSymbol && isImportableSymbol(exported, ch) && seenExports.AddIfAbsent(key) { - expInfoMap.add( - importingFile.Path(), - exported, - key, - moduleSymbol, - moduleFile, - ExportKindNamed, - isFromPackageJson, - ch, - symbolNameMatch, - flagMatch, - ) - } - }) - }) - expInfoMap.search( - ch, - importingFile.Path(), - /*preferCapitalized*/ isRightOfOpenTag, - func(symbolName string, targetFlags ast.SymbolFlags) bool { - return symbolNameMatch(symbolName) && flagMatch(targetFlags) - }, - action, - ) -} diff --git a/internal/ls/autoimportstypes.go b/internal/ls/autoimportstypes.go deleted file mode 100644 index 3ac6ba6417..0000000000 --- a/internal/ls/autoimportstypes.go +++ /dev/null @@ -1,215 +0,0 @@ -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" -) - -//go:generate go tool golang.org/x/tools/cmd/stringer -type=ExportKind -output=autoImports_stringer_generated.go -//go:generate go tool mvdan.cc/gofumpt -w autoImports_stringer_generated.go - -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 -) - -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 ImportFix struct { - kind ImportFixKind - isReExport *bool - exportInfo *SymbolExportInfo // !!! | FutureSymbolExportInfo | undefined - moduleSpecifierKind modulespecifiers.ResultKind - moduleSpecifier string - usagePosition *lsproto.Position - namespacePrefix *string - - importClauseOrBindingPattern *ast.Node // ImportClause | ObjectBindingPattern - importKind ImportKind // ImportKindDefault | ImportKindNamed - addAsTypeOnly AddAsTypeOnly - propertyName string // !!! not implemented - - useRequire bool - - typeOnlyAliasDeclaration *ast.Declaration // TypeOnlyAliasDeclaration -} - -func (i *ImportFix) qualification() *Qualification { - switch i.kind { - case ImportFixKindAddNew: - if i.usagePosition == nil || strPtrIsEmpty(i.namespacePrefix) { - return nil - } - fallthrough - case ImportFixKindUseNamespace: - return &Qualification{ - usagePosition: *i.usagePosition, - namespacePrefix: *i.namespacePrefix, - } - } - panic(fmt.Sprintf("no qualification with ImportFixKind %v", i.kind)) -} - -type Qualification struct { - usagePosition lsproto.Position - namespacePrefix string -} - -func getUseNamespaceImport( - moduleSpecifier string, - moduleSpecifierKind modulespecifiers.ResultKind, - namespacePrefix string, - usagePosition lsproto.Position, -) *ImportFix { - return &ImportFix{ - kind: ImportFixKindUseNamespace, - moduleSpecifierKind: moduleSpecifierKind, - moduleSpecifier: moduleSpecifier, - - usagePosition: ptrTo(usagePosition), - namespacePrefix: strPtrTo(namespacePrefix), - } -} - -func getAddJsdocTypeImport( - moduleSpecifier string, - moduleSpecifierKind modulespecifiers.ResultKind, - usagePosition *lsproto.Position, - exportInfo *SymbolExportInfo, - isReExport *bool, -) *ImportFix { - return &ImportFix{ - kind: ImportFixKindJsdocTypeImport, - isReExport: isReExport, - exportInfo: exportInfo, - moduleSpecifierKind: moduleSpecifierKind, - moduleSpecifier: moduleSpecifier, - usagePosition: usagePosition, - } -} - -func getAddToExistingImport( - importClauseOrBindingPattern *ast.Node, - importKind ImportKind, - moduleSpecifier string, - moduleSpecifierKind modulespecifiers.ResultKind, - addAsTypeOnly AddAsTypeOnly, -) *ImportFix { - return &ImportFix{ - kind: ImportFixKindAddToExisting, - moduleSpecifierKind: moduleSpecifierKind, - moduleSpecifier: moduleSpecifier, - importClauseOrBindingPattern: importClauseOrBindingPattern, - importKind: importKind, - addAsTypeOnly: addAsTypeOnly, - } -} - -func getNewAddNewImport( - moduleSpecifier string, - moduleSpecifierKind modulespecifiers.ResultKind, - importKind ImportKind, - useRequire bool, - addAsTypeOnly AddAsTypeOnly, - exportInfo *SymbolExportInfo, // !!! | FutureSymbolExportInfo - isReExport *bool, - qualification *Qualification, -) *ImportFix { - return &ImportFix{ - kind: ImportFixKindAddNew, - isReExport: isReExport, - exportInfo: exportInfo, - moduleSpecifierKind: modulespecifiers.ResultKindNone, - moduleSpecifier: moduleSpecifier, - importKind: importKind, - addAsTypeOnly: addAsTypeOnly, - useRequire: useRequire, - } -} - -func getNewPromoteTypeOnlyImport(typeOnlyAliasDeclaration *ast.Declaration) *ImportFix { - // !!! function stub - return &ImportFix{ - kind: ImportFixKindPromoteTypeOnly, - // isReExport *bool - // exportInfo *SymbolExportInfo // !!! | FutureSymbolExportInfo | undefined - // moduleSpecifierKind modulespecifiers.ResultKind - // moduleSpecifier string - typeOnlyAliasDeclaration: typeOnlyAliasDeclaration, - } -} - -/** Information needed to augment an existing import declaration. */ -// !!! after full implementation, 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, -) *ImportFix { - 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/codeactions.go b/internal/ls/codeactions.go index f843c49aeb..c581252bd7 100644 --- a/internal/ls/codeactions.go +++ b/internal/ls/codeactions.go @@ -13,9 +13,9 @@ import ( // CodeFixProvider represents a provider for a specific type of code fix type CodeFixProvider struct { ErrorCodes []int32 - GetCodeActions func(ctx context.Context, fixContext *CodeFixContext) []CodeAction + GetCodeActions func(ctx context.Context, fixContext *CodeFixContext) ([]CodeAction, error) FixIds []string - GetAllCodeActions func(ctx context.Context, fixContext *CodeFixContext) *CombinedCodeActions + GetAllCodeActions func(ctx context.Context, fixContext *CodeFixContext) (*CombinedCodeActions, error) } // CodeFixContext contains the context needed to generate code fixes @@ -82,7 +82,10 @@ func (l *LanguageService) ProvideCodeActions(ctx context.Context, params *lsprot } // Get code actions from the provider - providerActions := provider.GetCodeActions(ctx, fixContext) + providerActions, err := provider.GetCodeActions(ctx, fixContext) + if err != nil { + return lsproto.CodeActionResponse{}, err + } for _, action := range providerActions { actions = append(actions, convertToLSPCodeAction(&action, diag, params.TextDocument.Uri)) } diff --git a/internal/ls/codeactions_importfixes.go b/internal/ls/codeactions_importfixes.go index 49939e7cc6..7be81a91d6 100644 --- a/internal/ls/codeactions_importfixes.go +++ b/internal/ls/codeactions_importfixes.go @@ -1,26 +1,18 @@ package ls import ( - "cmp" "context" - "fmt" "slices" - "strings" "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/astnav" "github.com/microsoft/typescript-go/internal/checker" - "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/compiler" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/diagnostics" - "github.com/microsoft/typescript-go/internal/ls/change" - "github.com/microsoft/typescript-go/internal/ls/lsutil" - "github.com/microsoft/typescript-go/internal/ls/organizeimports" + "github.com/microsoft/typescript-go/internal/ls/autoimport" "github.com/microsoft/typescript-go/internal/lsp/lsproto" - "github.com/microsoft/typescript-go/internal/outputpaths" "github.com/microsoft/typescript-go/internal/scanner" - "github.com/microsoft/typescript-go/internal/tspath" ) var importFixErrorCodes = []int32{ @@ -59,56 +51,51 @@ var ImportFixProvider = &CodeFixProvider{ } type fixInfo struct { - fix *ImportFix + fix *autoimport.Fix symbolName string errorIdentifierText string isJsxNamespaceFix bool } -func getImportCodeActions(ctx context.Context, fixContext *CodeFixContext) []CodeAction { - info := getFixInfos(ctx, fixContext, fixContext.ErrorCode, fixContext.Span.Pos(), true /* useAutoImportProvider */) +func getImportCodeActions(ctx context.Context, fixContext *CodeFixContext) ([]CodeAction, error) { + info, err := getFixInfos(ctx, fixContext, fixContext.ErrorCode, fixContext.Span.Pos()) + if err != nil { + return nil, err + } if len(info) == 0 { - return nil + return nil, nil } var actions []CodeAction for _, fixInfo := range info { - tracker := change.NewTracker(ctx, fixContext.Program.Options(), fixContext.LS.FormatOptions(), fixContext.LS.converters) - msg := fixContext.LS.codeActionForFixWorker( + edits, description := fixInfo.fix.Edits( ctx, - tracker, fixContext.SourceFile, - fixInfo.symbolName, - fixInfo.fix, - fixInfo.symbolName != fixInfo.errorIdentifierText, + fixContext.Program.Options(), + fixContext.LS.FormatOptions(), + fixContext.LS.converters, + fixContext.LS.UserPreferences(), ) - if msg != "" { - // Convert changes to LSP edits - changes := tracker.GetChanges() - var edits []*lsproto.TextEdit - for _, fileChanges := range changes { - edits = append(edits, fileChanges...) - } - - actions = append(actions, CodeAction{ - Description: msg, - Changes: edits, - }) - } + actions = append(actions, CodeAction{ + Description: description, + Changes: edits, + }) } - return actions + return actions, nil } -func getFixInfos(ctx context.Context, fixContext *CodeFixContext, errorCode int32, pos int, useAutoImportProvider bool) []*fixInfo { +func getFixInfos(ctx context.Context, fixContext *CodeFixContext, errorCode int32, pos int) ([]*fixInfo, error) { symbolToken := astnav.GetTokenAtPosition(fixContext.SourceFile, pos) + var view *autoimport.View var info []*fixInfo if errorCode == diagnostics.X_0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.Code() { - info = getFixesInfoForUMDImport(ctx, fixContext, symbolToken) + view = fixContext.LS.getCurrentAutoImportView(fixContext.SourceFile) + info = getFixesInfoForUMDImport(ctx, fixContext, symbolToken, view) } else if !ast.IsIdentifier(symbolToken) { - return nil + return nil, nil } else if errorCode == diagnostics.X_0_cannot_be_used_as_a_value_because_it_was_imported_using_import_type.Code() { // Handle type-only import promotion ch, done := fixContext.Program.GetTypeChecker(ctx) @@ -121,18 +108,26 @@ func getFixInfos(ctx context.Context, fixContext *CodeFixContext, errorCode int3 symbolName := symbolNames[0] fix := getTypeOnlyPromotionFix(ctx, fixContext.SourceFile, symbolToken, symbolName, fixContext.Program) if fix != nil { - return []*fixInfo{{fix: fix, symbolName: symbolName, errorIdentifierText: symbolToken.Text()}} + return []*fixInfo{{fix: fix, symbolName: symbolName, errorIdentifierText: symbolToken.Text()}}, nil } - return nil + return nil, nil } else { - info = getFixesInfoForNonUMDImport(ctx, fixContext, symbolToken, useAutoImportProvider) + var err error + view, err = fixContext.LS.getPreparedAutoImportView(fixContext.SourceFile) + if err != nil { + return nil, err + } + info = getFixesInfoForNonUMDImport(ctx, fixContext, symbolToken, view) } // Sort fixes by preference - return sortFixInfo(info, fixContext) + if view == nil { + view = fixContext.LS.getCurrentAutoImportView(fixContext.SourceFile) + } + return sortFixInfo(info, fixContext, view), nil } -func getFixesInfoForUMDImport(ctx context.Context, fixContext *CodeFixContext, token *ast.Node) []*fixInfo { +func getFixesInfoForUMDImport(ctx context.Context, fixContext *CodeFixContext, token *ast.Node, view *autoimport.View) []*fixInfo { ch, done := fixContext.Program.GetTypeChecker(ctx) defer done() @@ -141,43 +136,18 @@ func getFixesInfoForUMDImport(ctx context.Context, fixContext *CodeFixContext, t return nil } - symbol := ch.GetAliasedSymbol(umdSymbol) - symbolName := umdSymbol.Name - exportInfo := []*SymbolExportInfo{{ - symbol: umdSymbol, - moduleSymbol: symbol, - moduleFileName: "", - exportKind: ExportKindUMD, - targetFlags: symbol.Flags, - isFromPackageJson: false, - }} - - useRequire := shouldUseRequire(fixContext.SourceFile, fixContext.Program) - // `usagePosition` is undefined because `token` may not actually be a usage of the symbol we're importing. - // For example, we might need to import `React` in order to use an arbitrary JSX tag. We could send a position - // for other UMD imports, but `usagePosition` is currently only used to insert a namespace qualification - // before a named import, like converting `writeFile` to `fs.writeFile` (whether `fs` is already imported or - // not), and this function will only be called for UMD symbols, which are necessarily an `export =`, not a - // named export. - _, fixes := fixContext.LS.getImportFixes( - ch, - exportInfo, - nil, // usagePosition undefined for UMD - ptrTo(false), - &useRequire, - fixContext.SourceFile, - false, // fromCacheOnly - ) + export := autoimport.SymbolToExport(umdSymbol, ch) + isValidTypeOnlyUseSite := ast.IsValidTypeOnlyAliasUseSite(token) var result []*fixInfo - for _, fix := range fixes { + for _, fix := range view.GetFixes(ctx, export, false, isValidTypeOnlyUseSite, nil) { errorIdentifierText := "" if ast.IsIdentifier(token) { errorIdentifierText = token.Text() } result = append(result, &fixInfo{ fix: fix, - symbolName: symbolName, + symbolName: umdSymbol.Name, errorIdentifierText: errorIdentifierText, }) } @@ -219,63 +189,41 @@ func isUMDExportSymbol(symbol *ast.Symbol) bool { ast.IsNamespaceExportDeclaration(symbol.Declarations[0]) } -func getFixesInfoForNonUMDImport(ctx context.Context, fixContext *CodeFixContext, symbolToken *ast.Node, useAutoImportProvider bool) []*fixInfo { +func getFixesInfoForNonUMDImport(ctx context.Context, fixContext *CodeFixContext, symbolToken *ast.Node, view *autoimport.View) []*fixInfo { ch, done := fixContext.Program.GetTypeChecker(ctx) defer done() compilerOptions := fixContext.Program.Options() + isValidTypeOnlyUseSite := ast.IsValidTypeOnlyAliasUseSite(symbolToken) symbolNames := getSymbolNamesToImport(fixContext.SourceFile, ch, symbolToken, compilerOptions) var allInfo []*fixInfo + // Compute usage position for JSDoc import type fixes + usagePosition := fixContext.LS.converters.PositionToLineAndCharacter(fixContext.SourceFile, core.TextPos(scanner.GetTokenPosOfNode(symbolToken, fixContext.SourceFile, false))) + for _, symbolName := range symbolNames { // "default" is a keyword and not a legal identifier for the import if symbolName == "default" { continue } - isValidTypeOnlyUseSite := ast.IsValidTypeOnlyAliasUseSite(symbolToken) - useRequire := shouldUseRequire(fixContext.SourceFile, fixContext.Program) - exportInfosMap := getExportInfos( - ctx, - symbolName, - ast.IsJsxTagName(symbolToken), - getMeaningFromLocation(symbolToken), - fixContext.SourceFile, - fixContext.Program, - fixContext.LS, - ) - - // Flatten all export infos from the map into a single slice - var allExportInfos []*SymbolExportInfo - for exportInfoList := range exportInfosMap.Values() { - allExportInfos = append(allExportInfos, exportInfoList...) + isJSXTagName := symbolName == symbolToken.Text() && ast.IsJsxTagName(symbolToken) + queryKind := autoimport.QueryKindExactMatch + if isJSXTagName { + queryKind = autoimport.QueryKindCaseInsensitiveMatch } - // Sort by moduleFileName to ensure deterministic iteration order - // TODO: This might not work 100% of the time; need to revisit this - slices.SortStableFunc(allExportInfos, func(a, b *SymbolExportInfo) int { - return strings.Compare(a.moduleFileName, b.moduleFileName) - }) - - if len(allExportInfos) > 0 { - usagePos := scanner.GetTokenPosOfNode(symbolToken, fixContext.SourceFile, false) - lspPos := fixContext.LS.converters.PositionToLineAndCharacter(fixContext.SourceFile, core.TextPos(usagePos)) - _, fixes := fixContext.LS.getImportFixes( - ch, - allExportInfos, - &lspPos, - &isValidTypeOnlyUseSite, - &useRequire, - fixContext.SourceFile, - false, // fromCacheOnly - ) + exports := view.Search(symbolName, queryKind) + for _, export := range exports { + if isJSXTagName && !(export.Name() == symbolName || export.IsRenameable()) { + continue + } + fixes := view.GetFixes(ctx, export, isJSXTagName, isValidTypeOnlyUseSite, &usagePosition) for _, fix := range fixes { allInfo = append(allInfo, &fixInfo{ - fix: fix, - symbolName: symbolName, - errorIdentifierText: symbolToken.Text(), - isJsxNamespaceFix: symbolName != symbolToken.Text(), + fix: fix, + symbolName: symbolName, }) } } @@ -284,7 +232,7 @@ func getFixesInfoForNonUMDImport(ctx context.Context, fixContext *CodeFixContext return allInfo } -func getTypeOnlyPromotionFix(ctx context.Context, sourceFile *ast.SourceFile, symbolToken *ast.Node, symbolName string, program *compiler.Program) *ImportFix { +func getTypeOnlyPromotionFix(ctx context.Context, sourceFile *ast.SourceFile, symbolToken *ast.Node, symbolName string, program *compiler.Program) *autoimport.Fix { ch, done := program.GetTypeChecker(ctx) defer done() @@ -300,9 +248,11 @@ func getTypeOnlyPromotionFix(ctx context.Context, sourceFile *ast.SourceFile, sy return nil } - return &ImportFix{ - kind: ImportFixKindPromoteTypeOnly, - typeOnlyAliasDeclaration: typeOnlyAliasDeclaration, + return &autoimport.Fix{ + AutoImportFix: &lsproto.AutoImportFix{ + Kind: lsproto.AutoImportFixKindPromoteTypeOnly, + }, + TypeOnlyAliasDeclaration: typeOnlyAliasDeclaration, } } @@ -343,89 +293,7 @@ func jsxModeNeedsExplicitImport(jsx core.JsxEmit) bool { return jsx == core.JsxEmitReact || jsx == core.JsxEmitReactNative } -func getExportInfos( - ctx context.Context, - symbolName string, - isJsxTagName bool, - currentTokenMeaning ast.SemanticMeaning, - fromFile *ast.SourceFile, - program *compiler.Program, - ls *LanguageService, -) *collections.MultiMap[ast.SymbolId, *SymbolExportInfo] { - // For each original symbol, keep all re-exports of that symbol together - // Maps symbol id to info for modules providing that symbol (original export + re-exports) - originalSymbolToExportInfos := &collections.MultiMap[ast.SymbolId, *SymbolExportInfo]{} - - ch, done := program.GetTypeChecker(ctx) - defer done() - - packageJsonFilter := ls.createPackageJsonImportFilter(fromFile) - - // Helper to add a symbol to the results map - addSymbol := func(moduleSymbol *ast.Symbol, toFile *ast.SourceFile, exportedSymbol *ast.Symbol, exportKind ExportKind, isFromPackageJson bool) { - if !ls.isImportable(fromFile, toFile, moduleSymbol, packageJsonFilter) { - return - } - - // Get unique ID for the exported symbol - symbolID := ast.GetSymbolId(exportedSymbol) - - moduleFileName := "" - if toFile != nil { - moduleFileName = toFile.FileName() - } - - originalSymbolToExportInfos.Add(symbolID, &SymbolExportInfo{ - symbol: exportedSymbol, - moduleSymbol: moduleSymbol, - moduleFileName: moduleFileName, - exportKind: exportKind, - targetFlags: ch.SkipAlias(exportedSymbol).Flags, - isFromPackageJson: isFromPackageJson, - }) - } - - // Iterate through all external modules - forEachExternalModuleToImportFrom( - ch, - program, - ls.UserPreferences(), - func(moduleSymbol *ast.Symbol, sourceFile *ast.SourceFile, checker *checker.Checker, isFromPackageJson bool) { - // Check for cancellation - if ctx.Err() != nil { - return - } - - compilerOptions := program.Options() - - // Check default export - defaultInfo := getDefaultLikeExportInfo(moduleSymbol, checker) - if defaultInfo != nil && - symbolFlagsHaveMeaning(checker.GetSymbolFlags(defaultInfo.exportingModuleSymbol), currentTokenMeaning) && - forEachNameOfDefaultExport(defaultInfo.exportingModuleSymbol, checker, compilerOptions.GetEmitScriptTarget(), func(name, capitalizedName string) string { - actualName := name - if isJsxTagName && capitalizedName != "" { - actualName = capitalizedName - } - if actualName == symbolName { - return actualName - } - return "" - }) != "" { - addSymbol(moduleSymbol, sourceFile, defaultInfo.exportingModuleSymbol, defaultInfo.exportKind, isFromPackageJson) - } - // Check for named export with identical name - exportSymbol := checker.TryGetMemberInModuleExportsAndProperties(symbolName, moduleSymbol) - if exportSymbol != nil && symbolFlagsHaveMeaning(checker.GetSymbolFlags(exportSymbol), currentTokenMeaning) { - addSymbol(moduleSymbol, sourceFile, exportSymbol, ExportKindNamed, isFromPackageJson) - } - }, - ) - - return originalSymbolToExportInfos -} - -func sortFixInfo(fixes []*fixInfo, fixContext *CodeFixContext) []*fixInfo { +func sortFixInfo(fixes []*fixInfo, fixContext *CodeFixContext, view *autoimport.View) []*fixInfo { if len(fixes) == 0 { return fixes } @@ -434,209 +302,16 @@ func sortFixInfo(fixes []*fixInfo, fixContext *CodeFixContext) []*fixInfo { sorted := make([]*fixInfo, len(fixes)) copy(sorted, fixes) - // Create package.json filter for import filtering - packageJsonFilter := fixContext.LS.createPackageJsonImportFilter(fixContext.SourceFile) - // Sort by: // 1. JSX namespace fixes last - // 2. Fix kind (UseNamespace and AddToExisting preferred) - // 3. Module specifier comparison + // 2. Fix comparison using view.CompareFixes slices.SortFunc(sorted, func(a, b *fixInfo) int { // JSX namespace fixes should come last if cmp := core.CompareBooleans(a.isJsxNamespaceFix, b.isJsxNamespaceFix); cmp != 0 { return cmp } - - // Compare fix kinds (lower is better) - if cmp := cmp.Compare(int(a.fix.kind), int(b.fix.kind)); cmp != 0 { - return cmp - } - - // Compare module specifiers - return fixContext.LS.compareModuleSpecifiers( - a.fix, - b.fix, - fixContext.SourceFile, - packageJsonFilter.allowsImportingSpecifier, - func(fileName string) tspath.Path { return tspath.Path(fileName) }, - ) + return view.CompareFixesForSorting(a.fix, b.fix) }) return sorted } - -func promoteFromTypeOnly( - changes *change.Tracker, - aliasDeclaration *ast.Declaration, - program *compiler.Program, - sourceFile *ast.SourceFile, - ls *LanguageService, -) *ast.Declaration { - compilerOptions := program.Options() - // See comment in `doAddExistingFix` on constant with the same name. - convertExistingToTypeOnly := compilerOptions.VerbatimModuleSyntax - - switch aliasDeclaration.Kind { - case ast.KindImportSpecifier: - spec := aliasDeclaration.AsImportSpecifier() - if spec.IsTypeOnly { - if spec.Parent != nil && spec.Parent.Kind == ast.KindNamedImports { - // TypeScript creates a new specifier with isTypeOnly=false, computes insertion index, - // and if different from current position, deletes and re-inserts at new position. - // For now, we just delete the range from the first token (type keyword) to the property name or name. - firstToken := lsutil.GetFirstToken(aliasDeclaration, sourceFile) - typeKeywordPos := scanner.GetTokenPosOfNode(firstToken, sourceFile, false) - var targetNode *ast.DeclarationName - if spec.PropertyName != nil { - targetNode = spec.PropertyName - } else { - targetNode = spec.Name() - } - targetPos := scanner.GetTokenPosOfNode(targetNode.AsNode(), sourceFile, false) - changes.DeleteRange(sourceFile, core.NewTextRange(typeKeywordPos, targetPos)) - } - return aliasDeclaration - } else { - // The parent import clause is type-only - if spec.Parent == nil || spec.Parent.Kind != ast.KindNamedImports { - panic("ImportSpecifier parent must be NamedImports") - } - if spec.Parent.Parent == nil || spec.Parent.Parent.Kind != ast.KindImportClause { - panic("NamedImports parent must be ImportClause") - } - promoteImportClause(changes, spec.Parent.Parent.AsImportClause(), program, sourceFile, ls, convertExistingToTypeOnly, aliasDeclaration) - return spec.Parent.Parent - } - - case ast.KindImportClause: - promoteImportClause(changes, aliasDeclaration.AsImportClause(), program, sourceFile, ls, convertExistingToTypeOnly, aliasDeclaration) - return aliasDeclaration - - case ast.KindNamespaceImport: - // Promote the parent import clause - if aliasDeclaration.Parent == nil || aliasDeclaration.Parent.Kind != ast.KindImportClause { - panic("NamespaceImport parent must be ImportClause") - } - promoteImportClause(changes, aliasDeclaration.Parent.AsImportClause(), program, sourceFile, ls, convertExistingToTypeOnly, aliasDeclaration) - return aliasDeclaration.Parent - - case ast.KindImportEqualsDeclaration: - // Remove the 'type' keyword (which is the second token: 'import' 'type' name '=' ...) - importEqDecl := aliasDeclaration.AsImportEqualsDeclaration() - // The type keyword is after 'import' and before the name - scan := scanner.GetScannerForSourceFile(sourceFile, importEqDecl.Pos()) - // Skip 'import' keyword to get to 'type' - scan.Scan() - deleteTypeKeyword(changes, sourceFile, scan.TokenStart()) - return aliasDeclaration - default: - panic(fmt.Sprintf("Unexpected alias declaration kind: %v", aliasDeclaration.Kind)) - } -} - -// promoteImportClause removes the type keyword from an import clause -func promoteImportClause( - changes *change.Tracker, - importClause *ast.ImportClause, - program *compiler.Program, - sourceFile *ast.SourceFile, - ls *LanguageService, - convertExistingToTypeOnly core.Tristate, - aliasDeclaration *ast.Declaration, -) { - // Delete the 'type' keyword - if importClause.PhaseModifier == ast.KindTypeKeyword { - deleteTypeKeyword(changes, sourceFile, importClause.Pos()) - } - - // Handle .ts extension conversion to .js if necessary - compilerOptions := program.Options() - if compilerOptions.AllowImportingTsExtensions.IsFalse() { - moduleSpecifier := checker.TryGetModuleSpecifierFromDeclaration(importClause.Parent) - if moduleSpecifier != nil { - resolvedModule := program.GetResolvedModuleFromModuleSpecifier(sourceFile, moduleSpecifier) - if resolvedModule != nil && resolvedModule.ResolvedUsingTsExtension { - moduleText := moduleSpecifier.AsStringLiteral().Text - changedExtension := tspath.ChangeExtension( - moduleText, - outputpaths.GetOutputExtension(moduleText, compilerOptions.Jsx), - ) - // Replace the module specifier with the new extension - newStringLiteral := changes.NewStringLiteral(changedExtension, moduleSpecifier.AsStringLiteral().TokenFlags) - changes.ReplaceNode(sourceFile, moduleSpecifier, newStringLiteral, nil) - } - } - } - - // Handle verbatimModuleSyntax conversion - // If convertExistingToTypeOnly is true, we need to add 'type' to other specifiers - // in the same import declaration - if convertExistingToTypeOnly.IsTrue() { - namedImports := importClause.NamedBindings - if namedImports != nil && namedImports.Kind == ast.KindNamedImports { - namedImportsData := namedImports.AsNamedImports() - if len(namedImportsData.Elements.Nodes) > 1 { - // Check if the list is sorted and if we need to reorder - _, isSorted := organizeimports.GetNamedImportSpecifierComparerWithDetection( - importClause.Parent, - sourceFile, - ls.UserPreferences(), - ) - - // If the alias declaration is an ImportSpecifier and the list is sorted, - // move it to index 0 (since it will be the only non-type-only import) - if isSorted.IsFalse() == false && // isSorted !== false - aliasDeclaration != nil && - aliasDeclaration.Kind == ast.KindImportSpecifier { - // Find the index of the alias declaration - aliasIndex := -1 - for i, element := range namedImportsData.Elements.Nodes { - if element == aliasDeclaration { - aliasIndex = i - break - } - } - // If not already at index 0, move it there - if aliasIndex > 0 { - // Delete the specifier from its current position - changes.Delete(sourceFile, aliasDeclaration) - // Insert it at index 0 - changes.InsertImportSpecifierAtIndex(sourceFile, aliasDeclaration, namedImports, 0) - } - } - - // Add 'type' keyword to all other import specifiers that aren't already type-only - for _, element := range namedImportsData.Elements.Nodes { - spec := element.AsImportSpecifier() - // Skip the specifier being promoted (if aliasDeclaration is an ImportSpecifier) - if aliasDeclaration != nil && aliasDeclaration.Kind == ast.KindImportSpecifier { - if element == aliasDeclaration { - continue - } - } - // Skip if already type-only - if !spec.IsTypeOnly { - changes.InsertModifierBefore(sourceFile, ast.KindTypeKeyword, element) - } - } - } - } - } -} - -// deleteTypeKeyword deletes the 'type' keyword token starting at the given position, -// including any trailing whitespace. -func deleteTypeKeyword(changes *change.Tracker, sourceFile *ast.SourceFile, startPos int) { - scan := scanner.GetScannerForSourceFile(sourceFile, startPos) - if scan.Token() != ast.KindTypeKeyword { - return - } - typeStart := scan.TokenStart() - typeEnd := scan.TokenEnd() - // Skip trailing whitespace - text := sourceFile.Text() - for typeEnd < len(text) && (text[typeEnd] == ' ' || text[typeEnd] == '\t') { - typeEnd++ - } - changes.DeleteRange(sourceFile, core.NewTextRange(typeStart, typeEnd)) -} diff --git a/internal/ls/completions.go b/internal/ls/completions.go index e8e7003b36..de07d41a83 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -12,7 +12,6 @@ import ( "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" @@ -20,15 +19,17 @@ import ( "github.com/microsoft/typescript-go/internal/debug" "github.com/microsoft/typescript-go/internal/format" "github.com/microsoft/typescript-go/internal/jsnum" + "github.com/microsoft/typescript-go/internal/ls/autoimport" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/nodebuilder" "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" ) +var ErrNeedsAutoImports = errors.New("completion list needs auto imports") + func (l *LanguageService) ProvideCompletion( ctx context.Context, documentURI lsproto.DocumentUri, @@ -41,12 +42,15 @@ func (l *LanguageService) ProvideCompletion( triggerCharacter = context.TriggerCharacter } position := int(l.converters.LineAndCharacterToPosition(file, LSPPosition)) - completionList := l.getCompletionsAtPosition( + completionList, err := l.getCompletionsAtPosition( ctx, file, position, triggerCharacter, ) + if err != nil { + return lsproto.CompletionItemsOrListOrNull{}, err + } completionList = ensureItemData(file.FileName(), position, completionList) return lsproto.CompletionItemsOrListOrNull{List: completionList}, nil } @@ -72,6 +76,7 @@ type completionData = any type completionDataData struct { symbols []*ast.Symbol + autoImports []*autoimport.FixAndExport completionKind CompletionKind isInSnippetScope bool // Note that the presence of this alone doesn't mean that we need a conversion. Only do that if the completion is not an ordinary identifier. @@ -199,16 +204,12 @@ type symbolOriginInfoKind int const ( symbolOriginInfoKindThisType symbolOriginInfoKind = 1 << iota symbolOriginInfoKindSymbolMember - symbolOriginInfoKindExport symbolOriginInfoKindPromise symbolOriginInfoKindNullable symbolOriginInfoKindTypeOnlyAlias symbolOriginInfoKindObjectLiteralMethod symbolOriginInfoKindIgnore symbolOriginInfoKindComputedPropertyName - - symbolOriginInfoKindSymbolMemberNoExport symbolOriginInfoKind = symbolOriginInfoKindSymbolMember - symbolOriginInfoKindSymbolMemberExport = symbolOriginInfoKindSymbolMember | symbolOriginInfoKindExport ) type symbolOriginInfo struct { @@ -221,8 +222,6 @@ type symbolOriginInfo struct { func (origin *symbolOriginInfo) symbolName() string { switch origin.data.(type) { - case *symbolOriginInfoExport: - return origin.data.(*symbolOriginInfoExport).symbolName case *symbolOriginInfoComputedPropertyName: return origin.data.(*symbolOriginInfoComputedPropertyName).symbolName default: @@ -230,45 +229,6 @@ func (origin *symbolOriginInfo) symbolName() string { } } -func (origin *symbolOriginInfo) moduleSymbol() *ast.Symbol { - switch origin.data.(type) { - case *symbolOriginInfoExport: - return origin.data.(*symbolOriginInfoExport).moduleSymbol - default: - panic(fmt.Sprintf("symbolOriginInfo: unknown data type for moduleSymbol(): %T", origin.data)) - } -} - -func (origin *symbolOriginInfo) toCompletionEntryData() *lsproto.AutoImportData { - debug.Assert(origin.kind&symbolOriginInfoKindExport != 0, fmt.Sprintf("completionEntryData is not generated for symbolOriginInfo of type %T", origin.data)) - var ambientModuleName string - if origin.fileName == "" { - ambientModuleName = stringutil.StripQuotes(origin.moduleSymbol().Name) - } - - data := origin.data.(*symbolOriginInfoExport) - return &lsproto.AutoImportData{ - ExportName: data.exportName, - ExportMapKey: data.exportMapKey, - ModuleSpecifier: data.moduleSpecifier, - AmbientModuleName: ambientModuleName, - FileName: origin.fileName, - IsPackageJsonImport: origin.isFromPackageJson, - } -} - -type symbolOriginInfoExport struct { - symbolName string - moduleSymbol *ast.Symbol - exportName string - exportMapKey lsproto.ExportInfoMapKey - moduleSpecifier string -} - -func (s *symbolOriginInfo) asExport() *symbolOriginInfoExport { - return s.data.(*symbolOriginInfoExport) -} - type symbolOriginInfoObjectLiteralMethod struct { insertText string labelDetails *lsproto.CompletionItemLabelDetails @@ -333,10 +293,10 @@ func (l *LanguageService) getCompletionsAtPosition( file *ast.SourceFile, position int, triggerCharacter *string, -) *lsproto.CompletionList { +) (*lsproto.CompletionList, error) { _, previousToken := getRelevantTokens(position, file) if triggerCharacter != nil && !IsInString(file, position, previousToken) && !isValidTrigger(file, *triggerCharacter, previousToken, position) { - return nil + return nil, nil } if triggerCharacter != nil && *triggerCharacter == " " { @@ -344,24 +304,28 @@ func (l *LanguageService) getCompletionsAtPosition( if l.UserPreferences().IncludeCompletionsForImportStatements.IsTrue() { return &lsproto.CompletionList{ IsIncomplete: true, - } + }, nil } - return nil + return nil, nil } compilerOptions := l.GetProgram().Options() // !!! see if incomplete completion list and continue or clean + checker, done := l.GetProgram().GetTypeCheckerForFile(ctx, file) + defer done() + stringCompletions := l.getStringLiteralCompletions( ctx, file, position, previousToken, + checker, compilerOptions, ) if stringCompletions != nil { - return stringCompletions + return stringCompletions, nil } if previousToken != nil && (previousToken.Kind == ast.KindBreakKeyword || @@ -374,15 +338,16 @@ func (l *LanguageService) getCompletionsAtPosition( file, position, l.getOptionalReplacementSpan(previousToken, file), - ) + ), nil } - checker, done := l.GetProgram().GetTypeCheckerForFile(ctx, file) - defer done() preferences := l.UserPreferences() - data := l.getCompletionData(ctx, checker, file, position, preferences) + data, err := l.getCompletionData(ctx, checker, file, position, preferences) + if err != nil { + return nil, err + } if data == nil { - return nil + return nil, nil } switch data := data.(type) { @@ -398,7 +363,7 @@ func (l *LanguageService) getCompletionsAtPosition( optionalReplacementSpan, ) // !!! check if response is incomplete - return response + return response, nil case *completionDataKeyword: optionalReplacementSpan := l.getOptionalReplacementSpan(previousToken, file) return l.specificKeywordCompletionInfo( @@ -408,7 +373,7 @@ func (l *LanguageService) getCompletionsAtPosition( data.keywordCompletions, data.isNewIdentifierLocation, optionalReplacementSpan, - ) + ), nil case *completionDataJSDocTagName: // If the current position is a jsDoc tag name, only tag names should be provided for completion items := getJSDocTagNameCompletions() @@ -421,7 +386,7 @@ func (l *LanguageService) getCompletionsAtPosition( preferences, /*tagNameOnly*/ true, )...) - return l.jsDocCompletionInfo(ctx, position, file, items) + return l.jsDocCompletionInfo(ctx, position, file, items), nil case *completionDataJSDocTag: // If the current position is a jsDoc tag, only tags should be provided for completion items := getJSDocTagCompletions() @@ -434,9 +399,9 @@ func (l *LanguageService) getCompletionsAtPosition( preferences, /*tagNameOnly*/ false, )...) - return l.jsDocCompletionInfo(ctx, position, file, items) + return l.jsDocCompletionInfo(ctx, position, file, items), nil case *completionDataJSDocParameterName: - return l.jsDocCompletionInfo(ctx, position, file, getJSDocParameterNameCompletions(data.tag)) + return l.jsDocCompletionInfo(ctx, position, file, getJSDocParameterNameCompletions(data.tag)), nil default: panic("getCompletionData() returned unexpected type: " + fmt.Sprintf("%T", data)) } @@ -448,7 +413,7 @@ func (l *LanguageService) getCompletionData( file *ast.SourceFile, position int, preferences *lsutil.UserPreferences, -) completionData { +) (completionData, error) { inCheckedFile := isCheckedFile(file, l.GetProgram().Options()) currentToken := astnav.GetTokenAtPosition(file, position) @@ -463,7 +428,7 @@ func (l *LanguageService) getCompletionData( if file.Text()[position] == '@' { // The current position is next to the '@' sign, when no tag name being provided yet. // Provide a full list of tag names - return &completionDataJSDocTagName{} + return &completionDataJSDocTagName{}, nil } else { // When completion is requested without "@", we will have check to make sure that // there are no comments prefix the request position. We will only allow "*" and space. @@ -490,7 +455,7 @@ func (l *LanguageService) getCompletionData( } } if noCommentPrefix { - return &completionDataJSDocTag{} + return &completionDataJSDocTag{}, nil } } } @@ -500,7 +465,7 @@ func (l *LanguageService) getCompletionData( // Completion should work in the brackets if tag := getJSDocTagAtPosition(currentToken, position); tag != nil { if tag.TagName().Pos() <= position && position <= tag.TagName().End() { - return &completionDataJSDocTagName{} + return &completionDataJSDocTagName{}, nil } if ast.IsJSDocImportTag(tag) { insideJsDocImportTag = true @@ -520,7 +485,7 @@ func (l *LanguageService) getCompletionData( (ast.NodeIsMissing(tag.Name()) || tag.Name().Pos() <= position && position <= tag.Name().End()) { return &completionDataJSDocParameterName{ tag: tag.AsJSDocParameterOrPropertyTag(), - } + }, nil } } } @@ -528,7 +493,7 @@ func (l *LanguageService) getCompletionData( if !insideJSDocTagTypeExpression && !insideJsDocImportTag { // Proceed if the current position is in JSDoc tag expression; otherwise it is a normal // comment or the plain text part of a JSDoc comment, so no completion should be available - return nil + return nil, nil } } @@ -566,7 +531,7 @@ func (l *LanguageService) getCompletionData( SortText: ptrTo(string(SortTextGlobalsOrKeywords)), }}, isNewIdentifierLocation: importStatementCompletionInfo.isNewIdentifierLocation, - } + }, nil } keywordFilters = keywordFiltersFromSyntaxKind(importStatementCompletionInfo.keywordCompletion) } @@ -579,9 +544,9 @@ func (l *LanguageService) getCompletionData( if isCompletionListBlocker(contextToken, previousToken, location, file, position, typeChecker) { if keywordFilters != KeywordCompletionFiltersNone { isNewIdentifierLocation, _ := computeCommitCharactersAndIsNewIdentifier(contextToken, file, position) - return keywordCompletionData(keywordFilters, isJSOnlyLocation, isNewIdentifierLocation) + return keywordCompletionData(keywordFilters, isJSOnlyLocation, isNewIdentifierLocation), nil } - return nil + return nil, nil } parent := contextToken.Parent @@ -601,7 +566,7 @@ func (l *LanguageService) getCompletionData( // eg: Math.min(./**/) // const x = function (./**/) {} // ({./**/}) - return nil + return nil, nil } case ast.KindQualifiedName: node = parent.AsQualifiedName().Left @@ -617,7 +582,7 @@ func (l *LanguageService) getCompletionData( default: // There is nothing that precedes the dot, so this likely just a stray character // or leading into a '...' token. Just bail out instead. - return nil + return nil, nil } } else { // !!! else if (!importStatementCompletion) // @@ -695,11 +660,11 @@ func (l *LanguageService) getCompletionData( hasUnresolvedAutoImports := false // This also gets mutated in nested-functions after the return var symbols []*ast.Symbol + var autoImports []*autoimport.FixAndExport // Keys are indexes of `symbols`. symbolToOriginInfoMap := map[int]*symbolOriginInfo{} symbolToSortTextMap := map[ast.SymbolId]SortText{} 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) && @@ -757,39 +722,9 @@ func (l *LanguageService) getCompletionData( if moduleSymbol == nil || !checker.IsExternalModuleSymbol(moduleSymbol) || typeChecker.TryGetMemberInModuleExportsAndProperties(firstAccessibleSymbol.Name, moduleSymbol) != firstAccessibleSymbol { - symbolToOriginInfoMap[len(symbols)-1] = &symbolOriginInfo{kind: getNullableSymbolOriginInfoKind(symbolOriginInfoKindSymbolMemberNoExport, insertQuestionDot)} + symbolToOriginInfoMap[len(symbols)-1] = &symbolOriginInfo{kind: getNullableSymbolOriginInfoKind(symbolOriginInfoKindSymbolMember, insertQuestionDot)} } else { - 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: false, - moduleSymbol: moduleSymbol, - symbol: firstAccessibleSymbol, - targetFlags: typeChecker.SkipAlias(firstAccessibleSymbol).Flags, - }}, - position, - ast.IsValidTypeOnlyAliasUseSite(location), - ) - - if result != nil { - symbolToOriginInfoMap[len(symbols)-1] = &symbolOriginInfo{ - kind: getNullableSymbolOriginInfoKind(symbolOriginInfoKindSymbolMemberExport, insertQuestionDot), - isDefaultExport: false, - fileName: fileName, - data: &symbolOriginInfoExport{ - moduleSymbol: moduleSymbol, - symbolName: firstAccessibleSymbol.Name, - exportName: firstAccessibleSymbol.Name, - moduleSpecifier: result.moduleSpecifier, - }, - } - } + // !!! auto-import symbol } } else if firstAccessibleSymbolId == 0 || !seenPropertySymbols.Has(firstAccessibleSymbolId) { symbols = append(symbols, symbol) @@ -966,10 +901,10 @@ func (l *LanguageService) getCompletionData( } // Aggregates relevant symbols for completion in object literals in type argument positions. - tryGetObjectTypeLiteralInTypeArgumentCompletionSymbols := func() globalsSearch { + tryGetObjectTypeLiteralInTypeArgumentCompletionSymbols := func() (globalsSearch, error) { typeLiteralNode := tryGetTypeLiteralNode(contextToken) if typeLiteralNode == nil { - return globalsSearchContinue + return globalsSearchContinue, nil } intersectionTypeNode := core.IfElse( @@ -983,7 +918,7 @@ func (l *LanguageService) getCompletionData( containerExpectedType := getConstraintOfTypeArgumentProperty(containerTypeNode, typeChecker) if containerExpectedType == nil { - return globalsSearchContinue + return globalsSearchContinue, nil } containerActualType := typeChecker.GetTypeFromTypeNode(containerTypeNode) @@ -1003,18 +938,18 @@ func (l *LanguageService) getCompletionData( completionKind = CompletionKindObjectPropertyDeclaration isNewIdentifierLocation = true - return globalsSearchSuccess + return globalsSearchSuccess, nil } // Aggregates relevant symbols for completion in object literals and object binding patterns. // Relevant symbols are stored in the captured 'symbols' variable. - tryGetObjectLikeCompletionSymbols := func() globalsSearch { + tryGetObjectLikeCompletionSymbols := func() (globalsSearch, error) { if contextToken != nil && contextToken.Kind == ast.KindDotDotDotToken { - return globalsSearchContinue + return globalsSearchContinue, nil } objectLikeContainer := tryGetObjectLikeCompletionContainer(contextToken, position, file) if objectLikeContainer == nil { - return globalsSearchContinue + return globalsSearchContinue, nil } // We're looking up possible property names from contextual/inferred/declared type. @@ -1029,9 +964,9 @@ func (l *LanguageService) getCompletionData( // Check completions for Object property value shorthand if instantiatedType == nil { if objectLikeContainer.Flags&ast.NodeFlagsInWithStatement != 0 { - return globalsSearchFail + return globalsSearchFail, nil } - return globalsSearchContinue + return globalsSearchContinue, nil } completionsType := typeChecker.GetContextualType(objectLikeContainer, checker.ContextFlagsCompletions) t := core.IfElse(completionsType != nil, completionsType, instantiatedType) @@ -1044,7 +979,7 @@ func (l *LanguageService) getCompletionData( if len(typeMembers) == 0 { // Edge case: If NumberIndexType exists if numberIndexType == nil { - return globalsSearchContinue + return globalsSearchContinue, nil } } } else { @@ -1078,7 +1013,7 @@ func (l *LanguageService) getCompletionData( if canGetType { typeForObject := typeChecker.GetTypeAtLocation(objectLikeContainer) if typeForObject == nil { - return globalsSearchFail + return globalsSearchFail, nil } typeMembers = core.Filter( typeChecker.GetPropertiesOfType(typeForObject), @@ -1127,7 +1062,7 @@ func (l *LanguageService) getCompletionData( } } - return globalsSearchSuccess + return globalsSearchSuccess, nil } shouldOfferImportCompletions := func() bool { @@ -1144,11 +1079,10 @@ func (l *LanguageService) getCompletionData( } // Mutates `symbols`, `symbolToOriginInfoMap`, and `symbolToSortTextMap` - collectAutoImports := func() { + collectAutoImports := func() error { if !shouldOfferImportCompletions() { - return + return nil } - // !!! CompletionInfoFlags // import { type | -> token text should be blank var lowerCaseTokenText string @@ -1156,116 +1090,24 @@ func (l *LanguageService) getCompletionData( lowerCaseTokenText = strings.ToLower(previousToken.Text()) } - // !!! 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. - isValidTypeOnlyUseSite := ast.IsValidTypeOnlyAliasUseSite(location) - - // !!! moduleSpecifierCache := host.getModuleSpecifierCache(); - // !!! packageJsonAutoImportProvider := host.getPackageJsonAutoImportProvider(); - addSymbolToList := func(info []*SymbolExportInfo, symbolName string, isFromAmbientModule bool, exportMapKey lsproto.ExportInfoMapKey) []*SymbolExportInfo { - // 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. - info = core.Filter(info, func(i *SymbolExportInfo) bool { - var toFile *ast.SourceFile - if ast.IsSourceFile(i.moduleSymbol.ValueDeclaration) { - toFile = i.moduleSymbol.ValueDeclaration.AsSourceFile() - } - return l.isImportable( - file, - toFile, - i.moduleSymbol, - importSpecifierResolver.packageJsonImportFilter(), - ) - }) - 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. 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 := importSpecifierResolver.getModuleSpecifierForBestExportInfo(typeChecker, info, position, isValidTypeOnlyUseSite) - if result == nil { - 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. - moduleSpecifier := result.moduleSpecifier - exportInfo := info[0] - if result.exportInfo != nil { - exportInfo = result.exportInfo - } - - isDefaultExport := exportInfo.exportKind == ExportKindDefault - if exportInfo.symbol == nil { - panic("should have handled `futureExportSymbolInfo` earlier") - } - symbol := exportInfo.symbol - if isDefaultExport { - if defaultSymbol := binder.GetLocalSymbolForExportDefault(symbol); defaultSymbol != nil { - symbol = defaultSymbol - } - } - - // 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 - } - originInfo := &symbolOriginInfo{ - kind: symbolOriginInfoKindExport, - isDefaultExport: isDefaultExport, - isFromPackageJson: exportInfo.isFromPackageJson, - fileName: exportInfo.moduleFileName, - data: &symbolOriginInfoExport{ - symbolName: symbolName, - moduleSymbol: exportInfo.moduleSymbol, - exportName: core.IfElse(exportInfo.exportKind == ExportKindExportEquals, ast.InternalSymbolNameExportEquals, exportInfo.symbol.Name), - exportMapKey: exportMapKey, - moduleSpecifier: moduleSpecifier, - }, - } - symbolToOriginInfoMap[len(symbols)] = originInfo - symbolToSortTextMap[symbolId] = core.IfElse(importStatementCompletion != nil, SortTextLocationPriority, SortTextAutoImportSuggestions) - symbols = append(symbols, symbol) - return nil + view, err := l.getPreparedAutoImportView(file) + if err != nil { + return err } - l.searchExportInfosForCompletions(ctx, - typeChecker, - file, - importStatementCompletion != nil, - isRightOfOpenTag, - isTypeOnlyLocation, - lowerCaseTokenText, - addSymbolToList, - ) - // !!! completionInfoFlags - // !!! logging + autoImports = view.GetCompletions(ctx, lowerCaseTokenText, isRightOfOpenTag, isTypeOnlyLocation) + return nil } - tryGetImportCompletionSymbols := func() globalsSearch { + tryGetImportCompletionSymbols := func() (globalsSearch, error) { if importStatementCompletion == nil { - return globalsSearchContinue + return globalsSearchContinue, nil } isNewIdentifierLocation = true - collectAutoImports() - return globalsSearchSuccess + if err := collectAutoImports(); err != nil { + return globalsSearchFail, err + } + return globalsSearchSuccess, nil } // Aggregates relevant symbols for completion in import clauses and export clauses @@ -1279,9 +1121,9 @@ func (l *LanguageService) getCompletionData( // export { | }; // // Relevant symbols are stored in the captured 'symbols' variable. - tryGetImportOrExportClauseCompletionSymbols := func() globalsSearch { + tryGetImportOrExportClauseCompletionSymbols := func() (globalsSearch, error) { if contextToken == nil { - return globalsSearchContinue + return globalsSearchContinue, nil } // `import { |` or `import { a as 0, | }` or `import { type | }` @@ -1297,7 +1139,7 @@ func (l *LanguageService) getCompletionData( } if namedImportsOrExports == nil { - return globalsSearchContinue + return globalsSearchContinue, nil } // We can at least offer `type` at `import { |` @@ -1313,15 +1155,15 @@ func (l *LanguageService) getCompletionData( if moduleSpecifier == nil { isNewIdentifierLocation = true if namedImportsOrExports.Kind == ast.KindNamedImports { - return globalsSearchFail + return globalsSearchFail, nil } - return globalsSearchContinue + return globalsSearchContinue, nil } moduleSpecifierSymbol := typeChecker.GetSymbolAtLocation(moduleSpecifier) if moduleSpecifierSymbol == nil { isNewIdentifierLocation = true - return globalsSearchFail + return globalsSearchFail, nil } completionKind = CompletionKindMemberLike @@ -1344,13 +1186,13 @@ func (l *LanguageService) getCompletionData( // If there's nothing else to import, don't offer `type` either. keywordFilters = KeywordCompletionFiltersNone } - return globalsSearchSuccess + return globalsSearchSuccess, nil } // import { x } from "foo" with { | } - tryGetImportAttributesCompletionSymbols := func() globalsSearch { + tryGetImportAttributesCompletionSymbols := func() (globalsSearch, error) { if contextToken == nil { - return globalsSearchContinue + return globalsSearchContinue, nil } var importAttributes *ast.ImportAttributesNode @@ -1362,7 +1204,7 @@ func (l *LanguageService) getCompletionData( } if importAttributes == nil { - return globalsSearchContinue + return globalsSearchContinue, nil } var elements []*ast.Node @@ -1379,7 +1221,7 @@ func (l *LanguageService) getCompletionData( return !existing.Has(ast.SymbolName(symbol)) }) symbols = append(symbols, uniques...) - return globalsSearchSuccess + return globalsSearchSuccess, nil } // Adds local declarations for completions in named exports: @@ -1387,9 +1229,9 @@ func (l *LanguageService) getCompletionData( // Does not check for the absence of a module specifier (`export {} from "./other"`) // because `tryGetImportOrExportClauseCompletionSymbols` runs first and handles that, // preventing this function from running. - tryGetLocalNamedExportCompletionSymbols := func() globalsSearch { + tryGetLocalNamedExportCompletionSymbols := func() (globalsSearch, error) { if contextToken == nil { - return globalsSearchContinue + return globalsSearchContinue, nil } var namedExports *ast.NamedExportsNode if contextToken.Kind == ast.KindOpenBraceToken || contextToken.Kind == ast.KindCommaToken { @@ -1397,7 +1239,7 @@ func (l *LanguageService) getCompletionData( } if namedExports == nil { - return globalsSearchContinue + return globalsSearchContinue, nil } localsContainer := ast.FindAncestor(namedExports, func(node *ast.Node) bool { @@ -1418,12 +1260,12 @@ func (l *LanguageService) getCompletionData( } } - return globalsSearchSuccess + return globalsSearchSuccess, nil } - tryGetConstructorCompletion := func() globalsSearch { + tryGetConstructorCompletion := func() (globalsSearch, error) { if tryGetConstructorLikeCompletionContainer(contextToken) == nil { - return globalsSearchContinue + return globalsSearchContinue, nil } // no members, only keywords @@ -1432,15 +1274,15 @@ func (l *LanguageService) getCompletionData( isNewIdentifierLocation = true // Has keywords for constructor parameter keywordFilters = KeywordCompletionFiltersConstructorParameterKeywords - return globalsSearchSuccess + return globalsSearchSuccess, nil } // Aggregates relevant symbols for completion in class declaration // Relevant symbols are stored in the captured 'symbols' variable. - tryGetClassLikeCompletionSymbols := func() globalsSearch { + tryGetClassLikeCompletionSymbols := func() (globalsSearch, error) { decl := tryGetObjectTypeDeclarationCompletionContainer(file, contextToken, location, position) if decl == nil { - return globalsSearchContinue + return globalsSearchContinue, nil } // We're looking up possible property names from parent type. @@ -1457,7 +1299,7 @@ func (l *LanguageService) getCompletionData( // If you're in an interface you don't want to repeat things from super-interface. So just stop here. if !ast.IsClassLike(decl) { - return globalsSearchSuccess + return globalsSearchSuccess, nil } var classElement *ast.Node @@ -1524,18 +1366,18 @@ func (l *LanguageService) getCompletionData( } } - return globalsSearchSuccess + return globalsSearchSuccess, nil } - tryGetJsxCompletionSymbols := func() globalsSearch { + tryGetJsxCompletionSymbols := func() (globalsSearch, error) { jsxContainer := tryGetContainingJsxElement(contextToken, file) if jsxContainer == nil { - return globalsSearchContinue + return globalsSearchContinue, nil } // Cursor is inside a JSX self-closing element or opening element. attrsType := typeChecker.GetContextualType(jsxContainer.Attributes(), checker.ContextFlagsNone) if attrsType == nil { - return globalsSearchContinue + return globalsSearchContinue, nil } completionsType := typeChecker.GetContextualType(jsxContainer.Attributes(), checker.ContextFlagsCompletions) filteredSymbols, spreadMemberNames := filterJsxAttributes( @@ -1563,10 +1405,10 @@ func (l *LanguageService) getCompletionData( completionKind = CompletionKindMemberLike isNewIdentifierLocation = false - return globalsSearchSuccess + return globalsSearchSuccess, nil } - getGlobalCompletions := func() globalsSearch { + getGlobalCompletions := func() (globalsSearch, error) { if tryGetFunctionLikeBodyCompletionContainer(contextToken) != nil { keywordFilters = KeywordCompletionFiltersFunctionLikeBodyKeywords } else { @@ -1662,7 +1504,9 @@ func (l *LanguageService) getCompletionData( } } - collectAutoImports() + if err := collectAutoImports(); err != nil { + return globalsSearchFail, err + } if isTypeOnlyLocation { if contextToken != nil && ast.IsAssertionExpression(contextToken.Parent) { keywordFilters = KeywordCompletionFiltersTypeAssertionKeywords @@ -1671,12 +1515,13 @@ func (l *LanguageService) getCompletionData( } } - return globalsSearchSuccess + return globalsSearchSuccess, nil } - tryGetGlobalSymbols := func() bool { + tryGetGlobalSymbols := func() (bool, error) { var result globalsSearch - globalSearchFuncs := []func() globalsSearch{ + var err error + globalSearchFuncs := []func() (globalsSearch, error){ tryGetObjectTypeLiteralInTypeArgumentCompletionSymbols, tryGetObjectLikeCompletionSymbols, tryGetImportCompletionSymbols, @@ -1689,12 +1534,15 @@ func (l *LanguageService) getCompletionData( getGlobalCompletions, } for _, globalSearchFunc := range globalSearchFuncs { - result = globalSearchFunc() + result, err = globalSearchFunc() + if err != nil { + return false, err + } if result != globalsSearchContinue { break } } - return result == globalsSearchSuccess + return result == globalsSearchSuccess, nil } if isRightOfDot || isRightOfQuestionDot { @@ -1702,7 +1550,9 @@ func (l *LanguageService) getCompletionData( } else if isRightOfOpenTag { symbols = typeChecker.GetJsxIntrinsicTagNamesAt(location) core.CheckEachDefined(symbols, "GetJsxIntrinsicTagNamesAt() should all be defined") - tryGetGlobalSymbols() + if _, err := tryGetGlobalSymbols(); err != nil { + return nil, err + } completionKind = CompletionKindGlobal keywordFilters = KeywordCompletionFiltersNone } else if isStartingCloseTag { @@ -1717,11 +1567,14 @@ func (l *LanguageService) getCompletionData( // For JavaScript or TypeScript, if we're not after a dot, then just try to get the // global symbols in scope. These results should be valid for either language as // the set of symbols that can be referenced from this location. - if !tryGetGlobalSymbols() { + if ok, err := tryGetGlobalSymbols(); !ok { + if err != nil { + return nil, err + } if keywordFilters != KeywordCompletionFiltersNone { - return keywordCompletionData(keywordFilters, isJSOnlyLocation, isNewIdentifierLocation) + return keywordCompletionData(keywordFilters, isJSOnlyLocation, isNewIdentifierLocation), nil } - return nil + return nil, nil } } @@ -1760,6 +1613,7 @@ func (l *LanguageService) getCompletionData( return &completionDataData{ symbols: symbols, + autoImports: autoImports, completionKind: completionKind, isInSnippetScope: isInSnippetScope, propertyAccessToConvert: propertyAccessToConvert, @@ -1781,7 +1635,7 @@ func (l *LanguageService) getCompletionData( importStatementCompletion: importStatementCompletion, hasUnresolvedAutoImports: hasUnresolvedAutoImports, defaultCommitCharacters: defaultCommitCharacters, - } + }, nil } func keywordCompletionData( @@ -1853,6 +1707,7 @@ func (l *LanguageService) completionInfoFromData( uniqueNames, sortedEntries := l.getCompletionEntriesFromSymbols( ctx, + typeChecker, data, nil, /*replacementToken*/ position, @@ -1867,7 +1722,7 @@ func (l *LanguageService) completionInfoFromData( !data.isTypeOnlyLocation && isContextualKeywordInAutoImportableExpressionSpace(keywordEntry.Label) || !uniqueNames.Has(keywordEntry.Label) { uniqueNames.Add(keywordEntry.Label) - sortedEntries = core.InsertSorted(sortedEntries, keywordEntry, compareCompletionEntries) + sortedEntries = append(sortedEntries, keywordEntry) } } } @@ -1875,14 +1730,14 @@ func (l *LanguageService) completionInfoFromData( for _, keywordEntry := range getContextualKeywords(file, contextToken, position) { if !uniqueNames.Has(keywordEntry.Label) { uniqueNames.Add(keywordEntry.Label) - sortedEntries = core.InsertSorted(sortedEntries, keywordEntry, compareCompletionEntries) + sortedEntries = append(sortedEntries, keywordEntry) } } for _, literal := range literals { literalEntry := createCompletionItemForLiteral(file, preferences, literal) uniqueNames.Add(literalEntry.Label) - sortedEntries = core.InsertSorted(sortedEntries, literalEntry, compareCompletionEntries) + sortedEntries = append(sortedEntries, literalEntry) } if !isChecked { @@ -1915,6 +1770,7 @@ func (l *LanguageService) completionInfoFromData( func (l *LanguageService) getCompletionEntriesFromSymbols( ctx context.Context, + typeChecker *checker.Checker, data *completionDataData, replacementToken *ast.Node, position int, @@ -1923,9 +1779,8 @@ func (l *LanguageService) getCompletionEntriesFromSymbols( ) (uniqueNames collections.Set[string], sortedEntries []*lsproto.CompletionItem) { closestSymbolDeclaration := getClosestSymbolDeclaration(data.contextToken, data.location) useSemicolons := lsutil.ProbablyUsesSemicolons(file) - typeChecker, done := l.GetProgram().GetTypeCheckerForFile(ctx, file) - defer done() isMemberCompletion := isMemberCompletionKind(data.completionKind) + sortedEntries = slices.Grow(sortedEntries, len(data.symbols)+len(data.autoImports)) // Tracks unique names. // Value is set to false for global variables or completions from external module exports, because we can have multiple of those; // true otherwise. Based on the order we add things we will always see locals first, then globals, then module exports. @@ -1987,7 +1842,55 @@ func (l *LanguageService) getCompletionEntriesFromSymbols( !(symbol.Parent == nil && !core.Some(symbol.Declarations, func(d *ast.Node) bool { return ast.GetSourceFileOfNode(d) == file })) uniques[name] = shouldShadowLaterSymbols - sortedEntries = core.InsertSorted(sortedEntries, entry, compareCompletionEntries) + sortedEntries = append(sortedEntries, entry) + } + + for _, autoImport := range data.autoImports { + // !!! check for type-only in JS + // !!! deprecation + + if data.importStatementCompletion != nil { + // !!! + continue + } + + if !autoImport.Export.IsUnresolvedAlias() { + if data.isTypeOnlyLocation { + if autoImport.Export.Flags&ast.SymbolFlagsType == 0 && autoImport.Export.Flags&ast.SymbolFlagsModule == 0 { + continue + } + } else if autoImport.Export.Flags&ast.SymbolFlagsValue == 0 { + continue + } + } + + entry := l.createLSPCompletionItem( + ctx, + autoImport.Fix.Name, + "", + "", + SortTextAutoImportSuggestions, + autoImport.Export.ScriptElementKind, + autoImport.Export.ScriptElementKindModifiers, + nil, + nil, + &lsproto.CompletionItemLabelDetails{ + Description: ptrTo(autoImport.Fix.ModuleSpecifier), + }, + file, + position, + false, /*isMemberCompletion*/ + false, /*isSnippet*/ + true, /*hasAction*/ + false, /*preselect*/ + autoImport.Fix.ModuleSpecifier, + autoImport.Fix.AutoImportFix, + ) + + if isShadowed, _ := uniques[autoImport.Fix.Name]; !isShadowed { + uniques[autoImport.Fix.Name] = false + sortedEntries = append(sortedEntries, entry) + } } uniqueSet := collections.NewSetWithSizeHint[string](len(uniques)) @@ -2043,7 +1946,6 @@ func (l *LanguageService) createCompletionItem( compilerOptions *core.CompilerOptions, isMemberCompletion bool, ) *lsproto.CompletionItem { - clientOptions := lsproto.GetClientCapabilities(ctx).TextDocument.Completion contextToken := data.contextToken var insertText string var filterText string @@ -2141,46 +2043,6 @@ func (l *LanguageService) createCompletionItem( file) } - if originIsExport(origin) { - resolvedOrigin := origin.asExport() - labelDetails = &lsproto.CompletionItemLabelDetails{ - Description: &resolvedOrigin.moduleSpecifier, // !!! vscode @link support - } - if data.importStatementCompletion != nil { - 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(clientOptions.CompletionItem.SnippetSupport, "$1", "") - importKind := getImportKind(file, exportKind, l.GetProgram(), 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 = clientOptions.CompletionItem.SnippetSupport - } - } - if originIsTypeOnlyAlias(origin) { hasAction = true } @@ -2264,12 +2126,6 @@ func (l *LanguageService) createCompletionItem( } } - var autoImportData *lsproto.AutoImportData - if originIsExport(origin) { - autoImportData = origin.toCompletionEntryData() - hasAction = data.importStatementCompletion == nil - } - parentNamedImportOrExport := ast.FindAncestor(data.location, isNamedImportsOrExports) if parentNamedImportOrExport != nil { if !scanner.IsIdentifierText(name, core.LanguageVariantStandard) { @@ -2288,7 +2144,7 @@ func (l *LanguageService) createCompletionItem( } else if parentNamedImportOrExport.Kind == ast.KindNamedImports { possibleToken := scanner.StringToToken(name) if possibleToken != ast.KindUnknown && - (possibleToken == ast.KindAwaitKeyword || isNonContextualKeyword(possibleToken)) { + (possibleToken == ast.KindAwaitKeyword || lsutil.IsNonContextualKeyword(possibleToken)) { insertText = fmt.Sprintf("%s as %s_", name, name) } } @@ -2296,10 +2152,10 @@ func (l *LanguageService) createCompletionItem( // Commit characters - elementKind := getSymbolKind(typeChecker, symbol, data.location) + elementKind := lsutil.GetSymbolKind(typeChecker, symbol, data.location) var commitCharacters *[]string if clientSupportsItemCommitCharacters(ctx) { - if elementKind == ScriptElementKindWarning || elementKind == ScriptElementKindString { + if elementKind == lsutil.ScriptElementKindWarning || elementKind == lsutil.ScriptElementKindString { commitCharacters = &[]string{} } else if !clientSupportsDefaultCommitCharacters(ctx) { commitCharacters = ptrTo(data.defaultCommitCharacters) @@ -2308,7 +2164,7 @@ func (l *LanguageService) createCompletionItem( } preselect := isRecommendedCompletionMatch(symbol, data.recommendedCompletion, typeChecker) - kindModifiers := getSymbolModifiers(typeChecker, symbol) + kindModifiers := lsutil.GetSymbolModifiers(typeChecker, symbol) return l.createLSPCompletionItem( ctx, @@ -2328,7 +2184,7 @@ func (l *LanguageService) createCompletionItem( hasAction, preselect, source, - autoImportData, + nil, ) } @@ -2531,7 +2387,7 @@ func isClassLikeMemberCompletion(symbol *ast.Symbol, location *ast.Node, file *a } func symbolAppearsToBeTypeOnly(symbol *ast.Symbol, typeChecker *checker.Checker) bool { - flags := checker.GetCombinedLocalAndExportSymbolFlags(checker.SkipAlias(symbol, typeChecker)) + flags := checker.SkipAlias(symbol, typeChecker).CombinedLocalAndExportSymbolFlags() return flags&ast.SymbolFlagsValue == 0 && (len(symbol.Declarations) == 0 || !ast.IsInJSFile(symbol.Declarations[0]) || flags&ast.SymbolFlagsType != 0) } @@ -2603,13 +2459,13 @@ func shouldIncludeSymbol( // Auto Imports are not available for scripts so this conditional is always false. if file.AsSourceFile().ExternalModuleIndicator != nil && compilerOptions.AllowUmdGlobalAccess != core.TSTrue && + symbol != symbolOrigin && data.symbolToSortTextMap[ast.GetSymbolId(symbol)] == SortTextGlobalsOrKeywords && - (data.symbolToSortTextMap[ast.GetSymbolId(symbolOrigin)] == SortTextAutoImportSuggestions || - data.symbolToSortTextMap[ast.GetSymbolId(symbolOrigin)] == SortTextLocationPriority) { + symbol.Parent != nil && checker.IsExternalModuleSymbol(symbol.Parent) { return false } - allFlags = allFlags | checker.GetCombinedLocalAndExportSymbolFlags(symbolOrigin) + allFlags = allFlags | symbolOrigin.CombinedLocalAndExportSymbolFlags() // import m = /**/ <-- It can only access namespace (if typing import = x. this would get member symbols and not namespace) if isInRightSideOfInternalImportEqualsDeclaration(data.location) { @@ -2692,11 +2548,7 @@ func originIsIgnore(origin *symbolOriginInfo) bool { } func originIncludesSymbolName(origin *symbolOriginInfo) bool { - return originIsExport(origin) || originIsComputedPropertyName(origin) -} - -func originIsExport(origin *symbolOriginInfo) bool { - return origin != nil && origin.kind&symbolOriginInfoKindExport != 0 + return originIsComputedPropertyName(origin) } func originIsComputedPropertyName(origin *symbolOriginInfo) bool { @@ -2728,14 +2580,6 @@ func originIsPromise(origin *symbolOriginInfo) bool { } func getSourceFromOrigin(origin *symbolOriginInfo) string { - if originIsExport(origin) { - return stringutil.StripQuotes(ast.SymbolName(origin.asExport().moduleSymbol)) - } - - if originIsExport(origin) { - return origin.asExport().moduleSpecifier - } - if originIsThisType(origin) { return string(completionSourceThisProperty) } @@ -2785,7 +2629,7 @@ func isValidTrigger(file *ast.SourceFile, triggerCharacter CompletionsTriggerCha return false } if ast.IsStringLiteralLike(contextToken) { - return tryGetImportFromModuleSpecifier(contextToken) != nil + return ast.TryGetImportFromModuleSpecifier(contextToken) != nil } return contextToken.Kind == ast.KindLessThanSlashToken && ast.IsJsxClosingElement(contextToken.Parent) case " ": @@ -3264,50 +3108,50 @@ func generateIdentifierForArbitraryString(text string) string { } // Copied from vscode TS extension. -func getCompletionsSymbolKind(kind ScriptElementKind) lsproto.CompletionItemKind { +func getCompletionsSymbolKind(kind lsutil.ScriptElementKind) lsproto.CompletionItemKind { switch kind { - case ScriptElementKindPrimitiveType, ScriptElementKindKeyword: + case lsutil.ScriptElementKindPrimitiveType, lsutil.ScriptElementKindKeyword: return lsproto.CompletionItemKindKeyword - case ScriptElementKindConstElement, ScriptElementKindLetElement, ScriptElementKindVariableElement, - ScriptElementKindLocalVariableElement, ScriptElementKindAlias, ScriptElementKindParameterElement: + case lsutil.ScriptElementKindConstElement, lsutil.ScriptElementKindLetElement, lsutil.ScriptElementKindVariableElement, + lsutil.ScriptElementKindLocalVariableElement, lsutil.ScriptElementKindAlias, lsutil.ScriptElementKindParameterElement: return lsproto.CompletionItemKindVariable - case ScriptElementKindMemberVariableElement, ScriptElementKindMemberGetAccessorElement, - ScriptElementKindMemberSetAccessorElement: + case lsutil.ScriptElementKindMemberVariableElement, lsutil.ScriptElementKindMemberGetAccessorElement, + lsutil.ScriptElementKindMemberSetAccessorElement: return lsproto.CompletionItemKindField - case ScriptElementKindFunctionElement, ScriptElementKindLocalFunctionElement: + case lsutil.ScriptElementKindFunctionElement, lsutil.ScriptElementKindLocalFunctionElement: return lsproto.CompletionItemKindFunction - case ScriptElementKindMemberFunctionElement, ScriptElementKindConstructSignatureElement, - ScriptElementKindCallSignatureElement, ScriptElementKindIndexSignatureElement: + case lsutil.ScriptElementKindMemberFunctionElement, lsutil.ScriptElementKindConstructSignatureElement, + lsutil.ScriptElementKindCallSignatureElement, lsutil.ScriptElementKindIndexSignatureElement: return lsproto.CompletionItemKindMethod - case ScriptElementKindEnumElement: + case lsutil.ScriptElementKindEnumElement: return lsproto.CompletionItemKindEnum - case ScriptElementKindEnumMemberElement: + case lsutil.ScriptElementKindEnumMemberElement: return lsproto.CompletionItemKindEnumMember - case ScriptElementKindModuleElement, ScriptElementKindExternalModuleName: + case lsutil.ScriptElementKindModuleElement, lsutil.ScriptElementKindExternalModuleName: return lsproto.CompletionItemKindModule - case ScriptElementKindClassElement, ScriptElementKindTypeElement: + case lsutil.ScriptElementKindClassElement, lsutil.ScriptElementKindTypeElement: return lsproto.CompletionItemKindClass - case ScriptElementKindInterfaceElement: + case lsutil.ScriptElementKindInterfaceElement: return lsproto.CompletionItemKindInterface - case ScriptElementKindWarning: + case lsutil.ScriptElementKindWarning: return lsproto.CompletionItemKindText - case ScriptElementKindScriptElement: + case lsutil.ScriptElementKindScriptElement: return lsproto.CompletionItemKindFile - case ScriptElementKindDirectory: + case lsutil.ScriptElementKindDirectory: return lsproto.CompletionItemKindFolder - case ScriptElementKindString: + case lsutil.ScriptElementKindString: return lsproto.CompletionItemKindConstant default: @@ -3319,83 +3163,15 @@ func getCompletionsSymbolKind(kind ScriptElementKind) lsproto.CompletionItemKind // So, it's important that we sort those ties in the order we want them displayed if it matters. We don't // strictly need to sort by name or SortText here since clients are going to do it anyway, but we have to // do the work of comparing them so we can sort those ties appropriately. -func compareCompletionEntries(entryInSlice *lsproto.CompletionItem, entryToInsert *lsproto.CompletionItem) int { +func CompareCompletionEntries(a, b *lsproto.CompletionItem) int { compareStrings := stringutil.CompareStringsCaseInsensitiveThenSensitive - result := compareStrings(*entryInSlice.SortText, *entryToInsert.SortText) + result := compareStrings(*a.SortText, *b.SortText) if result == stringutil.ComparisonEqual { - result = compareStrings(entryInSlice.Label, entryToInsert.Label) - } - if result == stringutil.ComparisonEqual && entryInSlice.Data != nil && entryToInsert.Data != nil { - sliceEntryData := entryInSlice.Data - insertEntryData := entryToInsert.Data - if sliceEntryData.AutoImport != nil && sliceEntryData.AutoImport.ModuleSpecifier != "" && - insertEntryData.AutoImport != nil && insertEntryData.AutoImport.ModuleSpecifier != "" { - // Sort same-named auto-imports by module specifier - result = tspath.CompareNumberOfDirectorySeparators( - sliceEntryData.AutoImport.ModuleSpecifier, - insertEntryData.AutoImport.ModuleSpecifier, - ) - if result == stringutil.ComparisonEqual { - result = compareStrings( - sliceEntryData.AutoImport.ModuleSpecifier, - insertEntryData.AutoImport.ModuleSpecifier, - ) - } - } - } - if result == stringutil.ComparisonEqual { - // Fall back to symbol order - if we return `EqualTo`, `insertSorted` will put later symbols first. - return stringutil.ComparisonLessThan + result = compareStrings(a.Label, b.Label) } 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 - lowerCaseRunes := []rune(lowercaseCharacters) - testChar := lowerCaseRunes[characterIndex] - - for _, strChar := range []rune(identifierString) { - if strChar == testChar || strChar == unicode.ToUpper(testChar) { - willMatchFirstChar := prevChar == 0 || // Beginning of word - 'a' <= prevChar && prevChar <= 'z' && 'A' <= strChar && strChar <= 'Z' || // camelCase transition - prevChar == '_' && strChar != '_' // snake_case transition - matchedFirstCharacter = matchedFirstCharacter || willMatchFirstChar - if !matchedFirstCharacter { - continue - } - characterIndex++ - if characterIndex == len(lowerCaseRunes) { - return true - } else { - testChar = lowerCaseRunes[characterIndex] - } - } - prevChar = strChar - } - - // Did not find all characters - return false -} - var ( keywordCompletionsCache = collections.SyncMap[KeywordCompletionFilters, []*lsproto.CompletionItem]{} allKeywordCompletions = sync.OnceValue(func() []*lsproto.CompletionItem { @@ -3590,16 +3366,12 @@ func (l *LanguageService) getJSCompletionEntries( } if !uniqueNames.Has(name) && scanner.IsIdentifierText(name, core.LanguageVariantStandard) { uniqueNames.Add(name) - sortedEntries = core.InsertSorted( - sortedEntries, - &lsproto.CompletionItem{ - Label: name, - Kind: ptrTo(lsproto.CompletionItemKindText), - SortText: ptrTo(string(SortTextJavascriptIdentifiers)), - CommitCharacters: ptrTo([]string{}), - }, - compareCompletionEntries, - ) + sortedEntries = append(sortedEntries, &lsproto.CompletionItem{ + Label: name, + Kind: ptrTo(lsproto.CompletionItemKindText), + SortText: ptrTo(string(SortTextJavascriptIdentifiers)), + CommitCharacters: ptrTo([]string{}), + }) } } return sortedEntries @@ -4498,8 +4270,8 @@ func (l *LanguageService) getJsxClosingTagCompletion( "", /*insertText*/ "", /*filterText*/ SortTextLocationPriority, - ScriptElementKindClassElement, - collections.Set[ScriptElementKindModifier]{}, /*kindModifiers*/ + lsutil.ScriptElementKindClassElement, + collections.Set[lsutil.ScriptElementKindModifier]{}, /*kindModifiers*/ nil, /*replacementSpan*/ nil, /*commitCharacters*/ nil, /*labelDetails*/ @@ -4535,8 +4307,8 @@ func (l *LanguageService) createLSPCompletionItem( insertText string, filterText string, sortText SortText, - elementKind ScriptElementKind, - kindModifiers collections.Set[ScriptElementKindModifier], + elementKind lsutil.ScriptElementKind, + kindModifiers collections.Set[lsutil.ScriptElementKindModifier], replacementSpan *lsproto.Range, commitCharacters *[]string, labelDetails *lsproto.CompletionItemLabelDetails, @@ -4547,7 +4319,7 @@ func (l *LanguageService) createLSPCompletionItem( hasAction bool, preselect bool, source string, - autoImportEntryData *lsproto.AutoImportData, + autoImportFix *lsproto.AutoImportFix, ) *lsproto.CompletionItem { kind := getCompletionsSymbolKind(elementKind) data := &lsproto.CompletionItemData{ @@ -4555,7 +4327,7 @@ func (l *LanguageService) createLSPCompletionItem( Position: int32(position), Source: source, Name: name, - AutoImport: autoImportEntryData, + AutoImport: autoImportFix, } // Text edit @@ -4582,7 +4354,7 @@ func (l *LanguageService) createLSPCompletionItem( var tags *[]lsproto.CompletionItemTag var detail *string // Copied from vscode ts extension: `MyCompletionItem.constructor`. - if kindModifiers.Has(ScriptElementKindModifierOptional) { + if kindModifiers.Has(lsutil.ScriptElementKindModifierOptional) { if insertText == "" { insertText = name } @@ -4591,11 +4363,11 @@ func (l *LanguageService) createLSPCompletionItem( } name = name + "?" } - if kindModifiers.Has(ScriptElementKindModifierDeprecated) { + if kindModifiers.Has(lsutil.ScriptElementKindModifierDeprecated) { tags = &[]lsproto.CompletionItemTag{lsproto.CompletionItemTagDeprecated} } if kind == lsproto.CompletionItemKindFile { - for _, extensionModifier := range fileExtensionKindModifiers { + for _, extensionModifier := range lsutil.FileExtensionKindModifiers { if kindModifiers.Has(extensionModifier) { if strings.HasSuffix(name, string(extensionModifier)) { detail = ptrTo(name) @@ -4684,8 +4456,8 @@ func (l *LanguageService) getLabelStatementCompletions( "", /*insertText*/ "", /*filterText*/ SortTextLocationPriority, - ScriptElementKindLabel, - collections.Set[ScriptElementKindModifier]{}, /*kindModifiers*/ + lsutil.ScriptElementKindLabel, + collections.Set[lsutil.ScriptElementKindModifier]{}, /*kindModifiers*/ nil, /*replacementSpan*/ nil, /*commitCharacters*/ nil, /*labelDetails*/ @@ -4972,16 +4744,6 @@ func getArgumentInfoForCompletions(node *ast.Node, position int, file *ast.Sourc } } -func autoImportDataToSymbolOriginExport(d *lsproto.AutoImportData, symbolName string, moduleSymbol *ast.Symbol, isDefaultExport bool) *symbolOriginInfoExport { - return &symbolOriginInfoExport{ - 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 // have a unique name/source combination, because those two fields @@ -5020,7 +4782,9 @@ func (l *LanguageService) ResolveCompletionItem( return nil, fmt.Errorf("file not found: %s", data.FileName) } - return l.getCompletionItemDetails(ctx, program, int(data.Position), file, item, data), nil + checker, done := program.GetTypeCheckerForFile(ctx, file) + defer done() + return l.getCompletionItemDetails(ctx, program, checker, int(data.Position), file, item, data), nil } func getCompletionDocumentationFormat(ctx context.Context) lsproto.MarkupKind { @@ -5030,13 +4794,12 @@ func getCompletionDocumentationFormat(ctx context.Context) lsproto.MarkupKind { func (l *LanguageService) getCompletionItemDetails( ctx context.Context, program *compiler.Program, + checker *checker.Checker, position int, file *ast.SourceFile, item *lsproto.CompletionItem, data *lsproto.CompletionItemData, ) *lsproto.CompletionItem { - checker, done := program.GetTypeCheckerForFile(ctx, file) - defer done() docFormat := getCompletionDocumentationFormat(ctx) contextToken, previousToken := getRelevantTokens(position, file) if IsInString(file, position, previousToken) { @@ -5052,6 +4815,13 @@ func (l *LanguageService) getCompletionItemDetails( ) } + if data.AutoImport != nil { + edits, description := (&autoimport.Fix{AutoImportFix: data.AutoImport}).Edits(ctx, file, program.Options(), l.FormatOptions(), l.converters, l.UserPreferences()) + item.AdditionalTextEdits = &edits + item.Detail = strPtrTo(description) + return item + } + // Compute all the completion symbols again. symbolCompletion := l.getSymbolCompletionFromItemData( ctx, @@ -5084,13 +4854,12 @@ func (l *LanguageService) getCompletionItemDetails( } case symbolCompletion.symbol != nil: symbolDetails := symbolCompletion.symbol - actions := l.getCompletionItemActions(ctx, checker, file, position, data, symbolDetails) return l.createCompletionDetailsForSymbol( item, symbolDetails.symbol, checker, symbolDetails.location, - actions, + nil, docFormat, ) case symbolCompletion.literal != nil: @@ -5139,17 +4908,12 @@ func (l *LanguageService) getSymbolCompletionFromItemData( cases: &struct{}{}, } } - if itemData.AutoImport != nil { - 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, err := l.getCompletionData(ctx, ch, file, position, &lsutil.UserPreferences{IncludeCompletionsForModuleExports: core.TSTrue, IncludeCompletionsForImportStatements: core.TSTrue}) + if err != nil { + panic(err) } - completionData := l.getCompletionData(ctx, ch, file, position, &lsutil.UserPreferences{IncludeCompletionsForModuleExports: core.TSTrue, IncludeCompletionsForImportStatements: core.TSTrue}) if completionData == nil { return detailsData{} } @@ -5204,49 +4968,6 @@ func (l *LanguageService) getSymbolCompletionFromItemData( return detailsData{} } -func (l *LanguageService) getAutoImportSymbolFromCompletionEntryData(ch *checker.Checker, name string, autoImportData *lsproto.AutoImportData) *symbolDetails { - containingProgram := l.GetProgram() // !!! isPackageJson ? packageJsonAutoimportProvider : program - var moduleSymbol *ast.Symbol - if autoImportData.AmbientModuleName != "" { - moduleSymbol = ch.TryFindAmbientModule(autoImportData.AmbientModuleName) - } else if autoImportData.FileName != "" { - 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{ - kind: symbolOriginInfoKindExport, - fileName: autoImportData.FileName, - isFromPackageJson: autoImportData.IsPackageJsonImport, - isDefaultExport: isDefaultExport, - data: autoImportDataToSymbolOriginExport(autoImportData, name, moduleSymbol, isDefaultExport), - } - - return &symbolDetails{symbol: symbol, origin: origin} -} - func createSimpleDetails( item *lsproto.CompletionItem, name string, @@ -5305,60 +5026,6 @@ func (l *LanguageService) createCompletionDetailsForSymbol( return createCompletionDetails(item, strings.Join(details, "\n\n"), documentation, docFormat) } -// !!! snippets -func (l *LanguageService) getCompletionItemActions(ctx context.Context, ch *checker.Checker, file *ast.SourceFile, position int, itemData *lsproto.CompletionItemData, symbolDetails *symbolDetails) []codeAction { - if itemData.AutoImport != nil && 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 || symbolDetails.origin.data == nil { - return nil - } - - symbol := symbolDetails.symbol - if symbol.ExportSymbol != nil { - symbol = symbol.ExportSymbol - } - targetSymbol := ch.GetMergedSymbol(ch.SkipAlias(symbol)) - isJsxOpeningTagName := symbolDetails.contextToken != nil && 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() - - var exportMapkey lsproto.ExportInfoMapKey - if itemData.AutoImport != nil { - exportMapkey = itemData.AutoImport.ExportMapKey - } - moduleSpecifier, importCompletionAction := l.getImportCompletionAction( - ctx, - ch, - targetSymbol, - moduleSymbol, - file, - position, - exportMapkey, - itemData.Name, - isJsxOpeningTagName, - // formatContext, - ) - - if !(moduleSpecifier == itemData.AutoImport.ModuleSpecifier || itemData.AutoImport.ModuleSpecifier == "") { - panic("") - } - return []codeAction{importCompletionAction} -} - func (l *LanguageService) getImportStatementCompletionInfo(contextToken *ast.Node, sourceFile *ast.SourceFile) importStatementCompletionInfo { result := importStatementCompletionInfo{} var candidate *ast.Node @@ -5516,7 +5183,7 @@ func getPotentiallyInvalidImportSpecifier(namedBindings *ast.NamedImportBindings return nil } return core.Find(namedBindings.Elements(), func(e *ast.Node) bool { - return e.PropertyName() == nil && isNonContextualKeyword(scanner.StringToToken(e.Name().Text())) && + return e.PropertyName() == nil && lsutil.IsNonContextualKeyword(scanner.StringToToken(e.Name().Text())) && astnav.FindPrecedingToken(ast.GetSourceFileOfNode(namedBindings), e.Name().Pos()).Kind != ast.KindCommaToken }) } @@ -5896,9 +5563,9 @@ func getJSDocParamAnnotation( inferredType := typeChecker.GetTypeAtLocation(initializer.Parent) if inferredType.Flags()&(checker.TypeFlagsAny|checker.TypeFlagsVoid) == 0 { file := ast.GetSourceFileOfNode(initializer) - quotePreference := getQuotePreference(file, preferences) + quotePreference := lsutil.GetQuotePreference(file, preferences) builderFlags := core.IfElse( - quotePreference == quotePreferenceSingle, + quotePreference == lsutil.QuotePreferenceSingle, nodebuilder.FlagsUseSingleQuotesForStringLiteralType, nodebuilder.FlagsNone, ) diff --git a/internal/ls/findallreferences.go b/internal/ls/findallreferences.go index 5744271afa..13b847c003 100644 --- a/internal/ls/findallreferences.go +++ b/internal/ls/findallreferences.go @@ -198,7 +198,7 @@ func getContextNodeForNodeEntry(node *ast.Node) *ast.Node { case ast.KindJsxSelfClosingElement, ast.KindLabeledStatement, ast.KindBreakStatement, ast.KindContinueStatement: return node.Parent case ast.KindStringLiteral, ast.KindNoSubstitutionTemplateLiteral: - if validImport := tryGetImportFromModuleSpecifier(node); validImport != nil { + if validImport := ast.TryGetImportFromModuleSpecifier(node); validImport != nil { declOrStatement := ast.FindAncestor(validImport, func(*ast.Node) bool { return ast.IsDeclaration(node) || ast.IsStatement(node) || ast.IsJSDocTag(node) }) @@ -736,7 +736,7 @@ func (l *LanguageService) symbolAndEntriesToRename(ctx context.Context, params * defer done() for _, entry := range entries { uri := l.getFileNameOfEntry(entry) - if l.UserPreferences().AllowRenameOfImportPath != core.TSTrue && entry.node != nil && ast.IsStringLiteralLike(entry.node) && tryGetImportFromModuleSpecifier(entry.node) != nil { + if l.UserPreferences().AllowRenameOfImportPath != core.TSTrue && entry.node != nil && ast.IsStringLiteralLike(entry.node) && ast.TryGetImportFromModuleSpecifier(entry.node) != nil { continue } textEdit := &lsproto.TextEdit{ diff --git a/internal/ls/host.go b/internal/ls/host.go index 8f517b787c..a0dc31967a 100644 --- a/internal/ls/host.go +++ b/internal/ls/host.go @@ -2,6 +2,7 @@ package ls import ( "github.com/microsoft/typescript-go/internal/format" + "github.com/microsoft/typescript-go/internal/ls/autoimport" "github.com/microsoft/typescript-go/internal/ls/lsconv" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/sourcemap" @@ -14,4 +15,5 @@ type Host interface { UserPreferences() *lsutil.UserPreferences FormatOptions() *format.FormatCodeSettings GetECMALineInfo(fileName string) *sourcemap.ECMALineInfo + AutoImportRegistry() *autoimport.Registry } diff --git a/internal/ls/importTracker.go b/internal/ls/importTracker.go index 88f6febb4c..630b0b3d83 100644 --- a/internal/ls/importTracker.go +++ b/internal/ls/importTracker.go @@ -25,6 +25,16 @@ type ImportExportSymbol struct { exportInfo *ExportInfo } +type ExportKind int + +const ( + ExportKindNamed ExportKind = 0 + ExportKindDefault ExportKind = 1 + ExportKindExportEquals ExportKind = 2 + ExportKindUMD ExportKind = 3 + ExportKindModule ExportKind = 4 +) + type ExportInfo struct { exportingModuleSymbol *ast.Symbol exportKind ExportKind @@ -87,7 +97,7 @@ func getDirectImportsMap(sourceFiles []*ast.SourceFile, checker *checker.Checker func forEachImport(sourceFile *ast.SourceFile, action func(importStatement *ast.Node, imported *ast.Node)) { if sourceFile.ExternalModuleIndicator != nil || len(sourceFile.Imports()) != 0 { for _, i := range sourceFile.Imports() { - action(importFromModuleSpecifier(i), i) + action(ast.ImportFromModuleSpecifier(i), i) } } else { forEachPossibleImportOrExportStatement(sourceFile.AsNode(), func(node *ast.Node) bool { diff --git a/internal/ls/inlay_hints.go b/internal/ls/inlay_hints.go index 5db495a290..81b87f3858 100644 --- a/internal/ls/inlay_hints.go +++ b/internal/ls/inlay_hints.go @@ -32,7 +32,7 @@ func (l *LanguageService) ProvideInlayHint( } program, file := l.getProgramAndFile(params.TextDocument.Uri) - quotePreference := getQuotePreference(file, userPreferences) + quotePreference := lsutil.GetQuotePreference(file, userPreferences) checker, done := program.GetTypeCheckerForFile(ctx, file) defer done() @@ -53,7 +53,7 @@ type inlayHintState struct { ctx context.Context span core.TextRange preferences *lsutil.InlayHintsPreferences - quotePreference quotePreference + quotePreference lsutil.QuotePreference file *ast.SourceFile checker *checker.Checker converters *lsconv.Converters @@ -772,7 +772,7 @@ func (s *inlayHintState) getNodeDisplayPart(text string, node *ast.Node) *lsprot func (s *inlayHintState) getLiteralText(node *ast.LiteralLikeNode) string { switch node.Kind { case ast.KindStringLiteral: - if s.quotePreference == quotePreferenceSingle { + if s.quotePreference == lsutil.QuotePreferenceSingle { return `'` + printer.EscapeString(node.Text(), printer.QuoteCharSingleQuote) + `'` } return `"` + printer.EscapeString(node.Text(), printer.QuoteCharDoubleQuote) + `"` diff --git a/internal/ls/languageservice.go b/internal/ls/languageservice.go index 2cfe172468..405bb7d7f1 100644 --- a/internal/ls/languageservice.go +++ b/internal/ls/languageservice.go @@ -4,6 +4,7 @@ import ( "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/compiler" "github.com/microsoft/typescript-go/internal/format" + "github.com/microsoft/typescript-go/internal/ls/autoimport" "github.com/microsoft/typescript-go/internal/ls/lsconv" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" @@ -12,6 +13,7 @@ import ( ) type LanguageService struct { + projectPath tspath.Path host Host program *compiler.Program converters *lsconv.Converters @@ -19,10 +21,12 @@ type LanguageService struct { } func NewLanguageService( + projectPath tspath.Path, program *compiler.Program, host Host, ) *LanguageService { return &LanguageService{ + projectPath: projectPath, host: host, program: program, converters: host.Converters(), @@ -84,3 +88,27 @@ func (l *LanguageService) UseCaseSensitiveFileNames() bool { func (l *LanguageService) GetECMALineInfo(fileName string) *sourcemap.ECMALineInfo { return l.host.GetECMALineInfo(fileName) } + +// getPreparedAutoImportView returns an auto-import view for the given file if the registry is prepared +// to provide up-to-date auto-imports for it. If not, it returns ErrNeedsAutoImports. +func (l *LanguageService) getPreparedAutoImportView(fromFile *ast.SourceFile) (*autoimport.View, error) { + registry := l.host.AutoImportRegistry() + if !registry.IsPreparedForImportingFile(fromFile.FileName(), l.projectPath, l.UserPreferences()) { + return nil, ErrNeedsAutoImports + } + + view := autoimport.NewView(registry, fromFile, l.projectPath, l.program, l.UserPreferences().ModuleSpecifierPreferences()) + return view, nil +} + +// getCurrentAutoImportView returns an auto-import view for the given file, based on the current state +// of the auto-import registry, which may or may not be up-to-date. +func (l *LanguageService) getCurrentAutoImportView(fromFile *ast.SourceFile) *autoimport.View { + return autoimport.NewView( + l.host.AutoImportRegistry(), + fromFile, + l.projectPath, + l.program, + l.UserPreferences().ModuleSpecifierPreferences(), + ) +} diff --git a/internal/ls/symbol_display.go b/internal/ls/lsutil/symbol_display.go similarity index 74% rename from internal/ls/symbol_display.go rename to internal/ls/lsutil/symbol_display.go index 328f79bda5..c421d4b6fc 100644 --- a/internal/ls/symbol_display.go +++ b/internal/ls/lsutil/symbol_display.go @@ -1,4 +1,4 @@ -package ls +package lsutil import ( "github.com/microsoft/typescript-go/internal/ast" @@ -7,79 +7,79 @@ import ( "github.com/microsoft/typescript-go/internal/core" ) -type ScriptElementKind string +type ScriptElementKind int const ( - ScriptElementKindUnknown ScriptElementKind = "" - ScriptElementKindWarning ScriptElementKind = "warning" + ScriptElementKindUnknown ScriptElementKind = iota + ScriptElementKindWarning // predefined type (void) or keyword (class) - ScriptElementKindKeyword ScriptElementKind = "keyword" + ScriptElementKindKeyword // top level script node - ScriptElementKindScriptElement ScriptElementKind = "script" + ScriptElementKindScriptElement // module foo {} - ScriptElementKindModuleElement ScriptElementKind = "module" + ScriptElementKindModuleElement // class X {} - ScriptElementKindClassElement ScriptElementKind = "class" + ScriptElementKindClassElement // var x = class X {} - ScriptElementKindLocalClassElement ScriptElementKind = "local class" + ScriptElementKindLocalClassElement // interface Y {} - ScriptElementKindInterfaceElement ScriptElementKind = "interface" + ScriptElementKindInterfaceElement // type T = ... - ScriptElementKindTypeElement ScriptElementKind = "type" + ScriptElementKindTypeElement // enum E {} - ScriptElementKindEnumElement ScriptElementKind = "enum" - ScriptElementKindEnumMemberElement ScriptElementKind = "enum member" + ScriptElementKindEnumElement + ScriptElementKindEnumMemberElement // Inside module and script only. // const v = ... - ScriptElementKindVariableElement ScriptElementKind = "var" + ScriptElementKindVariableElement // Inside function. - ScriptElementKindLocalVariableElement ScriptElementKind = "local var" + ScriptElementKindLocalVariableElement // using foo = ... - ScriptElementKindVariableUsingElement ScriptElementKind = "using" + ScriptElementKindVariableUsingElement // await using foo = ... - ScriptElementKindVariableAwaitUsingElement ScriptElementKind = "await using" + ScriptElementKindVariableAwaitUsingElement // Inside module and script only. // function f() {} - ScriptElementKindFunctionElement ScriptElementKind = "function" + ScriptElementKindFunctionElement // Inside function. - ScriptElementKindLocalFunctionElement ScriptElementKind = "local function" + ScriptElementKindLocalFunctionElement // class X { [public|private]* foo() {} } - ScriptElementKindMemberFunctionElement ScriptElementKind = "method" + ScriptElementKindMemberFunctionElement // class X { [public|private]* [get|set] foo:number; } - ScriptElementKindMemberGetAccessorElement ScriptElementKind = "getter" - ScriptElementKindMemberSetAccessorElement ScriptElementKind = "setter" + ScriptElementKindMemberGetAccessorElement + ScriptElementKindMemberSetAccessorElement // class X { [public|private]* foo:number; } // interface Y { foo:number; } - ScriptElementKindMemberVariableElement ScriptElementKind = "property" + ScriptElementKindMemberVariableElement // class X { [public|private]* accessor foo: number; } - ScriptElementKindMemberAccessorVariableElement ScriptElementKind = "accessor" + ScriptElementKindMemberAccessorVariableElement // class X { constructor() { } } // class X { static { } } - ScriptElementKindConstructorImplementationElement ScriptElementKind = "constructor" + ScriptElementKindConstructorImplementationElement // interface Y { ():number; } - ScriptElementKindCallSignatureElement ScriptElementKind = "call" + ScriptElementKindCallSignatureElement // interface Y { []:number; } - ScriptElementKindIndexSignatureElement ScriptElementKind = "index" + ScriptElementKindIndexSignatureElement // interface Y { new():Y; } - ScriptElementKindConstructSignatureElement ScriptElementKind = "construct" + ScriptElementKindConstructSignatureElement // function foo(*Y*: string) - ScriptElementKindParameterElement ScriptElementKind = "parameter" - ScriptElementKindTypeParameterElement ScriptElementKind = "type parameter" - ScriptElementKindPrimitiveType ScriptElementKind = "primitive type" - ScriptElementKindLabel ScriptElementKind = "label" - ScriptElementKindAlias ScriptElementKind = "alias" - ScriptElementKindConstElement ScriptElementKind = "const" - ScriptElementKindLetElement ScriptElementKind = "let" - ScriptElementKindDirectory ScriptElementKind = "directory" - ScriptElementKindExternalModuleName ScriptElementKind = "external module name" + ScriptElementKindParameterElement + ScriptElementKindTypeParameterElement + ScriptElementKindPrimitiveType + ScriptElementKindLabel + ScriptElementKindAlias + ScriptElementKindConstElement + ScriptElementKindLetElement + ScriptElementKindDirectory + ScriptElementKindExternalModuleName // String literal - ScriptElementKindString ScriptElementKind = "string" + ScriptElementKindString // Jsdoc @link: in `{@link C link text}`, the before and after text "{@link " and "}" - ScriptElementKindLink ScriptElementKind = "link" + ScriptElementKindLink // Jsdoc @link: in `{@link C link text}`, the entity name "C" - ScriptElementKindLinkName ScriptElementKind = "link name" + ScriptElementKindLinkName // Jsdoc @link: in `{@link C link text}`, the link text "link text" - ScriptElementKindLinkText ScriptElementKind = "link text" + ScriptElementKindLinkText ) type ScriptElementKindModifier string @@ -109,7 +109,7 @@ const ( ScriptElementKindModifierCjs ScriptElementKindModifier = ".cjs" ) -var fileExtensionKindModifiers = []ScriptElementKindModifier{ +var FileExtensionKindModifiers = []ScriptElementKindModifier{ ScriptElementKindModifierDts, ScriptElementKindModifierTs, ScriptElementKindModifierTsx, @@ -124,13 +124,12 @@ var fileExtensionKindModifiers = []ScriptElementKindModifier{ ScriptElementKindModifierCjs, } -func getSymbolKind(typeChecker *checker.Checker, symbol *ast.Symbol, location *ast.Node) ScriptElementKind { +func GetSymbolKind(typeChecker *checker.Checker, symbol *ast.Symbol, location *ast.Node) ScriptElementKind { result := getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker, symbol, location) if result != ScriptElementKindUnknown { return result } - - flags := checker.GetCombinedLocalAndExportSymbolFlags(symbol) + flags := symbol.CombinedLocalAndExportSymbolFlags() if flags&ast.SymbolFlagsClass != 0 { decl := ast.GetDeclarationOfKind(symbol, ast.KindClassExpression) if decl != nil { @@ -164,27 +163,35 @@ func getSymbolKind(typeChecker *checker.Checker, symbol *ast.Symbol, location *a } func getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker *checker.Checker, symbol *ast.Symbol, location *ast.Node) ScriptElementKind { - roots := typeChecker.GetRootSymbols(symbol) + var roots []*ast.Symbol + if typeChecker != nil { + roots = typeChecker.GetRootSymbols(symbol) + } else { + roots = []*ast.Symbol{symbol} + } + // If this is a method from a mapped type, leave as a method so long as it still has a call signature, as opposed to e.g. // `{ [K in keyof I]: number }`. if len(roots) == 1 && roots[0].Flags&ast.SymbolFlagsMethod != 0 && - len(typeChecker.GetCallSignatures(typeChecker.GetNonNullableType(typeChecker.GetTypeOfSymbolAtLocation(symbol, location)))) > 0 { + (typeChecker == nil || len(typeChecker.GetCallSignatures(typeChecker.GetNonNullableType(typeChecker.GetTypeOfSymbolAtLocation(symbol, location)))) > 0) { return ScriptElementKindMemberFunctionElement } - if typeChecker.IsUndefinedSymbol(symbol) { - return ScriptElementKindVariableElement - } - if typeChecker.IsArgumentsSymbol(symbol) { - return ScriptElementKindLocalVariableElement - } - if location.Kind == ast.KindThisKeyword && ast.IsExpression(location) || - ast.IsThisInTypeQuery(location) { - return ScriptElementKindParameterElement + if typeChecker != nil { + if typeChecker.IsUndefinedSymbol(symbol) { + return ScriptElementKindVariableElement + } + if typeChecker.IsArgumentsSymbol(symbol) { + return ScriptElementKindLocalVariableElement + } + if location.Kind == ast.KindThisKeyword && ast.IsExpression(location) || + ast.IsThisInTypeQuery(location) { + return ScriptElementKindParameterElement + } } - flags := checker.GetCombinedLocalAndExportSymbolFlags(symbol) + flags := symbol.CombinedLocalAndExportSymbolFlags() if flags&ast.SymbolFlagsVariable != 0 { if isFirstDeclarationOfSymbolParameter(symbol) { return ScriptElementKindParameterElement @@ -227,8 +234,7 @@ func getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker * } if flags&ast.SymbolFlagsProperty != 0 { - if flags&ast.SymbolFlagsTransient != 0 && - symbol.CheckFlags&ast.CheckFlagsSynthetic != 0 { + if typeChecker != nil && flags&ast.SymbolFlagsTransient != 0 && symbol.CheckFlags&ast.CheckFlagsSynthetic != 0 { // If union property is result of union of non method (property/accessors/variables), it is labeled as property var unionPropertyKind ScriptElementKind for _, rootSymbol := range roots { @@ -305,13 +311,13 @@ func isLocalVariableOrFunction(symbol *ast.Symbol) bool { return false } -func getSymbolModifiers(typeChecker *checker.Checker, symbol *ast.Symbol) collections.Set[ScriptElementKindModifier] { +func GetSymbolModifiers(typeChecker *checker.Checker, symbol *ast.Symbol) collections.Set[ScriptElementKindModifier] { if symbol == nil { return collections.Set[ScriptElementKindModifier]{} } modifiers := getNormalizedSymbolModifiers(typeChecker, symbol) - if symbol.Flags&ast.SymbolFlagsAlias != 0 { + if symbol.Flags&ast.SymbolFlagsAlias != 0 && typeChecker != nil { resolvedSymbol := typeChecker.GetAliasedSymbol(symbol) if resolvedSymbol != symbol { aliasModifiers := getNormalizedSymbolModifiers(typeChecker, resolvedSymbol) @@ -335,8 +341,8 @@ func getNormalizedSymbolModifiers(typeChecker *checker.Checker, symbol *ast.Symb // omit deprecated flag if some declarations are not deprecated var excludeFlags ast.ModifierFlags if len(declarations) > 0 && - typeChecker.IsDeprecatedDeclaration(declaration) && // !!! include jsdoc node flags - core.Some(declarations, func(d *ast.Node) bool { return !typeChecker.IsDeprecatedDeclaration(d) }) { + isDeprecatedDeclaration(typeChecker, declaration) && // !!! include jsdoc node flags + core.Some(declarations, func(d *ast.Node) bool { return !isDeprecatedDeclaration(typeChecker, d) }) { excludeFlags = ast.ModifierFlagsDeprecated } else { excludeFlags = ast.ModifierFlagsNone @@ -347,6 +353,13 @@ func getNormalizedSymbolModifiers(typeChecker *checker.Checker, symbol *ast.Symb return modifierSet } +func isDeprecatedDeclaration(typeChecker *checker.Checker, declaration *ast.Node) bool { + if typeChecker != nil { + return typeChecker.IsDeprecatedDeclaration(declaration) + } + return ast.GetCombinedNodeFlags(declaration)&ast.NodeFlagsDeprecated != 0 +} + func getNodeModifiers(node *ast.Node, excludeFlags ast.ModifierFlags) collections.Set[ScriptElementKindModifier] { var result collections.Set[ScriptElementKindModifier] var flags ast.ModifierFlags diff --git a/internal/ls/lsutil/userpreferences.go b/internal/ls/lsutil/userpreferences.go index d57197a027..a3568b100f 100644 --- a/internal/ls/lsutil/userpreferences.go +++ b/internal/ls/lsutil/userpreferences.go @@ -4,9 +4,11 @@ import ( "slices" "strings" + "github.com/dlclark/regexp2" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/modulespecifiers" "github.com/microsoft/typescript-go/internal/tsoptions" + "github.com/microsoft/typescript-go/internal/vfs" ) func NewDefaultUserPreferences() *UserPreferences { @@ -362,6 +364,13 @@ func (p *UserPreferences) CopyOrDefault() *UserPreferences { return p.Copy() } +func (p *UserPreferences) OrDefault() *UserPreferences { + if p == nil { + return NewDefaultUserPreferences() + } + return p +} + func (p *UserPreferences) ModuleSpecifierPreferences() modulespecifiers.UserPreferences { return modulespecifiers.UserPreferences{ ImportModuleSpecifierPreference: p.ImportModuleSpecifierPreference, @@ -719,3 +728,26 @@ func (p *UserPreferences) set(name string, value any) { p.CodeLens.ImplementationsCodeLensShowOnAllClassMethods = parseBoolWithDefault(value, false) } } + +func (p *UserPreferences) ParsedAutoImportFileExcludePatterns(useCaseSensitiveFileNames bool) []*regexp2.Regexp { + if len(p.AutoImportFileExcludePatterns) == 0 { + return nil + } + var patterns []*regexp2.Regexp + for _, spec := range p.AutoImportFileExcludePatterns { + pattern := vfs.GetSubPatternFromSpec(spec, "", vfs.UsageExclude, vfs.WildcardMatcher{}) + if pattern != "" { + if re := vfs.GetRegexFromPattern(pattern, useCaseSensitiveFileNames); re != nil { + patterns = append(patterns, re) + } + } + } + return patterns +} + +func (p *UserPreferences) IsModuleSpecifierExcluded(moduleSpecifier string) bool { + if modulespecifiers.IsExcludedByRegex(moduleSpecifier, p.AutoImportSpecifierExcludeRegexes) { + return true + } + return false +} diff --git a/internal/ls/lsutil/utilities.go b/internal/ls/lsutil/utilities.go index b5c6972bcc..4f88c6349f 100644 --- a/internal/ls/lsutil/utilities.go +++ b/internal/ls/lsutil/utilities.go @@ -2,12 +2,15 @@ package lsutil import ( "strings" + "unicode" "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/astnav" "github.com/microsoft/typescript-go/internal/compiler" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/scanner" + "github.com/microsoft/typescript-go/internal/stringutil" + "github.com/microsoft/typescript-go/internal/tspath" ) func ProbablyUsesSemicolons(file *ast.SourceFile) bool { @@ -68,16 +71,83 @@ func ProbablyUsesSemicolons(file *ast.SourceFile) bool { return withSemicolon/withoutSemicolon > 1/nStatementsToObserve } -func ShouldUseUriStyleNodeCoreModules(file *ast.SourceFile, program *compiler.Program) bool { +func ShouldUseUriStyleNodeCoreModules(file *ast.SourceFile, program *compiler.Program) core.Tristate { for _, node := range file.Imports() { if core.NodeCoreModules()[node.Text()] && !core.ExclusivelyPrefixedNodeCoreModules[node.Text()] { if strings.HasPrefix(node.Text(), "node:") { - return true + return core.TSTrue } else { - return false + return core.TSFalse } } } return program.UsesUriStyleNodeCoreModules() } + +func QuotePreferenceFromString(str *ast.StringLiteral) QuotePreference { + if str.TokenFlags&ast.TokenFlagsSingleQuote != 0 { + return QuotePreferenceSingle + } + return QuotePreferenceDouble +} + +func GetQuotePreference(sourceFile *ast.SourceFile, preferences *UserPreferences) QuotePreference { + if preferences.QuotePreference != "" && preferences.QuotePreference != "auto" { + if preferences.QuotePreference == "single" { + return QuotePreferenceSingle + } + return QuotePreferenceDouble + } + // ignore synthetic import added when importHelpers: true + firstModuleSpecifier := core.Find(sourceFile.Imports(), func(n *ast.Node) bool { + return ast.IsStringLiteral(n) && !ast.NodeIsSynthesized(n.Parent) + }) + if firstModuleSpecifier != nil { + return QuotePreferenceFromString(firstModuleSpecifier.AsStringLiteral()) + } + return QuotePreferenceDouble +} + +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 len(baseNameRunes) > 0 && 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 IsNonContextualKeyword(token ast.Kind) bool { + return ast.IsKeywordKind(token) && !ast.IsContextualKeyword(token) +} diff --git a/internal/ls/string_completions.go b/internal/ls/string_completions.go index a27b3768ef..37dedaa7a9 100644 --- a/internal/ls/string_completions.go +++ b/internal/ls/string_completions.go @@ -11,6 +11,7 @@ import ( "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/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/printer" "github.com/microsoft/typescript-go/internal/tspath" @@ -28,8 +29,8 @@ type completionsFromProperties struct { type pathCompletion struct { name string - // ScriptElementKindScriptElement | ScriptElementKindDirectory | ScriptElementKindExternalModuleName - kind ScriptElementKind + // lsutil.ScriptElementKindScriptElement | lsutil.ScriptElementKindDirectory | lsutil.ScriptElementKindExternalModuleName + kind lsutil.ScriptElementKind extension string textRange *core.TextRange } @@ -45,6 +46,7 @@ func (l *LanguageService) getStringLiteralCompletions( file *ast.SourceFile, position int, contextToken *ast.Node, + checker *checker.Checker, compilerOptions *core.CompilerOptions, ) *lsproto.CompletionList { // !!! reference comment @@ -56,13 +58,16 @@ func (l *LanguageService) getStringLiteralCompletions( ctx, file, contextToken, - position) + position, + checker, + ) return l.convertStringLiteralCompletions( ctx, entries, contextToken, file, position, + checker, compilerOptions, ) } @@ -75,6 +80,7 @@ func (l *LanguageService) convertStringLiteralCompletions( contextToken *ast.StringLiteralLike, file *ast.SourceFile, position int, + typeChecker *checker.Checker, options *core.CompilerOptions, ) *lsproto.CompletionList { if completion == nil { @@ -97,6 +103,7 @@ func (l *LanguageService) convertStringLiteralCompletions( } _, items := l.getCompletionEntriesFromSymbols( ctx, + typeChecker, data, contextToken, /*replacementToken*/ position, @@ -135,8 +142,8 @@ func (l *LanguageService) convertStringLiteralCompletions( "", /*insertText*/ "", /*filterText*/ SortTextLocationPriority, - ScriptElementKindString, - collections.Set[ScriptElementKindModifier]{}, + lsutil.ScriptElementKindString, + collections.Set[lsutil.ScriptElementKindModifier]{}, l.getReplacementRangeForContextToken(file, contextToken, position), nil, /*commitCharacters*/ nil, /*labelDetails*/ @@ -220,9 +227,8 @@ func (l *LanguageService) getStringLiteralCompletionEntries( file *ast.SourceFile, node *ast.StringLiteralLike, position int, + typeChecker *checker.Checker, ) *stringLiteralCompletions { - typeChecker, done := l.GetProgram().GetTypeCheckerForFile(ctx, file) - defer done() parent := walkUpParentheses(node.Parent) switch parent.Kind { case ast.KindLiteralType: @@ -578,36 +584,36 @@ func isRequireCallArgument(node *ast.Node) bool { ast.IsIdentifier(node.Parent.Expression()) && node.Parent.Expression().Text() == "require" } -func kindModifiersFromExtension(extension string) ScriptElementKindModifier { +func kindModifiersFromExtension(extension string) lsutil.ScriptElementKindModifier { switch extension { case tspath.ExtensionDts: - return ScriptElementKindModifierDts + return lsutil.ScriptElementKindModifierDts case tspath.ExtensionJs: - return ScriptElementKindModifierJs + return lsutil.ScriptElementKindModifierJs case tspath.ExtensionJson: - return ScriptElementKindModifierJson + return lsutil.ScriptElementKindModifierJson case tspath.ExtensionJsx: - return ScriptElementKindModifierJsx + return lsutil.ScriptElementKindModifierJsx case tspath.ExtensionTs: - return ScriptElementKindModifierTs + return lsutil.ScriptElementKindModifierTs case tspath.ExtensionTsx: - return ScriptElementKindModifierTsx + return lsutil.ScriptElementKindModifierTsx case tspath.ExtensionDmts: - return ScriptElementKindModifierDmts + return lsutil.ScriptElementKindModifierDmts case tspath.ExtensionMjs: - return ScriptElementKindModifierMjs + return lsutil.ScriptElementKindModifierMjs case tspath.ExtensionMts: - return ScriptElementKindModifierMts + return lsutil.ScriptElementKindModifierMts case tspath.ExtensionDcts: - return ScriptElementKindModifierDcts + return lsutil.ScriptElementKindModifierDcts case tspath.ExtensionCjs: - return ScriptElementKindModifierCjs + return lsutil.ScriptElementKindModifierCjs case tspath.ExtensionCts: - return ScriptElementKindModifierCts + return lsutil.ScriptElementKindModifierCts case tspath.ExtensionTsBuildInfo: panic(fmt.Sprintf("Extension %v is unsupported.", tspath.ExtensionTsBuildInfo)) case "": - return ScriptElementKindModifierNone + return lsutil.ScriptElementKindModifierNone default: panic(fmt.Sprintf("Unexpected extension: %v", extension)) } @@ -673,6 +679,7 @@ func (l *LanguageService) getStringLiteralCompletionDetails( file, contextToken, position, + checker, ) if completions == nil { return item diff --git a/internal/ls/utilities.go b/internal/ls/utilities.go index 9ffa49ae30..3a4989fc70 100644 --- a/internal/ls/utilities.go +++ b/internal/ls/utilities.go @@ -5,7 +5,6 @@ import ( "iter" "slices" "strings" - "unicode" "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/astnav" @@ -19,7 +18,6 @@ import ( "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/scanner" "github.com/microsoft/typescript-go/internal/stringutil" - "github.com/microsoft/typescript-go/internal/tspath" ) var quoteReplacer = strings.NewReplacer("'", `\'`, `\"`, `"`) @@ -44,37 +42,6 @@ func IsInString(sourceFile *ast.SourceFile, position int, previousToken *ast.Nod return false } -func importFromModuleSpecifier(node *ast.Node) *ast.Node { - if result := tryGetImportFromModuleSpecifier(node); result != nil { - return result - } - debug.FailBadSyntaxKind(node.Parent) - return nil -} - -func tryGetImportFromModuleSpecifier(node *ast.StringLiteralLike) *ast.Node { - switch node.Parent.Kind { - case ast.KindImportDeclaration, ast.KindJSImportDeclaration, ast.KindExportDeclaration: - return node.Parent - case ast.KindExternalModuleReference: - return node.Parent.Parent - case ast.KindCallExpression: - if ast.IsImportCall(node.Parent) || ast.IsRequireCall(node.Parent, false /*requireStringLiteralLikeArgument*/) { - return node.Parent - } - return nil - case ast.KindLiteralType: - if !ast.IsStringLiteral(node) { - return nil - } - if ast.IsImportTypeNode(node.Parent.Parent) { - return node.Parent.Parent - } - return nil - } - return nil -} - func isModuleSpecifierLike(node *ast.Node) bool { if !ast.IsStringLiteralLike(node) { return false @@ -100,45 +67,6 @@ 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 len(baseNameRunes) > 0 && 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 { @@ -378,49 +306,14 @@ func (l *LanguageService) createLspPosition(position int, file *ast.SourceFile) func quote(file *ast.SourceFile, preferences *lsutil.UserPreferences, text string) string { // Editors can pass in undefined or empty string - we want to infer the preference in those cases. - quotePreference := getQuotePreference(file, preferences) + quotePreference := lsutil.GetQuotePreference(file, preferences) quoted, _ := core.StringifyJson(text, "" /*prefix*/, "" /*indent*/) - if quotePreference == quotePreferenceSingle { + if quotePreference == lsutil.QuotePreferenceSingle { quoted = quoteReplacer.Replace(stringutil.StripQuotes(quoted)) } return quoted } -type quotePreference int - -const ( - quotePreferenceSingle quotePreference = iota - quotePreferenceDouble -) - -func quotePreferenceFromString(str *ast.StringLiteral) quotePreference { - if str.TokenFlags&ast.TokenFlagsSingleQuote != 0 { - return quotePreferenceSingle - } - return quotePreferenceDouble -} - -func getQuotePreference(sourceFile *ast.SourceFile, preferences *lsutil.UserPreferences) quotePreference { - if preferences.QuotePreference != "" && preferences.QuotePreference != "auto" { - if preferences.QuotePreference == "single" { - return quotePreferenceSingle - } - return quotePreferenceDouble - } - // ignore synthetic import added when importHelpers: true - firstModuleSpecifier := core.Find(sourceFile.Imports(), func(n *ast.Node) bool { - return ast.IsStringLiteral(n) && !ast.NodeIsSynthesized(n.Parent) - }) - if firstModuleSpecifier != nil { - return quotePreferenceFromString(firstModuleSpecifier.AsStringLiteral()) - } - return quotePreferenceDouble -} - -func isNonContextualKeyword(token ast.Kind) bool { - return ast.IsKeywordKind(token) && !ast.IsContextualKeyword(token) -} - var typeKeywords *collections.Set[ast.Kind] = collections.NewSetFromItems( ast.KindAnyKeyword, ast.KindAssertsKeyword, @@ -1590,3 +1483,7 @@ func toContextRange(textRange *core.TextRange, contextFile *ast.SourceFile, cont } return nil } + +func ptrTo[T any](v T) *T { + return &v +} diff --git a/internal/lsp/lsproto/_generate/generate.mts b/internal/lsp/lsproto/_generate/generate.mts index 2ea801f879..660aa6abd0 100644 --- a/internal/lsp/lsproto/_generate/generate.mts +++ b/internal/lsp/lsproto/_generate/generate.mts @@ -51,49 +51,30 @@ const customStructures: Structure[] = [ documentation: "InitializationOptions contains user-provided initialization options.", }, { - name: "ExportInfoMapKey", + name: "AutoImportFix", properties: [ { - name: "symbolName", - type: { kind: "base", name: "string" }, - documentation: "The symbol name.", - omitzeroValue: true, - }, - { - name: "symbolId", - type: { kind: "reference", name: "uint64" }, - documentation: "The symbol ID.", + name: "kind", + type: { kind: "reference", name: "AutoImportFixKind" }, omitzeroValue: true, }, { - name: "ambientModuleName", + name: "name", type: { kind: "base", name: "string" }, - documentation: "The ambient module name.", omitzeroValue: true, }, { - name: "moduleFile", - type: { kind: "base", name: "string" }, - documentation: "The module file path.", - omitzeroValue: true, + name: "importKind", + type: { kind: "reference", name: "ImportKind" }, }, - ], - documentation: "ExportInfoMapKey uniquely identifies an export for auto-import purposes.", - }, - { - name: "AutoImportData", - properties: [ { - name: "exportName", - type: { kind: "base", name: "string" }, - documentation: "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.", + name: "useRequire", + type: { kind: "base", name: "boolean" }, omitzeroValue: true, }, { - name: "exportMapKey", - type: { kind: "reference", name: "ExportInfoMapKey" }, - documentation: "The export map key for this auto-import.", - omitzeroValue: true, + name: "addAsTypeOnly", + type: { kind: "reference", name: "AddAsTypeOnly" }, }, { name: "moduleSpecifier", @@ -102,25 +83,22 @@ const customStructures: Structure[] = [ omitzeroValue: true, }, { - name: "fileName", - type: { kind: "base", name: "string" }, - documentation: "The file name declaring the export's module symbol, if it was an external module.", - omitzeroValue: true, + name: "importIndex", + type: { kind: "base", name: "integer" }, + documentation: "Index of the import to modify when adding to an existing import declaration.", }, { - name: "ambientModuleName", - type: { kind: "base", name: "string" }, - documentation: "The module name (with quotes stripped) of the export's module symbol, if it was an ambient module.", - omitzeroValue: true, + name: "usagePosition", + type: { kind: "reference", name: "Position" }, + optional: true, }, { - name: "isPackageJsonImport", - type: { kind: "base", name: "boolean" }, - documentation: "True if the export was found in the package.json AutoImportProvider.", + name: "namespacePrefix", + type: { kind: "base", name: "string" }, omitzeroValue: true, }, ], - documentation: "AutoImportData contains information about an auto-import suggestion.", + documentation: "AutoImportFix contains information about an auto-import suggestion.", }, { name: "CompletionItemData", @@ -151,7 +129,7 @@ const customStructures: Structure[] = [ }, { name: "autoImport", - type: { kind: "reference", name: "AutoImportData" }, + type: { kind: "reference", name: "AutoImportFix" }, optional: true, documentation: "Auto-import data for this completion item.", }, @@ -193,6 +171,36 @@ const customEnumerations: Enumeration[] = [ }, ], }, + { + name: "AutoImportFixKind", + type: { kind: "base", name: "integer" }, + values: [ + { name: "UseNamespace", value: 0, documentation: "Augment an existing namespace import." }, + { name: "JsdocTypeImport", value: 1, documentation: "Add a JSDoc-only type import." }, + { name: "AddToExisting", value: 2, documentation: "Insert into an existing import declaration." }, + { name: "AddNew", value: 3, documentation: "Create a fresh import statement." }, + { name: "PromoteTypeOnly", value: 4, documentation: "Promote a type-only import when necessary." }, + ], + }, + { + name: "ImportKind", + type: { kind: "base", name: "integer" }, + values: [ + { name: "Named", value: 0, documentation: "Adds a named import." }, + { name: "Default", value: 1, documentation: "Adds a default import." }, + { name: "Namespace", value: 2, documentation: "Adds a namespace import." }, + { name: "CommonJS", value: 3, documentation: "Adds a CommonJS import assignment." }, + ], + }, + { + name: "AddAsTypeOnly", + type: { kind: "base", name: "integer" }, + values: [ + { name: "Allowed", value: 1, documentation: "Import may be marked type-only if needed." }, + { name: "Required", value: 2, documentation: "Import must be marked type-only." }, + { name: "NotAllowed", value: 4, documentation: "Import cannot be marked type-only." }, + ], + }, ]; // Track which custom Data structures were declared explicitly diff --git a/internal/lsp/lsproto/lsp_generated.go b/internal/lsp/lsproto/lsp_generated.go index a93e686a73..de35dcefab 100644 --- a/internal/lsp/lsproto/lsp_generated.go +++ b/internal/lsp/lsproto/lsp_generated.go @@ -21628,40 +21628,116 @@ type InitializationOptions struct { CodeLensShowLocationsCommandName *string `json:"codeLensShowLocationsCommandName,omitzero"` } -// ExportInfoMapKey uniquely identifies an export for auto-import purposes. -type ExportInfoMapKey struct { - // The symbol name. - SymbolName string `json:"symbolName,omitzero"` +// AutoImportFix contains information about an auto-import suggestion. +type AutoImportFix struct { + Kind AutoImportFixKind `json:"kind,omitzero"` - // The symbol ID. - SymbolId uint64 `json:"symbolId,omitzero"` - - // The ambient module name. - AmbientModuleName string `json:"ambientModuleName,omitzero"` + Name string `json:"name,omitzero"` - // The module file path. - ModuleFile string `json:"moduleFile,omitzero"` -} + ImportKind ImportKind `json:"importKind"` -// AutoImportData contains information about an auto-import suggestion. -type AutoImportData struct { - // 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 `json:"exportName,omitzero"` + UseRequire bool `json:"useRequire,omitzero"` - // The export map key for this auto-import. - ExportMapKey ExportInfoMapKey `json:"exportMapKey,omitzero"` + AddAsTypeOnly AddAsTypeOnly `json:"addAsTypeOnly"` // The module specifier for this auto-import. ModuleSpecifier string `json:"moduleSpecifier,omitzero"` - // The file name declaring the export's module symbol, if it was an external module. - FileName string `json:"fileName,omitzero"` + // Index of the import to modify when adding to an existing import declaration. + ImportIndex int32 `json:"importIndex"` + + UsagePosition *Position `json:"usagePosition,omitzero"` + + NamespacePrefix string `json:"namespacePrefix,omitzero"` +} + +var _ json.UnmarshalerFrom = (*AutoImportFix)(nil) + +func (s *AutoImportFix) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + const ( + missingImportKind uint = 1 << iota + missingAddAsTypeOnly + missingImportIndex + _missingLast + ) + missing := _missingLast - 1 + + if k := dec.PeekKind(); k != '{' { + return fmt.Errorf("expected object start, but encountered %v", k) + } + if _, err := dec.ReadToken(); err != nil { + return err + } + + for dec.PeekKind() != '}' { + name, err := dec.ReadValue() + if err != nil { + return err + } + switch string(name) { + case `"kind"`: + if err := json.UnmarshalDecode(dec, &s.Kind); err != nil { + return err + } + case `"name"`: + if err := json.UnmarshalDecode(dec, &s.Name); err != nil { + return err + } + case `"importKind"`: + missing &^= missingImportKind + if err := json.UnmarshalDecode(dec, &s.ImportKind); err != nil { + return err + } + case `"useRequire"`: + if err := json.UnmarshalDecode(dec, &s.UseRequire); err != nil { + return err + } + case `"addAsTypeOnly"`: + missing &^= missingAddAsTypeOnly + if err := json.UnmarshalDecode(dec, &s.AddAsTypeOnly); err != nil { + return err + } + case `"moduleSpecifier"`: + if err := json.UnmarshalDecode(dec, &s.ModuleSpecifier); err != nil { + return err + } + case `"importIndex"`: + missing &^= missingImportIndex + if err := json.UnmarshalDecode(dec, &s.ImportIndex); err != nil { + return err + } + case `"usagePosition"`: + if err := json.UnmarshalDecode(dec, &s.UsagePosition); err != nil { + return err + } + case `"namespacePrefix"`: + if err := json.UnmarshalDecode(dec, &s.NamespacePrefix); err != nil { + return err + } + default: + // Ignore unknown properties. + } + } - // The module name (with quotes stripped) of the export's module symbol, if it was an ambient module. - AmbientModuleName string `json:"ambientModuleName,omitzero"` + if _, err := dec.ReadToken(); err != nil { + return err + } - // True if the export was found in the package.json AutoImportProvider. - IsPackageJsonImport bool `json:"isPackageJsonImport,omitzero"` + if missing != 0 { + var missingProps []string + if missing&missingImportKind != 0 { + missingProps = append(missingProps, "importKind") + } + if missing&missingAddAsTypeOnly != 0 { + missingProps = append(missingProps, "addAsTypeOnly") + } + if missing&missingImportIndex != 0 { + missingProps = append(missingProps, "importIndex") + } + return fmt.Errorf("missing required properties: %s", strings.Join(missingProps, ", ")) + } + + return nil } // CompletionItemData is preserved on a CompletionItem between CompletionRequest and CompletionResolveRequest. @@ -21679,7 +21755,7 @@ type CompletionItemData struct { Name string `json:"name,omitzero"` // Auto-import data for this completion item. - AutoImport *AutoImportData `json:"autoImport,omitzero"` + AutoImport *AutoImportFix `json:"autoImport,omitzero"` } type CodeLensData struct { @@ -22916,6 +22992,88 @@ const ( CodeLensKindImplementations CodeLensKind = "implementations" ) +type AutoImportFixKind int32 + +const ( + // Augment an existing namespace import. + AutoImportFixKindUseNamespace AutoImportFixKind = 0 + // Add a JSDoc-only type import. + AutoImportFixKindJsdocTypeImport AutoImportFixKind = 1 + // Insert into an existing import declaration. + AutoImportFixKindAddToExisting AutoImportFixKind = 2 + // Create a fresh import statement. + AutoImportFixKindAddNew AutoImportFixKind = 3 + // Promote a type-only import when necessary. + AutoImportFixKindPromoteTypeOnly AutoImportFixKind = 4 +) + +const _AutoImportFixKind_name = "UseNamespaceJsdocTypeImportAddToExistingAddNewPromoteTypeOnly" + +var _AutoImportFixKind_index = [...]uint16{0, 12, 27, 40, 46, 61} + +func (e AutoImportFixKind) String() string { + i := int(e) - 0 + if i < 0 || i >= len(_AutoImportFixKind_index)-1 { + return fmt.Sprintf("AutoImportFixKind(%d)", e) + } + return _AutoImportFixKind_name[_AutoImportFixKind_index[i]:_AutoImportFixKind_index[i+1]] +} + +type ImportKind int32 + +const ( + // Adds a named import. + ImportKindNamed ImportKind = 0 + // Adds a default import. + ImportKindDefault ImportKind = 1 + // Adds a namespace import. + ImportKindNamespace ImportKind = 2 + // Adds a CommonJS import assignment. + ImportKindCommonJS ImportKind = 3 +) + +const _ImportKind_name = "NamedDefaultNamespaceCommonJS" + +var _ImportKind_index = [...]uint16{0, 5, 12, 21, 29} + +func (e ImportKind) String() string { + i := int(e) - 0 + if i < 0 || i >= len(_ImportKind_index)-1 { + return fmt.Sprintf("ImportKind(%d)", e) + } + return _ImportKind_name[_ImportKind_index[i]:_ImportKind_index[i+1]] +} + +type AddAsTypeOnly int32 + +const ( + // Import may be marked type-only if needed. + AddAsTypeOnlyAllowed AddAsTypeOnly = 1 + // Import must be marked type-only. + AddAsTypeOnlyRequired AddAsTypeOnly = 2 + // Import cannot be marked type-only. + AddAsTypeOnlyNotAllowed AddAsTypeOnly = 4 +) + +const _AddAsTypeOnly_name = "AllowedRequiredNotAllowed" + +var ( + _AddAsTypeOnly_index_0 = [...]uint16{0, 7, 15} + _AddAsTypeOnly_index_1 = [...]uint16{0, 10} +) + +func (e AddAsTypeOnly) String() string { + switch { + case 1 <= e && e <= 2: + i := int(e) - 1 + return _AddAsTypeOnly_name[0+_AddAsTypeOnly_index_0[i] : 0+_AddAsTypeOnly_index_0[i+1]] + case e == 4: + return _AddAsTypeOnly_name[15:25] + default: + return fmt.Sprintf("AddAsTypeOnly(%d)", e) + } +} + func unmarshalParams(method Method, data []byte) (any, error) { switch method { case MethodTextDocumentImplementation: diff --git a/internal/lsp/server.go b/internal/lsp/server.go index ad30b34eae..21b7fd85bb 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -498,7 +498,10 @@ func (s *Server) handleRequestOrNotification(ctx context.Context, req *lsproto.R ctx = lsproto.WithClientCapabilities(ctx, &s.clientCapabilities) if handler := handlers()[req.Method]; handler != nil { - return handler(s, ctx, req) + start := time.Now() + err := handler(s, ctx, req) + s.logger.Info("handled method '", req.Method, "' in ", time.Since(start)) + return err } s.logger.Warn("unknown method", req.Method) if req.ID != nil { @@ -529,7 +532,6 @@ var handlers = sync.OnceValue(func() handlerMap { registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentHoverInfo, (*Server).handleHover) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentDefinitionInfo, (*Server).handleDefinition) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentTypeDefinitionInfo, (*Server).handleTypeDefinition) - registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentCompletionInfo, (*Server).handleCompletion) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentSignatureHelpInfo, (*Server).handleSignatureHelp) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentFormattingInfo, (*Server).handleDocumentFormat) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentRangeFormattingInfo, (*Server).handleDocumentRangeFormat) @@ -543,6 +545,9 @@ var handlers = sync.OnceValue(func() handlerMap { registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentPrepareCallHierarchyInfo, (*Server).handlePrepareCallHierarchy) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentFoldingRangeInfo, (*Server).handleFoldingRange) + registerLanguageServiceWithAutoImportsRequestHandler(handlers, lsproto.TextDocumentCompletionInfo, (*Server).handleCompletion) + registerLanguageServiceWithAutoImportsRequestHandler(handlers, lsproto.TextDocumentCodeActionInfo, (*Server).handleCodeAction) + registerMultiProjectReferenceRequestHandler(handlers, lsproto.TextDocumentReferencesInfo, (*ls.LanguageService).ProvideReferences) registerMultiProjectReferenceRequestHandler(handlers, lsproto.TextDocumentRenameInfo, (*ls.LanguageService).ProvideRename) registerMultiProjectReferenceRequestHandler(handlers, lsproto.TextDocumentImplementationInfo, (*ls.LanguageService).ProvideImplementations) @@ -626,6 +631,43 @@ func registerLanguageServiceDocumentRequestHandler[Req lsproto.HasTextDocumentUR } } +func registerLanguageServiceWithAutoImportsRequestHandler[Req lsproto.HasTextDocumentURI, Resp any](handlers handlerMap, info lsproto.RequestInfo[Req, Resp], fn func(*Server, context.Context, *ls.LanguageService, Req) (Resp, error)) { + handlers[info.Method] = func(s *Server, ctx context.Context, req *lsproto.RequestMessage) error { + var params Req + // Ignore empty params. + if req.Params != nil { + params = req.Params.(Req) + } + languageService, err := s.session.GetLanguageService(ctx, params.TextDocumentURI()) + if err != nil { + return err + } + defer s.recover(req) + resp, err := fn(s, ctx, languageService, params) + if errors.Is(err, ls.ErrNeedsAutoImports) { + languageService, err = s.session.GetLanguageServiceWithAutoImports(ctx, params.TextDocumentURI()) + if err != nil { + return err + } + if ctx.Err() != nil { + return ctx.Err() + } + resp, err = fn(s, ctx, languageService, params) + if errors.Is(err, ls.ErrNeedsAutoImports) { + panic(info.Method + " returned ErrNeedsAutoImports even after enabling auto imports") + } + } + if err != nil { + return err + } + if ctx.Err() != nil { + return ctx.Err() + } + s.sendResult(req.ID, resp) + return nil + } +} + func registerMultiProjectReferenceRequestHandler[Req lsproto.HasTextDocumentPosition, Resp any]( handlers handlerMap, info lsproto.RequestInfo[Req, Resp], diff --git a/internal/module/resolver.go b/internal/module/resolver.go index f5ae94746f..40bcfd8aa6 100644 --- a/internal/module/resolver.go +++ b/internal/module/resolver.go @@ -11,8 +11,9 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/diagnostics" "github.com/microsoft/typescript-go/internal/packagejson" - "github.com/microsoft/typescript-go/internal/semver" + "github.com/microsoft/typescript-go/internal/stringutil" "github.com/microsoft/typescript-go/internal/tspath" + "github.com/microsoft/typescript-go/internal/vfs" ) type resolved struct { @@ -1818,13 +1819,7 @@ func (r *resolutionState) conditionMatches(condition string) bool { if !slices.Contains(r.conditions, "types") { return false // only apply versioned types conditions if the types condition is applied } - if !strings.HasPrefix(condition, "types@") { - return false - } - if versionRange, ok := semver.TryParseVersionRange(condition[len("types@"):]); ok { - return versionRange.Test(&typeScriptVersion) - } - return false + return IsApplicableVersionedTypesKey(condition) } func (r *resolutionState) getTraceFunc() func(m *diagnostics.Message, args ...any) { @@ -2022,3 +2017,209 @@ func GetAutomaticTypeDirectiveNames(options *core.CompilerOptions, host Resoluti } return result } + +type ResolvedEntrypoints struct { + Entrypoints []*ResolvedEntrypoint + FailedLookupLocations []string +} + +type Ending int + +const ( + EndingFixed Ending = iota + EndingExtensionChangeable + EndingChangeable +) + +type ResolvedEntrypoint struct { + ResolvedFileName string + ModuleSpecifier string + Ending Ending + IncludeConditions *collections.Set[string] + ExcludeConditions *collections.Set[string] +} + +func (r *Resolver) GetEntrypointsFromPackageJsonInfo(packageJson *packagejson.InfoCacheEntry, packageName string) *ResolvedEntrypoints { + extensions := extensionsTypeScript | extensionsDeclaration + features := NodeResolutionFeaturesAll + state := &resolutionState{resolver: r, extensions: extensions, features: features, compilerOptions: r.compilerOptions} + if packageJson.Exists() && packageJson.Contents.Exports.IsPresent() { + entrypoints := state.loadEntrypointsFromExportMap(packageJson, packageName, packageJson.Contents.Exports) + return &ResolvedEntrypoints{ + Entrypoints: entrypoints, + FailedLookupLocations: state.failedLookupLocations, + } + } + + result := &ResolvedEntrypoints{} + mainResolution := state.loadNodeModuleFromDirectoryWorker( + extensions, + packageJson.PackageDirectory, + false, /*onlyRecordFailures*/ + packageJson, + ) + + otherFiles := vfs.ReadDirectory( + r.host.FS(), + r.host.GetCurrentDirectory(), + packageJson.PackageDirectory, + extensions.Array(), + []string{"node_modules"}, + []string{"**/*"}, + nil, + ) + + if mainResolution.isResolved() { + result.Entrypoints = append(result.Entrypoints, &ResolvedEntrypoint{ + ResolvedFileName: mainResolution.path, + ModuleSpecifier: packageName, + }) + } + + comparePathsOptions := tspath.ComparePathsOptions{UseCaseSensitiveFileNames: r.host.FS().UseCaseSensitiveFileNames()} + for _, file := range otherFiles { + if mainResolution.isResolved() && tspath.ComparePaths(file, mainResolution.path, comparePathsOptions) == 0 { + continue + } + result.Entrypoints = append(result.Entrypoints, &ResolvedEntrypoint{ + ResolvedFileName: file, + ModuleSpecifier: tspath.ResolvePath(packageName, tspath.GetRelativePathFromDirectory(packageJson.PackageDirectory, file, comparePathsOptions)), + Ending: EndingChangeable, + }) + } + + if len(result.Entrypoints) > 0 { + result.FailedLookupLocations = state.failedLookupLocations + return result + } + return nil +} + +func (r *resolutionState) loadEntrypointsFromExportMap( + packageJson *packagejson.InfoCacheEntry, + packageName string, + exports packagejson.ExportsOrImports, +) []*ResolvedEntrypoint { + var loadEntrypointsFromTargetExports func(subpath string, includeConditions *collections.Set[string], excludeConditions *collections.Set[string], exports packagejson.ExportsOrImports) + var entrypoints []*ResolvedEntrypoint + + loadEntrypointsFromTargetExports = func(subpath string, includeConditions *collections.Set[string], excludeConditions *collections.Set[string], exports packagejson.ExportsOrImports) { + if exports.Type == packagejson.JSONValueTypeString && strings.HasPrefix(exports.AsString(), "./") { + if strings.ContainsRune(exports.AsString(), '*') { + if strings.IndexByte(exports.AsString(), '*') != strings.LastIndexByte(exports.AsString(), '*') { + return + } + patternPath := tspath.ResolvePath(packageJson.PackageDirectory, exports.AsString()) + leadingSlice, trailingSlice, _ := strings.Cut(patternPath, "*") + caseSensitive := r.resolver.host.FS().UseCaseSensitiveFileNames() + files := vfs.ReadDirectory( + r.resolver.host.FS(), + r.resolver.host.GetCurrentDirectory(), + packageJson.PackageDirectory, + r.extensions.Array(), + nil, + []string{ + tspath.ChangeFullExtension(strings.Replace(exports.AsString(), "*", "**/*", 1), ".*"), + }, + nil, + ) + for _, file := range files { + matchedStar, ok := r.getMatchedStarForPatternEntrypoint(file, leadingSlice, trailingSlice, caseSensitive) + if !ok { + continue + } + moduleSpecifier := tspath.ResolvePath(packageName, strings.Replace(subpath, "*", matchedStar, 1)) + entrypoints = append(entrypoints, &ResolvedEntrypoint{ + ResolvedFileName: file, + ModuleSpecifier: moduleSpecifier, + IncludeConditions: includeConditions, + ExcludeConditions: excludeConditions, + Ending: core.IfElse(strings.HasSuffix(exports.AsString(), "*"), EndingExtensionChangeable, EndingFixed), + }) + } + } else { + partsAfterFirst := tspath.GetPathComponents(exports.AsString(), "")[2:] + if slices.Contains(partsAfterFirst, "..") || slices.Contains(partsAfterFirst, ".") || slices.Contains(partsAfterFirst, "node_modules") { + return + } + resolvedTarget := tspath.ResolvePath(packageJson.PackageDirectory, exports.AsString()) + if result := r.loadFileNameFromPackageJSONField(r.extensions, resolvedTarget, exports.AsString(), false /*onlyRecordFailures*/); result.isResolved() { + entrypoints = append(entrypoints, &ResolvedEntrypoint{ + ResolvedFileName: result.path, + ModuleSpecifier: tspath.ResolvePath(packageName, subpath), + IncludeConditions: includeConditions, + ExcludeConditions: excludeConditions, + }) + } + } + } else if exports.Type == packagejson.JSONValueTypeArray { + for _, element := range exports.AsArray() { + loadEntrypointsFromTargetExports(subpath, includeConditions, excludeConditions, element) + } + } else if exports.Type == packagejson.JSONValueTypeObject { + var prevConditions []string + for condition, export := range exports.AsObject().Entries() { + if excludeConditions != nil && excludeConditions.Has(condition) { + continue + } + + conditionAlwaysMatches := condition == "default" || condition == "types" || IsApplicableVersionedTypesKey(condition) + newIncludeConditions := includeConditions + if !(conditionAlwaysMatches) { + newIncludeConditions = includeConditions.Clone() + excludeConditions = excludeConditions.Clone() + if newIncludeConditions == nil { + newIncludeConditions = &collections.Set[string]{} + } + newIncludeConditions.Add(condition) + for _, prevCondition := range prevConditions { + if excludeConditions == nil { + excludeConditions = &collections.Set[string]{} + } + excludeConditions.Add(prevCondition) + } + } + + prevConditions = append(prevConditions, condition) + loadEntrypointsFromTargetExports(subpath, newIncludeConditions, excludeConditions, export) + if conditionAlwaysMatches { + break + } + } + } + } + + switch exports.Type { + case packagejson.JSONValueTypeArray: + for _, element := range exports.AsArray() { + loadEntrypointsFromTargetExports(".", nil, nil, element) + } + case packagejson.JSONValueTypeObject: + if exports.IsSubpaths() { + for subpath, export := range exports.AsObject().Entries() { + loadEntrypointsFromTargetExports(subpath, nil, nil, export) + } + } else { + loadEntrypointsFromTargetExports(".", nil, nil, exports) + } + default: + loadEntrypointsFromTargetExports(".", nil, nil, exports) + } + + return entrypoints +} + +func (r *resolutionState) getMatchedStarForPatternEntrypoint(file string, leadingSlice string, trailingSlice string, caseSensitive bool) (string, bool) { + if stringutil.HasPrefixAndSuffixWithoutOverlap(file, leadingSlice, trailingSlice, caseSensitive) { + return file[len(leadingSlice) : len(file)-len(trailingSlice)], true + } + + if jsExtension := TryGetJSExtensionForFile(file, r.compilerOptions); len(jsExtension) > 0 { + swapped := tspath.ChangeFullExtension(file, jsExtension) + if stringutil.HasPrefixAndSuffixWithoutOverlap(swapped, leadingSlice, trailingSlice, caseSensitive) { + return swapped[len(leadingSlice) : len(swapped)-len(trailingSlice)], true + } + } + + return "", false +} diff --git a/internal/module/types.go b/internal/module/types.go index 59b79950f3..2106544af8 100644 --- a/internal/module/types.go +++ b/internal/module/types.go @@ -135,13 +135,13 @@ func (e extensions) String() string { func (e extensions) Array() []string { result := []string{} if e&extensionsTypeScript != 0 { - result = append(result, tspath.ExtensionTs, tspath.ExtensionTsx) + result = append(result, tspath.SupportedTSImplementationExtensions...) } if e&extensionsJavaScript != 0 { - result = append(result, tspath.ExtensionJs, tspath.ExtensionJsx) + result = append(result, tspath.SupportedJSExtensionsFlat...) } if e&extensionsDeclaration != 0 { - result = append(result, tspath.ExtensionDts) + result = append(result, tspath.SupportedDeclarationExtensions...) } if e&extensionsJson != 0 { result = append(result, tspath.ExtensionJson) diff --git a/internal/module/util.go b/internal/module/util.go index 04f75150f4..1247a7c7fd 100644 --- a/internal/module/util.go +++ b/internal/module/util.go @@ -14,6 +14,17 @@ var typeScriptVersion = semver.MustParse(core.Version()) const InferredTypesContainingFile = "__inferred type names__.ts" +func IsApplicableVersionedTypesKey(key string) bool { + if !strings.HasPrefix(key, "types@") { + return false + } + range_, ok := semver.TryParseVersionRange(key[len("types@"):]) + if !ok { + return false + } + return range_.Test(&typeScriptVersion) +} + func ParseNodeModuleFromPath(resolved string, isFolder bool) string { path := tspath.NormalizePath(resolved) idx := strings.LastIndex(path, "/node_modules/") @@ -67,6 +78,14 @@ func GetTypesPackageName(packageName string) string { return "@types/" + MangleScopedPackageName(packageName) } +func GetPackageNameFromTypesPackageName(mangledName string) string { + withoutAtTypePrefix := strings.TrimPrefix(mangledName, "@types/") + if withoutAtTypePrefix != mangledName { + return UnmangleScopedPackageName(withoutAtTypePrefix) + } + return mangledName +} + func ComparePatternKeys(a, b string) int { aPatternIndex := strings.Index(a, "*") bPatternIndex := strings.Index(b, "*") @@ -153,3 +172,26 @@ func GetResolutionDiagnostic(options *core.CompilerOptions, resolvedModule *Reso return needAllowArbitraryExtensions() } } + +// TryGetJSExtensionForFile maps TS/JS/DTS extensions to the output JS-side extension. +// Returns an empty string if the extension is unsupported. +func TryGetJSExtensionForFile(fileName string, options *core.CompilerOptions) string { + ext := tspath.TryGetExtensionFromPath(fileName) + switch ext { + case tspath.ExtensionTs, tspath.ExtensionDts: + return tspath.ExtensionJs + case tspath.ExtensionTsx: + if options.Jsx == core.JsxEmitPreserve { + return tspath.ExtensionJsx + } + return tspath.ExtensionJs + case tspath.ExtensionJs, tspath.ExtensionJsx, tspath.ExtensionJson: + return ext + case tspath.ExtensionDmts, tspath.ExtensionMts, tspath.ExtensionMjs: + return tspath.ExtensionMjs + case tspath.ExtensionDcts, tspath.ExtensionCts, tspath.ExtensionCjs: + return tspath.ExtensionCjs + default: + return "" + } +} diff --git a/internal/modulespecifiers/preferences.go b/internal/modulespecifiers/preferences.go index f28f1293c6..16c8e9e4aa 100644 --- a/internal/modulespecifiers/preferences.go +++ b/internal/modulespecifiers/preferences.go @@ -144,6 +144,66 @@ type ModuleSpecifierPreferences struct { excludeRegexes []string } +func GetAllowedEndingsInPreferredOrder( + prefs UserPreferences, + host ModuleSpecifierGenerationHost, + compilerOptions *core.CompilerOptions, + importingSourceFile SourceFileForSpecifierGeneration, + oldImportSpecifier string, + syntaxImpliedNodeFormat core.ResolutionMode, +) []ModuleSpecifierEnding { + preferredEnding := getPreferredEnding( + prefs, + host, + compilerOptions, + importingSourceFile, + oldImportSpecifier, + core.ResolutionModeNone, + ) + resolutionMode := host.GetDefaultResolutionModeForFile(importingSourceFile) + if resolutionMode != syntaxImpliedNodeFormat { + preferredEnding = getPreferredEnding( + prefs, + host, + compilerOptions, + importingSourceFile, + oldImportSpecifier, + syntaxImpliedNodeFormat, + ) + } + moduleResolution := compilerOptions.GetModuleResolutionKind() + moduleResolutionIsNodeNext := core.ModuleResolutionKindNode16 <= moduleResolution && moduleResolution <= core.ModuleResolutionKindNodeNext + allowImportingTsExtension := shouldAllowImportingTsExtension(compilerOptions, importingSourceFile.FileName()) + if syntaxImpliedNodeFormat == core.ResolutionModeESM && moduleResolutionIsNodeNext { + if allowImportingTsExtension { + return []ModuleSpecifierEnding{ModuleSpecifierEndingTsExtension, ModuleSpecifierEndingJsExtension} + } + return []ModuleSpecifierEnding{ModuleSpecifierEndingJsExtension} + } + switch preferredEnding { + case ModuleSpecifierEndingJsExtension: + if allowImportingTsExtension { + return []ModuleSpecifierEnding{ModuleSpecifierEndingJsExtension, ModuleSpecifierEndingTsExtension, ModuleSpecifierEndingMinimal, ModuleSpecifierEndingIndex} + } + return []ModuleSpecifierEnding{ModuleSpecifierEndingJsExtension, ModuleSpecifierEndingMinimal, ModuleSpecifierEndingIndex} + case ModuleSpecifierEndingTsExtension: + return []ModuleSpecifierEnding{ModuleSpecifierEndingTsExtension, ModuleSpecifierEndingMinimal, ModuleSpecifierEndingJsExtension, ModuleSpecifierEndingIndex} + case ModuleSpecifierEndingIndex: + if allowImportingTsExtension { + return []ModuleSpecifierEnding{ModuleSpecifierEndingIndex, ModuleSpecifierEndingMinimal, ModuleSpecifierEndingTsExtension, ModuleSpecifierEndingJsExtension} + } + return []ModuleSpecifierEnding{ModuleSpecifierEndingIndex, ModuleSpecifierEndingMinimal, ModuleSpecifierEndingJsExtension} + case ModuleSpecifierEndingMinimal: + if allowImportingTsExtension { + return []ModuleSpecifierEnding{ModuleSpecifierEndingMinimal, ModuleSpecifierEndingIndex, ModuleSpecifierEndingTsExtension, ModuleSpecifierEndingJsExtension} + } + return []ModuleSpecifierEnding{ModuleSpecifierEndingMinimal, ModuleSpecifierEndingIndex, ModuleSpecifierEndingJsExtension} + default: + debug.AssertNever(preferredEnding) + } + return []ModuleSpecifierEnding{ModuleSpecifierEndingMinimal} +} + func getModuleSpecifierPreferences( prefs UserPreferences, host ModuleSpecifierGenerationHost, @@ -170,59 +230,16 @@ func getModuleSpecifierPreferences( // all others are shortest } } - filePreferredEnding := getPreferredEnding( - prefs, - host, - compilerOptions, - importingSourceFile, - oldImportSpecifier, - core.ResolutionModeNone, - ) getAllowedEndingsInPreferredOrder := func(syntaxImpliedNodeFormat core.ResolutionMode) []ModuleSpecifierEnding { - preferredEnding := filePreferredEnding - resolutionMode := host.GetDefaultResolutionModeForFile(importingSourceFile) - if resolutionMode != syntaxImpliedNodeFormat { - preferredEnding = getPreferredEnding( - prefs, - host, - compilerOptions, - importingSourceFile, - oldImportSpecifier, - syntaxImpliedNodeFormat, - ) - } - moduleResolution := compilerOptions.GetModuleResolutionKind() - moduleResolutionIsNodeNext := core.ModuleResolutionKindNode16 <= moduleResolution && moduleResolution <= core.ModuleResolutionKindNodeNext - allowImportingTsExtension := shouldAllowImportingTsExtension(compilerOptions, importingSourceFile.FileName()) - if syntaxImpliedNodeFormat == core.ResolutionModeESM && moduleResolutionIsNodeNext { - if allowImportingTsExtension { - return []ModuleSpecifierEnding{ModuleSpecifierEndingTsExtension, ModuleSpecifierEndingJsExtension} - } - return []ModuleSpecifierEnding{ModuleSpecifierEndingJsExtension} - } - switch preferredEnding { - case ModuleSpecifierEndingJsExtension: - if allowImportingTsExtension { - return []ModuleSpecifierEnding{ModuleSpecifierEndingJsExtension, ModuleSpecifierEndingTsExtension, ModuleSpecifierEndingMinimal, ModuleSpecifierEndingIndex} - } - return []ModuleSpecifierEnding{ModuleSpecifierEndingJsExtension, ModuleSpecifierEndingMinimal, ModuleSpecifierEndingIndex} - case ModuleSpecifierEndingTsExtension: - return []ModuleSpecifierEnding{ModuleSpecifierEndingTsExtension, ModuleSpecifierEndingMinimal, ModuleSpecifierEndingJsExtension, ModuleSpecifierEndingIndex} - case ModuleSpecifierEndingIndex: - if allowImportingTsExtension { - return []ModuleSpecifierEnding{ModuleSpecifierEndingIndex, ModuleSpecifierEndingMinimal, ModuleSpecifierEndingTsExtension, ModuleSpecifierEndingJsExtension} - } - return []ModuleSpecifierEnding{ModuleSpecifierEndingIndex, ModuleSpecifierEndingMinimal, ModuleSpecifierEndingJsExtension} - case ModuleSpecifierEndingMinimal: - if allowImportingTsExtension { - return []ModuleSpecifierEnding{ModuleSpecifierEndingMinimal, ModuleSpecifierEndingIndex, ModuleSpecifierEndingTsExtension, ModuleSpecifierEndingJsExtension} - } - return []ModuleSpecifierEnding{ModuleSpecifierEndingMinimal, ModuleSpecifierEndingIndex, ModuleSpecifierEndingJsExtension} - default: - debug.AssertNever(preferredEnding) - } - return []ModuleSpecifierEnding{ModuleSpecifierEndingMinimal} + return GetAllowedEndingsInPreferredOrder( + prefs, + host, + compilerOptions, + importingSourceFile, + oldImportSpecifier, + syntaxImpliedNodeFormat, + ) } return ModuleSpecifierPreferences{ diff --git a/internal/modulespecifiers/specifiers.go b/internal/modulespecifiers/specifiers.go index 2a1dad8b50..5f20843648 100644 --- a/internal/modulespecifiers/specifiers.go +++ b/internal/modulespecifiers/specifiers.go @@ -51,7 +51,7 @@ func GetModuleSpecifiersWithInfo( ) ([]string, ResultKind) { ambient := tryGetModuleNameFromAmbientModule(moduleSymbol, checker) if len(ambient) > 0 { - if forAutoImports && isExcludedByRegex(ambient, userPreferences.AutoImportSpecifierExcludeRegexes) { + if forAutoImports && IsExcludedByRegex(ambient, userPreferences.AutoImportSpecifierExcludeRegexes) { return nil, ResultKindAmbient } return []string{ambient}, ResultKindAmbient @@ -62,9 +62,29 @@ func GetModuleSpecifiersWithInfo( return nil, ResultKindNone } + return GetModuleSpecifiersForFileWithInfo( + importingSourceFile, + moduleSourceFile.FileName(), + compilerOptions, + host, + userPreferences, + options, + forAutoImports, + ) +} + +func GetModuleSpecifiersForFileWithInfo( + importingSourceFile SourceFileForSpecifierGeneration, + moduleFileName string, + compilerOptions *core.CompilerOptions, + host ModuleSpecifierGenerationHost, + userPreferences UserPreferences, + options ModuleSpecifierOptions, + forAutoImports bool, +) ([]string, ResultKind) { modulePaths := getAllModulePathsWorker( getInfo(host.GetSourceOfProjectReferenceIfOutputIncluded(importingSourceFile), host), - moduleSourceFile.FileName(), + moduleFileName, host, compilerOptions, options, @@ -83,7 +103,7 @@ func GetModuleSpecifiersWithInfo( func tryGetModuleNameFromAmbientModule(moduleSymbol *ast.Symbol, checker CheckerShape) string { for _, decl := range moduleSymbol.Declarations { - if isNonGlobalAmbientModule(decl) && (!ast.IsModuleAugmentationExternal(decl) || !tspath.IsExternalModuleNameRelative(decl.Name().Text())) { + if ast.IsModuleWithStringLiteralName(decl) && (!ast.IsModuleAugmentationExternal(decl) || !tspath.IsExternalModuleNameRelative(decl.Name().Text())) { return decl.Name().Text() } } @@ -103,7 +123,7 @@ func tryGetModuleNameFromAmbientModule(moduleSymbol *ast.Symbol, checker Checker continue } - possibleContainer := ast.FindAncestor(d, isNonGlobalAmbientModule) + possibleContainer := ast.FindAncestor(d, ast.IsModuleWithStringLiteralName) if possibleContainer == nil || possibleContainer.Parent == nil || !ast.IsSourceFile(possibleContainer.Parent) { continue } @@ -390,7 +410,7 @@ func computeModuleSpecifiers( if modulePath.IsInNodeModules { specifier = tryGetModuleNameAsNodeModule(modulePath, info, importingSourceFile, host, compilerOptions, userPreferences /*packageNameOnly*/, false, options.OverrideImportMode) } - if len(specifier) > 0 && !(forAutoImport && isExcludedByRegex(specifier, preferences.excludeRegexes)) { + if len(specifier) > 0 && !(forAutoImport && IsExcludedByRegex(specifier, preferences.excludeRegexes)) { nodeModulesSpecifiers = append(nodeModulesSpecifiers, specifier) if modulePath.IsRedirect { // If we got a specifier for a redirect, it was a bare package specifier (e.g. "@foo/bar", @@ -412,7 +432,7 @@ func computeModuleSpecifiers( preferences, /*pathsOnly*/ modulePath.IsRedirect || len(specifier) > 0, ) - if len(local) == 0 || forAutoImport && isExcludedByRegex(local, preferences.excludeRegexes) { + if len(local) == 0 || forAutoImport && IsExcludedByRegex(local, preferences.excludeRegexes) { continue } if modulePath.IsRedirect { @@ -538,8 +558,8 @@ func getLocalModuleSpecifier( return relativePath } - relativeIsExcluded := isExcludedByRegex(relativePath, preferences.excludeRegexes) - nonRelativeIsExcluded := isExcludedByRegex(maybeNonRelative, preferences.excludeRegexes) + relativeIsExcluded := IsExcludedByRegex(relativePath, preferences.excludeRegexes) + nonRelativeIsExcluded := IsExcludedByRegex(maybeNonRelative, preferences.excludeRegexes) if !relativeIsExcluded && nonRelativeIsExcluded { return relativePath } @@ -627,7 +647,7 @@ func processEnding( } if tspath.FileExtensionIsOneOf(fileName, []string{tspath.ExtensionDmts, tspath.ExtensionDcts}) { inputExt := tspath.GetDeclarationFileExtension(fileName) - ext := getJsExtensionForDeclarationFileExtension(inputExt) + ext := GetJSExtensionForDeclarationFileExtension(inputExt) return tspath.RemoveExtension(fileName, inputExt) + ext } if tspath.FileExtensionIsOneOf(fileName, []string{tspath.ExtensionMts, tspath.ExtensionCts}) { @@ -774,7 +794,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 module.GetPackageNameFromTypesPackageName(nodeModulesDirectoryName) } type pkgJsonDirAttemptResult struct { @@ -823,7 +843,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 := module.GetPackageNameFromTypesPackageName(nodeModulesDirectoryName) // Determine resolution mode for package.json exports condition matching. // TypeScript's tryDirectoryWithPackageJson uses the importing file's mode (moduleSpecifiers.ts:1257), @@ -1179,7 +1199,7 @@ func tryGetModuleNameFromExportsOrImports( pathOrPattern := tspath.GetNormalizedAbsolutePath(tspath.CombinePaths(packageDirectory, strValue), "") var extensionSwappedTarget string if tspath.HasTSFileExtension(targetFilePath) { - extensionSwappedTarget = tspath.RemoveFileExtension(targetFilePath) + tryGetJSExtensionForFile(targetFilePath, options) + extensionSwappedTarget = tspath.RemoveFileExtension(targetFilePath) + module.TryGetJSExtensionForFile(targetFilePath, options) } canTryTsExtension := preferTsExtension && tspath.HasImplementationTSFileExtension(targetFilePath) @@ -1241,7 +1261,7 @@ func tryGetModuleNameFromExportsOrImports( if len(declarationFile) > 0 && stringutil.HasPrefixAndSuffixWithoutOverlap(declarationFile, leadingSlice, trailingSlice, caseSensitive) { starReplacement := declarationFile[len(leadingSlice) : len(declarationFile)-len(trailingSlice)] substituted := replaceFirstStar(packageName, starReplacement) - jsExtension := tryGetJSExtensionForFile(declarationFile, options) + jsExtension := module.TryGetJSExtensionForFile(declarationFile, options) if len(jsExtension) > 0 { return tspath.ChangeFullExtension(substituted, jsExtension) } @@ -1260,7 +1280,7 @@ func tryGetModuleNameFromExportsOrImports( // conditional mapping obj := exports.AsObject() for key, value := range obj.Entries() { - if key == "default" || slices.Contains(conditions, key) || isApplicableVersionedTypesKey(conditions, key) { + if key == "default" || slices.Contains(conditions, key) || slices.Contains(conditions, "types") && module.IsApplicableVersionedTypesKey(key) { result := tryGetModuleNameFromExportsOrImports(options, host, targetFilePath, packageDirectory, packageName, value, conditions, mode, isImports, preferTsExtension) if len(result) > 0 { return result diff --git a/internal/modulespecifiers/util.go b/internal/modulespecifiers/util.go index 12a5c3a342..a6d75afece 100644 --- a/internal/modulespecifiers/util.go +++ b/internal/modulespecifiers/util.go @@ -12,7 +12,6 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/module" "github.com/microsoft/typescript-go/internal/packagejson" - "github.com/microsoft/typescript-go/internal/semver" "github.com/microsoft/typescript-go/internal/tsoptions" "github.com/microsoft/typescript-go/internal/tspath" ) @@ -27,10 +26,6 @@ var ( regexPatternCache = make(map[regexPatternCacheKey]*regexp2.Regexp) ) -func isNonGlobalAmbientModule(node *ast.Node) bool { - return ast.IsModuleDeclaration(node) && ast.IsStringLiteral(node.Name()) -} - func comparePathsByRedirectAndNumberOfDirectorySeparators(a ModulePath, b ModulePath) int { if a.IsRedirect == b.IsRedirect { return strings.Count(a.FileName, "/") - strings.Count(b.FileName, "/") @@ -45,7 +40,7 @@ func PathIsBareSpecifier(path string) bool { return !tspath.PathIsAbsolute(path) && !tspath.PathIsRelative(path) } -func isExcludedByRegex(moduleSpecifier string, excludes []string) bool { +func IsExcludedByRegex(moduleSpecifier string, excludes []string) bool { for _, pattern := range excludes { re := stringToRegex(pattern) if re == nil { @@ -140,7 +135,7 @@ func ensurePathIsNonModuleName(path string) string { return path } -func getJsExtensionForDeclarationFileExtension(ext string) string { +func GetJSExtensionForDeclarationFileExtension(ext string) string { switch ext { case tspath.ExtensionDts: return tspath.ExtensionJs @@ -155,7 +150,7 @@ func getJsExtensionForDeclarationFileExtension(ext string) string { } func getJSExtensionForFile(fileName string, options *core.CompilerOptions) string { - result := tryGetJSExtensionForFile(fileName, options) + result := module.TryGetJSExtensionForFile(fileName, options) if len(result) == 0 { panic(fmt.Sprintf("Extension %s is unsupported:: FileName:: %s", extensionFromPath(fileName), fileName)) } @@ -174,27 +169,6 @@ func extensionFromPath(path string) string { return ext } -func tryGetJSExtensionForFile(fileName string, options *core.CompilerOptions) string { - ext := tspath.TryGetExtensionFromPath(fileName) - switch ext { - case tspath.ExtensionTs, tspath.ExtensionDts: - return tspath.ExtensionJs - case tspath.ExtensionTsx: - if options.Jsx == core.JsxEmitPreserve { - return tspath.ExtensionJsx - } - return tspath.ExtensionJs - case tspath.ExtensionJs, tspath.ExtensionJsx, tspath.ExtensionJson: - return ext - case tspath.ExtensionDmts, tspath.ExtensionMts, tspath.ExtensionMjs: - return tspath.ExtensionMjs - case tspath.ExtensionDcts, tspath.ExtensionCts, tspath.ExtensionCjs: - return tspath.ExtensionCjs - default: - return "" - } -} - func tryGetAnyFileFromPath(host ModuleSpecifierGenerationHost, path string) bool { // !!! TODO: shouldn't this use readdir instead of fileexists for perf? // We check all js, `node` and `json` extensions in addition to TS, since node module resolution would also choose those over the directory @@ -271,22 +245,6 @@ func prefersTsExtension(allowedEndings []ModuleSpecifierEnding) bool { return false } -var typeScriptVersion = semver.MustParse(core.Version()) // TODO: unify with clone inside module resolver? - -func isApplicableVersionedTypesKey(conditions []string, key string) bool { - if !slices.Contains(conditions, "types") { - return false // only apply versioned types conditions if the types condition is applied - } - if !strings.HasPrefix(key, "types@") { - return false - } - range_, ok := semver.TryParseVersionRange(key[len("types@"):]) - if !ok { - return false - } - return range_.Test(&typeScriptVersion) -} - func replaceFirstStar(s string, replacement string) string { return strings.Replace(s, "*", replacement, 1) } @@ -378,14 +336,6 @@ func GetNodeModulesPackageName( return "" } -func GetPackageNameFromTypesPackageName(mangledName string) string { - withoutAtTypePrefix := strings.TrimPrefix(mangledName, "@types/") - if withoutAtTypePrefix != mangledName { - return module.UnmangleScopedPackageName(withoutAtTypePrefix) - } - return mangledName -} - func allKeysStartWithDot(obj *collections.OrderedMap[string, packagejson.ExportsOrImports]) bool { for k := range obj.Keys() { if !strings.HasPrefix(k, ".") { @@ -394,3 +344,133 @@ func allKeysStartWithDot(obj *collections.OrderedMap[string, packagejson.Exports } return true } + +func GetPackageNameFromDirectory(fileOrDirectoryPath string) string { + idx := strings.LastIndex(fileOrDirectoryPath, "/node_modules/") + if idx == -1 { + return "" + } + + basename := fileOrDirectoryPath[idx+len("/node_modules/"):] + nextSlash := strings.Index(basename, "/") + if nextSlash == -1 { + return basename + } + + if basename[0] != '@' || nextSlash == len(basename)-1 { + return basename[:nextSlash] + } + + secondSlash := strings.Index(basename[nextSlash+1:], "/") + if secondSlash == -1 { + return basename + } + + return basename[:nextSlash+1+secondSlash] +} + +// ProcessEntrypointEnding processes a pre-computed module specifier from a package.json exports +// entrypoint according to the entrypoint's Ending type and the user's preferred endings. +func ProcessEntrypointEnding( + entrypoint *module.ResolvedEntrypoint, + prefs UserPreferences, + host ModuleSpecifierGenerationHost, + options *core.CompilerOptions, + importingSourceFile SourceFileForSpecifierGeneration, + allowedEndings []ModuleSpecifierEnding, +) string { + specifier := entrypoint.ModuleSpecifier + if entrypoint.Ending == module.EndingFixed { + return specifier + } + + if len(allowedEndings) == 0 { + allowedEndings = GetAllowedEndingsInPreferredOrder( + prefs, + host, + options, + importingSourceFile, + "", + host.GetDefaultResolutionModeForFile(importingSourceFile), + ) + } + + preferredEnding := allowedEndings[0] + + // Handle declaration file extensions + dtsExtension := tspath.GetDeclarationFileExtension(specifier) + if dtsExtension != "" { + switch preferredEnding { + case ModuleSpecifierEndingTsExtension, ModuleSpecifierEndingJsExtension: + // Map .d.ts -> .js, .d.mts -> .mjs, .d.cts -> .cjs + jsExtension := GetJSExtensionForDeclarationFileExtension(dtsExtension) + return tspath.ChangeAnyExtension(specifier, jsExtension, []string{dtsExtension}, false) + case ModuleSpecifierEndingMinimal, ModuleSpecifierEndingIndex: + if entrypoint.Ending == module.EndingChangeable { + // .d.mts/.d.cts must keep an extension; rewrite to .mjs/.cjs instead of dropping + if dtsExtension == tspath.ExtensionDts { + specifier = tspath.RemoveExtension(specifier, dtsExtension) + if preferredEnding == ModuleSpecifierEndingMinimal { + specifier = strings.TrimSuffix(specifier, "/index") + } + return specifier + } + jsExtension := GetJSExtensionForDeclarationFileExtension(dtsExtension) + return tspath.ChangeAnyExtension(specifier, jsExtension, []string{dtsExtension}, false) + } + // EndingExtensionChangeable - can only change extension, not remove it + jsExtension := GetJSExtensionForDeclarationFileExtension(dtsExtension) + return tspath.ChangeAnyExtension(specifier, jsExtension, []string{dtsExtension}, false) + } + return specifier + } + + // Handle .ts/.tsx/.mts/.cts extensions + if tspath.FileExtensionIsOneOf(specifier, []string{tspath.ExtensionTs, tspath.ExtensionTsx, tspath.ExtensionMts, tspath.ExtensionCts}) { + switch preferredEnding { + case ModuleSpecifierEndingTsExtension: + return specifier + case ModuleSpecifierEndingJsExtension: + if jsExtension := module.TryGetJSExtensionForFile(specifier, options); jsExtension != "" { + return tspath.RemoveFileExtension(specifier) + jsExtension + } + return specifier + case ModuleSpecifierEndingMinimal, ModuleSpecifierEndingIndex: + if entrypoint.Ending == module.EndingChangeable { + specifier = tspath.RemoveFileExtension(specifier) + if preferredEnding == ModuleSpecifierEndingMinimal { + specifier = strings.TrimSuffix(specifier, "/index") + } + return specifier + } + // EndingExtensionChangeable - can only change extension, not remove it + if jsExtension := module.TryGetJSExtensionForFile(specifier, options); jsExtension != "" { + return tspath.RemoveFileExtension(specifier) + jsExtension + } + return specifier + } + return specifier + } + + // Handle .js/.jsx/.mjs/.cjs extensions + if tspath.FileExtensionIsOneOf(specifier, []string{tspath.ExtensionJs, tspath.ExtensionJsx, tspath.ExtensionMjs, tspath.ExtensionCjs}) { + switch preferredEnding { + case ModuleSpecifierEndingTsExtension, ModuleSpecifierEndingJsExtension: + return specifier + case ModuleSpecifierEndingMinimal, ModuleSpecifierEndingIndex: + if entrypoint.Ending == module.EndingChangeable { + specifier = tspath.RemoveFileExtension(specifier) + if preferredEnding == ModuleSpecifierEndingMinimal { + specifier = strings.TrimSuffix(specifier, "/index") + } + return specifier + } + // EndingExtensionChangeable - keep the extension + return specifier + } + return specifier + } + + // For other extensions (like .json), return as-is + return specifier +} diff --git a/internal/packagejson/jsonvalue.go b/internal/packagejson/jsonvalue.go index 5c38a59310..048b8f2153 100644 --- a/internal/packagejson/jsonvalue.go +++ b/internal/packagejson/jsonvalue.go @@ -44,6 +44,10 @@ type JSONValue struct { Value any } +func (v *JSONValue) IsPresent() bool { + return v.Type != JSONValueTypeNotPresent +} + func (v *JSONValue) IsFalsy() bool { switch v.Type { case JSONValueTypeNotPresent, JSONValueTypeNull: @@ -73,6 +77,13 @@ func (v JSONValue) AsArray() []JSONValue { return v.Value.([]JSONValue) } +func (v JSONValue) AsString() string { + if v.Type != JSONValueTypeString { + panic(fmt.Sprintf("expected string, got %v", v.Type)) + } + return v.Value.(string) +} + var _ json.UnmarshalerFrom = (*JSONValue)(nil) func (v *JSONValue) UnmarshalJSONFrom(dec *jsontext.Decoder) error { diff --git a/internal/packagejson/packagejson.go b/internal/packagejson/packagejson.go index 191a639185..dae1d64b77 100644 --- a/internal/packagejson/packagejson.go +++ b/internal/packagejson/packagejson.go @@ -56,6 +56,37 @@ func (df *DependencyFields) HasDependency(name string) bool { return false } +func (df *DependencyFields) RangeDependencies(f func(name, version, dependencyField string) bool) { + if deps, ok := df.Dependencies.GetValue(); ok { + for name, version := range deps { + if !f(name, version, "dependencies") { + return + } + } + } + if devDeps, ok := df.DevDependencies.GetValue(); ok { + for name, version := range devDeps { + if !f(name, version, "devDependencies") { + return + } + } + } + if peerDeps, ok := df.PeerDependencies.GetValue(); ok { + for name, version := range peerDeps { + if !f(name, version, "peerDependencies") { + return + } + } + } + if optDeps, ok := df.OptionalDependencies.GetValue(); ok { + for name, version := range optDeps { + if !f(name, version, "optionalDependencies") { + return + } + } + } +} + func (df *DependencyFields) GetRuntimeDependencyNames() *collections.Set[string] { var count int deps, _ := df.Dependencies.GetValue() diff --git a/internal/parser/references.go b/internal/parser/references.go index 7a6b4c01c8..4d688c5ec5 100644 --- a/internal/parser/references.go +++ b/internal/parser/references.go @@ -55,10 +55,7 @@ func collectModuleReferences(file *ast.SourceFile, node *ast.Statement, inAmbien if ast.IsExternalModule(file) || (inAmbientModule && !tspath.IsExternalModuleNameRelative(nameText)) { file.ModuleAugmentations = append(file.ModuleAugmentations, node.AsModuleDeclaration().Name()) } else if !inAmbientModule { - if file.IsDeclarationFile { - // for global .d.ts files record name of ambient module - file.AmbientModuleNames = append(file.AmbientModuleNames, nameText) - } + file.AmbientModuleNames = append(file.AmbientModuleNames, nameText) // An AmbientExternalModuleDeclaration declares an external module. // This type of declaration is permitted only in the global module. // The StringLiteral must specify a top - level external module name. diff --git a/internal/project/autoimport.go b/internal/project/autoimport.go new file mode 100644 index 0000000000..1f8d1e3a7c --- /dev/null +++ b/internal/project/autoimport.go @@ -0,0 +1,171 @@ +package project + +import ( + "sync" + + "github.com/microsoft/typescript-go/internal/ast" + "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/ls/autoimport" + "github.com/microsoft/typescript-go/internal/packagejson" + "github.com/microsoft/typescript-go/internal/tspath" + "github.com/microsoft/typescript-go/internal/vfs" +) + +type autoImportBuilderFS struct { + snapshotFSBuilder *snapshotFSBuilder + untrackedFiles collections.SyncMap[tspath.Path, FileHandle] +} + +var _ FileSource = (*autoImportBuilderFS)(nil) + +// FS implements FileSource. +func (a *autoImportBuilderFS) FS() vfs.FS { + return a.snapshotFSBuilder.fs +} + +// GetFile implements FileSource. +func (a *autoImportBuilderFS) GetFile(fileName string) FileHandle { + path := a.snapshotFSBuilder.toPath(fileName) + return a.GetFileByPath(fileName, path) +} + +// GetFileByPath implements FileSource. +func (a *autoImportBuilderFS) GetFileByPath(fileName string, path tspath.Path) FileHandle { + // We want to avoid long-term caching of files referenced only by auto-imports, so we + // override GetFileByPath to avoid collecting more files into the snapshotFSBuilder's + // diskFiles. (Note the reason we can't just use the finalized SnapshotFS is that changed + // files not read during other parts of the snapshot clone will be marked as dirty, but + // not yet refreshed from disk.) + if overlay, ok := a.snapshotFSBuilder.overlays[path]; ok { + return overlay + } + if diskFile, ok := a.snapshotFSBuilder.diskFiles.Load(path); ok { + return a.snapshotFSBuilder.reloadEntryIfNeeded(diskFile) + } + if fh, ok := a.untrackedFiles.Load(path); ok { + return fh + } + var fh FileHandle + content, ok := a.snapshotFSBuilder.fs.ReadFile(fileName) + if ok { + fh = newDiskFile(fileName, content) + } + fh, _ = a.untrackedFiles.LoadOrStore(path, fh) + return fh +} + +type autoImportRegistryCloneHost struct { + projectCollection *ProjectCollection + parseCache *ParseCache + fs *sourceFS + currentDirectory string + + filesMu sync.Mutex + files []ParseCacheKey +} + +var _ autoimport.RegistryCloneHost = (*autoImportRegistryCloneHost)(nil) + +func newAutoImportRegistryCloneHost( + projectCollection *ProjectCollection, + parseCache *ParseCache, + snapshotFSBuilder *snapshotFSBuilder, + currentDirectory string, + toPath func(fileName string) tspath.Path, +) *autoImportRegistryCloneHost { + return &autoImportRegistryCloneHost{ + projectCollection: projectCollection, + parseCache: parseCache, + fs: newSourceFS(false, &autoImportBuilderFS{snapshotFSBuilder: snapshotFSBuilder}, toPath), + } +} + +// FS implements autoimport.RegistryCloneHost. +func (a *autoImportRegistryCloneHost) FS() vfs.FS { + return a.fs +} + +// GetCurrentDirectory implements autoimport.RegistryCloneHost. +func (a *autoImportRegistryCloneHost) GetCurrentDirectory() string { + return a.currentDirectory +} + +// GetDefaultProject implements autoimport.RegistryCloneHost. +func (a *autoImportRegistryCloneHost) GetDefaultProject(path tspath.Path) (tspath.Path, *compiler.Program) { + project := a.projectCollection.GetDefaultProject(path) + if project == nil { + return "", nil + } + return project.configFilePath, project.GetProgram() +} + +// GetPackageJson implements autoimport.RegistryCloneHost. +func (a *autoImportRegistryCloneHost) GetPackageJson(fileName string) *packagejson.InfoCacheEntry { + // !!! ref-counted shared cache + fh := a.fs.GetFile(fileName) + packageDirectory := tspath.GetDirectoryPath(fileName) + if fh == nil { + return &packagejson.InfoCacheEntry{ + DirectoryExists: a.fs.DirectoryExists(packageDirectory), + PackageDirectory: packageDirectory, + } + } + fields, err := packagejson.Parse([]byte(fh.Content())) + if err != nil { + return &packagejson.InfoCacheEntry{ + DirectoryExists: true, + PackageDirectory: tspath.GetDirectoryPath(fileName), + Contents: &packagejson.PackageJson{ + Parseable: false, + }, + } + } + return &packagejson.InfoCacheEntry{ + DirectoryExists: true, + PackageDirectory: tspath.GetDirectoryPath(fileName), + Contents: &packagejson.PackageJson{ + Fields: fields, + Parseable: true, + }, + } +} + +// GetProgramForProject implements autoimport.RegistryCloneHost. +func (a *autoImportRegistryCloneHost) GetProgramForProject(projectPath tspath.Path) *compiler.Program { + project := a.projectCollection.GetProjectByPath(projectPath) + if project == nil { + return nil + } + return project.GetProgram() +} + +// GetSourceFile implements autoimport.RegistryCloneHost. +func (a *autoImportRegistryCloneHost) GetSourceFile(fileName string, path tspath.Path) *ast.SourceFile { + fh := a.fs.GetFile(fileName) + if fh == nil { + return nil + } + opts := ast.SourceFileParseOptions{ + FileName: fileName, + Path: path, + CompilerOptions: core.EmptyCompilerOptions.SourceFileAffecting(), + JSDocParsingMode: ast.JSDocParsingModeParseAll, + } + key := NewParseCacheKey(opts, fh.Hash(), fh.Kind()) + + a.filesMu.Lock() + a.files = append(a.files, key) + a.filesMu.Unlock() + return a.parseCache.Acquire(key, fh) +} + +// Dispose implements autoimport.RegistryCloneHost. +func (a *autoImportRegistryCloneHost) Dispose() { + a.filesMu.Lock() + defer a.filesMu.Unlock() + for _, key := range a.files { + a.parseCache.Deref(key) + } +} diff --git a/internal/project/compilerhost.go b/internal/project/compilerhost.go index d36d67593a..9e329ec4c2 100644 --- a/internal/project/compilerhost.go +++ b/internal/project/compilerhost.go @@ -1,10 +1,7 @@ package project import ( - "time" - "github.com/microsoft/typescript-go/internal/ast" - "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/compiler" "github.com/microsoft/typescript-go/internal/diagnostics" "github.com/microsoft/typescript-go/internal/project/logging" @@ -20,54 +17,27 @@ type compilerHost struct { currentDirectory string sessionOptions *SessionOptions - fs *snapshotFSBuilder - compilerFS *compilerFS + sourceFS *sourceFS configFileRegistry *ConfigFileRegistry - seenFiles *collections.SyncSet[tspath.Path] project *Project builder *ProjectCollectionBuilder logger *logging.LogTree } -type builderFileSource struct { - seenFiles *collections.SyncSet[tspath.Path] - snapshotFSBuilder *snapshotFSBuilder -} - -func (c *builderFileSource) GetFile(fileName string) FileHandle { - path := c.snapshotFSBuilder.toPath(fileName) - c.seenFiles.Add(path) - return c.snapshotFSBuilder.GetFileByPath(fileName, path) -} - -func (c *builderFileSource) FS() vfs.FS { - return c.snapshotFSBuilder.FS() -} - func newCompilerHost( currentDirectory string, project *Project, builder *ProjectCollectionBuilder, logger *logging.LogTree, ) *compilerHost { - seenFiles := &collections.SyncSet[tspath.Path]{} - compilerFS := &compilerFS{ - source: &builderFileSource{ - seenFiles: seenFiles, - snapshotFSBuilder: builder.fs, - }, - } - return &compilerHost{ configFilePath: project.configFilePath, currentDirectory: currentDirectory, sessionOptions: builder.sessionOptions, - compilerFS: compilerFS, - seenFiles: seenFiles, + sourceFS: newSourceFS(true, builder.fs, builder.toPath), - fs: builder.fs, project: project, builder: builder, logger: logger, @@ -80,9 +50,9 @@ func (c *compilerHost) freeze(snapshotFS *SnapshotFS, configFileRegistry *Config if c.builder == nil { panic("freeze can only be called once") } - c.compilerFS.source = snapshotFS + c.sourceFS.source = snapshotFS + c.sourceFS.DisableTracking() c.configFileRegistry = configFileRegistry - c.fs = nil c.builder = nil c.project = nil c.logger = nil @@ -101,7 +71,7 @@ func (c *compilerHost) DefaultLibraryPath() string { // FS implements compiler.CompilerHost. func (c *compilerHost) FS() vfs.FS { - return c.compilerFS + return c.sourceFS } // GetCurrentDirectory implements compiler.CompilerHost. @@ -114,7 +84,8 @@ func (c *compilerHost) GetResolvedProjectReference(fileName string, path tspath. if c.builder == nil { return c.configFileRegistry.GetConfig(path) } else { - c.seenFiles.Add(path) + // acquireConfigForProject will bypass sourceFS, so track the file here. + c.sourceFS.Track(fileName) return c.builder.configFileRegistryBuilder.acquireConfigForProject(fileName, path, c.project, c.logger) } } @@ -124,8 +95,7 @@ func (c *compilerHost) GetResolvedProjectReference(fileName string, path tspath. // be a corresponding release for each call made. func (c *compilerHost) GetSourceFile(opts ast.SourceFileParseOptions) *ast.SourceFile { c.ensureAlive() - c.seenFiles.Add(opts.Path) - if fh := c.fs.GetFileByPath(opts.FileName, opts.Path); fh != nil { + if fh := c.sourceFS.GetFileByPath(opts.FileName, opts.Path); fh != nil { return c.builder.parseCache.Acquire(NewParseCacheKey(opts, fh.Hash(), fh.Kind()), fh) } return nil @@ -135,70 +105,3 @@ func (c *compilerHost) GetSourceFile(opts ast.SourceFileParseOptions) *ast.Sourc func (c *compilerHost) Trace(msg *diagnostics.Message, args ...any) { panic("unimplemented") } - -var _ vfs.FS = (*compilerFS)(nil) - -type compilerFS struct { - source FileSource -} - -// DirectoryExists implements vfs.FS. -func (fs *compilerFS) DirectoryExists(path string) bool { - return fs.source.FS().DirectoryExists(path) -} - -// FileExists implements vfs.FS. -func (fs *compilerFS) FileExists(path string) bool { - if fh := fs.source.GetFile(path); fh != nil { - return true - } - return fs.source.FS().FileExists(path) -} - -// GetAccessibleEntries implements vfs.FS. -func (fs *compilerFS) GetAccessibleEntries(path string) vfs.Entries { - return fs.source.FS().GetAccessibleEntries(path) -} - -// ReadFile implements vfs.FS. -func (fs *compilerFS) ReadFile(path string) (contents string, ok bool) { - if fh := fs.source.GetFile(path); fh != nil { - return fh.Content(), true - } - return "", false -} - -// Realpath implements vfs.FS. -func (fs *compilerFS) Realpath(path string) string { - return fs.source.FS().Realpath(path) -} - -// Stat implements vfs.FS. -func (fs *compilerFS) Stat(path string) vfs.FileInfo { - return fs.source.FS().Stat(path) -} - -// UseCaseSensitiveFileNames implements vfs.FS. -func (fs *compilerFS) UseCaseSensitiveFileNames() bool { - return fs.source.FS().UseCaseSensitiveFileNames() -} - -// WalkDir implements vfs.FS. -func (fs *compilerFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { - panic("unimplemented") -} - -// WriteFile implements vfs.FS. -func (fs *compilerFS) WriteFile(path string, data string, writeByteOrderMark bool) error { - panic("unimplemented") -} - -// Remove implements vfs.FS. -func (fs *compilerFS) Remove(path string) error { - panic("unimplemented") -} - -// Chtimes implements vfs.FS. -func (fs *compilerFS) Chtimes(path string, atime time.Time, mtime time.Time) error { - panic("unimplemented") -} diff --git a/internal/project/dirty/map.go b/internal/project/dirty/map.go index 10f72d7ae4..e90d8b7130 100644 --- a/internal/project/dirty/map.go +++ b/internal/project/dirty/map.go @@ -19,6 +19,17 @@ func (e *MapEntry[K, V]) Change(apply func(V)) { apply(e.value) } +func (e *MapEntry[K, V]) Replace(newValue V) { + if e.delete { + panic("tried to change a deleted entry") + } + if !e.dirty { + e.dirty = true + e.m.dirty[e.key] = e + } + e.value = newValue +} + func (e *MapEntry[K, V]) ChangeIf(cond func(V) bool, apply func(V)) bool { if cond(e.Value()) { e.Change(apply) @@ -96,10 +107,16 @@ func (m *Map[K, V]) Change(key K, apply func(V)) { } } -func (m *Map[K, V]) Delete(key K) { +func (m *Map[K, V]) TryDelete(key K) bool { if entry, ok := m.Get(key); ok { entry.Delete() - } else { + return true + } + return false +} + +func (m *Map[K, V]) Delete(key K) { + if !m.TryDelete(key) { panic("tried to delete a non-existent entry") } } diff --git a/internal/project/dirty/mapbuilder.go b/internal/project/dirty/mapbuilder.go new file mode 100644 index 0000000000..f14e205bbd --- /dev/null +++ b/internal/project/dirty/mapbuilder.go @@ -0,0 +1,74 @@ +package dirty + +import "maps" + +type MapBuilder[K comparable, VBase any, VBuilder any] struct { + base map[K]VBase + dirty map[K]VBuilder + deleted map[K]struct{} + + toBuilder func(VBase) VBuilder + build func(VBuilder) VBase +} + +func NewMapBuilder[K comparable, VBase any, VBuilder any]( + base map[K]VBase, + toBuilder func(VBase) VBuilder, + build func(VBuilder) VBase, +) *MapBuilder[K, VBase, VBuilder] { + return &MapBuilder[K, VBase, VBuilder]{ + base: base, + dirty: make(map[K]VBuilder), + toBuilder: toBuilder, + build: build, + } +} + +func (mb *MapBuilder[K, VBase, VBuilder]) Set(key K, value VBuilder) { + mb.dirty[key] = value + delete(mb.deleted, key) +} + +func (mb *MapBuilder[K, VBase, VBuilder]) Delete(key K) { + if mb.deleted == nil { + mb.deleted = make(map[K]struct{}) + } + mb.deleted[key] = struct{}{} + delete(mb.dirty, key) +} + +func (mb *MapBuilder[K, VBase, VBuilder]) Clear() { + mb.dirty = make(map[K]VBuilder) + mb.deleted = make(map[K]struct{}, len(mb.base)) + for key := range mb.base { + mb.deleted[key] = struct{}{} + } +} + +func (mb *MapBuilder[K, VBase, VBuilder]) Has(key K) bool { + if _, ok := mb.deleted[key]; ok { + return false + } + if _, ok := mb.dirty[key]; ok { + return true + } + _, ok := mb.base[key] + return ok +} + +func (mb *MapBuilder[K, VBase, VBuilder]) Build() map[K]VBase { + if len(mb.dirty) == 0 && len(mb.deleted) == 0 { + return mb.base + } + result := maps.Clone(mb.base) + if result == nil { + result = make(map[K]VBase) + } + for key := range mb.deleted { + delete(result, key) + } + for key, value := range mb.dirty { + result[key] = mb.build(value) + } + return result +} diff --git a/internal/project/projectcollection.go b/internal/project/projectcollection.go index 079c80d0ce..c34bf6f08a 100644 --- a/internal/project/projectcollection.go +++ b/internal/project/projectcollection.go @@ -108,7 +108,7 @@ func (c *ProjectCollection) GetProjectsContainingFile(path tspath.Path) []ls.Pro } // !!! result could be cached -func (c *ProjectCollection) GetDefaultProject(fileName string, path tspath.Path) *Project { +func (c *ProjectCollection) GetDefaultProject(path tspath.Path) *Project { if result, ok := c.fileDefaultProjects[path]; ok { if result == inferredProjectName { return c.inferredProject @@ -155,20 +155,20 @@ func (c *ProjectCollection) GetDefaultProject(fileName string, path tspath.Path) return firstConfiguredProject } // Multiple projects include the file directly. - if defaultProject := c.findDefaultConfiguredProject(fileName, path); defaultProject != nil { + if defaultProject := c.findDefaultConfiguredProject(path); defaultProject != nil { return defaultProject } return firstConfiguredProject } -func (c *ProjectCollection) findDefaultConfiguredProject(fileName string, path tspath.Path) *Project { +func (c *ProjectCollection) findDefaultConfiguredProject(path tspath.Path) *Project { if configFileName := c.configFileRegistry.GetConfigFileName(path); configFileName != "" { - return c.findDefaultConfiguredProjectWorker(fileName, path, configFileName, nil, nil) + return c.findDefaultConfiguredProjectWorker(path, configFileName, nil, nil) } return nil } -func (c *ProjectCollection) findDefaultConfiguredProjectWorker(fileName string, path tspath.Path, configFileName string, visited *collections.SyncSet[*Project], fallback *Project) *Project { +func (c *ProjectCollection) findDefaultConfiguredProjectWorker(path tspath.Path, configFileName string, visited *collections.SyncSet[*Project], fallback *Project) *Project { configFilePath := c.toPath(configFileName) project, ok := c.configuredProjects[configFilePath] if !ok { @@ -218,7 +218,7 @@ func (c *ProjectCollection) findDefaultConfiguredProjectWorker(fileName string, return fallback } if ancestorConfigName := c.configFileRegistry.GetAncestorConfigFileName(path, configFileName); ancestorConfigName != "" { - return c.findDefaultConfiguredProjectWorker(fileName, path, ancestorConfigName, visited, fallback) + return c.findDefaultConfiguredProjectWorker(path, ancestorConfigName, visited, fallback) } return fallback } diff --git a/internal/project/projectcollectionbuilder.go b/internal/project/projectcollectionbuilder.go index fb06c9f6cc..269e2f96d5 100644 --- a/internal/project/projectcollectionbuilder.go +++ b/internal/project/projectcollectionbuilder.go @@ -981,7 +981,7 @@ func (b *ProjectCollectionBuilder) updateProgram(entry dirty.Value[*Project], lo project.ProgramUpdateKind = result.UpdateKind project.ProgramLastUpdate = b.newSnapshotID if result.UpdateKind == ProgramUpdateKindCloned { - project.host.seenFiles = oldHost.seenFiles + project.host.sourceFS.seenFiles = oldHost.sourceFS.seenFiles } if result.UpdateKind == ProgramUpdateKindNewFiles { filesChanged = true diff --git a/internal/project/refcountcache.go b/internal/project/refcountcache.go index 6c499ff8fb..8a18d0412e 100644 --- a/internal/project/refcountcache.go +++ b/internal/project/refcountcache.go @@ -38,8 +38,8 @@ func NewRefCountCache[K comparable, V any, AcquireArgs any]( } } -// Acquire retrieves or creates a cache entry for the given identity and hash. -// If an entry exists with matching identity and hash, its refcount is incremented +// Acquire retrieves or creates a cache entry for the given identity. +// If an entry exists with matching identity, its refcount is incremented // and the cached value is returned. Otherwise, parse() is called to create the // value, which is stored and returned with refcount 1. // diff --git a/internal/project/session.go b/internal/project/session.go index 0ef5d204b9..0abd13a2be 100644 --- a/internal/project/session.go +++ b/internal/project/session.go @@ -36,6 +36,7 @@ const ( UpdateReasonRequestedLanguageServiceForFileNotOpen UpdateReasonRequestedLanguageServiceProjectDirty UpdateReasonRequestedLoadProjectTree + UpdateReasonRequestedLanguageServiceWithAutoImports ) // SessionOptions are the immutable initialization options for a session. @@ -159,11 +160,24 @@ func NewSession(init *SessionInit) *Session { fs: init.FS, }, init.Options, - parseCache, - extendedConfigCache, &ConfigFileRegistry{}, nil, Config{}, + nil, + NewWatchedFiles( + "auto-import", + lsproto.WatchKindCreate|lsproto.WatchKindChange|lsproto.WatchKindDelete, + func(nodeModulesDirs map[tspath.Path]string) PatternsAndIgnored { + patterns := make([]string, 0, len(nodeModulesDirs)) + for _, dir := range nodeModulesDirs { + patterns = append(patterns, getRecursiveGlobPattern(dir)) + } + slices.Sort(patterns) + return PatternsAndIgnored{ + patterns: patterns, + } + }, + ), toPath, ), pendingATAChanges: make(map[tspath.Path]*ATAStateChange), @@ -414,6 +428,8 @@ func (s *Session) getSnapshot( updateReason = UpdateReasonRequestedLanguageServiceProjectDirty } else if request.ProjectTree != nil { updateReason = UpdateReasonRequestedLoadProjectTree + } else if request.AutoImports != "" { + updateReason = UpdateReasonRequestedLanguageServiceWithAutoImports } else { for _, document := range request.Documents { if snapshot.fs.isOpenFile(document.FileName()) { @@ -453,7 +469,7 @@ func (s *Session) getSnapshotAndDefaultProject(ctx context.Context, uri lsproto. if project == nil { return nil, nil, nil, fmt.Errorf("no project found for URI %s", uri) } - return snapshot, project, ls.NewLanguageService(project.GetProgram(), snapshot), nil + return snapshot, project, ls.NewLanguageService(project.configFilePath, project.GetProgram(), snapshot), nil } func (s *Session) GetLanguageService(ctx context.Context, uri lsproto.DocumentUri) (*ls.LanguageService, error) { @@ -499,7 +515,7 @@ func (s *Session) GetLanguageServiceForProjectWithFile(ctx context.Context, proj if !project.HasFile(uri.FileName()) { return nil } - return ls.NewLanguageService(project.GetProgram(), snapshot) + return ls.NewLanguageService(project.configFilePath, project.GetProgram(), snapshot) } func (s *Session) GetSnapshotLoadingProjectTree( @@ -514,6 +530,22 @@ func (s *Session) GetSnapshotLoadingProjectTree( return snapshot } +// GetLanguageServiceWithAutoImports clones the current snapshot with a request to +// prepare auto-imports for the given URI, then returns a LanguageService for the +// default project of that URI. It should only be called after GetLanguageService. +// !!! take snapshot that GetLanguageService initially returned +func (s *Session) GetLanguageServiceWithAutoImports(ctx context.Context, uri lsproto.DocumentUri) (*ls.LanguageService, error) { + snapshot := s.getSnapshot(ctx, ResourceRequest{ + Documents: []lsproto.DocumentUri{uri}, + AutoImports: uri, + }) + project := snapshot.GetDefaultProject(uri) + if project == nil { + return nil, fmt.Errorf("no project found for URI %s", uri) + } + return ls.NewLanguageService(project.configFilePath, project.GetProgram(), snapshot), nil +} + func (s *Session) UpdateSnapshot(ctx context.Context, overlays map[tspath.Path]*Overlay, change SnapshotChange) *Snapshot { s.snapshotMu.Lock() oldSnapshot := s.snapshot @@ -545,6 +577,7 @@ func (s *Session) UpdateSnapshot(ctx context.Context, overlays map[tspath.Path]* } } s.publishProgramDiagnostics(oldSnapshot, newSnapshot) + s.warmAutoImportCache(ctx, change, oldSnapshot, newSnapshot) }) return newSnapshot @@ -679,6 +712,10 @@ func (s *Session) updateWatches(oldSnapshot *Snapshot, newSnapshot *Snapshot) er }, ) + if oldSnapshot.autoImportsWatch.ID() != newSnapshot.autoImportsWatch.ID() { + errors = append(errors, updateWatch(ctx, s, s.logger, oldSnapshot.autoImportsWatch, newSnapshot.autoImportsWatch)...) + } + if len(errors) > 0 { return fmt.Errorf("errors updating watches: %v", errors) } else if s.options.LoggingEnabled { @@ -788,6 +825,25 @@ func (s *Session) logCacheStats(snapshot *Snapshot) { s.logger.Logf("Parse cache size: %6d", parseCacheSize) s.logger.Logf("Program count: %6d", programCount) s.logger.Logf("Extended config cache size: %6d", extendedConfigCount) + + s.logger.Log("Auto Imports:") + autoImportStats := snapshot.AutoImportRegistry().GetCacheStats() + if len(autoImportStats.ProjectBuckets) > 0 { + s.logger.Log("\tProject buckets:") + for _, bucket := range autoImportStats.ProjectBuckets { + s.logger.Logf("\t\t%s%s:", bucket.Path, core.IfElse(bucket.State.Dirty(), " (dirty)", "")) + s.logger.Logf("\t\t\tFiles: %d", bucket.FileCount) + s.logger.Logf("\t\t\tExports: %d", bucket.ExportCount) + } + } + if len(autoImportStats.NodeModulesBuckets) > 0 { + s.logger.Log("\tnode_modules buckets:") + for _, bucket := range autoImportStats.NodeModulesBuckets { + s.logger.Logf("\t\t%s%s:", bucket.Path, core.IfElse(bucket.State.Dirty(), " (dirty)", "")) + s.logger.Logf("\t\t\tFiles: %d", bucket.FileCount) + s.logger.Logf("\t\t\tExports: %d", bucket.ExportCount) + } + } } } @@ -904,3 +960,27 @@ func (s *Session) triggerATAForUpdatedProjects(newSnapshot *Snapshot) { } } } + +func (s *Session) warmAutoImportCache(ctx context.Context, change SnapshotChange, oldSnapshot, newSnapshot *Snapshot) { + if change.fileChanges.Changed.Len() == 1 { + var changedFile lsproto.DocumentUri + for uri := range change.fileChanges.Changed.Keys() { + changedFile = uri + } + if !newSnapshot.fs.isOpenFile(changedFile.FileName()) { + return + } + project := newSnapshot.GetDefaultProject(changedFile) + if project == nil { + return + } + if newSnapshot.AutoImports.IsPreparedForImportingFile( + changedFile.FileName(), + project.configFilePath, + newSnapshot.config.tsUserPreferences.OrDefault(), + ) { + return + } + _, _ = s.GetLanguageServiceWithAutoImports(ctx, changedFile) + } +} diff --git a/internal/project/snapshot.go b/internal/project/snapshot.go index 50889c7cbd..f8ece3835a 100644 --- a/internal/project/snapshot.go +++ b/internal/project/snapshot.go @@ -12,6 +12,7 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/format" "github.com/microsoft/typescript-go/internal/ls" + "github.com/microsoft/typescript-go/internal/ls/autoimport" "github.com/microsoft/typescript-go/internal/ls/lsconv" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" @@ -37,6 +38,8 @@ type Snapshot struct { fs *SnapshotFS ProjectCollection *ProjectCollection ConfigFileRegistry *ConfigFileRegistry + AutoImports *autoimport.Registry + autoImportsWatch *WatchedFiles[map[tspath.Path]string] compilerOptionsForInferredProjects *core.CompilerOptions config Config @@ -49,11 +52,11 @@ func NewSnapshot( id uint64, fs *SnapshotFS, sessionOptions *SessionOptions, - parseCache *ParseCache, - extendedConfigCache *ExtendedConfigCache, configFileRegistry *ConfigFileRegistry, compilerOptionsForInferredProjects *core.CompilerOptions, config Config, + autoImports *autoimport.Registry, + autoImportsWatch *WatchedFiles[map[tspath.Path]string], toPath func(fileName string) tspath.Path, ) *Snapshot { s := &Snapshot{ @@ -67,6 +70,8 @@ func NewSnapshot( ProjectCollection: &ProjectCollection{toPath: toPath}, compilerOptionsForInferredProjects: compilerOptionsForInferredProjects, config: config, + AutoImports: autoImports, + autoImportsWatch: autoImportsWatch, } s.converters = lsconv.NewConverters(s.sessionOptions.PositionEncoding, s.LSPLineMap) s.refCount.Store(1) @@ -74,9 +79,7 @@ func NewSnapshot( } func (s *Snapshot) GetDefaultProject(uri lsproto.DocumentUri) *Project { - fileName := uri.FileName() - path := s.toPath(fileName) - return s.ProjectCollection.GetDefaultProject(fileName, path) + return s.ProjectCollection.GetDefaultProject(uri.Path(s.UseCaseSensitiveFileNames())) } func (s *Snapshot) GetProjectsContainingFile(uri lsproto.DocumentUri) []ls.Project { @@ -116,6 +119,10 @@ func (s *Snapshot) Converters() *lsconv.Converters { return s.converters } +func (s *Snapshot) AutoImportRegistry() *autoimport.Registry { + return s.AutoImports +} + func (s *Snapshot) ID() uint64 { return s.id } @@ -170,6 +177,8 @@ type ResourceRequest struct { // This is used to compute the solution and project tree so that // we can find references across all the projects in the solution irrespective of which project is open ProjectTree *ProjectTreeRequest + // AutoImports is the document URI for which auto imports should be prepared. + AutoImports lsproto.DocumentUri } type SnapshotChange struct { @@ -317,23 +326,23 @@ func (s *Snapshot) Clone(ctx context.Context, change SnapshotChange, overlays ma projectCollection, configFileRegistry := projectCollectionBuilder.Finalize(logger) + projectsWithNewProgramStructure := make(map[tspath.Path]bool) + for _, project := range projectCollection.Projects() { + if project.ProgramLastUpdate == newSnapshotID && project.ProgramUpdateKind != ProgramUpdateKindCloned { + projectsWithNewProgramStructure[project.configFilePath] = project.ProgramUpdateKind == ProgramUpdateKindNewFiles + } + } + // Clean cached disk files not touched by any open project. It's not important that we do this on // file open specifically, but we don't need to do it on every snapshot clone. if len(change.fileChanges.Opened) != 0 { - var changedFiles bool - for _, project := range projectCollection.Projects() { - if project.ProgramLastUpdate == newSnapshotID && project.ProgramUpdateKind != ProgramUpdateKindCloned { - changedFiles = true - break - } - } // The set of seen files can change only if a program was constructed (not cloned) during this snapshot. - if changedFiles { + if len(projectsWithNewProgramStructure) > 0 { cleanFilesStart := time.Now() removedFiles := 0 fs.diskFiles.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *diskFile]) bool { for _, project := range projectCollection.Projects() { - if project.host != nil && project.host.seenFiles.Has(entry.Key()) { + if project.host != nil && project.host.sourceFS.Seen(entry.Key()) { return true } } @@ -357,16 +366,49 @@ func (s *Snapshot) Clone(ctx context.Context, change SnapshotChange, overlays ma } } + autoImportHost := newAutoImportRegistryCloneHost( + projectCollection, + session.parseCache, + fs, + s.sessionOptions.CurrentDirectory, + s.toPath, + ) + openFiles := make(map[tspath.Path]string, len(overlays)) + for path, overlay := range overlays { + openFiles[path] = overlay.FileName() + } + oldAutoImports := s.AutoImports + if oldAutoImports == nil { + oldAutoImports = autoimport.NewRegistry(s.toPath) + } + prepareAutoImports := tspath.Path("") + if change.ResourceRequest.AutoImports != "" { + prepareAutoImports = change.ResourceRequest.AutoImports.Path(s.UseCaseSensitiveFileNames()) + } + var autoImportsWatch *WatchedFiles[map[tspath.Path]string] + autoImports, err := oldAutoImports.Clone(ctx, autoimport.RegistryChange{ + RequestedFile: prepareAutoImports, + OpenFiles: openFiles, + Changed: change.fileChanges.Changed, + Created: change.fileChanges.Created, + Deleted: change.fileChanges.Deleted, + RebuiltPrograms: projectsWithNewProgramStructure, + UserPreferences: config.tsUserPreferences, + }, autoImportHost, logger.Fork("UpdateAutoImports")) + if err == nil { + autoImportsWatch = s.autoImportsWatch.Clone(autoImports.NodeModulesDirectories()) + } + snapshotFS, _ := fs.Finalize() newSnapshot := NewSnapshot( newSnapshotID, snapshotFS, s.sessionOptions, - session.parseCache, - session.extendedConfigCache, nil, compilerOptionsForInferredProjects, config, + autoImports, + autoImportsWatch, s.toPath, ) newSnapshot.parentId = s.id diff --git a/internal/project/snapshot_test.go b/internal/project/snapshot_test.go index ded5e16b5e..8032907bc1 100644 --- a/internal/project/snapshot_test.go +++ b/internal/project/snapshot_test.go @@ -67,7 +67,7 @@ func TestSnapshot(t *testing.T) { assert.Equal(t, snapshotBefore.ProjectCollection.InferredProject(), snapshotAfter.ProjectCollection.InferredProject()) assert.Equal(t, snapshotAfter.ProjectCollection.InferredProject().ProgramUpdateKind, ProgramUpdateKindNewFiles) // host for inferred project should not change - assert.Equal(t, snapshotAfter.ProjectCollection.InferredProject().host.compilerFS.source, snapshotBefore.fs) + assert.Equal(t, snapshotAfter.ProjectCollection.InferredProject().host.sourceFS.source, snapshotBefore.fs) }) t.Run("cached disk files are cleaned up", func(t *testing.T) { diff --git a/internal/project/snapshotfs.go b/internal/project/snapshotfs.go index bff9168ba9..46fe914bdb 100644 --- a/internal/project/snapshotfs.go +++ b/internal/project/snapshotfs.go @@ -3,6 +3,7 @@ package project import ( "strings" "sync" + "time" "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/lsp/lsproto" @@ -16,6 +17,7 @@ import ( type FileSource interface { FS() vfs.FS GetFile(fileName string) FileHandle + GetFileByPath(fileName string, path tspath.Path) FileHandle } var ( @@ -38,10 +40,14 @@ func (s *SnapshotFS) FS() vfs.FS { } func (s *SnapshotFS) GetFile(fileName string) FileHandle { - if file, ok := s.overlays[s.toPath(fileName)]; ok { + return s.GetFileByPath(fileName, s.toPath(fileName)) +} + +func (s *SnapshotFS) GetFileByPath(fileName string, path tspath.Path) FileHandle { + if file, ok := s.overlays[path]; ok { return file } - if file, ok := s.diskFiles[s.toPath(fileName)]; ok { + if file, ok := s.diskFiles[path]; ok { return file } newEntry := memoizedDiskFile(sync.OnceValue(func() FileHandle { @@ -50,7 +56,7 @@ func (s *SnapshotFS) GetFile(fileName string) FileHandle { } return nil })) - entry, _ := s.readFiles.LoadOrStore(s.toPath(fileName), newEntry) + entry, _ := s.readFiles.LoadOrStore(path, newEntry) return entry() } @@ -112,22 +118,26 @@ func (s *snapshotFSBuilder) GetFileByPath(fileName string, path tspath.Path) Fil if file, ok := s.overlays[path]; ok { return file } - entry, _ := s.diskFiles.LoadOrStore(path, &diskFile{fileBase: fileBase{fileName: fileName}, needsReload: true}) - if entry != nil { - entry.Locked(func(entry dirty.Value[*diskFile]) { - if entry.Value() != nil && !entry.Value().MatchesDiskText() { - if content, ok := s.fs.ReadFile(fileName); ok { - entry.Change(func(file *diskFile) { - file.content = content - file.hash = xxh3.HashString128(content) - file.needsReload = false - }) - } else { - entry.Delete() - } - } - }) + if entry, _ := s.diskFiles.LoadOrStore(path, &diskFile{fileBase: fileBase{fileName: fileName}, needsReload: true}); entry != nil { + return s.reloadEntryIfNeeded(entry) } + return nil +} + +func (s *snapshotFSBuilder) reloadEntryIfNeeded(entry *dirty.SyncMapEntry[tspath.Path, *diskFile]) FileHandle { + entry.Locked(func(entry dirty.Value[*diskFile]) { + if entry.Value() != nil && !entry.Value().MatchesDiskText() { + if content, ok := s.fs.ReadFile(entry.Value().fileName); ok { + entry.Change(func(file *diskFile) { + file.content = content + file.hash = xxh3.HashString128(content) + file.needsReload = false + }) + } else { + entry.Delete() + } + } + }) if entry == nil || entry.Value() == nil { return nil } @@ -188,3 +198,114 @@ func (s *snapshotFSBuilder) markDirtyFiles(change FileChangeSummary) { } } } + +// sourceFS is a vfs.FS that sources files from a FileSource and tracks seen files. +type sourceFS struct { + tracking bool + toPath func(fileName string) tspath.Path + seenFiles *collections.SyncSet[tspath.Path] + source FileSource +} + +func newSourceFS(tracking bool, source FileSource, toPath func(fileName string) tspath.Path) *sourceFS { + fs := &sourceFS{ + tracking: tracking, + toPath: toPath, + source: source, + } + if tracking { + fs.seenFiles = &collections.SyncSet[tspath.Path]{} + } + return fs +} + +var _ vfs.FS = (*sourceFS)(nil) + +func (fs *sourceFS) DisableTracking() { + fs.tracking = false +} + +func (fs *sourceFS) Track(fileName string) { + if !fs.tracking { + return + } + fs.seenFiles.Add(fs.toPath(fileName)) +} + +func (fs *sourceFS) Seen(path tspath.Path) bool { + if fs.seenFiles == nil { + return false + } + return fs.seenFiles.Has(path) +} + +func (fs *sourceFS) GetFile(fileName string) FileHandle { + fs.Track(fileName) + return fs.source.GetFile(fileName) +} + +func (fs *sourceFS) GetFileByPath(fileName string, path tspath.Path) FileHandle { + fs.Track(fileName) + return fs.source.GetFileByPath(fileName, path) +} + +// DirectoryExists implements vfs.FS. +func (fs *sourceFS) DirectoryExists(path string) bool { + return fs.source.FS().DirectoryExists(path) +} + +// FileExists implements vfs.FS. +func (fs *sourceFS) FileExists(path string) bool { + if fh := fs.GetFile(path); fh != nil { + return true + } + return fs.source.FS().FileExists(path) +} + +// GetAccessibleEntries implements vfs.FS. +func (fs *sourceFS) GetAccessibleEntries(path string) vfs.Entries { + return fs.source.FS().GetAccessibleEntries(path) +} + +// ReadFile implements vfs.FS. +func (fs *sourceFS) ReadFile(path string) (contents string, ok bool) { + if fh := fs.GetFile(path); fh != nil { + return fh.Content(), true + } + return "", false +} + +// Realpath implements vfs.FS. +func (fs *sourceFS) Realpath(path string) string { + return fs.source.FS().Realpath(path) +} + +// Stat implements vfs.FS. +func (fs *sourceFS) Stat(path string) vfs.FileInfo { + return fs.source.FS().Stat(path) +} + +// UseCaseSensitiveFileNames implements vfs.FS. +func (fs *sourceFS) UseCaseSensitiveFileNames() bool { + return fs.source.FS().UseCaseSensitiveFileNames() +} + +// WalkDir implements vfs.FS. +func (fs *sourceFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { + return fs.source.FS().WalkDir(root, walkFn) +} + +// WriteFile implements vfs.FS. +func (fs *sourceFS) WriteFile(path string, data string, writeByteOrderMark bool) error { + panic("unimplemented") +} + +// Remove implements vfs.FS. +func (fs *sourceFS) Remove(path string) error { + panic("unimplemented") +} + +// Chtimes implements vfs.FS. +func (fs *sourceFS) Chtimes(path string, atime time.Time, mtime time.Time) error { + panic("unimplemented") +} diff --git a/internal/testutil/autoimporttestutil/fixtures.go b/internal/testutil/autoimporttestutil/fixtures.go new file mode 100644 index 0000000000..0fcdb64b5d --- /dev/null +++ b/internal/testutil/autoimporttestutil/fixtures.go @@ -0,0 +1,590 @@ +package autoimporttestutil + +import ( + "fmt" + "maps" + "slices" + "strings" + "testing" + + "github.com/microsoft/typescript-go/internal/ls/lsconv" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/project" + "github.com/microsoft/typescript-go/internal/testutil/projecttestutil" + "github.com/microsoft/typescript-go/internal/tspath" +) + +// FileHandle represents a file created for an autoimport lifecycle test. +type FileHandle struct { + fileName string + content string +} + +func (f FileHandle) FileName() string { return f.fileName } +func (f FileHandle) Content() string { return f.content } +func (f FileHandle) URI() lsproto.DocumentUri { return lsconv.FileNameToDocumentURI(f.fileName) } + +// ProjectFileHandle adds export metadata for TypeScript source files. +type ProjectFileHandle struct { + FileHandle + exportIdentifier string +} + +// NodeModulesPackageHandle describes a generated package under node_modules. +type NodeModulesPackageHandle struct { + Name string + Directory string + packageJSON FileHandle + declaration FileHandle +} + +func (p NodeModulesPackageHandle) PackageJSONFile() FileHandle { return p.packageJSON } +func (p NodeModulesPackageHandle) DeclarationFile() FileHandle { return p.declaration } + +// MonorepoHandle exposes the generated monorepo layout including root and packages. +type MonorepoHandle struct { + root string + rootNodeModules []NodeModulesPackageHandle + rootDependencies []string + packages []ProjectHandle + rootTSConfig FileHandle + rootPackageJSON FileHandle +} + +func (m MonorepoHandle) Root() string { return m.root } +func (m MonorepoHandle) RootNodeModules() []NodeModulesPackageHandle { + return slices.Clone(m.rootNodeModules) +} +func (m MonorepoHandle) RootDependencies() []string { return slices.Clone(m.rootDependencies) } +func (m MonorepoHandle) Packages() []ProjectHandle { return slices.Clone(m.packages) } +func (m MonorepoHandle) Package(index int) ProjectHandle { + if index < 0 || index >= len(m.packages) { + panic(fmt.Sprintf("package index %d out of range", index)) + } + return m.packages[index] +} +func (m MonorepoHandle) RootTSConfig() FileHandle { return m.rootTSConfig } +func (m MonorepoHandle) RootPackageJSONFile() FileHandle { return m.rootPackageJSON } + +// ProjectHandle exposes the generated project layout for a fixture project root. +type ProjectHandle struct { + root string + files []ProjectFileHandle + tsconfig FileHandle + packageJSON FileHandle + nodeModules []NodeModulesPackageHandle + dependencies []string +} + +func (p ProjectHandle) Root() string { return p.root } +func (p ProjectHandle) Files() []ProjectFileHandle { return slices.Clone(p.files) } +func (p ProjectHandle) File(index int) ProjectFileHandle { + if index < 0 || index >= len(p.files) { + panic(fmt.Sprintf("file index %d out of range", index)) + } + return p.files[index] +} +func (p ProjectHandle) TSConfig() FileHandle { return p.tsconfig } +func (p ProjectHandle) PackageJSONFile() FileHandle { return p.packageJSON } +func (p ProjectHandle) NodeModules() []NodeModulesPackageHandle { + return slices.Clone(p.nodeModules) +} +func (p ProjectHandle) Dependencies() []string { return slices.Clone(p.dependencies) } + +func (p ProjectHandle) NodeModuleByName(name string) *NodeModulesPackageHandle { + for i := range p.nodeModules { + if p.nodeModules[i].Name == name { + return &p.nodeModules[i] + } + } + return nil +} + +// Fixture encapsulates a fully-initialized auto import lifecycle test session. +type Fixture struct { + session *project.Session + utils *projecttestutil.SessionUtils + projects []ProjectHandle +} + +func (f *Fixture) Session() *project.Session { return f.session } +func (f *Fixture) Utils() *projecttestutil.SessionUtils { return f.utils } +func (f *Fixture) Projects() []ProjectHandle { return slices.Clone(f.projects) } +func (f *Fixture) Project(index int) ProjectHandle { + if index < 0 || index >= len(f.projects) { + panic(fmt.Sprintf("project index %d out of range", index)) + } + return f.projects[index] +} +func (f *Fixture) SingleProject() ProjectHandle { return f.Project(0) } + +// MonorepoFixture encapsulates a fully-initialized monorepo lifecycle test session. +type MonorepoFixture struct { + session *project.Session + utils *projecttestutil.SessionUtils + monorepo MonorepoHandle + extra []FileHandle +} + +func (f *MonorepoFixture) Session() *project.Session { return f.session } +func (f *MonorepoFixture) Utils() *projecttestutil.SessionUtils { return f.utils } +func (f *MonorepoFixture) Monorepo() MonorepoHandle { return f.monorepo } +func (f *MonorepoFixture) ExtraFiles() []FileHandle { return slices.Clone(f.extra) } +func (f *MonorepoFixture) ExtraFile(path string) FileHandle { + normalized := normalizeAbsolutePath(path) + for _, handle := range f.extra { + if handle.fileName == normalized { + return handle + } + } + panic("extra file not found: " + path) +} + +// MonorepoPackageTemplate captures the reusable settings for a package.json scope: +// the node_modules packages that exist alongside the package.json and the dependency +// names that should be written into that package.json. When DependencyNames is empty, +// all available node_modules packages in scope are used. +type MonorepoPackageTemplate struct { + Name string + NodeModuleNames []string + DependencyNames []string +} + +// MonorepoSetupConfig describes the monorepo root and packages to create. +// The embedded MonorepoPackageTemplate describes the monorepo root package located at +// Root. DependencyNames defaults to NodeModuleNames when empty. +// Package.MonorepoPackageTemplate.DependencyNames defaults to the union of the root +// node_modules packages and the package's own NodeModuleNames when empty. +type MonorepoSetupConfig struct { + Root string + MonorepoPackageTemplate + Packages []MonorepoPackageConfig + ExtraFiles []TextFileSpec +} + +type MonorepoPackageConfig struct { + FileCount int + MonorepoPackageTemplate +} + +// TextFileSpec describes an additional file to place in the fixture. +type TextFileSpec struct { + Path string + Content string +} + +// SetupMonorepoLifecycleSession builds a monorepo workspace with root-level node_modules +// and multiple packages, each potentially with their own node_modules. +// The structure is: +// +// root/ +// ├── tsconfig.json (base config) +// ├── package.json +// ├── node_modules/ +// │ └── +// └── packages/ +// ├── package-a/ +// │ ├── tsconfig.json +// │ ├── package.json +// │ ├── node_modules/ +// │ │ └── +// │ └── *.ts files +// └── package-b/ +// └── ... +func SetupMonorepoLifecycleSession(t *testing.T, config MonorepoSetupConfig) *MonorepoFixture { + t.Helper() + builder := newFileMapBuilder(nil) + + monorepoRoot := normalizeAbsolutePath(config.Root) + monorepoName := config.MonorepoPackageTemplate.Name + if monorepoName == "" { + monorepoName = "monorepo" + } + + // Add root tsconfig.json + rootTSConfigPath := tspath.CombinePaths(monorepoRoot, "tsconfig.json") + rootTSConfigContent := "{\n \"compilerOptions\": {\n \"module\": \"esnext\",\n \"target\": \"esnext\",\n \"strict\": true,\n \"baseUrl\": \".\",\n \"allowJs\": true,\n \"checkJs\": true\n }\n}\n" + builder.AddTextFile(rootTSConfigPath, rootTSConfigContent) + rootTSConfig := FileHandle{fileName: rootTSConfigPath, content: rootTSConfigContent} + + // Add root node_modules + rootNodeModulesDir := tspath.CombinePaths(monorepoRoot, "node_modules") + rootNodeModules := builder.AddNodeModulesPackagesWithNames(rootNodeModulesDir, config.NodeModuleNames) + + // Add root package.json with dependencies (default to all root node_modules if unspecified) + rootDependencies := selectPackagesByName(rootNodeModules, config.DependencyNames) + rootPackageJSON := builder.addRootPackageJSON(monorepoRoot, monorepoName, rootDependencies) + rootDependencyNames := packageNames(rootDependencies) + + // Build each package in packages/ + packagesDir := tspath.CombinePaths(monorepoRoot, "packages") + packageHandles := make([]ProjectHandle, 0, len(config.Packages)) + for _, pkg := range config.Packages { + pkgDir := tspath.CombinePaths(packagesDir, pkg.Name) + builder.AddLocalProject(pkgDir, pkg.FileCount) + + var pkgNodeModules []NodeModulesPackageHandle + if len(pkg.NodeModuleNames) > 0 { + pkgNodeModulesDir := tspath.CombinePaths(pkgDir, "node_modules") + pkgNodeModules = builder.AddNodeModulesPackagesWithNames(pkgNodeModulesDir, pkg.NodeModuleNames) + } + + availableDeps := append(slices.Clone(rootNodeModules), pkgNodeModules...) + selectedDeps := selectPackagesByName(availableDeps, pkg.DependencyNames) + if len(selectedDeps) > 0 { + builder.AddPackageJSONWithDependenciesNamed(pkgDir, pkg.Name, selectedDeps) + } + } + + // Add arbitrary extra files + extraHandles := make([]FileHandle, 0, len(config.ExtraFiles)) + for _, extra := range config.ExtraFiles { + builder.AddTextFile(extra.Path, extra.Content) + extraHandles = append(extraHandles, FileHandle{fileName: normalizeAbsolutePath(extra.Path), content: extra.Content}) + } + + // Build project handles after all packages are created + for _, pkg := range config.Packages { + pkgDir := tspath.CombinePaths(packagesDir, pkg.Name) + if record, ok := builder.projects[pkgDir]; ok { + packageHandles = append(packageHandles, record.toHandles()) + } + } + + session, sessionUtils := projecttestutil.Setup(builder.Files()) + t.Cleanup(session.Close) + + // Build root node_modules handle by looking at the project record for the workspace root + // (created as side effect of AddNodeModulesPackages) + var rootNodeModulesHandles []NodeModulesPackageHandle + if rootRecord, ok := builder.projects[monorepoRoot]; ok { + rootNodeModulesHandles = rootRecord.nodeModules + } + + return &MonorepoFixture{ + session: session, + utils: sessionUtils, + monorepo: MonorepoHandle{ + root: monorepoRoot, + rootNodeModules: rootNodeModulesHandles, + rootDependencies: rootDependencyNames, + packages: packageHandles, + rootTSConfig: rootTSConfig, + rootPackageJSON: rootPackageJSON, + }, + extra: extraHandles, + } +} + +// SetupLifecycleSession builds a basic single-project workspace configured with the +// requested number of TypeScript files and a single synthetic node_modules package. +func SetupLifecycleSession(t *testing.T, projectRoot string, fileCount int) *Fixture { + t.Helper() + builder := newFileMapBuilder(nil) + builder.AddLocalProject(projectRoot, fileCount) + nodeModulesDir := tspath.CombinePaths(projectRoot, "node_modules") + deps := builder.AddNodeModulesPackages(nodeModulesDir, 1) + builder.AddPackageJSONWithDependencies(projectRoot, deps) + session, sessionUtils := projecttestutil.Setup(builder.Files()) + t.Cleanup(session.Close) + return &Fixture{ + session: session, + utils: sessionUtils, + projects: builder.projectHandles(), + } +} + +type fileMapBuilder struct { + files map[string]any + nextPackageID int + nextProjectID int + projects map[string]*projectRecord +} + +type projectRecord struct { + root string + sourceFiles []projectFile + tsconfig FileHandle + packageJSON *FileHandle + nodeModules []NodeModulesPackageHandle + dependencies []string +} + +type projectFile struct { + FileName string + ExportIdentifier string + Content string +} + +func newFileMapBuilder(initial map[string]any) *fileMapBuilder { + b := &fileMapBuilder{ + files: make(map[string]any), + projects: make(map[string]*projectRecord), + } + if len(initial) == 0 { + return b + } + for path, content := range initial { + b.files[normalizeAbsolutePath(path)] = content + } + return b +} + +func (b *fileMapBuilder) ensureProjectRecord(root string) *projectRecord { + if record, ok := b.projects[root]; ok { + return record + } + record := &projectRecord{root: root} + b.projects[root] = record + return record +} + +func (b *fileMapBuilder) projectHandles() []ProjectHandle { + keys := slices.Collect(maps.Keys(b.projects)) + slices.Sort(keys) + result := make([]ProjectHandle, 0, len(keys)) + for _, key := range keys { + result = append(result, b.projects[key].toHandles()) + } + return result +} + +func (r *projectRecord) toHandles() ProjectHandle { + files := make([]ProjectFileHandle, len(r.sourceFiles)) + for i, file := range r.sourceFiles { + files[i] = ProjectFileHandle{ + FileHandle: FileHandle{fileName: file.FileName, content: file.Content}, + exportIdentifier: file.ExportIdentifier, + } + } + packageJSON := FileHandle{} + if r.packageJSON != nil { + packageJSON = *r.packageJSON + } + return ProjectHandle{ + root: r.root, + files: files, + tsconfig: r.tsconfig, + packageJSON: packageJSON, + nodeModules: slices.Clone(r.nodeModules), + dependencies: slices.Clone(r.dependencies), + } +} + +func (b *fileMapBuilder) Files() map[string]any { + return maps.Clone(b.files) +} + +func (b *fileMapBuilder) AddTextFile(path string, contents string) { + b.ensureFiles() + b.files[normalizeAbsolutePath(path)] = contents +} + +func (b *fileMapBuilder) AddNodeModulesPackages(nodeModulesDir string, count int) []NodeModulesPackageHandle { + packages := make([]NodeModulesPackageHandle, 0, count) + for range count { + packages = append(packages, b.AddNodeModulesPackage(nodeModulesDir)) + } + return packages +} + +func (b *fileMapBuilder) AddNodeModulesPackagesWithNames(nodeModulesDir string, names []string) []NodeModulesPackageHandle { + if len(names) == 0 { + return nil + } + packages := make([]NodeModulesPackageHandle, 0, len(names)) + for _, name := range names { + packages = append(packages, b.AddNamedNodeModulesPackage(nodeModulesDir, name)) + } + return packages +} + +func (b *fileMapBuilder) AddNodeModulesPackage(nodeModulesDir string) NodeModulesPackageHandle { + return b.AddNamedNodeModulesPackage(nodeModulesDir, "") +} + +func (b *fileMapBuilder) AddNamedNodeModulesPackage(nodeModulesDir string, name string) NodeModulesPackageHandle { + b.ensureFiles() + normalizedDir := normalizeAbsolutePath(nodeModulesDir) + if tspath.GetBaseFileName(normalizedDir) != "node_modules" { + panic("nodeModulesDir must point to a node_modules directory: " + nodeModulesDir) + } + b.nextPackageID++ + resolvedName := name + if resolvedName == "" { + resolvedName = fmt.Sprintf("pkg%d", b.nextPackageID) + } + exportName := sanitizeIdentifier(resolvedName) + "_value" + pkgDir := tspath.CombinePaths(normalizedDir, resolvedName) + packageJSONPath := tspath.CombinePaths(pkgDir, "package.json") + packageJSONContent := fmt.Sprintf(`{"name":"%s","types":"index.d.ts"}`, resolvedName) + b.files[packageJSONPath] = packageJSONContent + declarationPath := tspath.CombinePaths(pkgDir, "index.d.ts") + declarationContent := fmt.Sprintf("export declare const %s: number;\n", exportName) + b.files[declarationPath] = declarationContent + packageHandle := NodeModulesPackageHandle{ + Name: resolvedName, + Directory: pkgDir, + packageJSON: FileHandle{fileName: packageJSONPath, content: packageJSONContent}, + declaration: FileHandle{fileName: declarationPath, content: declarationContent}, + } + projectRoot := tspath.GetDirectoryPath(normalizedDir) + record := b.ensureProjectRecord(projectRoot) + record.nodeModules = append(record.nodeModules, packageHandle) + return packageHandle +} + +func (b *fileMapBuilder) AddLocalProject(projectDir string, fileCount int) { + b.ensureFiles() + if fileCount < 0 { + panic("fileCount must be non-negative") + } + dir := normalizeAbsolutePath(projectDir) + record := b.ensureProjectRecord(dir) + b.nextProjectID++ + tsConfigPath := tspath.CombinePaths(dir, "tsconfig.json") + tsConfigContent := "{\n \"compilerOptions\": {\n \"module\": \"esnext\",\n \"target\": \"esnext\",\n \"strict\": true,\n \"allowJs\": true,\n \"checkJs\": true\n }\n}\n" + b.files[tsConfigPath] = tsConfigContent + record.tsconfig = FileHandle{fileName: tsConfigPath, content: tsConfigContent} + for i := 1; i <= fileCount; i++ { + path := tspath.CombinePaths(dir, fmt.Sprintf("file%d.ts", i)) + exportName := fmt.Sprintf("localExport%d_%d", b.nextProjectID, i) + content := fmt.Sprintf("export const %s = %d;\n", exportName, i) + b.files[path] = content + record.sourceFiles = append(record.sourceFiles, projectFile{FileName: path, ExportIdentifier: exportName, Content: content}) + } +} + +func (b *fileMapBuilder) AddPackageJSONWithDependencies(projectDir string, deps []NodeModulesPackageHandle) FileHandle { + b.nextProjectID++ + return b.AddPackageJSONWithDependenciesNamed(projectDir, fmt.Sprintf("local-project-%d", b.nextProjectID), deps) +} + +func (b *fileMapBuilder) AddPackageJSONWithDependenciesNamed(projectDir string, packageName string, deps []NodeModulesPackageHandle) FileHandle { + b.ensureFiles() + dir := normalizeAbsolutePath(projectDir) + packageJSONPath := tspath.CombinePaths(dir, "package.json") + dependencyLines := make([]string, 0, len(deps)) + for _, dep := range deps { + dependencyLines = append(dependencyLines, fmt.Sprintf("\"%s\": \"*\"", dep.Name)) + } + var builder strings.Builder + name := packageName + if name == "" { + b.nextProjectID++ + name = fmt.Sprintf("local-project-%d", b.nextProjectID) + } + builder.WriteString(fmt.Sprintf("{\n \"name\": \"%s\"", name)) + if len(dependencyLines) > 0 { + builder.WriteString(",\n \"dependencies\": {\n ") + builder.WriteString(strings.Join(dependencyLines, ",\n ")) + builder.WriteString("\n }\n") + } else { + builder.WriteString("\n") + } + builder.WriteString("}\n") + content := builder.String() + b.files[packageJSONPath] = content + record := b.ensureProjectRecord(dir) + packageHandle := FileHandle{fileName: packageJSONPath, content: content} + record.packageJSON = &packageHandle + record.dependencies = packageNames(deps) + return packageHandle +} + +// addRootPackageJSON creates a root package.json for a monorepo without creating a project record. +// This is used to set up the root workspace config without treating it as a project. +func (b *fileMapBuilder) addRootPackageJSON(rootDir string, packageName string, deps []NodeModulesPackageHandle) FileHandle { + b.ensureFiles() + dir := normalizeAbsolutePath(rootDir) + packageJSONPath := tspath.CombinePaths(dir, "package.json") + dependencyLines := make([]string, 0, len(deps)) + for _, dep := range deps { + dependencyLines = append(dependencyLines, fmt.Sprintf("\"%s\": \"*\"", dep.Name)) + } + var builder strings.Builder + pkgName := packageName + if pkgName == "" { + pkgName = "monorepo-root" + } + builder.WriteString(fmt.Sprintf("{\n \"name\": \"%s\",\n \"private\": true", pkgName)) + if len(dependencyLines) > 0 { + builder.WriteString(",\n \"dependencies\": {\n ") + builder.WriteString(strings.Join(dependencyLines, ",\n ")) + builder.WriteString("\n }\n") + } else { + builder.WriteString("\n") + } + builder.WriteString("}\n") + content := builder.String() + b.files[packageJSONPath] = content + return FileHandle{fileName: packageJSONPath, content: content} +} + +func selectPackagesByName(available []NodeModulesPackageHandle, names []string) []NodeModulesPackageHandle { + if len(names) == 0 { + return slices.Clone(available) + } + result := make([]NodeModulesPackageHandle, 0, len(names)) + for _, name := range names { + found := false + for _, candidate := range available { + if candidate.Name == name { + result = append(result, candidate) + found = true + break + } + } + if !found { + panic("dependency not found: " + name) + } + } + return result +} + +func packageNames(deps []NodeModulesPackageHandle) []string { + if len(deps) == 0 { + return nil + } + names := make([]string, 0, len(deps)) + for _, dep := range deps { + names = append(names, dep.Name) + } + return names +} + +func sanitizeIdentifier(name string) string { + sanitized := strings.Map(func(r rune) rune { + if r >= 'a' && r <= 'z' { + return r + } + if r >= 'A' && r <= 'Z' { + return r + } + if r >= '0' && r <= '9' { + return r + } + if r == '_' || r == '-' { + return '_' + } + return -1 + }, name) + if sanitized == "" { + return "pkg" + } + return sanitized +} + +func (b *fileMapBuilder) ensureFiles() { + if b.files == nil { + b.files = make(map[string]any) + } +} + +func normalizeAbsolutePath(path string) string { + normalized := tspath.NormalizePath(path) + if !tspath.PathIsAbsolute(normalized) { + panic("paths used in lifecycle tests must be absolute: " + path) + } + return normalized +} diff --git a/internal/testutil/projecttestutil/projecttestutil.go b/internal/testutil/projecttestutil/projecttestutil.go index 476212b4e3..e5f6a37ddc 100644 --- a/internal/testutil/projecttestutil/projecttestutil.go +++ b/internal/testutil/projecttestutil/projecttestutil.go @@ -3,6 +3,7 @@ package projecttestutil import ( "context" "fmt" + "os" "slices" "strings" "sync" @@ -14,8 +15,10 @@ import ( "github.com/microsoft/typescript-go/internal/project" "github.com/microsoft/typescript-go/internal/project/logging" "github.com/microsoft/typescript-go/internal/testutil/baseline" + "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" "github.com/microsoft/typescript-go/internal/vfs/iovfs" + "github.com/microsoft/typescript-go/internal/vfs/osvfs" "github.com/microsoft/typescript-go/internal/vfs/vfstest" ) @@ -35,12 +38,13 @@ type TypingsInstallerOptions struct { } type SessionUtils struct { - fsFromFileMap iovfs.FsWithSys - fs vfs.FS - client *ClientMock - npmExecutor *NpmExecutorMock - tiOptions *TypingsInstallerOptions - logger logging.LogCollector + currentDirectory string + fsFromFileMap iovfs.FsWithSys + fs vfs.FS + client *ClientMock + npmExecutor *NpmExecutorMock + tiOptions *TypingsInstallerOptions + logger logging.LogCollector } func (h *SessionUtils) FsFromFileMap() iovfs.FsWithSys { @@ -105,6 +109,10 @@ func (h *SessionUtils) SetupNpmExecutorForTypingsInstaller() { } } +func (h *SessionUtils) ToPath(fileName string) tspath.Path { + return tspath.ToPath(fileName, h.currentDirectory, h.fs.UseCaseSensitiveFileNames()) +} + func (h *SessionUtils) FS() vfs.FS { return h.fs } @@ -189,6 +197,39 @@ func Setup(files map[string]any) (*project.Session, *SessionUtils) { return SetupWithTypingsInstaller(files, &TypingsInstallerOptions{}) } +func SetupWithRealFS() (*project.Session, *SessionUtils) { + fs := bundled.WrapFS(osvfs.FS()) + clientMock := &ClientMock{} + npmExecutorMock := &NpmExecutorMock{} + wd, err := os.Getwd() + if err != nil { + panic(err) + } + + sessionUtils := &SessionUtils{ + currentDirectory: wd, + fs: fs, + client: clientMock, + npmExecutor: npmExecutorMock, + logger: logging.NewTestLogger(), + } + + return project.NewSession(&project.SessionInit{ + FS: fs, + Client: clientMock, + NpmExecutor: npmExecutorMock, + Logger: sessionUtils.logger, + Options: &project.SessionOptions{ + CurrentDirectory: wd, + DefaultLibraryPath: bundled.LibPath(), + PositionEncoding: lsproto.PositionEncodingKindUTF8, + WatchEnabled: true, + LoggingEnabled: true, + PushDiagnosticsEnabled: true, + }, + }), sessionUtils +} + func SetupWithOptions(files map[string]any, options *project.SessionOptions) (*project.Session, *SessionUtils) { return SetupWithOptionsAndTypingsInstaller(files, options, &TypingsInstallerOptions{}) } @@ -214,12 +255,13 @@ func GetSessionInitOptions(files map[string]any, options *project.SessionOptions clientMock := &ClientMock{} npmExecutorMock := &NpmExecutorMock{} sessionUtils := &SessionUtils{ - fsFromFileMap: fsFromFileMap.(iovfs.FsWithSys), - fs: fs, - client: clientMock, - npmExecutor: npmExecutorMock, - tiOptions: tiOptions, - logger: logging.NewTestLogger(), + currentDirectory: "/", + fsFromFileMap: fsFromFileMap.(iovfs.FsWithSys), + fs: fs, + client: clientMock, + npmExecutor: npmExecutorMock, + tiOptions: tiOptions, + logger: logging.NewTestLogger(), } // Configure the npm executor mock to handle typings installation diff --git a/internal/tspath/extension.go b/internal/tspath/extension.go index 1b4422e791..8303f0657d 100644 --- a/internal/tspath/extension.go +++ b/internal/tspath/extension.go @@ -23,8 +23,8 @@ const ( ) var ( - supportedDeclarationExtensions = []string{ExtensionDts, ExtensionDcts, ExtensionDmts} - supportedTSImplementationExtensions = []string{ExtensionTs, ExtensionTsx, ExtensionMts, ExtensionCts} + SupportedDeclarationExtensions = []string{ExtensionDts, ExtensionDcts, ExtensionDmts} + SupportedTSImplementationExtensions = []string{ExtensionTs, ExtensionTsx, ExtensionMts, ExtensionCts} supportedTSExtensionsForExtractExtension = []string{ExtensionDts, ExtensionDcts, ExtensionDmts, ExtensionTs, ExtensionTsx, ExtensionMts, ExtensionCts} AllSupportedExtensions = [][]string{{ExtensionTs, ExtensionTsx, ExtensionDts, ExtensionJs, ExtensionJsx}, {ExtensionCts, ExtensionDcts, ExtensionCjs}, {ExtensionMts, ExtensionDmts, ExtensionMjs}} SupportedTSExtensions = [][]string{{ExtensionTs, ExtensionTsx, ExtensionDts}, {ExtensionCts, ExtensionDcts}, {ExtensionMts, ExtensionDmts}} @@ -90,7 +90,7 @@ func HasTSFileExtension(path string) bool { } func HasImplementationTSFileExtension(path string) bool { - return FileExtensionIsOneOf(path, supportedTSImplementationExtensions) && !IsDeclarationFileName(path) + return FileExtensionIsOneOf(path, SupportedTSImplementationExtensions) && !IsDeclarationFileName(path) } func HasJSFileExtension(path string) bool { @@ -111,7 +111,7 @@ func ExtensionIsOneOf(ext string, extensions []string) bool { func GetDeclarationFileExtension(fileName string) string { base := GetBaseFileName(fileName) - for _, ext := range supportedDeclarationExtensions { + for _, ext := range SupportedDeclarationExtensions { if strings.HasSuffix(base, ext) { return ext } @@ -138,26 +138,28 @@ func GetDeclarationEmitExtensionForPath(path string) string { } } -// changeAnyExtension changes the extension of a path to the provided extension if it has one of the provided extensions. +// ChangeAnyExtension changes the extension of a path to the provided extension if it has one of the provided extensions. // -// changeAnyExtension("/path/to/file.ext", ".js", ".ext") === "/path/to/file.js" -// changeAnyExtension("/path/to/file.ext", ".js", ".ts") === "/path/to/file.ext" -// changeAnyExtension("/path/to/file.ext", ".js", [".ext", ".ts"]) === "/path/to/file.js" -func changeAnyExtension(path string, ext string, extensions []string, ignoreCase bool) string { +// ChangeAnyExtension("/path/to/file.ext", ".js", ".ext") === "/path/to/file.js" +// ChangeAnyExtension("/path/to/file.ext", ".js", ".ts") === "/path/to/file.ext" +// ChangeAnyExtension("/path/to/file.ext", ".js", [".ext", ".ts"]) === "/path/to/file.js" +func ChangeAnyExtension(path string, ext string, extensions []string, ignoreCase bool) string { pathext := GetAnyExtensionFromPath(path, extensions, ignoreCase) if pathext != "" { result := path[:len(path)-len(pathext)] + if ext == "" { + return result + } if strings.HasPrefix(ext, ".") { return result + ext - } else { - return result + "." + ext } + return result + "." + ext } return path } func ChangeExtension(path string, newExtension string) string { - return changeAnyExtension(path, newExtension, extensionsToRemove /*ignoreCase*/, false) + return ChangeAnyExtension(path, newExtension, extensionsToRemove /*ignoreCase*/, false) } // Like `changeAnyExtension`, but declaration file extensions are recognized diff --git a/testdata/baselines/reference/fourslash/autoImports/autoImportErrorMixedExportKinds.baseline.md b/testdata/baselines/reference/fourslash/autoImports/autoImportErrorMixedExportKinds.baseline.md new file mode 100644 index 0000000000..071262ccc3 --- /dev/null +++ b/testdata/baselines/reference/fourslash/autoImports/autoImportErrorMixedExportKinds.baseline.md @@ -0,0 +1,12 @@ +// === Auto Imports === +```ts +// @FileName: /b.ts +foo/**/ + +``````ts +import { foo } from "./a"; + +foo + +``` + diff --git a/testdata/baselines/reference/fourslash/autoImports/autoImportModuleAugmentation.baseline.md b/testdata/baselines/reference/fourslash/autoImports/autoImportModuleAugmentation.baseline.md new file mode 100644 index 0000000000..169ca5b2a7 --- /dev/null +++ b/testdata/baselines/reference/fourslash/autoImports/autoImportModuleAugmentation.baseline.md @@ -0,0 +1,12 @@ +// === Auto Imports === +```ts +// @FileName: /c.ts +Foo/**/ + +``````ts +import { Foo } from "./a"; + +Foo + +``` +