Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9e6e021
Factor out lsp server setup so we can use it for some unit testing
sheetalkamat Oct 10, 2025
b7dc8db
Add some tests that can verify the server multi project things
sheetalkamat Oct 31, 2025
28dd671
Rename tests
sheetalkamat Oct 14, 2025
1171089
Declaration map tests
sheetalkamat Oct 23, 2025
9516c6f
factor out todos
sheetalkamat Oct 3, 2025
f88b8f2
Handle existing projects
sheetalkamat Oct 8, 2025
4f25a86
Framework to queue more locations
sheetalkamat Oct 16, 2025
5d230c1
Use declaration maps to queue more projects
sheetalkamat Oct 17, 2025
fcfedab
Actually find and load the original location projects
sheetalkamat Oct 22, 2025
5470a9d
navigate to source when we can when reporting references
sheetalkamat Oct 23, 2025
12c0e93
Create ancestor tree
sheetalkamat Oct 10, 2025
64a2d44
Actually load ancestor project tree
sheetalkamat Oct 22, 2025
812c9bc
Fix the order of the results and deduplication
sheetalkamat Oct 24, 2025
fee7f97
Retain ancestor projects and references till open file is present
sheetalkamat Oct 24, 2025
70ca6da
Fix the config lifetime when getting project for non open file
sheetalkamat Oct 29, 2025
23f8f3f
Parallel
sheetalkamat Oct 29, 2025
2be521c
Port special localness checks to determine if we need to load project…
sheetalkamat Oct 30, 2025
0f005e7
Baseline workspace symbols
sheetalkamat Oct 30, 2025
3a76f2a
Workspace symbols to load more projects
sheetalkamat Oct 31, 2025
72e09da
Show output of panic found when updating fourslash tests and run them…
sheetalkamat Nov 3, 2025
e2c372c
Merge branch 'main' into multiProject
sheetalkamat Nov 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions internal/compiler/program.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,19 @@ func (p *Program) GetParseFileRedirect(fileName string) string {
return p.projectReferenceFileMapper.getParseFileRedirect(ast.NewHasFileName(fileName, p.toPath(fileName)))
}

