diff --git a/internal/compiler/checkerpool.go b/internal/compiler/checkerpool.go index 84ea8d824a6..3b0d68633b2 100644 --- a/internal/compiler/checkerpool.go +++ b/internal/compiler/checkerpool.go @@ -10,10 +10,14 @@ import ( "github.com/microsoft/typescript-go/internal/core" ) +// CheckerPool is implemented by the project system to provide checkers with +// request-scoped lifetime and reclamation. It returns a checker and a release +// function that must be called when the caller is done with the checker. +// The returned checker must not be accessed concurrently; each acquisition is exclusive. +// If file is non-nil, the pool may use it as an affinity hint to return the same +// checker for the same file across calls. type CheckerPool interface { - GetChecker(ctx context.Context) (*checker.Checker, func()) - GetCheckerForFile(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) - GetCheckerForFileExclusive(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) + GetChecker(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) } type checkerPool struct { @@ -46,12 +50,29 @@ func newCheckerPool(program *Program) *checkerPool { return pool } -func (p *checkerPool) GetCheckerForFile(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) { +// GetChecker implements CheckerPool. When file is non-nil, returns the checker +// associated with that file; otherwise returns the first checker. +func (p *checkerPool) GetChecker(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) { + if file != nil { + return p.getCheckerForFileExclusive(ctx, file) + } + p.createCheckers() + c := p.checkers[0] + p.locks[0].Lock() + return c, sync.OnceFunc(func() { + p.locks[0].Unlock() + }) +} + +// getCheckerForFileNonExclusive returns the checker for the given file without locking. +// This is only safe when the caller guarantees no concurrent access to the same checker, +// e.g. for read-only operations like obtaining an emit resolver. +func (p *checkerPool) getCheckerForFileNonExclusive(file *ast.SourceFile) (*checker.Checker, func()) { p.createCheckers() return p.fileAssociations[file], noop } -func (p *checkerPool) GetCheckerForFileExclusive(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) { +func (p *checkerPool) getCheckerForFileExclusive(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) { p.createCheckers() c := p.fileAssociations[file] idx := slices.Index(p.checkers, c) @@ -61,10 +82,10 @@ func (p *checkerPool) GetCheckerForFileExclusive(ctx context.Context, file *ast. }) } -func (p *checkerPool) GetChecker(ctx context.Context) (*checker.Checker, func()) { +// getCheckerNonExclusive returns the first checker without locking. +func (p *checkerPool) getCheckerNonExclusive() (*checker.Checker, func()) { p.createCheckers() - checker := p.checkers[0] - return checker, noop + return p.checkers[0], noop } func (p *checkerPool) createCheckers() { @@ -101,4 +122,35 @@ func (p *checkerPool) forEachCheckerParallel(cb func(idx int, c *checker.Checker wg.RunAndWait() } +func (p *checkerPool) GetGlobalDiagnostics() []*ast.Diagnostic { + p.createCheckers() + globalDiagnostics := make([][]*ast.Diagnostic, len(p.checkers)) + p.forEachCheckerParallel(func(idx int, checker *checker.Checker) { + globalDiagnostics[idx] = checker.GetGlobalDiagnostics() + }) + return SortAndDeduplicateDiagnostics(slices.Concat(globalDiagnostics...)) +} + +// forEachCheckerGroupDo runs one task per checker in parallel. Each task iterates +// the provided files, processing only those assigned to its checker. Within each +// checker's set, files are visited in their original order. +func (p *checkerPool) forEachCheckerGroupDo(ctx context.Context, files []*ast.SourceFile, singleThreaded bool, cb func(c *checker.Checker, fileIndex int, file *ast.SourceFile)) { + p.createCheckers() + + checkerCount := len(p.checkers) + wg := core.NewWorkGroup(singleThreaded) + for checkerIdx := range checkerCount { + wg.Queue(func() { + p.locks[checkerIdx].Lock() + defer p.locks[checkerIdx].Unlock() + for i, file := range files { + if checker := p.checkers[checkerIdx]; checker == p.fileAssociations[file] { + cb(checker, i, file) + } + } + }) + } + wg.RunAndWait() +} + func noop() {} diff --git a/internal/compiler/program.go b/internal/compiler/program.go index 92a3ae384e1..415ea1d8924 100644 --- a/internal/compiler/program.go +++ b/internal/compiler/program.go @@ -75,7 +75,12 @@ type packageNamesInfo struct { type Program struct { opts ProgramOptions - checkerPool CheckerPool + checkerPool CheckerPool // always set; used as fallback for project system pools + + // compilerCheckerPool is set only when the built-in compiler checker pool is in use + // (i.e. CreateCheckerPool was not provided). It enables grouped parallel iteration, + // non-exclusive access for emit, and direct global diagnostics collection. + compilerCheckerPool *checkerPool comparePathsOptions tspath.ComparePathsOptions @@ -313,7 +318,9 @@ func (p *Program) initCheckerPool() { if p.opts.CreateCheckerPool != nil { p.checkerPool = p.opts.CreateCheckerPool(p) } else { - p.checkerPool = newCheckerPool(p) + pool := newCheckerPool(p) + p.checkerPool = pool + p.compilerCheckerPool = pool } } @@ -407,12 +414,15 @@ func (p *Program) BindSourceFiles() { // Return the type checker associated with the program. func (p *Program) GetTypeChecker(ctx context.Context) (*checker.Checker, func()) { - return p.checkerPool.GetChecker(ctx) + if p.compilerCheckerPool != nil { + return p.compilerCheckerPool.getCheckerNonExclusive() + } + return p.checkerPool.GetChecker(ctx, nil) } func (p *Program) ForEachCheckerParallel(cb func(idx int, c *checker.Checker)) { - if pool, ok := p.checkerPool.(*checkerPool); ok { - pool.forEachCheckerParallel(cb) + if p.compilerCheckerPool != nil { + p.compilerCheckerPool.forEachCheckerParallel(cb) } } @@ -421,13 +431,19 @@ func (p *Program) ForEachCheckerParallel(cb func(idx int, c *checker.Checker)) { // types obtained from different checkers, so only non-type data (such as diagnostics or string // representations of types) should be obtained from checkers returned by this method. func (p *Program) GetTypeCheckerForFile(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) { - return p.checkerPool.GetCheckerForFile(ctx, file) + if p.compilerCheckerPool != nil { + return p.compilerCheckerPool.getCheckerForFileNonExclusive(file) + } + return p.checkerPool.GetChecker(ctx, file) } // Return a checker for the given file, locked to the current thread to prevent data races from multiple threads // accessing the same checker. The lock will be released when the `done` function is called. func (p *Program) GetTypeCheckerForFileExclusive(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) { - return p.checkerPool.GetCheckerForFileExclusive(ctx, file) + if p.compilerCheckerPool != nil { + return p.compilerCheckerPool.getCheckerForFileExclusive(ctx, file) + } + return p.checkerPool.GetChecker(ctx, file) } func (p *Program) GetResolvedModule(file ast.HasFileName, moduleReference string, mode core.ResolutionMode) *module.ResolvedModule { @@ -477,6 +493,48 @@ func (p *Program) collectDiagnosticsFromFiles(ctx context.Context, sourceFiles [ return diagnostics } +// collectCheckerDiagnostics collects diagnostics from a single file or all files, +// using a callback that receives the checker for each file. When the checker pool +// supports grouped iteration (compiler pool), files are grouped by checker and +// processed in parallel with one task per checker, reducing contention and improving +// cache locality. Otherwise, falls back to per-file concurrent collection. +func (p *Program) collectCheckerDiagnostics(ctx context.Context, sourceFile *ast.SourceFile, collect func(context.Context, *checker.Checker, *ast.SourceFile) []*ast.Diagnostic) []*ast.Diagnostic { + if sourceFile != nil { + if p.SkipTypeChecking(sourceFile, false) { + return nil + } + c, done := p.GetTypeCheckerForFileExclusive(ctx, sourceFile) + result := collect(ctx, c, sourceFile) + done() + return SortAndDeduplicateDiagnostics(result) + } + return SortAndDeduplicateDiagnostics(slices.Concat(p.collectCheckerDiagnosticsFromFiles(ctx, p.files, collect)...)) +} + +// collectCheckerDiagnosticsFromFiles collects checker diagnostics for a list of files. +func (p *Program) collectCheckerDiagnosticsFromFiles(ctx context.Context, sourceFiles []*ast.SourceFile, collect func(context.Context, *checker.Checker, *ast.SourceFile) []*ast.Diagnostic) [][]*ast.Diagnostic { + diagnostics := make([][]*ast.Diagnostic, len(sourceFiles)) + if p.compilerCheckerPool != nil { + p.compilerCheckerPool.forEachCheckerGroupDo(ctx, sourceFiles, p.SingleThreaded(), func(c *checker.Checker, fileIndex int, file *ast.SourceFile) { + diagnostics[fileIndex] = collect(ctx, c, file) + }) + } else { + wg := core.NewWorkGroup(p.SingleThreaded()) + for i, file := range sourceFiles { + if p.SkipTypeChecking(file, false) { + continue + } + wg.Queue(func() { + c, done := p.checkerPool.GetChecker(ctx, file) + diagnostics[i] = collect(ctx, c, file) + done() + }) + } + wg.RunAndWait() + } + return diagnostics +} + func (p *Program) GetSyntacticDiagnostics(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic { return p.collectDiagnostics(ctx, sourceFile, false /*concurrent*/, func(_ context.Context, file *ast.SourceFile) []*ast.Diagnostic { diags := core.Concatenate(file.Diagnostics(), file.JSDiagnostics()) @@ -533,20 +591,20 @@ func (p *Program) GetBindDiagnostics(ctx context.Context, sourceFile *ast.Source } func (p *Program) GetSemanticDiagnostics(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic { - return p.collectDiagnostics(ctx, sourceFile, true /*concurrent*/, p.getSemanticDiagnosticsForFile) + return p.collectCheckerDiagnostics(ctx, sourceFile, p.getSemanticDiagnosticsWithChecker) } func (p *Program) GetSemanticDiagnosticsWithoutNoEmitFiltering(ctx context.Context, sourceFiles []*ast.SourceFile) map[*ast.SourceFile][]*ast.Diagnostic { - diagnostics := p.collectDiagnosticsFromFiles(ctx, sourceFiles, true /*concurrent*/, p.getBindAndCheckDiagnosticsForFile) + allDiags := p.collectCheckerDiagnosticsFromFiles(ctx, sourceFiles, p.getBindAndCheckDiagnosticsWithChecker) result := make(map[*ast.SourceFile][]*ast.Diagnostic, len(sourceFiles)) - for i, diags := range diagnostics { + for i, diags := range allDiags { result[sourceFiles[i]] = SortAndDeduplicateDiagnostics(diags) } return result } func (p *Program) GetSuggestionDiagnostics(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic { - return p.collectDiagnostics(ctx, sourceFile, true /*concurrent*/, p.getSuggestionDiagnosticsForFile) + return p.collectCheckerDiagnostics(ctx, sourceFile, p.getSuggestionDiagnosticsWithChecker) } func (p *Program) GetProgramDiagnostics() []*ast.Diagnostic { @@ -1168,32 +1226,18 @@ func (p *Program) GetGlobalDiagnostics(ctx context.Context) []*ast.Diagnostic { if len(p.files) == 0 { return nil } - - pool := p.checkerPool.(*checkerPool) - - globalDiagnostics := make([][]*ast.Diagnostic, len(pool.checkers)) - pool.forEachCheckerParallel(func(idx int, checker *checker.Checker) { - globalDiagnostics[idx] = checker.GetGlobalDiagnostics() - }) - - return SortAndDeduplicateDiagnostics(slices.Concat(globalDiagnostics...)) + if p.compilerCheckerPool != nil { + return p.compilerCheckerPool.GetGlobalDiagnostics() + } + // For external pools (project system), global diagnostics are collected + // incrementally as checkers are used, not via a bulk query. + return nil } func (p *Program) GetDeclarationDiagnostics(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic { return p.collectDiagnostics(ctx, sourceFile, true /*concurrent*/, p.getDeclarationDiagnosticsForFile) } -func (p *Program) GetOptionsDiagnostics(ctx context.Context) []*ast.Diagnostic { - return SortAndDeduplicateDiagnostics(core.Concatenate(p.GetGlobalDiagnostics(ctx), p.getOptionsDiagnosticsOfConfigFile())) -} - -func (p *Program) getOptionsDiagnosticsOfConfigFile() []*ast.Diagnostic { - if p.Options() == nil || p.Options().ConfigFilePath == "" { - return nil - } - return p.GetConfigFileParsingDiagnostics() -} - func FilterNoEmitSemanticDiagnostics(diagnostics []*ast.Diagnostic, options *core.CompilerOptions) []*ast.Diagnostic { if !options.NoEmit.IsTrue() { return diagnostics @@ -1203,31 +1247,25 @@ func FilterNoEmitSemanticDiagnostics(diagnostics []*ast.Diagnostic, options *cor }) } -func (p *Program) getSemanticDiagnosticsForFile(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic { +func (p *Program) getSemanticDiagnosticsWithChecker(ctx context.Context, c *checker.Checker, sourceFile *ast.SourceFile) []*ast.Diagnostic { return core.Concatenate( - FilterNoEmitSemanticDiagnostics(p.getBindAndCheckDiagnosticsForFile(ctx, sourceFile), p.Options()), + FilterNoEmitSemanticDiagnostics(p.getBindAndCheckDiagnosticsWithChecker(ctx, c, sourceFile), p.Options()), p.GetIncludeProcessorDiagnostics(sourceFile), ) } -// getBindAndCheckDiagnosticsForFile gets semantic diagnostics for a single file, -// including bind diagnostics, checker diagnostics, and handling of @ts-ignore/@ts-expect-error directives. -func (p *Program) getBindAndCheckDiagnosticsForFile(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic { +// getBindAndCheckDiagnosticsWithChecker gets semantic diagnostics for a single file using a +// caller-provided checker, including bind diagnostics, checker diagnostics, and handling +// of @ts-ignore/@ts-expect-error directives. +func (p *Program) getBindAndCheckDiagnosticsWithChecker(ctx context.Context, fileChecker *checker.Checker, sourceFile *ast.SourceFile) []*ast.Diagnostic { compilerOptions := p.Options() if p.SkipTypeChecking(sourceFile, false) { return nil } - // IIFE to release checker as soon as possible. - diags := func() []*ast.Diagnostic { - fileChecker, done := p.checkerPool.GetCheckerForFileExclusive(ctx, sourceFile) - defer done() - - // Getting a checker will force a bind, so this will be populated. - diags := slices.Clip(sourceFile.BindDiagnostics()) - diags = append(diags, fileChecker.GetDiagnostics(ctx, sourceFile)...) - return diags - }() + // Checker creation forces binding, so bind diagnostics will be populated. + diags := slices.Clip(sourceFile.BindDiagnostics()) + diags = append(diags, fileChecker.GetDiagnostics(ctx, sourceFile)...) isPlainJS := ast.IsPlainJSFile(sourceFile, compilerOptions.CheckJs) if isPlainJS { @@ -1304,15 +1342,12 @@ func (p *Program) getDeclarationDiagnosticsForFile(ctx context.Context, sourceFi return diagnostics } -func (p *Program) getSuggestionDiagnosticsForFile(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic { +func (p *Program) getSuggestionDiagnosticsWithChecker(ctx context.Context, fileChecker *checker.Checker, sourceFile *ast.SourceFile) []*ast.Diagnostic { if p.SkipTypeChecking(sourceFile, false) { return nil } - fileChecker, done := p.checkerPool.GetCheckerForFileExclusive(ctx, sourceFile) - defer done() - - // Getting a checker will force a bind, so this will be populated. + // Checker creation forces binding, so bind suggestion diagnostics will be populated. diags := slices.Clip(sourceFile.BindSuggestionDiagnostics) diags = append(diags, fileChecker.GetSuggestionDiagnostics(ctx, sourceFile)...) @@ -1591,7 +1626,6 @@ type ProgramLike interface { GetConfigFileParsingDiagnostics() []*ast.Diagnostic GetSyntacticDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic GetBindDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic - GetOptionsDiagnostics(ctx context.Context) []*ast.Diagnostic GetProgramDiagnostics() []*ast.Diagnostic GetGlobalDiagnostics(ctx context.Context) []*ast.Diagnostic GetSemanticDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic @@ -1640,18 +1674,16 @@ func GetDiagnosticsOfAnyProgram( allDiagnostics = append(allDiagnostics, program.GetProgramDiagnostics()...) if len(allDiagnostics) == configFileParsingDiagnosticsLength { - // Options diagnostics include global diagnostics (even though we collect them separately), - // and global diagnostics create checkers, which then bind all of the files. Do this binding - // early so we can track the time. + // Do binding early so we can track the time. getBindDiagnostics(ctx, file) - allDiagnostics = append(allDiagnostics, program.GetOptionsDiagnostics(ctx)...) - if program.Options().ListFilesOnly.IsFalseOrUnknown() { allDiagnostics = append(allDiagnostics, program.GetGlobalDiagnostics(ctx)...) if len(allDiagnostics) == configFileParsingDiagnosticsLength { allDiagnostics = append(allDiagnostics, getSemanticDiagnostics(ctx, file)...) + // Ask for the global diagnostics again (they were empty above); we may have found new during checking, e.g. missing globals. + allDiagnostics = append(allDiagnostics, program.GetGlobalDiagnostics(ctx)...) } if (skipNoEmitCheckForDtsDiagnostics || program.Options().NoEmit.IsTrue()) && program.Options().GetEmitDeclarations() && len(allDiagnostics) == configFileParsingDiagnosticsLength { diff --git a/internal/execute/incremental/program.go b/internal/execute/incremental/program.go index fe77b2996ba..76dad5e07c9 100644 --- a/internal/execute/incremental/program.go +++ b/internal/execute/incremental/program.go @@ -132,12 +132,6 @@ func (p *Program) GetBindDiagnostics(ctx context.Context, file *ast.SourceFile) return p.program.GetBindDiagnostics(ctx, file) } -// GetOptionsDiagnostics implements compiler.AnyProgram interface. -func (p *Program) GetOptionsDiagnostics(ctx context.Context) []*ast.Diagnostic { - p.panicIfNoProgram("GetOptionsDiagnostics") - return p.program.GetOptionsDiagnostics(ctx) -} - func (p *Program) GetProgramDiagnostics() []*ast.Diagnostic { p.panicIfNoProgram("GetProgramDiagnostics") return p.program.GetProgramDiagnostics() @@ -365,7 +359,6 @@ func (p *Program) ensureHasErrorsForState(ctx context.Context, program *compiler len(program.GetConfigFileParsingDiagnostics()) > 0 || len(program.GetSyntacticDiagnostics(ctx, nil)) > 0 || len(program.GetProgramDiagnostics()) > 0 || - len(program.GetOptionsDiagnostics(ctx)) > 0 || len(program.GetGlobalDiagnostics(ctx)) > 0 { p.snapshot.hasErrors = core.TSTrue // Dont need to encode semantic errors state since the syntax and program diagnostics are encoded as present diff --git a/internal/lsp/server.go b/internal/lsp/server.go index f631de4609c..8c81d94a11a 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -777,6 +777,9 @@ func registerLanguageServiceDocumentRequestHandler[Req lsproto.HasTextDocumentUR return func() error { defer s.recover(req) resp, lsErr := fn(s, ctx, ls, params) + // After any language service request, check if new global diagnostics were + // discovered during checking and push updated tsconfig diagnostics if so. + s.session.EnqueuePublishGlobalDiagnostics() if lsErr != nil { return lsErr } diff --git a/internal/project/checkerpool.go b/internal/project/checkerpool.go index ec84b928cd7..f05df1cb3b2 100644 --- a/internal/project/checkerpool.go +++ b/internal/project/checkerpool.go @@ -3,6 +3,7 @@ package project import ( "context" "fmt" + "slices" "sync" "github.com/microsoft/typescript-go/internal/ast" @@ -15,35 +16,37 @@ type CheckerPool struct { maxCheckers int program *compiler.Program - mu sync.Mutex - cond *sync.Cond - createCheckersOnce sync.Once - checkers []*checker.Checker - locks []sync.Mutex - inUse map[*checker.Checker]bool - fileAssociations map[*ast.SourceFile]int - requestAssociations map[string]int - log func(msg string) + mu sync.Mutex + cond *sync.Cond + createCheckersOnce sync.Once + checkers []*checker.Checker + inUse map[*checker.Checker]bool + fileAssociations map[*ast.SourceFile]int + requestAssociations map[string]int + log func(msg string) + globalDiagAccumulated []*ast.Diagnostic + globalDiagChanged bool + globalDiagCheckerCount []int // per-checker count of globals last seen } var _ compiler.CheckerPool = (*CheckerPool)(nil) func newCheckerPool(maxCheckers int, program *compiler.Program, log func(msg string)) *CheckerPool { pool := &CheckerPool{ - program: program, - maxCheckers: maxCheckers, - checkers: make([]*checker.Checker, maxCheckers), - locks: make([]sync.Mutex, maxCheckers), - inUse: make(map[*checker.Checker]bool), - requestAssociations: make(map[string]int), - log: log, + program: program, + maxCheckers: maxCheckers, + checkers: make([]*checker.Checker, maxCheckers), + inUse: make(map[*checker.Checker]bool), + requestAssociations: make(map[string]int), + log: log, + globalDiagCheckerCount: make([]int, maxCheckers), } pool.cond = sync.NewCond(&pool.mu) return pool } -func (p *CheckerPool) GetCheckerForFile(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) { +func (p *CheckerPool) GetChecker(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) { p.mu.Lock() defer p.mu.Unlock() @@ -54,41 +57,35 @@ func (p *CheckerPool) GetCheckerForFile(ctx context.Context, file *ast.SourceFil } } - if p.fileAssociations == nil { - p.fileAssociations = make(map[*ast.SourceFile]int) - } + if file != nil { + if p.fileAssociations == nil { + p.fileAssociations = make(map[*ast.SourceFile]int) + } - if index, ok := p.fileAssociations[file]; ok { - checker := p.checkers[index] - if checker != nil { - if inUse := p.inUse[checker]; !inUse { - p.inUse[checker] = true - if requestID != "" { - p.requestAssociations[requestID] = index + if index, ok := p.fileAssociations[file]; ok { + checker := p.checkers[index] + if checker != nil { + if inUse := p.inUse[checker]; !inUse { + p.inUse[checker] = true + if requestID != "" { + p.requestAssociations[requestID] = index + } + return checker, p.createRelease(requestID, index, checker) } - return checker, p.createRelease(requestID, index, checker) } } } checker, index := p.getCheckerLocked(requestID) - p.fileAssociations[file] = index + if file != nil { + if p.fileAssociations == nil { + p.fileAssociations = make(map[*ast.SourceFile]int) + } + p.fileAssociations[file] = index + } return checker, p.createRelease(requestID, index, checker) } -// GetCheckerForFileExclusive is the same as GetCheckerForFile but also locks a mutex associated with the checker. -// Call `done` to free the lock. -func (p *CheckerPool) GetCheckerForFileExclusive(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) { - return p.GetCheckerForFile(ctx, file) -} - -func (p *CheckerPool) GetChecker(ctx context.Context) (*checker.Checker, func()) { - p.mu.Lock() - defer p.mu.Unlock() - checker, index := p.getCheckerLocked(core.GetRequestID(ctx)) - return checker, p.createRelease(core.GetRequestID(ctx), index, checker) -} - func (p *CheckerPool) getCheckerLocked(requestID string) (*checker.Checker, int) { if checker, index := p.getImmediatelyAvailableChecker(); checker != nil { p.inUse[checker] = true @@ -166,13 +163,49 @@ func (p *CheckerPool) createRelease(requestId string, index int, checker *checke p.log(fmt.Sprintf("checkerpool: Checker for request %s was canceled, disposing it", requestId)) p.checkers[index] = nil delete(p.inUse, checker) + p.globalDiagCheckerCount[index] = 0 } else { + p.mergeGlobalDiagnosticsFromCheckerLocked(index, checker) p.inUse[checker] = false } p.cond.Signal() } } +// mergeGlobalDiagnosticsFromCheckerLocked checks if the given checker has produced new global +// diagnostics since the last time we looked, and if so merges them into the accumulated set. +// Must be called with p.mu held. +func (p *CheckerPool) mergeGlobalDiagnosticsFromCheckerLocked(index int, c *checker.Checker) { + globals := c.GetGlobalDiagnostics() + if len(globals) == p.globalDiagCheckerCount[index] { + return + } + p.globalDiagCheckerCount[index] = len(globals) + before := len(p.globalDiagAccumulated) + p.globalDiagAccumulated = compiler.SortAndDeduplicateDiagnostics(append(p.globalDiagAccumulated, globals...)) + if len(p.globalDiagAccumulated) != before { + p.globalDiagChanged = true + } +} + +// GetGlobalDiagnostics returns the accumulated global diagnostics collected from +// all checkers that have been used so far in this pool's lifetime. +func (p *CheckerPool) GetGlobalDiagnostics() []*ast.Diagnostic { + p.mu.Lock() + defer p.mu.Unlock() + return slices.Clone(p.globalDiagAccumulated) +} + +// TakeNewGlobalDiagnostics reports whether new global diagnostics have been +// accumulated since the last call, and resets the flag. +func (p *CheckerPool) TakeNewGlobalDiagnostics() bool { + p.mu.Lock() + defer p.mu.Unlock() + changed := p.globalDiagChanged + p.globalDiagChanged = false + return changed +} + func (p *CheckerPool) isFullLocked() bool { for _, checker := range p.checkers { if checker == nil { @@ -193,29 +226,4 @@ func (p *CheckerPool) createCheckerLocked() (*checker.Checker, int) { panic("called createCheckerLocked when pool is full") } -func (p *CheckerPool) isRequestCheckerInUse(requestID string) bool { - p.mu.Lock() - defer p.mu.Unlock() - - if index, ok := p.requestAssociations[requestID]; ok { - checker := p.checkers[index] - if checker != nil { - return p.inUse[checker] - } - } - return false -} - -func (p *CheckerPool) size() int { - p.mu.Lock() - defer p.mu.Unlock() - size := 0 - for _, checker := range p.checkers { - if checker != nil { - size++ - } - } - return size -} - func noop() {} diff --git a/internal/project/project.go b/internal/project/project.go index 7174bd34bc3..e873c3c79fa 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -1,10 +1,12 @@ package project import ( + "context" "fmt" "strings" "sync" + "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/compiler" "github.com/microsoft/typescript-go/internal/core" @@ -207,6 +209,20 @@ func (p *Project) GetProgram() *compiler.Program { return p.Program } +// GetProjectDiagnostics returns program diagnostics combined with any global +// diagnostics discovered during checking. These are the diagnostics reported on +// the tsconfig.json file. +func (p *Project) GetProjectDiagnostics(ctx context.Context) []*ast.Diagnostic { + var globalDiags []*ast.Diagnostic + if p.checkerPool != nil { + globalDiags = p.checkerPool.GetGlobalDiagnostics() + } + return compiler.SortAndDeduplicateDiagnostics(core.Concatenate( + p.Program.GetProgramDiagnostics(), + globalDiags, + )) +} + func (p *Project) HasFile(fileName string) bool { return p.containsFile(p.toPath(fileName)) } diff --git a/internal/project/project_test.go b/internal/project/project_test.go index bdf8517d844..b92f27e0a3f 100644 --- a/internal/project/project_test.go +++ b/internal/project/project_test.go @@ -2,6 +2,7 @@ package project_test import ( "context" + "strings" "testing" "github.com/microsoft/typescript-go/internal/bundled" @@ -304,6 +305,56 @@ func TestPushDiagnostics(t *testing.T) { // 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("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) + _, 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, "Cannot find global") { + hasGlobalDiag = true + break + } + } + assert.Assert(t, hasGlobalDiag, "expected a 'Cannot find global' diagnostic on tsconfig.json, got: %v", lastTsconfigCall.Params.Diagnostics) + }) } func TestDisplayName(t *testing.T) { diff --git a/internal/project/session.go b/internal/project/session.go index ec35008fef9..74d8505021b 100644 --- a/internal/project/session.go +++ b/internal/project/session.go @@ -140,6 +140,11 @@ type Session struct { // are using each glob. watches map[fileSystemWatcherKey]*fileSystemWatcherValue watchesMu sync.Mutex + + // globalDiagPublishPending is set to true when a global diagnostics publish + // task should be enqueued. It is reset when the task runs, coalescing multiple + // requests into a single background task. + globalDiagPublishPending atomic.Bool } func NewSession(init *SessionInit) *Session { @@ -1084,7 +1089,7 @@ func (s *Session) publishProgramDiagnostics(oldSnapshot *Snapshot, newSnapshot * if !shouldPublishProgramDiagnostics(addedProject, newSnapshot.ID()) { return } - s.publishProjectDiagnostics(ctx, string(configFilePath), addedProject.Program.GetProgramDiagnostics(), newSnapshot.converters) + s.publishProjectDiagnostics(ctx, string(configFilePath), addedProject.GetProjectDiagnostics(ctx), newSnapshot.converters) }, func(configFilePath tspath.Path, removedProject *Project) { if removedProject.Kind != KindConfigured { @@ -1096,7 +1101,7 @@ func (s *Session) publishProgramDiagnostics(oldSnapshot *Snapshot, newSnapshot * if !shouldPublishProgramDiagnostics(newProject, newSnapshot.ID()) { return } - s.publishProjectDiagnostics(ctx, string(configFilePath), newProject.Program.GetProgramDiagnostics(), newSnapshot.converters) + s.publishProjectDiagnostics(ctx, string(configFilePath), newProject.GetProjectDiagnostics(ctx), newSnapshot.converters) }, ) } @@ -1122,6 +1127,37 @@ func (s *Session) publishProjectDiagnostics(ctx context.Context, configFilePath } } +// EnqueuePublishGlobalDiagnostics schedules a background check for new accumulated +// global diagnostics from checker pools, re-publishing tsconfig diagnostics if changed. +// Multiple calls are coalesced into a single background task. +func (s *Session) EnqueuePublishGlobalDiagnostics() { + if !s.options.PushDiagnosticsEnabled { + return + } + if s.globalDiagPublishPending.CompareAndSwap(false, true) { + s.backgroundQueue.Enqueue(s.backgroundCtx, s.publishGlobalDiagnostics) + } +} + +func (s *Session) publishGlobalDiagnostics(ctx context.Context) { + s.globalDiagPublishPending.Store(false) + + s.snapshotMu.RLock() + snapshot := s.snapshot + snapshot.ref() + s.snapshotMu.RUnlock() + defer snapshot.Deref(s) + + for _, project := range snapshot.ProjectCollection.Projects() { + if project.Kind != KindConfigured || project.checkerPool == nil { + continue + } + if project.checkerPool.TakeNewGlobalDiagnostics() { + s.publishProjectDiagnostics(ctx, string(project.configFilePath), project.GetProjectDiagnostics(ctx), snapshot.converters) + } + } +} + func (s *Session) triggerATAForUpdatedProjects(newSnapshot *Snapshot) { for _, project := range newSnapshot.ProjectCollection.Projects() { if project.ShouldTriggerATA(newSnapshot.ID()) { diff --git a/testdata/baselines/reference/submodule/fourslash/findAllReferences/findAllRefsForModuleGlobal.baseline.jsonc b/testdata/baselines/reference/submodule/fourslash/findAllReferences/findAllRefsForModuleGlobal.baseline.jsonc index cdad54c704f..3ac0503391c 100644 --- a/testdata/baselines/reference/submodule/fourslash/findAllReferences/findAllRefsForModuleGlobal.baseline.jsonc +++ b/testdata/baselines/reference/submodule/fourslash/findAllReferences/findAllRefsForModuleGlobal.baseline.jsonc @@ -1,5 +1,5 @@ // === findAllReferences === // === /b.ts === // /// -// import { x } from "/*FIND ALL REFS*/foo"; +// import { x } from "/*FIND ALL REFS*/[|foo|]"; // declare module "[|foo|]" {} \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/fourslash/findAllReferences/findAllRefsForModuleGlobal.baseline.jsonc.diff b/testdata/baselines/reference/submodule/fourslash/findAllReferences/findAllRefsForModuleGlobal.baseline.jsonc.diff deleted file mode 100644 index 7daa57499fe..00000000000 --- a/testdata/baselines/reference/submodule/fourslash/findAllReferences/findAllRefsForModuleGlobal.baseline.jsonc.diff +++ /dev/null @@ -1,9 +0,0 @@ ---- old.findAllRefsForModuleGlobal.baseline.jsonc -+++ new.findAllRefsForModuleGlobal.baseline.jsonc -@@= skipped -0, +0 lines =@@ - // === findAllReferences === - // === /b.ts === - // /// --// import { x } from "/*FIND ALL REFS*/[|foo|]"; -+// import { x } from "/*FIND ALL REFS*/foo"; - // declare module "[|foo|]" {} \ No newline at end of file diff --git a/testdata/baselines/reference/tsbuild/configFileErrors/reports-syntax-errors-in-config-file.js b/testdata/baselines/reference/tsbuild/configFileErrors/reports-syntax-errors-in-config-file.js index 58783aac79e..2df99f87a48 100644 --- a/testdata/baselines/reference/tsbuild/configFileErrors/reports-syntax-errors-in-config-file.js +++ b/testdata/baselines/reference/tsbuild/configFileErrors/reports-syntax-errors-in-config-file.js @@ -63,7 +63,7 @@ export declare function bar(): void; export function bar() { } //// [/home/src/workspaces/project/tsconfig.tsbuildinfo] *new* -{"version":"FakeTSVersion","errors":true,"root":[[2,3]],"fileNames":["lib.es2025.full.d.ts","./a.ts","./b.ts"],"fileInfos":[{"version":"8859c12c614ce56ba9a18e58384a198f-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ninterface SymbolConstructor {\n (desc?: string | number): symbol;\n for(name: string): symbol;\n readonly toStringTag: symbol;\n}\ndeclare var Symbol: SymbolConstructor;\ninterface Symbol {\n readonly [Symbol.toStringTag]: string;\n}\ndeclare const console: { log(msg: any): void; };","affectsGlobalScope":true,"impliedNodeFormat":1},{"version":"b8af959ef8294c415b0415508643e446-export function foo() { }","signature":"7ffb4ea6089b1a385965a214ba412941-export declare function foo(): void;\n","impliedNodeFormat":1},{"version":"492f7ec5be310332dc7e2ef503772d24-export function bar() { }","signature":"2f1e9992435d5724d3e1da8bdbc17eae-export declare function bar(): void;\n","impliedNodeFormat":1}],"options":{"composite":true},"semanticDiagnosticsPerFile":[1,2,3],"latestChangedDtsFile":"./b.d.ts"} +{"version":"FakeTSVersion","errors":true,"root":[[2,3]],"fileNames":["lib.es2025.full.d.ts","./a.ts","./b.ts"],"fileInfos":[{"version":"8859c12c614ce56ba9a18e58384a198f-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ninterface SymbolConstructor {\n (desc?: string | number): symbol;\n for(name: string): symbol;\n readonly toStringTag: symbol;\n}\ndeclare var Symbol: SymbolConstructor;\ninterface Symbol {\n readonly [Symbol.toStringTag]: string;\n}\ndeclare const console: { log(msg: any): void; };","affectsGlobalScope":true,"impliedNodeFormat":1},{"version":"b8af959ef8294c415b0415508643e446-export function foo() { }","signature":"7ffb4ea6089b1a385965a214ba412941-export declare function foo(): void;\n","impliedNodeFormat":1},{"version":"492f7ec5be310332dc7e2ef503772d24-export function bar() { }","signature":"2f1e9992435d5724d3e1da8bdbc17eae-export declare function bar(): void;\n","impliedNodeFormat":1}],"options":{"composite":true},"latestChangedDtsFile":"./b.d.ts"} //// [/home/src/workspaces/project/tsconfig.tsbuildinfo.readable.baseline.txt] *new* { "version": "FakeTSVersion", @@ -124,20 +124,15 @@ export function bar() { } "options": { "composite": true }, - "semanticDiagnosticsPerFile": [ - "lib.es2025.full.d.ts", - "./a.ts", - "./b.ts" - ], "latestChangedDtsFile": "./b.d.ts", - "size": 1357 + "size": 1320 } tsconfig.json:: SemanticDiagnostics:: -*not cached* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts -*not cached* /home/src/workspaces/project/a.ts -*not cached* /home/src/workspaces/project/b.ts +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/a.ts +*refresh* /home/src/workspaces/project/b.ts Signatures:: (stored at emit) /home/src/workspaces/project/a.ts (stored at emit) /home/src/workspaces/project/b.ts @@ -169,9 +164,6 @@ Found 1 error in tsconfig.json:7 tsconfig.json:: SemanticDiagnostics:: -*not cached* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts -*not cached* /home/src/workspaces/project/a.ts -*not cached* /home/src/workspaces/project/b.ts Signatures:: @@ -199,7 +191,7 @@ export function foo() { } export function fooBar() { } //// [/home/src/workspaces/project/tsconfig.tsbuildinfo] *modified* -{"version":"FakeTSVersion","errors":true,"root":[[2,3]],"fileNames":["lib.es2025.full.d.ts","./a.ts","./b.ts"],"fileInfos":[{"version":"8859c12c614ce56ba9a18e58384a198f-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ninterface SymbolConstructor {\n (desc?: string | number): symbol;\n for(name: string): symbol;\n readonly toStringTag: symbol;\n}\ndeclare var Symbol: SymbolConstructor;\ninterface Symbol {\n readonly [Symbol.toStringTag]: string;\n}\ndeclare const console: { log(msg: any): void; };","affectsGlobalScope":true,"impliedNodeFormat":1},{"version":"12981c250647eb82bb45c5fb79732976-export function foo() { }export function fooBar() { }","signature":"f3ff291f5185ac75eeeb6de19fc28a01-export declare function foo(): void;\nexport declare function fooBar(): void;\n","impliedNodeFormat":1},{"version":"492f7ec5be310332dc7e2ef503772d24-export function bar() { }","signature":"2f1e9992435d5724d3e1da8bdbc17eae-export declare function bar(): void;\n","impliedNodeFormat":1}],"options":{"composite":true,"declaration":true},"semanticDiagnosticsPerFile":[1,2,3],"latestChangedDtsFile":"./a.d.ts"} +{"version":"FakeTSVersion","errors":true,"root":[[2,3]],"fileNames":["lib.es2025.full.d.ts","./a.ts","./b.ts"],"fileInfos":[{"version":"8859c12c614ce56ba9a18e58384a198f-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ninterface SymbolConstructor {\n (desc?: string | number): symbol;\n for(name: string): symbol;\n readonly toStringTag: symbol;\n}\ndeclare var Symbol: SymbolConstructor;\ninterface Symbol {\n readonly [Symbol.toStringTag]: string;\n}\ndeclare const console: { log(msg: any): void; };","affectsGlobalScope":true,"impliedNodeFormat":1},{"version":"12981c250647eb82bb45c5fb79732976-export function foo() { }export function fooBar() { }","signature":"f3ff291f5185ac75eeeb6de19fc28a01-export declare function foo(): void;\nexport declare function fooBar(): void;\n","impliedNodeFormat":1},{"version":"492f7ec5be310332dc7e2ef503772d24-export function bar() { }","signature":"2f1e9992435d5724d3e1da8bdbc17eae-export declare function bar(): void;\n","impliedNodeFormat":1}],"options":{"composite":true,"declaration":true},"latestChangedDtsFile":"./a.d.ts"} //// [/home/src/workspaces/project/tsconfig.tsbuildinfo.readable.baseline.txt] *modified* { "version": "FakeTSVersion", @@ -261,20 +253,13 @@ export function fooBar() { } "composite": true, "declaration": true }, - "semanticDiagnosticsPerFile": [ - "lib.es2025.full.d.ts", - "./a.ts", - "./b.ts" - ], "latestChangedDtsFile": "./a.d.ts", - "size": 1445 + "size": 1408 } tsconfig.json:: SemanticDiagnostics:: -*not cached* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts -*not cached* /home/src/workspaces/project/a.ts -*not cached* /home/src/workspaces/project/b.ts +*refresh* /home/src/workspaces/project/a.ts Signatures:: (computed .d.ts) /home/src/workspaces/project/a.ts @@ -295,9 +280,6 @@ Found 1 error in tsconfig.json:7 tsconfig.json:: SemanticDiagnostics:: -*not cached* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts -*not cached* /home/src/workspaces/project/a.ts -*not cached* /home/src/workspaces/project/b.ts Signatures:: @@ -384,7 +366,4 @@ Output:: tsconfig.json:: SemanticDiagnostics:: -*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts -*refresh* /home/src/workspaces/project/a.ts -*refresh* /home/src/workspaces/project/b.ts Signatures:: diff --git a/testdata/baselines/reference/tsbuildWatch/configFileErrors/reports-syntax-errors-in-config-file.js b/testdata/baselines/reference/tsbuildWatch/configFileErrors/reports-syntax-errors-in-config-file.js index a34e6b9b083..e85a51f69f0 100644 --- a/testdata/baselines/reference/tsbuildWatch/configFileErrors/reports-syntax-errors-in-config-file.js +++ b/testdata/baselines/reference/tsbuildWatch/configFileErrors/reports-syntax-errors-in-config-file.js @@ -64,7 +64,7 @@ export declare function bar(): void; export function bar() { } //// [/home/src/workspaces/project/tsconfig.tsbuildinfo] *new* -{"version":"FakeTSVersion","errors":true,"root":[[2,3]],"fileNames":["lib.es2025.full.d.ts","./a.ts","./b.ts"],"fileInfos":[{"version":"8859c12c614ce56ba9a18e58384a198f-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ninterface SymbolConstructor {\n (desc?: string | number): symbol;\n for(name: string): symbol;\n readonly toStringTag: symbol;\n}\ndeclare var Symbol: SymbolConstructor;\ninterface Symbol {\n readonly [Symbol.toStringTag]: string;\n}\ndeclare const console: { log(msg: any): void; };","affectsGlobalScope":true,"impliedNodeFormat":1},{"version":"b8af959ef8294c415b0415508643e446-export function foo() { }","signature":"7ffb4ea6089b1a385965a214ba412941-export declare function foo(): void;\n","impliedNodeFormat":1},{"version":"492f7ec5be310332dc7e2ef503772d24-export function bar() { }","signature":"2f1e9992435d5724d3e1da8bdbc17eae-export declare function bar(): void;\n","impliedNodeFormat":1}],"options":{"composite":true},"semanticDiagnosticsPerFile":[1,2,3],"latestChangedDtsFile":"./b.d.ts"} +{"version":"FakeTSVersion","errors":true,"root":[[2,3]],"fileNames":["lib.es2025.full.d.ts","./a.ts","./b.ts"],"fileInfos":[{"version":"8859c12c614ce56ba9a18e58384a198f-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ninterface SymbolConstructor {\n (desc?: string | number): symbol;\n for(name: string): symbol;\n readonly toStringTag: symbol;\n}\ndeclare var Symbol: SymbolConstructor;\ninterface Symbol {\n readonly [Symbol.toStringTag]: string;\n}\ndeclare const console: { log(msg: any): void; };","affectsGlobalScope":true,"impliedNodeFormat":1},{"version":"b8af959ef8294c415b0415508643e446-export function foo() { }","signature":"7ffb4ea6089b1a385965a214ba412941-export declare function foo(): void;\n","impliedNodeFormat":1},{"version":"492f7ec5be310332dc7e2ef503772d24-export function bar() { }","signature":"2f1e9992435d5724d3e1da8bdbc17eae-export declare function bar(): void;\n","impliedNodeFormat":1}],"options":{"composite":true},"latestChangedDtsFile":"./b.d.ts"} //// [/home/src/workspaces/project/tsconfig.tsbuildinfo.readable.baseline.txt] *new* { "version": "FakeTSVersion", @@ -125,20 +125,15 @@ export function bar() { } "options": { "composite": true }, - "semanticDiagnosticsPerFile": [ - "lib.es2025.full.d.ts", - "./a.ts", - "./b.ts" - ], "latestChangedDtsFile": "./b.d.ts", - "size": 1357 + "size": 1320 } tsconfig.json:: SemanticDiagnostics:: -*not cached* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts -*not cached* /home/src/workspaces/project/a.ts -*not cached* /home/src/workspaces/project/b.ts +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/a.ts +*refresh* /home/src/workspaces/project/b.ts Signatures:: (stored at emit) /home/src/workspaces/project/a.ts (stored at emit) /home/src/workspaces/project/b.ts @@ -170,9 +165,6 @@ Output:: tsconfig.json:: SemanticDiagnostics:: -*not cached* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts -*not cached* /home/src/workspaces/project/a.ts -*not cached* /home/src/workspaces/project/b.ts Signatures:: @@ -200,7 +192,7 @@ export function foo() { } export function fooBar() { } //// [/home/src/workspaces/project/tsconfig.tsbuildinfo] *modified* -{"version":"FakeTSVersion","errors":true,"root":[[2,3]],"fileNames":["lib.es2025.full.d.ts","./a.ts","./b.ts"],"fileInfos":[{"version":"8859c12c614ce56ba9a18e58384a198f-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ninterface SymbolConstructor {\n (desc?: string | number): symbol;\n for(name: string): symbol;\n readonly toStringTag: symbol;\n}\ndeclare var Symbol: SymbolConstructor;\ninterface Symbol {\n readonly [Symbol.toStringTag]: string;\n}\ndeclare const console: { log(msg: any): void; };","affectsGlobalScope":true,"impliedNodeFormat":1},{"version":"12981c250647eb82bb45c5fb79732976-export function foo() { }export function fooBar() { }","signature":"f3ff291f5185ac75eeeb6de19fc28a01-export declare function foo(): void;\nexport declare function fooBar(): void;\n","impliedNodeFormat":1},{"version":"492f7ec5be310332dc7e2ef503772d24-export function bar() { }","signature":"2f1e9992435d5724d3e1da8bdbc17eae-export declare function bar(): void;\n","impliedNodeFormat":1}],"options":{"composite":true,"declaration":true},"semanticDiagnosticsPerFile":[1,2,3],"latestChangedDtsFile":"./a.d.ts"} +{"version":"FakeTSVersion","errors":true,"root":[[2,3]],"fileNames":["lib.es2025.full.d.ts","./a.ts","./b.ts"],"fileInfos":[{"version":"8859c12c614ce56ba9a18e58384a198f-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ninterface SymbolConstructor {\n (desc?: string | number): symbol;\n for(name: string): symbol;\n readonly toStringTag: symbol;\n}\ndeclare var Symbol: SymbolConstructor;\ninterface Symbol {\n readonly [Symbol.toStringTag]: string;\n}\ndeclare const console: { log(msg: any): void; };","affectsGlobalScope":true,"impliedNodeFormat":1},{"version":"12981c250647eb82bb45c5fb79732976-export function foo() { }export function fooBar() { }","signature":"f3ff291f5185ac75eeeb6de19fc28a01-export declare function foo(): void;\nexport declare function fooBar(): void;\n","impliedNodeFormat":1},{"version":"492f7ec5be310332dc7e2ef503772d24-export function bar() { }","signature":"2f1e9992435d5724d3e1da8bdbc17eae-export declare function bar(): void;\n","impliedNodeFormat":1}],"options":{"composite":true,"declaration":true},"latestChangedDtsFile":"./a.d.ts"} //// [/home/src/workspaces/project/tsconfig.tsbuildinfo.readable.baseline.txt] *modified* { "version": "FakeTSVersion", @@ -262,20 +254,13 @@ export function fooBar() { } "composite": true, "declaration": true }, - "semanticDiagnosticsPerFile": [ - "lib.es2025.full.d.ts", - "./a.ts", - "./b.ts" - ], "latestChangedDtsFile": "./a.d.ts", - "size": 1445 + "size": 1408 } tsconfig.json:: SemanticDiagnostics:: -*not cached* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts -*not cached* /home/src/workspaces/project/a.ts -*not cached* /home/src/workspaces/project/b.ts +*refresh* /home/src/workspaces/project/a.ts Signatures:: (computed .d.ts) /home/src/workspaces/project/a.ts @@ -297,9 +282,6 @@ Output:: tsconfig.json:: SemanticDiagnostics:: -*not cached* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts -*not cached* /home/src/workspaces/project/a.ts -*not cached* /home/src/workspaces/project/b.ts Signatures:: @@ -389,7 +371,4 @@ Output:: tsconfig.json:: SemanticDiagnostics:: -*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts -*refresh* /home/src/workspaces/project/a.ts -*refresh* /home/src/workspaces/project/b.ts Signatures:: diff --git a/testdata/baselines/reference/tsc/composite/converting-to-modules.js b/testdata/baselines/reference/tsc/composite/converting-to-modules.js index 75e50d1ddae..d815a96ec7a 100644 --- a/testdata/baselines/reference/tsc/composite/converting-to-modules.js +++ b/testdata/baselines/reference/tsc/composite/converting-to-modules.js @@ -53,7 +53,7 @@ declare const x = 10; const x = 10; //// [/home/src/workspaces/project/tsconfig.tsbuildinfo] *new* -{"version":"FakeTSVersion","errors":true,"root":[2],"fileNames":["lib.es2025.full.d.ts","./src/main.ts"],"fileInfos":[{"version":"8859c12c614ce56ba9a18e58384a198f-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ninterface SymbolConstructor {\n (desc?: string | number): symbol;\n for(name: string): symbol;\n readonly toStringTag: symbol;\n}\ndeclare var Symbol: SymbolConstructor;\ninterface Symbol {\n readonly [Symbol.toStringTag]: string;\n}\ndeclare const console: { log(msg: any): void; };","affectsGlobalScope":true,"impliedNodeFormat":1},{"version":"4447ab8c90027f28bdaff9f2056779ce-const x = 10;","signature":"4be7af7f970696121f4f582a5d074177-declare const x = 10;\n","affectsGlobalScope":true,"impliedNodeFormat":1}],"options":{"composite":true},"semanticDiagnosticsPerFile":[1,2],"latestChangedDtsFile":"./src/main.d.ts"} +{"version":"FakeTSVersion","errors":true,"root":[2],"fileNames":["lib.es2025.full.d.ts","./src/main.ts"],"fileInfos":[{"version":"8859c12c614ce56ba9a18e58384a198f-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ninterface SymbolConstructor {\n (desc?: string | number): symbol;\n for(name: string): symbol;\n readonly toStringTag: symbol;\n}\ndeclare var Symbol: SymbolConstructor;\ninterface Symbol {\n readonly [Symbol.toStringTag]: string;\n}\ndeclare const console: { log(msg: any): void; };","affectsGlobalScope":true,"impliedNodeFormat":1},{"version":"4447ab8c90027f28bdaff9f2056779ce-const x = 10;","signature":"4be7af7f970696121f4f582a5d074177-declare const x = 10;\n","affectsGlobalScope":true,"impliedNodeFormat":1}],"options":{"composite":true},"latestChangedDtsFile":"./src/main.d.ts"} //// [/home/src/workspaces/project/tsconfig.tsbuildinfo.readable.baseline.txt] *new* { "version": "FakeTSVersion", @@ -100,18 +100,14 @@ const x = 10; "options": { "composite": true }, - "semanticDiagnosticsPerFile": [ - "lib.es2025.full.d.ts", - "./src/main.ts" - ], "latestChangedDtsFile": "./src/main.d.ts", - "size": 1174 + "size": 1139 } tsconfig.json:: SemanticDiagnostics:: -*not cached* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts -*not cached* /home/src/workspaces/project/src/main.ts +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/src/main.ts Signatures:: (stored at emit) /home/src/workspaces/project/src/main.ts @@ -183,6 +179,4 @@ Output:: tsconfig.json:: SemanticDiagnostics:: -*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts -*refresh* /home/src/workspaces/project/src/main.ts Signatures:: diff --git a/testdata/baselines/reference/tsc/incremental/js-file-with-import-in-jsdoc-in-composite-project.js b/testdata/baselines/reference/tsc/incremental/js-file-with-import-in-jsdoc-in-composite-project.js index 590ad3c64aa..00438f16b8c 100644 --- a/testdata/baselines/reference/tsc/incremental/js-file-with-import-in-jsdoc-in-composite-project.js +++ b/testdata/baselines/reference/tsc/incremental/js-file-with-import-in-jsdoc-in-composite-project.js @@ -28,8 +28,12 @@ test("", async function () { {"compilerOptions": {"allowJs": true, "composite": true}} tsgo --noEmit -ExitStatus:: Success +ExitStatus:: DiagnosticsPresent_OutputsSkipped Output:: +error TS2318: Cannot find global type 'Promise'. + +Found 1 error. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} @@ -54,7 +58,7 @@ interface Symbol { } declare const console: { log(msg: any): void; }; //// [/home/src/workspaces/project/tsconfig.tsbuildinfo] *new* -{"version":"FakeTSVersion","errors":true,"root":[2],"fileNames":["lib.es2025.full.d.ts","./index.js"],"fileInfos":[{"version":"8859c12c614ce56ba9a18e58384a198f-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ninterface SymbolConstructor {\n (desc?: string | number): symbol;\n for(name: string): symbol;\n readonly toStringTag: symbol;\n}\ndeclare var Symbol: SymbolConstructor;\ninterface Symbol {\n readonly [Symbol.toStringTag]: string;\n}\ndeclare const console: { log(msg: any): void; };","affectsGlobalScope":true,"impliedNodeFormat":1},{"version":"a2c0c261f400e90f1ff304dbe3da7a53-test(\"\", async function () {\n ;(/** @type {typeof import(\"a\")} */ ({}))\n})\n\ntest(\"\", async function () {\n ;(/** @type {typeof import(\"a\")} */ a)\n})\n\ntest(\"\", async function () {\n (/** @type {typeof import(\"a\")} */ ({}))\n ;(/** @type {typeof import(\"a\")} */ ({}))\n})\n\ntest(\"\", async function () {\n (/** @type {typeof import(\"a\")} */ a)\n ;(/** @type {typeof import(\"a\")} */ a)\n})\n\ntest(\"\", async function () {\n (/** @type {typeof import(\"a\")} */ ({}))\n ;(/** @type {typeof import(\"a\")} */ ({}))\n})","affectsGlobalScope":true,"impliedNodeFormat":1}],"options":{"allowJs":true,"composite":true},"affectedFilesPendingEmit":[[2,17]],"emitSignatures":[2]} +{"version":"FakeTSVersion","errors":true,"root":[2],"fileNames":["lib.es2025.full.d.ts","./index.js"],"fileInfos":[{"version":"8859c12c614ce56ba9a18e58384a198f-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ninterface SymbolConstructor {\n (desc?: string | number): symbol;\n for(name: string): symbol;\n readonly toStringTag: symbol;\n}\ndeclare var Symbol: SymbolConstructor;\ninterface Symbol {\n readonly [Symbol.toStringTag]: string;\n}\ndeclare const console: { log(msg: any): void; };","affectsGlobalScope":true,"impliedNodeFormat":1},{"version":"a2c0c261f400e90f1ff304dbe3da7a53-test(\"\", async function () {\n ;(/** @type {typeof import(\"a\")} */ ({}))\n})\n\ntest(\"\", async function () {\n ;(/** @type {typeof import(\"a\")} */ a)\n})\n\ntest(\"\", async function () {\n (/** @type {typeof import(\"a\")} */ ({}))\n ;(/** @type {typeof import(\"a\")} */ ({}))\n})\n\ntest(\"\", async function () {\n (/** @type {typeof import(\"a\")} */ a)\n ;(/** @type {typeof import(\"a\")} */ a)\n})\n\ntest(\"\", async function () {\n (/** @type {typeof import(\"a\")} */ ({}))\n ;(/** @type {typeof import(\"a\")} */ ({}))\n})","affectsGlobalScope":true,"impliedNodeFormat":1}],"options":{"allowJs":true,"composite":true},"affectedFilesPendingEmit":[2],"emitSignatures":[2]} //// [/home/src/workspaces/project/tsconfig.tsbuildinfo.readable.baseline.txt] *new* { "version": "FakeTSVersion", @@ -104,11 +108,8 @@ declare const console: { log(msg: any): void; }; "affectedFilesPendingEmit": [ [ "./index.js", - "Js|DtsEmit", - [ - 2, - 17 - ] + "Js|Dts", + 2 ] ], "emitSignatures": [ @@ -117,7 +118,7 @@ declare const console: { log(msg: any): void; }; "original": 2 } ], - "size": 1633 + "size": 1628 } tsconfig.json::