Skip to content

Commit 6ae4d3a

Browse files
author
Andy
authored
Add code fix to remove unreachable code (#24028)
* Add code fix to remove unreachable code * Code review * Preserve more kinds of statements
1 parent e33e229 commit 6ae4d3a

File tree

8 files changed

+172
-0
lines changed

8 files changed

+172
-0
lines changed

src/compiler/diagnosticMessages.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4249,5 +4249,13 @@
42494249
"Move to a new file": {
42504250
"category": "Message",
42514251
"code": 95049
4252+
},
4253+
"Remove unreachable code": {
4254+
"category": "Message",
4255+
"code": 95050
4256+
},
4257+
"Remove all unreachable code": {
4258+
"category": "Message",
4259+
"code": 95051
42524260
}
42534261
}

src/harness/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@
105105
"../services/codefixes/fixExtendsInterfaceBecomesImplements.ts",
106106
"../services/codefixes/fixForgottenThisPropertyAccess.ts",
107107
"../services/codefixes/fixUnusedIdentifier.ts",
108+
"../services/codefixes/fixUnreachableCode.ts",
108109
"../services/codefixes/fixJSDocTypes.ts",
109110
"../services/codefixes/fixAwaitInSyncFunction.ts",
110111
"../services/codefixes/disableJsDiagnostics.ts",

src/server/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
"../services/codefixes/fixExtendsInterfaceBecomesImplements.ts",
102102
"../services/codefixes/fixForgottenThisPropertyAccess.ts",
103103
"../services/codefixes/fixUnusedIdentifier.ts",
104+
"../services/codefixes/fixUnreachableCode.ts",
104105
"../services/codefixes/fixJSDocTypes.ts",
105106
"../services/codefixes/fixAwaitInSyncFunction.ts",
106107
"../services/codefixes/disableJsDiagnostics.ts",

src/server/tsconfig.library.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
"../services/codefixes/fixExtendsInterfaceBecomesImplements.ts",
108108
"../services/codefixes/fixForgottenThisPropertyAccess.ts",
109109
"../services/codefixes/fixUnusedIdentifier.ts",
110+
"../services/codefixes/fixUnreachableCode.ts",
110111
"../services/codefixes/fixJSDocTypes.ts",
111112
"../services/codefixes/fixAwaitInSyncFunction.ts",
112113
"../services/codefixes/disableJsDiagnostics.ts",
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/* @internal */
2+
namespace ts.codefix {
3+
const fixId = "fixUnreachableCode";
4+
const errorCodes = [Diagnostics.Unreachable_code_detected.code];
5+
registerCodeFix({
6+
errorCodes,
7+
getCodeActions(context) {
8+
const changes = textChanges.ChangeTracker.with(context, t => doChange(t, context.sourceFile, context.span.start));
9+
return [createCodeFixAction(fixId, changes, Diagnostics.Remove_unreachable_code, fixId, Diagnostics.Remove_all_unreachable_code)];
10+
},
11+
fixIds: [fixId],
12+
getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, diag) => doChange(changes, diag.file, diag.start)),
13+
});
14+
15+
function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, start: number): void {
16+
const token = getTokenAtPosition(sourceFile, start, /*includeJsDocComment*/ false);
17+
const statement = findAncestor(token, isStatement);
18+
Debug.assert(statement.getStart(sourceFile) === token.getStart(sourceFile));
19+
20+
const container = (isBlock(statement.parent) ? statement.parent : statement).parent;
21+
switch (container.kind) {
22+
case SyntaxKind.IfStatement:
23+
if ((container as IfStatement).elseStatement) {
24+
if (isBlock(statement.parent)) {
25+
changes.deleteNodeRange(sourceFile, first(statement.parent.statements), last(statement.parent.statements));
26+
}
27+
else {
28+
changes.replaceNode(sourceFile, statement, createBlock(emptyArray));
29+
}
30+
break;
31+
}
32+
// falls through
33+
case SyntaxKind.WhileStatement:
34+
case SyntaxKind.ForStatement:
35+
changes.deleteNode(sourceFile, container);
36+
break;
37+
default:
38+
if (isBlock(statement.parent)) {
39+
split(sliceAfter(statement.parent.statements, statement), shouldRemove, (start, end) => changes.deleteNodeRange(sourceFile, start, end));
40+
}
41+
else {
42+
changes.deleteNode(sourceFile, statement);
43+
}
44+
}
45+
}
46+
47+
function shouldRemove(s: Statement): boolean {
48+
// Don't remove statements that can validly be used before they appear.
49+
return !isFunctionDeclaration(s) && !isPurelyTypeDeclaration(s) &&
50+
// `var x;` may declare a variable used above
51+
!(isVariableStatement(s) && !(getCombinedNodeFlags(s) & (NodeFlags.Let | NodeFlags.Const)) && s.declarationList.declarations.some(d => !d.initializer));
52+
}
53+
54+
function isPurelyTypeDeclaration(s: Statement): boolean {
55+
switch (s.kind) {
56+
case SyntaxKind.InterfaceDeclaration:
57+
case SyntaxKind.TypeAliasDeclaration:
58+
return true;
59+
case SyntaxKind.ModuleDeclaration:
60+
return getModuleInstanceState(s as ModuleDeclaration) !== ModuleInstanceState.Instantiated;
61+
case SyntaxKind.EnumDeclaration:
62+
return hasModifier(s, ModifierFlags.Const);
63+
}
64+
}
65+
66+
function sliceAfter<T>(arr: ReadonlyArray<T>, value: T): ReadonlyArray<T> {
67+
const index = arr.indexOf(value);
68+
Debug.assert(index !== -1);
69+
return arr.slice(index);
70+
}
71+
72+
// Calls 'cb' with the start and end of each range where 'pred' is true.
73+
function split<T>(arr: ReadonlyArray<T>, pred: (t: T) => boolean, cb: (start: T, end: T) => void): void {
74+
let start: T | undefined;
75+
for (let i = 0; i < arr.length; i++) {
76+
const value = arr[i];
77+
if (pred(value)) {
78+
start = start || value;
79+
}
80+
else {
81+
if (start) {
82+
cb(start, arr[i - 1]);
83+
start = undefined;
84+
}
85+
}
86+
}
87+
if (start) cb(start, arr[arr.length - 1]);
88+
}
89+
}

