diff --git a/internal/ast/diagnostic.go b/internal/ast/diagnostic.go index 11cac62a11..19b892762e 100644 --- a/internal/ast/diagnostic.go +++ b/internal/ast/diagnostic.go @@ -1,9 +1,9 @@ package ast import ( - "maps" "slices" "strings" + "sync" "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/core" @@ -141,6 +141,8 @@ func NewCompilerDiagnostic(message *diagnostics.Message, args ...any) *Diagnosti } type DiagnosticsCollection struct { + mu sync.Mutex + count int fileDiagnostics map[string][]*Diagnostic fileDiagnosticsSorted collections.Set[string] nonFileDiagnostics []*Diagnostic @@ -148,6 +150,11 @@ type DiagnosticsCollection struct { } func (c *DiagnosticsCollection) Add(diagnostic *Diagnostic) { + c.mu.Lock() + defer c.mu.Unlock() + + c.count++ + if diagnostic.File() != nil { fileName := diagnostic.File().FileName() if c.fileDiagnostics == nil { @@ -162,11 +169,14 @@ func (c *DiagnosticsCollection) Add(diagnostic *Diagnostic) { } func (c *DiagnosticsCollection) Lookup(diagnostic *Diagnostic) *Diagnostic { + c.mu.Lock() + defer c.mu.Unlock() + var diagnostics []*Diagnostic if diagnostic.File() != nil { - diagnostics = c.GetDiagnosticsForFile(diagnostic.File().FileName()) + diagnostics = c.getDiagnosticsForFileLocked(diagnostic.File().FileName()) } else { - diagnostics = c.GetGlobalDiagnostics() + diagnostics = c.getGlobalDiagnosticsLocked() } if i, ok := slices.BinarySearchFunc(diagnostics, diagnostic, CompareDiagnostics); ok { return diagnostics[i] @@ -175,28 +185,45 @@ func (c *DiagnosticsCollection) Lookup(diagnostic *Diagnostic) *Diagnostic { } func (c *DiagnosticsCollection) GetGlobalDiagnostics() []*Diagnostic { + c.mu.Lock() + defer c.mu.Unlock() + + return c.getGlobalDiagnosticsLocked() +} + +func (c *DiagnosticsCollection) getGlobalDiagnosticsLocked() []*Diagnostic { if !c.nonFileDiagnosticsSorted { slices.SortStableFunc(c.nonFileDiagnostics, CompareDiagnostics) c.nonFileDiagnosticsSorted = true } - return c.nonFileDiagnostics + return slices.Clone(c.nonFileDiagnostics) } func (c *DiagnosticsCollection) GetDiagnosticsForFile(fileName string) []*Diagnostic { + c.mu.Lock() + defer c.mu.Unlock() + + return c.getDiagnosticsForFileLocked(fileName) +} + +func (c *DiagnosticsCollection) getDiagnosticsForFileLocked(fileName string) []*Diagnostic { if !c.fileDiagnosticsSorted.Has(fileName) { slices.SortStableFunc(c.fileDiagnostics[fileName], CompareDiagnostics) c.fileDiagnosticsSorted.Add(fileName) } - return c.fileDiagnostics[fileName] + return slices.Clone(c.fileDiagnostics[fileName]) } func (c *DiagnosticsCollection) GetDiagnostics() []*Diagnostic { - fileNames := slices.Collect(maps.Keys(c.fileDiagnostics)) - slices.Sort(fileNames) - diagnostics := slices.Clip(c.nonFileDiagnostics) - for _, fileName := range fileNames { - diagnostics = append(diagnostics, c.fileDiagnostics[fileName]...) + c.mu.Lock() + defer c.mu.Unlock() + + diagnostics := make([]*Diagnostic, 0, c.count) + diagnostics = append(diagnostics, c.nonFileDiagnostics...) + for _, diags := range c.fileDiagnostics { + diagnostics = append(diagnostics, diags...) } + slices.SortFunc(diagnostics, CompareDiagnostics) return diagnostics } @@ -208,11 +235,17 @@ func getDiagnosticPath(d *Diagnostic) string { } func EqualDiagnostics(d1, d2 *Diagnostic) bool { + if d1 == d2 { + return true + } return EqualDiagnosticsNoRelatedInfo(d1, d2) && slices.EqualFunc(d1.RelatedInformation(), d2.RelatedInformation(), EqualDiagnostics) } func EqualDiagnosticsNoRelatedInfo(d1, d2 *Diagnostic) bool { + if d1 == d2 { + return true + } return getDiagnosticPath(d1) == getDiagnosticPath(d2) && d1.Loc() == d2.Loc() && d1.Code() == d2.Code() && @@ -221,6 +254,9 @@ func EqualDiagnosticsNoRelatedInfo(d1, d2 *Diagnostic) bool { } func equalMessageChain(c1, c2 *Diagnostic) bool { + if c1 == c2 { + return true + } return c1.Code() == c2.Code() && slices.Equal(c1.MessageArgs(), c2.MessageArgs()) && slices.EqualFunc(c1.MessageChain(), c2.MessageChain(), equalMessageChain) @@ -271,6 +307,9 @@ func compareRelatedInfo(r1, r2 []*Diagnostic) int { } func CompareDiagnostics(d1, d2 *Diagnostic) int { + if d1 == d2 { + return 0 + } c := strings.Compare(getDiagnosticPath(d1), getDiagnosticPath(d2)) if c != 0 { return c diff --git a/internal/checker/checker.go b/internal/checker/checker.go index ff0060a7a6..14330cfb23 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -540,7 +540,6 @@ type Program interface { GetJSXRuntimeImportSpecifier(path tspath.Path) (moduleReference string, specifier *ast.Node) GetImportHelpersImportSpecifier(path tspath.Path) *ast.Node SourceFileMayBeEmitted(sourceFile *ast.SourceFile, forceDtsEmit bool) bool - IsSourceFromProjectReference(path tspath.Path) bool IsSourceFileDefaultLibrary(path tspath.Path) bool GetProjectReferenceFromOutputDts(path tspath.Path) *tsoptions.SourceOutputAndProjectReference GetRedirectForResolution(file ast.HasFileName) *tsoptions.ParsedCommandLine @@ -930,8 +929,6 @@ func NewChecker(program Program) (*Checker, *sync.Mutex) { c.unionTypes = make(map[string]*Type) c.unionOfUnionTypes = make(map[UnionOfUnionKey]*Type) c.intersectionTypes = make(map[string]*Type) - c.diagnostics = ast.DiagnosticsCollection{} - c.suggestionDiagnostics = ast.DiagnosticsCollection{} c.mergedSymbols = make(map[*ast.Symbol]*ast.Symbol) c.patternForType = make(map[*Type]*ast.Node) c.contextFreeTypes = make(map[*ast.Node]*Type) @@ -2094,13 +2091,6 @@ func (c *Checker) getSymbol(symbols ast.SymbolTable, name string, meaning ast.Sy return nil } -func (c *Checker) CheckSourceFile(ctx context.Context, sourceFile *ast.SourceFile) { - if SkipTypeChecking(sourceFile, c.compilerOptions, c.program, false) { - return - } - c.checkSourceFile(ctx, sourceFile) -} - func (c *Checker) checkSourceFile(ctx context.Context, sourceFile *ast.SourceFile) { c.checkNotCanceled() links := c.sourceFileLinks.Get(sourceFile) @@ -13507,30 +13497,20 @@ func (c *Checker) getDiagnostics(ctx context.Context, sourceFile *ast.SourceFile c.checkNotCanceled() isSuggestionDiagnostics := collection == &c.suggestionDiagnostics - files := c.files - if sourceFile != nil { - files = []*ast.SourceFile{sourceFile} + c.checkSourceFile(ctx, sourceFile) + if c.wasCanceled { + return nil } - for _, file := range files { - c.CheckSourceFile(ctx, file) - if c.wasCanceled { - return nil - } - - // Check unused identifiers as suggestions if we're collecting suggestion diagnostics - // and they are not configured as errors - if isSuggestionDiagnostics && !file.IsDeclarationFile && - !(c.compilerOptions.NoUnusedLocals.IsTrue() || c.compilerOptions.NoUnusedParameters.IsTrue()) { - links := c.sourceFileLinks.Get(file) - c.checkUnusedIdentifiers(links.identifierCheckNodes) - } + // Check unused identifiers as suggestions if we're collecting suggestion diagnostics + // and they are not configured as errors + if isSuggestionDiagnostics && !sourceFile.IsDeclarationFile && + !(c.compilerOptions.NoUnusedLocals.IsTrue() || c.compilerOptions.NoUnusedParameters.IsTrue()) { + links := c.sourceFileLinks.Get(sourceFile) + c.checkUnusedIdentifiers(links.identifierCheckNodes) } - if sourceFile != nil { - return collection.GetDiagnosticsForFile(sourceFile.FileName()) - } - return collection.GetDiagnostics() + return collection.GetDiagnosticsForFile(sourceFile.FileName()) } func (c *Checker) GetGlobalDiagnostics() []*ast.Diagnostic { diff --git a/internal/checker/checker_test.go b/internal/checker/checker_test.go index f1bdede743..e46b994166 100644 --- a/internal/checker/checker_test.go +++ b/internal/checker/checker_test.go @@ -61,25 +61,6 @@ foo.bar;` } } -func TestCheckSrcCompiler(t *testing.T) { - t.Parallel() - - repo.SkipIfNoTypeScriptSubmodule(t) - fs := osvfs.FS() - fs = bundled.WrapFS(fs) - - rootPath := tspath.CombinePaths(tspath.NormalizeSlashes(repo.TypeScriptSubmodulePath), "src", "compiler") - - host := compiler.NewCompilerHost(rootPath, fs, bundled.LibPath(), nil, nil) - parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile(tspath.CombinePaths(rootPath, "tsconfig.json"), &core.CompilerOptions{}, nil, host, nil) - assert.Equal(t, len(errors), 0, "Expected no errors in parsed command line") - p := compiler.NewProgram(compiler.ProgramOptions{ - Config: parsed, - Host: host, - }) - p.CheckSourceFiles(t.Context(), nil) -} - func BenchmarkNewChecker(b *testing.B) { repo.SkipIfNoTypeScriptSubmodule(b) fs := osvfs.FS() diff --git a/internal/checker/utilities.go b/internal/checker/utilities.go index 640f9da9e9..31b833cb81 100644 --- a/internal/checker/utilities.go +++ b/internal/checker/utilities.go @@ -1283,34 +1283,6 @@ func forEachYieldExpression(body *ast.Node, visitor func(expr *ast.Node)) { traverse(body) } -func SkipTypeChecking(sourceFile *ast.SourceFile, options *core.CompilerOptions, host Program, ignoreNoCheck bool) bool { - return (!ignoreNoCheck && options.NoCheck.IsTrue()) || - options.SkipLibCheck.IsTrue() && sourceFile.IsDeclarationFile || - options.SkipDefaultLibCheck.IsTrue() && host.IsSourceFileDefaultLibrary(sourceFile.Path()) || - host.IsSourceFromProjectReference(sourceFile.Path()) || - !canIncludeBindAndCheckDiagnostics(sourceFile, options) -} - -func canIncludeBindAndCheckDiagnostics(sourceFile *ast.SourceFile, options *core.CompilerOptions) bool { - if sourceFile.CheckJsDirective != nil && !sourceFile.CheckJsDirective.Enabled { - return false - } - - if sourceFile.ScriptKind == core.ScriptKindTS || sourceFile.ScriptKind == core.ScriptKindTSX || sourceFile.ScriptKind == core.ScriptKindExternal { - return true - } - - isJS := sourceFile.ScriptKind == core.ScriptKindJS || sourceFile.ScriptKind == core.ScriptKindJSX - isCheckJS := isJS && ast.IsCheckJSEnabledForFile(sourceFile, options) - isPlainJS := ast.IsPlainJSFile(sourceFile, options.CheckJs) - - // By default, only type-check .ts, .tsx, Deferred, plain JS, checked JS and External - // - plain JS: .js files with no // ts-check and checkJs: undefined - // - check JS: .js files with either // ts-check or checkJs: true - // - external: files that are added by plugins - return isPlainJS || isCheckJS || sourceFile.ScriptKind == core.ScriptKindDeferred -} - func getEnclosingContainer(node *ast.Node) *ast.Node { return ast.FindAncestor(node.Parent, func(n *ast.Node) bool { return binder.GetContainerFlags(n)&binder.ContainerFlagsIsContainer != 0 diff --git a/internal/compiler/checkerpool.go b/internal/compiler/checkerpool.go index 88b22a6ac1..84ea8d824a 100644 --- a/internal/compiler/checkerpool.go +++ b/internal/compiler/checkerpool.go @@ -2,7 +2,6 @@ package compiler import ( "context" - "iter" "slices" "sync" @@ -12,17 +11,13 @@ import ( ) type CheckerPool interface { - Count() int 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()) - ForEachCheckerParallel(ctx context.Context, cb func(idx int, c *checker.Checker)) - Files(checker *checker.Checker) iter.Seq[*ast.SourceFile] } type checkerPool struct { - checkerCount int - program *Program + program *Program createCheckersOnce sync.Once checkers []*checker.Checker @@ -43,32 +38,26 @@ func newCheckerPool(program *Program) *checkerPool { checkerCount = max(min(checkerCount, len(program.files), 256), 1) pool := &checkerPool{ - program: program, - checkerCount: checkerCount, - checkers: make([]*checker.Checker, checkerCount), - locks: make([]*sync.Mutex, checkerCount), + program: program, + checkers: make([]*checker.Checker, checkerCount), + locks: make([]*sync.Mutex, checkerCount), } return pool } -func (p *checkerPool) Count() int { - return p.checkerCount -} - func (p *checkerPool) GetCheckerForFile(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) { p.createCheckers() - checker := p.fileAssociations[file] - return checker, noop + return p.fileAssociations[file], noop } func (p *checkerPool) GetCheckerForFileExclusive(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) { - c, done := p.GetCheckerForFile(ctx, file) + p.createCheckers() + c := p.fileAssociations[file] idx := slices.Index(p.checkers, c) p.locks[idx].Lock() return c, sync.OnceFunc(func() { p.locks[idx].Unlock() - done() }) } @@ -80,8 +69,9 @@ func (p *checkerPool) GetChecker(ctx context.Context) (*checker.Checker, func()) func (p *checkerPool) createCheckers() { p.createCheckersOnce.Do(func() { + checkerCount := len(p.checkers) wg := core.NewWorkGroup(p.program.SingleThreaded()) - for i := range p.checkerCount { + for i := range checkerCount { wg.Queue(func() { p.checkers[i], p.locks[i] = checker.NewChecker(p.program) }) @@ -91,14 +81,14 @@ func (p *checkerPool) createCheckers() { p.fileAssociations = make(map[*ast.SourceFile]*checker.Checker, len(p.program.files)) for i, file := range p.program.files { - p.fileAssociations[file] = p.checkers[i%p.checkerCount] + p.fileAssociations[file] = p.checkers[i%checkerCount] } }) } // Runs `cb` for each checker in the pool concurrently, locking and unlocking checker mutexes as it goes, -// making it safe to call `ForEachCheckerParallel` from many threads simultaneously. -func (p *checkerPool) ForEachCheckerParallel(ctx context.Context, cb func(idx int, c *checker.Checker)) { +// making it safe to call `forEachCheckerParallel` from many threads simultaneously. +func (p *checkerPool) forEachCheckerParallel(cb func(idx int, c *checker.Checker)) { p.createCheckers() wg := core.NewWorkGroup(p.program.SingleThreaded()) for idx, checker := range p.checkers { @@ -111,17 +101,4 @@ func (p *checkerPool) ForEachCheckerParallel(ctx context.Context, cb func(idx in wg.RunAndWait() } -func (p *checkerPool) Files(checker *checker.Checker) iter.Seq[*ast.SourceFile] { - checkerIndex := slices.Index(p.checkers, checker) - return func(yield func(*ast.SourceFile) bool) { - for i, file := range p.program.files { - if i%p.checkerCount == checkerIndex { - if !yield(file) { - return - } - } - } - } -} - func noop() {} diff --git a/internal/compiler/program.go b/internal/compiler/program.go index b41bb62e3d..a9a6b47f3e 100644 --- a/internal/compiler/program.go +++ b/internal/compiler/program.go @@ -371,23 +371,15 @@ func (p *Program) BindSourceFiles() { wg.RunAndWait() } -func (p *Program) CheckSourceFiles(ctx context.Context, files []*ast.SourceFile) { - p.checkerPool.ForEachCheckerParallel(ctx, func(_ int, checker *checker.Checker) { - for file := range p.checkerPool.Files(checker) { - if files == nil || slices.Contains(files, file) { - checker.CheckSourceFile(ctx, file) - } - } - }) -} - // Return the type checker associated with the program. func (p *Program) GetTypeChecker(ctx context.Context) (*checker.Checker, func()) { return p.checkerPool.GetChecker(ctx) } -func (p *Program) ForEachCheckerParallel(ctx context.Context, cb func(idx int, c *checker.Checker)) { - p.checkerPool.ForEachCheckerParallel(ctx, cb) +func (p *Program) ForEachCheckerParallel(cb func(idx int, c *checker.Checker)) { + if pool, ok := p.checkerPool.(*checkerPool); ok { + pool.forEachCheckerParallel(cb) + } } // Return a checker for the given file. We may have multiple checkers in concurrent scenarios and this @@ -425,49 +417,103 @@ func (p *Program) GetResolvedModules() map[tspath.Path]module.ModeAwareCache[*mo return p.resolvedModules } +// collectDiagnostics collects diagnostics from a single file or all files. +// If sourceFile is non-nil, returns diagnostics for just that file. +// If sourceFile is nil, returns diagnostics for all files in the program. +func (p *Program) collectDiagnostics(ctx context.Context, sourceFile *ast.SourceFile, concurrent bool, collect func(context.Context, *ast.SourceFile) []*ast.Diagnostic) []*ast.Diagnostic { + var result []*ast.Diagnostic + if sourceFile != nil { + result = collect(ctx, sourceFile) + } else { + diagnostics := make([][]*ast.Diagnostic, len(p.files)) + wg := core.NewWorkGroup(!concurrent || p.SingleThreaded()) + for i, file := range p.files { + wg.Queue(func() { + diagnostics[i] = collect(ctx, file) + }) + } + wg.RunAndWait() + result = slices.Concat(diagnostics...) + } + return SortAndDeduplicateDiagnostics(result) +} + func (p *Program) GetSyntacticDiagnostics(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic { - return p.getDiagnosticsHelper(ctx, sourceFile, false /*ensureBound*/, false /*ensureChecked*/, p.getSyntacticDiagnosticsForFile) + return p.collectDiagnostics(ctx, sourceFile, false /*concurrent*/, func(_ context.Context, file *ast.SourceFile) []*ast.Diagnostic { + return core.Concatenate(file.Diagnostics(), file.JSDiagnostics()) + }) } func (p *Program) GetBindDiagnostics(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic { - return p.getDiagnosticsHelper(ctx, sourceFile, true /*ensureBound*/, false /*ensureChecked*/, p.getBindDiagnosticsForFile) + if sourceFile != nil { + binder.BindSourceFile(sourceFile) + } else { + p.BindSourceFiles() + } + return p.collectDiagnostics(ctx, sourceFile, false /*concurrent*/, func(_ context.Context, file *ast.SourceFile) []*ast.Diagnostic { + return file.BindDiagnostics() + }) } func (p *Program) GetSemanticDiagnostics(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic { - return p.getDiagnosticsHelper(ctx, sourceFile, true /*ensureBound*/, true /*ensureChecked*/, p.getSemanticDiagnosticsForFile) + return p.collectDiagnostics(ctx, sourceFile, true /*concurrent*/, p.getSemanticDiagnosticsForFile) } -func (p *Program) GetSemanticDiagnosticsNoFilter(ctx context.Context, sourceFiles []*ast.SourceFile) map[*ast.SourceFile][]*ast.Diagnostic { - p.BindSourceFiles() - p.CheckSourceFiles(ctx, sourceFiles) - if ctx.Err() != nil { - return nil - } +func (p *Program) GetSemanticDiagnosticsWithoutNoEmitFiltering(ctx context.Context, sourceFiles []*ast.SourceFile) map[*ast.SourceFile][]*ast.Diagnostic { result := make(map[*ast.SourceFile][]*ast.Diagnostic, len(sourceFiles)) for _, file := range sourceFiles { - result[file] = SortAndDeduplicateDiagnostics(p.getSemanticDiagnosticsForFileNotFilter(ctx, file)) + result[file] = SortAndDeduplicateDiagnostics(p.getBindAndCheckDiagnosticsForFile(ctx, file)) } return result } func (p *Program) GetSuggestionDiagnostics(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic { - return p.getDiagnosticsHelper(ctx, sourceFile, true /*ensureBound*/, true /*ensureChecked*/, p.getSuggestionDiagnosticsForFile) + return p.collectDiagnostics(ctx, sourceFile, true /*concurrent*/, p.getSuggestionDiagnosticsForFile) } func (p *Program) GetProgramDiagnostics() []*ast.Diagnostic { - return SortAndDeduplicateDiagnostics(slices.Concat( + return SortAndDeduplicateDiagnostics(core.Concatenate( p.programDiagnostics, - p.includeProcessor.getDiagnostics(p).GetGlobalDiagnostics())) + p.includeProcessor.getDiagnostics(p).GetGlobalDiagnostics(), + )) } func (p *Program) GetIncludeProcessorDiagnostics(sourceFile *ast.SourceFile) []*ast.Diagnostic { - if checker.SkipTypeChecking(sourceFile, p.Options(), p, false) { + if p.SkipTypeChecking(sourceFile, false) { return nil } filtered, _ := p.getDiagnosticsWithPrecedingDirectives(sourceFile, p.includeProcessor.getDiagnostics(p).GetDiagnosticsForFile(sourceFile.FileName())) return filtered } +func (p *Program) SkipTypeChecking(sourceFile *ast.SourceFile, ignoreNoCheck bool) bool { + return (!ignoreNoCheck && p.Options().NoCheck.IsTrue()) || + p.Options().SkipLibCheck.IsTrue() && sourceFile.IsDeclarationFile || + p.Options().SkipDefaultLibCheck.IsTrue() && p.IsSourceFileDefaultLibrary(sourceFile.Path()) || + p.IsSourceFromProjectReference(sourceFile.Path()) || + !p.canIncludeBindAndCheckDiagnostics(sourceFile) +} + +func (p *Program) canIncludeBindAndCheckDiagnostics(sourceFile *ast.SourceFile) bool { + if sourceFile.CheckJsDirective != nil && !sourceFile.CheckJsDirective.Enabled { + return false + } + + if sourceFile.ScriptKind == core.ScriptKindTS || sourceFile.ScriptKind == core.ScriptKindTSX || sourceFile.ScriptKind == core.ScriptKindExternal { + return true + } + + isJS := sourceFile.ScriptKind == core.ScriptKindJS || sourceFile.ScriptKind == core.ScriptKindJSX + isCheckJS := isJS && ast.IsCheckJSEnabledForFile(sourceFile, p.Options()) + isPlainJS := ast.IsPlainJSFile(sourceFile, p.Options().CheckJs) + + // By default, only type-check .ts, .tsx, Deferred, plain JS, checked JS and External + // - plain JS: .js files with no // ts-check and checkJs: undefined + // - check JS: .js files with either // ts-check or checkJs: true + // - external: files that are added by plugins + return isPlainJS || isCheckJS || sourceFile.ScriptKind == core.ScriptKindDeferred +} + func (p *Program) getSourceFilesToEmit(targetSourceFile *ast.SourceFile, forceDtsEmit bool) []*ast.SourceFile { if targetSourceFile == nil && !forceDtsEmit { p.sourceFilesToEmitOnce.Do(func() { @@ -985,8 +1031,10 @@ func (p *Program) GetGlobalDiagnostics(ctx context.Context) []*ast.Diagnostic { return nil } - globalDiagnostics := make([][]*ast.Diagnostic, p.checkerPool.Count()) - p.checkerPool.ForEachCheckerParallel(ctx, func(idx int, checker *checker.Checker) { + pool := p.checkerPool.(*checkerPool) + + globalDiagnostics := make([][]*ast.Diagnostic, len(pool.checkers)) + pool.forEachCheckerParallel(func(idx int, checker *checker.Checker) { globalDiagnostics[idx] = checker.GetGlobalDiagnostics() }) @@ -994,32 +1042,18 @@ func (p *Program) GetGlobalDiagnostics(ctx context.Context) []*ast.Diagnostic { } func (p *Program) GetDeclarationDiagnostics(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic { - return p.getDiagnosticsHelper(ctx, sourceFile, true /*ensureBound*/, true /*ensureChecked*/, p.getDeclarationDiagnosticsForFile) + return p.collectDiagnostics(ctx, sourceFile, true /*concurrent*/, p.getDeclarationDiagnosticsForFile) } func (p *Program) GetOptionsDiagnostics(ctx context.Context) []*ast.Diagnostic { - return SortAndDeduplicateDiagnostics(append(p.GetGlobalDiagnostics(ctx), p.getOptionsDiagnosticsOfConfigFile()...)) + return SortAndDeduplicateDiagnostics(core.Concatenate(p.GetGlobalDiagnostics(ctx), p.getOptionsDiagnosticsOfConfigFile())) } func (p *Program) getOptionsDiagnosticsOfConfigFile() []*ast.Diagnostic { - // todo update p.configParsingDiagnostics when updateAndGetProgramDiagnostics is implemented if p.Options() == nil || p.Options().ConfigFilePath == "" { return nil } - return p.GetConfigFileParsingDiagnostics() // TODO: actually call getDiagnosticsHelper on config path -} - -func (p *Program) getSyntacticDiagnosticsForFile(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic { - return core.Concatenate(sourceFile.Diagnostics(), sourceFile.JSDiagnostics()) -} - -func (p *Program) getBindDiagnosticsForFile(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic { - // TODO: restore this; tsgo's main depends on this function binding all files for timing. - // if checker.SkipTypeChecking(sourceFile, p.compilerOptions) { - // return nil - // } - - return sourceFile.BindDiagnostics() + return p.GetConfigFileParsingDiagnostics() } func FilterNoEmitSemanticDiagnostics(diagnostics []*ast.Diagnostic, options *core.CompilerOptions) []*ast.Diagnostic { @@ -1032,40 +1066,30 @@ func FilterNoEmitSemanticDiagnostics(diagnostics []*ast.Diagnostic, options *cor } func (p *Program) getSemanticDiagnosticsForFile(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic { - return slices.Concat( - FilterNoEmitSemanticDiagnostics(p.getSemanticDiagnosticsForFileNotFilter(ctx, sourceFile), p.Options()), + return core.Concatenate( + FilterNoEmitSemanticDiagnostics(p.getBindAndCheckDiagnosticsForFile(ctx, sourceFile), p.Options()), p.GetIncludeProcessorDiagnostics(sourceFile), ) } -func (p *Program) getSemanticDiagnosticsForFileNotFilter(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic { +// 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 { compilerOptions := p.Options() - if checker.SkipTypeChecking(sourceFile, compilerOptions, p, false) { + if p.SkipTypeChecking(sourceFile, false) { return nil } - var fileChecker *checker.Checker - var done func() - if sourceFile != nil { - fileChecker, done = p.checkerPool.GetCheckerForFile(ctx, sourceFile) + // IIFE to release checker as soon as possible. + diags := func() []*ast.Diagnostic { + fileChecker, done := p.checkerPool.GetCheckerForFileExclusive(ctx, sourceFile) defer done() - } - diags := slices.Clip(sourceFile.BindDiagnostics()) - // Ask for diags from all checkers; checking one file may add diagnostics to other files. - // These are deduplicated later. - checkerDiags := make([][]*ast.Diagnostic, p.checkerPool.Count()) - p.checkerPool.ForEachCheckerParallel(ctx, func(idx int, checker *checker.Checker) { - if sourceFile == nil || checker == fileChecker { - checkerDiags[idx] = checker.GetDiagnostics(ctx, sourceFile) - } - }) - if ctx.Err() != nil { - return nil - } - - diags = append(diags, slices.Concat(checkerDiags...)...) - // !!! This should be rewritten to work like getBindAndCheckDiagnosticsForFileNoCache. + // 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 + }() isPlainJS := ast.IsPlainJSFile(sourceFile, compilerOptions.CheckJs) if isPlainJS { @@ -1143,32 +1167,16 @@ func (p *Program) getDeclarationDiagnosticsForFile(ctx context.Context, sourceFi } func (p *Program) getSuggestionDiagnosticsForFile(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic { - if checker.SkipTypeChecking(sourceFile, p.Options(), p, false) { + if p.SkipTypeChecking(sourceFile, false) { return nil } - var fileChecker *checker.Checker - var done func() - if sourceFile != nil { - fileChecker, done = p.checkerPool.GetCheckerForFile(ctx, sourceFile) - defer done() - } + 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.BindSuggestionDiagnostics) - - checkerDiags := make([][]*ast.Diagnostic, p.checkerPool.Count()) - p.checkerPool.ForEachCheckerParallel(ctx, func(idx int, checker *checker.Checker) { - if sourceFile == nil || checker == fileChecker { - checkerDiags[idx] = checker.GetSuggestionDiagnostics(ctx, sourceFile) - } else { - // !!! is there any case where suggestion diagnostics are produced in other checkers? - } - }) - if ctx.Err() != nil { - return nil - } - - diags = append(diags, slices.Concat(checkerDiags...)...) + diags = append(diags, fileChecker.GetSuggestionDiagnostics(ctx, sourceFile)...) return diags } @@ -1221,29 +1229,6 @@ func compactAndMergeRelatedInfos(diagnostics []*ast.Diagnostic) []*ast.Diagnosti return diagnostics[:j] } -func (p *Program) getDiagnosticsHelper(ctx context.Context, sourceFile *ast.SourceFile, ensureBound bool, ensureChecked bool, getDiagnostics func(context.Context, *ast.SourceFile) []*ast.Diagnostic) []*ast.Diagnostic { - if sourceFile != nil { - if ensureBound { - binder.BindSourceFile(sourceFile) - } - return SortAndDeduplicateDiagnostics(getDiagnostics(ctx, sourceFile)) - } - if ensureBound { - p.BindSourceFiles() - } - if ensureChecked { - p.CheckSourceFiles(ctx, nil) - if ctx.Err() != nil { - return nil - } - } - var result []*ast.Diagnostic - for _, file := range p.files { - result = append(result, getDiagnostics(ctx, file)...) - } - return SortAndDeduplicateDiagnostics(result) -} - func (p *Program) LineCount() int { var count int for _, file := range p.files { @@ -1267,7 +1252,7 @@ func (p *Program) SymbolCount() int { } var val atomic.Uint32 val.Store(uint32(count)) - p.checkerPool.ForEachCheckerParallel(context.Background(), func(idx int, c *checker.Checker) { + p.ForEachCheckerParallel(func(_ int, c *checker.Checker) { val.Add(c.SymbolCount) }) return int(val.Load()) @@ -1275,7 +1260,7 @@ func (p *Program) SymbolCount() int { func (p *Program) TypeCount() int { var val atomic.Uint32 - p.checkerPool.ForEachCheckerParallel(context.Background(), func(idx int, c *checker.Checker) { + p.ForEachCheckerParallel(func(_ int, c *checker.Checker) { val.Add(c.TypeCount) }) return int(val.Load()) @@ -1283,7 +1268,7 @@ func (p *Program) TypeCount() int { func (p *Program) InstantiationCount() int { var val atomic.Uint32 - p.checkerPool.ForEachCheckerParallel(context.Background(), func(idx int, c *checker.Checker) { + p.ForEachCheckerParallel(func(_ int, c *checker.Checker) { val.Add(c.TotalInstantiationCount) }) return int(val.Load()) @@ -1378,8 +1363,6 @@ type SourceMapEmitResult struct { } func (p *Program) Emit(ctx context.Context, options EmitOptions) *EmitResult { - // !!! performance measurement - p.BindSourceFiles() if options.EmitOnly != EmitOnlyForcedDts { result := HandleNoEmitOnError( ctx, diff --git a/internal/execute/incremental/affectedfileshandler.go b/internal/execute/incremental/affectedfileshandler.go index ed82d52f5c..ff457aced2 100644 --- a/internal/execute/incremental/affectedfileshandler.go +++ b/internal/execute/incremental/affectedfileshandler.go @@ -59,7 +59,7 @@ func (h *affectedFilesHandler) removeSemanticDiagnosticsOf(path tspath.Path) { func (h *affectedFilesHandler) removeDiagnosticsOfLibraryFiles() { h.cleanedDiagnosticsOfLibFiles.Do(func() { for _, file := range h.program.GetSourceFiles() { - if h.program.program.IsSourceFileDefaultLibrary(file.Path()) && !checker.SkipTypeChecking(file, h.program.snapshot.options, h.program.program, true) { + if h.program.program.IsSourceFileDefaultLibrary(file.Path()) && !h.program.program.SkipTypeChecking(file, true) { h.removeSemanticDiagnosticsOf(file.Path()) } } diff --git a/internal/execute/incremental/program.go b/internal/execute/incremental/program.go index dd94d16241..2d5e595a6b 100644 --- a/internal/execute/incremental/program.go +++ b/internal/execute/incremental/program.go @@ -262,7 +262,7 @@ func (p *Program) collectSemanticDiagnosticsOfAffectedFiles(ctx context.Context, } // Get their diagnostics and cache them - diagnosticsPerFile := p.program.GetSemanticDiagnosticsNoFilter(ctx, affectedFiles) + diagnosticsPerFile := p.program.GetSemanticDiagnosticsWithoutNoEmitFiltering(ctx, affectedFiles) // commit changes if no err if ctx.Err() != nil { return diff --git a/internal/project/checkerpool.go b/internal/project/checkerpool.go index 0e1ba1cd04..ec84b928cd 100644 --- a/internal/project/checkerpool.go +++ b/internal/project/checkerpool.go @@ -3,7 +3,6 @@ package project import ( "context" "fmt" - "iter" "sync" "github.com/microsoft/typescript-go/internal/ast" @@ -80,7 +79,7 @@ func (p *CheckerPool) GetCheckerForFile(ctx context.Context, file *ast.SourceFil // 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()) { - panic("unimplemented") // implement if used by LS + return p.GetCheckerForFile(ctx, file) } func (p *CheckerPool) GetChecker(ctx context.Context) (*checker.Checker, func()) { @@ -90,38 +89,6 @@ func (p *CheckerPool) GetChecker(ctx context.Context) (*checker.Checker, func()) return checker, p.createRelease(core.GetRequestID(ctx), index, checker) } -func (p *CheckerPool) Files(checker *checker.Checker) iter.Seq[*ast.SourceFile] { - panic("unimplemented") -} - -func (p *CheckerPool) Count() int { - return p.maxCheckers -} - -func (p *CheckerPool) ForEachCheckerParallel(ctx context.Context, cb func(idx int, c *checker.Checker)) { - p.mu.Lock() - defer p.mu.Unlock() - - requestID := core.GetRequestID(ctx) - if requestID == "" { - panic("cannot call ForEachCheckerParallel on a project.checkerPool without a request ID") - } - - // A request can only access one checker - if c, release := p.getRequestCheckerLocked(requestID); c != nil { - defer release() - cb(0, c) - return - } - - // TODO: Does this ever work without deadlocking? `p.GetChecker` also tries to lock this mutex. - // Should this just be a panic? - c, release := p.GetChecker(ctx) - defer release() - cb(0, c) - return -} - func (p *CheckerPool) getCheckerLocked(requestID string) (*checker.Checker, int) { if checker, index := p.getImmediatelyAvailableChecker(); checker != nil { p.inUse[checker] = true diff --git a/internal/testrunner/compiler_runner.go b/internal/testrunner/compiler_runner.go index 41f89814be..54ece44da7 100644 --- a/internal/testrunner/compiler_runner.go +++ b/internal/testrunner/compiler_runner.go @@ -518,7 +518,7 @@ func createHarnessTestFile(unit *testUnit, currentDirectory string) *harnessutil func (c *compilerTest) verifyUnionOrdering(t *testing.T) { t.Run("union ordering", func(t *testing.T) { p := c.result.Program.Program() - p.ForEachCheckerParallel(t.Context(), func(_ int, c *checker.Checker) { + p.ForEachCheckerParallel(func(_ int, c *checker.Checker) { for union := range c.UnionTypes() { types := union.Types()