From 255da2c797578353986d02e9dcb6a55ac1379dee Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 19 Nov 2025 06:55:45 +0000
Subject: [PATCH 1/8] Initial plan
From 768bfa5bbdefd09863d9d45a3ab92453c694cbe4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 19 Nov 2025 07:07:00 +0000
Subject: [PATCH 2/8] Add failing test for unreachable code detection bug
Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
---
.../fourslash/unreachableCodeAfterEdit.ts | 26 +++++++++++++++++++
1 file changed, 26 insertions(+)
create mode 100644 tests/cases/fourslash/unreachableCodeAfterEdit.ts
diff --git a/tests/cases/fourslash/unreachableCodeAfterEdit.ts b/tests/cases/fourslash/unreachableCodeAfterEdit.ts
new file mode 100644
index 0000000000000..4baea58fcdb67
--- /dev/null
+++ b/tests/cases/fourslash/unreachableCodeAfterEdit.ts
@@ -0,0 +1,26 @@
+///
+
+// @allowUnreachableCode: false
+
+//// /*before*/const x = 1;
+//// const y = 2;
+//// const z = 3;
+//// /*after*/
+
+// Initially, there should be no errors
+verify.numberOfErrorsInCurrentFile(0);
+
+// Add a throw statement at the beginning
+goTo.marker("before");
+edit.insert("throw new Error('foo');\n");
+
+// Now there should be unreachable code errors (the const declarations)
+verify.numberOfErrorsInCurrentFile(1);
+
+// Remove the throw statement
+goTo.marker("before");
+edit.deleteAtCaret("throw new Error('foo');\n".length);
+
+// After removing the throw, there should be no errors again
+// BUG: This will fail because reportedUnreachableNodes is not cleared properly
+verify.numberOfErrorsInCurrentFile(0);
From 0942dae4dca3c043807308ebb1f6d4e2a4df2488 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 19 Nov 2025 07:09:26 +0000
Subject: [PATCH 3/8] Update test to match issue scenario with imports,
interface, and class
Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
---
.../fourslash/unreachableCodeAfterEdit.ts | 36 +++++++++++++++----
1 file changed, 30 insertions(+), 6 deletions(-)
diff --git a/tests/cases/fourslash/unreachableCodeAfterEdit.ts b/tests/cases/fourslash/unreachableCodeAfterEdit.ts
index 4baea58fcdb67..534d4468e8d66 100644
--- a/tests/cases/fourslash/unreachableCodeAfterEdit.ts
+++ b/tests/cases/fourslash/unreachableCodeAfterEdit.ts
@@ -1,20 +1,44 @@
///
// @allowUnreachableCode: false
+// @lib: es2015
-//// /*before*/const x = 1;
-//// const y = 2;
-//// const z = 3;
-//// /*after*/
+// @Filename: /base/browser/browser.ts
+//// export const isStandalone = true;
+
+// @Filename: /base/browser/dom.ts
+//// export function addDisposableListener() {}
+
+// @Filename: /base/browser/window.ts
+//// export const mainWindow = {} as Window;
+
+// @Filename: /workbench.ts
+//// /*before*/import { isStandalone } from './base/browser/browser';
+//// import { addDisposableListener } from './base/browser/dom';
+//// import { mainWindow } from './base/browser/window';
+////
+//// interface ISecretStorageCrypto {
+//// seal(data: string): Promise;
+//// unseal(data: string): Promise;
+//// }
+////
+//// export class TransparentCrypto implements ISecretStorageCrypto {
+//// async seal(data: string): Promise {
+//// return data;
+//// }
+//// async unseal(data: string): Promise {
+//// return data;
+//// }
+//// }
// Initially, there should be no errors
verify.numberOfErrorsInCurrentFile(0);
-// Add a throw statement at the beginning
+// Add a throw statement ABOVE the imports
goTo.marker("before");
edit.insert("throw new Error('foo');\n");
-// Now there should be unreachable code errors (the const declarations)
+// Now there should be unreachable code errors
verify.numberOfErrorsInCurrentFile(1);
// Remove the throw statement
From 37c6b8f18460f5eb6057b8d494e2bcebab686d32 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 19 Nov 2025 07:23:36 +0000
Subject: [PATCH 4/8] Fix unreachable code detection bug by clearing
Unreachable flag in binder
Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
---
src/compiler/binder.ts | 7 ++++++-
src/compiler/checker.ts | 2 +-
2 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts
index cfa409e878c82..b3048aa3531c4 100644
--- a/src/compiler/binder.ts
+++ b/src/compiler/binder.ts
@@ -1024,7 +1024,7 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
hasExplicitReturn = false;
bindChildren(node);
// Reset all reachability check related flags on node (for incremental scenarios)
- node.flags &= ~NodeFlags.ReachabilityAndEmitFlags;
+ node.flags &= ~(NodeFlags.ReachabilityAndEmitFlags | NodeFlags.Unreachable);
if (!(currentFlow.flags & FlowFlags.Unreachable) && containerFlags & ContainerFlags.IsFunctionLike && nodeIsPresent((node as FunctionLikeDeclaration | ClassStaticBlockDeclaration).body)) {
node.flags |= NodeFlags.HasImplicitReturn;
if (hasExplicitReturn) node.flags |= NodeFlags.HasExplicitReturn;
@@ -1091,6 +1091,11 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
// and set it before we descend into nodes that could actually be part of an assignment pattern.
inAssignmentPattern = false;
+ // Clear Unreachable flag from previous binding (for incremental scenarios)
+ if (isPotentiallyExecutableNode(node)) {
+ (node as Mutable).flags &= ~NodeFlags.Unreachable;
+ }
+
if (currentFlow === unreachableFlow) {
if (canHaveFlowNode(node)) {
node.flowNode = undefined;
diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts
index ba5765c0826a3..a512c12287256 100644
--- a/src/compiler/checker.ts
+++ b/src/compiler/checker.ts
@@ -49472,11 +49472,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const beforeMark = nodesToCheck ? "beforeCheckNodes" : "beforeCheck";
const afterMark = nodesToCheck ? "afterCheckNodes" : "afterCheck";
performance.mark(beforeMark);
+ reportedUnreachableNodes = undefined;
nodesToCheck ? checkSourceFileNodesWorker(node, nodesToCheck) : checkSourceFileWorker(node);
performance.mark(afterMark);
performance.measure("Check", beforeMark, afterMark);
tracing?.pop();
- reportedUnreachableNodes = undefined;
}
function unusedIsError(kind: UnusedKind, isAmbient: boolean): boolean {
From e7303b411b0b7adf6aea33e1ac1a9bf9cbbeda89 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 19 Nov 2025 07:36:26 +0000
Subject: [PATCH 5/8] Simplify fix - only clear Unreachable flag in
bindChildren
Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
---
src/compiler/binder.ts | 2 +-
src/compiler/checker.ts | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts
index b3048aa3531c4..72b9e4acadedb 100644
--- a/src/compiler/binder.ts
+++ b/src/compiler/binder.ts
@@ -1024,7 +1024,7 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
hasExplicitReturn = false;
bindChildren(node);
// Reset all reachability check related flags on node (for incremental scenarios)
- node.flags &= ~(NodeFlags.ReachabilityAndEmitFlags | NodeFlags.Unreachable);
+ node.flags &= ~NodeFlags.ReachabilityAndEmitFlags;
if (!(currentFlow.flags & FlowFlags.Unreachable) && containerFlags & ContainerFlags.IsFunctionLike && nodeIsPresent((node as FunctionLikeDeclaration | ClassStaticBlockDeclaration).body)) {
node.flags |= NodeFlags.HasImplicitReturn;
if (hasExplicitReturn) node.flags |= NodeFlags.HasExplicitReturn;
diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts
index a512c12287256..ba5765c0826a3 100644
--- a/src/compiler/checker.ts
+++ b/src/compiler/checker.ts
@@ -49472,11 +49472,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const beforeMark = nodesToCheck ? "beforeCheckNodes" : "beforeCheck";
const afterMark = nodesToCheck ? "afterCheckNodes" : "afterCheck";
performance.mark(beforeMark);
- reportedUnreachableNodes = undefined;
nodesToCheck ? checkSourceFileNodesWorker(node, nodesToCheck) : checkSourceFileWorker(node);
performance.mark(afterMark);
performance.measure("Check", beforeMark, afterMark);
tracing?.pop();
+ reportedUnreachableNodes = undefined;
}
function unusedIsError(kind: UnusedKind, isAmbient: boolean): boolean {
From 940d8c2633d13928c7d045ad2f935911378670ba Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 19 Nov 2025 07:52:12 +0000
Subject: [PATCH 6/8] Remove unnecessary comments from test as requested
Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
---
tests/cases/fourslash/unreachableCodeAfterEdit.ts | 6 ------
1 file changed, 6 deletions(-)
diff --git a/tests/cases/fourslash/unreachableCodeAfterEdit.ts b/tests/cases/fourslash/unreachableCodeAfterEdit.ts
index 534d4468e8d66..af07c67463c98 100644
--- a/tests/cases/fourslash/unreachableCodeAfterEdit.ts
+++ b/tests/cases/fourslash/unreachableCodeAfterEdit.ts
@@ -31,20 +31,14 @@
//// }
//// }
-// Initially, there should be no errors
verify.numberOfErrorsInCurrentFile(0);
-// Add a throw statement ABOVE the imports
goTo.marker("before");
edit.insert("throw new Error('foo');\n");
-// Now there should be unreachable code errors
verify.numberOfErrorsInCurrentFile(1);
-// Remove the throw statement
goTo.marker("before");
edit.deleteAtCaret("throw new Error('foo');\n".length);
-// After removing the throw, there should be no errors again
-// BUG: This will fail because reportedUnreachableNodes is not cleared properly
verify.numberOfErrorsInCurrentFile(0);
From 424a0dd2aa84ae7c0894e6f6801f4f947f46ae50 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 19 Nov 2025 08:30:44 +0000
Subject: [PATCH 7/8] Clear Unreachable flag for labels in incremental
scenarios
Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
---
src/compiler/binder.ts | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts
index 72b9e4acadedb..1f409f69b3be4 100644
--- a/src/compiler/binder.ts
+++ b/src/compiler/binder.ts
@@ -1791,8 +1791,14 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
bind(node.label);
bind(node.statement);
if (!activeLabelList.referenced) {
+ // Clear Unreachable flag from previous binding (for incremental scenarios)
+ (node.label as Mutable).flags &= ~NodeFlags.Unreachable;
(node.label as Mutable).flags |= NodeFlags.Unreachable;
}
+ else {
+ // Clear Unreachable flag if label is now referenced (for incremental scenarios)
+ (node.label as Mutable).flags &= ~NodeFlags.Unreachable;
+ }
activeLabelList = activeLabelList.next;
addAntecedent(postStatementLabel, currentFlow);
currentFlow = finishFlowLabel(postStatementLabel);
From ff3e6fd0f3a09be14f0d4c8ab211f37a7e4666f5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 19 Nov 2025 08:38:52 +0000
Subject: [PATCH 8/8] Add test for unused label detection after edits
Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
---
src/compiler/binder.ts | 6 -----
tests/cases/fourslash/unusedLabelAfterEdit.ts | 23 +++++++++++++++++++
2 files changed, 23 insertions(+), 6 deletions(-)
create mode 100644 tests/cases/fourslash/unusedLabelAfterEdit.ts
diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts
index 1f409f69b3be4..72b9e4acadedb 100644
--- a/src/compiler/binder.ts
+++ b/src/compiler/binder.ts
@@ -1791,14 +1791,8 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
bind(node.label);
bind(node.statement);
if (!activeLabelList.referenced) {
- // Clear Unreachable flag from previous binding (for incremental scenarios)
- (node.label as Mutable).flags &= ~NodeFlags.Unreachable;
(node.label as Mutable).flags |= NodeFlags.Unreachable;
}
- else {
- // Clear Unreachable flag if label is now referenced (for incremental scenarios)
- (node.label as Mutable).flags &= ~NodeFlags.Unreachable;
- }
activeLabelList = activeLabelList.next;
addAntecedent(postStatementLabel, currentFlow);
currentFlow = finishFlowLabel(postStatementLabel);
diff --git a/tests/cases/fourslash/unusedLabelAfterEdit.ts b/tests/cases/fourslash/unusedLabelAfterEdit.ts
new file mode 100644
index 0000000000000..eb5fa6991fe7c
--- /dev/null
+++ b/tests/cases/fourslash/unusedLabelAfterEdit.ts
@@ -0,0 +1,23 @@
+///
+
+// @allowUnusedLabels: false
+
+//// myLabel: while (true) {
+//// if (Math.random() > 0.5) {
+//// /*marker*/break myLabel;
+//// }
+//// }
+
+verify.numberOfErrorsInCurrentFile(0);
+
+goTo.marker("marker");
+edit.deleteAtCaret("break myLabel;".length);
+edit.insert("break;");
+
+verify.numberOfErrorsInCurrentFile(1);
+
+goTo.marker("marker");
+edit.deleteAtCaret("break;".length);
+edit.insert("break myLabel;");
+
+verify.numberOfErrorsInCurrentFile(0);