Skip to content

Optimize import fixes for projects with many symlinks #42150

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jan 20, 2021
26 changes: 15 additions & 11 deletions src/compiler/moduleSpecifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3773,7 +3773,7 @@ namespace ts {
return;
}

symlinkCache.setSymlinkedDirectory(directoryPath, {
symlinkCache.setSymlinkedDirectory(directory, {
real: ensureTrailingDirectorySeparator(real),
realPath
});
Expand Down
31 changes: 21 additions & 10 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6079,49 +6079,60 @@ namespace ts {
}

export interface SymlinkCache {
/** Gets a map from symlink to realpath. Keys have trailing directory separators. */
getSymlinkedDirectories(): ReadonlyESMap<Path, SymlinkedDirectory | false> | undefined;
/** Gets a map from realpath to symlinks. Keys have trailing directory separators. */
getSymlinkedDirectoriesByRealpath(): MultiMap<Path, string> | undefined;
/** Gets a map from symlink to realpath */
getSymlinkedFiles(): ReadonlyESMap<Path, string> | 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<Path, SymlinkedDirectory | false> | undefined;
let symlinkedDirectoriesByRealpath: MultiMap<Path, string> | undefined;
let symlinkedFiles: ESMap<Path, string> | 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<readonly [string, string]>(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)))));
for (const [resolvedPath, originalPath] of symlinks) {
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) });
}
}
return cache;
}

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) &&
Expand Down
21 changes: 21 additions & 0 deletions tests/cases/fourslash/autoImportSymlinkCaseSensitive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/// <reference path="fourslash.ts" />

// @Filename: /tsconfig.json
//// { "compilerOptions": { "module": "commonjs" } }

// @Filename: /node_modules/.pnpm/[email protected]/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/[email protected]/node_modules/MobX -> /node_modules/MobX
// @link: /node_modules/.pnpm/[email protected]/node_modules/MobX -> /node_modules/.pnpm/[email protected]/node_modules/MobX

goTo.marker("");
verify.importFixAtPosition([`import { autorun } from "MobX/Foo";

autorun`]);