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);