func (p *Program) ForEachResolvedProjectReference(
fn func(path tspath.Path, config *tsoptions.ParsedCommandLine, parent *tsoptions.ParsedCommandLine, index int),
) {
p.projectReferenceFileMapper.forEachResolvedProjectReference(fn)
func (p *Program) GetResolvedProjectReferences() []*tsoptions.ParsedCommandLine {
return p.projectReferenceFileMapper.getResolvedProjectReferences()
}

func (p *Program) ForEachResolvedProjectReference(fn func(path tspath.Path, config *tsoptions.ParsedCommandLine, parent *tsoptions.ParsedCommandLine, index int) bool) bool {
return p.projectReferenceFileMapper.forEachResolvedProjectReference(fn)
}

func (p *Program) ForEachResolvedProjectReferenceInChildConfig(
childConfig *tsoptions.ParsedCommandLine,
fn func(path tspath.Path, config *tsoptions.ParsedCommandLine, parent *tsoptions.ParsedCommandLine, index int) bool,
) bool {
return p.projectReferenceFileMapper.forEachResolvedProjectReferenceInChildConfig(childConfig, fn)
}

// UseCaseSensitiveFileNames implements checker.Program.
Expand Down Expand Up @@ -904,13 +913,13 @@ func (p *Program) verifyProjectReferences() {
p.programDiagnostics = append(p.programDiagnostics, diag)
}

p.ForEachResolvedProjectReference(func(path tspath.Path, config *tsoptions.ParsedCommandLine, parent *tsoptions.ParsedCommandLine, index int) {
p.ForEachResolvedProjectReference(func(path tspath.Path, config *tsoptions.ParsedCommandLine, parent *tsoptions.ParsedCommandLine, index int) bool {
ref := parent.ProjectReferences()[index]
// !!! Deprecated in 5.0 and removed since 5.5
// verifyRemovedProjectReference(ref, parent, index);
if config == nil {
createDiagnosticForReference(parent, index, diagnostics.File_0_not_found, ref.Path)
return
return false
}
refOptions := config.CompilerOptions()
if !refOptions.Composite.IsTrue() || refOptions.NoEmit.IsTrue() {
Expand All @@ -927,6 +936,7 @@ func (p *Program) verifyProjectReferences() {
createDiagnosticForReference(parent, index, diagnostics.Cannot_write_file_0_because_it_will_overwrite_tsbuildinfo_file_generated_by_referenced_project_1, buildInfoFileName, ref.Path)
p.hasEmitBlockingDiagnostics.Add(p.toPath(buildInfoFileName))
}
return false
})
}

Expand Down
37 changes: 29 additions & 8 deletions internal/compiler/projectreferencefilemapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ func (mapper *projectReferenceFileMapper) getParseFileRedirect(file ast.HasFileN
}

func (mapper *projectReferenceFileMapper) getResolvedProjectReferences() []*tsoptions.ParsedCommandLine {
if mapper.opts.Config.ConfigFile == nil {
return nil
}
refs, ok := mapper.referencesInConfigFile[mapper.opts.Config.ConfigFile.SourceFile.Path()]
var result []*tsoptions.ParsedCommandLine
if ok {
Expand Down Expand Up @@ -107,31 +110,49 @@ func (mapper *projectReferenceFileMapper) getResolvedReferenceFor(path tspath.Pa
}

func (mapper *projectReferenceFileMapper) forEachResolvedProjectReference(
fn func(path tspath.Path, config *tsoptions.ParsedCommandLine, parent *tsoptions.ParsedCommandLine, index int),
) {
fn func(path tspath.Path, config *tsoptions.ParsedCommandLine, parent *tsoptions.ParsedCommandLine, index int) bool,
) bool {
if mapper.opts.Config.ConfigFile == nil {
return
return false
}
seenRef := collections.NewSetWithSizeHint[tspath.Path](len(mapper.referencesInConfigFile))
seenRef.Add(mapper.opts.Config.ConfigFile.SourceFile.Path())
refs := mapper.referencesInConfigFile[mapper.opts.Config.ConfigFile.SourceFile.Path()]
mapper.forEachResolvedReferenceWorker(refs, fn, mapper.opts.Config, seenRef)
return mapper.forEachResolvedReferenceWorker(refs, fn, mapper.opts.Config, seenRef)
}

func (mapper *projectReferenceFileMapper) forEachResolvedReferenceWorker(
references []tspath.Path,
fn func(path tspath.Path, config *tsoptions.ParsedCommandLine, parent *tsoptions.ParsedCommandLine, index int),
fn func(path tspath.Path, config *tsoptions.ParsedCommandLine, parent *tsoptions.ParsedCommandLine, index int) bool,
parent *tsoptions.ParsedCommandLine,
seenRef *collections.Set[tspath.Path],
) {
) bool {
for index, path := range references {
if !seenRef.AddIfAbsent(path) {
continue
}
config, _ := mapper.configToProjectReference[path]
fn(path, config, parent, index)
mapper.forEachResolvedReferenceWorker(mapper.referencesInConfigFile[path], fn, config, seenRef)
if fn(path, config, parent, index) {
return true
}
if mapper.forEachResolvedReferenceWorker(mapper.referencesInConfigFile[path], fn, config, seenRef) {
return true
}
}
return false
}

func (mapper *projectReferenceFileMapper) forEachResolvedProjectReferenceInChildConfig(
childConfig *tsoptions.ParsedCommandLine,
fn func(path tspath.Path, config *tsoptions.ParsedCommandLine, parent *tsoptions.ParsedCommandLine, index int) bool,
) bool {
if childConfig == nil || childConfig.ConfigFile == nil {
return false
}
seenRef := collections.NewSetWithSizeHint[tspath.Path](len(mapper.referencesInConfigFile))
seenRef.Add(childConfig.ConfigFile.SourceFile.Path())
refs := mapper.referencesInConfigFile[childConfig.ConfigFile.SourceFile.Path()]
return mapper.forEachResolvedReferenceWorker(refs, fn, mapper.opts.Config, seenRef)
}

func (mapper *projectReferenceFileMapper) getSourceToDtsIfSymlink(file ast.HasFileName) *tsoptions.SourceOutputAndProjectReference {
Expand Down
123 changes: 27 additions & 96 deletions internal/execute/tsctests/sys.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ package tsctests
import (
"fmt"
"io"
"io/fs"
"maps"
"slices"
"strconv"
"strings"
"sync"
Expand All @@ -14,8 +12,10 @@ import (
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/execute"
"github.com/microsoft/typescript-go/internal/execute/incremental"
"github.com/microsoft/typescript-go/internal/execute/tsc"
"github.com/microsoft/typescript-go/internal/testutil/fsbaselineutil"
"github.com/microsoft/typescript-go/internal/testutil/harnessutil"
"github.com/microsoft/typescript-go/internal/testutil/stringtestutil"
"github.com/microsoft/typescript-go/internal/tsoptions"
Expand Down Expand Up @@ -95,6 +95,20 @@ func NewTscSystem(files FileMap, useCaseSensitiveFileNames bool, cwd string) *Te
}
}

func GetFileMapWithBuild(files FileMap, commandLineArgs []string) FileMap {
sys := newTestSys(&tscInput{
files: maps.Clone(files),
}, false)
execute.CommandLine(sys, commandLineArgs, sys)
sys.fs.writtenFiles.Range(func(key string) bool {
if text, ok := sys.fsFromFileMap().ReadFile(key); ok {
files[key] = text
}
return true
})
return files
}

func newTestSys(tscInput *tscInput, forIncrementalCorrectness bool) *TestSys {
cwd := tscInput.cwd
if cwd == "" {
Expand All @@ -114,6 +128,11 @@ func newTestSys(tscInput *tscInput, forIncrementalCorrectness bool) *TestSys {
}, currentWrite)
sys.env = tscInput.env
sys.forIncrementalCorrectness = forIncrementalCorrectness
sys.fsDiffer = &fsbaselineutil.FSDiffer{
FS: sys.fs.FS.(iovfs.FsWithSys),
DefaultLibs: func() *collections.SyncSet[string] { return sys.fs.defaultLibs },
WrittenFiles: &sys.fs.writtenFiles,
}

// Ensure the default library file is present
sys.ensureLibPathExists("lib.d.ts")
Expand All @@ -126,24 +145,12 @@ func newTestSys(tscInput *tscInput, forIncrementalCorrectness bool) *TestSys {
return sys
}

type diffEntry struct {
content string
mTime time.Time
isWritten bool
symlinkTarget string
}

type snapshot struct {
snap map[string]*diffEntry
defaultLibs *collections.SyncSet[string]
}

type TestSys struct {
currentWrite *strings.Builder
programBaselines strings.Builder
programIncludeBaselines strings.Builder
tracer *harnessutil.TracerForBaselining
serializedDiff *snapshot
fsDiffer *fsbaselineutil.FSDiffer
forIncrementalCorrectness bool

fs *testFs
Expand Down Expand Up @@ -171,11 +178,11 @@ func (s *TestSys) FS() vfs.FS {
}

func (s *TestSys) fsFromFileMap() iovfs.FsWithSys {
return s.fs.FS.(iovfs.FsWithSys)
return s.fsDiffer.FS
}

func (s *TestSys) mapFs() *vfstest.MapFS {
return s.fsFromFileMap().FSys().(*vfstest.MapFS)
return s.fsDiffer.MapFs()
}

func (s *TestSys) ensureLibPathExists(path string) {
Expand Down Expand Up @@ -223,8 +230,8 @@ func (s *TestSys) OnEmittedFiles(result *compiler.EmitResult, mTimesCache *colle
if result != nil {
for _, file := range result.EmittedFiles {
modTime := s.mapFs().GetModTime(file)
if s.serializedDiff != nil {
if diff, ok := s.serializedDiff.snap[file]; ok && diff.mTime.Equal(modTime) {
if serializedDiff := s.fsDiffer.SerializedDiff(); serializedDiff != nil {
if diff, ok := serializedDiff.Snap[file]; ok && diff.MTime.Equal(modTime) {
// Even though written, timestamp was reverted
continue
}
Expand Down Expand Up @@ -500,83 +507,7 @@ func (s *TestSys) clearOutput() {
}

func (s *TestSys) baselineFSwithDiff(baseline io.Writer) {
// todo: baselines the entire fs, possibly doesn't correctly diff all cases of emitted files, since emit isn't fully implemented and doesn't always emit the same way as strada
snap := map[string]*diffEntry{}

diffs := map[string]string{}

for path, file := range s.mapFs().Entries() {
if file.Mode&fs.ModeSymlink != 0 {
target, ok := s.mapFs().GetTargetOfSymlink(path)
if !ok {
panic("Failed to resolve symlink target: " + path)
}
newEntry := &diffEntry{symlinkTarget: target}
snap[path] = newEntry
s.addFsEntryDiff(diffs, newEntry, path)
continue
} else if file.Mode.IsRegular() {
newEntry := &diffEntry{content: string(file.Data), mTime: file.ModTime, isWritten: s.fs.writtenFiles.Has(path)}
snap[path] = newEntry
s.addFsEntryDiff(diffs, newEntry, path)
}
}
if s.serializedDiff != nil {
for path := range s.serializedDiff.snap {
if fileInfo := s.mapFs().GetFileInfo(path); fileInfo == nil {
// report deleted
s.addFsEntryDiff(diffs, nil, path)
}
}
}
var defaultLibs collections.SyncSet[string]
if s.fs.defaultLibs != nil {
s.fs.defaultLibs.Range(func(libPath string) bool {
defaultLibs.Add(libPath)
return true
})
}
s.serializedDiff = &snapshot{
snap: snap,
defaultLibs: &defaultLibs,
}
diffKeys := slices.Collect(maps.Keys(diffs))
slices.Sort(diffKeys)
for _, path := range diffKeys {
fmt.Fprint(baseline, "//// ["+path+"] ", diffs[path], "\n")
}
fmt.Fprintln(baseline)
s.fs.writtenFiles = collections.SyncSet[string]{} // Reset written files after baseline
}

func (s *TestSys) addFsEntryDiff(diffs map[string]string, newDirContent *diffEntry, path string) {
var oldDirContent *diffEntry
var defaultLibs *collections.SyncSet[string]
if s.serializedDiff != nil {
oldDirContent = s.serializedDiff.snap[path]
defaultLibs = s.serializedDiff.defaultLibs
}
// todo handle more cases of fs changes
if oldDirContent == nil {
if s.fs.defaultLibs == nil || !s.fs.defaultLibs.Has(path) {
if newDirContent.symlinkTarget != "" {
diffs[path] = "-> " + newDirContent.symlinkTarget + " *new*"
} else {
diffs[path] = "*new* \n" + newDirContent.content
}
}
} else if newDirContent == nil {
diffs[path] = "*deleted*"
} else if newDirContent.content != oldDirContent.content {
diffs[path] = "*modified* \n" + newDirContent.content
} else if newDirContent.isWritten {
diffs[path] = "*rewrite with same content*"
} else if newDirContent.mTime != oldDirContent.mTime {
diffs[path] = "*mTime changed*"
} else if defaultLibs != nil && defaultLibs.Has(path) && s.fs.defaultLibs != nil && !s.fs.defaultLibs.Has(path) {
// Lib file that was read
diffs[path] = "*Lib*\n" + newDirContent.content
}
s.fsDiffer.BaselineFSwithDiff(baseline)
}

func (s *TestSys) writeFileNoError(path string, content string, writeByteOrderMark bool) {
Expand Down
4 changes: 2 additions & 2 deletions internal/fourslash/_scripts/updateFailing.mts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ function main() {
const go = which.sync("go");
let testOutput: string;
try {
testOutput = cp.execFileSync(go, ["test", "./internal/fourslash/tests/gen"], { encoding: "utf-8" });
testOutput = cp.execFileSync(go, ["test", "-v", "./internal/fourslash/tests/gen"], { encoding: "utf-8" });
}
catch (error) {
testOutput = (error as { stdout: string; }).stdout as string;
}
const panicRegex = /^panic/m;
if (panicRegex.test(testOutput)) {
fs.writeFileSync(failingTestsPath, oldFailingTests, "utf-8");
throw new Error("Unrecovered panic detected in tests");
throw new Error("Unrecovered panic detected in tests\n" + testOutput);
}
const failRegex = /--- FAIL: ([\S]+)/gm;
const failingTests: string[] = [];
Expand Down
Loading