diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index c034258533b..1b6bfc9c6ee 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -267,13 +267,17 @@ interface Array { length: number; [n: number]: T; }` inode: number | undefined; } - export interface ReloadWatchInvokeOptions { + export interface WatchInvokeOptions { /** Invokes the directory watcher for the parent instead of the file changed */ invokeDirectoryWatcherInsteadOfFileChanged: boolean; /** When new file is created, do not invoke watches for it */ ignoreWatchInvokedWithTriggerAsFileCreate: boolean; /** Invoke the file delete, followed by create instead of file changed */ invokeFileDeleteCreateAsPartInsteadOfChange: boolean; + /** Dont invoke delete watches */ + ignoreDelete: boolean; + /** Skip inode check on file or folder create*/ + skipInodeCheckOnCreate: boolean; } export enum Tsc_WatchFile { @@ -324,7 +328,7 @@ interface Array { length: number; [n: number]: T; }` public defaultWatchFileKind?: () => WatchFileKind | undefined; public storeFilesChangingSignatureDuringEmit = true; watchFile: HostWatchFile; - private readonly inodeWatching: boolean | undefined; + private inodeWatching: boolean | undefined; private readonly inodes?: ESMap; watchDirectory: HostWatchDirectory; constructor( @@ -376,7 +380,7 @@ interface Array { length: number; [n: number]: T; }` tscWatchDirectory, defaultWatchFileKind: () => this.defaultWatchFileKind?.(), inodeWatching: !!this.inodeWatching, - sysLog: this.write.bind(this) + sysLog: s => this.write(s + this.newLine), }); this.watchFile = watchFile; this.watchDirectory = watchDirectory; @@ -441,7 +445,7 @@ interface Array { length: number; [n: number]: T; }` } } - modifyFile(filePath: string, content: string, options?: Partial) { + modifyFile(filePath: string, content: string, options?: Partial) { const path = this.toFullPath(filePath); const currentEntry = this.fs.get(path); if (!currentEntry || !isFsFile(currentEntry)) { @@ -449,8 +453,8 @@ interface Array { length: number; [n: number]: T; }` } if (options && options.invokeFileDeleteCreateAsPartInsteadOfChange) { - this.removeFileOrFolder(currentEntry, returnFalse); - this.ensureFileOrFolder({ path: filePath, content }); + this.removeFileOrFolder(currentEntry, /*isRenaming*/ false, options); + this.ensureFileOrFolder({ path: filePath, content }, /*ignoreWatchInvokedWithTriggerAsFileCreate*/ undefined, /*ignoreParentWatch*/ undefined, options); } else { currentEntry.content = content; @@ -475,7 +479,7 @@ interface Array { length: number; [n: number]: T; }` Debug.assert(!!file); // Only remove the file - this.removeFileOrFolder(file, returnFalse, /*isRenaming*/ true); + this.removeFileOrFolder(file, /*isRenaming*/ true); // Add updated folder with new folder name const newFullPath = getNormalizedAbsolutePath(newFileName, this.currentDirectory); @@ -495,7 +499,7 @@ interface Array { length: number; [n: number]: T; }` Debug.assert(!!folder); // Only remove the folder - this.removeFileOrFolder(folder, returnFalse, /*isRenaming*/ true); + this.removeFileOrFolder(folder, /*isRenaming*/ true); // Add updated folder with new folder name const newFullPath = getNormalizedAbsolutePath(newFolderName, this.currentDirectory); @@ -530,29 +534,29 @@ interface Array { length: number; [n: number]: T; }` } } - ensureFileOrFolder(fileOrDirectoryOrSymLink: FileOrFolderOrSymLink, ignoreWatchInvokedWithTriggerAsFileCreate?: boolean, ignoreParentWatch?: boolean) { + ensureFileOrFolder(fileOrDirectoryOrSymLink: FileOrFolderOrSymLink, ignoreWatchInvokedWithTriggerAsFileCreate?: boolean, ignoreParentWatch?: boolean, options?: Partial) { if (isFile(fileOrDirectoryOrSymLink)) { const file = this.toFsFile(fileOrDirectoryOrSymLink); // file may already exist when updating existing type declaration file if (!this.fs.get(file.path)) { - const baseFolder = this.ensureFolder(getDirectoryPath(file.fullPath), ignoreParentWatch); - this.addFileOrFolderInFolder(baseFolder, file, ignoreWatchInvokedWithTriggerAsFileCreate); + const baseFolder = this.ensureFolder(getDirectoryPath(file.fullPath), ignoreParentWatch, options); + this.addFileOrFolderInFolder(baseFolder, file, ignoreWatchInvokedWithTriggerAsFileCreate, options); } } else if (isSymLink(fileOrDirectoryOrSymLink)) { const symLink = this.toFsSymLink(fileOrDirectoryOrSymLink); Debug.assert(!this.fs.get(symLink.path)); - const baseFolder = this.ensureFolder(getDirectoryPath(symLink.fullPath), ignoreParentWatch); - this.addFileOrFolderInFolder(baseFolder, symLink, ignoreWatchInvokedWithTriggerAsFileCreate); + const baseFolder = this.ensureFolder(getDirectoryPath(symLink.fullPath), ignoreParentWatch, options); + this.addFileOrFolderInFolder(baseFolder, symLink, ignoreWatchInvokedWithTriggerAsFileCreate, options); } else { const fullPath = getNormalizedAbsolutePath(fileOrDirectoryOrSymLink.path, this.currentDirectory); - this.ensureFolder(getDirectoryPath(fullPath), ignoreParentWatch); - this.ensureFolder(fullPath, ignoreWatchInvokedWithTriggerAsFileCreate); + this.ensureFolder(getDirectoryPath(fullPath), ignoreParentWatch, options); + this.ensureFolder(fullPath, ignoreWatchInvokedWithTriggerAsFileCreate, options); } } - private ensureFolder(fullPath: string, ignoreWatch: boolean | undefined): FsFolder { + private ensureFolder(fullPath: string, ignoreWatch: boolean | undefined, options: Partial | undefined): FsFolder { const path = this.toPath(fullPath); let folder = this.fs.get(path) as FsFolder; if (!folder) { @@ -560,8 +564,8 @@ interface Array { length: number; [n: number]: T; }` const baseFullPath = getDirectoryPath(fullPath); if (fullPath !== baseFullPath) { // Add folder in the base folder - const baseFolder = this.ensureFolder(baseFullPath, ignoreWatch); - this.addFileOrFolderInFolder(baseFolder, folder, ignoreWatch); + const baseFolder = this.ensureFolder(baseFullPath, ignoreWatch, options); + this.addFileOrFolderInFolder(baseFolder, folder, ignoreWatch, options); } else { // root folder @@ -574,7 +578,7 @@ interface Array { length: number; [n: number]: T; }` return folder; } - private addFileOrFolderInFolder(folder: FsFolder, fileOrDirectory: FsFile | FsFolder | FsSymLink, ignoreWatch?: boolean) { + private addFileOrFolderInFolder(folder: FsFolder, fileOrDirectory: FsFile | FsFolder | FsSymLink, ignoreWatch?: boolean, options?: Partial) { if (!this.fs.has(fileOrDirectory.path)) { insertSorted(folder.entries, fileOrDirectory, (a, b) => compareStringsCaseSensitive(getBaseFileName(a.path), getBaseFileName(b.path))); } @@ -585,11 +589,14 @@ interface Array { length: number; [n: number]: T; }` if (ignoreWatch) { return; } + const inodeWatching = this.inodeWatching; + if (options?.skipInodeCheckOnCreate) this.inodeWatching = false; this.invokeFileAndFsWatches(fileOrDirectory.fullPath, FileWatcherEventKind.Created); this.invokeFileAndFsWatches(folder.fullPath, FileWatcherEventKind.Changed); + this.inodeWatching = inodeWatching; } - private removeFileOrFolder(fileOrDirectory: FsFile | FsFolder | FsSymLink, isRemovableLeafFolder: (folder: FsFolder) => boolean, isRenaming = false) { + private removeFileOrFolder(fileOrDirectory: FsFile | FsFolder | FsSymLink, isRenaming?: boolean, options?: Partial) { const basePath = getDirectoryPath(fileOrDirectory.path); const baseFolder = this.fs.get(basePath) as FsFolder; if (basePath !== fileOrDirectory.path) { @@ -602,21 +609,16 @@ interface Array { length: number; [n: number]: T; }` if (isFsFolder(fileOrDirectory)) { Debug.assert(fileOrDirectory.entries.length === 0 || isRenaming); } - this.invokeFileAndFsWatches(fileOrDirectory.fullPath, FileWatcherEventKind.Deleted); + if (!options?.ignoreDelete) this.invokeFileAndFsWatches(fileOrDirectory.fullPath, FileWatcherEventKind.Deleted); this.inodes?.delete(fileOrDirectory.path); - this.invokeFileAndFsWatches(baseFolder.fullPath, FileWatcherEventKind.Changed); - if (basePath !== fileOrDirectory.path && - baseFolder.entries.length === 0 && - isRemovableLeafFolder(baseFolder)) { - this.removeFileOrFolder(baseFolder, isRemovableLeafFolder); - } + if (!options?.ignoreDelete) this.invokeFileAndFsWatches(baseFolder.fullPath, FileWatcherEventKind.Changed); } deleteFile(filePath: string) { const path = this.toFullPath(filePath); const currentEntry = this.fs.get(path) as FsFile; Debug.assert(isFsFile(currentEntry)); - this.removeFileOrFolder(currentEntry, returnFalse); + this.removeFileOrFolder(currentEntry); } deleteFolder(folderPath: string, recursive?: boolean) { @@ -630,11 +632,11 @@ interface Array { length: number; [n: number]: T; }` this.deleteFolder(fsEntry.fullPath, recursive); } else { - this.removeFileOrFolder(fsEntry, returnFalse); + this.removeFileOrFolder(fsEntry); } }); } - this.removeFileOrFolder(currentEntry, returnFalse); + this.removeFileOrFolder(currentEntry); } private watchFileWorker(fileName: string, cb: FileWatcherCallback, pollingInterval: PollingInterval) { @@ -947,11 +949,11 @@ interface Array { length: number; [n: number]: T; }` } } - prependFile(path: string, content: string, options?: Partial): void { + prependFile(path: string, content: string, options?: Partial): void { this.modifyFile(path, content + this.readFile(path), options); } - appendFile(path: string, content: string, options?: Partial): void { + appendFile(path: string, content: string, options?: Partial): void { this.modifyFile(path, this.readFile(path) + content, options); } diff --git a/src/testRunner/unittests/tscWatch/watchEnvironment.ts b/src/testRunner/unittests/tscWatch/watchEnvironment.ts index 5cc92d55b5f..aa3290baf40 100644 --- a/src/testRunner/unittests/tscWatch/watchEnvironment.ts +++ b/src/testRunner/unittests/tscWatch/watchEnvironment.ts @@ -612,6 +612,45 @@ namespace ts.tscWatch { }, ] }); + + verifyTscWatch({ + scenario, + subScenario: `fsWatch/when using file watching thats on inode when rename occurs when file is still on the disk`, + commandLineArgs: ["-w", "--extendedDiagnostics"], + sys: () => createWatchedSystem( + { + [libFile.path]: libFile.content, + [`${projectRoot}/main.ts`]: `import { foo } from "./foo"; foo();`, + [`${projectRoot}/foo.ts`]: `export declare function foo(): string;`, + [`${projectRoot}/tsconfig.json`]: JSON.stringify({ + watchOptions: { watchFile: "useFsEvents" }, + files: ["foo.ts", "main.ts"] + }), + }, + { + currentDirectory: projectRoot, + inodeWatching: true, + } + ), + changes: [ + { + caption: "Introduce error such that when callback happens file is already appeared", + // vm's wq generates this kind of event + // Skip delete event so inode changes but when the create's rename occurs file is on disk + change: sys => sys.modifyFile(`${projectRoot}/foo.ts`, `export declare function foo2(): string;`, { + invokeFileDeleteCreateAsPartInsteadOfChange: true, + ignoreDelete: true, + skipInodeCheckOnCreate: true + }), + timeouts: sys => sys.checkTimeoutQueueLengthAndRun(1), + }, + { + caption: "Replace file with rename event that fixes error", + change: sys => sys.modifyFile(`${projectRoot}/foo.ts`, `export declare function foo(): string;`, { invokeFileDeleteCreateAsPartInsteadOfChange: true, }), + timeouts: sys => sys.checkTimeoutQueueLengthAndRun(0), + }, + ] + }); }); }); } diff --git a/tests/baselines/reference/tscWatch/watchEnvironment/fsWatch/when-using-file-watching-thats-on-inode-when-rename-occurs-when-file-is-still-on-the-disk.js b/tests/baselines/reference/tscWatch/watchEnvironment/fsWatch/when-using-file-watching-thats-on-inode-when-rename-occurs-when-file-is-still-on-the-disk.js new file mode 100644 index 00000000000..252b7eec6ef --- /dev/null +++ b/tests/baselines/reference/tscWatch/watchEnvironment/fsWatch/when-using-file-watching-thats-on-inode-when-rename-occurs-when-file-is-still-on-the-disk.js @@ -0,0 +1,187 @@ +Input:: +//// [/a/lib/lib.d.ts] Inode:: 3 +/// +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; } + +//// [/user/username/projects/myproject/main.ts] Inode:: 8 +import { foo } from "./foo"; foo(); + +//// [/user/username/projects/myproject/foo.ts] Inode:: 9 +export declare function foo(): string; + +//// [/user/username/projects/myproject/tsconfig.json] Inode:: 10 +{"watchOptions":{"watchFile":"useFsEvents"},"files":["foo.ts","main.ts"]} + + +/a/lib/tsc.js -w --extendedDiagnostics +Output:: +[12:00:23 AM] Starting compilation in watch mode... + +Current directory: /user/username/projects/myproject CaseSensitiveFileNames: false +FileWatcher:: Added:: WatchInfo: /user/username/projects/myproject/tsconfig.json 2000 {"watchFile":4} Config file +Synchronizing program +CreatingProgramWith:: + roots: ["/user/username/projects/myproject/foo.ts","/user/username/projects/myproject/main.ts"] + options: {"watch":true,"extendedDiagnostics":true,"configFilePath":"/user/username/projects/myproject/tsconfig.json"} +FileWatcher:: Added:: WatchInfo: /user/username/projects/myproject/foo.ts 250 {"watchFile":4} Source file +FileWatcher:: Added:: WatchInfo: /user/username/projects/myproject/main.ts 250 {"watchFile":4} Source file +FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 250 {"watchFile":4} Source file +DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/myproject/node_modules/@types 1 {"watchFile":4} Type roots +Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/myproject/node_modules/@types 1 {"watchFile":4} Type roots +[12:00:28 AM] Found 0 errors. Watching for file changes. + + + +Program root files: ["/user/username/projects/myproject/foo.ts","/user/username/projects/myproject/main.ts"] +Program options: {"watch":true,"extendedDiagnostics":true,"configFilePath":"/user/username/projects/myproject/tsconfig.json"} +Program structureReused: Not +Program files:: +/a/lib/lib.d.ts +/user/username/projects/myproject/foo.ts +/user/username/projects/myproject/main.ts + +Semantic diagnostics in builder refreshed for:: +/a/lib/lib.d.ts +/user/username/projects/myproject/foo.ts +/user/username/projects/myproject/main.ts + +Shape signatures in builder refreshed for:: +/a/lib/lib.d.ts (used version) +/user/username/projects/myproject/foo.ts (used version) +/user/username/projects/myproject/main.ts (used version) + +WatchedFiles:: +/user/username/projects/myproject/node_modules/@types: + {"fileName":"/user/username/projects/myproject/node_modules/@types","pollingInterval":500} + +FsWatches:: +/user/username/projects/myproject/tsconfig.json: + {"directoryName":"/user/username/projects/myproject/tsconfig.json","inode":10} +/user/username/projects/myproject/foo.ts: + {"directoryName":"/user/username/projects/myproject/foo.ts","inode":9} +/user/username/projects/myproject/main.ts: + {"directoryName":"/user/username/projects/myproject/main.ts","inode":8} +/a/lib/lib.d.ts: + {"directoryName":"/a/lib/lib.d.ts","inode":3} + +FsWatchesRecursive:: + +exitCode:: ExitStatus.undefined + +//// [/user/username/projects/myproject/foo.js] Inode:: 11 +"use strict"; +exports.__esModule = true; + + +//// [/user/username/projects/myproject/main.js] Inode:: 12 +"use strict"; +exports.__esModule = true; +var foo_1 = require("./foo"); +(0, foo_1.foo)(); + + + +Change:: Introduce error such that when callback happens file is already appeared + +Input:: +//// [/user/username/projects/myproject/foo.ts] Inode:: 13 +export declare function foo2(): string; + + +Output:: +FileWatcher:: Triggered with /user/username/projects/myproject/foo.ts 0:: WatchInfo: /user/username/projects/myproject/foo.ts 250 {"watchFile":4} Source file +Scheduling update +Elapsed:: *ms FileWatcher:: Triggered with /user/username/projects/myproject/foo.ts 0:: WatchInfo: /user/username/projects/myproject/foo.ts 250 {"watchFile":4} Source file +Synchronizing program +[12:00:32 AM] File change detected. Starting incremental compilation... + +CreatingProgramWith:: + roots: ["/user/username/projects/myproject/foo.ts","/user/username/projects/myproject/main.ts"] + options: {"watch":true,"extendedDiagnostics":true,"configFilePath":"/user/username/projects/myproject/tsconfig.json"} +main.ts:1:10 - error TS2724: '"./foo"' has no exported member named 'foo'. Did you mean 'foo2'? + +1 import { foo } from "./foo"; foo(); +   ~~~ + + foo.ts:1:25 + 1 export declare function foo2(): string; +    ~~~~ + 'foo2' is declared here. + +[12:00:39 AM] Found 1 error. Watching for file changes. + + + +Program root files: ["/user/username/projects/myproject/foo.ts","/user/username/projects/myproject/main.ts"] +Program options: {"watch":true,"extendedDiagnostics":true,"configFilePath":"/user/username/projects/myproject/tsconfig.json"} +Program structureReused: Completely +Program files:: +/a/lib/lib.d.ts +/user/username/projects/myproject/foo.ts +/user/username/projects/myproject/main.ts + +Semantic diagnostics in builder refreshed for:: +/user/username/projects/myproject/foo.ts +/user/username/projects/myproject/main.ts + +Shape signatures in builder refreshed for:: +/user/username/projects/myproject/foo.ts (computed .d.ts) +/user/username/projects/myproject/main.ts (computed .d.ts) + +WatchedFiles:: +/user/username/projects/myproject/node_modules/@types: + {"fileName":"/user/username/projects/myproject/node_modules/@types","pollingInterval":500} + +FsWatches:: +/user/username/projects/myproject/tsconfig.json: + {"directoryName":"/user/username/projects/myproject/tsconfig.json","inode":10} +/user/username/projects/myproject/foo.ts: + {"directoryName":"/user/username/projects/myproject/foo.ts","inode":9} +/user/username/projects/myproject/main.ts: + {"directoryName":"/user/username/projects/myproject/main.ts","inode":8} +/a/lib/lib.d.ts: + {"directoryName":"/a/lib/lib.d.ts","inode":3} + +FsWatchesRecursive:: + +exitCode:: ExitStatus.undefined + +//// [/user/username/projects/myproject/foo.js] file written with same contents Inode:: 11 +//// [/user/username/projects/myproject/main.js] file written with same contents Inode:: 12 + +Change:: Replace file with rename event that fixes error + +Input:: +//// [/user/username/projects/myproject/foo.ts] Inode:: 14 +export declare function foo(): string; + + +Output:: + +WatchedFiles:: +/user/username/projects/myproject/node_modules/@types: + {"fileName":"/user/username/projects/myproject/node_modules/@types","pollingInterval":500} + +FsWatches:: +/user/username/projects/myproject/tsconfig.json: + {"directoryName":"/user/username/projects/myproject/tsconfig.json","inode":10} +/user/username/projects/myproject/foo.ts: + {"directoryName":"/user/username/projects/myproject/foo.ts","inode":9} +/user/username/projects/myproject/main.ts: + {"directoryName":"/user/username/projects/myproject/main.ts","inode":8} +/a/lib/lib.d.ts: + {"directoryName":"/a/lib/lib.d.ts","inode":3} + +FsWatchesRecursive:: + +exitCode:: ExitStatus.undefined +