vendor tsgo
This commit is contained in:
236
tools/tsgo/internal/ls/autoimport/aliasresolver.go
Normal file
236
tools/tsgo/internal/ls/autoimport/aliasresolver.go
Normal file
@@ -0,0 +1,236 @@
|
||||
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 pathAndFileName struct {
|
||||
path tspath.Path
|
||||
fileName string
|
||||
}
|
||||
|
||||
type aliasResolver struct {
|
||||
toPath func(fileName string) tspath.Path
|
||||
host RegistryCloneHost
|
||||
moduleResolver *module.Resolver
|
||||
|
||||
rootFiles []*ast.SourceFile
|
||||
// symlinks maps from realpath to symlinked path and file name
|
||||
symlinks map[tspath.Path]pathAndFileName
|
||||
onFailedAmbientModuleLookup func(source ast.HasFileName, moduleName string)
|
||||
resolvedModules collections.SyncMap[tspath.Path, *collections.SyncMap[module.ModeAwareCacheKey, *module.ResolvedModule]]
|
||||
}
|
||||
|
||||
func newAliasResolver(
|
||||
rootFiles []*ast.SourceFile,
|
||||
symlinks map[tspath.Path]pathAndFileName,
|
||||
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,
|
||||
symlinks: symlinks,
|
||||
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))
|
||||
// file may be nil due to symlink/realpath mismatch; see TestAutoImportBuilderFS
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
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 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
func (r *aliasResolver) GetPackagesMap() map[string]bool {
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ checker.Program = (*aliasResolver)(nil)
|
||||
@@ -0,0 +1,73 @@
|
||||
package autoimport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"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/compiler"
|
||||
"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/parser"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
"github.com/microsoft/typescript-go/internal/vfs/vfstest"
|
||||
)
|
||||
|
||||
type fakeCloneHost struct {
|
||||
fs vfs.FS
|
||||
}
|
||||
|
||||
func (h *fakeCloneHost) FS() vfs.FS { return h.fs }
|
||||
func (h *fakeCloneHost) GetCurrentDirectory() string { return "/" }
|
||||
func (h *fakeCloneHost) GetDefaultProject(path tspath.Path) (tspath.Path, *compiler.Program) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (h *fakeCloneHost) GetProgramForProject(projectPath tspath.Path) *compiler.Program { return nil }
|
||||
|
||||
func (h *fakeCloneHost) GetPackageJson(fileName string) *packagejson.InfoCacheEntry { return nil }
|
||||
|
||||
func (h *fakeCloneHost) GetSourceFile(fileName string, path tspath.Path) *ast.SourceFile { return nil }
|
||||
func (h *fakeCloneHost) Dispose() {}
|
||||
|
||||
var _ RegistryCloneHost = (*fakeCloneHost)(nil)
|
||||
|
||||
// Regression test for microsoft/typescript-go#4322.
|
||||
//
|
||||
// During auto-import export extraction, the checker is built on top of an
|
||||
// aliasResolver standing in for a real program. This file has a type error, and
|
||||
// extracting exports should still complete without crashing.
|
||||
func TestAliasResolverGetDiagnosticsDoesNotPanic(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const fileName = "/pkg/index.ts"
|
||||
text := "declare function f(arg: { a: string }): () => void;\nexport const x = f({ a: 1 });\n"
|
||||
|
||||
fs := vfstest.FromMap(map[string]string{fileName: text}, true /*useCaseSensitiveFileNames*/)
|
||||
host := &fakeCloneHost{fs: fs}
|
||||
|
||||
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
|
||||
FileName: fileName,
|
||||
Path: tspath.Path(fileName),
|
||||
}, text, core.ScriptKindTS)
|
||||
binder.BindSourceFile(sourceFile)
|
||||
|
||||
resolver := module.NewResolver(host, core.EmptyCompilerOptions, "", "")
|
||||
r := newAliasResolver(
|
||||
[]*ast.SourceFile{sourceFile},
|
||||
nil,
|
||||
host,
|
||||
resolver,
|
||||
func(f string) tspath.Path { return tspath.Path(f) },
|
||||
func(ast.HasFileName, string) {},
|
||||
)
|
||||
|
||||
ch, _ := checker.NewChecker(r, nil)
|
||||
|
||||
// Type-checking this file's diagnostics must not panic.
|
||||
ch.GetDiagnostics(context.Background(), sourceFile)
|
||||
}
|
||||
142
tools/tsgo/internal/ls/autoimport/export.go
Normal file
142
tools/tsgo/internal/ls/autoimport/export.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package autoimport
|
||||
|
||||
import (
|
||||
"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/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 npx dprint fmt 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 lsutil.ScriptElementKindModifier
|
||||
|
||||
// The file where the export was found.
|
||||
Path tspath.Path
|
||||
|
||||
PackageName string
|
||||
}
|
||||
|
||||
func (e *Export) Name() string {
|
||||
if e.localName != "" {
|
||||
return e.localName
|
||||
}
|
||||
if e.ExportName == ast.InternalSymbolNameExportEquals {
|
||||
return e.Target.ExportName
|
||||
}
|
||||
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) {
|
||||
if moduleID, moduleFileName, ok := tryGetModuleIDAndFileNameOfModuleSymbol(symbol.Parent); ok {
|
||||
return extractFirstExport(symbol, ch, moduleID, moduleFileName, ast.GetSourceFileOfModule(symbol.Parent))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
declaration := core.FirstOrNil(symbol.Declarations)
|
||||
if declaration == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
file := ast.GetSourceFileOfNode(declaration)
|
||||
if file.Symbol == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
moduleSymbol := ch.GetMergedSymbol(file.Symbol)
|
||||
moduleID := ModuleID(file.Path())
|
||||
moduleFileName := file.FileName()
|
||||
target := ch.GetMergedSymbol(ch.SkipAlias(symbol))
|
||||
|
||||
if export := tryGetModuleExport(ast.InternalSymbolNameDefault, target, moduleSymbol, ch, moduleID, moduleFileName, file); export != nil {
|
||||
return export
|
||||
}
|
||||
if export := tryGetModuleExport(ast.InternalSymbolNameExportEquals, target, moduleSymbol, ch, moduleID, moduleFileName, file); export != nil {
|
||||
return export
|
||||
}
|
||||
return tryGetModuleExport(symbol.Name, target, moduleSymbol, ch, moduleID, moduleFileName, file)
|
||||
}
|
||||
|
||||
func tryGetModuleExport(exportName string, target *ast.Symbol, moduleSymbol *ast.Symbol, ch *checker.Checker, moduleID ModuleID, moduleFileName string, file *ast.SourceFile) *Export {
|
||||
exported := ch.TryGetMemberInModuleExportsAndProperties(exportName, moduleSymbol)
|
||||
if exported != nil && ch.GetMergedSymbol(ch.SkipAlias(exported)) == target {
|
||||
return extractFirstExport(exported, ch, moduleID, moduleFileName, file)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractFirstExport(symbol *ast.Symbol, ch *checker.Checker, moduleID ModuleID, moduleFileName string, file *ast.SourceFile) *Export {
|
||||
var exports []*Export
|
||||
extractor := newSymbolExtractor("", ch, nil, nil)
|
||||
extractor.extractFromSymbol(symbol.Name, symbol, moduleID, moduleFileName, file, &exports)
|
||||
return core.FirstOrNil(exports)
|
||||
}
|
||||
@@ -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]]
|
||||
}
|
||||
461
tools/tsgo/internal/ls/autoimport/extract.go
Normal file
461
tools/tsgo/internal/ls/autoimport/extract.go
Normal file
@@ -0,0 +1,461 @@
|
||||
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 {
|
||||
packageName string
|
||||
stats *extractorStats
|
||||
|
||||
localNameResolver *binder.NameResolver
|
||||
checker *checker.Checker
|
||||
toPath func(fileName string) tspath.Path
|
||||
// realpath, if set, is used to resolve symlinks for ModuleID generation.
|
||||
// This ensures that symlinked packages use their realpath as ModuleID,
|
||||
// deduplicating exports from files that appear via multiple symlink paths.
|
||||
realpath func(fileName string) string
|
||||
}
|
||||
|
||||
type exportExtractor struct {
|
||||
*symbolExtractor
|
||||
moduleResolver *module.Resolver
|
||||
}
|
||||
|
||||
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(packageName string, checker *checker.Checker, toPath func(string) tspath.Path, realpath func(string) string) *symbolExtractor {
|
||||
return &symbolExtractor{
|
||||
packageName: packageName,
|
||||
checker: checker,
|
||||
localNameResolver: &binder.NameResolver{
|
||||
CompilerOptions: core.EmptyCompilerOptions,
|
||||
},
|
||||
stats: &extractorStats{},
|
||||
toPath: toPath,
|
||||
realpath: realpath,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *registryBuilder) newExportExtractor(packageName string, checker *checker.Checker, moduleResolver *module.Resolver, realpath func(string) string) *exportExtractor {
|
||||
return &exportExtractor{
|
||||
symbolExtractor: newSymbolExtractor(packageName, checker, b.base.toPath, realpath),
|
||||
moduleResolver: moduleResolver,
|
||||
}
|
||||
}
|
||||
|
||||
// getModuleID returns the ModuleID for a file, using realpath if available.
|
||||
func (e *symbolExtractor) getModuleID(file *ast.SourceFile) ModuleID {
|
||||
if e.realpath != nil && e.toPath != nil {
|
||||
realpath := e.realpath(file.FileName())
|
||||
return ModuleID(e.toPath(realpath))
|
||||
}
|
||||
return ModuleID(file.Path())
|
||||
}
|
||||
|
||||
// getModuleIDForSymbol returns the ModuleID for a module symbol, using realpath
|
||||
// normalization when available for source files.
|
||||
func (e *symbolExtractor) getModuleIDForSymbol(symbol *ast.Symbol) (ModuleID, bool) {
|
||||
moduleID, fileName, ok := tryGetModuleIDAndFileNameOfModuleSymbol(symbol)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
// If fileName is set, this is a source file that may need realpath normalization
|
||||
if fileName != "" && e.realpath != nil {
|
||||
decl := ast.GetNonAugmentationDeclaration(symbol)
|
||||
if decl != nil && decl.Kind == ast.KindSourceFile {
|
||||
return e.getModuleID(decl.AsSourceFile()), true
|
||||
}
|
||||
}
|
||||
return moduleID, true
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
moduleID := e.getModuleID(file)
|
||||
exports := make([]*Export, 0, len(file.Symbol.Exports)+augmentationExportCount)
|
||||
for name, symbol := range file.Symbol.Exports {
|
||||
e.extractFromSymbol(name, symbol, moduleID, 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 := checkerLease.GetChecker().GetMergedSymbol(reexportedSymbol.Parent)
|
||||
if parent != nil && parent.IsExternalModule() {
|
||||
if targetModuleID, ok := e.getModuleIDForSymbol(parent); ok {
|
||||
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
|
||||
}
|
||||
|
||||
*exports = append(*exports, export)
|
||||
|
||||
if target != nil {
|
||||
if syntax == ExportSyntaxEquals && target.Flags&ast.SymbolFlagsNamespace != 0 {
|
||||
*exports = slices.Grow(*exports, len(target.Exports))
|
||||
for innerName, namedExport := range target.Exports {
|
||||
if innerName != ast.InternalSymbolNameExportStar {
|
||||
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].AsBinaryExpression().Right
|
||||
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(),
|
||||
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")
|
||||
}
|
||||
|
||||
parent := targetSymbol.Parent
|
||||
if checker := checkerLease.TryChecker(); checker != nil {
|
||||
export.Flags = checker.GetSymbolFlags(targetSymbol)
|
||||
export.IsTypeOnly = checker.GetTypeOnlyAliasDeclaration(symbol) != nil
|
||||
parent = checker.GetMergedSymbol(parent)
|
||||
} 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)
|
||||
targetModuleID := ModuleID(ast.GetSourceFileOfNode(decl).Path())
|
||||
if parent != nil && parent.IsExternalModule() {
|
||||
if id, ok := e.getModuleIDForSymbol(parent); ok {
|
||||
targetModuleID = id
|
||||
}
|
||||
}
|
||||
export.Target = ExportID{
|
||||
ExportName: targetSymbol.Name,
|
||||
ModuleID: targetModuleID,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
export.ScriptElementKind = lsutil.GetSymbolKind(checkerLease.TryChecker(), symbol, symbol.Declarations[0])
|
||||
export.ScriptElementKindModifiers = lsutil.GetSymbolModifiers(checkerLease.TryChecker(), symbol)
|
||||
}
|
||||
|
||||
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 targetSymbol != nil {
|
||||
namedSymbol = targetSymbol
|
||||
if s := binder.GetLocalSymbolForExportDefault(targetSymbol); s != nil {
|
||||
namedSymbol = s
|
||||
}
|
||||
export.localName = getDefaultLikeExportNameFromDeclaration(namedSymbol)
|
||||
}
|
||||
}
|
||||
if isUnusableName(export.localName) {
|
||||
// Last resort: derive identifier from the file name. Use FileName() (original
|
||||
// casing) rather than ModuleID/Path() which is lowercased on case-insensitive
|
||||
// file systems, losing PascalCase.
|
||||
export.localName = lsutil.ModuleSpecifierToValidIdentifier(fileNameForDefaultExportName(targetSymbol, moduleFileName, moduleID), false)
|
||||
}
|
||||
}
|
||||
|
||||
if isUnusableName(export.Name()) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
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.KindBinaryExpression:
|
||||
switch ast.GetAssignmentDeclarationKind(decl) {
|
||||
case ast.JSDeclarationKindModuleExports:
|
||||
return ExportSyntaxCommonJSModuleExports
|
||||
case ast.JSDeclarationKindExportsProperty:
|
||||
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
|
||||
}
|
||||
|
||||
// fileNameForDefaultExportName returns the best file name to use when deriving
|
||||
// a fallback identifier for a default-like export. It prefers the target symbol's
|
||||
// source file (closest to the export origin), falls back to the module's original
|
||||
// file name, and uses the lowercased moduleID only for ambient modules where no
|
||||
// original file name is available.
|
||||
func fileNameForDefaultExportName(targetSymbol *ast.Symbol, moduleFileName string, moduleID ModuleID) string {
|
||||
if targetSymbol != nil && len(targetSymbol.Declarations) > 0 {
|
||||
if fn := ast.GetSourceFileOfNode(targetSymbol.Declarations[0]).FileName(); fn != "" {
|
||||
return fn
|
||||
}
|
||||
}
|
||||
if moduleFileName != "" {
|
||||
return moduleFileName
|
||||
}
|
||||
return string(moduleID)
|
||||
}
|
||||
1314
tools/tsgo/internal/ls/autoimport/fix.go
Normal file
1314
tools/tsgo/internal/ls/autoimport/fix.go
Normal file
File diff suppressed because it is too large
Load Diff
501
tools/tsgo/internal/ls/autoimport/import_adder.go
Normal file
501
tools/tsgo/internal/ls/autoimport/import_adder.go
Normal file
@@ -0,0 +1,501 @@
|
||||
package autoimport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/checker"
|
||||
"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/locale"
|
||||
|
||||
// "github.com/microsoft/typescript-go/internal/ls"
|
||||
"github.com/microsoft/typescript-go/internal/ls/change"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsconv"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsutil"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/nodebuilder"
|
||||
)
|
||||
|
||||
type ImportAdder interface {
|
||||
HasFixes() bool
|
||||
AddImportFromExportedSymbol(symbol *ast.Symbol, isValidTypeOnlyUseSite bool)
|
||||
AddImportFix(fix *Fix)
|
||||
Edits() []*lsproto.TextEdit
|
||||
}
|
||||
|
||||
// addToExistingState tracks modifications to an existing import clause or binding pattern
|
||||
type addToExistingState struct {
|
||||
importClauseOrBindingPattern *ast.ImportClauseOrBindingPattern
|
||||
defaultImport *newImportBinding
|
||||
namedImports map[string]*newImportBinding
|
||||
}
|
||||
|
||||
// importsCollection tracks new imports to be created for a given module specifier
|
||||
type importsCollection struct {
|
||||
defaultImport *newImportBinding
|
||||
namedImports map[string]*newImportBinding
|
||||
namespaceLikeImport *newImportBinding
|
||||
useRequire bool
|
||||
}
|
||||
|
||||
func newImportsKey(moduleSpecifier string, topLevelTypeOnly bool) string {
|
||||
if topLevelTypeOnly {
|
||||
return "1|" + moduleSpecifier
|
||||
}
|
||||
return "0|" + moduleSpecifier
|
||||
}
|
||||
|
||||
type importAdder struct {
|
||||
// Context
|
||||
ctx context.Context
|
||||
checker *checker.Checker
|
||||
view *View
|
||||
formatOptions lsutil.FormatCodeSettings
|
||||
converters *lsconv.Converters
|
||||
preferences lsutil.UserPreferences
|
||||
|
||||
// State
|
||||
addToNamespace []*Fix // Namespace fixes don't conflict, so just build a list
|
||||
importType []*Fix // JSDoc type import fixes
|
||||
addToExisting map[*ast.ImportClauseOrBindingPattern]*addToExistingState // importClauseOrBindingPattern -> default or named bindings
|
||||
newImports map[string]*importsCollection // module specifier + type only -> imports
|
||||
// !!! removeExisting, verbatimImports?
|
||||
}
|
||||
|
||||
func NewImportAdder(
|
||||
ctx context.Context,
|
||||
program *compiler.Program,
|
||||
checker *checker.Checker,
|
||||
file *ast.SourceFile,
|
||||
view *View,
|
||||
formatOptions lsutil.FormatCodeSettings,
|
||||
converters *lsconv.Converters,
|
||||
preferences lsutil.UserPreferences,
|
||||
) ImportAdder {
|
||||
return &importAdder{
|
||||
ctx: ctx,
|
||||
checker: checker,
|
||||
view: view,
|
||||
formatOptions: formatOptions,
|
||||
converters: converters,
|
||||
preferences: preferences,
|
||||
addToNamespace: nil,
|
||||
importType: nil,
|
||||
addToExisting: make(map[*ast.Node]*addToExistingState),
|
||||
newImports: make(map[string]*importsCollection),
|
||||
}
|
||||
}
|
||||
|
||||
func (adder *importAdder) HasFixes() bool {
|
||||
return len(adder.addToNamespace) > 0 ||
|
||||
len(adder.importType) > 0 ||
|
||||
len(adder.addToExisting) > 0 ||
|
||||
len(adder.newImports) > 0
|
||||
}
|
||||
|
||||
// !!! referenceImport
|
||||
func (adder *importAdder) AddImportFromExportedSymbol(exportedSymbol *ast.Symbol, isValidTypeOnlyUseSite bool) {
|
||||
symbol := adder.checker.GetMergedSymbol(adder.checker.SkipAlias(exportedSymbol))
|
||||
exportInfos := adder.getAllExportsForSymbol(symbol)
|
||||
if len(exportInfos) == 0 {
|
||||
// If no exportInfo is found, this means export could not be resolved when we have filtered for autoImportFileExcludePatterns,
|
||||
// so we should not generate an import.
|
||||
// debug.Assert(len(adder.ls.UserPreferences().AutoImportFileExcludePatterns) > 0)
|
||||
return
|
||||
}
|
||||
fix := adder.getImportFixForSymbol(adder.view, adder.view.importingFile, exportInfos, isValidTypeOnlyUseSite)
|
||||
if fix != nil {
|
||||
// !!! referenceImport -> propertyName
|
||||
adder.AddImportFix(fix)
|
||||
}
|
||||
}
|
||||
|
||||
func (adder *importAdder) Edits() []*lsproto.TextEdit {
|
||||
// !!! organize imports?
|
||||
tracker := change.NewTracker(adder.ctx, adder.view.program.Options(), adder.formatOptions, adder.converters)
|
||||
quotePreference := lsutil.GetQuotePreference(adder.view.importingFile, adder.preferences)
|
||||
for _, fix := range adder.addToNamespace {
|
||||
addNamespaceQualifier(fix, tracker, adder.view.importingFile, locale.Default)
|
||||
}
|
||||
for _, fix := range adder.importType {
|
||||
addImportType(fix, adder.view.importingFile, adder.preferences, tracker, locale.Default)
|
||||
}
|
||||
for clauseOrPattern, entry := range adder.addToExisting {
|
||||
addToExistingImport(
|
||||
tracker,
|
||||
adder.view.importingFile,
|
||||
clauseOrPattern,
|
||||
entry.defaultImport,
|
||||
sortedNamedImports(entry.namedImports),
|
||||
adder.preferences,
|
||||
)
|
||||
}
|
||||
|
||||
var newDeclarations []*ast.AnyImportOrRequireStatement
|
||||
for key, newImport := range adder.newImports {
|
||||
moduleSpecifier := key[2:] // From `${0 | 1}|${moduleSpecifier}` format
|
||||
var declarations []*ast.AnyImportOrRequireStatement
|
||||
if newImport.useRequire {
|
||||
declarations = getNewRequires(
|
||||
tracker,
|
||||
moduleSpecifier,
|
||||
quotePreference,
|
||||
newImport.defaultImport,
|
||||
sortedNamedImports(newImport.namedImports),
|
||||
newImport.namespaceLikeImport,
|
||||
adder.view.program.Options(),
|
||||
)
|
||||
} else {
|
||||
declarations = getNewImports(
|
||||
tracker,
|
||||
moduleSpecifier,
|
||||
quotePreference,
|
||||
newImport.defaultImport,
|
||||
sortedNamedImports(newImport.namedImports),
|
||||
newImport.namespaceLikeImport,
|
||||
adder.view.program.Options(),
|
||||
adder.preferences,
|
||||
)
|
||||
}
|
||||
newDeclarations = append(newDeclarations, declarations...)
|
||||
}
|
||||
|
||||
if len(newDeclarations) > 0 {
|
||||
insertImports(tracker, adder.view.importingFile, newDeclarations, true /*blankLineBetween*/, adder.preferences)
|
||||
}
|
||||
|
||||
return tracker.GetChanges()[adder.view.importingFile.FileName()]
|
||||
}
|
||||
|
||||
func sortedNamedImports(m map[string]*newImportBinding) []*newImportBinding {
|
||||
keys := slices.Sorted(maps.Keys(m))
|
||||
result := make([]*newImportBinding, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
result = append(result, m[k])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// AddImportFix adds a fix to the import adder, accumulating it with other fixes
|
||||
// so that multiple imports from the same module are coalesced into a single import statement.
|
||||
func (adder *importAdder) AddImportFix(fix *Fix) {
|
||||
symbolName := fix.Name
|
||||
compilerOptions := adder.view.program.Options()
|
||||
|
||||
switch fix.Kind {
|
||||
case lsproto.AutoImportFixKindUseNamespace:
|
||||
adder.addToNamespace = append(adder.addToNamespace, fix)
|
||||
case lsproto.AutoImportFixKindJsdocTypeImport:
|
||||
adder.importType = append(adder.importType, fix)
|
||||
case lsproto.AutoImportFixKindAddToExisting:
|
||||
existingFix := getAddToExistingImportFix(adder.view.importingFile, fix)
|
||||
entry := adder.addToExisting[existingFix.importClauseOrBindingPattern]
|
||||
if entry == nil {
|
||||
entry = &addToExistingState{
|
||||
importClauseOrBindingPattern: existingFix.importClauseOrBindingPattern,
|
||||
namedImports: make(map[string]*newImportBinding),
|
||||
}
|
||||
adder.addToExisting[existingFix.importClauseOrBindingPattern] = entry
|
||||
}
|
||||
|
||||
if fix.ImportKind == lsproto.ImportKindNamed {
|
||||
prevImport := entry.namedImports[symbolName]
|
||||
var prevTypeOnly lsproto.AddAsTypeOnly
|
||||
if prevImport != nil {
|
||||
prevTypeOnly = prevImport.addAsTypeOnly
|
||||
}
|
||||
entry.namedImports[symbolName] = &newImportBinding{
|
||||
kind: lsproto.ImportKindNamed,
|
||||
name: symbolName,
|
||||
addAsTypeOnly: reduceAddAsTypeOnlyValues(prevTypeOnly, fix.AddAsTypeOnly),
|
||||
propertyName: existingFix.namedImport.propertyName,
|
||||
}
|
||||
} else {
|
||||
// Default import
|
||||
debug.Assert(
|
||||
entry.defaultImport == nil || entry.defaultImport.name == symbolName,
|
||||
"(Add to Existing) Default import should be missing or match symbolName",
|
||||
)
|
||||
var prevTypeOnly lsproto.AddAsTypeOnly
|
||||
if entry.defaultImport != nil {
|
||||
prevTypeOnly = entry.defaultImport.addAsTypeOnly
|
||||
}
|
||||
entry.defaultImport = &newImportBinding{
|
||||
kind: lsproto.ImportKindDefault,
|
||||
name: symbolName,
|
||||
addAsTypeOnly: reduceAddAsTypeOnlyValues(prevTypeOnly, fix.AddAsTypeOnly),
|
||||
}
|
||||
}
|
||||
|
||||
case lsproto.AutoImportFixKindAddNew:
|
||||
entry := adder.getNewImportEntry(fix.ModuleSpecifier, fix.ImportKind, fix.UseRequire, fix.AddAsTypeOnly)
|
||||
debug.Assert(
|
||||
entry.useRequire == fix.UseRequire,
|
||||
"(Add new) Tried to add an `import` and a `require` for the same module",
|
||||
)
|
||||
|
||||
switch fix.ImportKind {
|
||||
case lsproto.ImportKindDefault:
|
||||
debug.Assert(
|
||||
entry.defaultImport == nil || entry.defaultImport.name == symbolName,
|
||||
"(Add new) Default import should be missing or match symbolName",
|
||||
)
|
||||
var prevTypeOnly lsproto.AddAsTypeOnly
|
||||
if entry.defaultImport != nil {
|
||||
prevTypeOnly = entry.defaultImport.addAsTypeOnly
|
||||
}
|
||||
entry.defaultImport = &newImportBinding{
|
||||
kind: lsproto.ImportKindDefault,
|
||||
name: symbolName,
|
||||
addAsTypeOnly: reduceAddAsTypeOnlyValues(prevTypeOnly, fix.AddAsTypeOnly),
|
||||
}
|
||||
|
||||
case lsproto.ImportKindNamed:
|
||||
if entry.namedImports == nil {
|
||||
entry.namedImports = make(map[string]*newImportBinding)
|
||||
}
|
||||
prevImport := entry.namedImports[symbolName]
|
||||
var prevTypeOnly lsproto.AddAsTypeOnly
|
||||
if prevImport != nil {
|
||||
prevTypeOnly = prevImport.addAsTypeOnly
|
||||
}
|
||||
entry.namedImports[symbolName] = &newImportBinding{
|
||||
kind: lsproto.ImportKindNamed,
|
||||
name: symbolName,
|
||||
addAsTypeOnly: reduceAddAsTypeOnlyValues(prevTypeOnly, fix.AddAsTypeOnly),
|
||||
// !!! propertyName
|
||||
}
|
||||
|
||||
case lsproto.ImportKindCommonJS:
|
||||
if compilerOptions.VerbatimModuleSyntax == core.TSTrue {
|
||||
if entry.namedImports == nil {
|
||||
entry.namedImports = make(map[string]*newImportBinding)
|
||||
}
|
||||
prevImport := entry.namedImports[symbolName]
|
||||
var prevTypeOnly lsproto.AddAsTypeOnly
|
||||
if prevImport != nil {
|
||||
prevTypeOnly = prevImport.addAsTypeOnly
|
||||
}
|
||||
entry.namedImports[symbolName] = &newImportBinding{
|
||||
kind: lsproto.ImportKindCommonJS,
|
||||
name: symbolName,
|
||||
addAsTypeOnly: reduceAddAsTypeOnlyValues(prevTypeOnly, fix.AddAsTypeOnly),
|
||||
// !!! propertyName
|
||||
}
|
||||
} else {
|
||||
debug.Assert(
|
||||
entry.namespaceLikeImport == nil || entry.namespaceLikeImport.name == symbolName,
|
||||
"Namespacelike import should be missing or match symbolName",
|
||||
)
|
||||
entry.namespaceLikeImport = &newImportBinding{
|
||||
kind: lsproto.ImportKindCommonJS,
|
||||
name: symbolName,
|
||||
addAsTypeOnly: fix.AddAsTypeOnly,
|
||||
}
|
||||
}
|
||||
|
||||
case lsproto.ImportKindNamespace:
|
||||
debug.Assert(
|
||||
entry.namespaceLikeImport == nil || entry.namespaceLikeImport.name == symbolName,
|
||||
"Namespacelike import should be missing or match symbolName",
|
||||
)
|
||||
entry.namespaceLikeImport = &newImportBinding{
|
||||
kind: lsproto.ImportKindNamespace,
|
||||
name: symbolName,
|
||||
addAsTypeOnly: fix.AddAsTypeOnly,
|
||||
}
|
||||
}
|
||||
|
||||
case lsproto.AutoImportFixKindPromoteTypeOnly:
|
||||
// Excluding from fix-all
|
||||
default:
|
||||
debug.Fail(fmt.Sprintf("Unexpected fix kind: %v", fix.Kind))
|
||||
}
|
||||
}
|
||||
|
||||
// `NotAllowed` overrides `Required` because one addition of a new import might be required to be type-only
|
||||
// because of `--importsNotUsedAsValues=error`, but if a second addition of the same import is `NotAllowed`
|
||||
// to be type-only, the reason the first one was `Required` - the unused runtime dependency - is now moot.
|
||||
// Alternatively, if one addition is `Required` because it has no value meaning under `--preserveValueImports`
|
||||
// and `--isolatedModules`, it should be impossible for another addition to be `NotAllowed` since that would
|
||||
// mean a type is being referenced in a value location.
|
||||
func reduceAddAsTypeOnlyValues(prevValue, newValue lsproto.AddAsTypeOnly) lsproto.AddAsTypeOnly {
|
||||
if newValue > prevValue {
|
||||
return newValue
|
||||
}
|
||||
return prevValue
|
||||
}
|
||||
|
||||
func (adder *importAdder) getNewImportEntry(moduleSpecifier string, importKind lsproto.ImportKind, useRequire bool, addAsTypeOnly lsproto.AddAsTypeOnly) *importsCollection {
|
||||
// A default import that requires type-only makes the whole import type-only.
|
||||
// (We could add `default` as a named import, but that style seems undesirable.)
|
||||
// Under `--preserveValueImports` and `--importsNotUsedAsValues=error`, if a
|
||||
// module default-exports a type but named-exports some values (weird), you would
|
||||
// have to use a type-only default import and non-type-only named imports. These
|
||||
// require two separate import declarations, so we build this into the map key.
|
||||
typeOnlyKey := newImportsKey(moduleSpecifier, true /*topLevelTypeOnly*/)
|
||||
nonTypeOnlyKey := newImportsKey(moduleSpecifier, false /*topLevelTypeOnly*/)
|
||||
typeOnlyEntry := adder.newImports[typeOnlyKey]
|
||||
nonTypeOnlyEntry := adder.newImports[nonTypeOnlyKey]
|
||||
newEntry := &importsCollection{
|
||||
useRequire: useRequire,
|
||||
}
|
||||
|
||||
if importKind == lsproto.ImportKindDefault && addAsTypeOnly == lsproto.AddAsTypeOnlyRequired {
|
||||
if typeOnlyEntry != nil {
|
||||
return typeOnlyEntry
|
||||
}
|
||||
adder.newImports[typeOnlyKey] = newEntry
|
||||
return newEntry
|
||||
}
|
||||
|
||||
if addAsTypeOnly == lsproto.AddAsTypeOnlyAllowed && (typeOnlyEntry != nil || nonTypeOnlyEntry != nil) {
|
||||
if typeOnlyEntry != nil {
|
||||
return typeOnlyEntry
|
||||
}
|
||||
return nonTypeOnlyEntry
|
||||
}
|
||||
|
||||
if nonTypeOnlyEntry != nil {
|
||||
return nonTypeOnlyEntry
|
||||
}
|
||||
|
||||
adder.newImports[nonTypeOnlyKey] = newEntry
|
||||
return newEntry
|
||||
}
|
||||
|
||||
func (adder *importAdder) getAllExportsForSymbol(
|
||||
symbol *ast.Symbol,
|
||||
) []*Export {
|
||||
if export := SymbolToExport(symbol, adder.checker); export != nil {
|
||||
return adder.view.SearchByExportID(export.ExportID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TypeToAutoImportableTypeNode(
|
||||
c *checker.Checker,
|
||||
importAdder ImportAdder,
|
||||
t *checker.Type,
|
||||
contextNode *ast.Node, // !!! flags
|
||||
) *ast.TypeNode {
|
||||
idToSymbol := make(map[*ast.IdentifierNode]*ast.Symbol)
|
||||
typeNode := c.TypeToTypeNode(t, contextNode, nodebuilder.FlagsNone, idToSymbol)
|
||||
if typeNode == nil {
|
||||
return nil
|
||||
}
|
||||
return TypeNodeToAutoImportableTypeNode(typeNode, importAdder, idToSymbol)
|
||||
}
|
||||
|
||||
// TypeNodeToAutoImportableTypeNode converts import type references in a type node to
|
||||
// simple type references and registers needed imports with the import adder.
|
||||
func TypeNodeToAutoImportableTypeNode(
|
||||
typeNode *ast.TypeNode,
|
||||
importAdder ImportAdder,
|
||||
idToSymbol map[*ast.IdentifierNode]*ast.Symbol,
|
||||
) *ast.TypeNode {
|
||||
referenceTypeNode, importableSymbols := TryGetAutoImportableReferenceFromTypeNode(typeNode, idToSymbol)
|
||||
if referenceTypeNode != nil {
|
||||
if importAdder != nil {
|
||||
importSymbols(importAdder, importableSymbols)
|
||||
}
|
||||
typeNode = referenceTypeNode
|
||||
}
|
||||
|
||||
// !!! handle type node reuse: nodes needs to be fresh here but also preserve symbols
|
||||
return typeNode
|
||||
}
|
||||
|
||||
func importSymbols(importAdder ImportAdder, symbols []*ast.Symbol) {
|
||||
for _, symbol := range symbols {
|
||||
importAdder.AddImportFromExportedSymbol(symbol, true /*isValidTypeOnlyUseSite*/)
|
||||
}
|
||||
}
|
||||
|
||||
// Given a type node containing 'import("./a").SomeType<import("./b").OtherType<...>>',
|
||||
// returns an equivalent type reference node with any nested ImportTypeNodes also replaced
|
||||
// with type references, and a list of symbols that must be imported to use the type reference.
|
||||
// TryGetAutoImportableReferenceFromTypeNode converts import type references in a type node
|
||||
// to simple type references and returns the transformed type node and the symbols that need
|
||||
// to be imported.
|
||||
func TryGetAutoImportableReferenceFromTypeNode(importTypeNode *ast.TypeNode, idToSymbol map[*ast.IdentifierNode]*ast.Symbol) (*ast.TypeNode, []*ast.Symbol) {
|
||||
var symbols []*ast.Symbol
|
||||
var visitor *ast.NodeVisitor
|
||||
factory := ast.NewNodeFactory(ast.NodeFactoryHooks{})
|
||||
visit := func(node *ast.Node) *ast.Node {
|
||||
if ast.IsLiteralImportTypeNode(node) && node.AsImportTypeNode().Qualifier != nil {
|
||||
importTypeNode := node.AsImportTypeNode()
|
||||
// Symbol for the left-most thing after the dot
|
||||
firstIdentifier := ast.GetFirstIdentifier(importTypeNode.Qualifier)
|
||||
symbol := idToSymbol[firstIdentifier]
|
||||
if symbol == nil {
|
||||
// if symbol is missing then this doesn't come from a synthesized import type node
|
||||
// it has to be an import type node authored by the user and thus it has to be valid
|
||||
// it can't refer to reserved internal symbol names and such
|
||||
return node.VisitEachChild(visitor)
|
||||
}
|
||||
name := getNameForExportedSymbol(symbol, false /*preferCapitalized*/)
|
||||
var qualifier *ast.EntityName
|
||||
if name != firstIdentifier.Text() {
|
||||
qualifier = replaceFirstIdentifierOfEntityName(factory, importTypeNode.Qualifier, factory.NewIdentifier(name))
|
||||
} else {
|
||||
qualifier = importTypeNode.Qualifier
|
||||
}
|
||||
symbols = append(symbols, symbol)
|
||||
typeArguments := visitor.VisitNodes(importTypeNode.TypeArguments)
|
||||
return factory.NewTypeReferenceNode(qualifier, typeArguments)
|
||||
}
|
||||
return visitor.VisitEachChild(node)
|
||||
}
|
||||
visitor = ast.NewNodeVisitor(visit, factory, ast.NodeVisitorHooks{})
|
||||
|
||||
typeNode := visitor.VisitNode(importTypeNode)
|
||||
debug.Assert(typeNode == nil || ast.IsTypeNode(typeNode), "expected a type node")
|
||||
return typeNode, symbols
|
||||
}
|
||||
|
||||
// If a type checker and multiple files are available, consider using `forEachNameOfDefaultExport`
|
||||
// instead, which searches for names of re-exported defaults/namespaces in target files.
|
||||
func getNameForExportedSymbol(symbol *ast.Symbol, preferCapitalized bool) string {
|
||||
if symbol.Name == ast.InternalSymbolNameExportEquals || symbol.Name == ast.InternalSymbolNameDefault {
|
||||
// Names for default exports:
|
||||
// - export default foo => foo
|
||||
// - export { foo as default } => foo
|
||||
// - export default 0 => filename converted to camelCase
|
||||
name := getDefaultLikeExportNameFromDeclaration(symbol)
|
||||
if name != "" {
|
||||
return name
|
||||
}
|
||||
debug.Assert(symbol.Parent != nil, "Expected exported symbol to have module symbol as parent")
|
||||
return lsutil.ModuleSymbolToValidIdentifier(symbol.Parent, preferCapitalized)
|
||||
}
|
||||
return symbol.Name
|
||||
}
|
||||
|
||||
func replaceFirstIdentifierOfEntityName(factory *ast.NodeFactory, name *ast.EntityName, newIdentifier *ast.IdentifierNode) *ast.EntityName {
|
||||
if name.Kind == ast.KindIdentifier {
|
||||
return newIdentifier
|
||||
}
|
||||
return factory.NewQualifiedName(
|
||||
replaceFirstIdentifierOfEntityName(factory, name.AsQualifiedName().Left, newIdentifier),
|
||||
name.AsQualifiedName().Right,
|
||||
)
|
||||
}
|
||||
|
||||
func (adder *importAdder) getImportFixForSymbol(view *View, file *ast.SourceFile, exports []*Export, isValidTypeOnlyUseSite bool) *Fix {
|
||||
fixes := core.FlatMap(exports, func(export *Export) []*Fix {
|
||||
return view.GetFixes(adder.ctx, export, false /*forJSX*/, isValidTypeOnlyUseSite, nil /*usagePosition*/)
|
||||
})
|
||||
slices.SortFunc(fixes, func(a, b *Fix) int {
|
||||
return view.CompareFixesForRanking(a, b)
|
||||
})
|
||||
if len(fixes) > 0 {
|
||||
return fixes[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
186
tools/tsgo/internal/ls/autoimport/index.go
Normal file
186
tools/tsgo/internal/ls/autoimport/index.go
Normal file
@@ -0,0 +1,186 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clone creates a new Index containing only entries for which filter returns true.
|
||||
func (idx *Index[T]) Clone(filter func(T) bool) *Index[T] {
|
||||
if idx == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
newIdx := &Index[T]{
|
||||
entries: make([]T, 0, len(idx.entries)),
|
||||
index: make(map[rune][]int, len(idx.index)),
|
||||
}
|
||||
|
||||
// Build mapping from old index to new index for filtered entries
|
||||
oldToNew := make(map[int]int, len(idx.entries))
|
||||
for oldIndex, entry := range idx.entries {
|
||||
if filter(entry) {
|
||||
newIndex := len(newIdx.entries)
|
||||
newIdx.entries = append(newIdx.entries, entry)
|
||||
oldToNew[oldIndex] = newIndex
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild the index with remapped indices
|
||||
for r, oldIndices := range idx.index {
|
||||
newIndices := make([]int, 0, len(oldIndices))
|
||||
for _, oldIndex := range oldIndices {
|
||||
if newIndex, ok := oldToNew[oldIndex]; ok {
|
||||
newIndices = append(newIndices, newIndex)
|
||||
}
|
||||
}
|
||||
if len(newIndices) > 0 {
|
||||
newIdx.index[r] = newIndices
|
||||
}
|
||||
}
|
||||
|
||||
return newIdx
|
||||
}
|
||||
79
tools/tsgo/internal/ls/autoimport/index_test.go
Normal file
79
tools/tsgo/internal/ls/autoimport/index_test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package autoimport
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
type testEntry struct {
|
||||
name string
|
||||
package_ string
|
||||
}
|
||||
|
||||
func (e *testEntry) Name() string { return e.name }
|
||||
|
||||
func TestIndexClone(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("filters entries by package", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
idx := &Index[*testEntry]{}
|
||||
idx.insertAsWords(&testEntry{name: "fooBar", package_: "pkg-a"})
|
||||
idx.insertAsWords(&testEntry{name: "bazQux", package_: "pkg-b"})
|
||||
idx.insertAsWords(&testEntry{name: "fooQux", package_: "pkg-a"})
|
||||
|
||||
// Clone excluding pkg-b
|
||||
cloned := idx.Clone(func(e *testEntry) bool {
|
||||
return e.package_ != "pkg-b"
|
||||
})
|
||||
|
||||
// Original should have all 3 entries
|
||||
assert.Equal(t, len(idx.entries), 3)
|
||||
|
||||
// Cloned should have 2 entries (only pkg-a)
|
||||
assert.Equal(t, len(cloned.entries), 2)
|
||||
|
||||
// Search should work on cloned index
|
||||
results := cloned.Find("fooBar", true)
|
||||
assert.Equal(t, len(results), 1)
|
||||
assert.Equal(t, results[0].name, "fooBar")
|
||||
|
||||
// bazQux should not be in cloned index
|
||||
results = cloned.Find("bazQux", true)
|
||||
assert.Equal(t, len(results), 0)
|
||||
|
||||
// Word prefix search should work
|
||||
results = cloned.SearchWordPrefix("foo")
|
||||
assert.Equal(t, len(results), 2)
|
||||
})
|
||||
|
||||
t.Run("handles nil index", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var idx *Index[*testEntry]
|
||||
cloned := idx.Clone(func(e *testEntry) bool { return true })
|
||||
assert.Assert(t, cloned == nil)
|
||||
})
|
||||
|
||||
t.Run("handles empty index", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
idx := &Index[*testEntry]{}
|
||||
cloned := idx.Clone(func(e *testEntry) bool { return true })
|
||||
assert.Equal(t, len(cloned.entries), 0)
|
||||
})
|
||||
|
||||
t.Run("filters all entries", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
idx := &Index[*testEntry]{}
|
||||
idx.insertAsWords(&testEntry{name: "fooBar", package_: "pkg-a"})
|
||||
idx.insertAsWords(&testEntry{name: "bazQux", package_: "pkg-b"})
|
||||
|
||||
cloned := idx.Clone(func(e *testEntry) bool { return false })
|
||||
assert.Equal(t, len(cloned.entries), 0)
|
||||
assert.Equal(t, len(cloned.index), 0)
|
||||
})
|
||||
}
|
||||
1826
tools/tsgo/internal/ls/autoimport/registry.go
Normal file
1826
tools/tsgo/internal/ls/autoimport/registry.go
Normal file
File diff suppressed because it is too large
Load Diff
1183
tools/tsgo/internal/ls/autoimport/registry_test.go
Normal file
1183
tools/tsgo/internal/ls/autoimport/registry_test.go
Normal file
File diff suppressed because it is too large
Load Diff
75
tools/tsgo/internal/ls/autoimport/specifiers.go
Normal file
75
tools/tsgo/internal/ls/autoimport/specifiers.go
Normal file
@@ -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.PackageName != "" {
|
||||
if entrypoints, ok := v.registry.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.PackageName == "" {
|
||||
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
|
||||
}
|
||||
14
tools/tsgo/internal/ls/autoimport/testmain_test.go
Normal file
14
tools/tsgo/internal/ls/autoimport/testmain_test.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package autoimport_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/testutil/baseline"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
core.ApplyDebugStackLimit()
|
||||
defer baseline.Track()()
|
||||
m.Run()
|
||||
}
|
||||
323
tools/tsgo/internal/ls/autoimport/util.go
Normal file
323
tools/tsgo/internal/ls/autoimport/util.go
Normal file
@@ -0,0 +1,323 @@
|
||||
package autoimport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"strings"
|
||||
"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/packagejson"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
"github.com/microsoft/typescript-go/internal/vfs/wrapvfs"
|
||||
)
|
||||
|
||||
func tryGetModuleIDAndFileNameOfModuleSymbol(symbol *ast.Symbol) (ModuleID, string, bool) {
|
||||
if !symbol.IsExternalModule() {
|
||||
return "", "", false
|
||||
}
|
||||
decl := ast.GetNonAugmentationDeclaration(symbol)
|
||||
if decl == nil {
|
||||
return "", "", false
|
||||
}
|
||||
if decl.Kind == ast.KindSourceFile {
|
||||
return ModuleID(decl.AsSourceFile().Path()), decl.AsSourceFile().FileName(), true
|
||||
}
|
||||
if ast.IsModuleWithStringLiteralName(decl) {
|
||||
return ModuleID(decl.Name().Text()), "", true
|
||||
}
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
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] {
|
||||
packageNames := &collections.Set[string]{}
|
||||
if tspath.GetBaseFileName(nodeModulesDir) != "node_modules" {
|
||||
panic("nodeModulesDir is not a node_modules directory")
|
||||
}
|
||||
// A missing node_modules directory yields no entries (GetAccessibleEntries returns
|
||||
// empty), so there's no need to check existence first: a deleted node_modules is
|
||||
// handled upstream in updateBucketAndDirectoryExistence, which drops the bucket.
|
||||
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
|
||||
}
|
||||
|
||||
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] {
|
||||
rawNames := program.ResolvedPackageNames()
|
||||
unresolvedPackageNames := program.UnresolvedPackageNames()
|
||||
|
||||
// Normalize @types/ package names to their actual package names
|
||||
// (e.g., "@types/react" → "react"). ResolvedPackageNames can contain
|
||||
// @types names when the program resolves an import like "react" to
|
||||
// "@types/react/index.d.ts" via the PackageId.Name field.
|
||||
resolvedPackageNames := collections.NewSetWithSizeHint[string](rawNames.Len())
|
||||
for name := range rawNames.Keys() {
|
||||
resolvedPackageNames.Add(module.GetPackageNameFromTypesPackageName(name))
|
||||
}
|
||||
|
||||
for _, name := range program.Options().Types {
|
||||
if name != "*" {
|
||||
resolvedPackageNames.Add(module.GetPackageNameFromTypesPackageName(name))
|
||||
}
|
||||
}
|
||||
|
||||
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(module.GetPackageNameFromTypesPackageName(packageName))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return resolvedPackageNames
|
||||
}
|
||||
|
||||
// addProjectReferenceOutputMappings adds output .d.ts to source file mappings
|
||||
// from a program's project references to the provided map.
|
||||
// This is used during node_modules bucket building to redirect extraction
|
||||
// from output files to source files when the output is from a project reference.
|
||||
func addProjectReferenceOutputMappings(program *compiler.Program, result map[tspath.Path]string) {
|
||||
refs := program.GetResolvedProjectReferences()
|
||||
for _, ref := range refs {
|
||||
if ref == nil {
|
||||
continue
|
||||
}
|
||||
ref.ParseInputOutputNames()
|
||||
for outputDtsPath, mapping := range ref.OutputDtsToProjectReference() {
|
||||
// Only add if not already present (first program wins)
|
||||
if _, exists := result[outputDtsPath]; !exists {
|
||||
result[outputDtsPath] = mapping.Source
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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, nil))
|
||||
return ch, func() { pool <- ch }
|
||||
}
|
||||
}
|
||||
}, func() {
|
||||
close(pool)
|
||||
}, func() int32 {
|
||||
return created.Load()
|
||||
}
|
||||
}
|
||||
|
||||
// addPackageJsonDependencies adds all dependencies and peerDependencies from a package.json
|
||||
// to the given set, canonicalizing @types package names to their base names.
|
||||
func addPackageJsonDependencies(contents *packagejson.PackageJson, deps *collections.Set[string]) {
|
||||
contents.RangeDependencies(func(name, _, field string) bool {
|
||||
if name == "" || name == "@types/" || name[0] == '.' {
|
||||
// Edge cases that could make us blow up probably
|
||||
return true
|
||||
}
|
||||
if field == "dependencies" || field == "peerDependencies" {
|
||||
deps.Add(module.GetPackageNameFromTypesPackageName(name))
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// getPackageRealpathFuncs returns functions to transform between symlink and realpath for files within a package.
|
||||
// It calls FS.Realpath once per package directory and uses prefix substitution for files within that directory,
|
||||
// avoiding expensive realpath syscalls for each file. For files outside the package (e.g. re-exported
|
||||
// dependencies reached through node_modules symlinks), it resolves the file's directory realpath once,
|
||||
// finds the symlink boundary (the package root where the symlink lives), and caches that prefix mapping.
|
||||
// All subsequent files under the same symlinked package directory use prefix substitution with no syscalls.
|
||||
func getPackageRealpathFuncs(fs vfs.FS, packageDir string) (toRealpath, toSymlink func(string) string) {
|
||||
realPackageDir := fs.Realpath(packageDir)
|
||||
isSymlinked := realPackageDir != packageDir
|
||||
// Cache of package-directory-level symlink→realpath prefix mappings for
|
||||
// external packages encountered via re-exports. Keyed by the node_modules
|
||||
// package directory (e.g. "/app/node_modules/dep"), so all files under
|
||||
// that package reuse a single realpath lookup.
|
||||
dirCache := make(map[string]string)
|
||||
toRealpath = func(fileName string) string {
|
||||
// Fast path: files within the package use prefix substitution.
|
||||
if isSymlinked {
|
||||
if after, ok := strings.CutPrefix(fileName, packageDir); ok {
|
||||
return realPackageDir + after
|
||||
}
|
||||
}
|
||||
// Files outside the package (e.g. re-exports into symlinked deps):
|
||||
// find the node_modules package directory, resolve it once, and cache.
|
||||
pkgDir := module.ParseNodeModuleFromPath(fileName, false /*isFolder*/)
|
||||
if pkgDir == "" {
|
||||
return fileName
|
||||
}
|
||||
if realDir, ok := dirCache[pkgDir]; ok {
|
||||
if realDir == pkgDir {
|
||||
return fileName
|
||||
}
|
||||
return realDir + fileName[len(pkgDir):]
|
||||
}
|
||||
realDir := fs.Realpath(pkgDir)
|
||||
dirCache[pkgDir] = realDir
|
||||
if realDir == pkgDir {
|
||||
return fileName
|
||||
}
|
||||
return realDir + fileName[len(pkgDir):]
|
||||
}
|
||||
if !isSymlinked {
|
||||
return toRealpath, core.Identity
|
||||
}
|
||||
// toSymlink only handles files within the package directory (reversing the
|
||||
// packageDir→realPackageDir substitution). It does not handle arbitrary external
|
||||
// paths; callers should only use it for files known to be within the package.
|
||||
toSymlink = func(fileName string) string {
|
||||
if after, ok := strings.CutPrefix(fileName, realPackageDir); ok {
|
||||
return packageDir + after
|
||||
}
|
||||
return fileName
|
||||
}
|
||||
return toRealpath, toSymlink
|
||||
}
|
||||
|
||||
type resolutionHost struct {
|
||||
fs vfs.FS
|
||||
currentDirectory string
|
||||
}
|
||||
|
||||
var _ module.ResolutionHost = (*resolutionHost)(nil)
|
||||
|
||||
func (rh *resolutionHost) GetCurrentDirectory() string {
|
||||
return rh.currentDirectory
|
||||
}
|
||||
|
||||
func (rh *resolutionHost) FS() vfs.FS {
|
||||
return rh.fs
|
||||
}
|
||||
|
||||
func getModuleResolver(host RegistryCloneHost, realpath func(string) string, opts module.ResolverOptions) *module.Resolver {
|
||||
rh := &resolutionHost{
|
||||
fs: wrapvfs.Wrap(host.FS(), wrapvfs.Replacements{Realpath: realpath}),
|
||||
currentDirectory: host.GetCurrentDirectory(),
|
||||
}
|
||||
return module.NewResolverWithOptions(rh, core.EmptyCompilerOptions, "", "", opts)
|
||||
}
|
||||
220
tools/tsgo/internal/ls/autoimport/util_test.go
Normal file
220
tools/tsgo/internal/ls/autoimport/util_test.go
Normal file
@@ -0,0 +1,220 @@
|
||||
package autoimport
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/vfs/vfstest"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetPackageRealpathFuncs_FollowsNodeModulesSymlinks tests that toRealpath correctly
|
||||
// follows symlinks for files outside the package directory (e.g. node_modules entries).
|
||||
// Without this, the module resolver uses unresolved symlink paths as cache keys, causing
|
||||
// the same file to be loaded multiple times and triggering massive memory usage when
|
||||
// barrel files re-export from many symlinked packages (issue #2780).
|
||||
func TestGetPackageRealpathFuncs_FollowsNodeModulesSymlinks(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Simulate a layout where the package directory is itself a symlink (e.g. Bazel's
|
||||
// convenience symlinks or pnpm's virtual store):
|
||||
// /symlink-bin/pkg/ -> symlink to /real/bin/pkg/
|
||||
// /real/bin/pkg/node_modules/dep -> symlink to /real/dep/
|
||||
//
|
||||
// When toRealpath is used as the module resolver's Realpath, it must follow
|
||||
// the node_modules symlink so that /real/bin/pkg/node_modules/dep/index.d.ts
|
||||
// resolves to /real/dep/index.d.ts — otherwise the same dep file gets different
|
||||
// cache keys depending on which path it was reached through.
|
||||
fs := vfstest.FromMap(map[string]any{
|
||||
"/symlink-bin/pkg": vfstest.Symlink("/real/bin/pkg"),
|
||||
"/real/bin/pkg/index.d.ts": "export declare const a: number;",
|
||||
"/real/bin/pkg/node_modules/dep": vfstest.Symlink("/real/dep"),
|
||||
"/real/dep/index.d.ts": "export declare const b: number;",
|
||||
"/real/dep/src/utils/helper.d.ts": "export declare const c: number;",
|
||||
}, true)
|
||||
|
||||
toRealpath, _ := getPackageRealpathFuncs(fs, "/symlink-bin/pkg")
|
||||
|
||||
// Files inside the package should be converted via string replacement (fast path).
|
||||
assert.Equal(
|
||||
t,
|
||||
toRealpath("/symlink-bin/pkg/index.d.ts"),
|
||||
"/real/bin/pkg/index.d.ts",
|
||||
"package files should be converted via prefix replacement",
|
||||
)
|
||||
|
||||
// Files outside the package (e.g. node_modules symlinks) should be resolved via
|
||||
// fs.Realpath so the cache key is the canonical realpath, not the symlink path.
|
||||
assert.Equal(
|
||||
t,
|
||||
toRealpath("/real/bin/pkg/node_modules/dep/index.d.ts"),
|
||||
"/real/dep/index.d.ts",
|
||||
"node_modules symlinks must be followed so the same file gets a consistent cache key",
|
||||
)
|
||||
|
||||
// Files in subdirectories of an already-resolved external package should
|
||||
// use the cached prefix mapping without additional realpath calls.
|
||||
assert.Equal(
|
||||
t,
|
||||
toRealpath("/real/bin/pkg/node_modules/dep/src/utils/helper.d.ts"),
|
||||
"/real/dep/src/utils/helper.d.ts",
|
||||
"subdirectories of a resolved external package should use cached prefix mapping",
|
||||
)
|
||||
}
|
||||
|
||||
// TestGetPackageRealpathFuncs_DuplicateCacheKeys demonstrates how the broken toRealpath
|
||||
// causes the same physical file to get different cache keys when reached through different
|
||||
// symlink paths. In pnpm/Bazel monorepos, multiple packages may have node_modules symlinks
|
||||
// that point to the same physical dependency. Because toRealpath doesn't follow symlinks
|
||||
// for files outside the package directory, each path is treated as distinct, leading to
|
||||
// duplicate file loads and memory bloat (issue #2780).
|
||||
func TestGetPackageRealpathFuncs_DuplicateCacheKeys(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Simulate two packages (app-a, app-b) that each have a node_modules symlink to
|
||||
// the same shared dependency. This is a typical pnpm/Bazel layout:
|
||||
// /workspace/packages/app-a/ -> symlink to /store/app-a/
|
||||
// /workspace/packages/app-b/ -> symlink to /store/app-b/
|
||||
// /store/app-a/node_modules/shared-lib -> symlink to /store/shared-lib/
|
||||
// /store/app-b/node_modules/shared-lib -> symlink to /store/shared-lib/
|
||||
fs := vfstest.FromMap(map[string]any{
|
||||
"/workspace/packages/app-a": vfstest.Symlink("/store/app-a"),
|
||||
"/workspace/packages/app-b": vfstest.Symlink("/store/app-b"),
|
||||
"/store/app-a/index.d.ts": "export declare const a: number;",
|
||||
"/store/app-b/index.d.ts": "export declare const b: number;",
|
||||
"/store/app-a/node_modules/shared-lib": vfstest.Symlink("/store/shared-lib"),
|
||||
"/store/app-b/node_modules/shared-lib": vfstest.Symlink("/store/shared-lib"),
|
||||
"/store/shared-lib/index.d.ts": "export declare const shared: string;",
|
||||
}, true)
|
||||
|
||||
toRealpathA, _ := getPackageRealpathFuncs(fs, "/workspace/packages/app-a")
|
||||
toRealpathB, _ := getPackageRealpathFuncs(fs, "/workspace/packages/app-b")
|
||||
|
||||
sharedFileViaA := "/store/app-a/node_modules/shared-lib/index.d.ts"
|
||||
sharedFileViaB := "/store/app-b/node_modules/shared-lib/index.d.ts"
|
||||
|
||||
resolvedA := toRealpathA(sharedFileViaA)
|
||||
resolvedB := toRealpathB(sharedFileViaB)
|
||||
|
||||
// Both should resolve to the same canonical realpath so the module resolver
|
||||
// uses a single cache key for the shared dependency, avoiding duplicate loads.
|
||||
expectedRealpath := "/store/shared-lib/index.d.ts"
|
||||
assert.Equal(t, resolvedA, expectedRealpath,
|
||||
"app-a's toRealpath should follow the node_modules symlink to the realpath")
|
||||
assert.Equal(t, resolvedB, expectedRealpath,
|
||||
"app-b's toRealpath should follow the node_modules symlink to the realpath")
|
||||
}
|
||||
|
||||
// TestGetPackageRealpathFuncs_NonSymlinkedPackageWithSymlinkedDeps tests that even when the
|
||||
// package directory itself is NOT a symlink, toRealpath still follows symlinks for files
|
||||
// outside the package (e.g. re-exports reaching into symlinked node_modules dependencies).
|
||||
func TestGetPackageRealpathFuncs_NonSymlinkedPackageWithSymlinkedDeps(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fs := vfstest.FromMap(map[string]any{
|
||||
"/real/my-pkg/index.d.ts": "export declare const a: number;",
|
||||
"/real/my-pkg/node_modules/dep": vfstest.Symlink("/real/dep"),
|
||||
"/real/dep/index.d.ts": "export declare const b: number;",
|
||||
}, true)
|
||||
|
||||
toRealpath, _ := getPackageRealpathFuncs(fs, "/real/my-pkg")
|
||||
|
||||
// Files inside the (non-symlinked) package should be returned unchanged.
|
||||
assert.Equal(
|
||||
t,
|
||||
toRealpath("/real/my-pkg/index.d.ts"),
|
||||
"/real/my-pkg/index.d.ts",
|
||||
)
|
||||
|
||||
// Files outside the package reached via symlinked node_modules should still be resolved.
|
||||
assert.Equal(
|
||||
t,
|
||||
toRealpath("/real/my-pkg/node_modules/dep/index.d.ts"),
|
||||
"/real/dep/index.d.ts",
|
||||
"symlinked deps must be resolved even when the package dir itself is not a symlink",
|
||||
)
|
||||
}
|
||||
256
tools/tsgo/internal/ls/autoimport/view.go
Normal file
256
tools/tsgo/internal/ls/autoimport/view.go
Normal file
@@ -0,0 +1,256 @@
|
||||
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/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/module"
|
||||
"github.com/microsoft/typescript-go/internal/modulespecifiers"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
"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 {
|
||||
searchFn := 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")
|
||||
}
|
||||
}
|
||||
|
||||
return v.search(searchFn)
|
||||
}
|
||||
|
||||
func (v *View) SearchByExportID(id ExportID) []*Export {
|
||||
search := func(bucket *RegistryBucket) []*Export {
|
||||
return core.Filter(bucket.Index.entries, func(e *Export) bool {
|
||||
return e.ExportID == id
|
||||
})
|
||||
}
|
||||
|
||||
return v.search(search)
|
||||
}
|
||||
|
||||
func (v *View) search(searchFn func(*RegistryBucket) []*Export) []*Export {
|
||||
var results []*Export
|
||||
|
||||
if bucket, ok := v.registry.projects[v.projectKey]; ok {
|
||||
exports := searchFn(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)
|
||||
}
|
||||
}
|
||||
|
||||
// Compute the set of packages accessible to the importing file.
|
||||
// This includes packages from package.json dependencies (aggregated from ancestor directories)
|
||||
// plus packages that are directly imported by the project's program files.
|
||||
// If no package.json is found, allowedPackages remains nil and all packages are allowed.
|
||||
var allowedPackages *collections.Set[string]
|
||||
tspath.ForEachAncestorDirectoryPath(v.importingFile.Path().GetDirectoryPath(), func(dirPath tspath.Path) (result any, stop bool) {
|
||||
if dir, ok := v.registry.directories[dirPath]; ok {
|
||||
if pj := dir.packageJson; pj.Exists() && pj.Contents.Parseable {
|
||||
// Initialize to empty set if this is the first package.json we've seen
|
||||
if allowedPackages == nil {
|
||||
allowedPackages = &collections.Set[string]{}
|
||||
}
|
||||
addPackageJsonDependencies(pj.Contents, allowedPackages)
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
})
|
||||
// If we found at least one package.json, also include packages directly imported by the project
|
||||
if allowedPackages != nil {
|
||||
if bucket, ok := v.registry.projects[v.projectKey]; ok {
|
||||
allowedPackages = allowedPackages.UnionedWith(bucket.ResolvedPackageNames)
|
||||
}
|
||||
}
|
||||
|
||||
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 := searchFn(nodeModulesBucket)
|
||||
results = slices.Grow(results, len(exports))
|
||||
for _, e := range exports {
|
||||
// Exclude packages found in lower node_modules (shadowing)
|
||||
if excludePackages.Has(e.PackageName) {
|
||||
continue
|
||||
}
|
||||
// If allowedPackages is nil, no package.json was found, so include all packages.
|
||||
// Otherwise, only include packages that are dependencies or directly imported.
|
||||
if allowedPackages != nil && !allowedPackages.Has(e.PackageName) {
|
||||
continue
|
||||
}
|
||||
results = append(results, e)
|
||||
}
|
||||
|
||||
// As we go up the directory tree, exclude packages found in lower node_modules
|
||||
for pkgName := range nodeModulesBucket.PackageFiles {
|
||||
excludePackages.Add(pkgName)
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
})
|
||||
return results
|
||||
}
|
||||
|
||||
type FixAndExport struct {
|
||||
Fix *Fix
|
||||
Export *Export
|
||||
}
|
||||
|
||||
func (v *View) GetCompletions(ctx context.Context, prefix string, position lsproto.Position, 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 !scanner.IsIdentifierText(name, core.LanguageVariantStandard) {
|
||||
continue
|
||||
}
|
||||
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,
|
||||
PackageName: e.PackageName,
|
||||
IsTypeOnly: e.IsTypeOnly || ex.IsTypeOnly,
|
||||
Syntax: min(e.Syntax, ex.Syntax),
|
||||
Flags: e.Flags | ex.Flags,
|
||||
ScriptElementKind: min(e.ScriptElementKind, ex.ScriptElementKind),
|
||||
ScriptElementKindModifiers: e.ScriptElementKindModifiers | ex.ScriptElementKindModifiers,
|
||||
localName: e.localName,
|
||||
Target: e.Target,
|
||||
Path: e.Path,
|
||||
})
|
||||
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, &position) {
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user