vendor tsgo

This commit is contained in:
2026-07-09 16:50:43 -04:00
parent c06ea2e5a4
commit 98978e4930
5804 changed files with 1556156 additions and 101 deletions

View 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)

View File

@@ -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)
}

View 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)
}

View File

@@ -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]]
}

View 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)
}

File diff suppressed because it is too large Load Diff

View 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
}

View 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
}

View 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)
})
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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
}

View 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()
}

View 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)
}

View 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",
)
}

View 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
}