From 7fb35607bb6eb3e902f6e41e3e739fc20367d608 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 10 Dec 2025 10:00:23 -0800 Subject: [PATCH 01/19] Remove GetOptionsDiagnostics --- internal/compiler/program.go | 23 +++++------------------ internal/execute/incremental/program.go | 7 ------- 2 files changed, 5 insertions(+), 25 deletions(-) diff --git a/internal/compiler/program.go b/internal/compiler/program.go index a9a6b47f3e2..ef7642466af 100644 --- a/internal/compiler/program.go +++ b/internal/compiler/program.go @@ -1031,7 +1031,10 @@ func (p *Program) GetGlobalDiagnostics(ctx context.Context) []*ast.Diagnostic { return nil } - pool := p.checkerPool.(*checkerPool) + pool, ok := p.checkerPool.(*checkerPool) + if !ok { + return nil // !!! Global diagnostics in the editor? + } globalDiagnostics := make([][]*ast.Diagnostic, len(pool.checkers)) pool.forEachCheckerParallel(func(idx int, checker *checker.Checker) { @@ -1045,17 +1048,6 @@ func (p *Program) GetDeclarationDiagnostics(ctx context.Context, sourceFile *ast 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 @@ -1445,7 +1437,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 @@ -1494,13 +1485,9 @@ 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)...) diff --git a/internal/execute/incremental/program.go b/internal/execute/incremental/program.go index 2d5e595a6ba..08a1aee1217 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 From e533802f20e1751c3e46b74f55e83222d2e69add Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 10 Dec 2025 10:01:02 -0800 Subject: [PATCH 02/19] Baselines --- .../reports-syntax-errors-in-config-file.js | 37 ++++--------------- .../reports-syntax-errors-in-config-file.js | 37 ++++--------------- 2 files changed, 16 insertions(+), 58 deletions(-) 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 23310d06e40..fa4e984be8c 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 @@ -69,7 +69,7 @@ exports.bar = bar; function bar() { } //// [/home/src/workspaces/project/tsconfig.tsbuildinfo] *new* -{"version":"FakeTSVersion","errors":true,"root":[[2,3]],"fileNames":["lib.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.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", @@ -130,20 +130,15 @@ function bar() { } "options": { "composite": true }, - "semanticDiagnosticsPerFile": [ - "lib.d.ts", - "./a.ts", - "./b.ts" - ], "latestChangedDtsFile": "./b.d.ts", - "size": 1345 + "size": 1308 } tsconfig.json:: SemanticDiagnostics:: -*not cached* /home/src/tslibs/TS/Lib/lib.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.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 @@ -175,9 +170,6 @@ Found 1 error in tsconfig.json:7 tsconfig.json:: SemanticDiagnostics:: -*not cached* /home/src/tslibs/TS/Lib/lib.d.ts -*not cached* /home/src/workspaces/project/a.ts -*not cached* /home/src/workspaces/project/b.ts Signatures:: @@ -209,7 +201,7 @@ function foo() { } function fooBar() { } //// [/home/src/workspaces/project/tsconfig.tsbuildinfo] *modified* -{"version":"FakeTSVersion","errors":true,"root":[[2,3]],"fileNames":["lib.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.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", @@ -271,20 +263,13 @@ function fooBar() { } "composite": true, "declaration": true }, - "semanticDiagnosticsPerFile": [ - "lib.d.ts", - "./a.ts", - "./b.ts" - ], "latestChangedDtsFile": "./a.d.ts", - "size": 1433 + "size": 1396 } tsconfig.json:: SemanticDiagnostics:: -*not cached* /home/src/tslibs/TS/Lib/lib.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 @@ -305,9 +290,6 @@ Found 1 error in tsconfig.json:7 tsconfig.json:: SemanticDiagnostics:: -*not cached* /home/src/tslibs/TS/Lib/lib.d.ts -*not cached* /home/src/workspaces/project/a.ts -*not cached* /home/src/workspaces/project/b.ts Signatures:: @@ -394,7 +376,4 @@ Output:: tsconfig.json:: SemanticDiagnostics:: -*refresh* /home/src/tslibs/TS/Lib/lib.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 1ae2b8dea6e..ab961f7a186 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 @@ -70,7 +70,7 @@ exports.bar = bar; function bar() { } //// [/home/src/workspaces/project/tsconfig.tsbuildinfo] *new* -{"version":"FakeTSVersion","errors":true,"root":[[2,3]],"fileNames":["lib.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.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", @@ -131,20 +131,15 @@ function bar() { } "options": { "composite": true }, - "semanticDiagnosticsPerFile": [ - "lib.d.ts", - "./a.ts", - "./b.ts" - ], "latestChangedDtsFile": "./b.d.ts", - "size": 1345 + "size": 1308 } tsconfig.json:: SemanticDiagnostics:: -*not cached* /home/src/tslibs/TS/Lib/lib.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.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 @@ -176,9 +171,6 @@ Output:: tsconfig.json:: SemanticDiagnostics:: -*not cached* /home/src/tslibs/TS/Lib/lib.d.ts -*not cached* /home/src/workspaces/project/a.ts -*not cached* /home/src/workspaces/project/b.ts Signatures:: @@ -210,7 +202,7 @@ function foo() { } function fooBar() { } //// [/home/src/workspaces/project/tsconfig.tsbuildinfo] *modified* -{"version":"FakeTSVersion","errors":true,"root":[[2,3]],"fileNames":["lib.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.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", @@ -272,20 +264,13 @@ function fooBar() { } "composite": true, "declaration": true }, - "semanticDiagnosticsPerFile": [ - "lib.d.ts", - "./a.ts", - "./b.ts" - ], "latestChangedDtsFile": "./a.d.ts", - "size": 1433 + "size": 1396 } tsconfig.json:: SemanticDiagnostics:: -*not cached* /home/src/tslibs/TS/Lib/lib.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 @@ -307,9 +292,6 @@ Output:: tsconfig.json:: SemanticDiagnostics:: -*not cached* /home/src/tslibs/TS/Lib/lib.d.ts -*not cached* /home/src/workspaces/project/a.ts -*not cached* /home/src/workspaces/project/b.ts Signatures:: @@ -399,7 +381,4 @@ Output:: tsconfig.json:: SemanticDiagnostics:: -*refresh* /home/src/tslibs/TS/Lib/lib.d.ts -*refresh* /home/src/workspaces/project/a.ts -*refresh* /home/src/workspaces/project/b.ts Signatures:: From 401c4ba9fd573540dae2e296b52f87849ea552b3 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 10 Dec 2025 10:50:37 -0800 Subject: [PATCH 03/19] Deal with globals --- internal/compiler/program.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/compiler/program.go b/internal/compiler/program.go index ef7642466af..4f15f547667 100644 --- a/internal/compiler/program.go +++ b/internal/compiler/program.go @@ -1493,6 +1493,8 @@ func GetDiagnosticsOfAnyProgram( 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 { From d381d136c0de7dd363a3728edf5f541ae2d9ce8f Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 10 Dec 2025 11:12:45 -0800 Subject: [PATCH 04/19] Also delete GetGlobalDiagnostics --- internal/compiler/program.go | 17 ++++++++--------- internal/execute/incremental/program.go | 8 +------- internal/testutil/harnessutil/harnessutil.go | 1 - 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/internal/compiler/program.go b/internal/compiler/program.go index 4f15f547667..33ebf9dd2e2 100644 --- a/internal/compiler/program.go +++ b/internal/compiler/program.go @@ -456,7 +456,11 @@ 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) + diags := p.collectDiagnostics(ctx, sourceFile, true /*concurrent*/, p.getSemanticDiagnosticsForFile) + if sourceFile == nil { + diags = core.Concatenate(diags, p.getGlobalDiagnostics()) + } + return SortAndDeduplicateDiagnostics(diags) } func (p *Program) GetSemanticDiagnosticsWithoutNoEmitFiltering(ctx context.Context, sourceFiles []*ast.SourceFile) map[*ast.SourceFile][]*ast.Diagnostic { @@ -1026,7 +1030,7 @@ func emitModuleKindIsNonNodeESM(moduleKind core.ModuleKind) bool { return moduleKind >= core.ModuleKindES2015 && moduleKind <= core.ModuleKindESNext } -func (p *Program) GetGlobalDiagnostics(ctx context.Context) []*ast.Diagnostic { +func (p *Program) getGlobalDiagnostics() []*ast.Diagnostic { if len(p.files) == 0 { return nil } @@ -1041,7 +1045,7 @@ func (p *Program) GetGlobalDiagnostics(ctx context.Context) []*ast.Diagnostic { globalDiagnostics[idx] = checker.GetGlobalDiagnostics() }) - return SortAndDeduplicateDiagnostics(slices.Concat(globalDiagnostics...)) + return slices.Concat(globalDiagnostics...) } func (p *Program) GetDeclarationDiagnostics(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic { @@ -1438,7 +1442,6 @@ type ProgramLike interface { GetSyntacticDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic GetBindDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic GetProgramDiagnostics() []*ast.Diagnostic - GetGlobalDiagnostics(ctx context.Context) []*ast.Diagnostic GetSemanticDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic GetDeclarationDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic GetSuggestionDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic @@ -1489,15 +1492,11 @@ func GetDiagnosticsOfAnyProgram( getBindDiagnostics(ctx, file) 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 { + if len(allDiagnostics) == configFileParsingDiagnosticsLength && (skipNoEmitCheckForDtsDiagnostics || program.Options().NoEmit.IsTrue()) && program.Options().GetEmitDeclarations() { allDiagnostics = append(allDiagnostics, program.GetDeclarationDiagnostics(ctx, file)...) } } diff --git a/internal/execute/incremental/program.go b/internal/execute/incremental/program.go index 08a1aee1217..bc8434bd630 100644 --- a/internal/execute/incremental/program.go +++ b/internal/execute/incremental/program.go @@ -137,11 +137,6 @@ func (p *Program) GetProgramDiagnostics() []*ast.Diagnostic { return p.program.GetProgramDiagnostics() } -func (p *Program) GetGlobalDiagnostics(ctx context.Context) []*ast.Diagnostic { - p.panicIfNoProgram("GetGlobalDiagnostics") - return p.program.GetGlobalDiagnostics(ctx) -} - // GetSemanticDiagnostics implements compiler.AnyProgram interface. func (p *Program) GetSemanticDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic { p.panicIfNoProgram("GetSemanticDiagnostics") @@ -358,8 +353,7 @@ func (p *Program) ensureHasErrorsForState(ctx context.Context, program *compiler if hasIncludeProcessingDiagnostics() || len(program.GetConfigFileParsingDiagnostics()) > 0 || len(program.GetSyntacticDiagnostics(ctx, nil)) > 0 || - len(program.GetProgramDiagnostics()) > 0 || - len(program.GetGlobalDiagnostics(ctx)) > 0 { + len(program.GetProgramDiagnostics()) > 0 { p.snapshot.hasErrors = core.TSTrue // Dont need to encode semantic errors state since the syntax and program diagnostics are encoded as present p.snapshot.hasSemanticErrors = false diff --git a/internal/testutil/harnessutil/harnessutil.go b/internal/testutil/harnessutil/harnessutil.go index 482552a77a0..b49ca1f597c 100644 --- a/internal/testutil/harnessutil/harnessutil.go +++ b/internal/testutil/harnessutil/harnessutil.go @@ -681,7 +681,6 @@ func compileFilesWithHost( diagnostics = append(diagnostics, program.GetProgramDiagnostics()...) diagnostics = append(diagnostics, program.GetSyntacticDiagnostics(ctx, nil)...) diagnostics = append(diagnostics, program.GetSemanticDiagnostics(ctx, nil)...) - diagnostics = append(diagnostics, program.GetGlobalDiagnostics(ctx)...) if config.CompilerOptions().GetEmitDeclarations() { diagnostics = append(diagnostics, program.GetDeclarationDiagnostics(ctx, nil)...) } From dc8b9936c2449c9416d5c79fd5a1712d3958b6ad Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 10 Dec 2025 11:36:44 -0800 Subject: [PATCH 05/19] Revert "Also delete GetGlobalDiagnostics" This reverts commit d381d136c0de7dd363a3728edf5f541ae2d9ce8f. --- internal/compiler/program.go | 17 +++++++++-------- internal/execute/incremental/program.go | 8 +++++++- internal/testutil/harnessutil/harnessutil.go | 1 + 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/internal/compiler/program.go b/internal/compiler/program.go index 33ebf9dd2e2..4f15f547667 100644 --- a/internal/compiler/program.go +++ b/internal/compiler/program.go @@ -456,11 +456,7 @@ func (p *Program) GetBindDiagnostics(ctx context.Context, sourceFile *ast.Source } func (p *Program) GetSemanticDiagnostics(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic { - diags := p.collectDiagnostics(ctx, sourceFile, true /*concurrent*/, p.getSemanticDiagnosticsForFile) - if sourceFile == nil { - diags = core.Concatenate(diags, p.getGlobalDiagnostics()) - } - return SortAndDeduplicateDiagnostics(diags) + return p.collectDiagnostics(ctx, sourceFile, true /*concurrent*/, p.getSemanticDiagnosticsForFile) } func (p *Program) GetSemanticDiagnosticsWithoutNoEmitFiltering(ctx context.Context, sourceFiles []*ast.SourceFile) map[*ast.SourceFile][]*ast.Diagnostic { @@ -1030,7 +1026,7 @@ func emitModuleKindIsNonNodeESM(moduleKind core.ModuleKind) bool { return moduleKind >= core.ModuleKindES2015 && moduleKind <= core.ModuleKindESNext } -func (p *Program) getGlobalDiagnostics() []*ast.Diagnostic { +func (p *Program) GetGlobalDiagnostics(ctx context.Context) []*ast.Diagnostic { if len(p.files) == 0 { return nil } @@ -1045,7 +1041,7 @@ func (p *Program) getGlobalDiagnostics() []*ast.Diagnostic { globalDiagnostics[idx] = checker.GetGlobalDiagnostics() }) - return slices.Concat(globalDiagnostics...) + return SortAndDeduplicateDiagnostics(slices.Concat(globalDiagnostics...)) } func (p *Program) GetDeclarationDiagnostics(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic { @@ -1442,6 +1438,7 @@ type ProgramLike interface { GetSyntacticDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic GetBindDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic GetProgramDiagnostics() []*ast.Diagnostic + GetGlobalDiagnostics(ctx context.Context) []*ast.Diagnostic GetSemanticDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic GetDeclarationDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic GetSuggestionDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic @@ -1492,11 +1489,15 @@ func GetDiagnosticsOfAnyProgram( getBindDiagnostics(ctx, file) 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 len(allDiagnostics) == configFileParsingDiagnosticsLength && (skipNoEmitCheckForDtsDiagnostics || program.Options().NoEmit.IsTrue()) && program.Options().GetEmitDeclarations() { + if (skipNoEmitCheckForDtsDiagnostics || program.Options().NoEmit.IsTrue()) && program.Options().GetEmitDeclarations() && len(allDiagnostics) == configFileParsingDiagnosticsLength { allDiagnostics = append(allDiagnostics, program.GetDeclarationDiagnostics(ctx, file)...) } } diff --git a/internal/execute/incremental/program.go b/internal/execute/incremental/program.go index bc8434bd630..08a1aee1217 100644 --- a/internal/execute/incremental/program.go +++ b/internal/execute/incremental/program.go @@ -137,6 +137,11 @@ func (p *Program) GetProgramDiagnostics() []*ast.Diagnostic { return p.program.GetProgramDiagnostics() } +func (p *Program) GetGlobalDiagnostics(ctx context.Context) []*ast.Diagnostic { + p.panicIfNoProgram("GetGlobalDiagnostics") + return p.program.GetGlobalDiagnostics(ctx) +} + // GetSemanticDiagnostics implements compiler.AnyProgram interface. func (p *Program) GetSemanticDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic { p.panicIfNoProgram("GetSemanticDiagnostics") @@ -353,7 +358,8 @@ func (p *Program) ensureHasErrorsForState(ctx context.Context, program *compiler if hasIncludeProcessingDiagnostics() || len(program.GetConfigFileParsingDiagnostics()) > 0 || len(program.GetSyntacticDiagnostics(ctx, nil)) > 0 || - len(program.GetProgramDiagnostics()) > 0 { + len(program.GetProgramDiagnostics()) > 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 p.snapshot.hasSemanticErrors = false diff --git a/internal/testutil/harnessutil/harnessutil.go b/internal/testutil/harnessutil/harnessutil.go index b49ca1f597c..482552a77a0 100644 --- a/internal/testutil/harnessutil/harnessutil.go +++ b/internal/testutil/harnessutil/harnessutil.go @@ -681,6 +681,7 @@ func compileFilesWithHost( diagnostics = append(diagnostics, program.GetProgramDiagnostics()...) diagnostics = append(diagnostics, program.GetSyntacticDiagnostics(ctx, nil)...) diagnostics = append(diagnostics, program.GetSemanticDiagnostics(ctx, nil)...) + diagnostics = append(diagnostics, program.GetGlobalDiagnostics(ctx)...) if config.CompilerOptions().GetEmitDeclarations() { diagnostics = append(diagnostics, program.GetDeclarationDiagnostics(ctx, nil)...) } From aa08cbfe1e4ea185a91dbbfd84e417cad1a7691e Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:33:07 -0800 Subject: [PATCH 06/19] update baselines --- .../tsc/composite/converting-to-modules.js | 14 ++++---------- ...with-import-in-jsdoc-in-composite-project.js | 17 +++++++++-------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/testdata/baselines/reference/tsc/composite/converting-to-modules.js b/testdata/baselines/reference/tsc/composite/converting-to-modules.js index 519a4747f95..3f7a52d47f6 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:: From bca71e95ba6289bea933535e318dbb04b3ade9a0 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:21:16 -0700 Subject: [PATCH 07/19] Expose global diagnostics through the LS --- internal/compiler/program.go | 4 +- internal/lsp/server.go | 3 ++ internal/project/checkerpool.go | 75 ++++++++++++++++++++++++++------- internal/project/project.go | 16 +++++++ internal/project/session.go | 30 ++++++++++++- 5 files changed, 109 insertions(+), 19 deletions(-) diff --git a/internal/compiler/program.go b/internal/compiler/program.go index 3a354c50f7c..9993dab9ef8 100644 --- a/internal/compiler/program.go +++ b/internal/compiler/program.go @@ -1171,7 +1171,9 @@ func (p *Program) GetGlobalDiagnostics(ctx context.Context) []*ast.Diagnostic { pool, ok := p.checkerPool.(*checkerPool) if !ok { - return nil // !!! Global diagnostics in the editor? + // In the editor, global diagnostics are accumulated by the project's CheckerPool + // and published to the tsconfig URI via push diagnostics. + return nil } globalDiagnostics := make([][]*ast.Diagnostic, len(pool.checkers)) 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..4b308ee3193 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,28 +16,32 @@ 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 + locks []sync.Mutex + 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), + locks: make([]sync.Mutex, maxCheckers), + inUse: make(map[*checker.Checker]bool), + requestAssociations: make(map[string]int), + log: log, + globalDiagCheckerCount: make([]int, maxCheckers), } pool.cond = sync.NewCond(&pool.mu) @@ -160,12 +165,16 @@ func (p *CheckerPool) createRelease(requestId string, index int, checker *checke p.mu.Lock() defer p.mu.Unlock() + // Collect new global diagnostics while we still exclusively hold the checker. + p.mergeGlobalDiagnosticsFromCheckerLocked(index, checker) + delete(p.requestAssociations, requestId) if checker.WasCanceled() { // Canceled checkers must be disposed 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.inUse[checker] = false } @@ -173,6 +182,40 @@ func (p *CheckerPool) createRelease(requestId string, index int, checker *checke } } +// 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) +} + +// GlobalDiagnosticsChanged reports whether global diagnostics have changed since +// the last call, and resets the flag. +func (p *CheckerPool) GlobalDiagnosticsChanged() 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 { diff --git a/internal/project/project.go b/internal/project/project.go index 7174bd34bc3..b8a6cf2502d 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -5,6 +5,7 @@ import ( "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 +208,21 @@ func (p *Project) GetProgram() *compiler.Program { return p.Program } +// GetProjectDiagnostics returns program diagnostics combined with any accumulated +// global diagnostics from the checker pool. These are the diagnostics reported on +// the tsconfig.json file. +func (p *Project) GetProjectDiagnostics() []*ast.Diagnostic { + programDiags := p.Program.GetProgramDiagnostics() + if p.checkerPool == nil { + return programDiags + } + globalDiags := p.checkerPool.GetGlobalDiagnostics() + if len(globalDiags) == 0 { + return programDiags + } + return compiler.SortAndDeduplicateDiagnostics(append(programDiags, globalDiags...)) +} + func (p *Project) HasFile(fileName string) bool { return p.containsFile(p.toPath(fileName)) } diff --git a/internal/project/session.go b/internal/project/session.go index ec35008fef9..d82a971eecc 100644 --- a/internal/project/session.go +++ b/internal/project/session.go @@ -1084,7 +1084,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(), newSnapshot.converters) }, func(configFilePath tspath.Path, removedProject *Project) { if removedProject.Kind != KindConfigured { @@ -1096,7 +1096,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(), newSnapshot.converters) }, ) } @@ -1122,6 +1122,32 @@ 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. +func (s *Session) EnqueuePublishGlobalDiagnostics() { + if !s.options.PushDiagnosticsEnabled { + return + } + s.backgroundQueue.Enqueue(s.backgroundCtx, s.publishGlobalDiagnostics) +} + +func (s *Session) publishGlobalDiagnostics(ctx context.Context) { + 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.Program == nil || project.checkerPool == nil { + continue + } + if project.checkerPool.GlobalDiagnosticsChanged() { + s.publishProjectDiagnostics(ctx, string(project.configFilePath), project.GetProjectDiagnostics(), snapshot.converters) + } + } +} + func (s *Session) triggerATAForUpdatedProjects(newSnapshot *Snapshot) { for _, project := range newSnapshot.ProjectCollection.Projects() { if project.ShouldTriggerATA(newSnapshot.ID()) { From 7ce9ed2cfe73c47f045a949e2aa2e53254870c5c Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:41:26 -0700 Subject: [PATCH 08/19] Shift things around --- internal/compiler/checkerpool.go | 10 ++++++++++ internal/compiler/program.go | 15 +-------------- internal/project/project.go | 20 ++++++++------------ internal/project/session.go | 8 ++++---- 4 files changed, 23 insertions(+), 30 deletions(-) diff --git a/internal/compiler/checkerpool.go b/internal/compiler/checkerpool.go index 84ea8d824a6..6b821c7adb6 100644 --- a/internal/compiler/checkerpool.go +++ b/internal/compiler/checkerpool.go @@ -14,6 +14,7 @@ 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()) + GetGlobalDiagnostics() []*ast.Diagnostic } type checkerPool struct { @@ -101,4 +102,13 @@ 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...)) +} + func noop() {} diff --git a/internal/compiler/program.go b/internal/compiler/program.go index 9993dab9ef8..771c2190aa9 100644 --- a/internal/compiler/program.go +++ b/internal/compiler/program.go @@ -1168,20 +1168,7 @@ func (p *Program) GetGlobalDiagnostics(ctx context.Context) []*ast.Diagnostic { if len(p.files) == 0 { return nil } - - pool, ok := p.checkerPool.(*checkerPool) - if !ok { - // In the editor, global diagnostics are accumulated by the project's CheckerPool - // and published to the tsconfig URI via push diagnostics. - return nil - } - - globalDiagnostics := make([][]*ast.Diagnostic, len(pool.checkers)) - pool.forEachCheckerParallel(func(idx int, checker *checker.Checker) { - globalDiagnostics[idx] = checker.GetGlobalDiagnostics() - }) - - return SortAndDeduplicateDiagnostics(slices.Concat(globalDiagnostics...)) + return p.checkerPool.GetGlobalDiagnostics() } func (p *Program) GetDeclarationDiagnostics(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic { diff --git a/internal/project/project.go b/internal/project/project.go index b8a6cf2502d..8094c3b5546 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -1,6 +1,7 @@ package project import ( + "context" "fmt" "strings" "sync" @@ -208,19 +209,14 @@ func (p *Project) GetProgram() *compiler.Program { return p.Program } -// GetProjectDiagnostics returns program diagnostics combined with any accumulated -// global diagnostics from the checker pool. These are the diagnostics reported on +// 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() []*ast.Diagnostic { - programDiags := p.Program.GetProgramDiagnostics() - if p.checkerPool == nil { - return programDiags - } - globalDiags := p.checkerPool.GetGlobalDiagnostics() - if len(globalDiags) == 0 { - return programDiags - } - return compiler.SortAndDeduplicateDiagnostics(append(programDiags, globalDiags...)) +func (p *Project) GetProjectDiagnostics(ctx context.Context) []*ast.Diagnostic { + return compiler.SortAndDeduplicateDiagnostics(core.Concatenate( + p.Program.GetProgramDiagnostics(), + p.Program.GetGlobalDiagnostics(ctx), + )) } func (p *Project) HasFile(fileName string) bool { diff --git a/internal/project/session.go b/internal/project/session.go index d82a971eecc..853cb45689f 100644 --- a/internal/project/session.go +++ b/internal/project/session.go @@ -1084,7 +1084,7 @@ func (s *Session) publishProgramDiagnostics(oldSnapshot *Snapshot, newSnapshot * if !shouldPublishProgramDiagnostics(addedProject, newSnapshot.ID()) { return } - s.publishProjectDiagnostics(ctx, string(configFilePath), addedProject.GetProjectDiagnostics(), newSnapshot.converters) + s.publishProjectDiagnostics(ctx, string(configFilePath), addedProject.GetProjectDiagnostics(ctx), newSnapshot.converters) }, func(configFilePath tspath.Path, removedProject *Project) { if removedProject.Kind != KindConfigured { @@ -1096,7 +1096,7 @@ func (s *Session) publishProgramDiagnostics(oldSnapshot *Snapshot, newSnapshot * if !shouldPublishProgramDiagnostics(newProject, newSnapshot.ID()) { return } - s.publishProjectDiagnostics(ctx, string(configFilePath), newProject.GetProjectDiagnostics(), newSnapshot.converters) + s.publishProjectDiagnostics(ctx, string(configFilePath), newProject.GetProjectDiagnostics(ctx), newSnapshot.converters) }, ) } @@ -1139,11 +1139,11 @@ func (s *Session) publishGlobalDiagnostics(ctx context.Context) { defer snapshot.Deref(s) for _, project := range snapshot.ProjectCollection.Projects() { - if project.Kind != KindConfigured || project.Program == nil || project.checkerPool == nil { + if project.Kind != KindConfigured || project.checkerPool == nil { continue } if project.checkerPool.GlobalDiagnosticsChanged() { - s.publishProjectDiagnostics(ctx, string(project.configFilePath), project.GetProjectDiagnostics(), snapshot.converters) + s.publishProjectDiagnostics(ctx, string(project.configFilePath), project.GetProjectDiagnostics(ctx), snapshot.converters) } } } From 72db87fc04768d91aaf0d5f3504ed8a0fd6b9a37 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:57:43 -0700 Subject: [PATCH 09/19] Add a test --- internal/project/project_test.go | 50 ++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/internal/project/project_test.go b/internal/project/project_test.go index bdf8517d844..be3b96845d2 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,55 @@ 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) + ls.ProvideDiagnostics(projecttestutil.WithRequestID(context.Background()), lsproto.DocumentUri("file:///src/index.ts")) + // 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) { From 023d3ddc8722a1a0e159bf0be6c1e7252ce0ffb2 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 27 Mar 2026 20:10:42 -0700 Subject: [PATCH 10/19] PR feedback --- internal/project/checkerpool.go | 10 ++++------ internal/project/session.go | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/internal/project/checkerpool.go b/internal/project/checkerpool.go index 4b308ee3193..1b1754f200c 100644 --- a/internal/project/checkerpool.go +++ b/internal/project/checkerpool.go @@ -165,9 +165,6 @@ func (p *CheckerPool) createRelease(requestId string, index int, checker *checke p.mu.Lock() defer p.mu.Unlock() - // Collect new global diagnostics while we still exclusively hold the checker. - p.mergeGlobalDiagnosticsFromCheckerLocked(index, checker) - delete(p.requestAssociations, requestId) if checker.WasCanceled() { // Canceled checkers must be disposed @@ -176,6 +173,7 @@ func (p *CheckerPool) createRelease(requestId string, index int, checker *checke delete(p.inUse, checker) p.globalDiagCheckerCount[index] = 0 } else { + p.mergeGlobalDiagnosticsFromCheckerLocked(index, checker) p.inUse[checker] = false } p.cond.Signal() @@ -206,9 +204,9 @@ func (p *CheckerPool) GetGlobalDiagnostics() []*ast.Diagnostic { return slices.Clone(p.globalDiagAccumulated) } -// GlobalDiagnosticsChanged reports whether global diagnostics have changed since -// the last call, and resets the flag. -func (p *CheckerPool) GlobalDiagnosticsChanged() bool { +// 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 diff --git a/internal/project/session.go b/internal/project/session.go index 853cb45689f..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 { @@ -1124,14 +1129,19 @@ 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 } - s.backgroundQueue.Enqueue(s.backgroundCtx, s.publishGlobalDiagnostics) + 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() @@ -1142,7 +1152,7 @@ func (s *Session) publishGlobalDiagnostics(ctx context.Context) { if project.Kind != KindConfigured || project.checkerPool == nil { continue } - if project.checkerPool.GlobalDiagnosticsChanged() { + if project.checkerPool.TakeNewGlobalDiagnostics() { s.publishProjectDiagnostics(ctx, string(project.configFilePath), project.GetProjectDiagnostics(ctx), snapshot.converters) } } From 945bf2309f06a8c5aa7661fe97ee4bc78fb312d5 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 27 Mar 2026 20:59:00 -0700 Subject: [PATCH 11/19] Fix lint --- internal/project/project_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/project/project_test.go b/internal/project/project_test.go index be3b96845d2..b92f27e0a3f 100644 --- a/internal/project/project_test.go +++ b/internal/project/project_test.go @@ -326,7 +326,8 @@ func TestPushDiagnostics(t *testing.T) { // 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) - ls.ProvideDiagnostics(projecttestutil.WithRequestID(context.Background()), lsproto.DocumentUri("file:///src/index.ts")) + _, 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() From 98c71dd0f640704fc5d110528c1bc4d06bf4eb08 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sat, 28 Mar 2026 11:17:51 -0700 Subject: [PATCH 12/19] Restore per-checker looping --- internal/compiler/checkerpool.go | 39 +++++++++++++++++ internal/compiler/program.go | 75 ++++++++++++++++++++++---------- 2 files changed, 90 insertions(+), 24 deletions(-) diff --git a/internal/compiler/checkerpool.go b/internal/compiler/checkerpool.go index 6b821c7adb6..a3010d07bf0 100644 --- a/internal/compiler/checkerpool.go +++ b/internal/compiler/checkerpool.go @@ -111,4 +111,43 @@ func (p *checkerPool) GetGlobalDiagnostics() []*ast.Diagnostic { return SortAndDeduplicateDiagnostics(slices.Concat(globalDiagnostics...)) } +// forEachCheckerGroupDo groups the provided files by their associated checker and +// processes each group in parallel, one task per checker. Within each group, files +// are processed sequentially in their original order relative to the input slice. +// The callback receives the checker (held exclusively) along with the file index and file. +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) + // Build reverse map from checker pointer to index for efficient grouping. + checkerIndices := make(map[*checker.Checker]int, checkerCount) + for i, c := range p.checkers { + checkerIndices[c] = i + } + + // Group file indices by their associated checker, preserving relative order. + groups := make([][]int, checkerCount) + for i, file := range files { + c := p.fileAssociations[file] + idx := checkerIndices[c] + groups[idx] = append(groups[idx], i) + } + + // Process each checker's files in parallel, one task per checker. + wg := core.NewWorkGroup(singleThreaded) + for checkerIdx := range checkerCount { + if len(groups[checkerIdx]) == 0 { + continue + } + wg.Queue(func() { + p.locks[checkerIdx].Lock() + defer p.locks[checkerIdx].Unlock() + for _, fileIdx := range groups[checkerIdx] { + cb(p.checkers[checkerIdx], fileIdx, files[fileIdx]) + } + }) + } + wg.RunAndWait() +} + func noop() {} diff --git a/internal/compiler/program.go b/internal/compiler/program.go index 771c2190aa9..38d3d1e62bf 100644 --- a/internal/compiler/program.go +++ b/internal/compiler/program.go @@ -477,6 +477,42 @@ 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 { + c, done := p.checkerPool.GetCheckerForFileExclusive(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 pool, ok := p.checkerPool.(*checkerPool); ok { + pool.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 { + wg.Queue(func() { + c, done := p.checkerPool.GetCheckerForFileExclusive(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 +569,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 { @@ -1184,31 +1220,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 { @@ -1285,15 +1315,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)...) From 80946c6e6341a9e1b5b044fa0d70314194aa0f77 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sat, 28 Mar 2026 11:19:32 -0700 Subject: [PATCH 13/19] Restore SkipTypeChecking early bail outs --- internal/compiler/program.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/compiler/program.go b/internal/compiler/program.go index 38d3d1e62bf..7fd45500458 100644 --- a/internal/compiler/program.go +++ b/internal/compiler/program.go @@ -484,6 +484,9 @@ func (p *Program) collectDiagnosticsFromFiles(ctx context.Context, sourceFiles [ // 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.checkerPool.GetCheckerForFileExclusive(ctx, sourceFile) result := collect(ctx, c, sourceFile) done() @@ -502,6 +505,9 @@ func (p *Program) collectCheckerDiagnosticsFromFiles(ctx context.Context, source } else { wg := core.NewWorkGroup(p.SingleThreaded()) for i, file := range sourceFiles { + if p.SkipTypeChecking(file, false) { + continue + } wg.Queue(func() { c, done := p.checkerPool.GetCheckerForFileExclusive(ctx, file) diagnostics[i] = collect(ctx, c, file) From d62855c41ac2fc523d1a7ca44f5f83211e4d18fc Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sat, 28 Mar 2026 11:37:12 -0700 Subject: [PATCH 14/19] reconfigure code to require less from CheckerPool --- internal/compiler/checkerpool.go | 30 +++++++++++++++++++---- internal/compiler/program.go | 41 ++++++++++++++++++++++++-------- internal/project/checkerpool.go | 6 ----- internal/project/project.go | 6 ++++- 4 files changed, 61 insertions(+), 22 deletions(-) diff --git a/internal/compiler/checkerpool.go b/internal/compiler/checkerpool.go index a3010d07bf0..ed679b7fb85 100644 --- a/internal/compiler/checkerpool.go +++ b/internal/compiler/checkerpool.go @@ -10,11 +10,13 @@ import ( "github.com/microsoft/typescript-go/internal/core" ) +// CheckerPool is implemented by the project system to provide checkers with +// request-scoped lifetime and reclamation. Both methods return 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. 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()) - GetGlobalDiagnostics() []*ast.Diagnostic } type checkerPool struct { @@ -47,12 +49,21 @@ func newCheckerPool(program *Program) *checkerPool { return pool } +// GetCheckerForFile returns the checker for the given file with exclusive access. +// The returned release function must be called when the caller is done. func (p *checkerPool) GetCheckerForFile(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) { + return p.getCheckerForFileExclusive(ctx, file) +} + +// 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(ctx context.Context, 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) @@ -64,8 +75,17 @@ func (p *checkerPool) GetCheckerForFileExclusive(ctx context.Context, file *ast. func (p *checkerPool) GetChecker(ctx context.Context) (*checker.Checker, func()) { p.createCheckers() - checker := p.checkers[0] - return checker, noop + c := p.checkers[0] + p.locks[0].Lock() + return c, sync.OnceFunc(func() { + p.locks[0].Unlock() + }) +} + +// getCheckerNonExclusive returns the first checker without locking. +func (p *checkerPool) getCheckerNonExclusive(ctx context.Context) (*checker.Checker, func()) { + p.createCheckers() + return p.checkers[0], noop } func (p *checkerPool) createCheckers() { diff --git a/internal/compiler/program.go b/internal/compiler/program.go index 7fd45500458..460a6e4d41d 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()) { + if p.compilerCheckerPool != nil { + return p.compilerCheckerPool.getCheckerNonExclusive(ctx) + } return p.checkerPool.GetChecker(ctx) } 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()) { + if p.compilerCheckerPool != nil { + return p.compilerCheckerPool.getCheckerForFileNonExclusive(ctx, file) + } return p.checkerPool.GetCheckerForFile(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.GetCheckerForFile(ctx, file) } func (p *Program) GetResolvedModule(file ast.HasFileName, moduleReference string, mode core.ResolutionMode) *module.ResolvedModule { @@ -487,7 +503,7 @@ func (p *Program) collectCheckerDiagnostics(ctx context.Context, sourceFile *ast if p.SkipTypeChecking(sourceFile, false) { return nil } - c, done := p.checkerPool.GetCheckerForFileExclusive(ctx, sourceFile) + c, done := p.GetTypeCheckerForFileExclusive(ctx, sourceFile) result := collect(ctx, c, sourceFile) done() return SortAndDeduplicateDiagnostics(result) @@ -498,8 +514,8 @@ func (p *Program) collectCheckerDiagnostics(ctx context.Context, sourceFile *ast // 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 pool, ok := p.checkerPool.(*checkerPool); ok { - pool.forEachCheckerGroupDo(ctx, sourceFiles, p.SingleThreaded(), func(c *checker.Checker, fileIndex int, file *ast.SourceFile) { + 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 { @@ -509,7 +525,7 @@ func (p *Program) collectCheckerDiagnosticsFromFiles(ctx context.Context, source continue } wg.Queue(func() { - c, done := p.checkerPool.GetCheckerForFileExclusive(ctx, file) + c, done := p.checkerPool.GetCheckerForFile(ctx, file) diagnostics[i] = collect(ctx, c, file) done() }) @@ -1210,7 +1226,12 @@ func (p *Program) GetGlobalDiagnostics(ctx context.Context) []*ast.Diagnostic { if len(p.files) == 0 { return nil } - return p.checkerPool.GetGlobalDiagnostics() + 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 { diff --git a/internal/project/checkerpool.go b/internal/project/checkerpool.go index 1b1754f200c..6b08f0da296 100644 --- a/internal/project/checkerpool.go +++ b/internal/project/checkerpool.go @@ -81,12 +81,6 @@ func (p *CheckerPool) GetCheckerForFile(ctx context.Context, file *ast.SourceFil 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() diff --git a/internal/project/project.go b/internal/project/project.go index 8094c3b5546..e873c3c79fa 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -213,9 +213,13 @@ func (p *Project) GetProgram() *compiler.Program { // 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(), - p.Program.GetGlobalDiagnostics(ctx), + globalDiags, )) } From 3e6e91fd4fd938a4deac5c3cbff1d10f9a6dc719 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:03:08 -0700 Subject: [PATCH 15/19] Simplfy checker pool down even harder --- internal/compiler/checkerpool.go | 38 +++++++++--------- internal/compiler/program.go | 12 +++--- internal/project/checkerpool.go | 67 ++++++++++---------------------- 3 files changed, 45 insertions(+), 72 deletions(-) diff --git a/internal/compiler/checkerpool.go b/internal/compiler/checkerpool.go index ed679b7fb85..bf47b68e527 100644 --- a/internal/compiler/checkerpool.go +++ b/internal/compiler/checkerpool.go @@ -11,12 +11,13 @@ import ( ) // CheckerPool is implemented by the project system to provide checkers with -// request-scoped lifetime and reclamation. Both methods return a checker and a -// release function that must be called when the caller is done with the checker. +// 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()) + GetChecker(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) } type checkerPool struct { @@ -49,16 +50,24 @@ func newCheckerPool(program *Program) *checkerPool { return pool } -// GetCheckerForFile returns the checker for the given file with exclusive access. -// The returned release function must be called when the caller is done. -func (p *checkerPool) GetCheckerForFile(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) { - return p.getCheckerForFileExclusive(ctx, file) +// 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(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) { +func (p *checkerPool) getCheckerForFileNonExclusive(file *ast.SourceFile) (*checker.Checker, func()) { p.createCheckers() return p.fileAssociations[file], noop } @@ -73,17 +82,8 @@ func (p *checkerPool) getCheckerForFileExclusive(ctx context.Context, file *ast. }) } -func (p *checkerPool) GetChecker(ctx context.Context) (*checker.Checker, func()) { - p.createCheckers() - c := p.checkers[0] - p.locks[0].Lock() - return c, sync.OnceFunc(func() { - p.locks[0].Unlock() - }) -} - // getCheckerNonExclusive returns the first checker without locking. -func (p *checkerPool) getCheckerNonExclusive(ctx context.Context) (*checker.Checker, func()) { +func (p *checkerPool) getCheckerNonExclusive() (*checker.Checker, func()) { p.createCheckers() return p.checkers[0], noop } diff --git a/internal/compiler/program.go b/internal/compiler/program.go index 460a6e4d41d..415ea1d8924 100644 --- a/internal/compiler/program.go +++ b/internal/compiler/program.go @@ -415,9 +415,9 @@ func (p *Program) BindSourceFiles() { // Return the type checker associated with the program. func (p *Program) GetTypeChecker(ctx context.Context) (*checker.Checker, func()) { if p.compilerCheckerPool != nil { - return p.compilerCheckerPool.getCheckerNonExclusive(ctx) + return p.compilerCheckerPool.getCheckerNonExclusive() } - return p.checkerPool.GetChecker(ctx) + return p.checkerPool.GetChecker(ctx, nil) } func (p *Program) ForEachCheckerParallel(cb func(idx int, c *checker.Checker)) { @@ -432,9 +432,9 @@ func (p *Program) ForEachCheckerParallel(cb func(idx int, c *checker.Checker)) { // 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()) { if p.compilerCheckerPool != nil { - return p.compilerCheckerPool.getCheckerForFileNonExclusive(ctx, file) + return p.compilerCheckerPool.getCheckerForFileNonExclusive(file) } - return p.checkerPool.GetCheckerForFile(ctx, 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 @@ -443,7 +443,7 @@ func (p *Program) GetTypeCheckerForFileExclusive(ctx context.Context, file *ast. if p.compilerCheckerPool != nil { return p.compilerCheckerPool.getCheckerForFileExclusive(ctx, file) } - return p.checkerPool.GetCheckerForFile(ctx, file) + return p.checkerPool.GetChecker(ctx, file) } func (p *Program) GetResolvedModule(file ast.HasFileName, moduleReference string, mode core.ResolutionMode) *module.ResolvedModule { @@ -525,7 +525,7 @@ func (p *Program) collectCheckerDiagnosticsFromFiles(ctx context.Context, source continue } wg.Queue(func() { - c, done := p.checkerPool.GetCheckerForFile(ctx, file) + c, done := p.checkerPool.GetChecker(ctx, file) diagnostics[i] = collect(ctx, c, file) done() }) diff --git a/internal/project/checkerpool.go b/internal/project/checkerpool.go index 6b08f0da296..f05df1cb3b2 100644 --- a/internal/project/checkerpool.go +++ b/internal/project/checkerpool.go @@ -20,7 +20,6 @@ type CheckerPool struct { 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 @@ -37,7 +36,6 @@ func newCheckerPool(maxCheckers int, program *compiler.Program, log func(msg str 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, @@ -48,7 +46,7 @@ func newCheckerPool(maxCheckers int, program *compiler.Program, log func(msg str 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() @@ -59,35 +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) } -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 @@ -228,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() {} From 8937d0a2ffe24c1ff1995abaa2be8a9388e10a4b Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:47:46 -0700 Subject: [PATCH 16/19] Baselines --- .../findAllRefsForModuleGlobal.baseline.jsonc | 2 +- .../findAllRefsForModuleGlobal.baseline.jsonc.diff | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) delete mode 100644 testdata/baselines/reference/submodule/fourslash/findAllReferences/findAllRefsForModuleGlobal.baseline.jsonc.diff 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 From c6c4c12da5c0f4de6cb9e7c93414b846b1765b85 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sat, 28 Mar 2026 13:06:41 -0700 Subject: [PATCH 17/19] Just loop over every file, it's fine --- internal/compiler/checkerpool.go | 31 +++++++------------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/internal/compiler/checkerpool.go b/internal/compiler/checkerpool.go index bf47b68e527..ec5db2edb91 100644 --- a/internal/compiler/checkerpool.go +++ b/internal/compiler/checkerpool.go @@ -131,39 +131,22 @@ func (p *checkerPool) GetGlobalDiagnostics() []*ast.Diagnostic { return SortAndDeduplicateDiagnostics(slices.Concat(globalDiagnostics...)) } -// forEachCheckerGroupDo groups the provided files by their associated checker and -// processes each group in parallel, one task per checker. Within each group, files -// are processed sequentially in their original order relative to the input slice. -// The callback receives the checker (held exclusively) along with the file index and file. +// 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) - // Build reverse map from checker pointer to index for efficient grouping. - checkerIndices := make(map[*checker.Checker]int, checkerCount) - for i, c := range p.checkers { - checkerIndices[c] = i - } - - // Group file indices by their associated checker, preserving relative order. - groups := make([][]int, checkerCount) - for i, file := range files { - c := p.fileAssociations[file] - idx := checkerIndices[c] - groups[idx] = append(groups[idx], i) - } - - // Process each checker's files in parallel, one task per checker. wg := core.NewWorkGroup(singleThreaded) for checkerIdx := range checkerCount { - if len(groups[checkerIdx]) == 0 { - continue - } wg.Queue(func() { p.locks[checkerIdx].Lock() defer p.locks[checkerIdx].Unlock() - for _, fileIdx := range groups[checkerIdx] { - cb(p.checkers[checkerIdx], fileIdx, files[fileIdx]) + for i, file := range files { + if p.fileAssociations[file] == p.checkers[checkerIdx] { + cb(p.checkers[checkerIdx], i, file) + } } }) } From a0fc2b957b6407579989182c7ce565a661981d87 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sat, 28 Mar 2026 13:08:11 -0700 Subject: [PATCH 18/19] simpl --- internal/compiler/checkerpool.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/compiler/checkerpool.go b/internal/compiler/checkerpool.go index ec5db2edb91..5179a1beeb0 100644 --- a/internal/compiler/checkerpool.go +++ b/internal/compiler/checkerpool.go @@ -144,8 +144,8 @@ func (p *checkerPool) forEachCheckerGroupDo(ctx context.Context, files []*ast.So p.locks[checkerIdx].Lock() defer p.locks[checkerIdx].Unlock() for i, file := range files { - if p.fileAssociations[file] == p.checkers[checkerIdx] { - cb(p.checkers[checkerIdx], i, file) + if c := p.checkers[checkerIdx]; c == p.fileAssociations[file] { + cb(c, i, file) } } }) From b61f52043e3ddc9bef90691a3aaea65b9c69f6ba Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sat, 28 Mar 2026 13:08:24 -0700 Subject: [PATCH 19/19] name --- internal/compiler/checkerpool.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/compiler/checkerpool.go b/internal/compiler/checkerpool.go index 5179a1beeb0..3b0d68633b2 100644 --- a/internal/compiler/checkerpool.go +++ b/internal/compiler/checkerpool.go @@ -144,8 +144,8 @@ func (p *checkerPool) forEachCheckerGroupDo(ctx context.Context, files []*ast.So p.locks[checkerIdx].Lock() defer p.locks[checkerIdx].Unlock() for i, file := range files { - if c := p.checkers[checkerIdx]; c == p.fileAssociations[file] { - cb(c, i, file) + if checker := p.checkers[checkerIdx]; checker == p.fileAssociations[file] { + cb(checker, i, file) } } })