From 5f5437af6689f9b5a6d4f6e48b6ff46c48cd5097 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 24 Feb 2021 09:51:19 -0800 Subject: [PATCH] Do not cache directory structure for symlinked directories (#42868) Fixes #42839 --- src/compiler/watchUtilities.ts | 55 ++++-- src/harness/virtualFileSystemWithWatch.ts | 14 +- .../unittests/tscWatch/programUpdates.ts | 38 ++++ .../tsserver/cachingFileSystemInformation.ts | 35 ++++ ...n-creating-new-file-in-symlinked-folder.js | 168 ++++++++++++++++++ 5 files changed, 290 insertions(+), 20 deletions(-) create mode 100644 tests/baselines/reference/tscWatch/programUpdates/when-creating-new-file-in-symlinked-folder.js diff --git a/src/compiler/watchUtilities.ts b/src/compiler/watchUtilities.ts index 80fd5fe7a8f..091ae43494b 100644 --- a/src/compiler/watchUtilities.ts +++ b/src/compiler/watchUtilities.ts @@ -44,7 +44,7 @@ namespace ts { return undefined; } - const cachedReadDirectoryResult = new Map(); + const cachedReadDirectoryResult = new Map(); const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); return { useCaseSensitiveFileNames, @@ -65,11 +65,11 @@ namespace ts { return ts.toPath(fileName, currentDirectory, getCanonicalFileName); } - function getCachedFileSystemEntries(rootDirPath: Path): MutableFileSystemEntries | undefined { + function getCachedFileSystemEntries(rootDirPath: Path) { return cachedReadDirectoryResult.get(ensureTrailingDirectorySeparator(rootDirPath)); } - function getCachedFileSystemEntriesForBaseDir(path: Path): MutableFileSystemEntries | undefined { + function getCachedFileSystemEntriesForBaseDir(path: Path) { return getCachedFileSystemEntries(getDirectoryPath(path)); } @@ -78,13 +78,24 @@ namespace ts { } function createCachedFileSystemEntries(rootDir: string, rootDirPath: Path) { - const resultFromHost: MutableFileSystemEntries = { - files: map(host.readDirectory!(rootDir, /*extensions*/ undefined, /*exclude*/ undefined, /*include*/["*.*"]), getBaseNameOfFileName) || [], - directories: host.getDirectories!(rootDir) || [] - }; + if (!host.realpath || ensureTrailingDirectorySeparator(toPath(host.realpath(rootDir))) === rootDirPath) { + const resultFromHost: MutableFileSystemEntries = { + files: map(host.readDirectory!(rootDir, /*extensions*/ undefined, /*exclude*/ undefined, /*include*/["*.*"]), getBaseNameOfFileName) || [], + directories: host.getDirectories!(rootDir) || [] + }; - cachedReadDirectoryResult.set(ensureTrailingDirectorySeparator(rootDirPath), resultFromHost); - return resultFromHost; + cachedReadDirectoryResult.set(ensureTrailingDirectorySeparator(rootDirPath), resultFromHost); + return resultFromHost; + } + + // If the directory is symlink do not cache the result + if (host.directoryExists?.(rootDir)) { + cachedReadDirectoryResult.set(rootDirPath, false); + return false; + } + + // Non existing directory + return undefined; } /** @@ -92,7 +103,7 @@ namespace ts { * Otherwise gets result from host and caches it. * The host request is done under try catch block to avoid caching incorrect result */ - function tryReadDirectory(rootDir: string, rootDirPath: Path): MutableFileSystemEntries | undefined { + function tryReadDirectory(rootDir: string, rootDirPath: Path) { rootDirPath = ensureTrailingDirectorySeparator(rootDirPath); const cachedResult = getCachedFileSystemEntries(rootDirPath); if (cachedResult) { @@ -170,8 +181,9 @@ namespace ts { function readDirectory(rootDir: string, extensions?: readonly string[], excludes?: readonly string[], includes?: readonly string[], depth?: number): string[] { const rootDirPath = toPath(rootDir); - const result = tryReadDirectory(rootDir, rootDirPath); - if (result) { + const rootResult = tryReadDirectory(rootDir, rootDirPath); + let rootSymLinkResult: FileSystemEntries | undefined; + if (rootResult !== undefined) { return matchFiles(rootDir, extensions, excludes, includes, useCaseSensitiveFileNames, currentDirectory, depth, getFileSystemEntries, realpath); } return host.readDirectory!(rootDir, extensions, excludes, includes, depth); @@ -179,9 +191,22 @@ namespace ts { function getFileSystemEntries(dir: string): FileSystemEntries { const path = toPath(dir); if (path === rootDirPath) { - return result!; + return rootResult || getFileSystemEntriesFromHost(dir, path); } - return tryReadDirectory(dir, path) || emptyFileSystemEntries; + const result = tryReadDirectory(dir, path); + return result !== undefined ? + result || getFileSystemEntriesFromHost(dir, path) : + emptyFileSystemEntries; + } + + function getFileSystemEntriesFromHost(dir: string, path: Path): FileSystemEntries { + if (rootSymLinkResult && path === rootDirPath) return rootSymLinkResult; + const result: FileSystemEntries = { + files: map(host.readDirectory!(dir, /*extensions*/ undefined, /*exclude*/ undefined, /*include*/["*.*"]), getBaseNameOfFileName) || emptyArray, + directories: host.getDirectories!(dir) || emptyArray + }; + if (path === rootDirPath) rootSymLinkResult = result; + return result; } } @@ -191,7 +216,7 @@ namespace ts { function addOrDeleteFileOrDirectory(fileOrDirectory: string, fileOrDirectoryPath: Path) { const existingResult = getCachedFileSystemEntries(fileOrDirectoryPath); - if (existingResult) { + if (existingResult !== undefined) { // Just clear the cache for now // For now just clear the cache, since this could mean that multiple level entries might need to be re-evaluated clearCache(); diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index c3fc8440b58..6b589331625 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -1001,14 +1001,18 @@ interface Array { length: number; [n: number]: T; }` // base folder has to be present const base = getDirectoryPath(file.path); - const folder = this.fs.get(base) as FsFolder; - Debug.assert(isFsFolder(folder)); + const folder = Debug.checkDefined(this.getRealFolder(base)); - if (!this.fs.has(file.path)) { - this.addFileOrFolderInFolder(folder, file); + if (folder.path === base) { + if (!this.fs.has(file.path)) { + this.addFileOrFolderInFolder(folder, file); + } + else { + this.modifyFile(path, content); + } } else { - this.modifyFile(path, content); + this.writeFile(this.realpath(path), content); } } diff --git a/src/testRunner/unittests/tscWatch/programUpdates.ts b/src/testRunner/unittests/tscWatch/programUpdates.ts index 80a4d60cf70..1e9dcccee76 100644 --- a/src/testRunner/unittests/tscWatch/programUpdates.ts +++ b/src/testRunner/unittests/tscWatch/programUpdates.ts @@ -1696,5 +1696,43 @@ import { x } from "../b";`), }, ] }); + + verifyTscWatch({ + scenario, + subScenario: "when creating new file in symlinked folder", + commandLineArgs: ["-w", "-p", ".", "--extendedDiagnostics"], + sys: () => { + const module1: File = { + path: `${projectRoot}/client/folder1/module1.ts`, + content: `export class Module1Class { }` + }; + const module2: File = { + path: `${projectRoot}/folder2/module2.ts`, + content: `import * as M from "folder1/module1";` + }; + const symlink: SymLink = { + path: `${projectRoot}/client/linktofolder2`, + symLink: `${projectRoot}/folder2`, + }; + const config: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + baseUrl: "client", + paths: { "*": ["*"] }, + }, + include: ["client/**/*", "folder2"] + }) + }; + return createWatchedSystem([module1, module2, symlink, config, libFile], { currentDirectory: projectRoot }); + }, + changes: [ + { + caption: "Add module3 to folder2", + change: sys => sys.writeFile(`${projectRoot}/client/linktofolder2/module3.ts`, `import * as M from "folder1/module1";`), + timeouts: checkSingleTimeoutQueueLengthAndRun, + }, + ] + }); }); } diff --git a/src/testRunner/unittests/tsserver/cachingFileSystemInformation.ts b/src/testRunner/unittests/tsserver/cachingFileSystemInformation.ts index b4a6c2f12f2..51b5ae9f5d3 100644 --- a/src/testRunner/unittests/tsserver/cachingFileSystemInformation.ts +++ b/src/testRunner/unittests/tsserver/cachingFileSystemInformation.ts @@ -713,5 +713,40 @@ namespace ts.projectSystem { checkProjectActualFiles(project, files.map(f => f.path)); assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(app.path).map(diag => diag.messageText), []); }); + + it("when creating new file in symlinked folder", () => { + const module1: File = { + path: `${tscWatch.projectRoot}/client/folder1/module1.ts`, + content: `export class Module1Class { }` + }; + const module2: File = { + path: `${tscWatch.projectRoot}/folder2/module2.ts`, + content: `import * as M from "folder1/module1";` + }; + const symlink: SymLink = { + path: `${tscWatch.projectRoot}/client/linktofolder2`, + symLink: `${tscWatch.projectRoot}/folder2`, + }; + const config: File = { + path: `${tscWatch.projectRoot}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + baseUrl: "client", + paths: { "*": ["*"] }, + }, + include: ["client/**/*", "folder2"] + }) + }; + const host = createServerHost([module1, module2, symlink, config, libFile]); + const service = createProjectService(host); + service.openClientFile(`${symlink.path}/module2.ts`); + checkNumberOfProjects(service, { configuredProjects: 1 }); + const project = Debug.checkDefined(service.configuredProjects.get(config.path)); + checkProjectActualFiles(project, [module1.path, `${symlink.path}/module2.ts`, config.path, libFile.path]); + host.writeFile(`${symlink.path}/module3.ts`, `import * as M from "folder1/module1";`); + host.runQueuedTimeoutCallbacks(); + checkNumberOfProjects(service, { configuredProjects: 1 }); + checkProjectActualFiles(project, [module1.path, `${symlink.path}/module2.ts`, config.path, libFile.path, `${symlink.path}/module3.ts`]); + }); }); } diff --git a/tests/baselines/reference/tscWatch/programUpdates/when-creating-new-file-in-symlinked-folder.js b/tests/baselines/reference/tscWatch/programUpdates/when-creating-new-file-in-symlinked-folder.js new file mode 100644 index 00000000000..867e21afbe4 --- /dev/null +++ b/tests/baselines/reference/tscWatch/programUpdates/when-creating-new-file-in-symlinked-folder.js @@ -0,0 +1,168 @@ +Input:: +//// [/user/username/projects/myproject/client/folder1/module1.ts] +export class Module1Class { } + +//// [/user/username/projects/myproject/folder2/module2.ts] +import * as M from "folder1/module1"; + +//// [/user/username/projects/myproject/client/linktofolder2] symlink(/user/username/projects/myproject/folder2) +//// [/user/username/projects/myproject/tsconfig.json] +{"compilerOptions":{"baseUrl":"client","paths":{"*":["*"]}},"include":["client/**/*","folder2"]} + +//// [/a/lib/lib.d.ts] +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } + + +/a/lib/tsc.js -w -p . --extendedDiagnostics +Output:: +[12:00:31 AM] Starting compilation in watch mode... + +Current directory: /user/username/projects/myproject CaseSensitiveFileNames: false +FileWatcher:: Added:: WatchInfo: /user/username/projects/myproject/tsconfig.json 2000 undefined Config file +Synchronizing program +CreatingProgramWith:: + roots: ["/user/username/projects/myproject/client/folder1/module1.ts","/user/username/projects/myproject/client/linktofolder2/module2.ts"] + options: {"baseUrl":"/user/username/projects/myproject/client","paths":{"*":["*"]},"pathsBasePath":"/user/username/projects/myproject","watch":true,"project":"/user/username/projects/myproject","extendedDiagnostics":true,"configFilePath":"/user/username/projects/myproject/tsconfig.json"} +FileWatcher:: Added:: WatchInfo: /user/username/projects/myproject/client/folder1/module1.ts 250 undefined Source file +FileWatcher:: Added:: WatchInfo: /user/username/projects/myproject/client/linktofolder2/module2.ts 250 undefined Source file +FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 250 undefined Source file +DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/myproject/node_modules/@types 1 undefined Type roots +Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/myproject/node_modules/@types 1 undefined Type roots +[12:00:37 AM] Found 0 errors. Watching for file changes. + +DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/myproject/client 1 undefined Wild card directory +Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/myproject/client 1 undefined Wild card directory +DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/myproject/folder2 1 undefined Wild card directory +Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/myproject/folder2 1 undefined Wild card directory + + +Program root files: ["/user/username/projects/myproject/client/folder1/module1.ts","/user/username/projects/myproject/client/linktofolder2/module2.ts"] +Program options: {"baseUrl":"/user/username/projects/myproject/client","paths":{"*":["*"]},"pathsBasePath":"/user/username/projects/myproject","watch":true,"project":"/user/username/projects/myproject","extendedDiagnostics":true,"configFilePath":"/user/username/projects/myproject/tsconfig.json"} +Program structureReused: Not +Program files:: +/a/lib/lib.d.ts +/user/username/projects/myproject/client/folder1/module1.ts +/user/username/projects/myproject/client/linktofolder2/module2.ts + +Semantic diagnostics in builder refreshed for:: +/a/lib/lib.d.ts +/user/username/projects/myproject/client/folder1/module1.ts +/user/username/projects/myproject/client/linktofolder2/module2.ts + +WatchedFiles:: +/user/username/projects/myproject/tsconfig.json: + {"fileName":"/user/username/projects/myproject/tsconfig.json","pollingInterval":250} +/user/username/projects/myproject/client/folder1/module1.ts: + {"fileName":"/user/username/projects/myproject/client/folder1/module1.ts","pollingInterval":250} +/user/username/projects/myproject/client/linktofolder2/module2.ts: + {"fileName":"/user/username/projects/myproject/client/linktofolder2/module2.ts","pollingInterval":250} +/a/lib/lib.d.ts: + {"fileName":"/a/lib/lib.d.ts","pollingInterval":250} + +FsWatches:: + +FsWatchesRecursive:: +/user/username/projects/myproject/node_modules/@types: + {"directoryName":"/user/username/projects/myproject/node_modules/@types","fallbackPollingInterval":500,"fallbackOptions":{"watchFile":"PriorityPollingInterval"}} +/user/username/projects/myproject/client: + {"directoryName":"/user/username/projects/myproject/client","fallbackPollingInterval":500,"fallbackOptions":{"watchFile":"PriorityPollingInterval"}} +/user/username/projects/myproject/folder2: + {"directoryName":"/user/username/projects/myproject/folder2","fallbackPollingInterval":500,"fallbackOptions":{"watchFile":"PriorityPollingInterval"}} + +exitCode:: ExitStatus.undefined + +//// [/user/username/projects/myproject/client/folder1/module1.js] +"use strict"; +exports.__esModule = true; +exports.Module1Class = void 0; +var Module1Class = /** @class */ (function () { + function Module1Class() { + } + return Module1Class; +}()); +exports.Module1Class = Module1Class; + + +//// [/user/username/projects/myproject/folder2/module2.js] +"use strict"; +exports.__esModule = true; + + + +Change:: Add module3 to folder2 + +Input:: +//// [/user/username/projects/myproject/folder2/module3.ts] +import * as M from "folder1/module1"; + + +Output:: +DirectoryWatcher:: Triggered with /user/username/projects/myproject/folder2/module3.ts :: WatchInfo: /user/username/projects/myproject/folder2 1 undefined Wild card directory +Scheduling update +Elapsed:: *ms DirectoryWatcher:: Triggered with /user/username/projects/myproject/folder2/module3.ts :: WatchInfo: /user/username/projects/myproject/folder2 1 undefined Wild card directory +[12:00:41 AM] File change detected. Starting incremental compilation... + +Reloading new file names and options +Synchronizing program +CreatingProgramWith:: + roots: ["/user/username/projects/myproject/client/folder1/module1.ts","/user/username/projects/myproject/client/linktofolder2/module2.ts","/user/username/projects/myproject/client/linktofolder2/module3.ts"] + options: {"baseUrl":"/user/username/projects/myproject/client","paths":{"*":["*"]},"pathsBasePath":"/user/username/projects/myproject","watch":true,"project":"/user/username/projects/myproject","extendedDiagnostics":true,"configFilePath":"/user/username/projects/myproject/tsconfig.json"} +FileWatcher:: Added:: WatchInfo: /user/username/projects/myproject/client/linktofolder2/module3.ts 250 undefined Source file +DirectoryWatcher:: Triggered with /user/username/projects/myproject/folder2/module3.js :: WatchInfo: /user/username/projects/myproject/folder2 1 undefined Wild card directory +Project: /user/username/projects/myproject/tsconfig.json Detected file add/remove of non supported extension: /user/username/projects/myproject/folder2/module3.js +Elapsed:: *ms DirectoryWatcher:: Triggered with /user/username/projects/myproject/folder2/module3.js :: WatchInfo: /user/username/projects/myproject/folder2 1 undefined Wild card directory +[12:00:45 AM] Found 0 errors. Watching for file changes. + + + +Program root files: ["/user/username/projects/myproject/client/folder1/module1.ts","/user/username/projects/myproject/client/linktofolder2/module2.ts","/user/username/projects/myproject/client/linktofolder2/module3.ts"] +Program options: {"baseUrl":"/user/username/projects/myproject/client","paths":{"*":["*"]},"pathsBasePath":"/user/username/projects/myproject","watch":true,"project":"/user/username/projects/myproject","extendedDiagnostics":true,"configFilePath":"/user/username/projects/myproject/tsconfig.json"} +Program structureReused: Not +Program files:: +/a/lib/lib.d.ts +/user/username/projects/myproject/client/folder1/module1.ts +/user/username/projects/myproject/client/linktofolder2/module2.ts +/user/username/projects/myproject/client/linktofolder2/module3.ts + +Semantic diagnostics in builder refreshed for:: +/user/username/projects/myproject/client/linktofolder2/module3.ts + +WatchedFiles:: +/user/username/projects/myproject/tsconfig.json: + {"fileName":"/user/username/projects/myproject/tsconfig.json","pollingInterval":250} +/user/username/projects/myproject/client/folder1/module1.ts: + {"fileName":"/user/username/projects/myproject/client/folder1/module1.ts","pollingInterval":250} +/user/username/projects/myproject/client/linktofolder2/module2.ts: + {"fileName":"/user/username/projects/myproject/client/linktofolder2/module2.ts","pollingInterval":250} +/a/lib/lib.d.ts: + {"fileName":"/a/lib/lib.d.ts","pollingInterval":250} +/user/username/projects/myproject/client/linktofolder2/module3.ts: + {"fileName":"/user/username/projects/myproject/client/linktofolder2/module3.ts","pollingInterval":250} + +FsWatches:: + +FsWatchesRecursive:: +/user/username/projects/myproject/node_modules/@types: + {"directoryName":"/user/username/projects/myproject/node_modules/@types","fallbackPollingInterval":500,"fallbackOptions":{"watchFile":"PriorityPollingInterval"}} +/user/username/projects/myproject/client: + {"directoryName":"/user/username/projects/myproject/client","fallbackPollingInterval":500,"fallbackOptions":{"watchFile":"PriorityPollingInterval"}} +/user/username/projects/myproject/folder2: + {"directoryName":"/user/username/projects/myproject/folder2","fallbackPollingInterval":500,"fallbackOptions":{"watchFile":"PriorityPollingInterval"}} + +exitCode:: ExitStatus.undefined + +//// [/user/username/projects/myproject/folder2/module3.js] +"use strict"; +exports.__esModule = true; + +