Skip to content

Commit 5f5437a

Browse files
authored
Do not cache directory structure for symlinked directories (#42868)
Fixes #42839
1 parent 26bbdf1 commit 5f5437a

File tree

5 files changed

+290
-20
lines changed

5 files changed

+290
-20
lines changed

src/compiler/watchUtilities.ts

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ namespace ts {
4444
return undefined;
4545
}
4646

47-
const cachedReadDirectoryResult = new Map<string, MutableFileSystemEntries>();
47+
const cachedReadDirectoryResult = new Map<string, MutableFileSystemEntries | false>();
4848
const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
4949
return {
5050
useCaseSensitiveFileNames,
@@ -65,11 +65,11 @@ namespace ts {
6565
return ts.toPath(fileName, currentDirectory, getCanonicalFileName);
6666
}
6767

68-
function getCachedFileSystemEntries(rootDirPath: Path): MutableFileSystemEntries | undefined {
68+
function getCachedFileSystemEntries(rootDirPath: Path) {
6969
return cachedReadDirectoryResult.get(ensureTrailingDirectorySeparator(rootDirPath));
7070
}
7171

72-
function getCachedFileSystemEntriesForBaseDir(path: Path): MutableFileSystemEntries | undefined {
72+
function getCachedFileSystemEntriesForBaseDir(path: Path) {
7373
return getCachedFileSystemEntries(getDirectoryPath(path));
7474
}
7575

@@ -78,21 +78,32 @@ namespace ts {
7878
}
7979

8080
function createCachedFileSystemEntries(rootDir: string, rootDirPath: Path) {
81-
const resultFromHost: MutableFileSystemEntries = {
82-
files: map(host.readDirectory!(rootDir, /*extensions*/ undefined, /*exclude*/ undefined, /*include*/["*.*"]), getBaseNameOfFileName) || [],
83-
directories: host.getDirectories!(rootDir) || []
84-
};
81+
if (!host.realpath || ensureTrailingDirectorySeparator(toPath(host.realpath(rootDir))) === rootDirPath) {
82+
const resultFromHost: MutableFileSystemEntries = {
83+
files: map(host.readDirectory!(rootDir, /*extensions*/ undefined, /*exclude*/ undefined, /*include*/["*.*"]), getBaseNameOfFileName) || [],
84+
directories: host.getDirectories!(rootDir) || []
85+
};
86+
87+
cachedReadDirectoryResult.set(ensureTrailingDirectorySeparator(rootDirPath), resultFromHost);
88+
return resultFromHost;
89+
}
8590

86-
cachedReadDirectoryResult.set(ensureTrailingDirectorySeparator(rootDirPath), resultFromHost);
87-
return resultFromHost;
91+
// If the directory is symlink do not cache the result
92+
if (host.directoryExists?.(rootDir)) {
93+
cachedReadDirectoryResult.set(rootDirPath, false);
94+
return false;
95+
}
96+
97+
// Non existing directory
98+
return undefined;
8899
}
89100

90101
/**
91102
* If the readDirectory result was already cached, it returns that
92103
* Otherwise gets result from host and caches it.
93104
* The host request is done under try catch block to avoid caching incorrect result
94105
*/
95-
function tryReadDirectory(rootDir: string, rootDirPath: Path): MutableFileSystemEntries | undefined {
106+
function tryReadDirectory(rootDir: string, rootDirPath: Path) {
96107
rootDirPath = ensureTrailingDirectorySeparator(rootDirPath);
97108
const cachedResult = getCachedFileSystemEntries(rootDirPath);
98109
if (cachedResult) {
@@ -170,18 +181,32 @@ namespace ts {
170181

171182
function readDirectory(rootDir: string, extensions?: readonly string[], excludes?: readonly string[], includes?: readonly string[], depth?: number): string[] {
172183
const rootDirPath = toPath(rootDir);
173-
const result = tryReadDirectory(rootDir, rootDirPath);
174-
if (result) {
184+
const rootResult = tryReadDirectory(rootDir, rootDirPath);
185+
let rootSymLinkResult: FileSystemEntries | undefined;
186+
if (rootResult !== undefined) {
175187
return matchFiles(rootDir, extensions, excludes, includes, useCaseSensitiveFileNames, currentDirectory, depth, getFileSystemEntries, realpath);
176188
}
177189
return host.readDirectory!(rootDir, extensions, excludes, includes, depth);
178190

179191
function getFileSystemEntries(dir: string): FileSystemEntries {
180192
const path = toPath(dir);
181193
if (path === rootDirPath) {
182-
return result!;
194+
return rootResult || getFileSystemEntriesFromHost(dir, path);
183195
}
184-
return tryReadDirectory(dir, path) || emptyFileSystemEntries;
196+
const result = tryReadDirectory(dir, path);
197+
return result !== undefined ?
198+
result || getFileSystemEntriesFromHost(dir, path) :
199+
emptyFileSystemEntries;
200+
}
201+
202+
function getFileSystemEntriesFromHost(dir: string, path: Path): FileSystemEntries {
203+
if (rootSymLinkResult && path === rootDirPath) return rootSymLinkResult;
204+
const result: FileSystemEntries = {
205+
files: map(host.readDirectory!(dir, /*extensions*/ undefined, /*exclude*/ undefined, /*include*/["*.*"]), getBaseNameOfFileName) || emptyArray,
206+
directories: host.getDirectories!(dir) || emptyArray
207+
};
208+
if (path === rootDirPath) rootSymLinkResult = result;
209+
return result;
185210
}
186211
}
187212

@@ -191,7 +216,7 @@ namespace ts {
191216

192217
function addOrDeleteFileOrDirectory(fileOrDirectory: string, fileOrDirectoryPath: Path) {
193218
const existingResult = getCachedFileSystemEntries(fileOrDirectoryPath);
194-
if (existingResult) {
219+
if (existingResult !== undefined) {
195220
// Just clear the cache for now
196221
// For now just clear the cache, since this could mean that multiple level entries might need to be re-evaluated
197222
clearCache();

src/harness/virtualFileSystemWithWatch.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,14 +1001,18 @@ interface Array<T> { length: number; [n: number]: T; }`
10011001

10021002
// base folder has to be present
10031003
const base = getDirectoryPath(file.path);
1004-
const folder = this.fs.get(base) as FsFolder;
1005-
Debug.assert(isFsFolder(folder));
1004+
const folder = Debug.checkDefined(this.getRealFolder(base));
10061005

1007-
if (!this.fs.has(file.path)) {
1008-
this.addFileOrFolderInFolder(folder, file);
1006+
if (folder.path === base) {
1007+
if (!this.fs.has(file.path)) {
1008+
this.addFileOrFolderInFolder(folder, file);
1009+
}
1010+
else {
1011+
this.modifyFile(path, content);
1012+
}
10091013
}
10101014
else {
1011-
this.modifyFile(path, content);
1015+
this.writeFile(this.realpath(path), content);
10121016
}
10131017
}
10141018

src/testRunner/unittests/tscWatch/programUpdates.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1696,5 +1696,43 @@ import { x } from "../b";`),
16961696
},
16971697
]
16981698
});
1699+
1700+
verifyTscWatch({
1701+
scenario,
1702+
subScenario: "when creating new file in symlinked folder",
1703+
commandLineArgs: ["-w", "-p", ".", "--extendedDiagnostics"],
1704+
sys: () => {
1705+
const module1: File = {
1706+
path: `${projectRoot}/client/folder1/module1.ts`,
1707+
content: `export class Module1Class { }`
1708+
};
1709+
const module2: File = {
1710+
path: `${projectRoot}/folder2/module2.ts`,
1711+
content: `import * as M from "folder1/module1";`
1712+
};
1713+
const symlink: SymLink = {
1714+
path: `${projectRoot}/client/linktofolder2`,
1715+
symLink: `${projectRoot}/folder2`,
1716+
};
1717+
const config: File = {
1718+
path: `${projectRoot}/tsconfig.json`,
1719+
content: JSON.stringify({
1720+
compilerOptions: {
1721+
baseUrl: "client",
1722+
paths: { "*": ["*"] },
1723+
},
1724+
include: ["client/**/*", "folder2"]
1725+
})
1726+
};
1727+
return createWatchedSystem([module1, module2, symlink, config, libFile], { currentDirectory: projectRoot });
1728+
},
1729+
changes: [
1730+
{
1731+
caption: "Add module3 to folder2",
1732+
change: sys => sys.writeFile(`${projectRoot}/client/linktofolder2/module3.ts`, `import * as M from "folder1/module1";`),
1733+
timeouts: checkSingleTimeoutQueueLengthAndRun,
1734+
},
1735+
]
1736+
});
16991737
});
17001738
}

src/testRunner/unittests/tsserver/cachingFileSystemInformation.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,5 +713,40 @@ namespace ts.projectSystem {
713713
checkProjectActualFiles(project, files.map(f => f.path));
714714
assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(app.path).map(diag => diag.messageText), []);
715715
});
716+
717+
it("when creating new file in symlinked folder", () => {
718+
const module1: File = {
719+
path: `${tscWatch.projectRoot}/client/folder1/module1.ts`,
720+
content: `export class Module1Class { }`
721+
};
722+
const module2: File = {
723+
path: `${tscWatch.projectRoot}/folder2/module2.ts`,
724+
content: `import * as M from "folder1/module1";`
725+
};
726+
const symlink: SymLink = {
727+
path: `${tscWatch.projectRoot}/client/linktofolder2`,
728+
symLink: `${tscWatch.projectRoot}/folder2`,
729+
};
730+
const config: File = {
731+
path: `${tscWatch.projectRoot}/tsconfig.json`,
732+
content: JSON.stringify({
733+
compilerOptions: {
734+
baseUrl: "client",
735+
paths: { "*": ["*"] },
736+
},
737+
include: ["client/**/*", "folder2"]
738+
})
739+
};
740+
const host = createServerHost([module1, module2, symlink, config, libFile]);
741+
const service = createProjectService(host);
742+
service.openClientFile(`${symlink.path}/module2.ts`);
743+
checkNumberOfProjects(service, { configuredProjects: 1 });
744+
const project = Debug.checkDefined(service.configuredProjects.get(config.path));
745+
checkProjectActualFiles(project, [module1.path, `${symlink.path}/module2.ts`, config.path, libFile.path]);
746+
host.writeFile(`${symlink.path}/module3.ts`, `import * as M from "folder1/module1";`);
747+
host.runQueuedTimeoutCallbacks();
748+
checkNumberOfProjects(service, { configuredProjects: 1 });
749+
checkProjectActualFiles(project, [module1.path, `${symlink.path}/module2.ts`, config.path, libFile.path, `${symlink.path}/module3.ts`]);
750+
});
716751
});
717752
}

0 commit comments

Comments
 (0)