You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This bug is reproducible between TypeScript 5.1.6 and 5.3.0-nightly (commit b555727).
I can test versions prior to 5.1.6 if necessary.
⏯ Playground Link
This bug is specific to a local editing environment and file system quirks. A playground would not be able reproduce this.
🪜 Steps to Reproduce
A few developers on my team have noticed that IntelliSense operations intermittently do not work in newly created files in VS Code. IntelliSense works in the new file after restarting TSServer.
Breakpoint debugging TSServer, I believe there's a file system caching bug related to case sensitivity. The bug can be reproduced consistently on macOS.
Open a directory in VS Code with a tsconfig.json file on a case-sensitive volume.
Ensure the TypeScript version is set to "Use VS Code's Version". The problem only reproduces when tsserver.js is on a case-insensitive volume, which is usually true for /Applications/Visual Studio Code.app/Contents/Resources/app/extensions/node_modules/typescript/lib/tsserver.js.
Create a new file with at least one capital letter in its name. (e.g. Foo.tsx).
Observe that IntelliSense operations performed within the new Foo.tsx file do not have information on other files of the same tsconfig.json program.
🙁 Actual behavior
I've created a small repo to help reproduce the bug. There isn't anything special about the repo. It's more where the repo is cloned to and the file system case sensitivity that matters. https://github.com/gluxon/tsserver-new-file-intellisense-bug
Here's a video of the bug.
The newly created bar.ts file is able to see IntelliSense information from the rest of the project.
The newly created Baz.ts file is not.
bug.mov
Looking over the TSServer logs for the video above, we can see that bar.ts is matched with the right project. The Baz.ts file is instead mapped to /dev/null/inferredProject2*.
Info 743 [15:52:29.973] Open files:
Info 743 [15:52:29.973] FileName: /Volumes/Code/tsserver-new-file-intellisense-bug/foo.ts ProjectRootPath: /Volumes/Code/tsserver-new-file-intellisense-bug
Info 743 [15:52:29.973] Projects: /Volumes/Code/tsserver-new-file-intellisense-bug/tsconfig.json
Info 743 [15:52:29.973] FileName: /Volumes/Code/tsserver-new-file-intellisense-bug/bar.ts ProjectRootPath: /Volumes/Code/tsserver-new-file-intellisense-bug
Info 743 [15:52:29.973] Projects: /Volumes/Code/tsserver-new-file-intellisense-bug/tsconfig.json
Info 743 [15:52:29.973] FileName: /Volumes/Code/tsserver-new-file-intellisense-bug/Baz.ts ProjectRootPath: /Volumes/Code/tsserver-new-file-intellisense-bug
Info 743 [15:52:29.973] Projects: /dev/null/inferredProject2*
🙂 Expected behavior
TSServer always associates new files with the tsconfig.json file that includes it instead of an inferred project.
🔬 Self-Debugging
We noticed that this problem does not reproduce when tsserver.js is loaded from the same case-sensitive file system. It only happens when tsserver.js is loaded from a case-insensitive file system. The main way of changing where tsserver.js was loaded from was through typescript.tsdk in .vscode/settings.json.
With that toggle, we were able to reliably control whether or not the problem reproduces and compare TSServer's internal state for the successful and failing cases. Skip to the bottom for the conclusion.
Program Loading and Caching
Starting at a high-level, we've confirmed in a debugger a few important pieces of information:
The TypeScript Program object's rootFilesMap records what files are part of a tsconfig.json. The new file is not added to the expected program's rootFilesMap in the failing case, but is added in the successful case. It makes sense why TSServer associates the newly created file to an inferred project given this.
The rootFiles and rootFilesMap fields are computed through getFileNamesFromConfigSpecs(...). This function does not return the newly created file in the failing case (but does in the successful case).
The getFileNamesFromConfigSpecs function calls host.readDirectory(...). This readDirectory call does not return the newly created file in the failing case, but does in the successful case.
The CachedDirectoryStructureHost object is kept up to date through addOrDeleteFileOrDirectory in src/compiler/watchUtilities.ts. We believe the fundamental difference between the failing and successful cases is the evaluation of this line
Once updateFilesOfFileSystemEntry runs, this changes whether or not the CachedDirectoryStructureHost sees the newly created file. I'm able to verify this with a breakpoint on this line, and evaluating the results of readDirectory in the debug console.
Ultimately, I think the problem is before the call to updateFilesOfFileSystemEntry problem though. I would guess updateFilesOfFileSystemEntry should be passed a value of fsQueryResult.fileExists that's true regardless of file system case sensitivity.
How does TypeScript decide when to lowercase?
The fileOrDirectoryPath may or may not be lowercased through:
There's likely an assumption to unwind related to this.host.useCaseSensitiveFileNames. I'm not sure if this is the TypeScript team's preferred fix, but I suspect we need to compute a different toCanonicalFileName function per-project or per-watched folder since the case sensitivity of an opened project folder is independent to where tsserver.js loads from.
Conclusion
The CachedDirectoryStructureHost has a bug where it does not update its cachedReadDirectoryResult internal variable if (1) newly created files have a capital letter and (2) tsserver.js is spawned from a file system volume that's case-insensitive.
🔎 Search Terms
"tsserver", "cache invalidation", "new file", "case sensitive", "IntelliSense", "inferred project", "mac os", "watch", "tsserver restart"
🕗 Version & Regression Information
This bug is reproducible between TypeScript 5.1.6 and 5.3.0-nightly (commit b555727).
I can test versions prior to 5.1.6 if necessary.
⏯ Playground Link
This bug is specific to a local editing environment and file system quirks. A playground would not be able reproduce this.
🪜 Steps to Reproduce
A few developers on my team have noticed that IntelliSense operations intermittently do not work in newly created files in VS Code. IntelliSense works in the new file after restarting TSServer.
Breakpoint debugging TSServer, I believe there's a file system caching bug related to case sensitivity. The bug can be reproduced consistently on macOS.
tsconfig.jsonfile on a case-sensitive volume.tsserver.jsis on a case-insensitive volume, which is usually true for/Applications/Visual Studio Code.app/Contents/Resources/app/extensions/node_modules/typescript/lib/tsserver.js.Foo.tsx).Foo.tsxfile do not have information on other files of the sametsconfig.jsonprogram.🙁 Actual behavior
I've created a small repo to help reproduce the bug. There isn't anything special about the repo. It's more where the repo is cloned to and the file system case sensitivity that matters. https://github.com/gluxon/tsserver-new-file-intellisense-bug
Here's a video of the bug.
bar.tsfile is able to see IntelliSense information from the rest of the project.Baz.tsfile is not.bug.mov
Looking over the TSServer logs for the video above, we can see that
bar.tsis matched with the right project. TheBaz.tsfile is instead mapped to/dev/null/inferredProject2*.🙂 Expected behavior
TSServer always associates new files with the
tsconfig.jsonfile that includes it instead of an inferred project.🔬 Self-Debugging
We noticed that this problem does not reproduce when
tsserver.jsis loaded from the same case-sensitive file system. It only happens whentsserver.jsis loaded from a case-insensitive file system. The main way of changing wheretsserver.jswas loaded from was throughtypescript.tsdkin.vscode/settings.json.With that toggle, we were able to reliably control whether or not the problem reproduces and compare TSServer's internal state for the successful and failing cases. Skip to the bottom for the conclusion.
Program Loading and Caching
Starting at a high-level, we've confirmed in a debugger a few important pieces of information:
Programobject'srootFilesMaprecords what files are part of atsconfig.json. The new file is not added to the expected program'srootFilesMapin the failing case, but is added in the successful case. It makes sense why TSServer associates the newly created file to an inferred project given this.rootFilesandrootFilesMapfields are computed throughgetFileNamesFromConfigSpecs(...). This function does not return the newly created file in the failing case (but does in the successful case).getFileNamesFromConfigSpecsfunction callshost.readDirectory(...). ThisreadDirectorycall does not return the newly created file in the failing case, but does in the successful case.The
hostobject ingetFileNamesFromConfigSpecsis an instance of theCachedDirectoryStructureHostDebugging
CachedDirectoryStructureHost.The
CachedDirectoryStructureHostobject is kept up to date throughaddOrDeleteFileOrDirectoryinsrc/compiler/watchUtilities.ts. We believe the fundamental difference between the failing and successful cases is the evaluation of this lineTypeScript/src/compiler/watchUtilities.ts
Line 315 in 913f65c
Failing Case
fileOrDirectoryPathconst evaluates to/volumes/code/tsserver-new-file-intellisense-bug/baz.tsfsQueryResult.fileExistssubsequently becomesfalse.Note that the entire
fileOrDirectoryPathstring has been been shifted to be lowercase.Successful Case
fileOrDirectoryPathconst evaluates to/Volumes/Code/tsserver-new-file-intellisense-bug/Baz.tsfsQueryResult.fileExistssubsequently becomestrue.In this scenario, the casing of the new file is preserved.
Why is
fsQueryResult.fileExistsimportant?The value of
fsQueryResult.fileExistsis passed toupdateFilesOfFileSystemEntry.TypeScript/src/compiler/watchUtilities.ts
Line 324 in a0e0104
Once
updateFilesOfFileSystemEntryruns, this changes whether or not theCachedDirectoryStructureHostsees the newly created file. I'm able to verify this with a breakpoint on this line, and evaluating the results ofreadDirectoryin the debug console.TypeScript/src/compiler/watchUtilities.ts
Line 326 in a0e0104
In the successful case
In the failing case, this always evaluates to
false, even after theupdateFilesOfFileSystemEntrycall.Ultimately, I think the problem is before the call to
updateFilesOfFileSystemEntryproblem though. I would guessupdateFilesOfFileSystemEntryshould be passed a value offsQueryResult.fileExiststhat'strueregardless of file system case sensitivity.How does TypeScript decide when to lowercase?
The
fileOrDirectoryPathmay or may not be lowercased through:TypeScript/src/server/editorServices.ts
Line 1505 in a0e0104
TypeScript/src/server/editorServices.ts
Lines 1066 to 1068 in a0e0104
The
this.toCanonicalFileNamefunction performs lower-casing based onthis.host.useCaseSensitiveFileNames.TypeScript/src/compiler/core.ts
Line 2604 in a0e0104
TypeScript/src/server/editorServices.ts
Line 1028 in a0e0104
There's likely an assumption to unwind related to
this.host.useCaseSensitiveFileNames. I'm not sure if this is the TypeScript team's preferred fix, but I suspect we need to compute a differenttoCanonicalFileNamefunction per-project or per-watched folder since the case sensitivity of an opened project folder is independent to wheretsserver.jsloads from.Conclusion
The
CachedDirectoryStructureHosthas a bug where it does not update itscachedReadDirectoryResultinternal variable if (1) newly created files have a capital letter and (2)tsserver.jsis spawned from a file system volume that's case-insensitive.