755 lines
33 KiB
Go
755 lines
33 KiB
Go
package project_test
|
|
|
|
import (
|
|
"context"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/microsoft/typescript-go/internal/bundled"
|
|
"github.com/microsoft/typescript-go/internal/core"
|
|
"github.com/microsoft/typescript-go/internal/diagnostics"
|
|
"github.com/microsoft/typescript-go/internal/ls/lsutil"
|
|
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
|
"github.com/microsoft/typescript-go/internal/project"
|
|
"github.com/microsoft/typescript-go/internal/testutil/projecttestutil"
|
|
"github.com/microsoft/typescript-go/internal/tspath"
|
|
"gotest.tools/v3/assert"
|
|
)
|
|
|
|
type publishDiagnosticsCall = struct {
|
|
Ctx context.Context
|
|
Params *lsproto.PublishDiagnosticsParams
|
|
}
|
|
|
|
// filterDiagnosticsByURI returns all PublishDiagnostics calls matching the given URI,
|
|
// starting from the given index.
|
|
func filterDiagnosticsByURI(calls []publishDiagnosticsCall, uri lsproto.DocumentUri, from int) []publishDiagnosticsCall {
|
|
var result []publishDiagnosticsCall
|
|
for i := from; i < len(calls); i++ {
|
|
if calls[i].Params.Uri == uri {
|
|
result = append(result, calls[i])
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// These tests explicitly verify ProgramUpdateKind using subtests with shared helpers.
|
|
func TestProjectProgramUpdateKind(t *testing.T) {
|
|
t.Parallel()
|
|
if !bundled.Embedded {
|
|
t.Skip("bundled files are not embedded")
|
|
}
|
|
|
|
// Use the default session setup for tests.
|
|
|
|
t.Run("NewFiles on initial build", func(t *testing.T) {
|
|
t.Parallel()
|
|
files := map[string]any{
|
|
"/src/tsconfig.json": "{}",
|
|
"/src/index.ts": "export const x = 1;",
|
|
}
|
|
session, _ := projecttestutil.Setup(files)
|
|
session.DidOpenFile(context.Background(), "file:///src/index.ts", 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
_, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts"))
|
|
assert.NilError(t, err)
|
|
snapshot := session.Snapshot()
|
|
configured := snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/src/tsconfig.json"))
|
|
assert.Assert(t, configured != nil)
|
|
assert.Equal(t, configured.ProgramUpdateKind, project.ProgramUpdateKindNewFiles)
|
|
})
|
|
|
|
t.Run("Cloned on single-file change", func(t *testing.T) {
|
|
t.Parallel()
|
|
files := map[string]any{
|
|
"/src/tsconfig.json": "{}",
|
|
"/src/index.ts": "console.log('Hello');",
|
|
}
|
|
session, _ := projecttestutil.Setup(files)
|
|
session.DidOpenFile(context.Background(), "file:///src/index.ts", 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
_, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts"))
|
|
assert.NilError(t, err)
|
|
session.DidChangeFile(context.Background(), "file:///src/index.ts", 2, []lsproto.TextDocumentContentChangePartialOrWholeDocument{{
|
|
Partial: &lsproto.TextDocumentContentChangePartial{Text: "\n", Range: lsproto.Range{Start: lsproto.Position{Line: 0, Character: 20}, End: lsproto.Position{Line: 0, Character: 20}}},
|
|
}})
|
|
_, err = session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts"))
|
|
assert.NilError(t, err)
|
|
snapshot := session.Snapshot()
|
|
configured := snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/src/tsconfig.json"))
|
|
assert.Assert(t, configured != nil)
|
|
assert.Equal(t, configured.ProgramUpdateKind, project.ProgramUpdateKindCloned)
|
|
})
|
|
|
|
t.Run("SameFileNames on config change without root changes", func(t *testing.T) {
|
|
t.Parallel()
|
|
files := map[string]any{
|
|
"/src/tsconfig.json": `{"compilerOptions": {"strict": true}}`,
|
|
"/src/index.ts": "export const x = 1;",
|
|
}
|
|
session, utils := projecttestutil.Setup(files)
|
|
session.DidOpenFile(context.Background(), "file:///src/index.ts", 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
_, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts"))
|
|
assert.NilError(t, err)
|
|
err = utils.FS().WriteFile("/src/tsconfig.json", `{"compilerOptions": {"strict": false}}`)
|
|
assert.NilError(t, err)
|
|
session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{{Uri: lsproto.DocumentUri("file:///src/tsconfig.json"), Type: lsproto.FileChangeTypeChanged}})
|
|
_, err = session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts"))
|
|
assert.NilError(t, err)
|
|
snapshot := session.Snapshot()
|
|
configured := snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/src/tsconfig.json"))
|
|
assert.Assert(t, configured != nil)
|
|
assert.Equal(t, configured.ProgramUpdateKind, project.ProgramUpdateKindSameFileNames)
|
|
})
|
|
|
|
t.Run("NewFiles on root addition", func(t *testing.T) {
|
|
t.Parallel()
|
|
files := map[string]any{
|
|
"/src/tsconfig.json": "{}",
|
|
"/src/index.ts": "export {}",
|
|
}
|
|
session, utils := projecttestutil.Setup(files)
|
|
session.DidOpenFile(context.Background(), "file:///src/index.ts", 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
_, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts"))
|
|
assert.NilError(t, err)
|
|
content := "export const y = 2;"
|
|
err = utils.FS().WriteFile("/src/newfile.ts", content)
|
|
assert.NilError(t, err)
|
|
session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{{Uri: lsproto.DocumentUri("file:///src/newfile.ts"), Type: lsproto.FileChangeTypeCreated}})
|
|
session.DidOpenFile(context.Background(), "file:///src/newfile.ts", 1, content, lsproto.LanguageKindTypeScript)
|
|
_, err = session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/newfile.ts"))
|
|
assert.NilError(t, err)
|
|
snapshot := session.Snapshot()
|
|
configured := snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/src/tsconfig.json"))
|
|
assert.Assert(t, configured != nil)
|
|
assert.Equal(t, configured.ProgramUpdateKind, project.ProgramUpdateKindNewFiles)
|
|
})
|
|
|
|
t.Run("SameFileNames when adding an unresolvable import with multi-file change", func(t *testing.T) {
|
|
t.Parallel()
|
|
files := map[string]any{
|
|
"/src/tsconfig.json": "{}",
|
|
"/src/index.ts": "export const x = 1;",
|
|
"/src/other.ts": "export const z = 3;",
|
|
}
|
|
session, _ := projecttestutil.Setup(files)
|
|
session.DidOpenFile(context.Background(), "file:///src/index.ts", 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
_, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts"))
|
|
assert.NilError(t, err)
|
|
// Change index.ts to add an unresolvable import
|
|
session.DidChangeFile(context.Background(), "file:///src/index.ts", 2, []lsproto.TextDocumentContentChangePartialOrWholeDocument{{
|
|
Partial: &lsproto.TextDocumentContentChangePartial{Text: "\nimport \"./does-not-exist\";\n", Range: lsproto.Range{Start: lsproto.Position{Line: 0, Character: 0}, End: lsproto.Position{Line: 0, Character: 0}}},
|
|
}})
|
|
_, err = session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts"))
|
|
assert.NilError(t, err)
|
|
snapshot := session.Snapshot()
|
|
configured := snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/src/tsconfig.json"))
|
|
assert.Assert(t, configured != nil)
|
|
assert.Equal(t, configured.ProgramUpdateKind, project.ProgramUpdateKindSameFileNames)
|
|
})
|
|
}
|
|
|
|
func TestProject(t *testing.T) {
|
|
t.Parallel()
|
|
if !bundled.Embedded {
|
|
t.Skip("bundled files are not embedded")
|
|
}
|
|
|
|
t.Run("commandLineWithTypingsFiles is reset on CommandLine change", func(t *testing.T) {
|
|
t.Parallel()
|
|
files := map[string]any{
|
|
"/user/username/projects/project1/app.js": ``,
|
|
"/user/username/projects/project1/package.json": `{"name":"p1","dependencies":{"jquery":"^3.1.0"}}`,
|
|
"/user/username/projects/project2/app.js": ``,
|
|
}
|
|
|
|
session, utils := projecttestutil.SetupWithTypingsInstaller(files, &projecttestutil.TypingsInstallerOptions{
|
|
PackageToFile: map[string]string{
|
|
// Provide typings content to be installed for jquery so ATA actually installs something
|
|
"jquery": `declare const $: { x: number }`,
|
|
},
|
|
})
|
|
|
|
// 1) Open an inferred project file that triggers ATA
|
|
uri1 := lsproto.DocumentUri("file:///user/username/projects/project1/app.js")
|
|
session.DidOpenFile(context.Background(), uri1, 1, files["/user/username/projects/project1/app.js"].(string), lsproto.LanguageKindJavaScript)
|
|
|
|
// 2) Wait for ATA/background tasks to finish, then get a language service for the first file
|
|
session.WaitForBackgroundTasks()
|
|
// Sanity check: ensure ATA performed at least one install
|
|
npmCalls := utils.NpmExecutor().NpmInstallCalls()
|
|
assert.Assert(t, len(npmCalls) > 0, "expected at least one npm install call from ATA")
|
|
_, err := session.GetLanguageService(context.Background(), uri1)
|
|
assert.NilError(t, err)
|
|
|
|
// 3) Open another inferred project file
|
|
uri2 := lsproto.DocumentUri("file:///user/username/projects/project2/app.js")
|
|
session.DidOpenFile(context.Background(), uri2, 1, ``, lsproto.LanguageKindJavaScript)
|
|
|
|
// 4) Get a language service for the second file
|
|
// If commandLineWithTypingsFiles was not reset, the new program command line
|
|
// won't include the newly opened file and this will fail.
|
|
_, err = session.GetLanguageService(context.Background(), uri2)
|
|
assert.NilError(t, err)
|
|
})
|
|
|
|
t.Run("inferred project rebuilt twice in one snapshot after typings install does not crash", func(t *testing.T) {
|
|
t.Parallel()
|
|
// Reproduces the nil-command-line crash in (*Program).SingleThreaded (issue #3873).
|
|
//
|
|
// `getCommandLineWithTypingsFiles` memoizes `commandLineWithTypingsFiles` behind a
|
|
// `sync.Once`. When `CommandLine` (or the inferred roots) change, the cached value is
|
|
// reset to nil but the `Once` is NOT reset. `Project.Clone()` produces a fresh `Once`,
|
|
// so the first mutation of a project within a snapshot normally clears the staleness.
|
|
// But if the *same* in-snapshot clone has its program built, then its roots change, then
|
|
// its program is built again, the second build hits a consumed `Once` and reads the nil
|
|
// cached value, passing a nil command line to `compiler.NewProgram`.
|
|
//
|
|
// This sequence is produced within a single snapshot by `DidRequestFile`'s
|
|
// build -> cleanupInferredProject -> build path, when:
|
|
// 1. the inferred project has installed typings (so the `Once` path is taken), and
|
|
// 2. a pending content change marks the inferred project dirty (forcing build #1), and
|
|
// 3. a pending tsconfig creation moves another open file out of the inferred project,
|
|
// so `cleanupInferredProject` changes the inferred roots between the two builds.
|
|
files := map[string]any{
|
|
"/user/username/projects/project1/a.js": ``,
|
|
"/user/username/projects/project1/package.json": `{"name":"p1","dependencies":{"jquery":"^3.1.0"}}`,
|
|
"/user/username/projects/project1/b.ts": `export const y = 1;`,
|
|
}
|
|
|
|
session, utils := projecttestutil.SetupWithTypingsInstaller(files, &projecttestutil.TypingsInstallerOptions{
|
|
PackageToFile: map[string]string{
|
|
"jquery": `declare const $: { x: number }`,
|
|
},
|
|
})
|
|
|
|
aURI := lsproto.DocumentUri("file:///user/username/projects/project1/a.js")
|
|
bURI := lsproto.DocumentUri("file:///user/username/projects/project1/b.ts")
|
|
|
|
// 1) Open the file that triggers ATA, plus another file that joins the inferred project.
|
|
session.DidOpenFile(context.Background(), aURI, 1, ``, lsproto.LanguageKindJavaScript)
|
|
session.DidOpenFile(context.Background(), bURI, 1, files["/user/username/projects/project1/b.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
|
|
// 2) Let ATA install jquery typings, then build the inferred program so its
|
|
// typings files are populated.
|
|
session.WaitForBackgroundTasks()
|
|
npmCalls := utils.NpmExecutor().NpmInstallCalls()
|
|
assert.Assert(t, len(npmCalls) > 0, "expected at least one npm install call from ATA")
|
|
_, err := session.GetLanguageService(context.Background(), aURI)
|
|
assert.NilError(t, err)
|
|
|
|
// 3) Queue two pending changes that will be flushed together in the next snapshot:
|
|
// - a content change to a.js (marks the inferred project dirty -> first program build)
|
|
// - creation of a tsconfig.json that captures b.ts (moves it out of the inferred
|
|
// project, so cleanupInferredProject shrinks the inferred roots between builds).
|
|
session.DidChangeFile(context.Background(), aURI, 2, []lsproto.TextDocumentContentChangePartialOrWholeDocument{
|
|
{WholeDocument: &lsproto.TextDocumentContentChangeWholeDocument{Text: `// changed`}},
|
|
})
|
|
err = utils.FS().WriteFile("/user/username/projects/project1/tsconfig.json", `{"compilerOptions":{},"files":["b.ts"]}`)
|
|
assert.NilError(t, err)
|
|
session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{
|
|
{
|
|
Type: lsproto.FileChangeTypeCreated,
|
|
Uri: "file:///user/username/projects/project1/tsconfig.json",
|
|
},
|
|
})
|
|
|
|
// 4) Flush the pending changes and the request in a single snapshot. With the bug,
|
|
// the inferred project's program is built, its roots change, and it is rebuilt with
|
|
// a nil command line, panicking in (*Program).SingleThreaded.
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Fatalf("crash reproduced (issue #3873): %v", r)
|
|
}
|
|
}()
|
|
_, err = session.GetLanguageService(context.Background(), aURI)
|
|
assert.NilError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestPushDiagnostics(t *testing.T) {
|
|
t.Parallel()
|
|
if !bundled.Embedded {
|
|
t.Skip("bundled files are not embedded")
|
|
}
|
|
|
|
t.Run("publishes program diagnostics on initial program creation", func(t *testing.T) {
|
|
t.Parallel()
|
|
files := map[string]any{
|
|
"/src/tsconfig.json": `{"compilerOptions": {"baseUrl": "."}}`,
|
|
"/src/index.ts": "export const x = 1;",
|
|
}
|
|
session, utils := projecttestutil.Setup(files)
|
|
session.DidOpenFile(context.Background(), "file:///src/index.ts", 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
_, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts"))
|
|
assert.NilError(t, err)
|
|
|
|
session.WaitForBackgroundTasks()
|
|
|
|
calls := utils.Client().PublishDiagnosticsCalls()
|
|
assert.Assert(t, len(calls) > 0, "expected at least one PublishDiagnostics call")
|
|
|
|
// Find the call for tsconfig.json
|
|
var tsconfigCall *struct {
|
|
Ctx context.Context
|
|
Params *lsproto.PublishDiagnosticsParams
|
|
}
|
|
for i := range calls {
|
|
if calls[i].Params.Uri == "file:///src/tsconfig.json" {
|
|
tsconfigCall = &calls[i]
|
|
break
|
|
}
|
|
}
|
|
assert.Assert(t, tsconfigCall != nil, "expected PublishDiagnostics call for tsconfig.json")
|
|
assert.Assert(t, len(tsconfigCall.Params.Diagnostics) > 0, "expected at least one diagnostic")
|
|
})
|
|
|
|
t.Run("publishes config parsing diagnostics on initial program creation", func(t *testing.T) {
|
|
t.Parallel()
|
|
files := map[string]any{
|
|
"/src/tsconfig.json": `{
|
|
"compilerOptions": {
|
|
"target": "nope"
|
|
}
|
|
}`,
|
|
"/src/index.ts": "export const x = 1;",
|
|
}
|
|
session, utils := projecttestutil.Setup(files)
|
|
session.DidOpenFile(context.Background(), "file:///src/index.ts", 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
_, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts"))
|
|
assert.NilError(t, err)
|
|
|
|
session.WaitForBackgroundTasks()
|
|
|
|
calls := utils.Client().PublishDiagnosticsCalls()
|
|
tsconfigCalls := filterDiagnosticsByURI(calls, "file:///src/tsconfig.json", 0)
|
|
assert.Assert(t, len(tsconfigCalls) > 0, "expected PublishDiagnostics call for tsconfig.json")
|
|
lastTsconfigCall := tsconfigCalls[len(tsconfigCalls)-1]
|
|
|
|
expectedMessage := "Argument for '--target' option must be:"
|
|
assert.Assert(t, slices.ContainsFunc(lastTsconfigCall.Params.Diagnostics, func(diag *lsproto.Diagnostic) bool {
|
|
return strings.Contains(diag.Message.AsString(), expectedMessage)
|
|
}), "expected invalid target diagnostic on tsconfig.json, got: %v", lastTsconfigCall.Params.Diagnostics)
|
|
})
|
|
|
|
t.Run("clears diagnostics when project is removed", func(t *testing.T) {
|
|
t.Parallel()
|
|
files := map[string]any{
|
|
"/src/tsconfig.json": `{"compilerOptions": {"baseUrl": "."}}`,
|
|
"/src/index.ts": "export const x = 1;",
|
|
"/src2/tsconfig.json": `{"compilerOptions": {}}`,
|
|
"/src2/index.ts": "export const y = 2;",
|
|
}
|
|
session, utils := projecttestutil.Setup(files)
|
|
session.DidOpenFile(context.Background(), "file:///src/index.ts", 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
_, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts"))
|
|
assert.NilError(t, err)
|
|
session.WaitForBackgroundTasks()
|
|
|
|
// Open a file in a different project to trigger cleanup of the first
|
|
session.DidCloseFile(context.Background(), "file:///src/index.ts")
|
|
session.DidOpenFile(context.Background(), "file:///src2/index.ts", 1, files["/src2/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
_, err = session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src2/index.ts"))
|
|
assert.NilError(t, err)
|
|
session.WaitForBackgroundTasks()
|
|
|
|
calls := utils.Client().PublishDiagnosticsCalls()
|
|
// Should have at least one call for the first project with diagnostics,
|
|
// and one clearing it after switching projects
|
|
var firstProjectCalls []struct {
|
|
Ctx context.Context
|
|
Params *lsproto.PublishDiagnosticsParams
|
|
}
|
|
for i := range calls {
|
|
if calls[i].Params.Uri == "file:///src/tsconfig.json" {
|
|
firstProjectCalls = append(firstProjectCalls, calls[i])
|
|
}
|
|
}
|
|
assert.Assert(t, len(firstProjectCalls) >= 2, "expected at least 2 PublishDiagnostics calls for first project")
|
|
// Last call should clear diagnostics
|
|
lastCall := firstProjectCalls[len(firstProjectCalls)-1]
|
|
assert.Equal(t, len(lastCall.Params.Diagnostics), 0, "expected empty diagnostics after project cleanup")
|
|
})
|
|
|
|
t.Run("updates diagnostics when program changes", func(t *testing.T) {
|
|
t.Parallel()
|
|
files := map[string]any{
|
|
"/src/tsconfig.json": `{"compilerOptions": {"baseUrl": "."}}`,
|
|
"/src/index.ts": "export const x = 1;",
|
|
}
|
|
session, utils := projecttestutil.Setup(files)
|
|
session.DidOpenFile(context.Background(), "file:///src/index.ts", 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
_, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts"))
|
|
assert.NilError(t, err)
|
|
session.WaitForBackgroundTasks()
|
|
|
|
initialCallCount := len(utils.Client().PublishDiagnosticsCalls())
|
|
|
|
// Change the tsconfig to remove baseUrl
|
|
err = utils.FS().WriteFile("/src/tsconfig.json", `{"compilerOptions": {}}`)
|
|
assert.NilError(t, err)
|
|
session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{{Uri: lsproto.DocumentUri("file:///src/tsconfig.json"), Type: lsproto.FileChangeTypeChanged}})
|
|
_, err = session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts"))
|
|
assert.NilError(t, err)
|
|
session.WaitForBackgroundTasks()
|
|
|
|
calls := utils.Client().PublishDiagnosticsCalls()
|
|
assert.Assert(t, len(calls) > initialCallCount, "expected additional PublishDiagnostics call after change")
|
|
|
|
// Find the last call for tsconfig.json
|
|
var lastTsconfigCall *struct {
|
|
Ctx context.Context
|
|
Params *lsproto.PublishDiagnosticsParams
|
|
}
|
|
for i := len(calls) - 1; i >= 0; i-- {
|
|
if calls[i].Params.Uri == "file:///src/tsconfig.json" {
|
|
lastTsconfigCall = &calls[i]
|
|
break
|
|
}
|
|
}
|
|
assert.Assert(t, lastTsconfigCall != nil, "expected PublishDiagnostics call for tsconfig.json")
|
|
// After fixing the error, there should be no program diagnostics
|
|
assert.Equal(t, len(lastTsconfigCall.Params.Diagnostics), 0, "expected no diagnostics after removing baseUrl option")
|
|
})
|
|
|
|
t.Run("does not publish for inferred projects", func(t *testing.T) {
|
|
t.Parallel()
|
|
files := map[string]any{
|
|
"/src/index.ts": "let x: number = 'not a number';",
|
|
}
|
|
session, utils := projecttestutil.Setup(files)
|
|
session.DidOpenFile(context.Background(), "file:///src/index.ts", 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
_, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts"))
|
|
assert.NilError(t, err)
|
|
session.WaitForBackgroundTasks()
|
|
|
|
calls := utils.Client().PublishDiagnosticsCalls()
|
|
// Should not have any calls since inferred projects don't have tsconfig.json
|
|
assert.Equal(t, len(calls), 0, "expected no PublishDiagnostics calls for inferred projects")
|
|
})
|
|
|
|
t.Run("does not publish when validation is disabled", func(t *testing.T) {
|
|
t.Parallel()
|
|
files := map[string]any{
|
|
"/src/tsconfig.json": `{
|
|
"compilerOptions": {
|
|
"target": "nope"
|
|
}
|
|
}`,
|
|
"/src/index.ts": "export const x = 1;",
|
|
}
|
|
session, utils := projecttestutil.Setup(files)
|
|
prefs := lsutil.NewDefaultUserPreferences()
|
|
prefs.EnableValidation = core.TSFalse
|
|
session.Configure(prefs)
|
|
session.DidOpenFile(context.Background(), "file:///src/index.ts", 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
_, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts"))
|
|
assert.NilError(t, err)
|
|
session.WaitForBackgroundTasks()
|
|
|
|
calls := utils.Client().PublishDiagnosticsCalls()
|
|
for _, call := range calls {
|
|
assert.Equal(t, len(call.Params.Diagnostics), 0, "expected only empty PublishDiagnostics calls when validation is disabled")
|
|
}
|
|
})
|
|
|
|
t.Run("clears diagnostics when validation is disabled", func(t *testing.T) {
|
|
t.Parallel()
|
|
files := map[string]any{
|
|
"/src/tsconfig.json": `{
|
|
"compilerOptions": {
|
|
"target": "nope"
|
|
}
|
|
}`,
|
|
"/src/index.ts": "export const x = 1;",
|
|
}
|
|
session, utils := projecttestutil.Setup(files)
|
|
session.DidOpenFile(context.Background(), "file:///src/index.ts", 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
_, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts"))
|
|
assert.NilError(t, err)
|
|
session.WaitForBackgroundTasks()
|
|
|
|
calls := utils.Client().PublishDiagnosticsCalls()
|
|
tsconfigCalls := filterDiagnosticsByURI(calls, "file:///src/tsconfig.json", 0)
|
|
assert.Assert(t, len(tsconfigCalls) > 0, "expected initial PublishDiagnostics call for tsconfig.json")
|
|
assert.Assert(t, len(tsconfigCalls[len(tsconfigCalls)-1].Params.Diagnostics) > 0, "expected initial diagnostics")
|
|
|
|
prefs := lsutil.NewDefaultUserPreferences()
|
|
prefs.EnableValidation = core.TSFalse
|
|
session.Configure(prefs)
|
|
_, err = session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts"))
|
|
assert.NilError(t, err)
|
|
session.WaitForBackgroundTasks()
|
|
|
|
calls = utils.Client().PublishDiagnosticsCalls()
|
|
tsconfigCalls = filterDiagnosticsByURI(calls, "file:///src/tsconfig.json", len(calls)-1)
|
|
if len(tsconfigCalls) == 0 {
|
|
tsconfigCalls = filterDiagnosticsByURI(calls, "file:///src/tsconfig.json", 0)
|
|
}
|
|
lastTsconfigCall := tsconfigCalls[len(tsconfigCalls)-1]
|
|
assert.Equal(t, len(lastTsconfigCall.Params.Diagnostics), 0, "expected diagnostics to be cleared when validation is disabled")
|
|
})
|
|
|
|
t.Run("publishes global diagnostics after checking", func(t *testing.T) {
|
|
t.Parallel()
|
|
// Use a target/lib that does not include Disposable, then write code that needs it.
|
|
// This triggers a deferred "Cannot find global type 'Disposable'" global diagnostic
|
|
// during checking, which should be accumulated and published on the tsconfig URI.
|
|
files := map[string]any{
|
|
"/src/tsconfig.json": `{
|
|
"compilerOptions": {
|
|
"target": "es2020"
|
|
}
|
|
}`,
|
|
"/src/index.ts": `export function f() {
|
|
using x = { [Symbol.dispose]() {} };
|
|
}`,
|
|
}
|
|
session, utils := projecttestutil.Setup(files)
|
|
session.DidOpenFile(context.Background(), "file:///src/index.ts", 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
// Request semantic diagnostics to trigger checking, which triggers the global type resolvers.
|
|
ls, err := session.GetLanguageService(projecttestutil.WithRequestID(context.Background()), lsproto.DocumentUri("file:///src/index.ts"))
|
|
assert.NilError(t, err)
|
|
// Drain background tasks from DidOpenFile (publishProgramDiagnostics, etc.)
|
|
// before triggering global diagnostics, to avoid racing with publishGlobalDiagnostics.
|
|
session.WaitForBackgroundTasks()
|
|
|
|
_, err = ls.ProvideDiagnostics(projecttestutil.WithRequestID(context.Background()), lsproto.DocumentUri("file:///src/index.ts"))
|
|
assert.NilError(t, err)
|
|
// Enqueue global diagnostics publishing (normally done by the LSP server after each request).
|
|
session.EnqueuePublishGlobalDiagnostics()
|
|
session.WaitForBackgroundTasks()
|
|
|
|
calls := utils.Client().PublishDiagnosticsCalls()
|
|
// Find the last call for tsconfig.json
|
|
var lastTsconfigCall *struct {
|
|
Ctx context.Context
|
|
Params *lsproto.PublishDiagnosticsParams
|
|
}
|
|
for i := len(calls) - 1; i >= 0; i-- {
|
|
if calls[i].Params.Uri == "file:///src/tsconfig.json" {
|
|
lastTsconfigCall = &calls[i]
|
|
break
|
|
}
|
|
}
|
|
assert.Assert(t, lastTsconfigCall != nil, "expected PublishDiagnostics call for tsconfig.json")
|
|
// Should have global diagnostics (e.g., Cannot find global type 'Disposable')
|
|
hasGlobalDiag := false
|
|
for _, diag := range lastTsconfigCall.Params.Diagnostics {
|
|
if strings.Contains(diag.Message.AsString(), "Cannot find global") {
|
|
hasGlobalDiag = true
|
|
break
|
|
}
|
|
}
|
|
assert.Assert(t, hasGlobalDiag, "expected a 'Cannot find global' diagnostic on tsconfig.json, got: %v", lastTsconfigCall.Params.Diagnostics)
|
|
})
|
|
|
|
t.Run("cleans tsconfig diagnostics after TS files close and restores them after TS file is reopened", func(t *testing.T) {
|
|
t.Parallel()
|
|
files := map[string]any{
|
|
"/src/tsconfig.json": `{"compilerOptions": {"baseUrl": "."}}`,
|
|
"/src/index.ts": "export const x = 1;",
|
|
}
|
|
session, utils := projecttestutil.Setup(files)
|
|
uri := lsproto.DocumentUri("file:///src/index.ts")
|
|
session.DidOpenFile(context.Background(), uri, 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
_, err := session.GetLanguageService(context.Background(), uri)
|
|
assert.NilError(t, err)
|
|
session.WaitForBackgroundTasks()
|
|
|
|
calls := utils.Client().PublishDiagnosticsCalls()
|
|
tsconfigCalls := filterDiagnosticsByURI(calls, "file:///src/tsconfig.json", 0)
|
|
assert.Assert(t, len(tsconfigCalls) > 0, "expected PublishDiagnostics call for tsconfig.json after opening file")
|
|
assert.Equal(t, len(tsconfigCalls[0].Params.Diagnostics), 1, "expected one diagnostic on tsconfig.json after opening file")
|
|
|
|
callsBeforeClose := len(calls)
|
|
|
|
session.DidCloseFile(context.Background(), uri)
|
|
session.WaitForBackgroundTasks()
|
|
|
|
// Cleans up diagnostics after close
|
|
calls = utils.Client().PublishDiagnosticsCalls()
|
|
clearCalls := filterDiagnosticsByURI(calls, "file:///src/tsconfig.json", callsBeforeClose)
|
|
assert.Assert(t, len(clearCalls) > 0, "expected PublishDiagnostics call for tsconfig.json after project close")
|
|
lastClearCall := clearCalls[len(clearCalls)-1]
|
|
assert.Equal(t, len(lastClearCall.Params.Diagnostics), 0, "expected empty diagnostics after project close")
|
|
|
|
callsBeforeReopen := len(calls)
|
|
|
|
session.DidOpenFile(context.Background(), uri, 2, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
_, err = session.GetLanguageService(context.Background(), uri)
|
|
assert.NilError(t, err)
|
|
session.WaitForBackgroundTasks()
|
|
|
|
// Restores diagnostics after reopen
|
|
calls = utils.Client().PublishDiagnosticsCalls()
|
|
reopenedCalls := filterDiagnosticsByURI(calls, "file:///src/tsconfig.json", callsBeforeReopen)
|
|
assert.Assert(t, len(reopenedCalls) > 0, "expected PublishDiagnostics call for tsconfig.json after reopening file")
|
|
lastReopenedCall := reopenedCalls[len(reopenedCalls)-1]
|
|
assert.Equal(t, len(lastReopenedCall.Params.Diagnostics), 1, "expected one diagnostic on tsconfig.json after reopening file")
|
|
})
|
|
}
|
|
|
|
func TestDisplayName(t *testing.T) {
|
|
t.Parallel()
|
|
if !bundled.Embedded {
|
|
t.Skip("bundled files are not embedded")
|
|
}
|
|
|
|
t.Run("configured project returns relative config path", func(t *testing.T) {
|
|
t.Parallel()
|
|
files := map[string]any{
|
|
"/home/projects/tsconfig.json": `{}`,
|
|
"/home/projects/index.ts": "export const x = 1;",
|
|
}
|
|
session, _ := projecttestutil.Setup(files)
|
|
session.DidOpenFile(context.Background(), "file:///home/projects/index.ts", 1, "export const x = 1;", lsproto.LanguageKindTypeScript)
|
|
_, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///home/projects/index.ts"))
|
|
assert.NilError(t, err)
|
|
|
|
snapshot := session.Snapshot()
|
|
configured := snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/tsconfig.json"))
|
|
assert.Assert(t, configured != nil)
|
|
assert.Equal(t, configured.DisplayName("/home/projects"), "tsconfig.json")
|
|
})
|
|
|
|
t.Run("configured project with nested config", func(t *testing.T) {
|
|
t.Parallel()
|
|
files := map[string]any{
|
|
"/home/projects/sub/tsconfig.json": `{}`,
|
|
"/home/projects/sub/index.ts": "export const x = 1;",
|
|
}
|
|
session, _ := projecttestutil.Setup(files)
|
|
session.DidOpenFile(context.Background(), "file:///home/projects/sub/index.ts", 1, "export const x = 1;", lsproto.LanguageKindTypeScript)
|
|
_, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///home/projects/sub/index.ts"))
|
|
assert.NilError(t, err)
|
|
|
|
snapshot := session.Snapshot()
|
|
configured := snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/sub/tsconfig.json"))
|
|
assert.Assert(t, configured != nil)
|
|
assert.Equal(t, configured.DisplayName("/home/projects"), "sub/tsconfig.json")
|
|
})
|
|
|
|
t.Run("inferred project returns directory base name", func(t *testing.T) {
|
|
t.Parallel()
|
|
files := map[string]any{
|
|
"/home/projects/index.ts": "export const x = 1;",
|
|
}
|
|
session, _ := projecttestutil.SetupWithOptions(files, &project.SessionOptions{
|
|
CurrentDirectory: "/home/projects",
|
|
DefaultLibraryPath: bundled.LibPath(),
|
|
PositionEncoding: lsproto.PositionEncodingKindUTF8,
|
|
WatchEnabled: true,
|
|
LoggingEnabled: true,
|
|
PushDiagnosticsEnabled: true,
|
|
})
|
|
session.DidOpenFile(context.Background(), "file:///home/projects/index.ts", 1, "export const x = 1;", lsproto.LanguageKindTypeScript)
|
|
_, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///home/projects/index.ts"))
|
|
assert.NilError(t, err)
|
|
|
|
snapshot := session.Snapshot()
|
|
inferred := snapshot.ProjectCollection.InferredProject()
|
|
assert.Assert(t, inferred != nil)
|
|
name := inferred.DisplayName("/home")
|
|
assert.Equal(t, name, "projects")
|
|
})
|
|
}
|
|
|
|
func TestProgressNotifications(t *testing.T) {
|
|
t.Parallel()
|
|
if !bundled.Embedded {
|
|
t.Skip("bundled files are not embedded")
|
|
}
|
|
|
|
t.Run("emits progress for configured project loading", func(t *testing.T) {
|
|
t.Parallel()
|
|
files := map[string]any{
|
|
"/home/projects/tsconfig.json": `{}`,
|
|
"/home/projects/index.ts": "export const x = 1;",
|
|
}
|
|
session, utils := projecttestutil.Setup(files)
|
|
session.DidOpenFile(context.Background(), "file:///home/projects/index.ts", 1, "export const x = 1;", lsproto.LanguageKindTypeScript)
|
|
_, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///home/projects/index.ts"))
|
|
assert.NilError(t, err)
|
|
|
|
startCalls := utils.Client().ProgressStartCalls()
|
|
finishCalls := utils.Client().ProgressFinishCalls()
|
|
|
|
assert.Assert(t, len(startCalls) > 0, "expected at least one ProgressStart call")
|
|
assert.Assert(t, len(finishCalls) > 0, "expected at least one ProgressFinish call")
|
|
|
|
foundProjectStart := false
|
|
for _, call := range startCalls {
|
|
if call.Message == diagnostics.Project_0 {
|
|
foundProjectStart = true
|
|
break
|
|
}
|
|
}
|
|
assert.Assert(t, foundProjectStart, "expected ProgressStart with Project_0 message")
|
|
|
|
foundProjectFinish := false
|
|
for _, call := range finishCalls {
|
|
if call.Message == diagnostics.Project_0 {
|
|
foundProjectFinish = true
|
|
break
|
|
}
|
|
}
|
|
assert.Assert(t, foundProjectFinish, "expected ProgressFinish with Project_0 message")
|
|
})
|
|
|
|
t.Run("emits progress for inferred project loading", func(t *testing.T) {
|
|
t.Parallel()
|
|
files := map[string]any{
|
|
"/home/projects/index.ts": "export const x = 1;",
|
|
}
|
|
session, utils := projecttestutil.Setup(files)
|
|
session.DidOpenFile(context.Background(), "file:///home/projects/index.ts", 1, "export const x = 1;", lsproto.LanguageKindTypeScript)
|
|
_, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///home/projects/index.ts"))
|
|
assert.NilError(t, err)
|
|
|
|
startCalls := utils.Client().ProgressStartCalls()
|
|
finishCalls := utils.Client().ProgressFinishCalls()
|
|
|
|
assert.Assert(t, len(startCalls) > 0, "expected at least one ProgressStart call")
|
|
assert.Assert(t, len(finishCalls) > 0, "expected at least one ProgressFinish call")
|
|
|
|
foundProjectStart := false
|
|
for _, call := range startCalls {
|
|
if call.Message == diagnostics.Project_0 {
|
|
foundProjectStart = true
|
|
break
|
|
}
|
|
}
|
|
assert.Assert(t, foundProjectStart, "expected ProgressStart with Project_0 message")
|
|
})
|
|
|
|
t.Run("each start has a matching finish", func(t *testing.T) {
|
|
t.Parallel()
|
|
files := map[string]any{
|
|
"/home/projects/tsconfig.json": `{}`,
|
|
"/home/projects/a.ts": "export const a = 1;",
|
|
"/home/projects/b.ts": "export const b = 2;",
|
|
}
|
|
session, utils := projecttestutil.Setup(files)
|
|
session.DidOpenFile(context.Background(), "file:///home/projects/a.ts", 1, "export const a = 1;", lsproto.LanguageKindTypeScript)
|
|
_, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///home/projects/a.ts"))
|
|
assert.NilError(t, err)
|
|
|
|
startCalls := utils.Client().ProgressStartCalls()
|
|
finishCalls := utils.Client().ProgressFinishCalls()
|
|
|
|
starts := 0
|
|
finishes := 0
|
|
for _, call := range startCalls {
|
|
if call.Message == diagnostics.Project_0 {
|
|
starts++
|
|
}
|
|
}
|
|
for _, call := range finishCalls {
|
|
if call.Message == diagnostics.Project_0 {
|
|
finishes++
|
|
}
|
|
}
|
|
assert.Equal(t, starts, finishes, "ProgressStart and ProgressFinish calls for Project_0 should be balanced")
|
|
})
|
|
}
|