Skip to content

Commit c86b860

Browse files
authored
Make Program diagnostic API clearer (#2197)
1 parent 25d94f6 commit c86b860

File tree

7 files changed

+73
-162
lines changed

7 files changed

+73
-162
lines changed

internal/ast/diagnostic.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,11 +195,17 @@ func getDiagnosticPath(d *Diagnostic) string {
195195
}
196196

197197
func EqualDiagnostics(d1, d2 *Diagnostic) bool {
198+
if d1 == d2 {
199+
return true
200+
}
198201
return EqualDiagnosticsNoRelatedInfo(d1, d2) &&
199202
slices.EqualFunc(d1.RelatedInformation(), d2.RelatedInformation(), EqualDiagnostics)
200203
}
201204

202205
func EqualDiagnosticsNoRelatedInfo(d1, d2 *Diagnostic) bool {
206+
if d1 == d2 {
207+
return true
208+
}
203209
return getDiagnosticPath(d1) == getDiagnosticPath(d2) &&
204210
d1.Loc() == d2.Loc() &&
205211
d1.Code() == d2.Code() &&
@@ -208,6 +214,9 @@ func EqualDiagnosticsNoRelatedInfo(d1, d2 *Diagnostic) bool {
208214
}
209215

210216
func equalMessageChain(c1, c2 *Diagnostic) bool {
217+
if c1 == c2 {
218+
return true
219+
}
211220
return c1.Code() == c2.Code() &&
212221
slices.Equal(c1.MessageArgs(), c2.MessageArgs()) &&
213222
slices.EqualFunc(c1.MessageChain(), c2.MessageChain(), equalMessageChain)
@@ -258,6 +267,9 @@ func compareRelatedInfo(r1, r2 []*Diagnostic) int {
258267
}
259268

260269
func CompareDiagnostics(d1, d2 *Diagnostic) int {
270+
if d1 == d2 {
271+
return 0
272+
}
261273
c := strings.Compare(getDiagnosticPath(d1), getDiagnosticPath(d2))
262274
if c != 0 {
263275
return c

internal/checker/checker_test.go

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -61,25 +61,6 @@ foo.bar;`
6161
}
6262
}
6363

64-
func TestCheckSrcCompiler(t *testing.T) {
65-
t.Parallel()
66-
67-
repo.SkipIfNoTypeScriptSubmodule(t)
68-
fs := osvfs.FS()
69-
fs = bundled.WrapFS(fs)
70-
71-
rootPath := tspath.CombinePaths(tspath.NormalizeSlashes(repo.TypeScriptSubmodulePath), "src", "compiler")
72-
73-
host := compiler.NewCompilerHost(rootPath, fs, bundled.LibPath(), nil, nil)
74-
parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile(tspath.CombinePaths(rootPath, "tsconfig.json"), &core.CompilerOptions{}, nil, host, nil)
75-
assert.Equal(t, len(errors), 0, "Expected no errors in parsed command line")
76-
p := compiler.NewProgram(compiler.ProgramOptions{
77-
Config: parsed,
78-
Host: host,
79-
})
80-
p.CheckSourceFiles(t.Context(), nil)
81-
}
82-
8364
func BenchmarkNewChecker(b *testing.B) {
8465
repo.SkipIfNoTypeScriptSubmodule(b)
8566
fs := osvfs.FS()

internal/compiler/checkerpool.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ type CheckerPool interface {
1616
GetChecker(ctx context.Context) (*checker.Checker, func())
1717
GetCheckerForFile(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func())
1818
GetCheckerForFileExclusive(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func())
19-
ForEachCheckerParallel(ctx context.Context, cb func(idx int, c *checker.Checker))
2019
Files(checker *checker.Checker) iter.Seq[*ast.SourceFile]
2120
}
2221

@@ -98,7 +97,7 @@ func (p *checkerPool) createCheckers() {
9897

9998
// Runs `cb` for each checker in the pool concurrently, locking and unlocking checker mutexes as it goes,
10099
// making it safe to call `ForEachCheckerParallel` from many threads simultaneously.
101-
func (p *checkerPool) ForEachCheckerParallel(ctx context.Context, cb func(idx int, c *checker.Checker)) {
100+
func (p *checkerPool) ForEachCheckerParallel(cb func(idx int, c *checker.Checker)) {
102101
p.createCheckers()
103102
wg := core.NewWorkGroup(p.program.SingleThreaded())
104103
for idx, checker := range p.checkers {

internal/compiler/program.go

Lines changed: 58 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -363,23 +363,15 @@ func (p *Program) BindSourceFiles() {
363363
wg.RunAndWait()
364364
}
365365

366-
func (p *Program) CheckSourceFiles(ctx context.Context, files []*ast.SourceFile) {
367-
p.checkerPool.ForEachCheckerParallel(ctx, func(_ int, checker *checker.Checker) {
368-
for file := range p.checkerPool.Files(checker) {
369-
if files == nil || slices.Contains(files, file) {
370-
checker.CheckSourceFile(ctx, file)
371-
}
372-
}
373-
})
374-
}
375-
376366
// Return the type checker associated with the program.
377367
func (p *Program) GetTypeChecker(ctx context.Context) (*checker.Checker, func()) {
378368
return p.checkerPool.GetChecker(ctx)
379369
}
380370

381-
func (p *Program) ForEachCheckerParallel(ctx context.Context, cb func(idx int, c *checker.Checker)) {
382-
p.checkerPool.ForEachCheckerParallel(ctx, cb)
371+
func (p *Program) ForEachCheckerParallel(cb func(idx int, c *checker.Checker)) {
372+
if pool, ok := p.checkerPool.(*checkerPool); ok {
373+
pool.ForEachCheckerParallel(cb)
374+
}
383375
}
384376

385377
// Return a checker for the given file. We may have multiple checkers in concurrent scenarios and this
@@ -417,39 +409,59 @@ func (p *Program) GetResolvedModules() map[tspath.Path]module.ModeAwareCache[*mo
417409
return p.resolvedModules
418410
}
419411

412+
// collectDiagnostics collects diagnostics from a single file or all files.
413+
// If sourceFile is non-nil, returns diagnostics for just that file.
414+
// If sourceFile is nil, returns diagnostics for all files in the program.
415+
func (p *Program) collectDiagnostics(ctx context.Context, sourceFile *ast.SourceFile, collect func(context.Context, *ast.SourceFile) []*ast.Diagnostic) []*ast.Diagnostic {
416+
var result []*ast.Diagnostic
417+
if sourceFile != nil {
418+
result = collect(ctx, sourceFile)
419+
} else {
420+
for _, file := range p.files {
421+
result = append(result, collect(ctx, file)...)
422+
}
423+
}
424+
return SortAndDeduplicateDiagnostics(result)
425+
}
426+
420427
func (p *Program) GetSyntacticDiagnostics(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic {
421-
return p.getDiagnosticsHelper(ctx, sourceFile, false /*ensureBound*/, false /*ensureChecked*/, p.getSyntacticDiagnosticsForFile)
428+
return p.collectDiagnostics(ctx, sourceFile, func(_ context.Context, file *ast.SourceFile) []*ast.Diagnostic {
429+
return core.Concatenate(file.Diagnostics(), file.JSDiagnostics())
430+
})
422431
}
423432

424433
func (p *Program) GetBindDiagnostics(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic {
425-
return p.getDiagnosticsHelper(ctx, sourceFile, true /*ensureBound*/, false /*ensureChecked*/, p.getBindDiagnosticsForFile)
434+
if sourceFile != nil {
435+
binder.BindSourceFile(sourceFile)
436+
} else {
437+
p.BindSourceFiles()
438+
}
439+
return p.collectDiagnostics(ctx, sourceFile, func(_ context.Context, file *ast.SourceFile) []*ast.Diagnostic {
440+
return file.BindDiagnostics()
441+
})
426442
}
427443

428444
func (p *Program) GetSemanticDiagnostics(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic {
429-
return p.getDiagnosticsHelper(ctx, sourceFile, true /*ensureBound*/, true /*ensureChecked*/, p.getSemanticDiagnosticsForFile)
445+
return p.collectDiagnostics(ctx, sourceFile, p.getSemanticDiagnosticsForFile)
430446
}
431447

432-
func (p *Program) GetSemanticDiagnosticsNoFilter(ctx context.Context, sourceFiles []*ast.SourceFile) map[*ast.SourceFile][]*ast.Diagnostic {
433-
p.BindSourceFiles()
434-
p.CheckSourceFiles(ctx, sourceFiles)
435-
if ctx.Err() != nil {
436-
return nil
437-
}
448+
func (p *Program) GetSemanticDiagnosticsWithoutNoEmitFiltering(ctx context.Context, sourceFiles []*ast.SourceFile) map[*ast.SourceFile][]*ast.Diagnostic {
438449
result := make(map[*ast.SourceFile][]*ast.Diagnostic, len(sourceFiles))
439450
for _, file := range sourceFiles {
440-
result[file] = SortAndDeduplicateDiagnostics(p.getSemanticDiagnosticsForFileNotFilter(ctx, file))
451+
result[file] = SortAndDeduplicateDiagnostics(p.getBindAndCheckDiagnosticsForFile(ctx, file))
441452
}
442453
return result
443454
}
444455

445456
func (p *Program) GetSuggestionDiagnostics(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic {
446-
return p.getDiagnosticsHelper(ctx, sourceFile, true /*ensureBound*/, true /*ensureChecked*/, p.getSuggestionDiagnosticsForFile)
457+
return p.collectDiagnostics(ctx, sourceFile, p.getSuggestionDiagnosticsForFile)
447458
}
448459

449460
func (p *Program) GetProgramDiagnostics() []*ast.Diagnostic {
450-
return SortAndDeduplicateDiagnostics(slices.Concat(
461+
return SortAndDeduplicateDiagnostics(core.Concatenate(
451462
p.programDiagnostics,
452-
p.includeProcessor.getDiagnostics(p).GetGlobalDiagnostics()))
463+
p.includeProcessor.getDiagnostics(p).GetGlobalDiagnostics(),
464+
))
453465
}
454466

455467
func (p *Program) GetIncludeProcessorDiagnostics(sourceFile *ast.SourceFile) []*ast.Diagnostic {
@@ -978,40 +990,26 @@ func (p *Program) GetGlobalDiagnostics(ctx context.Context) []*ast.Diagnostic {
978990
}
979991

980992
globalDiagnostics := make([][]*ast.Diagnostic, p.checkerPool.Count())
981-
p.checkerPool.ForEachCheckerParallel(ctx, func(idx int, checker *checker.Checker) {
993+
p.ForEachCheckerParallel(func(idx int, checker *checker.Checker) {
982994
globalDiagnostics[idx] = checker.GetGlobalDiagnostics()
983995
})
984996

985997
return SortAndDeduplicateDiagnostics(slices.Concat(globalDiagnostics...))
986998
}
987999

9881000
func (p *Program) GetDeclarationDiagnostics(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic {
989-
return p.getDiagnosticsHelper(ctx, sourceFile, true /*ensureBound*/, true /*ensureChecked*/, p.getDeclarationDiagnosticsForFile)
1001+
return p.collectDiagnostics(ctx, sourceFile, p.getDeclarationDiagnosticsForFile)
9901002
}
9911003

9921004
func (p *Program) GetOptionsDiagnostics(ctx context.Context) []*ast.Diagnostic {
993-
return SortAndDeduplicateDiagnostics(append(p.GetGlobalDiagnostics(ctx), p.getOptionsDiagnosticsOfConfigFile()...))
1005+
return SortAndDeduplicateDiagnostics(core.Concatenate(p.GetGlobalDiagnostics(ctx), p.getOptionsDiagnosticsOfConfigFile()))
9941006
}
9951007

9961008
func (p *Program) getOptionsDiagnosticsOfConfigFile() []*ast.Diagnostic {
997-
// todo update p.configParsingDiagnostics when updateAndGetProgramDiagnostics is implemented
9981009
if p.Options() == nil || p.Options().ConfigFilePath == "" {
9991010
return nil
10001011
}
1001-
return p.GetConfigFileParsingDiagnostics() // TODO: actually call getDiagnosticsHelper on config path
1002-
}
1003-
1004-
func (p *Program) getSyntacticDiagnosticsForFile(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic {
1005-
return core.Concatenate(sourceFile.Diagnostics(), sourceFile.JSDiagnostics())
1006-
}
1007-
1008-
func (p *Program) getBindDiagnosticsForFile(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic {
1009-
// TODO: restore this; tsgo's main depends on this function binding all files for timing.
1010-
// if checker.SkipTypeChecking(sourceFile, p.compilerOptions) {
1011-
// return nil
1012-
// }
1013-
1014-
return sourceFile.BindDiagnostics()
1012+
return p.GetConfigFileParsingDiagnostics()
10151013
}
10161014

10171015
func FilterNoEmitSemanticDiagnostics(diagnostics []*ast.Diagnostic, options *core.CompilerOptions) []*ast.Diagnostic {
@@ -1024,40 +1022,26 @@ func FilterNoEmitSemanticDiagnostics(diagnostics []*ast.Diagnostic, options *cor
10241022
}
10251023

10261024
func (p *Program) getSemanticDiagnosticsForFile(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic {
1027-
return slices.Concat(
1028-
FilterNoEmitSemanticDiagnostics(p.getSemanticDiagnosticsForFileNotFilter(ctx, sourceFile), p.Options()),
1025+
return core.Concatenate(
1026+
FilterNoEmitSemanticDiagnostics(p.getBindAndCheckDiagnosticsForFile(ctx, sourceFile), p.Options()),
10291027
p.GetIncludeProcessorDiagnostics(sourceFile),
10301028
)
10311029
}
10321030

1033-
func (p *Program) getSemanticDiagnosticsForFileNotFilter(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic {
1031+
// getBindAndCheckDiagnosticsForFile gets semantic diagnostics for a single file,
1032+
// including bind diagnostics, checker diagnostics, and handling of @ts-ignore/@ts-expect-error directives.
1033+
func (p *Program) getBindAndCheckDiagnosticsForFile(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic {
10341034
compilerOptions := p.Options()
10351035
if checker.SkipTypeChecking(sourceFile, compilerOptions, p, false) {
10361036
return nil
10371037
}
10381038

1039-
var fileChecker *checker.Checker
1040-
var done func()
1041-
if sourceFile != nil {
1042-
fileChecker, done = p.checkerPool.GetCheckerForFile(ctx, sourceFile)
1043-
defer done()
1044-
}
1045-
diags := slices.Clip(sourceFile.BindDiagnostics())
1046-
// Ask for diags from all checkers; checking one file may add diagnostics to other files.
1047-
// These are deduplicated later.
1048-
checkerDiags := make([][]*ast.Diagnostic, p.checkerPool.Count())
1049-
p.checkerPool.ForEachCheckerParallel(ctx, func(idx int, checker *checker.Checker) {
1050-
if sourceFile == nil || checker == fileChecker {
1051-
checkerDiags[idx] = checker.GetDiagnostics(ctx, sourceFile)
1052-
}
1053-
})
1054-
if ctx.Err() != nil {
1055-
return nil
1056-
}
1057-
1058-
diags = append(diags, slices.Concat(checkerDiags...)...)
1039+
fileChecker, done := p.checkerPool.GetCheckerForFile(ctx, sourceFile)
1040+
defer done()
10591041

1060-
// !!! This should be rewritten to work like getBindAndCheckDiagnosticsForFileNoCache.
1042+
// Getting a checker will force a bind, so this will be populated.
1043+
diags := slices.Clip(sourceFile.BindDiagnostics())
1044+
diags = append(diags, fileChecker.GetDiagnostics(ctx, sourceFile)...)
10611045

10621046
isPlainJS := ast.IsPlainJSFile(sourceFile, compilerOptions.CheckJs)
10631047
if isPlainJS {
@@ -1139,28 +1123,12 @@ func (p *Program) getSuggestionDiagnosticsForFile(ctx context.Context, sourceFil
11391123
return nil
11401124
}
11411125

1142-
var fileChecker *checker.Checker
1143-
var done func()
1144-
if sourceFile != nil {
1145-
fileChecker, done = p.checkerPool.GetCheckerForFile(ctx, sourceFile)
1146-
defer done()
1147-
}
1126+
fileChecker, done := p.checkerPool.GetCheckerForFile(ctx, sourceFile)
1127+
defer done()
11481128

1129+
// Getting a checker will force a bind, so this will be populated.
11491130
diags := slices.Clip(sourceFile.BindSuggestionDiagnostics)
1150-
1151-
checkerDiags := make([][]*ast.Diagnostic, p.checkerPool.Count())
1152-
p.checkerPool.ForEachCheckerParallel(ctx, func(idx int, checker *checker.Checker) {
1153-
if sourceFile == nil || checker == fileChecker {
1154-
checkerDiags[idx] = checker.GetSuggestionDiagnostics(ctx, sourceFile)
1155-
} else {
1156-
// !!! is there any case where suggestion diagnostics are produced in other checkers?
1157-
}
1158-
})
1159-
if ctx.Err() != nil {
1160-
return nil
1161-
}
1162-
1163-
diags = append(diags, slices.Concat(checkerDiags...)...)
1131+
diags = append(diags, fileChecker.GetSuggestionDiagnostics(ctx, sourceFile)...)
11641132

11651133
return diags
11661134
}
@@ -1213,29 +1181,6 @@ func compactAndMergeRelatedInfos(diagnostics []*ast.Diagnostic) []*ast.Diagnosti
12131181
return diagnostics[:j]
12141182
}
12151183

1216-
func (p *Program) getDiagnosticsHelper(ctx context.Context, sourceFile *ast.SourceFile, ensureBound bool, ensureChecked bool, getDiagnostics func(context.Context, *ast.SourceFile) []*ast.Diagnostic) []*ast.Diagnostic {
1217-
if sourceFile != nil {
1218-
if ensureBound {
1219-
binder.BindSourceFile(sourceFile)
1220-
}
1221-
return SortAndDeduplicateDiagnostics(getDiagnostics(ctx, sourceFile))
1222-
}
1223-
if ensureBound {
1224-
p.BindSourceFiles()
1225-
}
1226-
if ensureChecked {
1227-
p.CheckSourceFiles(ctx, nil)
1228-
if ctx.Err() != nil {
1229-
return nil
1230-
}
1231-
}
1232-
var result []*ast.Diagnostic
1233-
for _, file := range p.files {
1234-
result = append(result, getDiagnostics(ctx, file)...)
1235-
}
1236-
return SortAndDeduplicateDiagnostics(result)
1237-
}
1238-
12391184
func (p *Program) LineCount() int {
12401185
var count int
12411186
for _, file := range p.files {
@@ -1259,23 +1204,23 @@ func (p *Program) SymbolCount() int {
12591204
}
12601205
var val atomic.Uint32
12611206
val.Store(uint32(count))
1262-
p.checkerPool.ForEachCheckerParallel(context.Background(), func(idx int, c *checker.Checker) {
1207+
p.ForEachCheckerParallel(func(_ int, c *checker.Checker) {
12631208
val.Add(c.SymbolCount)
12641209
})
12651210
return int(val.Load())
12661211
}
12671212

12681213
func (p *Program) TypeCount() int {
12691214
var val atomic.Uint32
1270-
p.checkerPool.ForEachCheckerParallel(context.Background(), func(idx int, c *checker.Checker) {
1215+
p.ForEachCheckerParallel(func(_ int, c *checker.Checker) {
12711216
val.Add(c.TypeCount)
12721217
})
12731218
return int(val.Load())
12741219
}
12751220

12761221
func (p *Program) InstantiationCount() int {
12771222
var val atomic.Uint32
1278-
p.checkerPool.ForEachCheckerParallel(context.Background(), func(idx int, c *checker.Checker) {
1223+
p.ForEachCheckerParallel(func(_ int, c *checker.Checker) {
12791224
val.Add(c.TotalInstantiationCount)
12801225
})
12811226
return int(val.Load())
@@ -1370,8 +1315,6 @@ type SourceMapEmitResult struct {
13701315
}
13711316

13721317
func (p *Program) Emit(ctx context.Context, options EmitOptions) *EmitResult {
1373-
// !!! performance measurement
1374-
p.BindSourceFiles()
13751318
if options.EmitOnly != EmitOnlyForcedDts {
13761319
result := HandleNoEmitOnError(
13771320
ctx,

internal/execute/incremental/program.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ func (p *Program) collectSemanticDiagnosticsOfAffectedFiles(ctx context.Context,
262262
}
263263

264264
// Get their diagnostics and cache them
265-
diagnosticsPerFile := p.program.GetSemanticDiagnosticsNoFilter(ctx, affectedFiles)
265+
diagnosticsPerFile := p.program.GetSemanticDiagnosticsWithoutNoEmitFiltering(ctx, affectedFiles)
266266
// commit changes if no err
267267
if ctx.Err() != nil {
268268
return

0 commit comments

Comments
 (0)