src/services/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
"codefixes/fixExtendsInterfaceBecomesImplements.ts",
9999
"codefixes/fixForgottenThisPropertyAccess.ts",
100100
"codefixes/fixUnusedIdentifier.ts",
101+
"codefixes/fixUnreachableCode.ts",
101102
"codefixes/fixJSDocTypes.ts",
102103
"codefixes/fixAwaitInSyncFunction.ts",
103104
"codefixes/disableJsDiagnostics.ts",
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////function f() {
4+
//// return f();
5+
//// return 1;
6+
//// function f() {}
7+
//// return 2;
8+
//// type T = number;
9+
//// interface I {}
10+
//// const enum E {}
11+
//// enum E {}
12+
//// namespace N { export type T = number; }
13+
//// namespace N { export const x = 0; }
14+
//// var x;
15+
//// var y = 0;
16+
////}
17+
18+
verify.codeFix({
19+
description: "Remove unreachable code",
20+
index: 0,
21+
newFileContent:
22+
`function f() {
23+
return f();
24+
function f() {}
25+
type T = number;
26+
interface I {}
27+
const enum E {}
28+
namespace N { export type T = number; }
29+
var x;
30+
}`,
31+
});
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////if (false) a;
4+
////if (false) {
5+
//// a;
6+
////}
7+
////
8+
////// No good way to delete just the 'if' part
9+
////if (false) a; else b;
10+
////if (false) {
11+
//// a;
12+
////} else {
13+
//// b;
14+
////}
15+
////
16+
////while (false) a;
17+
////while (false) {
18+
//// a;
19+
////}
20+
////
21+
////for (let x = 0; false; ++x) a;
22+
////for (let x = 0; false; ++x) {
23+
//// a;
24+
////}
25+
26+
verify.codeFixAll({
27+
fixId: "fixUnreachableCode",
28+
fixAllDescription: "Remove all unreachable code",
29+
newFileContent:
30+
`
31+
// No good way to delete just the 'if' part
32+
if (false) { } else b;
33+
if (false) {
34+
} else {
35+
b;
36+
}
37+
38+
39+
`,
40+
});

0 commit comments

Comments
 (0)