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