package project import ( "slices" "sync" "testing" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/project/dirty" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" "github.com/microsoft/typescript-go/internal/vfs/vfstest" "gotest.tools/v3/assert" ) func TestSnapshotFSBuilder(t *testing.T) { t.Parallel() toPath := func(fileName string) tspath.Path { return tspath.Path(fileName) } t.Run("builds directory tree on file add", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/foo.ts": "const foo = 1;", }, false /* useCaseSensitiveFileNames */) builder := newSnapshotFSBuilder( testFS, make(map[tspath.Path]*Overlay), // prevOverlays make(map[tspath.Path]*Overlay), // overlays make(map[tspath.Path]*diskFile), make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), nil, // nodeModulesRealpathAliases lsproto.PositionEncodingKindUTF16, toPath, ) // Read the file to add it to the diskFiles fh := builder.GetFile("/src/foo.ts") assert.Assert(t, fh != nil, "file should exist") assert.Equal(t, fh.Content(), "const foo = 1;") // Finalize and check directories snapshot, changed := builder.Finalize() assert.Assert(t, changed, "should have changed") // Check that directory structure was built // /src should contain /src/foo.ts srcDir, ok := snapshot.diskDirectories[tspath.Path("/src")] assert.Assert(t, ok, "/src directory should exist") _, hasFoo := srcDir[tspath.Path("/src/foo.ts")] assert.Assert(t, hasFoo, "/src should contain /src/foo.ts") // / should contain /src rootDir, ok := snapshot.diskDirectories[tspath.Path("/")] assert.Assert(t, ok, "/ directory should exist") _, hasSrc := rootDir[tspath.Path("/src")] assert.Assert(t, hasSrc, "/ should contain /src") }) t.Run("builds nested directory tree", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/nested/deep/file.ts": "export const x = 1;", }, false /* useCaseSensitiveFileNames */) builder := newSnapshotFSBuilder( testFS, make(map[tspath.Path]*Overlay), // prevOverlays make(map[tspath.Path]*Overlay), // overlays make(map[tspath.Path]*diskFile), make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), nil, // nodeModulesRealpathAliases lsproto.PositionEncodingKindUTF16, toPath, ) // Read the file to add it to the diskFiles fh := builder.GetFile("/src/nested/deep/file.ts") assert.Assert(t, fh != nil, "file should exist") snapshot, changed := builder.Finalize() assert.Assert(t, changed, "should have changed") // Check the complete directory tree _, hasFile := snapshot.diskDirectories[tspath.Path("/src/nested/deep")][tspath.Path("/src/nested/deep/file.ts")] assert.Assert(t, hasFile) _, hasDeep := snapshot.diskDirectories[tspath.Path("/src/nested")][tspath.Path("/src/nested/deep")] assert.Assert(t, hasDeep) _, hasNested := snapshot.diskDirectories[tspath.Path("/src")][tspath.Path("/src/nested")] assert.Assert(t, hasNested) _, hasSrc := snapshot.diskDirectories[tspath.Path("/")][tspath.Path("/src")] assert.Assert(t, hasSrc) }) t.Run("removes directory entries on file delete", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/foo.ts": "const foo = 1;", }, false /* useCaseSensitiveFileNames */) // Start with existing diskFiles and directories existingDiskFiles := map[tspath.Path]*diskFile{ tspath.Path("/src/foo.ts"): newDiskFile("/src/foo.ts", "const foo = 1;"), } existingDirs := map[tspath.Path]dirty.CloneableMap[tspath.Path, string]{ tspath.Path("/"): { tspath.Path("/src"): "src", }, tspath.Path("/src"): { tspath.Path("/src/foo.ts"): "foo.ts", }, } builder := newSnapshotFSBuilder( testFS, make(map[tspath.Path]*Overlay), // prevOverlays make(map[tspath.Path]*Overlay), // overlays existingDiskFiles, existingDirs, nil, // nodeModulesRealpathAliases lsproto.PositionEncodingKindUTF16, toPath, ) // Mark the file for deletion by loading and deleting if entry, ok := builder.diskFiles.Load(tspath.Path("/src/foo.ts")); ok { entry.Delete() } snapshot, changed := builder.Finalize() assert.Assert(t, changed, "should have changed") // File should be deleted _, hasFile := snapshot.diskFiles[tspath.Path("/src/foo.ts")] assert.Assert(t, !hasFile, "file should be deleted") // Directory tree should be cleaned up _, hasSrcDir := snapshot.diskDirectories[tspath.Path("/src")] assert.Assert(t, !hasSrcDir, "/src directory should be removed") _, hasRootDir := snapshot.diskDirectories[tspath.Path("/")] assert.Assert(t, !hasRootDir, "root directory should be removed") }) t.Run("removes only empty directories on file delete", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/foo.ts": "const foo = 1;", "/src/bar.ts": "const bar = 2;", }, false /* useCaseSensitiveFileNames */) // Start with existing diskFiles and directories existingDiskFiles := map[tspath.Path]*diskFile{ tspath.Path("/src/foo.ts"): newDiskFile("/src/foo.ts", "const foo = 1;"), tspath.Path("/src/bar.ts"): newDiskFile("/src/bar.ts", "const bar = 2;"), } existingDirs := map[tspath.Path]dirty.CloneableMap[tspath.Path, string]{ tspath.Path("/"): { tspath.Path("/src"): "src", }, tspath.Path("/src"): { tspath.Path("/src/foo.ts"): "foo.ts", tspath.Path("/src/bar.ts"): "bar.ts", }, } builder := newSnapshotFSBuilder( testFS, make(map[tspath.Path]*Overlay), // prevOverlays make(map[tspath.Path]*Overlay), // overlays existingDiskFiles, existingDirs, nil, // nodeModulesRealpathAliases lsproto.PositionEncodingKindUTF16, toPath, ) // Delete only foo.ts if entry, ok := builder.diskFiles.Load(tspath.Path("/src/foo.ts")); ok { entry.Delete() } snapshot, changed := builder.Finalize() assert.Assert(t, changed, "should have changed") // foo.ts should be deleted _, hasFile := snapshot.diskFiles[tspath.Path("/src/foo.ts")] assert.Assert(t, !hasFile, "foo.ts should be deleted") // bar.ts should still exist _, hasBar := snapshot.diskFiles[tspath.Path("/src/bar.ts")] assert.Assert(t, hasBar, "bar.ts should still exist") // /src directory should still exist with bar.ts srcDir, hasSrcDir := snapshot.diskDirectories[tspath.Path("/src")] assert.Assert(t, hasSrcDir, "/src directory should still exist") _, hasFoo := srcDir[tspath.Path("/src/foo.ts")] assert.Assert(t, !hasFoo, "/src should not contain foo.ts") _, hasBarInDir := srcDir[tspath.Path("/src/bar.ts")] assert.Assert(t, hasBarInDir, "/src should contain bar.ts") // root should still contain /src rootDir, hasRootDir := snapshot.diskDirectories[tspath.Path("/")] assert.Assert(t, hasRootDir, "root directory should still exist") _, hasSrc := rootDir[tspath.Path("/src")] assert.Assert(t, hasSrc, "root should contain /src") }) t.Run("adds file to existing directory", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/foo.ts": "const foo = 1;", "/src/bar.ts": "const bar = 2;", }, false /* useCaseSensitiveFileNames */) // Start with existing file and directories existingDiskFiles := map[tspath.Path]*diskFile{ tspath.Path("/src/foo.ts"): newDiskFile("/src/foo.ts", "const foo = 1;"), } existingDirs := map[tspath.Path]dirty.CloneableMap[tspath.Path, string]{ tspath.Path("/"): { tspath.Path("/src"): "src", }, tspath.Path("/src"): { tspath.Path("/src/foo.ts"): "foo.ts", }, } builder := newSnapshotFSBuilder( testFS, make(map[tspath.Path]*Overlay), // prevOverlays make(map[tspath.Path]*Overlay), // overlays existingDiskFiles, existingDirs, nil, // nodeModulesRealpathAliases lsproto.PositionEncodingKindUTF16, toPath, ) // Read bar.ts to add it fh := builder.GetFile("/src/bar.ts") assert.Assert(t, fh != nil, "bar.ts should exist") snapshot, changed := builder.Finalize() assert.Assert(t, changed, "should have changed") // /src should contain both files srcDir := snapshot.diskDirectories[tspath.Path("/src")] _, hasFoo := srcDir[tspath.Path("/src/foo.ts")] assert.Assert(t, hasFoo, "/src should contain foo.ts") _, hasBar := srcDir[tspath.Path("/src/bar.ts")] assert.Assert(t, hasBar, "/src should contain bar.ts") }) t.Run("no change when no files added or deleted", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/foo.ts": "const foo = 1;", }, false /* useCaseSensitiveFileNames */) existingDiskFiles := map[tspath.Path]*diskFile{ tspath.Path("/src/foo.ts"): newDiskFile("/src/foo.ts", "const foo = 1;"), } existingDirs := map[tspath.Path]dirty.CloneableMap[tspath.Path, string]{ tspath.Path("/"): { tspath.Path("/src"): "src", }, tspath.Path("/src"): { tspath.Path("/src/foo.ts"): "foo.ts", }, } builder := newSnapshotFSBuilder( testFS, make(map[tspath.Path]*Overlay), // prevOverlays make(map[tspath.Path]*Overlay), // overlays existingDiskFiles, existingDirs, nil, // nodeModulesRealpathAliases lsproto.PositionEncodingKindUTF16, toPath, ) // Don't add or delete any files snapshot, changed := builder.Finalize() assert.Assert(t, !changed, "should not have changed") // Directories should remain the same srcDir := snapshot.diskDirectories[tspath.Path("/src")] _, hasFoo := srcDir[tspath.Path("/src/foo.ts")] assert.Assert(t, hasFoo) }) t.Run("overlay files are returned over disk files", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/foo.ts": "const foo = 1;", }, false /* useCaseSensitiveFileNames */) overlays := map[tspath.Path]*Overlay{ tspath.Path("/src/foo.ts"): { fileBase: fileBase{fileName: "/src/foo.ts", content: "const foo = 999;"}, }, } builder := newSnapshotFSBuilder( testFS, make(map[tspath.Path]*Overlay), // prevOverlays overlays, make(map[tspath.Path]*diskFile), make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), nil, // nodeModulesRealpathAliases lsproto.PositionEncodingKindUTF16, toPath, ) // Should return overlay content fh := builder.GetFile("/src/foo.ts") assert.Assert(t, fh != nil) assert.Equal(t, fh.Content(), "const foo = 999;") }) t.Run("multiple files added and deleted in single cycle", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/a.ts": "const a = 1;", "/src/b.ts": "const b = 2;", "/lib/utils.ts": "export const util = 1;", "/lib/helpers.ts": "export const helper = 1;", "/other/single.ts": "const single = 1;", }, false /* useCaseSensitiveFileNames */) // Start with some existing files existingDiskFiles := map[tspath.Path]*diskFile{ tspath.Path("/src/a.ts"): newDiskFile("/src/a.ts", "const a = 1;"), tspath.Path("/other/single.ts"): newDiskFile("/other/single.ts", "const single = 1;"), } existingDirs := map[tspath.Path]dirty.CloneableMap[tspath.Path, string]{ tspath.Path("/"): { tspath.Path("/src"): "src", tspath.Path("/other"): "other", }, tspath.Path("/src"): { tspath.Path("/src/a.ts"): "a.ts", }, tspath.Path("/other"): { tspath.Path("/other/single.ts"): "single.ts", }, } builder := newSnapshotFSBuilder( testFS, make(map[tspath.Path]*Overlay), // prevOverlays make(map[tspath.Path]*Overlay), // overlays existingDiskFiles, existingDirs, nil, // nodeModulesRealpathAliases lsproto.PositionEncodingKindUTF16, toPath, ) // Add new files fh := builder.GetFile("/src/b.ts") assert.Assert(t, fh != nil) fh = builder.GetFile("/lib/utils.ts") assert.Assert(t, fh != nil) fh = builder.GetFile("/lib/helpers.ts") assert.Assert(t, fh != nil) // Delete existing files if entry, ok := builder.diskFiles.Load(tspath.Path("/src/a.ts")); ok { entry.Delete() } if entry, ok := builder.diskFiles.Load(tspath.Path("/other/single.ts")); ok { entry.Delete() } snapshot, changed := builder.Finalize() assert.Assert(t, changed, "should have changed") // Verify deleted files are gone _, hasA := snapshot.diskFiles[tspath.Path("/src/a.ts")] assert.Assert(t, !hasA, "/src/a.ts should be deleted") _, hasSingle := snapshot.diskFiles[tspath.Path("/other/single.ts")] assert.Assert(t, !hasSingle, "/other/single.ts should be deleted") // Verify added files exist _, hasB := snapshot.diskFiles[tspath.Path("/src/b.ts")] assert.Assert(t, hasB, "/src/b.ts should exist") _, hasUtils := snapshot.diskFiles[tspath.Path("/lib/utils.ts")] assert.Assert(t, hasUtils, "/lib/utils.ts should exist") _, hasHelpers := snapshot.diskFiles[tspath.Path("/lib/helpers.ts")] assert.Assert(t, hasHelpers, "/lib/helpers.ts should exist") // Verify /other directory is cleaned up (was only entry deleted) _, hasOther := snapshot.diskDirectories[tspath.Path("/other")] assert.Assert(t, !hasOther, "/other directory should be removed") // Verify /src still exists with b.ts (a.ts deleted, b.ts added) srcDir, hasSrc := snapshot.diskDirectories[tspath.Path("/src")] assert.Assert(t, hasSrc, "/src directory should exist") _, hasAInDir := srcDir[tspath.Path("/src/a.ts")] assert.Assert(t, !hasAInDir, "/src should not contain a.ts") _, hasBInDir := srcDir[tspath.Path("/src/b.ts")] assert.Assert(t, hasBInDir, "/src should contain b.ts") // Verify /lib was created with both files libDir, hasLib := snapshot.diskDirectories[tspath.Path("/lib")] assert.Assert(t, hasLib, "/lib directory should exist") _, hasUtilsInDir := libDir[tspath.Path("/lib/utils.ts")] assert.Assert(t, hasUtilsInDir, "/lib should contain utils.ts") _, hasHelpersInDir := libDir[tspath.Path("/lib/helpers.ts")] assert.Assert(t, hasHelpersInDir, "/lib should contain helpers.ts") // Verify root contains /src and /lib but not /other rootDir := snapshot.diskDirectories[tspath.Path("/")] _, hasSrcInRoot := rootDir[tspath.Path("/src")] assert.Assert(t, hasSrcInRoot, "root should contain /src") _, hasLibInRoot := rootDir[tspath.Path("/lib")] assert.Assert(t, hasLibInRoot, "root should contain /lib") _, hasOtherInRoot := rootDir[tspath.Path("/other")] assert.Assert(t, !hasOtherInRoot, "root should not contain /other") }) t.Run("overlay directories are computed from overlays", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{}, false /* useCaseSensitiveFileNames */) overlays := map[tspath.Path]*Overlay{ tspath.Path("/src/overlay.ts"): { fileBase: fileBase{fileName: "/src/overlay.ts", content: "const x = 1;"}, }, tspath.Path("/src/nested/deep.ts"): { fileBase: fileBase{fileName: "/src/nested/deep.ts", content: "const y = 2;"}, }, } builder := newSnapshotFSBuilder( testFS, make(map[tspath.Path]*Overlay), // prevOverlays overlays, make(map[tspath.Path]*diskFile), make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), nil, // nodeModulesRealpathAliases lsproto.PositionEncodingKindUTF16, toPath, ) // Check overlayDirectories was built correctly srcDir, ok := builder.overlayDirectories[tspath.Path("/src")] assert.Assert(t, ok, "/src overlay directory should exist") _, hasOverlay := srcDir[tspath.Path("/src/overlay.ts")] assert.Assert(t, hasOverlay, "/src should contain overlay.ts") _, hasNested := srcDir[tspath.Path("/src/nested")] assert.Assert(t, hasNested, "/src should contain nested/") nestedDir, ok := builder.overlayDirectories[tspath.Path("/src/nested")] assert.Assert(t, ok, "/src/nested overlay directory should exist") _, hasDeep := nestedDir[tspath.Path("/src/nested/deep.ts")] assert.Assert(t, hasDeep, "/src/nested should contain deep.ts") rootDir, ok := builder.overlayDirectories[tspath.Path("/")] assert.Assert(t, ok, "/ overlay directory should exist") _, hasSrc := rootDir[tspath.Path("/src")] assert.Assert(t, hasSrc, "/ should contain /src") }) t.Run("GetAccessibleEntries combines disk and overlay", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/disk.ts": "const disk = 1;", }, false /* useCaseSensitiveFileNames */) overlays := map[tspath.Path]*Overlay{ tspath.Path("/src/overlay.ts"): { fileBase: fileBase{fileName: "/src/overlay.ts", content: "const overlay = 1;"}, }, } builder := newSnapshotFSBuilder( testFS, make(map[tspath.Path]*Overlay), // prevOverlays overlays, make(map[tspath.Path]*diskFile), make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), nil, // nodeModulesRealpathAliases lsproto.PositionEncodingKindUTF16, toPath, ) entries := builder.GetAccessibleEntries("/src") // Should contain both disk file and overlay file (both as basenames) assert.Assert(t, slices.Contains(entries.Files, "disk.ts"), "should contain disk.ts") assert.Assert(t, slices.Contains(entries.Files, "overlay.ts"), "should contain overlay.ts") }) t.Run("GetAccessibleEntries is safe under concurrent calls", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/a.ts": "", "/src/b.ts": "", "/src/c.ts": "", "/src/d.ts": "", "/src/e.ts": "", }, false /* useCaseSensitiveFileNames */) overlays := map[tspath.Path]*Overlay{ tspath.Path("/src/overlay.ts"): { fileBase: fileBase{fileName: "/src/overlay.ts", content: ""}, }, } builder := newSnapshotFSBuilder( testFS, make(map[tspath.Path]*Overlay), // prevOverlays overlays, make(map[tspath.Path]*diskFile), make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), nil, // nodeModulesRealpathAliases lsproto.PositionEncodingKindUTF16, toPath, ) _ = builder.fs.GetAccessibleEntries("/src") start := make(chan struct{}) var wg sync.WaitGroup for range 50 { wg.Go(func() { <-start _ = builder.GetAccessibleEntries("/src") }) } close(start) wg.Wait() // Sanity: the overlay is still reported after all the concurrent calls. entries := builder.GetAccessibleEntries("/src") assert.Assert(t, slices.Contains(entries.Files, "overlay.ts"), "should contain overlay.ts") }) } func TestSnapshotFS(t *testing.T) { t.Parallel() toPath := func(fileName string) tspath.Path { return tspath.Path(fileName) } t.Run("GetFile returns overlay file", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/foo.ts": "disk content", }, false /* useCaseSensitiveFileNames */) overlays := map[tspath.Path]*Overlay{ tspath.Path("/src/foo.ts"): { fileBase: fileBase{fileName: "/src/foo.ts", content: "overlay content"}, }, } snapshot := &SnapshotFS{ toPath: toPath, fs: testFS, overlays: overlays, overlayDirectories: make(map[tspath.Path]map[tspath.Path]string), diskFiles: make(map[tspath.Path]*diskFile), diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), } fh := snapshot.GetFile("/src/foo.ts") assert.Assert(t, fh != nil) assert.Equal(t, fh.Content(), "overlay content") }) t.Run("GetFile returns disk file when not in overlay", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/foo.ts": "disk content", }, false /* useCaseSensitiveFileNames */) diskFiles := map[tspath.Path]*diskFile{ tspath.Path("/src/foo.ts"): newDiskFile("/src/foo.ts", "disk content"), } snapshot := &SnapshotFS{ toPath: toPath, fs: testFS, overlays: make(map[tspath.Path]*Overlay), overlayDirectories: make(map[tspath.Path]map[tspath.Path]string), diskFiles: diskFiles, diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), } fh := snapshot.GetFile("/src/foo.ts") assert.Assert(t, fh != nil) assert.Equal(t, fh.Content(), "disk content") }) t.Run("GetFile reads from fs when not cached", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/foo.ts": "fs content", }, false /* useCaseSensitiveFileNames */) snapshot := &SnapshotFS{ toPath: toPath, fs: testFS, overlays: make(map[tspath.Path]*Overlay), overlayDirectories: make(map[tspath.Path]map[tspath.Path]string), diskFiles: make(map[tspath.Path]*diskFile), diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), } fh := snapshot.GetFile("/src/foo.ts") assert.Assert(t, fh != nil) assert.Equal(t, fh.Content(), "fs content") }) t.Run("GetFile returns nil for non-existent file", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{}, false /* useCaseSensitiveFileNames */) snapshot := &SnapshotFS{ toPath: toPath, fs: testFS, overlays: make(map[tspath.Path]*Overlay), overlayDirectories: make(map[tspath.Path]map[tspath.Path]string), diskFiles: make(map[tspath.Path]*diskFile), diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), } fh := snapshot.GetFile("/src/nonexistent.ts") assert.Assert(t, fh == nil, "should return nil for non-existent file") }) t.Run("isOpenFile returns true for overlays", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{}, false /* useCaseSensitiveFileNames */) overlays := map[tspath.Path]*Overlay{ tspath.Path("/src/foo.ts"): { fileBase: fileBase{fileName: "/src/foo.ts", content: "overlay content"}, }, } snapshot := &SnapshotFS{ toPath: toPath, fs: testFS, overlays: overlays, overlayDirectories: make(map[tspath.Path]map[tspath.Path]string), diskFiles: make(map[tspath.Path]*diskFile), diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), } assert.Assert(t, snapshot.isOpenFile("/src/foo.ts"), "overlay file should be open") assert.Assert(t, !snapshot.isOpenFile("/src/bar.ts"), "non-overlay file should not be open") }) t.Run("GetFileByPath uses provided path", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/foo.ts": "disk content", }, false /* useCaseSensitiveFileNames */) overlays := map[tspath.Path]*Overlay{ tspath.Path("/src/foo.ts"): { fileBase: fileBase{fileName: "/src/foo.ts", content: "overlay content"}, }, } snapshot := &SnapshotFS{ toPath: toPath, fs: testFS, overlays: overlays, overlayDirectories: make(map[tspath.Path]map[tspath.Path]string), diskFiles: make(map[tspath.Path]*diskFile), diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), } // GetFileByPath should use the provided path directly fh := snapshot.GetFileByPath("/src/foo.ts", tspath.Path("/src/foo.ts")) assert.Assert(t, fh != nil) assert.Equal(t, fh.Content(), "overlay content") }) t.Run("GetAccessibleEntries combines disk and overlay directories", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{}, false /* useCaseSensitiveFileNames */) overlays := map[tspath.Path]*Overlay{ tspath.Path("/src/overlay.ts"): { fileBase: fileBase{fileName: "/src/overlay.ts", content: "overlay content"}, }, } overlayDirectories := map[tspath.Path]map[tspath.Path]string{ tspath.Path("/"): { tspath.Path("/src"): "src", }, tspath.Path("/src"): { tspath.Path("/src/overlay.ts"): "overlay.ts", }, } diskFiles := map[tspath.Path]*diskFile{ tspath.Path("/src/disk.ts"): newDiskFile("/src/disk.ts", "disk content"), } diskDirectories := map[tspath.Path]dirty.CloneableMap[tspath.Path, string]{ tspath.Path("/"): { tspath.Path("/src"): "src", }, tspath.Path("/src"): { tspath.Path("/src/disk.ts"): "disk.ts", }, } snapshot := &SnapshotFS{ toPath: toPath, fs: testFS, overlays: overlays, overlayDirectories: overlayDirectories, diskFiles: diskFiles, diskDirectories: diskDirectories, } entries := snapshot.GetAccessibleEntries("/src") // Should contain both disk file and overlay file (both as basenames) assert.Assert(t, slices.Contains(entries.Files, "disk.ts"), "should contain disk.ts") assert.Assert(t, slices.Contains(entries.Files, "overlay.ts"), "should contain overlay.ts") }) } func TestSourceFS(t *testing.T) { t.Parallel() toPath := func(fileName string) tspath.Path { return tspath.Path(fileName) } t.Run("tracks files when tracking enabled", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/foo.ts": "content", }, false /* useCaseSensitiveFileNames */) snapshot := &SnapshotFS{ toPath: toPath, fs: testFS, overlays: make(map[tspath.Path]*Overlay), overlayDirectories: make(map[tspath.Path]map[tspath.Path]string), diskFiles: make(map[tspath.Path]*diskFile), diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), } sourceFS := newSourceFS(true /* tracking */, snapshot, toPath) // File should not be seen yet assert.Assert(t, !sourceFS.SeenFile(tspath.Path("/src/foo.ts"))) // Read the file fh := sourceFS.GetFile("/src/foo.ts") assert.Assert(t, fh != nil) // Now it should be seen assert.Assert(t, sourceFS.SeenFile(tspath.Path("/src/foo.ts"))) }) t.Run("does not track files when tracking disabled", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/foo.ts": "content", }, false /* useCaseSensitiveFileNames */) snapshot := &SnapshotFS{ toPath: toPath, fs: testFS, overlays: make(map[tspath.Path]*Overlay), overlayDirectories: make(map[tspath.Path]map[tspath.Path]string), diskFiles: make(map[tspath.Path]*diskFile), diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), } sourceFS := newSourceFS(false /* tracking */, snapshot, toPath) // Read the file fh := sourceFS.GetFile("/src/foo.ts") assert.Assert(t, fh != nil) // Should not be seen since tracking is disabled assert.Assert(t, !sourceFS.SeenFile(tspath.Path("/src/foo.ts"))) }) t.Run("DisableTracking stops tracking", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/foo.ts": "content", "/src/bar.ts": "content", }, false /* useCaseSensitiveFileNames */) snapshot := &SnapshotFS{ toPath: toPath, fs: testFS, overlays: make(map[tspath.Path]*Overlay), overlayDirectories: make(map[tspath.Path]map[tspath.Path]string), diskFiles: make(map[tspath.Path]*diskFile), diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), } sourceFS := newSourceFS(true /* tracking */, snapshot, toPath) // Read foo while tracking sourceFS.GetFile("/src/foo.ts") assert.Assert(t, sourceFS.SeenFile(tspath.Path("/src/foo.ts"))) // Disable tracking sourceFS.DisableTracking() // Read bar after tracking disabled sourceFS.GetFile("/src/bar.ts") assert.Assert(t, !sourceFS.SeenFile(tspath.Path("/src/bar.ts"))) }) t.Run("FileExists returns true for files in source", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/foo.ts": "content", }, false /* useCaseSensitiveFileNames */) snapshot := &SnapshotFS{ toPath: toPath, fs: testFS, overlays: make(map[tspath.Path]*Overlay), overlayDirectories: make(map[tspath.Path]map[tspath.Path]string), diskFiles: make(map[tspath.Path]*diskFile), diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), } sourceFS := newSourceFS(false /* tracking */, snapshot, toPath) assert.Assert(t, sourceFS.FileExists("/src/foo.ts")) assert.Assert(t, !sourceFS.FileExists("/src/nonexistent.ts")) }) t.Run("ReadFile returns content for files in source", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]string{ "/src/foo.ts": "file content", }, false /* useCaseSensitiveFileNames */) snapshot := &SnapshotFS{ toPath: toPath, fs: testFS, overlays: make(map[tspath.Path]*Overlay), overlayDirectories: make(map[tspath.Path]map[tspath.Path]string), diskFiles: make(map[tspath.Path]*diskFile), diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), } sourceFS := newSourceFS(false /* tracking */, snapshot, toPath) content, ok := sourceFS.ReadFile("/src/foo.ts") assert.Assert(t, ok) assert.Equal(t, content, "file content") _, ok = sourceFS.ReadFile("/src/nonexistent.ts") assert.Assert(t, !ok) }) } func TestAutoImportBuilderFS(t *testing.T) { t.Parallel() toPath := func(fileName string) tspath.Path { return tspath.Path(fileName) } // This test demonstrates that autoImportBuilderFS stores files in untrackedFiles keyed // by the path derived from the filename. When module resolution reads a file via its // symlink path, the file is cached at the symlink path key. If the file is subsequently // requested by its realpath (which is the typical case after module resolution resolves // the symlink), the cache is bypassed. If the file was deleted from disk between those // two operations, the realpath read fails and returns nil. This is the mechanism behind // the nil source file crash in aliasResolver.GetSourceFile. t.Run("symlink cache mismatch: file cached at symlink path, missed at realpath after deletion", func(t *testing.T) { t.Parallel() // Create a VFS with a real file and a symlinked directory pointing to it. // /real/pkg/index.d.ts is the real file. // /project/node_modules/pkg is a symlink to /real/pkg. testFS := vfstest.FromMap(map[string]any{ "/real/pkg/index.d.ts": "export declare const x: number;", "/project/node_modules/pkg": vfstest.Symlink("/real/pkg"), }, true /* useCaseSensitiveFileNames */) // Verify symlink works as expected symlinkPath := "/project/node_modules/pkg/index.d.ts" realpathPath := testFS.Realpath(symlinkPath) assert.Equal(t, realpathPath, "/real/pkg/index.d.ts", "Realpath should resolve the symlink to the real path") builder := newSnapshotFSBuilder( testFS, make(map[tspath.Path]*Overlay), make(map[tspath.Path]*Overlay), make(map[tspath.Path]*diskFile), make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), nil, // nodeModulesRealpathAliases lsproto.PositionEncodingKindUTF16, toPath, ) autoImportFS := &autoImportBuilderFS{ snapshotFSBuilder: builder, } // Step 1: Read the file via its symlink path (simulating what module resolution does // during FileExists). This caches the file in untrackedFiles at the symlink path key. fh := autoImportFS.GetFile(symlinkPath) assert.Assert(t, fh != nil, "File should be readable via symlink path") assert.Equal(t, fh.Content(), "export declare const x: number;") // Step 2: Simulate a file deletion from disk (e.g., npm install running concurrently). // This deletes both the real file and effectively breaks the symlink. err := testFS.Remove("/real/pkg/index.d.ts") assert.NilError(t, err) // Step 3: Request the file by its realpath (simulating what GetSourceFile does after // the checker resolves the module). This bypasses the symlink-path cache entry in // untrackedFiles because the realpath has a different key. fh2 := autoImportFS.GetFile(realpathPath) // The file was cached at the symlink path, but the realpath lookup misses the cache // and goes to disk where the file is now deleted. This returns nil. assert.Assert(t, fh2 == nil, "File should be nil when accessed by realpath after deletion from disk") }) } func TestRealpathAliasLifecycle(t *testing.T) { t.Parallel() toPath := func(fileName string) tspath.Path { return tspath.Path(fileName) } t.Run("alias recorded when reading symlinked node_modules file", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]any{ "/project/node_modules/mylib": vfstest.Symlink("/packages/mylib"), "/packages/mylib/package.json": `{"name": "mylib", "main": "index.js"}`, "/packages/mylib/index.d.ts": `export declare const x: number;`, "/project/node_modules/nolink/package.json": `{"name": "nolink"}`, }, false) builder := newSnapshotFSBuilder( testFS, make(map[tspath.Path]*Overlay), make(map[tspath.Path]*Overlay), make(map[tspath.Path]*diskFile), make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), nil, lsproto.PositionEncodingKindUTF16, toPath, ) // Read a file through the symlink — should record an alias. fh := builder.GetFile("/project/node_modules/mylib/package.json") assert.Assert(t, fh != nil) assert.Equal(t, fh.Content(), `{"name": "mylib", "main": "index.js"}`) // Read a non-symlinked node_modules file — should NOT record an alias. fh2 := builder.GetFile("/project/node_modules/nolink/package.json") assert.Assert(t, fh2 != nil) snapshot, _ := builder.Finalize() // Alias exists for the symlinked file. aliases, ok := snapshot.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/package.json")] assert.Assert(t, ok, "alias should exist for realpath of symlinked file") assert.Assert(t, aliases.paths.Has(tspath.Path("/project/node_modules/mylib/package.json"))) // No alias for the non-symlinked file. _, ok = snapshot.nodeModulesRealpathAliases[tspath.Path("/project/node_modules/nolink/package.json")] assert.Assert(t, !ok, "no alias should exist for non-symlinked file") }) t.Run("no alias recorded for files outside node_modules", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]any{ "/project/link": vfstest.Symlink("/elsewhere"), "/elsewhere/index.ts": `export const x = 1;`, }, false) builder := newSnapshotFSBuilder( testFS, make(map[tspath.Path]*Overlay), make(map[tspath.Path]*Overlay), make(map[tspath.Path]*diskFile), make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), nil, lsproto.PositionEncodingKindUTF16, toPath, ) fh := builder.GetFile("/project/link/index.ts") assert.Assert(t, fh != nil) snapshot, _ := builder.Finalize() assert.Equal(t, len(snapshot.nodeModulesRealpathAliases), 0, "no aliases for non-node_modules symlinks") }) t.Run("aliases carried over across snapshots", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]any{ "/project/node_modules/mylib": vfstest.Symlink("/packages/mylib"), "/packages/mylib/package.json": `{"name": "mylib"}`, }, false) // Build first snapshot. builder1 := newSnapshotFSBuilder( testFS, make(map[tspath.Path]*Overlay), make(map[tspath.Path]*Overlay), make(map[tspath.Path]*diskFile), make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), nil, lsproto.PositionEncodingKindUTF16, toPath, ) builder1.GetFile("/project/node_modules/mylib/package.json") snapshot1, _ := builder1.Finalize() // Build second snapshot from the first, without reading the file again. builder2 := newSnapshotFSBuilder( testFS, make(map[tspath.Path]*Overlay), make(map[tspath.Path]*Overlay), snapshot1.diskFiles, snapshot1.diskDirectories, snapshot1.nodeModulesRealpathAliases, lsproto.PositionEncodingKindUTF16, toPath, ) snapshot2, _ := builder2.Finalize() // Alias should still be present. aliases, ok := snapshot2.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/package.json")] assert.Assert(t, ok, "alias should survive across snapshots") assert.Assert(t, aliases.paths.Has(tspath.Path("/project/node_modules/mylib/package.json"))) }) t.Run("alias pruned when symlinked file is deleted", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]any{ "/project/node_modules/mylib": vfstest.Symlink("/packages/mylib"), "/packages/mylib/package.json": `{"name": "mylib"}`, "/packages/mylib/index.d.ts": `export declare const x: number;`, }, false) // Build first snapshot — read both files. builder1 := newSnapshotFSBuilder( testFS, make(map[tspath.Path]*Overlay), make(map[tspath.Path]*Overlay), make(map[tspath.Path]*diskFile), make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), nil, lsproto.PositionEncodingKindUTF16, toPath, ) builder1.GetFile("/project/node_modules/mylib/package.json") builder1.GetFile("/project/node_modules/mylib/index.d.ts") snapshot1, _ := builder1.Finalize() // Both should be aliased under the same realpath directory but separate files. _, ok := snapshot1.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/package.json")] assert.Assert(t, ok) _, ok = snapshot1.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/index.d.ts")] assert.Assert(t, ok) // Build second snapshot — delete one file via markDirtyFiles. builder2 := newSnapshotFSBuilder( testFS, make(map[tspath.Path]*Overlay), make(map[tspath.Path]*Overlay), snapshot1.diskFiles, snapshot1.diskDirectories, snapshot1.nodeModulesRealpathAliases, lsproto.PositionEncodingKindUTF16, toPath, ) // Simulate deletion of index.d.ts from the disk file cache. var entry *dirty.SyncMapEntry[tspath.Path, *diskFile] if entry, ok = builder2.diskFiles.Load(tspath.Path("/project/node_modules/mylib/index.d.ts")); ok { entry.Delete() } snapshot2, _ := builder2.Finalize() // package.json alias should remain. aliases, ok := snapshot2.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/package.json")] assert.Assert(t, ok, "package.json alias should survive") assert.Assert(t, aliases.paths.Has(tspath.Path("/project/node_modules/mylib/package.json"))) // index.d.ts alias should be fully pruned (empty set → removed from map). _, ok = snapshot2.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/index.d.ts")] assert.Assert(t, !ok, "index.d.ts alias should be pruned after deletion") }) t.Run("multiple symlinks to same realpath", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]any{ "/project/node_modules/mylib": vfstest.Symlink("/packages/mylib"), "/project/node_modules/alias": vfstest.Symlink("/packages/mylib"), "/packages/mylib/package.json": `{"name": "mylib"}`, }, false) builder := newSnapshotFSBuilder( testFS, make(map[tspath.Path]*Overlay), make(map[tspath.Path]*Overlay), make(map[tspath.Path]*diskFile), make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), nil, lsproto.PositionEncodingKindUTF16, toPath, ) // Read via both symlinks. fh1 := builder.GetFile("/project/node_modules/mylib/package.json") assert.Assert(t, fh1 != nil) fh2 := builder.GetFile("/project/node_modules/alias/package.json") assert.Assert(t, fh2 != nil) snapshot, _ := builder.Finalize() aliases, ok := snapshot.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/package.json")] assert.Assert(t, ok, "alias should exist") assert.Assert(t, aliases.paths.Has(tspath.Path("/project/node_modules/mylib/package.json"))) assert.Assert(t, aliases.paths.Has(tspath.Path("/project/node_modules/alias/package.json"))) }) t.Run("multiple symlinks pruned individually", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]any{ "/project/node_modules/mylib": vfstest.Symlink("/packages/mylib"), "/project/node_modules/alias": vfstest.Symlink("/packages/mylib"), "/packages/mylib/package.json": `{"name": "mylib"}`, }, false) // Build first snapshot – read via both symlinks. builder1 := newSnapshotFSBuilder( testFS, make(map[tspath.Path]*Overlay), make(map[tspath.Path]*Overlay), make(map[tspath.Path]*diskFile), make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), nil, lsproto.PositionEncodingKindUTF16, toPath, ) builder1.GetFile("/project/node_modules/mylib/package.json") builder1.GetFile("/project/node_modules/alias/package.json") snapshot1, _ := builder1.Finalize() // Build second snapshot – delete ONE of the symlink disk entries. builder2 := newSnapshotFSBuilder( testFS, make(map[tspath.Path]*Overlay), make(map[tspath.Path]*Overlay), snapshot1.diskFiles, snapshot1.diskDirectories, snapshot1.nodeModulesRealpathAliases, lsproto.PositionEncodingKindUTF16, toPath, ) if entry, ok := builder2.diskFiles.Load(tspath.Path("/project/node_modules/alias/package.json")); ok { entry.Delete() } snapshot2, _ := builder2.Finalize() // The realpath alias set should still exist, but only contain the surviving symlink. aliases, ok := snapshot2.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/package.json")] assert.Assert(t, ok, "alias set should still exist") assert.Assert(t, aliases.paths.Has(tspath.Path("/project/node_modules/mylib/package.json")), "surviving symlink should remain") assert.Assert(t, !aliases.paths.Has(tspath.Path("/project/node_modules/alias/package.json")), "deleted symlink should be pruned") }) t.Run("expandRealpathAliases expands change events", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]any{ "/project/node_modules/mylib": vfstest.Symlink("/packages/mylib"), "/packages/mylib/package.json": `{"name": "mylib"}`, }, false) builder := newSnapshotFSBuilder( testFS, make(map[tspath.Path]*Overlay), make(map[tspath.Path]*Overlay), make(map[tspath.Path]*diskFile), make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), nil, lsproto.PositionEncodingKindUTF16, toPath, ) builder.GetFile("/project/node_modules/mylib/package.json") snapshot, _ := builder.Finalize() // Simulate a watch event on the REALPATH. change := FileChangeSummary{} change.Changed.Add("file:///packages/mylib/package.json") expanded := snapshot.expandRealpathAliases(change) // Should now also contain the symlink path. assert.Assert(t, expanded.Changed.Has("file:///packages/mylib/package.json"), "original event should remain") assert.Assert(t, expanded.Changed.Has("file:///project/node_modules/mylib/package.json"), "symlink event should be added") }) t.Run("expandRealpathAliases expands delete events", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]any{ "/project/node_modules/mylib": vfstest.Symlink("/packages/mylib"), "/packages/mylib/package.json": `{"name": "mylib"}`, }, false) builder := newSnapshotFSBuilder( testFS, make(map[tspath.Path]*Overlay), make(map[tspath.Path]*Overlay), make(map[tspath.Path]*diskFile), make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), nil, lsproto.PositionEncodingKindUTF16, toPath, ) builder.GetFile("/project/node_modules/mylib/package.json") snapshot, _ := builder.Finalize() // Simulate a delete watch event on the REALPATH. change := FileChangeSummary{} change.Deleted.Add("file:///packages/mylib/package.json") expanded := snapshot.expandRealpathAliases(change) assert.Assert(t, expanded.Deleted.Has("file:///project/node_modules/mylib/package.json"), "symlink deletion should be added") }) t.Run("expandRealpathAliases is a no-op with no aliases", func(t *testing.T) { t.Parallel() snapshot := &SnapshotFS{ toPath: toPath, nodeModulesRealpathAliases: nil, } change := FileChangeSummary{} change.Changed.Add("file:///some/file.ts") expanded := snapshot.expandRealpathAliases(change) assert.Equal(t, expanded.Changed.Len(), 1) assert.Assert(t, expanded.Changed.Has("file:///some/file.ts")) }) t.Run("markDirtyFiles invalidates symlinked file via realpath event", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]any{ "/project/node_modules/mylib": vfstest.Symlink("/packages/mylib"), "/packages/mylib/package.json": `{"name": "mylib", "main": "index.js"}`, }, false) // Build first snapshot — read the symlinked file. builder1 := newSnapshotFSBuilder( testFS, make(map[tspath.Path]*Overlay), make(map[tspath.Path]*Overlay), make(map[tspath.Path]*diskFile), make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), nil, lsproto.PositionEncodingKindUTF16, toPath, ) fh := builder1.GetFile("/project/node_modules/mylib/package.json") assert.Assert(t, fh != nil) assert.Equal(t, fh.Content(), `{"name": "mylib", "main": "index.js"}`) snapshot1, _ := builder1.Finalize() // Modify the real file on disk. err := testFS.WriteFile("/packages/mylib/package.json", `{"name": "mylib"}`) assert.NilError(t, err) // Build second snapshot — simulate realpath change event, expanded via aliases. builder2 := newSnapshotFSBuilder( testFS, make(map[tspath.Path]*Overlay), make(map[tspath.Path]*Overlay), snapshot1.diskFiles, snapshot1.diskDirectories, snapshot1.nodeModulesRealpathAliases, lsproto.PositionEncodingKindUTF16, toPath, ) change := FileChangeSummary{} change.Changed.Add("file:///packages/mylib/package.json") // Expand the realpath event to include the symlink path. change = snapshot1.expandRealpathAliases(change) // Now mark dirty — should find the file under the symlink key. builder2.markDirtyFiles(change) // Trigger reload by reading the file (simulates program construction). fh = builder2.GetFile("/project/node_modules/mylib/package.json") assert.Assert(t, fh != nil) assert.Equal(t, fh.Content(), `{"name": "mylib"}`, "builder should serve updated content after dirty marking") snapshot2, _ := builder2.Finalize() // The file should have been reloaded with new content. file, ok := snapshot2.diskFiles[tspath.Path("/project/node_modules/mylib/package.json")] assert.Assert(t, ok, "file should still be in diskFiles") assert.Equal(t, file.Content(), `{"name": "mylib"}`, "content should be updated") }) t.Run("alias clone isolation between snapshots", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]any{ "/project/node_modules/mylib": vfstest.Symlink("/packages/mylib"), "/project/node_modules/other": vfstest.Symlink("/packages/other"), "/packages/mylib/package.json": `{"name": "mylib"}`, "/packages/other/package.json": `{"name": "other"}`, }, false) // Build first snapshot — read only mylib. builder1 := newSnapshotFSBuilder( testFS, make(map[tspath.Path]*Overlay), make(map[tspath.Path]*Overlay), make(map[tspath.Path]*diskFile), make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), nil, lsproto.PositionEncodingKindUTF16, toPath, ) builder1.GetFile("/project/node_modules/mylib/package.json") snapshot1, _ := builder1.Finalize() // Build second snapshot — also read other. builder2 := newSnapshotFSBuilder( testFS, make(map[tspath.Path]*Overlay), make(map[tspath.Path]*Overlay), snapshot1.diskFiles, snapshot1.diskDirectories, snapshot1.nodeModulesRealpathAliases, lsproto.PositionEncodingKindUTF16, toPath, ) builder2.GetFile("/project/node_modules/other/package.json") snapshot2, _ := builder2.Finalize() // snapshot1 should only have mylib alias. _, ok := snapshot1.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/package.json")] assert.Assert(t, ok, "snapshot1 should have mylib alias") _, ok = snapshot1.nodeModulesRealpathAliases[tspath.Path("/packages/other/package.json")] assert.Assert(t, !ok, "snapshot1 should NOT have other alias — it was added in a later snapshot") // snapshot2 should have both. _, ok = snapshot2.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/package.json")] assert.Assert(t, ok, "snapshot2 should have mylib alias") _, ok = snapshot2.nodeModulesRealpathAliases[tspath.Path("/packages/other/package.json")] assert.Assert(t, ok, "snapshot2 should have other alias") }) t.Run("adding symlink to inherited realpath key does not mutate previous snapshot", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]any{ "/project/node_modules/mylib": vfstest.Symlink("/packages/mylib"), "/project/node_modules/alias": vfstest.Symlink("/packages/mylib"), "/packages/mylib/package.json": `{"name": "mylib"}`, }, false) // Snapshot 1: read via one symlink only. builder1 := newSnapshotFSBuilder( testFS, make(map[tspath.Path]*Overlay), make(map[tspath.Path]*Overlay), make(map[tspath.Path]*diskFile), make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), nil, lsproto.PositionEncodingKindUTF16, toPath, ) builder1.GetFile("/project/node_modules/mylib/package.json") snapshot1, _ := builder1.Finalize() // Verify snapshot1 has exactly one alias for the realpath. aliases1, ok := snapshot1.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/package.json")] assert.Assert(t, ok) assert.Equal(t, aliases1.paths.Len(), 1) assert.Assert(t, aliases1.paths.Has(tspath.Path("/project/node_modules/mylib/package.json"))) // Snapshot 2: read via the SECOND symlink, which maps to the same realpath. // This exercises the case where LoadOrStore finds the key in the base map // and must clone-on-write rather than mutating the shared set. builder2 := newSnapshotFSBuilder( testFS, make(map[tspath.Path]*Overlay), make(map[tspath.Path]*Overlay), snapshot1.diskFiles, snapshot1.diskDirectories, snapshot1.nodeModulesRealpathAliases, lsproto.PositionEncodingKindUTF16, toPath, ) builder2.GetFile("/project/node_modules/alias/package.json") snapshot2, _ := builder2.Finalize() // Snapshot 2 should have both symlinks. aliases2, ok := snapshot2.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/package.json")] assert.Assert(t, ok) assert.Equal(t, aliases2.paths.Len(), 2) assert.Assert(t, aliases2.paths.Has(tspath.Path("/project/node_modules/mylib/package.json"))) assert.Assert(t, aliases2.paths.Has(tspath.Path("/project/node_modules/alias/package.json"))) // Snapshot 1 must NOT have been mutated — it should still have only one alias. assert.Equal(t, aliases1.paths.Len(), 1, "snapshot1 alias set must not be mutated by snapshot2") assert.Assert(t, !aliases1.paths.Has(tspath.Path("/project/node_modules/alias/package.json")), "snapshot1 must not contain alias added in snapshot2") }) } func TestExpandAndFilterWatchEvents(t *testing.T) { t.Parallel() toPath := func(fileName string) tspath.Path { return tspath.Path(fileName) } newBuilder := func(testFS vfs.FS) *snapshotFSBuilder { return newSnapshotFSBuilder( testFS, make(map[tspath.Path]*Overlay), make(map[tspath.Path]*Overlay), make(map[tspath.Path]*diskFile), make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]), nil, lsproto.PositionEncodingKindUTF16, toPath, ) } t.Run("preserves node_modules directory deletion even when untracked", func(t *testing.T) { t.Parallel() // node_modules package files are read transiently and never tracked in // diskDirectories, so a node_modules directory deletion can neither be // expanded nor matched by extension. It must still be preserved. builder := newBuilder(vfstest.FromMap(map[string]string{ "/project/index.ts": "export const x = 1;", }, false)) change := FileChangeSummary{} change.Deleted.Add("file:///project/node_modules") expanded := builder.expandAndFilterWatchEvents(change) assert.Assert(t, expanded.Deleted.Has("file:///project/node_modules"), "bare node_modules directory deletion should be preserved") }) t.Run("preserves deletion of a package directory inside node_modules", func(t *testing.T) { t.Parallel() builder := newBuilder(vfstest.FromMap(map[string]string{ "/project/index.ts": "export const x = 1;", }, false)) change := FileChangeSummary{} change.Deleted.Add("file:///project/node_modules/@scope/pkg") expanded := builder.expandAndFilterWatchEvents(change) assert.Assert(t, expanded.Deleted.Has("file:///project/node_modules/@scope/pkg"), "package directory deletion inside node_modules should be preserved") }) t.Run("drops irrelevant untracked deletion outside node_modules", func(t *testing.T) { t.Parallel() builder := newBuilder(vfstest.FromMap(map[string]string{ "/project/index.ts": "export const x = 1;", }, false)) change := FileChangeSummary{} change.Deleted.Add("file:///project/build") expanded := builder.expandAndFilterWatchEvents(change) assert.Equal(t, expanded.Deleted.Len(), 0, "untracked non-node_modules directory deletion should be dropped") }) t.Run("expands tracked directory deletion into file deletions", func(t *testing.T) { t.Parallel() existingDiskFiles := map[tspath.Path]*diskFile{ tspath.Path("/src/foo.ts"): newDiskFile("/src/foo.ts", "const foo = 1;"), } existingDirs := map[tspath.Path]dirty.CloneableMap[tspath.Path, string]{ tspath.Path("/"): {tspath.Path("/src"): "src"}, tspath.Path("/src"): {tspath.Path("/src/foo.ts"): "foo.ts"}, } builder := newSnapshotFSBuilder( vfstest.FromMap(map[string]string{"/src/foo.ts": "const foo = 1;"}, false), make(map[tspath.Path]*Overlay), make(map[tspath.Path]*Overlay), existingDiskFiles, existingDirs, nil, lsproto.PositionEncodingKindUTF16, toPath, ) change := FileChangeSummary{} change.Deleted.Add("file:///src") expanded := builder.expandAndFilterWatchEvents(change) assert.Assert(t, expanded.Deleted.Has("file:///src/foo.ts"), "tracked directory deletion should expand to contained file deletions") assert.Assert(t, !expanded.Deleted.Has("file:///src"), "the directory URI itself should be replaced by its files") }) }