Skip to content

Commit c0c5840

Browse files
authored
Support cancellation in checker (#856)
1 parent 4252f06 commit c0c5840

File tree

9 files changed

+110
-44
lines changed

9 files changed

+110
-44
lines changed

cmd/tsgo/main.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
package main
33

44
import (
5+
"context"
56
"encoding/json"
67
"flag"
78
"fmt"
@@ -223,19 +224,19 @@ func runMain() int {
223224
return 1
224225
}
225226

226-
diagnostics = program.GetSyntacticDiagnostics(nil)
227+
diagnostics = program.GetSyntacticDiagnostics(context.Background(), nil)
227228
if len(diagnostics) == 0 {
228229
if opts.devel.printTypes {
229230
program.PrintSourceFileWithTypes()
230231
} else {
231232
bindStart := time.Now()
232-
_ = program.GetBindDiagnostics(nil)
233+
_ = program.GetBindDiagnostics(context.Background(), nil)
233234
bindTime = time.Since(bindStart)
234235

235236
// !!! the checker already reads noCheck, but do it here just for stats printing for now
236237
if compilerOptions.NoCheck.IsFalseOrUnknown() {
237238
checkStart := time.Now()
238-
diagnostics = slices.Concat(program.GetGlobalDiagnostics(), program.GetSemanticDiagnostics(nil))
239+
diagnostics = slices.Concat(program.GetGlobalDiagnostics(), program.GetSemanticDiagnostics(context.Background(), nil))
239240
checkTime = time.Since(checkStart)
240241
}
241242
}

internal/ast/ast.go

+20
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,26 @@ func (n *Node) Members() []*Node {
472472
return nil
473473
}
474474

475+
func (n *Node) StatementList() *NodeList {
476+
switch n.Kind {
477+
case KindSourceFile:
478+
return n.AsSourceFile().Statements
479+
case KindBlock:
480+
return n.AsBlock().Statements
481+
case KindModuleBlock:
482+
return n.AsModuleBlock().Statements
483+
}
484+
panic("Unhandled case in Node.StatementList: " + n.Kind.String())
485+
}
486+
487+
func (n *Node) Statements() []*Node {
488+
list := n.StatementList()
489+
if list != nil {
490+
return list.Nodes
491+
}
492+
return nil
493+
}
494+
475495
func (n *Node) ModifierFlags() ModifierFlags {
476496
modifiers := n.Modifiers()
477497
if modifiers != nil {

internal/checker/checker.go

+43-16
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package checker
22

33
import (
4+
"context"
45
"fmt"
56
"iter"
67
"maps"
@@ -567,6 +568,7 @@ type Checker struct {
567568
useUnknownInCatchVariables bool
568569
exactOptionalPropertyTypes bool
569570
canCollectSymbolAliasAccessibilityData bool
571+
wasCanceled bool
570572
arrayVariances []VarianceFlags
571573
globals ast.SymbolTable
572574
globalSymbols []*ast.Symbol
@@ -824,6 +826,7 @@ type Checker struct {
824826
_jsxNamespace string
825827
_jsxFactoryEntity *ast.Node
826828
skipDirectInferenceNodes core.Set[*ast.Node]
829+
ctx context.Context
827830
}
828831

829832
func NewChecker(program Program) *Checker {
@@ -2045,16 +2048,18 @@ func (c *Checker) getSymbol(symbols ast.SymbolTable, name string, meaning ast.Sy
20452048
return nil
20462049
}
20472050

2048-
func (c *Checker) CheckSourceFile(sourceFile *ast.SourceFile) {
2051+
func (c *Checker) CheckSourceFile(ctx context.Context, sourceFile *ast.SourceFile) {
20492052
if SkipTypeChecking(sourceFile, c.compilerOptions) {
20502053
return
20512054
}
2052-
c.checkSourceFile(sourceFile)
2055+
c.checkSourceFile(ctx, sourceFile)
20532056
}
20542057

2055-
func (c *Checker) checkSourceFile(sourceFile *ast.SourceFile) {
2058+
func (c *Checker) checkSourceFile(ctx context.Context, sourceFile *ast.SourceFile) {
2059+
c.checkNotCanceled()
20562060
links := c.sourceFileLinks.Get(sourceFile)
20572061
if !links.typeChecked {
2062+
c.ctx = ctx
20582063
// Grammar checking
20592064
c.checkGrammarSourceFile(sourceFile)
20602065
c.renamedBindingElementsInTypes = nil
@@ -2065,19 +2070,27 @@ func (c *Checker) checkSourceFile(sourceFile *ast.SourceFile) {
20652070
c.checkExternalModuleExports(sourceFile.AsNode())
20662071
c.registerForUnusedIdentifiersCheck(sourceFile.AsNode())
20672072
}
2068-
// This relies on the results of other lazy diagnostics, so must be computed after them
2069-
if !sourceFile.IsDeclarationFile && (c.compilerOptions.NoUnusedLocals.IsTrue() || c.compilerOptions.NoUnusedParameters.IsTrue()) {
2070-
c.checkUnusedIdentifiers(links.identifierCheckNodes)
2071-
}
2072-
if !sourceFile.IsDeclarationFile {
2073-
c.checkUnusedRenamedBindingElements()
2073+
if ctx.Err() == nil {
2074+
// This relies on the results of other lazy diagnostics, so must be computed after them
2075+
if !sourceFile.IsDeclarationFile && (c.compilerOptions.NoUnusedLocals.IsTrue() || c.compilerOptions.NoUnusedParameters.IsTrue()) {
2076+
c.checkUnusedIdentifiers(links.identifierCheckNodes)
2077+
}
2078+
if !sourceFile.IsDeclarationFile {
2079+
c.checkUnusedRenamedBindingElements()
2080+
}
2081+
} else {
2082+
c.wasCanceled = true
20742083
}
2084+
c.ctx = nil
20752085
links.typeChecked = true
20762086
}
20772087
}
20782088

20792089
func (c *Checker) checkSourceElements(nodes []*ast.Node) {
20802090
for _, node := range nodes {
2091+
if c.isCanceled() {
2092+
break
2093+
}
20812094
c.checkSourceElement(node)
20822095
}
20832096
}
@@ -2094,7 +2107,6 @@ func (c *Checker) checkSourceElement(node *ast.Node) bool {
20942107
}
20952108

20962109
func (c *Checker) checkSourceElementWorker(node *ast.Node) {
2097-
// !!! Cancellation
20982110
kind := node.Kind
20992111
if kind >= ast.KindFirstStatement && kind <= ast.KindLastStatement {
21002112
flowNode := node.FlowNodeData().FlowNode
@@ -2246,6 +2258,9 @@ func (c *Checker) checkNodeDeferred(node *ast.Node) {
22462258
func (c *Checker) checkDeferredNodes(context *ast.SourceFile) {
22472259
links := c.sourceFileLinks.Get(context)
22482260
for node := range links.deferredNodes.Values() {
2261+
if c.isCanceled() {
2262+
break
2263+
}
22492264
c.checkDeferredNode(node)
22502265
}
22512266
links.deferredNodes.Clear()
@@ -2291,6 +2306,9 @@ func (c *Checker) checkJSDocNodes(sourceFile *ast.SourceFile) {
22912306
// set parent references in JSDoc nodes.
22922307
for location, jsdocs := range sourceFile.JSDocCache() {
22932308
for _, jsdoc := range jsdocs {
2309+
if c.isCanceled() {
2310+
return
2311+
}
22942312
c.checkJSDocComments(jsdoc, location)
22952313
tags := jsdoc.AsJSDoc().Tags
22962314
if tags != nil {
@@ -3510,10 +3528,10 @@ func (c *Checker) checkBlock(node *ast.Node) {
35103528
}
35113529
if ast.IsFunctionOrModuleBlock(node) {
35123530
saveFlowAnalysisDisabled := c.flowAnalysisDisabled
3513-
node.ForEachChild(c.checkSourceElement)
3531+
c.checkSourceElements(node.Statements())
35143532
c.flowAnalysisDisabled = saveFlowAnalysisDisabled
35153533
} else {
3516-
node.ForEachChild(c.checkSourceElement)
3534+
c.checkSourceElements(node.Statements())
35173535
}
35183536
if len(node.Locals()) != 0 {
35193537
c.registerForUnusedIdentifiersCheck(node)
@@ -13134,22 +13152,31 @@ func (c *Checker) getCannotFindNameDiagnosticForName(node *ast.Node) *diagnostic
1313413152
}
1313513153
}
1313613154

13137-
func (c *Checker) GetDiagnostics(sourceFile *ast.SourceFile) []*ast.Diagnostic {
13155+
func (c *Checker) GetDiagnostics(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic {
13156+
c.checkNotCanceled()
1313813157
if sourceFile != nil {
13139-
c.CheckSourceFile(sourceFile)
13158+
c.CheckSourceFile(ctx, sourceFile)
13159+
if c.wasCanceled {
13160+
return nil
13161+
}
1314013162
return c.diagnostics.GetDiagnosticsForFile(sourceFile.FileName())
1314113163
}
1314213164
for _, file := range c.files {
13143-
c.CheckSourceFile(file)
13165+
c.CheckSourceFile(ctx, file)
13166+
if c.wasCanceled {
13167+
return nil
13168+
}
1314413169
}
1314513170
return c.diagnostics.GetDiagnostics()
1314613171
}
1314713172

1314813173
func (c *Checker) GetDiagnosticsWithoutCheck(sourceFile *ast.SourceFile) []*ast.Diagnostic {
13174+
c.checkNotCanceled()
1314913175
return c.diagnostics.GetDiagnosticsForFile(sourceFile.FileName())
1315013176
}
1315113177

1315213178
func (c *Checker) GetGlobalDiagnostics() []*ast.Diagnostic {
13179+
c.checkNotCanceled()
1315313180
return c.diagnostics.GetGlobalDiagnostics()
1315413181
}
1315513182

@@ -29800,7 +29827,7 @@ func (c *Checker) GetEmitResolver(file *ast.SourceFile, skipDiagnostics bool) pr
2980029827
// emitter questions of this resolver will return the right information.
2980129828
c.emitResolver.checkerMu.Lock()
2980229829
defer c.emitResolver.checkerMu.Unlock()
29803-
c.checkSourceFile(file)
29830+
c.checkSourceFile(context.Background(), file)
2980429831
}
2980529832
return c.emitResolver
2980629833
}

internal/checker/checker_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func TestCheckSrcCompiler(t *testing.T) {
6868
ConfigFileName: tspath.CombinePaths(rootPath, "tsconfig.json"),
6969
}
7070
p := compiler.NewProgram(opts)
71-
p.CheckSourceFiles()
71+
p.CheckSourceFiles(t.Context())
7272
}
7373

7474
func BenchmarkNewChecker(b *testing.B) {

internal/checker/utilities.go

+10
Original file line numberDiff line numberDiff line change
@@ -2063,3 +2063,13 @@ func IsExternalModuleSymbol(moduleSymbol *ast.Symbol) bool {
20632063
firstRune, _ := utf8.DecodeRuneInString(moduleSymbol.Name)
20642064
return moduleSymbol.Flags&ast.SymbolFlagsModule != 0 && firstRune == '"'
20652065
}
2066+
2067+
func (c *Checker) isCanceled() bool {
2068+
return c.ctx != nil && c.ctx.Err() != nil
2069+
}
2070+
2071+
func (c *Checker) checkNotCanceled() {
2072+
if c.wasCanceled {
2073+
panic("Checker was previously cancelled")
2074+
}
2075+
}

internal/compiler/program.go

+23-18
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package compiler
22

33
import (
4+
"context"
45
"fmt"
56
"slices"
67
"sync"
@@ -206,13 +207,13 @@ func (p *Program) BindSourceFiles() {
206207
wg.RunAndWait()
207208
}
208209

209-
func (p *Program) CheckSourceFiles() {
210+
func (p *Program) CheckSourceFiles(ctx context.Context) {
210211
p.createCheckers()
211212
wg := core.NewWorkGroup(p.programOptions.SingleThreaded)
212213
for index, checker := range p.checkers {
213214
wg.Queue(func() {
214215
for i := index; i < len(p.files); i += len(p.checkers) {
215-
checker.CheckSourceFile(p.files[i])
216+
checker.CheckSourceFile(ctx, p.files[i])
216217
}
217218
})
218219
}
@@ -277,16 +278,16 @@ func (p *Program) findSourceFile(candidate string, reason FileIncludeReason) *as
277278
return p.filesByPath[path]
278279
}
279280

280-
func (p *Program) GetSyntacticDiagnostics(sourceFile *ast.SourceFile) []*ast.Diagnostic {
281-
return p.getDiagnosticsHelper(sourceFile, false /*ensureBound*/, false /*ensureChecked*/, p.getSyntacticDiagnosticsForFile)
281+
func (p *Program) GetSyntacticDiagnostics(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic {
282+
return p.getDiagnosticsHelper(ctx, sourceFile, false /*ensureBound*/, false /*ensureChecked*/, p.getSyntacticDiagnosticsForFile)
282283
}
283284

284-
func (p *Program) GetBindDiagnostics(sourceFile *ast.SourceFile) []*ast.Diagnostic {
285-
return p.getDiagnosticsHelper(sourceFile, true /*ensureBound*/, false /*ensureChecked*/, p.getBindDiagnosticsForFile)
285+
func (p *Program) GetBindDiagnostics(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic {
286+
return p.getDiagnosticsHelper(ctx, sourceFile, true /*ensureBound*/, false /*ensureChecked*/, p.getBindDiagnosticsForFile)
286287
}
287288

288-
func (p *Program) GetSemanticDiagnostics(sourceFile *ast.SourceFile) []*ast.Diagnostic {
289-
return p.getDiagnosticsHelper(sourceFile, true /*ensureBound*/, true /*ensureChecked*/, p.getSemanticDiagnosticsForFile)
289+
func (p *Program) GetSemanticDiagnostics(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic {
290+
return p.getDiagnosticsHelper(ctx, sourceFile, true /*ensureBound*/, true /*ensureChecked*/, p.getSemanticDiagnosticsForFile)
290291
}
291292

292293
func (p *Program) GetGlobalDiagnostics() []*ast.Diagnostic {
@@ -310,11 +311,11 @@ func (p *Program) getOptionsDiagnosticsOfConfigFile() []*ast.Diagnostic {
310311
return p.configFileParsingDiagnostics // TODO: actually call getDiagnosticsHelper on config path
311312
}
312313

313-
func (p *Program) getSyntacticDiagnosticsForFile(sourceFile *ast.SourceFile) []*ast.Diagnostic {
314+
func (p *Program) getSyntacticDiagnosticsForFile(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic {
314315
return sourceFile.Diagnostics()
315316
}
316317

317-
func (p *Program) getBindDiagnosticsForFile(sourceFile *ast.SourceFile) []*ast.Diagnostic {
318+
func (p *Program) getBindDiagnosticsForFile(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic {
318319
// TODO: restore this; tsgo's main depends on this function binding all files for timing.
319320
// if checker.SkipTypeChecking(sourceFile, p.compilerOptions) {
320321
// return nil
@@ -323,26 +324,27 @@ func (p *Program) getBindDiagnosticsForFile(sourceFile *ast.SourceFile) []*ast.D
323324
return sourceFile.BindDiagnostics()
324325
}
325326

326-
func (p *Program) getSemanticDiagnosticsForFile(sourceFile *ast.SourceFile) []*ast.Diagnostic {
327+
func (p *Program) getSemanticDiagnosticsForFile(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic {
327328
if checker.SkipTypeChecking(sourceFile, p.compilerOptions) {
328329
return nil
329330
}
330-
331331
var fileChecker *checker.Checker
332332
if sourceFile != nil {
333333
fileChecker = p.GetTypeCheckerForFile(sourceFile)
334334
}
335-
336335
diags := slices.Clip(sourceFile.BindDiagnostics())
337336
// Ask for diags from all checkers; checking one file may add diagnostics to other files.
338337
// These are deduplicated later.
339338
for _, checker := range p.checkers {
340339
if sourceFile == nil || checker == fileChecker {
341-
diags = append(diags, checker.GetDiagnostics(sourceFile)...)
340+
diags = append(diags, checker.GetDiagnostics(ctx, sourceFile)...)
342341
} else {
343342
diags = append(diags, checker.GetDiagnosticsWithoutCheck(sourceFile)...)
344343
}
345344
}
345+
if ctx.Err() != nil {
346+
return nil
347+
}
346348
if len(sourceFile.CommentDirectives) == 0 {
347349
return diags
348350
}
@@ -432,22 +434,25 @@ func compactAndMergeRelatedInfos(diagnostics []*ast.Diagnostic) []*ast.Diagnosti
432434
return diagnostics[:j]
433435
}
434436

435-
func (p *Program) getDiagnosticsHelper(sourceFile *ast.SourceFile, ensureBound bool, ensureChecked bool, getDiagnostics func(*ast.SourceFile) []*ast.Diagnostic) []*ast.Diagnostic {
437+
func (p *Program) getDiagnosticsHelper(ctx context.Context, sourceFile *ast.SourceFile, ensureBound bool, ensureChecked bool, getDiagnostics func(context.Context, *ast.SourceFile) []*ast.Diagnostic) []*ast.Diagnostic {
436438
if sourceFile != nil {
437439
if ensureBound {
438440
binder.BindSourceFile(sourceFile, p.getSourceAffectingCompilerOptions())
439441
}
440-
return SortAndDeduplicateDiagnostics(getDiagnostics(sourceFile))
442+
return SortAndDeduplicateDiagnostics(getDiagnostics(ctx, sourceFile))
441443
}
442444
if ensureBound {
443445
p.BindSourceFiles()
444446
}
445447
if ensureChecked {
446-
p.CheckSourceFiles()
448+
p.CheckSourceFiles(ctx)
449+
if ctx.Err() != nil {
450+
return nil
451+
}
447452
}
448453
var result []*ast.Diagnostic
449454
for _, file := range p.files {
450-
result = append(result, getDiagnostics(file)...)
455+
result = append(result, getDiagnostics(ctx, file)...)
451456
}
452457
return SortAndDeduplicateDiagnostics(result)
453458
}

internal/execute/tsc.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package execute
22

33
import (
4+
"context"
45
"fmt"
56

67
"github.com/microsoft/typescript-go/internal/ast"
@@ -205,7 +206,7 @@ func compileAndEmit(sys System, program *compiler.Program, reportDiagnostic diag
205206
allDiagnostics := program.GetConfigFileParsingDiagnostics()
206207

207208
// todo: early exit logic and append diagnostics
208-
diagnostics := program.GetSyntacticDiagnostics(nil)
209+
diagnostics := program.GetSyntacticDiagnostics(context.Background(), nil)
209210
if len(diagnostics) == 0 {
210211
diagnostics = append(diagnostics, program.GetOptionsDiagnostics()...)
211212
if options.ListFilesOnly.IsFalse() {
@@ -214,7 +215,7 @@ func compileAndEmit(sys System, program *compiler.Program, reportDiagnostic diag
214215
}
215216
}
216217
if len(diagnostics) == 0 {
217-
diagnostics = append(diagnostics, program.GetSemanticDiagnostics(nil)...)
218+
diagnostics = append(diagnostics, program.GetSemanticDiagnostics(context.Background(), nil)...)
218219
}
219220
// TODO: declaration diagnostics
220221
if len(diagnostics) == 0 && options.NoEmit == core.TSTrue && (options.Declaration.IsTrue() && options.Composite.IsTrue()) {

0 commit comments

Comments
 (0)