diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 3521e4292b003..2956faef0b3ba 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -4249,5 +4249,13 @@ "Move to a new file": { "category": "Message", "code": 95049 + }, + "Remove unreachable code": { + "category": "Message", + "code": 95050 + }, + "Remove all unreachable code": { + "category": "Message", + "code": 95051 } } diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json index a9a0f9176fbfb..e9422dbf2d61e 100644 --- a/src/harness/tsconfig.json +++ b/src/harness/tsconfig.json @@ -105,6 +105,7 @@ "../services/codefixes/fixExtendsInterfaceBecomesImplements.ts", "../services/codefixes/fixForgottenThisPropertyAccess.ts", "../services/codefixes/fixUnusedIdentifier.ts", + "../services/codefixes/fixUnreachableCode.ts", "../services/codefixes/fixJSDocTypes.ts", "../services/codefixes/fixAwaitInSyncFunction.ts", "../services/codefixes/disableJsDiagnostics.ts", diff --git a/src/server/tsconfig.json b/src/server/tsconfig.json index 87a76a1e3c416..9980d9131da7f 100644 --- a/src/server/tsconfig.json +++ b/src/server/tsconfig.json @@ -101,6 +101,7 @@ "../services/codefixes/fixExtendsInterfaceBecomesImplements.ts", "../services/codefixes/fixForgottenThisPropertyAccess.ts", "../services/codefixes/fixUnusedIdentifier.ts", + "../services/codefixes/fixUnreachableCode.ts", "../services/codefixes/fixJSDocTypes.ts", "../services/codefixes/fixAwaitInSyncFunction.ts", "../services/codefixes/disableJsDiagnostics.ts", diff --git a/src/server/tsconfig.library.json b/src/server/tsconfig.library.json index 3dc0402e9a733..931393ca7bd5b 100644 --- a/src/server/tsconfig.library.json +++ b/src/server/tsconfig.library.json @@ -107,6 +107,7 @@ "../services/codefixes/fixExtendsInterfaceBecomesImplements.ts", "../services/codefixes/fixForgottenThisPropertyAccess.ts", "../services/codefixes/fixUnusedIdentifier.ts", + "../services/codefixes/fixUnreachableCode.ts", "../services/codefixes/fixJSDocTypes.ts", "../services/codefixes/fixAwaitInSyncFunction.ts", "../services/codefixes/disableJsDiagnostics.ts", diff --git a/src/services/codefixes/fixUnreachableCode.ts b/src/services/codefixes/fixUnreachableCode.ts new file mode 100644 index 0000000000000..5b075d6af1fdf --- /dev/null +++ b/src/services/codefixes/fixUnreachableCode.ts @@ -0,0 +1,89 @@ +/* @internal */ +namespace ts.codefix { + const fixId = "fixUnreachableCode"; + const errorCodes = [Diagnostics.Unreachable_code_detected.code]; + registerCodeFix({ + errorCodes, + getCodeActions(context) { + const changes = textChanges.ChangeTracker.with(context, t => doChange(t, context.sourceFile, context.span.start)); + return [createCodeFixAction(fixId, changes, Diagnostics.Remove_unreachable_code, fixId, Diagnostics.Remove_all_unreachable_code)]; + }, + fixIds: [fixId], + getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, diag) => doChange(changes, diag.file, diag.start)), + }); + + function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, start: number): void { + const token = getTokenAtPosition(sourceFile, start, /*includeJsDocComment*/ false); + const statement = findAncestor(token, isStatement); + Debug.assert(statement.getStart(sourceFile) === token.getStart(sourceFile)); + + const container = (isBlock(statement.parent) ? statement.parent : statement).parent; + switch (container.kind) { + case SyntaxKind.IfStatement: + if ((container as IfStatement).elseStatement) { + if (isBlock(statement.parent)) { + changes.deleteNodeRange(sourceFile, first(statement.parent.statements), last(statement.parent.statements)); + } + else { + changes.replaceNode(sourceFile, statement, createBlock(emptyArray)); + } + break; + } + // falls through + case SyntaxKind.WhileStatement: + case SyntaxKind.ForStatement: + changes.deleteNode(sourceFile, container); + break; + default: + if (isBlock(statement.parent)) { + split(sliceAfter(statement.parent.statements, statement), shouldRemove, (start, end) => changes.deleteNodeRange(sourceFile, start, end)); + } + else { + changes.deleteNode(sourceFile, statement); + } + } + } + + function shouldRemove(s: Statement): boolean { + // Don't remove statements that can validly be used before they appear. + return !isFunctionDeclaration(s) && !isPurelyTypeDeclaration(s) && + // `var x;` may declare a variable used above + !(isVariableStatement(s) && !(getCombinedNodeFlags(s) & (NodeFlags.Let | NodeFlags.Const)) && s.declarationList.declarations.some(d => !d.initializer)); + } + + function isPurelyTypeDeclaration(s: Statement): boolean { + switch (s.kind) { + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.TypeAliasDeclaration: + return true; + case SyntaxKind.ModuleDeclaration: + return getModuleInstanceState(s as ModuleDeclaration) !== ModuleInstanceState.Instantiated; + case SyntaxKind.EnumDeclaration: + return hasModifier(s, ModifierFlags.Const); + } + } + + function sliceAfter(arr: ReadonlyArray, value: T): ReadonlyArray { + const index = arr.indexOf(value); + Debug.assert(index !== -1); + return arr.slice(index); + } + + // Calls 'cb' with the start and end of each range where 'pred' is true. + function split(arr: ReadonlyArray, pred: (t: T) => boolean, cb: (start: T, end: T) => void): void { + let start: T | undefined; + for (let i = 0; i < arr.length; i++) { + const value = arr[i]; + if (pred(value)) { + start = start || value; + } + else { + if (start) { + cb(start, arr[i - 1]); + start = undefined; + } + } + } + if (start) cb(start, arr[arr.length - 1]); + } +} diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index c38a7324b30cd..85760d4566c9b 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -98,6 +98,7 @@ "codefixes/fixExtendsInterfaceBecomesImplements.ts", "codefixes/fixForgottenThisPropertyAccess.ts", "codefixes/fixUnusedIdentifier.ts", + "codefixes/fixUnreachableCode.ts", "codefixes/fixJSDocTypes.ts", "codefixes/fixAwaitInSyncFunction.ts", "codefixes/disableJsDiagnostics.ts", diff --git a/tests/cases/fourslash/codeFixUnreachableCode.ts b/tests/cases/fourslash/codeFixUnreachableCode.ts new file mode 100644 index 0000000000000..bbe716e792e35 --- /dev/null +++ b/tests/cases/fourslash/codeFixUnreachableCode.ts @@ -0,0 +1,31 @@ +/// + +////function f() { +//// return f(); +//// return 1; +//// function f() {} +//// return 2; +//// type T = number; +//// interface I {} +//// const enum E {} +//// enum E {} +//// namespace N { export type T = number; } +//// namespace N { export const x = 0; } +//// var x; +//// var y = 0; +////} + +verify.codeFix({ + description: "Remove unreachable code", + index: 0, + newFileContent: +`function f() { + return f(); + function f() {} + type T = number; + interface I {} + const enum E {} + namespace N { export type T = number; } + var x; +}`, +}); diff --git a/tests/cases/fourslash/codeFixUnreachableCode_if.ts b/tests/cases/fourslash/codeFixUnreachableCode_if.ts new file mode 100644 index 0000000000000..ac94cfd4eb2aa --- /dev/null +++ b/tests/cases/fourslash/codeFixUnreachableCode_if.ts @@ -0,0 +1,40 @@ +/// + +////if (false) a; +////if (false) { +//// a; +////} +//// +////// No good way to delete just the 'if' part +////if (false) a; else b; +////if (false) { +//// a; +////} else { +//// b; +////} +//// +////while (false) a; +////while (false) { +//// a; +////} +//// +////for (let x = 0; false; ++x) a; +////for (let x = 0; false; ++x) { +//// a; +////} + +verify.codeFixAll({ + fixId: "fixUnreachableCode", + fixAllDescription: "Remove all unreachable code", + newFileContent: +` +// No good way to delete just the 'if' part +if (false) { } else b; +if (false) { +} else { + b; +} + + +`, +});