vendor tsgo
This commit is contained in:
391
tools/tsgo/internal/project/bulkcache_test.go
Normal file
391
tools/tsgo/internal/project/bulkcache_test.go
Normal file
@@ -0,0 +1,391 @@
|
||||
package project_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/bundled"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/project"
|
||||
"github.com/microsoft/typescript-go/internal/testutil/projecttestutil"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestBulkCacheInvalidation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if !bundled.Embedded {
|
||||
t.Skip("bundled files are not embedded")
|
||||
}
|
||||
|
||||
// Base file structure for testing
|
||||
baseFiles := map[string]any{
|
||||
"/project/tsconfig.json": `{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"target": "es2015",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}`,
|
||||
"/project/src/index.ts": `import { helper } from "./helper"; console.log(helper);`,
|
||||
"/project/src/helper.ts": `export const helper = "test";`,
|
||||
"/project/src/utils/lib.ts": `export function util() { return "util"; }`,
|
||||
|
||||
"/project/node_modules/@types/node/index.d.ts": `import "./fs"; import "./console";`,
|
||||
"/project/node_modules/@types/node/fs.d.ts": ``,
|
||||
"/project/node_modules/@types/node/console.d.ts": ``,
|
||||
}
|
||||
|
||||
t.Run("large number of node_modules changes invalidates only node_modules cache", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
test := func(t *testing.T, fileEvents []*lsproto.FileEvent, expectNodeModulesInvalidation bool) {
|
||||
session, utils := projecttestutil.Setup(baseFiles)
|
||||
|
||||
// Open a file to create the project
|
||||
session.DidOpenFile(context.Background(), "file:///project/src/index.ts", 1, baseFiles["/project/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
||||
|
||||
// Get initial snapshot and verify config
|
||||
ls, err := session.GetLanguageService(context.Background(), "file:///project/src/index.ts")
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, ls.GetProgram().Options().Target, core.ScriptTargetES2015)
|
||||
|
||||
snapshotBefore := session.Snapshot()
|
||||
configBefore := snapshotBefore.ConfigFileRegistry
|
||||
|
||||
// Update tsconfig.json on disk to test that configs don't get reloaded
|
||||
err = utils.FS().WriteFile("/project/tsconfig.json", `{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"target": "esnext",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}`)
|
||||
assert.NilError(t, err)
|
||||
// Update fs.d.ts in node_modules
|
||||
err = utils.FS().WriteFile("/project/node_modules/@types/node/fs.d.ts", "new text")
|
||||
assert.NilError(t, err)
|
||||
|
||||
// Process the excessive node_modules changes
|
||||
session.DidChangeWatchedFiles(context.Background(), fileEvents)
|
||||
|
||||
// Get language service again to trigger snapshot update
|
||||
ls, err = session.GetLanguageService(context.Background(), "file:///project/src/index.ts")
|
||||
assert.NilError(t, err)
|
||||
|
||||
snapshotAfter := session.Snapshot()
|
||||
configAfter := snapshotAfter.ConfigFileRegistry
|
||||
|
||||
// Config should NOT have been reloaded (target should remain ES2015, not esnext)
|
||||
assert.Equal(t, ls.GetProgram().Options().Target, core.ScriptTargetES2015, "Config should not have been reloaded for node_modules-only changes")
|
||||
|
||||
// Config registry should be the same instance (no configs reloaded)
|
||||
assert.Equal(t, configBefore, configAfter, "Config registry should not have changed for node_modules-only changes")
|
||||
|
||||
fsDtsText := snapshotAfter.GetFile("/project/node_modules/@types/node/fs.d.ts").Content()
|
||||
if expectNodeModulesInvalidation {
|
||||
assert.Equal(t, fsDtsText, "new text")
|
||||
} else {
|
||||
assert.Equal(t, fsDtsText, "")
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("with file existing in cache", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
fileEvents := generateFileEvents(1001, "file:///project/node_modules/generated/file%d.js", lsproto.FileChangeTypeCreated)
|
||||
// Include two files in the program to trigger a full program creation.
|
||||
// Exclude fs.d.ts to show that its content still gets invalidated.
|
||||
fileEvents = append(fileEvents, &lsproto.FileEvent{
|
||||
Uri: "file:///project/node_modules/@types/node/index.d.ts",
|
||||
Type: lsproto.FileChangeTypeChanged,
|
||||
}, &lsproto.FileEvent{
|
||||
Uri: "file:///project/node_modules/@types/node/console.d.ts",
|
||||
Type: lsproto.FileChangeTypeChanged,
|
||||
})
|
||||
|
||||
test(t, fileEvents, true)
|
||||
})
|
||||
|
||||
t.Run("without file existing in cache", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
fileEvents := generateFileEvents(1001, "file:///project/node_modules/generated/file%d.js", lsproto.FileChangeTypeCreated)
|
||||
test(t, fileEvents, false)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("large number of changes outside node_modules", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
test := func(t *testing.T, fileEvents []*lsproto.FileEvent, expectConfigReload bool) {
|
||||
session, utils := projecttestutil.Setup(baseFiles)
|
||||
|
||||
// Open a file to create the project
|
||||
session.DidOpenFile(context.Background(), "file:///project/src/index.ts", 1, baseFiles["/project/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
||||
|
||||
// Get initial state
|
||||
ls, err := session.GetLanguageService(context.Background(), "file:///project/src/index.ts")
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, ls.GetProgram().Options().Target, core.ScriptTargetES2015)
|
||||
|
||||
// Update tsconfig.json on disk
|
||||
err = utils.FS().WriteFile("/project/tsconfig.json", `{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"target": "esnext",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}`)
|
||||
assert.NilError(t, err)
|
||||
// Add root file
|
||||
err = utils.FS().WriteFile("/project/src/rootFile.ts", `console.log("root file")`)
|
||||
assert.NilError(t, err)
|
||||
|
||||
session.DidChangeWatchedFiles(context.Background(), fileEvents)
|
||||
ls, err = session.GetLanguageService(context.Background(), "file:///project/src/index.ts")
|
||||
assert.NilError(t, err)
|
||||
|
||||
if expectConfigReload {
|
||||
assert.Equal(t, ls.GetProgram().Options().Target, core.ScriptTargetESNext, "Config should have been reloaded for changes outside node_modules")
|
||||
assert.Check(t, ls.GetProgram().GetSourceFile("/project/src/rootFile.ts") != nil, "New root file should be present")
|
||||
} else {
|
||||
assert.Equal(t, ls.GetProgram().Options().Target, core.ScriptTargetES2015, "Config should not have been reloaded for changes outside node_modules")
|
||||
assert.Check(t, ls.GetProgram().GetSourceFile("/project/src/rootFile.ts") == nil, "New root file should not be present")
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("with event matching include glob", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
fileEvents := generateFileEvents(1001, "file:///project/generated/file%d.ts", lsproto.FileChangeTypeCreated)
|
||||
fileEvents = append(fileEvents, &lsproto.FileEvent{
|
||||
Uri: "file:///project/src/rootFile.ts",
|
||||
Type: lsproto.FileChangeTypeCreated,
|
||||
})
|
||||
test(t, fileEvents, true)
|
||||
})
|
||||
|
||||
t.Run("without event matching include glob", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
fileEvents := generateFileEvents(1001, "file:///project/generated/file%d.ts", lsproto.FileChangeTypeCreated)
|
||||
test(t, fileEvents, false)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("large number of changes outside node_modules causes project reevaluation", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
session, utils := projecttestutil.Setup(baseFiles)
|
||||
|
||||
// Open a file that will initially use the root tsconfig
|
||||
session.DidOpenFile(context.Background(), "file:///project/src/utils/lib.ts", 1, baseFiles["/project/src/utils/lib.ts"].(string), lsproto.LanguageKindTypeScript)
|
||||
|
||||
// Initially, the file should use the root project (strict mode)
|
||||
snapshot := session.Snapshot()
|
||||
initialProject := snapshot.GetDefaultProject("file:///project/src/utils/lib.ts")
|
||||
assert.Equal(t, initialProject.Name(), "/project/tsconfig.json", "Should initially use root tsconfig")
|
||||
|
||||
// Get language service to verify initial strict mode
|
||||
ls, err := session.GetLanguageService(context.Background(), "file:///project/src/utils/lib.ts")
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, ls.GetProgram().Options().Strict, core.TSTrue, "Should initially use strict mode from root config")
|
||||
|
||||
// Now create the nested tsconfig (this would normally be detected, but we'll simulate a missed event)
|
||||
err = utils.FS().WriteFile("/project/src/utils/tsconfig.json", `{
|
||||
"compilerOptions": {
|
||||
"strict": false,
|
||||
"target": "esnext"
|
||||
}
|
||||
}`)
|
||||
assert.NilError(t, err)
|
||||
|
||||
// Create excessive changes to trigger bulk invalidation
|
||||
fileEvents := generateFileEvents(1001, "file:///project/src/generated/file%d.ts", lsproto.FileChangeTypeCreated)
|
||||
|
||||
// Process the excessive changes - this should trigger project reevaluation
|
||||
session.DidChangeWatchedFiles(context.Background(), fileEvents)
|
||||
|
||||
// Get language service - this should now find the nested config and switch projects
|
||||
ls, err = session.GetLanguageService(context.Background(), "file:///project/src/utils/lib.ts")
|
||||
assert.NilError(t, err)
|
||||
|
||||
snapshot = session.Snapshot()
|
||||
newProject := snapshot.GetDefaultProject("file:///project/src/utils/lib.ts")
|
||||
|
||||
// The file should now use the nested tsconfig
|
||||
assert.Equal(t, newProject.Name(), "/project/src/utils/tsconfig.json", "Should now use nested tsconfig after bulk invalidation")
|
||||
assert.Equal(t, ls.GetProgram().Options().Strict, core.TSFalse, "Should now use non-strict mode from nested config")
|
||||
assert.Equal(t, ls.GetProgram().Options().Target, core.ScriptTargetESNext, "Should use esnext target from nested config")
|
||||
})
|
||||
|
||||
t.Run("config file names cache", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
test := func(t *testing.T, fileEvents []*lsproto.FileEvent, expectConfigDiscovery bool, testName string) {
|
||||
files := map[string]any{
|
||||
"/project/src/index.ts": `console.log("test");`, // No tsconfig initially
|
||||
}
|
||||
session, utils := projecttestutil.Setup(files)
|
||||
|
||||
// Open file without tsconfig - should create inferred project
|
||||
session.DidOpenFile(context.Background(), "file:///project/src/index.ts", 1, files["/project/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
||||
|
||||
snapshot := session.Snapshot()
|
||||
assert.Assert(t, snapshot.ProjectCollection.InferredProject() != nil, "Should have inferred project")
|
||||
assert.Equal(t, snapshot.GetDefaultProject("file:///project/src/index.ts").Kind, project.KindInferred)
|
||||
|
||||
// Create a tsconfig that would affect this file (simulating a missed creation event)
|
||||
err := utils.FS().WriteFile("/project/tsconfig.json", `{
|
||||
"compilerOptions": {
|
||||
"strict": true
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}`)
|
||||
assert.NilError(t, err)
|
||||
|
||||
// Process the changes
|
||||
session.DidChangeWatchedFiles(context.Background(), fileEvents)
|
||||
|
||||
// Get language service to trigger config discovery
|
||||
_, err = session.GetLanguageService(context.Background(), "file:///project/src/index.ts")
|
||||
assert.NilError(t, err)
|
||||
|
||||
snapshot = session.Snapshot()
|
||||
newProject := snapshot.GetDefaultProject("file:///project/src/index.ts")
|
||||
|
||||
// Check expected behavior
|
||||
if expectConfigDiscovery {
|
||||
// Should now use configured project instead of inferred
|
||||
assert.Equal(t, newProject.Kind, project.KindConfigured, "Should now use configured project after cache invalidation")
|
||||
assert.Equal(t, newProject.Name(), "/project/tsconfig.json", "Should use the newly discovered tsconfig")
|
||||
} else {
|
||||
// Should still use inferred project (config file names cache not cleared)
|
||||
assert.Assert(t, newProject == snapshot.ProjectCollection.InferredProject(), "Should still use inferred project after node_modules-only changes")
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("excessive changes only in node_modules does not affect config file names cache", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
fileEvents := generateFileEvents(1001, "file:///project/node_modules/generated/file%d.js", lsproto.FileChangeTypeCreated)
|
||||
test(t, fileEvents, false, "node_modules changes should not clear config cache")
|
||||
})
|
||||
|
||||
t.Run("excessive changes outside node_modules clears config file names cache", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
fileEvents := generateFileEvents(1001, "file:///project/src/generated/file%d.ts", lsproto.FileChangeTypeCreated)
|
||||
// Presence of any tsconfig.json file event triggers rediscovery for config for all open files
|
||||
fileEvents = append(fileEvents, &lsproto.FileEvent{
|
||||
Uri: lsproto.DocumentUri("file:///project/src/generated/tsconfig.json"),
|
||||
Type: lsproto.FileChangeTypeCreated,
|
||||
})
|
||||
test(t, fileEvents, true, "non-node_modules changes should clear config cache")
|
||||
})
|
||||
})
|
||||
|
||||
// Simulate external build tool changing files in dist/ (not included by any project)
|
||||
t.Run("excessive changes in dist folder do not invalidate", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
files := map[string]any{
|
||||
"/project/src/index.ts": `console.log("test");`, // No tsconfig initially
|
||||
}
|
||||
session, utils := projecttestutil.Setup(files)
|
||||
|
||||
// Open file without tsconfig - should create inferred project
|
||||
session.DidOpenFile(context.Background(), "file:///project/src/index.ts", 1, files["/project/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
||||
|
||||
snapshot := session.Snapshot()
|
||||
assert.Equal(t, snapshot.GetDefaultProject("file:///project/src/index.ts").Kind, project.KindInferred)
|
||||
|
||||
// Create a tsconfig that would affect this file (simulating a missed creation event)
|
||||
// This should NOT be discovered after dist-folder changes
|
||||
err := utils.FS().WriteFile("/project/tsconfig.json", `{
|
||||
"compilerOptions": {
|
||||
"strict": true
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}`)
|
||||
assert.NilError(t, err)
|
||||
|
||||
// Create excessive changes in dist folder only
|
||||
fileEvents := generateFileEvents(1001, "file:///project/dist/generated/file%d.js", lsproto.FileChangeTypeCreated)
|
||||
session.DidChangeWatchedFiles(context.Background(), fileEvents)
|
||||
|
||||
// File should still use inferred project (config file names cache NOT cleared for dist changes)
|
||||
_, err = session.GetLanguageService(context.Background(), "file:///project/src/index.ts")
|
||||
assert.NilError(t, err)
|
||||
|
||||
snapshot = session.Snapshot()
|
||||
newProject := snapshot.GetDefaultProject("file:///project/src/index.ts")
|
||||
assert.Equal(t, newProject.Kind, project.KindInferred, "dist-folder changes should not cause config discovery")
|
||||
// This assertion will fail until we implement logic to ignore dist folder changes
|
||||
})
|
||||
|
||||
// Regression test for https://github.com/microsoft/typescript-go/issues/4545
|
||||
//
|
||||
// A config file entry can be retained (here, by an open file whose default
|
||||
// project search fanned out to a referenced config) while its commandLine is
|
||||
// nil, because the referenced config file does not exist. A bulk cache
|
||||
// invalidation triggered by an excessive number of watch events ranges over
|
||||
// all config entries and used to dereference entry.commandLine.ConfigFile
|
||||
// without a nil check, crashing.
|
||||
//
|
||||
// The nil-check-avoiding short circuit `!ok || text != entry.commandLine...`
|
||||
// only protects the case where the file cannot be read, so the config file
|
||||
// must exist on disk (readable) while the entry's commandLine is still nil to
|
||||
// reach the crash.
|
||||
t.Run("bulk invalidation with retained config whose command line is nil", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
appConfig := `{
|
||||
"compilerOptions": { "composite": true, "target": "esnext" },
|
||||
"include": ["**/*"]
|
||||
}`
|
||||
files := map[string]any{
|
||||
// Solution config references ./app, but app/tsconfig.json does not exist.
|
||||
"/project/tsconfig.json": `{
|
||||
"compilerOptions": { "composite": true },
|
||||
"files": [],
|
||||
"references": [{ "path": "./app" }]
|
||||
}`,
|
||||
"/project/app/main.ts": `export const main = 1;`,
|
||||
}
|
||||
|
||||
session, utils := projecttestutil.Setup(files)
|
||||
|
||||
// Open a file in the (non-existent) referenced project. The default project
|
||||
// search fans out to app/tsconfig.json, creating a retained config entry
|
||||
// with commandLine == nil and pendingReload == None.
|
||||
session.DidOpenFile(context.Background(), "file:///project/app/main.ts", 1, files["/project/app/main.ts"].(string), lsproto.LanguageKindTypeScript)
|
||||
_, err := session.GetLanguageService(context.Background(), "file:///project/app/main.ts")
|
||||
assert.NilError(t, err)
|
||||
|
||||
// Create the referenced config on disk WITHOUT notifying, so the
|
||||
// nil-commandLine entry is not reloaded (pendingReload stays None) but the
|
||||
// file becomes readable.
|
||||
err = utils.FS().WriteFile("/project/app/tsconfig.json", appConfig)
|
||||
assert.NilError(t, err)
|
||||
|
||||
// Trigger a bulk cache invalidation with an excessive number of watch events.
|
||||
// The creation of a config file drives the excessive-change path into
|
||||
// invalidateCache, which ranges over all config entries -- including the one
|
||||
// whose commandLine is nil -- and used to crash.
|
||||
fileEvents := generateFileEvents(1001, "file:///project/app/generated/file%d.ts", lsproto.FileChangeTypeCreated)
|
||||
fileEvents = append(fileEvents, &lsproto.FileEvent{
|
||||
Uri: "file:///project/newdir/tsconfig.json",
|
||||
Type: lsproto.FileChangeTypeCreated,
|
||||
})
|
||||
session.DidChangeWatchedFiles(context.Background(), fileEvents)
|
||||
_, err = session.GetLanguageService(context.Background(), "file:///project/app/main.ts")
|
||||
assert.NilError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// Helper function to generate excessive file change events
|
||||
func generateFileEvents(count int, pathTemplate string, changeType lsproto.FileChangeType) []*lsproto.FileEvent {
|
||||
var events []*lsproto.FileEvent
|
||||
for i := range count {
|
||||
events = append(events, &lsproto.FileEvent{
|
||||
Uri: lsproto.DocumentUri(fmt.Sprintf(pathTemplate, i)),
|
||||
Type: changeType,
|
||||
})
|
||||
}
|
||||
return events
|
||||
}
|
||||
Reference in New Issue
Block a user