diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index 3bf80f19c85..77ba9a205f2 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -297,6 +297,8 @@ interface Array { length: number; [n: number]: T; }` ignoreDelete: boolean; /** Skip inode check on file or folder create*/ skipInodeCheckOnCreate: boolean; + /** When invoking rename event on fs watch, send event with file name suffixed with tilde */ + useTildeAsSuffixInRenameEventFileName: boolean; } export enum Tsc_WatchFile { @@ -490,11 +492,11 @@ interface Array { length: number; [n: number]: T; }` if (options && options.invokeDirectoryWatcherInsteadOfFileChanged) { const directoryFullPath = getDirectoryPath(currentEntry.fullPath); this.invokeFileWatcher(directoryFullPath, FileWatcherEventKind.Changed, /*useFileNameInCallback*/ true); - this.invokeFsWatchesCallbacks(directoryFullPath, "rename", currentEntry.fullPath); - this.invokeRecursiveFsWatches(directoryFullPath, "rename", currentEntry.fullPath); + this.invokeFsWatchesCallbacks(directoryFullPath, "rename", currentEntry.fullPath, options.useTildeAsSuffixInRenameEventFileName); + this.invokeRecursiveFsWatches(directoryFullPath, "rename", currentEntry.fullPath, options.useTildeAsSuffixInRenameEventFileName); } else { - this.invokeFileAndFsWatches(currentEntry.fullPath, FileWatcherEventKind.Changed); + this.invokeFileAndFsWatches(currentEntry.fullPath, FileWatcherEventKind.Changed, options?.useTildeAsSuffixInRenameEventFileName); } } } @@ -618,8 +620,8 @@ interface Array { length: number; [n: number]: T; }` } const inodeWatching = this.inodeWatching; if (options?.skipInodeCheckOnCreate) this.inodeWatching = false; - this.invokeFileAndFsWatches(fileOrDirectory.fullPath, FileWatcherEventKind.Created); - this.invokeFileAndFsWatches(folder.fullPath, FileWatcherEventKind.Changed); + this.invokeFileAndFsWatches(fileOrDirectory.fullPath, FileWatcherEventKind.Created, options?.useTildeAsSuffixInRenameEventFileName); + this.invokeFileAndFsWatches(folder.fullPath, FileWatcherEventKind.Changed, options?.useTildeAsSuffixInRenameEventFileName); this.inodeWatching = inodeWatching; } @@ -636,9 +638,9 @@ interface Array { length: number; [n: number]: T; }` if (isFsFolder(fileOrDirectory)) { Debug.assert(fileOrDirectory.entries.length === 0 || isRenaming); } - if (!options?.ignoreDelete) this.invokeFileAndFsWatches(fileOrDirectory.fullPath, FileWatcherEventKind.Deleted); + if (!options?.ignoreDelete) this.invokeFileAndFsWatches(fileOrDirectory.fullPath, FileWatcherEventKind.Deleted, options?.useTildeAsSuffixInRenameEventFileName); this.inodes?.delete(fileOrDirectory.path); - if (!options?.ignoreDelete) this.invokeFileAndFsWatches(baseFolder.fullPath, FileWatcherEventKind.Changed); + if (!options?.ignoreDelete) this.invokeFileAndFsWatches(baseFolder.fullPath, FileWatcherEventKind.Changed, options?.useTildeAsSuffixInRenameEventFileName); } deleteFile(filePath: string) { @@ -700,45 +702,47 @@ interface Array { length: number; [n: number]: T; }` invokeWatcherCallbacks(this.watchedFiles.get(this.toPath(fileFullPath)), ({ cb, fileName }) => cb(useFileNameInCallback ? fileName : fileFullPath, eventKind)); } - private fsWatchCallback(map: MultiMap, fullPath: string, eventName: "rename" | "change", entryFullPath?: string) { + private fsWatchCallback(map: MultiMap, fullPath: string, eventName: "rename" | "change", entryFullPath: string | undefined, useTildeSuffix: boolean | undefined) { const path = this.toPath(fullPath); const currentInode = this.inodes?.get(path); invokeWatcherCallbacks(map.get(path), ({ cb, inode }) => { // TODO:: if (this.inodeWatching && inode !== undefined && inode !== currentInode) return; - cb(eventName, entryFullPath ? this.getRelativePathToDirectory(fullPath, entryFullPath) : ""); + let relativeFileName = (entryFullPath ? this.getRelativePathToDirectory(fullPath, entryFullPath) : ""); + if (useTildeSuffix) relativeFileName = (relativeFileName ? relativeFileName : getBaseFileName(fullPath)) + "~"; + cb(eventName, relativeFileName); }); } - invokeFsWatchesCallbacks(fullPath: string, eventName: "rename" | "change", entryFullPath?: string) { - this.fsWatchCallback(this.fsWatches, fullPath, eventName, entryFullPath); + invokeFsWatchesCallbacks(fullPath: string, eventName: "rename" | "change", entryFullPath?: string, useTildeSuffix?: boolean) { + this.fsWatchCallback(this.fsWatches, fullPath, eventName, entryFullPath, useTildeSuffix); } - invokeFsWatchesRecursiveCallbacks(fullPath: string, eventName: "rename" | "change", entryFullPath?: string) { - this.fsWatchCallback(this.fsWatchesRecursive, fullPath, eventName, entryFullPath); + invokeFsWatchesRecursiveCallbacks(fullPath: string, eventName: "rename" | "change", entryFullPath?: string, useTildeSuffix?: boolean) { + this.fsWatchCallback(this.fsWatchesRecursive, fullPath, eventName, entryFullPath, useTildeSuffix); } private getRelativePathToDirectory(directoryFullPath: string, fileFullPath: string) { return getRelativePathToDirectoryOrUrl(directoryFullPath, fileFullPath, this.currentDirectory, this.getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); } - private invokeRecursiveFsWatches(fullPath: string, eventName: "rename" | "change", entryFullPath?: string) { - this.invokeFsWatchesRecursiveCallbacks(fullPath, eventName, entryFullPath); + private invokeRecursiveFsWatches(fullPath: string, eventName: "rename" | "change", entryFullPath?: string, useTildeSuffix?: boolean) { + this.invokeFsWatchesRecursiveCallbacks(fullPath, eventName, entryFullPath, useTildeSuffix); const basePath = getDirectoryPath(fullPath); if (this.getCanonicalFileName(fullPath) !== this.getCanonicalFileName(basePath)) { - this.invokeRecursiveFsWatches(basePath, eventName, entryFullPath || fullPath); + this.invokeRecursiveFsWatches(basePath, eventName, entryFullPath || fullPath, useTildeSuffix); } } - private invokeFsWatches(fullPath: string, eventName: "rename" | "change") { - this.invokeFsWatchesCallbacks(fullPath, eventName); - this.invokeFsWatchesCallbacks(getDirectoryPath(fullPath), eventName, fullPath); - this.invokeRecursiveFsWatches(fullPath, eventName); + private invokeFsWatches(fullPath: string, eventName: "rename" | "change", useTildeSuffix: boolean | undefined) { + this.invokeFsWatchesCallbacks(fullPath, eventName, fullPath, useTildeSuffix); + this.invokeFsWatchesCallbacks(getDirectoryPath(fullPath), eventName, fullPath, useTildeSuffix); + this.invokeRecursiveFsWatches(fullPath, eventName, /*entryFullPath*/ undefined, useTildeSuffix); } - private invokeFileAndFsWatches(fileOrFolderFullPath: string, eventKind: FileWatcherEventKind) { + private invokeFileAndFsWatches(fileOrFolderFullPath: string, eventKind: FileWatcherEventKind, useTildeSuffix?: boolean) { this.invokeFileWatcher(fileOrFolderFullPath, eventKind); - this.invokeFsWatches(fileOrFolderFullPath, eventKind === FileWatcherEventKind.Changed ? "change" : "rename"); + this.invokeFsWatches(fileOrFolderFullPath, eventKind === FileWatcherEventKind.Changed ? "change" : "rename", useTildeSuffix); } private toFsEntry(path: string): FSEntryBase { diff --git a/src/testRunner/unittests/tscWatch/watchEnvironment.ts b/src/testRunner/unittests/tscWatch/watchEnvironment.ts index 7428d9d1a15..8eee88bae65 100644 --- a/src/testRunner/unittests/tscWatch/watchEnvironment.ts +++ b/src/testRunner/unittests/tscWatch/watchEnvironment.ts @@ -648,6 +648,36 @@ namespace ts.tscWatch { ] }); + verifyTscWatch({ + scenario, + subScenario: `fsWatch/when using file watching thats on inode when rename event ends with tilde`, + commandLineArgs: ["-w", "--extendedDiagnostics"], + sys: () => createWatchedSystem( + { + [libFile.path]: libFile.content, + [`${projectRoot}/main.ts`]: `import { foo } from "./foo"; foo();`, + [`${projectRoot}/foo.d.ts`]: `export function foo(): string;`, + [`${projectRoot}/tsconfig.json`]: JSON.stringify({ watchOptions: { watchFile: "useFsEvents" }, files: ["foo.d.ts", "main.ts"] }), + }, + { + currentDirectory: projectRoot, + inodeWatching: true + } + ), + changes: [ + { + caption: "Replace file with rename event that introduces error", + change: sys => sys.modifyFile(`${projectRoot}/foo.d.ts`, `export function foo2(): string;`, { invokeFileDeleteCreateAsPartInsteadOfChange: true, useTildeAsSuffixInRenameEventFileName: true }), + timeouts: sys => sys.checkTimeoutQueueLengthAndRun(1), + }, + { + caption: "Replace file with rename event that fixes error", + change: sys => sys.modifyFile(`${projectRoot}/foo.d.ts`, `export function foo(): string;`, { invokeFileDeleteCreateAsPartInsteadOfChange: true, useTildeAsSuffixInRenameEventFileName: true }), + timeouts: sys => sys.checkTimeoutQueueLengthAndRun(0), + }, + ] + }); + verifyTscWatch({ scenario, subScenario: `fsWatch/when using file watching thats on inode when rename occurs when file is still on the disk`, diff --git a/tests/baselines/reference/tscWatch/watchEnvironment/fsWatch/when-using-file-watching-thats-on-inode-when-rename-event-ends-with-tilde.js b/tests/baselines/reference/tscWatch/watchEnvironment/fsWatch/when-using-file-watching-thats-on-inode-when-rename-event-ends-with-tilde.js new file mode 100644 index 00000000000..91667a046e0 --- /dev/null +++ b/tests/baselines/reference/tscWatch/watchEnvironment/fsWatch/when-using-file-watching-thats-on-inode-when-rename-event-ends-with-tilde.js @@ -0,0 +1,200 @@ +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.d.ts] Inode:: 9 +export function foo(): string; + +//// [/user/username/projects/myproject/tsconfig.json] Inode:: 10 +{"watchOptions":{"watchFile":"useFsEvents"},"files":["foo.d.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.d.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.d.ts 250 {"watchFile":4} Source file +FileWatcher:: Added:: WatchInfo: /user/username/projects/myproject/main.ts 250 {"watchFile":4} Source file +DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/myproject 0 {"watchFile":4} Failed Lookup Locations +Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/myproject 0 {"watchFile":4} Failed Lookup Locations +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 +DirectoryWatcher:: Triggered with /user/username/projects/myproject/main.js :: WatchInfo: /user/username/projects/myproject 0 {"watchFile":4} Failed Lookup Locations +Elapsed:: *ms DirectoryWatcher:: Triggered with /user/username/projects/myproject/main.js :: WatchInfo: /user/username/projects/myproject 0 {"watchFile":4} Failed Lookup Locations +[12:00:26 AM] Found 0 errors. Watching for file changes. + + + +Program root files: ["/user/username/projects/myproject/foo.d.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.d.ts +/user/username/projects/myproject/main.ts + +Semantic diagnostics in builder refreshed for:: +/a/lib/lib.d.ts +/user/username/projects/myproject/foo.d.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.d.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.d.ts: + {"directoryName":"/user/username/projects/myproject/foo.d.ts","inode":9} +/user/username/projects/myproject/main.ts: + {"directoryName":"/user/username/projects/myproject/main.ts","inode":8} +/user/username/projects/myproject: + {"directoryName":"/user/username/projects/myproject","inode":7} +/a/lib/lib.d.ts: + {"directoryName":"/a/lib/lib.d.ts","inode":3} + +FsWatchesRecursive:: + +exitCode:: ExitStatus.undefined + +//// [/user/username/projects/myproject/main.js] Inode:: 11 +"use strict"; +exports.__esModule = true; +var foo_1 = require("./foo"); +(0, foo_1.foo)(); + + + +Change:: Replace file with rename event that introduces error + +Input:: +//// [/user/username/projects/myproject/foo.d.ts] Inode:: 12 +export function foo2(): string; + + +Output:: +FileWatcher:: Triggered with /user/username/projects/myproject/foo.d.ts 2:: WatchInfo: /user/username/projects/myproject/foo.d.ts 250 {"watchFile":4} Source file +Scheduling update +Elapsed:: *ms FileWatcher:: Triggered with /user/username/projects/myproject/foo.d.ts 2:: WatchInfo: /user/username/projects/myproject/foo.d.ts 250 {"watchFile":4} Source file +DirectoryWatcher:: Triggered with /user/username/projects/myproject/foo.d.ts~ :: WatchInfo: /user/username/projects/myproject 0 {"watchFile":4} Failed Lookup Locations +Elapsed:: *ms DirectoryWatcher:: Triggered with /user/username/projects/myproject/foo.d.ts~ :: WatchInfo: /user/username/projects/myproject 0 {"watchFile":4} Failed Lookup Locations +DirectoryWatcher:: Triggered with /user/username/projects/myproject/foo.d.ts~ :: WatchInfo: /user/username/projects/myproject 0 {"watchFile":4} Failed Lookup Locations +Elapsed:: *ms DirectoryWatcher:: Triggered with /user/username/projects/myproject/foo.d.ts~ :: WatchInfo: /user/username/projects/myproject 0 {"watchFile":4} Failed Lookup Locations +Synchronizing program +[12:00:30 AM] File change detected. Starting incremental compilation... + +CreatingProgramWith:: + roots: ["/user/username/projects/myproject/foo.d.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.d.ts:1:17 + 1 export function foo2(): string; +    ~~~~ + 'foo2' is declared here. + +[12:00:34 AM] Found 1 error. Watching for file changes. + + + +Program root files: ["/user/username/projects/myproject/foo.d.ts","/user/username/projects/myproject/main.ts"] +Program options: {"watch":true,"extendedDiagnostics":true,"configFilePath":"/user/username/projects/myproject/tsconfig.json"} +Program structureReused: SafeModules +Program files:: +/a/lib/lib.d.ts +/user/username/projects/myproject/foo.d.ts +/user/username/projects/myproject/main.ts + +Semantic diagnostics in builder refreshed for:: +/user/username/projects/myproject/foo.d.ts +/user/username/projects/myproject/main.ts + +Shape signatures in builder refreshed for:: +/user/username/projects/myproject/foo.d.ts (used version) +/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.d.ts: + {"directoryName":"/user/username/projects/myproject/foo.d.ts","inode":9} +/user/username/projects/myproject/main.ts: + {"directoryName":"/user/username/projects/myproject/main.ts","inode":8} +/user/username/projects/myproject: + {"directoryName":"/user/username/projects/myproject","inode":7} +/a/lib/lib.d.ts: + {"directoryName":"/a/lib/lib.d.ts","inode":3} + +FsWatchesRecursive:: + +exitCode:: ExitStatus.undefined + +//// [/user/username/projects/myproject/main.js] file written with same contents Inode:: 11 + +Change:: Replace file with rename event that fixes error + +Input:: +//// [/user/username/projects/myproject/foo.d.ts] Inode:: 13 +export function foo(): string; + + +Output:: +DirectoryWatcher:: Triggered with /user/username/projects/myproject/foo.d.ts~ :: WatchInfo: /user/username/projects/myproject 0 {"watchFile":4} Failed Lookup Locations +Elapsed:: *ms DirectoryWatcher:: Triggered with /user/username/projects/myproject/foo.d.ts~ :: WatchInfo: /user/username/projects/myproject 0 {"watchFile":4} Failed Lookup Locations +DirectoryWatcher:: Triggered with /user/username/projects/myproject/foo.d.ts~ :: WatchInfo: /user/username/projects/myproject 0 {"watchFile":4} Failed Lookup Locations +Elapsed:: *ms DirectoryWatcher:: Triggered with /user/username/projects/myproject/foo.d.ts~ :: WatchInfo: /user/username/projects/myproject 0 {"watchFile":4} Failed Lookup Locations + + +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.d.ts: + {"directoryName":"/user/username/projects/myproject/foo.d.ts","inode":9} +/user/username/projects/myproject/main.ts: + {"directoryName":"/user/username/projects/myproject/main.ts","inode":8} +/user/username/projects/myproject: + {"directoryName":"/user/username/projects/myproject","inode":7} +/a/lib/lib.d.ts: + {"directoryName":"/a/lib/lib.d.ts","inode":3} + +FsWatchesRecursive:: + +exitCode:: ExitStatus.undefined +