Skip to content

Commit 0a27bad

Browse files
authored
Fix incremental diagnostics after global file deletion (#64845)
1 parent abcbb2e commit 0a27bad

17 files changed

Lines changed: 7002 additions & 21 deletions

‎tsc/internal/execute/incremental/buildinfo_path_test.go‎

Lines changed: 412 additions & 0 deletions
Large diffs are not rendered by default.

‎tsc/internal/execute/incremental/programtosnapshot.go‎

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,24 @@ func (t *toProgramSnapshot) reuseFromOldProgram() {
5151
if t.snapshot.options.Composite.IsTrue() {
5252
t.snapshot.latestChangedDtsFile = t.oldProgram.snapshot.latestChangedDtsFile
5353
}
54+
t.snapshot.buildInfoEmitPending.Store(t.oldProgram.snapshot.buildInfoEmitPending.Load())
5455
// Copy old snapshot's changed files set
5556
t.oldProgram.snapshot.changedFilesSet.Range(func(key tspath.PathKey) bool {
56-
t.snapshot.changedFilesSet.Add(key)
57+
if t.program.GetSourceFileByPath(key) != nil {
58+
t.snapshot.changedFilesSet.Add(key)
59+
} else {
60+
t.snapshot.buildInfoEmitPending.Store(true)
61+
}
5762
return true
5863
})
5964
t.oldProgram.snapshot.affectedFilesPendingEmit.Range(func(key tspath.PathKey, emitKind FileEmitKind) bool {
60-
t.snapshot.affectedFilesPendingEmit.Store(key, emitKind)
65+
if t.program.GetSourceFileByPath(key) != nil {
66+
t.snapshot.affectedFilesPendingEmit.Store(key, emitKind)
67+
} else {
68+
t.snapshot.buildInfoEmitPending.Store(true)
69+
}
6170
return true
6271
})
63-
t.snapshot.buildInfoEmitPending.Store(t.oldProgram.snapshot.buildInfoEmitPending.Load())
6472
t.snapshot.hasErrorsFromOldState = t.oldProgram.snapshot.hasErrors
6573
t.snapshot.hasSemanticErrorsFromOldState = t.oldProgram.snapshot.hasSemanticErrors
6674
t.snapshot.packageJsonsFromOldState = t.oldProgram.snapshot.packageJsons
@@ -161,21 +169,41 @@ func (t *toProgramSnapshot) computeProgramFileChanges() {
161169

162170
func (t *toProgramSnapshot) handleFileDelete() {
163171
if t.oldProgram != nil {
164-
// If the global file is removed, add all files as changed
165-
t.oldProgram.snapshot.fileInfos.Range(func(filePath tspath.PathKey, oldInfo *FileInfo) bool {
166-
if _, ok := t.snapshot.fileInfos.Load(filePath); !ok {
167-
if oldInfo.affectsGlobalScope {
168-
for _, file := range t.snapshot.getAllFilesExcludingDefaultLibraryFile(t.program, nil) {
169-
t.snapshot.addFileToChangeSet(file.PathKey())
170-
}
171-
t.globalFileRemoved = true
172-
} else {
173-
t.snapshot.buildInfoEmitPending.Store(true)
174-
}
175-
return false
176-
}
177-
return true
178-
})
172+
forEachDeletedFile(t.oldProgram.snapshot.fileInfos.Range, &t.snapshot.fileInfos, t.handleDeletedFile)
173+
if t.globalFileRemoved {
174+
t.invalidateGlobalScope()
175+
}
176+
}
177+
}
178+
179+
func forEachDeletedFile(
180+
rangeOldFileInfos func(func(tspath.PathKey, *FileInfo) bool),
181+
newFileInfos *collections.SyncMap[tspath.PathKey, *FileInfo],
182+
onDeletedFile func(tspath.PathKey, *FileInfo),
183+
) {
184+
rangeOldFileInfos(func(filePath tspath.PathKey, oldInfo *FileInfo) bool {
185+
if _, retained := newFileInfos.Load(filePath); !retained {
186+
onDeletedFile(filePath, oldInfo)
187+
}
188+
return true
189+
})
190+
}
191+
192+
func (t *toProgramSnapshot) handleDeletedFile(filePath tspath.PathKey, oldInfo *FileInfo) {
193+
t.snapshot.buildInfoEmitPending.Store(true)
194+
t.snapshot.changedFilesSet.Delete(filePath)
195+
t.snapshot.affectedFilesPendingEmit.Delete(filePath)
196+
t.globalFileRemoved = t.globalFileRemoved || oldInfo.affectsGlobalScope
197+
}
198+
199+
func (t *toProgramSnapshot) invalidateGlobalScope() {
200+
t.snapshot.buildInfoEmitPending.Store(true)
201+
for _, file := range t.program.GetSourceFiles() {
202+
if !t.program.IsSourceFileDefaultLibrary(file.PathKey()) {
203+
t.snapshot.addFileToChangeSet(file.PathKey())
204+
} else if !t.program.SkipTypeChecking(file, true) {
205+
t.snapshot.semanticDiagnosticsPerFile.Delete(file.PathKey())
206+
}
179207
}
180208
}
181209

@@ -195,9 +223,7 @@ func (t *toProgramSnapshot) handleGlobalScopeChange() {
195223
return true
196224
})
197225
if globalScopeLost {
198-
for _, file := range t.snapshot.getAllFilesExcludingDefaultLibraryFile(t.program, nil) {
199-
t.snapshot.addFileToChangeSet(file.PathKey())
200-
}
226+
t.invalidateGlobalScope()
201227
}
202228
}
203229

‎tsc/internal/execute/tsctests/tsc_test.go‎

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2569,6 +2569,153 @@ func TestTscIncremental(t *testing.T) {
25692569
}
25702570
}
25712571

