diff --git a/src/compiler/moduleSpecifiers.ts b/src/compiler/moduleSpecifiers.ts index 72332a2201fb6..99ccfb8c98286 100644 --- a/src/compiler/moduleSpecifiers.ts +++ b/src/compiler/moduleSpecifiers.ts @@ -286,7 +286,8 @@ namespace ts.moduleSpecifiers { const getCanonicalFileName = hostGetCanonicalFileName(host); const cwd = host.getCurrentDirectory(); const referenceRedirect = host.isSourceOfProjectReferenceRedirect(importedFileName) ? host.getProjectReferenceRedirect(importedFileName) : undefined; - const redirects = host.redirectTargetsMap.get(toPath(importedFileName, cwd, getCanonicalFileName)) || emptyArray; + const importedPath = toPath(importedFileName, cwd, getCanonicalFileName); + const redirects = host.redirectTargetsMap.get(importedPath) || emptyArray; const importedFileNames = [...(referenceRedirect ? [referenceRedirect] : emptyArray), importedFileName, ...redirects]; const targets = importedFileNames.map(f => getNormalizedAbsolutePath(f, cwd)); if (!preferSymlinks) { @@ -299,22 +300,25 @@ namespace ts.moduleSpecifiers { ? host.getSymlinkCache() : discoverProbableSymlinks(host.getSourceFiles(), getCanonicalFileName, cwd); - const symlinkedDirectories = links.getSymlinkedDirectories(); - const useCaseSensitiveFileNames = !host.useCaseSensitiveFileNames || host.useCaseSensitiveFileNames(); - const result = symlinkedDirectories && forEachEntry(symlinkedDirectories, (resolved, path) => { - if (resolved === false) return undefined; - if (startsWithDirectory(importingFileName, resolved.realPath, getCanonicalFileName)) { - return undefined; // Don't want to a package to globally import from itself + const symlinkedDirectories = links.getSymlinkedDirectoriesByRealpath(); + const fullImportedFileName = getNormalizedAbsolutePath(importedFileName, cwd); + const result = symlinkedDirectories && forEachAncestorDirectory(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 (!containsPath(resolved.real, target, !useCaseSensitiveFileNames)) { + if (!startsWithDirectory(target, realPathDirectory, getCanonicalFileName)) { return; } - const relative = getRelativePathFromDirectory(resolved.real, target, getCanonicalFileName); - const option = resolvePath(path, relative); - if (!host.fileExists || host.fileExists(option)) { + const relative = getRelativePathFromDirectory(realPathDirectory, target, getCanonicalFileName); + for (const symlinkDirectory of symlinkDirectories) { + const option = resolvePath(symlinkDirectory, relative); const result = cb(option, target === referenceRedirect); if (result) return result; } diff --git a/src/compiler/path.ts b/src/compiler/path.ts index d4ee8600d7190..6a9374873b1a0 100644 --- a/src/compiler/path.ts +++ b/src/compiler/path.ts @@ -762,7 +762,7 @@ namespace ts { * Determines whether `fileName` starts with the specified `directoryName` using the provided path canonicalization callback. * Comparison is case-sensitive between the canonical paths. * - * @deprecated Use `containsPath` if possible. + * Use `containsPath` if file names are not already reduced and absolute. */ export function startsWithDirectory(fileName: string, directoryName: string, getCanonicalFileName: GetCanonicalFileName): boolean { const canonicalFileName = getCanonicalFileName(fileName); diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 10ac23aa196ea..5288c26ffd1a8 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -3773,7 +3773,7 @@ namespace ts { return; } - symlinkCache.setSymlinkedDirectory(directoryPath, { + symlinkCache.setSymlinkedDirectory(directory, { real: ensureTrailingDirectorySeparator(real), realPath }); diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 85a19df7f0694..3ae479a75175f 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -6079,32 +6079,43 @@ namespace ts { } export interface SymlinkCache { + /** Gets a map from symlink to realpath. Keys have trailing directory separators. */ getSymlinkedDirectories(): ReadonlyESMap | undefined; + /** Gets a map from realpath to symlinks. Keys have trailing directory separators. */ + getSymlinkedDirectoriesByRealpath(): MultiMap | undefined; + /** Gets a map from symlink to realpath */ getSymlinkedFiles(): ReadonlyESMap | undefined; - setSymlinkedDirectory(path: Path, directory: SymlinkedDirectory | false): void; - setSymlinkedFile(path: Path, real: string): void; + setSymlinkedDirectory(symlink: string, real: SymlinkedDirectory | false): void; + setSymlinkedFile(symlinkPath: Path, real: string): void; } - export function createSymlinkCache(): SymlinkCache { + export function createSymlinkCache(cwd: string, getCanonicalFileName: GetCanonicalFileName): SymlinkCache { let symlinkedDirectories: ESMap | undefined; + let symlinkedDirectoriesByRealpath: MultiMap | undefined; let symlinkedFiles: ESMap | undefined; return { getSymlinkedFiles: () => symlinkedFiles, getSymlinkedDirectories: () => symlinkedDirectories, + getSymlinkedDirectoriesByRealpath: () => symlinkedDirectoriesByRealpath, setSymlinkedFile: (path, real) => (symlinkedFiles || (symlinkedFiles = new Map())).set(path, real), - setSymlinkedDirectory: (path, directory) => { + setSymlinkedDirectory: (symlink, real) => { // 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 (!containsIgnoredPath(path)) { - (symlinkedDirectories || (symlinkedDirectories = new Map())).set(path, directory); + let symlinkPath = toPath(symlink, cwd, getCanonicalFileName); + if (!containsIgnoredPath(symlinkPath)) { + symlinkPath = ensureTrailingDirectorySeparator(symlinkPath); + if (real !== false && !symlinkedDirectories?.has(symlinkPath)) { + (symlinkedDirectoriesByRealpath ||= createMultiMap()).add(ensureTrailingDirectorySeparator(real.realPath), symlink); + } + (symlinkedDirectories || (symlinkedDirectories = new Map())).set(symlinkPath, real); } } }; } export function discoverProbableSymlinks(files: readonly SourceFile[], getCanonicalFileName: GetCanonicalFileName, cwd: string): SymlinkCache { - const cache = createSymlinkCache(); + const cache = createSymlinkCache(cwd, getCanonicalFileName); const symlinks = flatten(mapDefined(files, sf => sf.resolvedModules && compact(arrayFrom(mapIterator(sf.resolvedModules.values(), res => res && res.originalPath && res.resolvedFileName !== res.originalPath ? [res.resolvedFileName, res.originalPath] as const : undefined))))); @@ -6112,7 +6123,7 @@ namespace ts { const [commonResolved, commonOriginal] = guessDirectorySymlink(resolvedPath, originalPath, cwd, getCanonicalFileName) || emptyArray; if (commonResolved && commonOriginal) { cache.setSymlinkedDirectory( - toPath(commonOriginal, cwd, getCanonicalFileName), + commonOriginal, { real: commonResolved, realPath: toPath(commonResolved, cwd, getCanonicalFileName) }); } } @@ -6120,8 +6131,8 @@ namespace ts { } function guessDirectorySymlink(a: string, b: string, cwd: string, getCanonicalFileName: GetCanonicalFileName): [string, string] | undefined { - const aParts = getPathComponents(toPath(a, cwd, getCanonicalFileName)); - const bParts = getPathComponents(toPath(b, cwd, getCanonicalFileName)); + const aParts = getPathComponents(getNormalizedAbsolutePath(a, cwd)); + const bParts = getPathComponents(getNormalizedAbsolutePath(b, cwd)); let isDirectory = false; while (!isNodeModulesOrScopedPackageDirectory(aParts[aParts.length - 2], getCanonicalFileName) && !isNodeModulesOrScopedPackageDirectory(bParts[bParts.length - 2], getCanonicalFileName) && diff --git a/tests/cases/fourslash/autoImportSymlinkCaseSensitive.ts b/tests/cases/fourslash/autoImportSymlinkCaseSensitive.ts new file mode 100644 index 0000000000000..1ae2db666502e --- /dev/null +++ b/tests/cases/fourslash/autoImportSymlinkCaseSensitive.ts @@ -0,0 +1,21 @@ +/// + +// @Filename: /tsconfig.json +//// { "compilerOptions": { "module": "commonjs" } } + +// @Filename: /node_modules/.pnpm/mobx@6.0.4/node_modules/MobX/Foo.d.ts +//// export declare function autorun(): void; + +// @Filename: /index.ts +//// autorun/**/ + +// @Filename: /utils.ts +//// import "MobX/Foo"; + +// @link: /node_modules/.pnpm/mobx@6.0.4/node_modules/MobX -> /node_modules/MobX +// @link: /node_modules/.pnpm/mobx@6.0.4/node_modules/MobX -> /node_modules/.pnpm/cool-mobx-dependent@1.2.3/node_modules/MobX + +goTo.marker(""); +verify.importFixAtPosition([`import { autorun } from "MobX/Foo"; + +autorun`]);