diff --git a/internal/compiler/emitHost.go b/internal/compiler/emitHost.go index fcc0406342..d09c24f919 100644 --- a/internal/compiler/emitHost.go +++ b/internal/compiler/emitHost.go @@ -9,6 +9,7 @@ import ( "github.com/microsoft/typescript-go/internal/modulespecifiers" "github.com/microsoft/typescript-go/internal/outputpaths" "github.com/microsoft/typescript-go/internal/printer" + "github.com/microsoft/typescript-go/internal/symlinks" "github.com/microsoft/typescript-go/internal/transformers/declarations" "github.com/microsoft/typescript-go/internal/tsoptions" "github.com/microsoft/typescript-go/internal/tspath" @@ -126,3 +127,7 @@ func (host *emitHost) GetEmitResolver() printer.EmitResolver { func (host *emitHost) IsSourceFileFromExternalLibrary(file *ast.SourceFile) bool { return host.program.IsSourceFileFromExternalLibrary(file) } + +func (host *emitHost) GetSymlinkCache() *symlinks.KnownSymlinks { + return host.program.GetSymlinkCache(); +} diff --git a/internal/compiler/knownsymlinks.go b/internal/compiler/knownsymlinks.go deleted file mode 100644 index 246a641127..0000000000 --- a/internal/compiler/knownsymlinks.go +++ /dev/null @@ -1,53 +0,0 @@ -package compiler - -import ( - "github.com/microsoft/typescript-go/internal/collections" - "github.com/microsoft/typescript-go/internal/tspath" -) - -type knownDirectoryLink struct { - /** - * Matches the casing returned by `realpath`. Used to compute the `realpath` of children. - * Always has trailing directory separator - */ - Real string - /** - * toPath(real). Stored to avoid repeated recomputation. - * Always has trailing directory separator - */ - RealPath tspath.Path -} - -type knownSymlinks struct { - directories collections.SyncMap[tspath.Path, *knownDirectoryLink] - files collections.SyncMap[tspath.Path, string] -} - -/** Gets a map from symlink to realpath. Keys have trailing directory separators. */ -func (cache *knownSymlinks) Directories() *collections.SyncMap[tspath.Path, *knownDirectoryLink] { - return &cache.directories -} - -/** Gets a map from symlink to realpath */ -func (cache *knownSymlinks) Files() *collections.SyncMap[tspath.Path, string] { - return &cache.files -} - -// all callers should check !containsIgnoredPath(symlinkPath) -func (cache *knownSymlinks) SetDirectory(symlink string, symlinkPath tspath.Path, realDirectory *knownDirectoryLink) { - // Large, interconnected dependency graphs in pnpm will have a huge number of symlinks - // where both the realpath and the symlink path are inside node_modules/.pnpm. Since - // this path is never a candidate for a module specifier, we can ignore it entirely. - - // !!! - // if realDirectory != nil { - // if _, ok := cache.directories.Load(symlinkPath); !ok { - // cache.directoriesByRealpath.Add(realDirectory.RealPath, symlink) - // } - // } - cache.directories.Store(symlinkPath, realDirectory) -} - -func (cache *knownSymlinks) SetFile(symlinkPath tspath.Path, realpath string) { - cache.files.Store(symlinkPath, realpath) -} diff --git a/internal/compiler/program.go b/internal/compiler/program.go index b70a00197f..057ce540b5 100644 --- a/internal/compiler/program.go +++ b/internal/compiler/program.go @@ -23,6 +23,7 @@ import ( "github.com/microsoft/typescript-go/internal/printer" "github.com/microsoft/typescript-go/internal/scanner" "github.com/microsoft/typescript-go/internal/sourcemap" + "github.com/microsoft/typescript-go/internal/symlinks" "github.com/microsoft/typescript-go/internal/tsoptions" "github.com/microsoft/typescript-go/internal/tspath" ) @@ -66,6 +67,7 @@ type Program struct { // Cached unresolved imports for ATA unresolvedImportsOnce sync.Once unresolvedImports *collections.Set[string] + knownSymlinks *symlinks.KnownSymlinks } // FileExists implements checker.Program. @@ -1624,6 +1626,43 @@ func (p *Program) SourceFileMayBeEmitted(sourceFile *ast.SourceFile, forceDtsEmi return sourceFileMayBeEmitted(sourceFile, p, forceDtsEmit) } +func (p *Program) GetSymlinkCache() *symlinks.KnownSymlinks { + // if p.Host().GetSymlinkCache() != nil { + // return p.Host().GetSymlinkCache() + // } + if p.knownSymlinks == nil { + p.knownSymlinks = symlinks.NewKnownSymlink(p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames()) + } + if p.files != nil && !p.knownSymlinks.HasProcessedResolutions { + p.knownSymlinks.SetSymlinksFromResolutions(p.ForEachResolvedModule, p.ForEachResolvedTypeReferenceDirective) + } + return p.knownSymlinks +} + +func (p *Program) ForEachResolvedModule(callback func(resolution *module.ResolvedModule, moduleName string, mode core.ResolutionMode, filePath tspath.Path), file *ast.SourceFile) { + forEachResolution(p.resolvedModules, callback, file) +} + +func (p *Program) ForEachResolvedTypeReferenceDirective(callback func(resolution *module.ResolvedTypeReferenceDirective, moduleName string, mode core.ResolutionMode, filePath tspath.Path), file *ast.SourceFile) { + forEachResolution(p.typeResolutionsInFile, callback, file) +} + +func forEachResolution[T any](resolutionCache map[tspath.Path]module.ModeAwareCache[T], callback func(resolution T, moduleName string, mode core.ResolutionMode, filePath tspath.Path), file *ast.SourceFile) { + if file != nil { + if resolutions, ok := resolutionCache[file.Path()]; ok { + for key, resolution := range resolutions { + callback(resolution, key.Name, key.Mode, file.Path()) + } + } + } else { + for filePath, resolutions := range resolutionCache { + for key, resolution := range resolutions { + callback(resolution, key.Name, key.Mode, filePath) + } + } + } +} + var plainJSErrors = collections.NewSetFromItems( // binder errors diagnostics.Cannot_redeclare_block_scoped_variable_0.Code(), diff --git a/internal/compiler/projectreferencedtsfakinghost.go b/internal/compiler/projectreferencedtsfakinghost.go index 916dce2225..b63b324290 100644 --- a/internal/compiler/projectreferencedtsfakinghost.go +++ b/internal/compiler/projectreferencedtsfakinghost.go @@ -7,6 +7,7 @@ import ( "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/symlinks" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" "github.com/microsoft/typescript-go/internal/vfs/cachedvfs" @@ -26,7 +27,7 @@ func newProjectReferenceDtsFakingHost(loader *fileLoader) module.ResolutionHost fs: cachedvfs.From(&projectReferenceDtsFakingVfs{ projectReferenceFileMapper: loader.projectReferenceFileMapper, dtsDirectories: loader.dtsDirectories, - knownSymlinks: knownSymlinks{}, + knownSymlinks: symlinks.KnownSymlinks{}, }), } return host @@ -45,7 +46,7 @@ func (h *projectReferenceDtsFakingHost) GetCurrentDirectory() string { type projectReferenceDtsFakingVfs struct { projectReferenceFileMapper *projectReferenceFileMapper dtsDirectories collections.Set[tspath.Path] - knownSymlinks knownSymlinks + knownSymlinks symlinks.KnownSymlinks } var _ vfs.FS = (*projectReferenceDtsFakingVfs)(nil) @@ -150,7 +151,7 @@ func (fs *projectReferenceDtsFakingVfs) handleDirectoryCouldBeSymlink(directory // not symlinked return } - fs.knownSymlinks.SetDirectory(directory, directoryPath, &knownDirectoryLink{ + fs.knownSymlinks.SetDirectory(directory, directoryPath, &symlinks.KnownDirectoryLink{ Real: tspath.EnsureTrailingDirectorySeparator(realDirectory), RealPath: realPath, }) @@ -181,7 +182,7 @@ func (fs *projectReferenceDtsFakingVfs) fileOrDirectoryExistsUsingSource(fileOrD // If it contains node_modules check if its one of the symlinked path we know of var exists bool - knownDirectoryLinks.Range(func(directoryPath tspath.Path, knownDirectoryLink *knownDirectoryLink) bool { + knownDirectoryLinks.Range(func(directoryPath tspath.Path, knownDirectoryLink *symlinks.KnownDirectoryLink) bool { relative, hasPrefix := strings.CutPrefix(string(fileOrDirectoryPath), string(directoryPath)) if !hasPrefix { return true diff --git a/internal/ls/autoimports.go b/internal/ls/autoimports.go index cf89e08fab..bad7b13157 100644 --- a/internal/ls/autoimports.go +++ b/internal/ls/autoimports.go @@ -164,7 +164,8 @@ func (e *exportInfoMap) add( } moduleName := stringutil.StripQuotes(moduleSymbol.Name) - id := e.exportInfoId + 1 + id := e.exportInfoId + e.exportInfoId += 1 target := ch.SkipAlias(symbol) if flagMatch != nil && !flagMatch(target.Flags) { diff --git a/internal/modulespecifiers/specifiers.go b/internal/modulespecifiers/specifiers.go index 1f8e26b040..7bb747bc8f 100644 --- a/internal/modulespecifiers/specifiers.go +++ b/internal/modulespecifiers/specifiers.go @@ -280,36 +280,50 @@ func GetEachFileNameOfModule( } } - // !!! TODO: Symlink directory handling - // const symlinkedDirectories = host.getSymlinkCache?.().getSymlinkedDirectoriesByRealpath(); - // const fullImportedFileName = getNormalizedAbsolutePath(importedFileName, cwd); - // const result = symlinkedDirectories && forEachAncestorDirectoryStoppingAtGlobalCache( - // host, - // getDirectoryPath(fullImportedFileName), - // realPathDirectory => { - // const symlinkDirectories = symlinkedDirectories.get(ensureTrailingDirectorySeparator(toPath(realPathDirectory, cwd, getCanonicalFileName))); - // if (!symlinkDirectories) return undefined; // Continue to ancestor directory - - // // Don't want to a package to globally import from itself (importNameCodeFix_symlink_own_package.ts) - // if (startsWithDirectory(importingFileName, realPathDirectory, getCanonicalFileName)) { - // return false; // Stop search, each ancestor directory will also hit this condition - // } - - // return forEach(targets, target => { - // if (!startsWithDirectory(target, realPathDirectory, getCanonicalFileName)) { - // return; - // } - - // const relative = getRelativePathFromDirectory(realPathDirectory, target, getCanonicalFileName); - // for (const symlinkDirectory of symlinkDirectories) { - // const option = resolvePath(symlinkDirectory, relative); - // const result = cb(option, target === referenceRedirect); - // shouldFilterIgnoredPaths = true; // We found a non-ignored path in symlinks, so we can reject ignored-path realpaths - // if (result) return result; - // } - // }); - // }, - // ); + symlinkedDirectories := host.GetSymlinkCache().DirectoriesByRealpath() + fullImportedFileName := tspath.GetNormalizedAbsolutePath(importedFileName, cwd) + if symlinkedDirectories != nil { + tspath.ForEachAncestorDirectoryStoppingAtGlobalCache( + host.GetGlobalTypingsCacheLocation(), + tspath.GetDirectoryPath(fullImportedFileName), + func(realPathDirectory string) (bool, bool) { + symlinkDirectories := symlinkedDirectories.Get(tspath.ToPath(realPathDirectory, cwd, host.UseCaseSensitiveFileNames()).EnsureTrailingDirectorySeparator()) + if symlinkDirectories == nil { + return false, false + } // Continue to ancestor directory + + // Don't want to a package to globally import from itself (importNameCodeFix_symlink_own_package.ts) + if tspath.StartsWithDirectory(importingFileName, realPathDirectory, host.UseCaseSensitiveFileNames()) { + return false, true // Stop search, each ancestor directory will also hit this condition + } + + for _, target := range targets { + if !tspath.StartsWithDirectory(target, realPathDirectory, host.UseCaseSensitiveFileNames()) { + continue + } + + relative := tspath.GetRelativePathFromDirectory( + realPathDirectory, + target, + tspath.ComparePathsOptions{ + UseCaseSensitiveFileNames: host.UseCaseSensitiveFileNames(), + CurrentDirectory: cwd, + }) + for _, symlinkDirectory := range symlinkDirectories { + option := tspath.ResolvePath(symlinkDirectory, relative) + results = append(results, ModulePath{ + FileName: option, + IsInNodeModules: ContainsNodeModules(option), + IsRedirect: target == referenceRedirect, + }) + shouldFilterIgnoredPaths = true // We found a non-ignored path in symlinks, so we can reject ignored-path realpaths + } + } + + return false, false + }, + ) + } if preferSymlinks { for _, p := range targets { diff --git a/internal/modulespecifiers/types.go b/internal/modulespecifiers/types.go index 6cdfa3e689..b22218c08b 100644 --- a/internal/modulespecifiers/types.go +++ b/internal/modulespecifiers/types.go @@ -5,6 +5,7 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/module" "github.com/microsoft/typescript-go/internal/packagejson" + "github.com/microsoft/typescript-go/internal/symlinks" "github.com/microsoft/typescript-go/internal/tsoptions" "github.com/microsoft/typescript-go/internal/tspath" ) @@ -45,7 +46,7 @@ type PackageJsonInfo interface { type ModuleSpecifierGenerationHost interface { // GetModuleResolutionCache() any // !!! TODO: adapt new resolution cache model - // GetSymlinkCache() any // !!! TODO: adapt new resolution cache model + GetSymlinkCache() *symlinks.KnownSymlinks // GetFileIncludeReasons() any // !!! TODO: adapt new resolution cache model CommonSourceDirectory() string GetGlobalTypingsCacheLocation() string diff --git a/internal/symlinks/knownsymlinks.go b/internal/symlinks/knownsymlinks.go new file mode 100644 index 0000000000..6e160660ca --- /dev/null +++ b/internal/symlinks/knownsymlinks.go @@ -0,0 +1,129 @@ +package symlinks + +import ( + "strings" + + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/collections" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/debug" + "github.com/microsoft/typescript-go/internal/module" + "github.com/microsoft/typescript-go/internal/tspath" +) + +type KnownDirectoryLink struct { + /** + * Matches the casing returned by `realpath`. Used to compute the `realpath` of children. + * Always has trailing directory separator + */ + Real string + /** + * toPath(real). Stored to avoid repeated recomputation. + * Always has trailing directory separator + */ + RealPath tspath.Path +} + +type KnownSymlinks struct { + directories collections.SyncMap[tspath.Path, *KnownDirectoryLink] + directoriesByRealpath collections.MultiMap[tspath.Path, string] + files collections.SyncMap[tspath.Path, string] + HasProcessedResolutions bool + cwd string + useCaseSensitiveFileNames bool +} + +/** Gets a map from symlink to realpath. Keys have trailing directory separators. */ +func (cache *KnownSymlinks) Directories() *collections.SyncMap[tspath.Path, *KnownDirectoryLink] { + return &cache.directories +} + +func (cache *KnownSymlinks) DirectoriesByRealpath() *collections.MultiMap[tspath.Path, string] { + return &cache.directoriesByRealpath +} + +/** Gets a map from symlink to realpath */ +func (cache *KnownSymlinks) Files() *collections.SyncMap[tspath.Path, string] { + return &cache.files +} + +// all callers should check !containsIgnoredPath(symlinkPath) +func (cache *KnownSymlinks) SetDirectory(symlink string, symlinkPath tspath.Path, realDirectory *KnownDirectoryLink) { + // Large, interconnected dependency graphs in pnpm will have a huge number of symlinks + // where both the realpath and the symlink path are inside node_modules/.pnpm. Since + // this path is never a candidate for a module specifier, we can ignore it entirely. + + if realDirectory != nil { + if _, ok := cache.directories.Load(symlinkPath); !ok { + cache.directoriesByRealpath.Add(realDirectory.RealPath, symlink) + } + } + cache.directories.Store(symlinkPath, realDirectory) +} + +func (cache *KnownSymlinks) SetFile(symlinkPath tspath.Path, realpath string) { + cache.files.Store(symlinkPath, realpath) +} + +func NewKnownSymlink(currentDirectory string, useCaseSensitiveFileNames bool) *KnownSymlinks { + return &KnownSymlinks{cwd: currentDirectory, useCaseSensitiveFileNames: useCaseSensitiveFileNames} +} + +func (cache *KnownSymlinks) SetSymlinksFromResolutions( + forEachResolvedModule func(callback func(resolution *module.ResolvedModule, moduleName string, mode core.ResolutionMode, filePath tspath.Path), file *ast.SourceFile), + forEachResolvedTypeReferenceDirective func(callback func(resolution *module.ResolvedTypeReferenceDirective, moduleName string, mode core.ResolutionMode, filePath tspath.Path), file *ast.SourceFile), +) { + debug.Assert(!cache.HasProcessedResolutions) + cache.HasProcessedResolutions = true + forEachResolvedModule(func(resolution *module.ResolvedModule, moduleName string, mode core.ResolutionMode, filePath tspath.Path) { + cache.processResolution(resolution.OriginalPath, resolution.ResolvedFileName) + }, nil) + forEachResolvedTypeReferenceDirective(func(resolution *module.ResolvedTypeReferenceDirective, moduleName string, mode core.ResolutionMode, filePath tspath.Path) { + cache.processResolution(resolution.OriginalPath, resolution.ResolvedFileName) + }, nil) +} + +func (cache *KnownSymlinks) processResolution(originalPath string, resolvedFileName string) { + if originalPath == "" || resolvedFileName == "" { + return + } + cache.SetFile(tspath.ToPath(originalPath, cache.cwd, cache.useCaseSensitiveFileNames), resolvedFileName) + commonResolved, commonOriginal := cache.guessDirectorySymlink(resolvedFileName, originalPath, cache.cwd) + if commonResolved != "" && commonOriginal != "" { + symlinkPath := tspath.ToPath(commonOriginal, cache.cwd, cache.useCaseSensitiveFileNames) + if !tspath.ContainsIgnoredPath(string(symlinkPath)) { + cache.SetDirectory( + commonOriginal, + symlinkPath.EnsureTrailingDirectorySeparator(), + &KnownDirectoryLink{ + Real: tspath.EnsureTrailingDirectorySeparator(commonResolved), + RealPath: tspath.ToPath(commonResolved, cache.cwd, cache.useCaseSensitiveFileNames).EnsureTrailingDirectorySeparator(), + }, + ) + } + } +} + +func (cache *KnownSymlinks) guessDirectorySymlink(a string, b string, cwd string) (string, string) { + aParts := tspath.GetPathComponents(tspath.GetNormalizedAbsolutePath(a, cwd), "") + bParts := tspath.GetPathComponents(tspath.GetNormalizedAbsolutePath(b, cwd), "") + isDirectory := false + for len(aParts) >= 2 && len(bParts) >= 2 && + !cache.isNodeModulesOrScopedPackageDirectory(aParts[len(aParts)-2]) && + !cache.isNodeModulesOrScopedPackageDirectory(bParts[len(bParts)-2]) && + tspath.GetCanonicalFileName(aParts[len(aParts)-1], cache.useCaseSensitiveFileNames) == tspath.GetCanonicalFileName(bParts[len(bParts)-1], cache.useCaseSensitiveFileNames) { + aParts = aParts[:len(aParts)-1] + bParts = bParts[:len(bParts)-1] + isDirectory = true + } + if isDirectory { + return tspath.GetPathFromPathComponents(aParts), tspath.GetPathFromPathComponents(bParts) + } + return "", "" +} + +// KLUDGE: Don't assume one 'node_modules' links to another. More likely a single directory inside the node_modules is the symlink. +// ALso, don't assume that an `@foo` directory is linked. More likely the contents of that are linked. +func (cache *KnownSymlinks) isNodeModulesOrScopedPackageDirectory(s string) bool { + return s != "" && (tspath.GetCanonicalFileName(s, cache.useCaseSensitiveFileNames) == "node_modules" || strings.HasPrefix(s, "@")) +} diff --git a/internal/tspath/path.go b/internal/tspath/path.go index fae3423721..d03de21bc2 100644 --- a/internal/tspath/path.go +++ b/internal/tspath/path.go @@ -1127,3 +1127,9 @@ func getCommonParentsWorker(componentGroups [][]string, minComponents int, optio return [][]string{componentGroups[0][:maxDepth]} } + +func StartsWithDirectory(fileName string, directoryName string, useCaseSensitiveFileNames bool) bool { + canonicalFileName := GetCanonicalFileName(fileName, useCaseSensitiveFileNames) + canonicalDirectoryName := GetCanonicalFileName(directoryName, useCaseSensitiveFileNames) + return strings.HasPrefix(canonicalFileName, canonicalDirectoryName+"/") || strings.HasPrefix(canonicalFileName, canonicalDirectoryName+"\\") +}