2572+
func TestTscIncrementalDeletion(t *testing.T) {
2573+
t.Parallel()
2574+
for _, mode := range []struct {
2575+
name string
2576+
args []string
2577+
}{
2578+
{},
2579+
{" with single threaded", []string{"--singleThreaded"}},
2580+
{" with one checker", []string{"--checkers", "1"}},
2581+
} {
2582+
config := func(options string) string {
2583+
return `{"compilerOptions":{"incremental":true,"noEmit":true,"types":[]` + options + `},"files":["a.ts"]}`
2584+
}
2585+
(&tscInput{
2586+
subScenario: "lib and target changes delete mixed library files" + mode.name,
2587+
commandLineArgs: mode.args,
2588+
files: FileMap{
2589+
"/home/src/workspaces/project/tsconfig.json": config(`,"target":"esnext"`),
2590+
"/home/src/workspaces/project/a.ts": `export const values = [3, 1, 2].toSorted();`,
2591+
getTestLibPathFor("es5"): tscDefaultLibContent,
2592+
getTestLibPathFor("esnext.array"): stringtestutil.Dedent(`
2593+
/// <reference no-default-lib="true"/>
2594+
interface Array<T> {
2595+
toSorted(): T[];
2596+
}
2597+
`),
2598+
getTestLibPathFor("decorators"): stringtestutil.Dedent(`
2599+
/// <reference no-default-lib="true"/>
2600+
export {};
2601+
`),
2602+
getTestLibPathFor("esnext.full"): stringtestutil.Dedent(`
2603+
/// <reference no-default-lib="true"/>
2604+
/// <reference lib="es5"/>
2605+
/// <reference lib="esnext.array"/>
2606+
/// <reference lib="decorators"/>
2607+
`),
2608+
getTestLibPathFor("es2020"): stringtestutil.Dedent(`
2609+
/// <reference no-default-lib="true"/>
2610+
/// <reference lib="es5"/>
2611+
`),
2612+
getTestLibPathFor("es2020.full"): stringtestutil.Dedent(`
2613+
/// <reference no-default-lib="true"/>
2614+
/// <reference lib="es5"/>
2615+
`),
2616+
},
2617+
edits: []*tscEdit{
2618+
{caption: "narrow lib", edit: func(sys *TestSys) {
2619+
sys.writeFileNoError("/home/src/workspaces/project/tsconfig.json", config(`,"target":"esnext","lib":["es2020"]`))
2620+
}},
2621+
noChange,
2622+
{caption: "restore lib", edit: func(sys *TestSys) {
2623+
sys.writeFileNoError("/home/src/workspaces/project/tsconfig.json", config(`,"target":"esnext"`))
2624+
}},
2625+
{caption: "narrow target", edit: func(sys *TestSys) {
2626+
sys.writeFileNoError("/home/src/workspaces/project/tsconfig.json", config(`,"target":"es2020"`))
2627+
}},
2628+
noChange,
2629+
{caption: "restore target", edit: func(sys *TestSys) {
2630+
sys.writeFileNoError("/home/src/workspaces/project/tsconfig.json", config(`,"target":"esnext"`))
2631+
}},
2632+
noChange,
2633+
},
2634+
}).run(t, "incremental")
2635+
(&tscInput{
2636+
subScenario: "mixed file deletion with add and change" + mode.name,
2637+
commandLineArgs: mode.args,
2638+
files: FileMap{
2639+
"/home/src/workspaces/project/tsconfig.json": `{"compilerOptions":{"incremental":true,"noEmit":true,"types":[]}}`,
2640+
"/home/src/workspaces/project/consumer.ts": `declare const value: RemovedGlobal; value.property;`,
2641+
"/home/src/workspaces/project/global.d.ts": `interface RemovedGlobal { property: string; }`,
2642+
"/home/src/workspaces/project/module.d.ts": `export interface ModuleOnly { property: string; }`,
2643+
"/home/src/workspaces/project/changed.ts": `export const changed = 1;`,
2644+
},
2645+
edits: []*tscEdit{
2646+
{caption: "delete global and non-global files while adding and changing sources", edit: func(sys *TestSys) {
2647+
sys.removeNoError("/home/src/workspaces/project/global.d.ts")
2648+
sys.removeNoError("/home/src/workspaces/project/module.d.ts")
2649+
sys.replaceFileText("/home/src/workspaces/project/changed.ts", "1", "2")
2650+
sys.writeFileNoError("/home/src/workspaces/project/added.ts", `export const added = 1;`)
2651+
}},
2652+
noChange,
2653+
{caption: "restore global file", edit: func(sys *TestSys) {
2654+
sys.writeFileNoError("/home/src/workspaces/project/global.d.ts", `interface RemovedGlobal { property: number; }`)
2655+
}},
2656+
{caption: "delete ordinary source file", edit: func(sys *TestSys) {
2657+
sys.removeNoError("/home/src/workspaces/project/added.ts")
2658+
}},
2659+
noChange,
2660+
},
2661+
}).run(t, "incremental")
2662+
for _, cleared := range []bool{false, true} {
2663+
library := tscDefaultLibContent + "\ninterface LibraryConsumer { property: RemovedGlobal; }"
2664+
global := "interface RemovedGlobal {}"
2665+
if cleared {
2666+
library = tscDefaultLibContent + "\ninterface Shared<T> {}"
2667+
global = "interface Shared<T, U> {}"
2668+
}
2669+
(&tscInput{
2670+
subScenario: fmt.Sprintf("retained library diagnostics after global deletion cleared %t%s", cleared, mode.name),
2671+
commandLineArgs: mode.args,
2672+
files: FileMap{
2673+
"/home/src/workspaces/project/tsconfig.json": `{"compilerOptions":{"incremental":true,"noEmit":true,"types":[],"lib":["es5"]}}`,
2674+
"/home/src/workspaces/project/consumer.ts": "export const unchanged = 1;",
2675+
"/home/src/workspaces/project/global.d.ts": global,
2676+
"/home/src/workspaces/project/module.d.ts": "export interface Unrelated {}",
2677+
getTestLibPathFor("es5"): library,
2678+
},
2679+
edits: []*tscEdit{
2680+
{caption: "delete global and module declarations", edit: func(sys *TestSys) {
2681+
sys.removeNoError("/home/src/workspaces/project/global.d.ts")
2682+
sys.removeNoError("/home/src/workspaces/project/module.d.ts")
2683+
}},
2684+
noChange,
2685+
{caption: "restore global declaration", edit: func(sys *TestSys) {
2686+
sys.writeFileNoError("/home/src/workspaces/project/global.d.ts", global)
2687+
}},
2688+
noChange,
2689+
},
2690+
}).run(t, "incremental")
2691+
}
2692+
}
2693+
for _, composite := range []bool{false, true} {
2694+
(&tscInput{
2695+
subScenario: fmt.Sprintf("unchecked deleted paths with no emit composite %t", composite),
2696+
files: FileMap{
2697+
"/home/src/workspaces/project/tsconfig.json": fmt.Sprintf(`{"compilerOptions":{"incremental":true,"noCheck":true,"noEmit":true,"composite":%t,"types":[]}}`, composite),
2698+
"/home/src/workspaces/project/stable.ts": "export const stable = 1;",
2699+
},
2700+
edits: []*tscEdit{
2701+
{caption: "add unchecked sources", edit: func(sys *TestSys) {
2702+
sys.writeFileNoError("/home/src/workspaces/project/first.ts", "export const first = 1;")
2703+
sys.writeFileNoError("/home/src/workspaces/project/second.ts", "export const second = 1;")
2704+
}},
2705+
{caption: "delete unchecked sources", edit: func(sys *TestSys) {
2706+
sys.removeNoError("/home/src/workspaces/project/first.ts")
2707+
sys.removeNoError("/home/src/workspaces/project/second.ts")
2708+
}},
2709+
noChange,
2710+
{caption: "enable checking and emit", edit: func(sys *TestSys) {
2711+
sys.replaceFileText("/home/src/workspaces/project/tsconfig.json", `"noCheck":true,"noEmit":true`, `"noCheck":false,"noEmit":false`)
2712+
}},
2713+
noChange,
2714+
},
2715+
}).run(t, "incremental")
2716+
}
2717+
}
2718+
25722719
func TestTscLibraryResolution(t *testing.T) {
25732720
t.Parallel()
25742721
getTscLibraryResolutionFileMap := func(libReplacement bool) FileMap {

0 commit comments

Comments
 (0)