From 6efd86a30cdcd1664212e489699b86684a299b1a Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 11 May 2020 18:46:55 -0700 Subject: [PATCH] Cleaning up fix all for JS/TS --- .../src/features/fixAll.ts | 57 ++++++++---- .../src/features/quickFix.ts | 22 ++--- .../src/test/fixAll.test.ts | 89 +++++++++++++++++++ .../src/utils/errorCodes.ts | 4 + .../src/utils/fixNames.ts | 15 ++++ 5 files changed, 158 insertions(+), 29 deletions(-) create mode 100644 extensions/typescript-language-features/src/test/fixAll.test.ts create mode 100644 extensions/typescript-language-features/src/utils/fixNames.ts diff --git a/extensions/typescript-language-features/src/features/fixAll.ts b/extensions/typescript-language-features/src/features/fixAll.ts index 0f2e0cc78d2..f52b7f76071 100644 --- a/extensions/typescript-language-features/src/features/fixAll.ts +++ b/extensions/typescript-language-features/src/features/fixAll.ts @@ -8,24 +8,32 @@ import * as nls from 'vscode-nls'; import type * as Proto from '../protocol'; import { ITypeScriptServiceClient } from '../typescriptService'; import API from '../utils/api'; -import { ConfigurationDependentRegistration, VersionDependentRegistration } from '../utils/dependentRegistration'; +import { VersionDependentRegistration } from '../utils/dependentRegistration'; import * as typeConverters from '../utils/typeConverters'; import { DiagnosticsManager } from './diagnostics'; import FileConfigurationManager from './fileConfigurationManager'; +import * as errorCodes from '../utils/errorCodes'; +import * as fixNames from '../utils/fixNames'; const localize = nls.loadMessageBundle(); -const autoFixableDiagnosticCodes = new Set([ - 2420, // Incorrectly implemented interface - 2552, // Cannot find name -]); +interface AutoFixableError { + readonly code: number; + readonly fixName: string; +} + +const fixImplementInterface = Object.freeze({ code: errorCodes.incorrectlyImplementsInterface, fixName: fixNames.classIncorrectlyImplementsInterface }); +const fixUnreachable = Object.freeze({ code: errorCodes.unreachableCode, fixName: fixNames.unreachableCode }); +const fixAsync = Object.freeze({ code: errorCodes.asyncOnlyAllowedInAsyncFunctions, fixName: fixNames.awaitInSyncFunction }); class TypeScriptAutoFixProvider implements vscode.CodeActionProvider { - private static readonly kind = vscode.CodeActionKind.SourceFixAll.append('ts'); + private static readonly fixAllKind = vscode.CodeActionKind.SourceFixAll.append('ts'); public static readonly metadata: vscode.CodeActionProviderMetadata = { - providedCodeActionKinds: [TypeScriptAutoFixProvider.kind] + providedCodeActionKinds: [ + TypeScriptAutoFixProvider.fixAllKind, + ] }; constructor( @@ -49,35 +57,40 @@ class TypeScriptAutoFixProvider implements vscode.CodeActionProvider { return undefined; } - const autoFixableDiagnostics = this.getAutoFixableDiagnostics(document); + const autoFixes = this.getAutoFixes(); + + const autoFixableDiagnostics = this.getAutoFixableDiagnostics(document, autoFixes); if (!autoFixableDiagnostics.length) { return undefined; } - const fixAllAction = await this.getFixAllCodeAction(document, file, autoFixableDiagnostics, token); + const fixAllAction = await this.getFixAllCodeAction(document, file, autoFixableDiagnostics, autoFixes, token); return fixAllAction ? [fixAllAction] : undefined; } private getAutoFixableDiagnostics( - document: vscode.TextDocument + document: vscode.TextDocument, + autoFixes: readonly AutoFixableError[], ): vscode.Diagnostic[] { if (this.client.bufferSyncSupport.hasPendingDiagnostics(document.uri)) { return []; } + const fixableCodes = new Set(autoFixes.map(x => x.code)); return this.diagnosticsManager.getDiagnostics(document.uri) - .filter(x => autoFixableDiagnosticCodes.has(x.code as number)); + .filter(x => fixableCodes.has(+(x.code as number))); } private async getFixAllCodeAction( document: vscode.TextDocument, file: string, diagnostics: ReadonlyArray, + autoFixes: readonly AutoFixableError[], token: vscode.CancellationToken, ): Promise { await this.fileConfigurationManager.ensureConfigurationForDocument(document, token); - const autoFixResponse = await this.getAutoFixEdit(file, diagnostics, token); + const autoFixResponse = await this.getAutoFixEdit(file, diagnostics, autoFixes, token); if (!autoFixResponse) { return undefined; } @@ -88,7 +101,7 @@ class TypeScriptAutoFixProvider implements vscode.CodeActionProvider { const codeAction = new vscode.CodeAction( localize('autoFix.label', 'Auto fix'), - TypeScriptAutoFixProvider.kind); + TypeScriptAutoFixProvider.fixAllKind); codeAction.edit = edit; codeAction.diagnostics = fixedDiagnostics; @@ -98,6 +111,7 @@ class TypeScriptAutoFixProvider implements vscode.CodeActionProvider { private async getAutoFixEdit( file: string, diagnostics: ReadonlyArray, + autoFixes: readonly AutoFixableError[], token: vscode.CancellationToken, ): Promise<{ edit: vscode.WorkspaceEdit, fixedDiagnostics: vscode.Diagnostic[] } | undefined> { const edit = new vscode.WorkspaceEdit(); @@ -113,7 +127,7 @@ class TypeScriptAutoFixProvider implements vscode.CodeActionProvider { } const fix = response.body[0]; - if (['fixClassIncorrectlyImplementsInterface', 'spelling'].includes(fix.fixName)) { + if (autoFixes.some(autoFix => autoFix.fixName.includes(fix.fixName))) { typeConverters.WorkspaceEdit.withFileCodeEdits(edit, this.client, fix.changes); fixedDiagnostics.push(diagnostic); } @@ -125,6 +139,14 @@ class TypeScriptAutoFixProvider implements vscode.CodeActionProvider { return { edit, fixedDiagnostics }; } + + private getAutoFixes(): readonly AutoFixableError[] { + return [ + fixImplementInterface, + fixUnreachable, + fixAsync, + ]; + } } export function register( @@ -133,8 +155,7 @@ export function register( fileConfigurationManager: FileConfigurationManager, diagnosticsManager: DiagnosticsManager) { return new VersionDependentRegistration(client, API.v300, () => - new ConfigurationDependentRegistration('typescript', 'experimental.autoFix.enabled', () => - vscode.languages.registerCodeActionsProvider(selector, - new TypeScriptAutoFixProvider(client, fileConfigurationManager, diagnosticsManager), - TypeScriptAutoFixProvider.metadata))); + vscode.languages.registerCodeActionsProvider(selector, + new TypeScriptAutoFixProvider(client, fileConfigurationManager, diagnosticsManager), + TypeScriptAutoFixProvider.metadata)); } diff --git a/extensions/typescript-language-features/src/features/quickFix.ts b/extensions/typescript-language-features/src/features/quickFix.ts index 0023c41600e..5c9ff0ab802 100644 --- a/extensions/typescript-language-features/src/features/quickFix.ts +++ b/extensions/typescript-language-features/src/features/quickFix.ts @@ -11,6 +11,7 @@ import API from '../utils/api'; import { nulToken } from '../utils/cancellation'; import { applyCodeActionCommands, getEditForCodeAction } from '../utils/codeAction'; import { Command, CommandManager } from '../utils/commandManager'; +import * as fixNames from '../utils/fixNames'; import { memoize } from '../utils/memoize'; import { TelemetryReporter } from '../utils/telemetry'; import * as typeConverters from '../utils/typeConverters'; @@ -332,18 +333,17 @@ const fixAllErrorCodes = new Map([ [2345, 2339], ]); - const preferredFixes = new Map([ - ['annotateWithTypeFromJSDoc', 0], - ['constructorForDerivedNeedSuperCall', 0], - ['extendsInterfaceBecomesImplements', 0], - ['fixAwaitInSyncFunction', 0], - ['fixClassIncorrectlyImplementsInterface', 1], - ['fixUnreachableCode', 0], - ['unusedIdentifier', 0], - ['forgottenThisPropertyAccess', 0], - ['spelling', 1], - ['addMissingAwait', 0], + [fixNames.annotateWithTypeFromJSDoc, 0], + [fixNames.constructorForDerivedNeedSuperCall, 0], + [fixNames.extendsInterfaceBecomesImplements, 0], + [fixNames.awaitInSyncFunction, 0], + [fixNames.classIncorrectlyImplementsInterface, 1], + [fixNames.unreachableCode, 0], + [fixNames.unusedIdentifier, 0], + [fixNames.forgottenThisPropertyAccess, 0], + [fixNames.spelling, 1], + [fixNames.addMissingAwait, 0], ]); function isPreferredFix( diff --git a/extensions/typescript-language-features/src/test/fixAll.test.ts b/extensions/typescript-language-features/src/test/fixAll.test.ts new file mode 100644 index 00000000000..eb5448a1f90 --- /dev/null +++ b/extensions/typescript-language-features/src/test/fixAll.test.ts @@ -0,0 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'mocha'; +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import { disposeAll } from '../utils/dispose'; +import { createTestEditor, wait, joinLines } from './testUtils'; + +const testDocumentUri = vscode.Uri.parse('untitled:test.ts'); + +const emptyRange = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0)); + +suite('TypeScript Fix All', () => { + + const _disposables: vscode.Disposable[] = []; + + teardown(async () => { + disposeAll(_disposables); + + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + }); + + test('Should fix unreachable code', async () => { + const editor = await createTestEditor(testDocumentUri, + `function foo() {`, + ` return 1;`, + ` return 2;`, + `};`, + `function boo() {`, + ` return 3;`, + ` return 4;`, + `};`, + ); + + await wait(2500); + + const fixes = await vscode.commands.executeCommand('vscode.executeCodeActionProvider', + testDocumentUri, + emptyRange, + vscode.CodeActionKind.SourceFixAll + ); + assert.strictEqual(fixes?.length, 1); + + await vscode.workspace.applyEdit(fixes![0].edit!); + assert.strictEqual(editor.document.getText(), joinLines( + `function foo() {`, + ` return 1;`, + `};`, + `function boo() {`, + ` return 3;`, + `};`, + )); + }); + + test('Should implement interfaces', async () => { + const editor = await createTestEditor(testDocumentUri, + `interface I {`, + ` x: number;`, + `}`, + `class A implements I {}`, + `class B implements I {}`, + ); + + await wait(2500); + + const fixes = await vscode.commands.executeCommand('vscode.executeCodeActionProvider', + testDocumentUri, + emptyRange, + vscode.CodeActionKind.SourceFixAll + ); + assert.strictEqual(fixes?.length, 1); + + await vscode.workspace.applyEdit(fixes![0].edit!); + assert.strictEqual(editor.document.getText(), joinLines( + `interface I {`, + ` x: number;`, + `}`, + `class A implements I {`, + ` x: number;`, + `}`, + `class B implements I {`, + ` x: number;`, + `}`, + )); + }); +}); diff --git a/extensions/typescript-language-features/src/utils/errorCodes.ts b/extensions/typescript-language-features/src/utils/errorCodes.ts index 4407b3a054b..fa29edc6e4f 100644 --- a/extensions/typescript-language-features/src/utils/errorCodes.ts +++ b/extensions/typescript-language-features/src/utils/errorCodes.ts @@ -10,3 +10,7 @@ export const unreachableCode = 7027; export const unusedLabel = 7028; export const fallThroughCaseInSwitch = 7029; export const notAllCodePathsReturnAValue = 7030; +export const incorrectlyImplementsInterface = 2420; +export const cannotFindName = 2552; +export const extendsShouldBeImplements = 2689; +export const asyncOnlyAllowedInAsyncFunctions = 1308; diff --git a/extensions/typescript-language-features/src/utils/fixNames.ts b/extensions/typescript-language-features/src/utils/fixNames.ts new file mode 100644 index 00000000000..f14dbdbe6ac --- /dev/null +++ b/extensions/typescript-language-features/src/utils/fixNames.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export const annotateWithTypeFromJSDoc = 'annotateWithTypeFromJSDoc'; +export const constructorForDerivedNeedSuperCall = 'constructorForDerivedNeedSuperCall'; +export const extendsInterfaceBecomesImplements = 'extendsInterfaceBecomesImplements'; +export const awaitInSyncFunction = 'fixAwaitInSyncFunction'; +export const classIncorrectlyImplementsInterface = 'fixClassIncorrectlyImplementsInterface'; +export const unreachableCode = 'fixUnreachableCode'; +export const unusedIdentifier = 'unusedIdentifier'; +export const forgottenThisPropertyAccess = 'forgottenThisPropertyAccess'; +export const spelling = 'spelling'; +export const addMissingAwait = 'addMissingAwait';