diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 075d4247460e9..17a54328b378f 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3500,8 +3500,6 @@ "category": "Message", "code": 90021 }, - - "Octal literal types must use ES2015 syntax. Use the syntax '{0}'.": { "category": "Error", "code": 8017 @@ -3513,5 +3511,9 @@ "Report errors in .js files.": { "category": "Message", "code": 8019 + }, + "Convert function '{0}' to ES6 class.": { + "category": "CodeFix", + "code": 100000 } } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index f871aee0baa08..e2bcf54aa6d1e 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3349,6 +3349,7 @@ namespace ts { Warning, Error, Message, + CodeFix } export enum ModuleResolutionKind { diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 5ed1162bd93a2..1d7de2b1d5e25 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -481,14 +481,29 @@ namespace FourSlash { private getDiagnostics(fileName: string): ts.Diagnostic[] { const syntacticErrors = this.languageService.getSyntacticDiagnostics(fileName); const semanticErrors = this.languageService.getSemanticDiagnostics(fileName); + const codeFixDiagnostics = this.getCodeFixDiagnostics(fileName); const diagnostics: ts.Diagnostic[] = []; diagnostics.push.apply(diagnostics, syntacticErrors); diagnostics.push.apply(diagnostics, semanticErrors); + diagnostics.push.apply(diagnostics, codeFixDiagnostics); return diagnostics; } + private getCodeFixDiagnostics(fileName: string): ts.Diagnostic[] { + let result: ts.Diagnostic[]; + + // In some language service implementation the `getCodeFixDiagnostics` is not implemented + try { + result = this.languageService.getCodeFixDiagnostics(fileName); + } + catch (e) { + result = []; + } + return result; + } + private getAllDiagnostics(): ts.Diagnostic[] { const diagnostics: ts.Diagnostic[] = []; @@ -2609,6 +2624,81 @@ namespace FourSlash { } } + public verifyCodeFixDiagnosticsAvailableAtMarkers(negative: boolean, markerNames: string[], diagnosticCode?: number) { + const refactorDiagnostics = this.getCodeFixDiagnostics(this.activeFile.fileName); + + for (const markerName of markerNames) { + const marker = this.getMarkerByName(markerName); + let foundDiagnostic = false; + for (const diag of refactorDiagnostics) { + if (diag.start <= marker.position && diag.start + diag.length >= marker.position) { + foundDiagnostic = diagnosticCode === undefined || diagnosticCode === diag.code; + } + } + + if (negative && foundDiagnostic) { + this.raiseError(`verifyCodeFixDiagnosticsAvailableAtMarkers failed - expected no codeFix diagnostic at marker ${markerName} but found some.`); + } + if (!negative && !foundDiagnostic) { + this.raiseError(`verifyCodeFixDiagnosticsAvailableAtMarkers failed - expected a codeFix diagnostic at marker ${markerName} but found none.`); + } + } + } + + public verifyApplicableRefactorAvailableAtMarker(negative: boolean, markerName: string) { + const marker = this.getMarkerByName(markerName); + const applicableRefactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, marker.position); + const isAvailable = applicableRefactors && applicableRefactors.length > 0; + if (negative && isAvailable) { + this.raiseError(`verifyApplicableRefactorAvailableAtMarker failed - expected no refactor at marker ${markerName} but found some.`); + } + if (!negative && !isAvailable) { + this.raiseError(`verifyApplicableRefactorAvailableAtMarker failed - expected a refactor at marker ${markerName} but found none.`); + } + } + + public verifyApplicableRefactorAvailableForRange(negative: boolean) { + const ranges = this.getRanges(); + if (!(ranges && ranges.length === 1)) { + throw new Error("Exactly one refactor range is allowed per test."); + } + + const applicableRefactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, { pos: ranges[0].start, end: ranges[0].end }); + const isAvailable = applicableRefactors && applicableRefactors.length > 0; + if (negative && isAvailable) { + this.raiseError(`verifyApplicableRefactorAvailableForRange failed - expected no refactor but found some.`); + } + if (!negative && !isAvailable) { + this.raiseError(`verifyApplicableRefactorAvailableForRange failed - expected a refactor but found none.`); + } + } + + public verifyFileAfterApplyingRefactorAtMarker( + markerName: string, + expectedContent: string, + refactorNameToApply: string, + formattingOptions?: ts.FormatCodeSettings) { + + formattingOptions = formattingOptions || this.formatCodeSettings; + const markerPos = this.getMarkerByName(markerName).position; + + const applicableRefactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, markerPos); + const applicableRefactorToApply = ts.find(applicableRefactors, refactor => refactor.refactorName === refactorNameToApply); + + if (!applicableRefactorToApply) { + this.raiseError(`The expected refactor: ${refactorNameToApply} is not available at the marker location.`); + } + + const codeActions = this.languageService.getRefactorCodeActions(this.activeFile.fileName, formattingOptions, markerPos, refactorNameToApply); + + this.applyCodeAction(this.activeFile.fileName, codeActions); + const actualContent = this.getFileContent(this.activeFile.fileName); + + if (this.normalizeNewlines(actualContent) !== this.normalizeNewlines(expectedContent)) { + this.raiseError(`verifyFileAfterApplyingRefactors failed: expected:\n${expectedContent}\nactual:\n${actualContent}`); + } + } + public printAvailableCodeFixes() { const codeFixes = this.getCodeFixActions(this.activeFile.fileName); Harness.IO.log(stringify(codeFixes)); @@ -3405,6 +3495,18 @@ namespace FourSlashInterface { public codeFixAvailable() { this.state.verifyCodeFixAvailable(this.negative); } + + public codeFixDiagnosticsAvailableAtMarkers(markerNames: string[], refactorCode?: number) { + this.state.verifyCodeFixDiagnosticsAvailableAtMarkers(this.negative, markerNames, refactorCode); + } + + public applicableRefactorAvailableAtMarker(markerName: string) { + this.state.verifyApplicableRefactorAvailableAtMarker(this.negative, markerName); + } + + public applicableRefactorAvailableForRange() { + this.state.verifyApplicableRefactorAvailableForRange(this.negative); + } } export class Verify extends VerifyNegatable { @@ -3615,6 +3717,10 @@ namespace FourSlashInterface { this.state.verifyRangeAfterCodeFix(expectedText, includeWhiteSpace, errorCode, index); } + public fileAfterApplyingRefactorAtMarker(markerName: string, expectedContent: string, refactorNameToApply: string, formattingOptions?: ts.FormatCodeSettings): void { + this.state.verifyFileAfterApplyingRefactorAtMarker(markerName, expectedContent, refactorNameToApply, formattingOptions); + } + public importFixAtPosition(expectedTextArray: string[], errorCode?: number): void { this.state.verifyImportFixAtPosition(expectedTextArray, errorCode); } diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index c6477240161cb..519c1dc700a1e 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -489,6 +489,15 @@ namespace Harness.LanguageService { getCodeFixesAtPosition(): ts.CodeAction[] { throw new Error("Not supported on the shim."); } + getCodeFixDiagnostics(): ts.Diagnostic[] { + throw new Error("Not supported on the shim."); + } + getRefactorCodeActions(): ts.CodeAction[] { + throw new Error("Not supported on the shim."); + } + getApplicableRefactors(): ts.ApplicableRefactorInfo[] { + throw new Error("Not supported on the shim."); + } getEmitOutput(fileName: string): ts.EmitOutput { return unwrapJSONCallResult(this.shim.getEmitOutput(fileName)); } diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 8364420709cc3..956514c7dac22 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -323,6 +323,7 @@ namespace ts.projectSystem { this.map[timeoutId] = cb.bind(/*this*/ undefined, ...args); return timeoutId; } + unregister(id: any) { if (typeof id === "number") { delete this.map[id]; @@ -338,10 +339,13 @@ namespace ts.projectSystem { } invoke() { + // Note: invoking a callback may result in new callbacks been queued, + // so do not clear the entire callback list regardless. Only remove the + // ones we have invoked. for (const key in this.map) { this.map[key](); + delete this.map[key]; } - this.map = []; } } @@ -3589,7 +3593,7 @@ namespace ts.projectSystem { // run first step host.runQueuedTimeoutCallbacks(); - assert.equal(host.getOutput().length, 1, "expect 1 messages"); + assert.equal(host.getOutput().length, 1, "expect 1 message"); const e1 = getMessage(0); assert.equal(e1.event, "syntaxDiag"); host.clearOutput(); @@ -3611,15 +3615,23 @@ namespace ts.projectSystem { // run first step host.runQueuedTimeoutCallbacks(); - assert.equal(host.getOutput().length, 1, "expect 1 messages"); + assert.equal(host.getOutput().length, 1, "expect 1 message"); const e1 = getMessage(0); assert.equal(e1.event, "syntaxDiag"); host.clearOutput(); + // the semanticDiag message host.runQueuedImmediateCallbacks(); - assert.equal(host.getOutput().length, 2, "expect 2 messages"); + assert.equal(host.getOutput().length, 1, "expect 1 message"); const e2 = getMessage(0); assert.equal(e2.event, "semanticDiag"); + host.clearOutput(); + + // the refactor diagnostics check + host.runQueuedImmediateCallbacks(); + assert.equal(host.getOutput().length, 2, "expect 2 messages"); + const e3 = getMessage(0); + assert.equal(e3.event, "codeFixDiag"); verifyRequestCompleted(getErrId, 1); cancellationToken.resetToken(); @@ -3633,7 +3645,7 @@ namespace ts.projectSystem { assert.equal(host.getOutput().length, 0, "expect 0 messages"); // run first step host.runQueuedTimeoutCallbacks(); - assert.equal(host.getOutput().length, 1, "expect 1 messages"); + assert.equal(host.getOutput().length, 1, "expect 1 message"); const e1 = getMessage(0); assert.equal(e1.event, "syntaxDiag"); host.clearOutput(); diff --git a/src/server/client.ts b/src/server/client.ts index 623b00f1ed304..ca7882cb6965e 100644 --- a/src/server/client.ts +++ b/src/server/client.ts @@ -695,6 +695,50 @@ namespace ts.server { return response.body.map(entry => this.convertCodeActions(entry, fileName)); } + getCodeFixDiagnostics(_fileName: string): Diagnostic[] { + return notImplemented(); + } + + private createFileLocationOrRangeRequestArgs(positionOrRange: number | TextRange, fileName: string): protocol.FileLocationOrRangeRequestArgs { + if (typeof positionOrRange === "number") { + const { line, offset } = this.positionToOneBasedLineOffset(fileName, positionOrRange); + return { file: fileName, line, offset }; + } + const { line: startLine, offset: startOffset } = this.positionToOneBasedLineOffset(fileName, positionOrRange.pos); + const { line: endLine, offset: endOffset } = this.positionToOneBasedLineOffset(fileName, positionOrRange.end); + return { + file: fileName, + startLine, + startOffset, + endLine, + endOffset + }; + } + + getApplicableRefactors(fileName: string, positionOrRange: number | TextRange): ApplicableRefactorInfo[] { + const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName); + + const request = this.processRequest(CommandNames.GetApplicableRefactors, args); + const response = this.processResponse(request); + + return response.body.refactors; + } + + getRefactorCodeActions( + fileName: string, + _formatOptions: FormatCodeSettings, + positionOrRange: number | TextRange, + refactorName: string) { + + const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName) as protocol.GetRefactorCodeActionsRequestArgs; + args.refactorName = refactorName; + + const request = this.processRequest(CommandNames.GetRefactorCodeActions, args); + const codeActions = this.processResponse(request).body.actions; + + return map(codeActions, codeAction => this.convertCodeActions(codeAction, fileName)); + } + convertCodeActions(entry: protocol.CodeAction, fileName: string): CodeAction { return { description: entry.description, diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 71998666fedd0..7673dbf1b73b7 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -95,6 +95,9 @@ namespace ts.server.protocol { /* @internal */ export type GetCodeFixesFull = "getCodeFixes-full"; export type GetSupportedCodeFixes = "getSupportedCodeFixes"; + + export type GetApplicableRefactors = "getApplicableRefactors"; + export type GetRefactorCodeActions = "getRefactorCodeActions"; } /** @@ -394,6 +397,43 @@ namespace ts.server.protocol { position?: number; } + export type FileLocationOrRangeRequestArgs = FileLocationRequestArgs | FileRangeRequestArgs; + + export interface GetApplicableRefactorsRequest extends Request { + command: CommandTypes.GetApplicableRefactors; + arguments: GetApplicableRefactorsRequestArgs; + } + + export type GetApplicableRefactorsRequestArgs = FileLocationOrRangeRequestArgs; + + export interface ApplicableRefactorInfo { + refactorName: string; + description: string; + } + + export interface GetApplicableRefactorsResponse extends Response { + body?: { refactors: ApplicableRefactorInfo[] }; + } + + export interface GetRefactorCodeActionsRequest extends Request { + command: CommandTypes.GetRefactorCodeActions; + arguments: GetRefactorCodeActionsRequestArgs; + } + + export type GetRefactorCodeActionsRequestArgs = FileLocationOrRangeRequestArgs & { + /* The kind of the applicable refactor */ + refactorName: string; + }; + + export interface GetRefactorCodeActionsResponse extends Response { + body?: { actions: CodeAction[] }; + } + + export interface CodeFixDiagnosticEventBody { + file: string; + diagnostics: Diagnostic[]; + } + /** * Request for the available codefixes at a specific position. */ @@ -402,10 +442,7 @@ namespace ts.server.protocol { arguments: CodeFixRequestArgs; } - /** - * Instances of this interface specify errorcodes on a specific location in a sourcefile. - */ - export interface CodeFixRequestArgs extends FileRequestArgs { + export interface FileRangeRequestArgs extends FileRequestArgs { /** * The line number for the request (1-based). */ @@ -437,7 +474,12 @@ namespace ts.server.protocol { */ /* @internal */ endPosition?: number; + } + /** + * Instances of this interface specify errorcodes on a specific location in a sourcefile. + */ + export interface CodeFixRequestArgs extends FileRangeRequestArgs { /** * Errorcodes we want to get the fixes for. */ diff --git a/src/server/session.ts b/src/server/session.ts index 26dbceece45ba..34cb6ca78caae 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -85,7 +85,7 @@ namespace ts.server { } export interface EventSender { - event(payload: any, eventName: string): void; + event(payload: T, eventName: string): void; } function allEditsBeforePos(edits: ts.TextChange[], pos: number) { @@ -190,6 +190,9 @@ namespace ts.server { /* @internal */ export const GetCodeFixesFull: protocol.CommandTypes.GetCodeFixesFull = "getCodeFixes-full"; export const GetSupportedCodeFixes: protocol.CommandTypes.GetSupportedCodeFixes = "getSupportedCodeFixes"; + + export const GetApplicableRefactors: protocol.CommandTypes.GetApplicableRefactors = "getApplicableRefactors"; + export const GetRefactorCodeActions: protocol.CommandTypes.GetRefactorCodeActions = "getRefactorCodeActions"; } export function formatMessage(msg: T, logger: server.Logger, byteLength: (s: string, encoding: string) => number, newLine: string): string { @@ -381,7 +384,7 @@ namespace ts.server { break; case ProjectLanguageServiceStateEvent: const eventName: protocol.ProjectLanguageServiceStateEventName = "projectLanguageServiceState"; - this.event({ + this.event({ projectName: event.data.project.getProjectName(), languageServiceEnabled: event.data.languageServiceEnabled }, eventName); @@ -425,7 +428,7 @@ namespace ts.server { this.send(ev); } - public event(info: any, eventName: string) { + public event(info: T, eventName: string) { const ev: protocol.Event = { seq: 0, type: "event", @@ -460,7 +463,7 @@ namespace ts.server { } const bakedDiags = diags.map((diag) => formatDiag(file, project, diag)); - this.event({ file: file, diagnostics: bakedDiags }, "semanticDiag"); + this.event({ file: file, diagnostics: bakedDiags }, "semanticDiag"); } catch (err) { this.logError(err, "semantic check"); @@ -472,7 +475,7 @@ namespace ts.server { const diags = project.getLanguageService().getSyntacticDiagnostics(file); if (diags) { const bakedDiags = diags.map((diag) => formatDiag(file, project, diag)); - this.event({ file: file, diagnostics: bakedDiags }, "syntaxDiag"); + this.event({ file: file, diagnostics: bakedDiags }, "syntaxDiag"); } } catch (err) { @@ -502,9 +505,12 @@ namespace ts.server { this.syntacticCheck(checkSpec.fileName, checkSpec.project); next.immediate(() => { this.semanticCheck(checkSpec.fileName, checkSpec.project); - if (checkList.length > index) { - next.delay(followMs, checkOne); - } + next.immediate(() => { + this.codeFixDiagnosticsCheck(checkSpec.fileName, checkSpec.project); + if (checkList.length > index) { + next.delay(followMs, checkOne); + } + }); }); } } @@ -1314,8 +1320,8 @@ namespace ts.server { return !items ? undefined : simplifiedResult - ? this.decorateNavigationBarItems(items, project.getScriptInfoForNormalizedPath(file)) - : items; + ? this.decorateNavigationBarItems(items, project.getScriptInfoForNormalizedPath(file)) + : items; } private decorateNavigationTree(tree: ts.NavigationTree, scriptInfo: ScriptInfo): protocol.NavigationTree { @@ -1341,8 +1347,8 @@ namespace ts.server { return !tree ? undefined : simplifiedResult - ? this.decorateNavigationTree(tree, project.getScriptInfoForNormalizedPath(file)) - : tree; + ? this.decorateNavigationTree(tree, project.getScriptInfoForNormalizedPath(file)) + : tree; } private getNavigateToItems(args: protocol.NavtoRequestArgs, simplifiedResult: boolean): protocol.NavtoItem[] | NavigateToItem[] { @@ -1429,6 +1435,55 @@ namespace ts.server { return ts.getSupportedCodeFixes(); } + private codeFixDiagnosticsCheck(file: NormalizedPath, project: Project): void { + const codeFixDiags = project.getLanguageService().getCodeFixDiagnostics(file); + const diagnostics = codeFixDiags.map(d => formatDiag(file, project, d)); + + this.event({ file, diagnostics }, "codeFixDiag"); + } + + private isLocation(locationOrSpan: protocol.FileLocationOrRangeRequestArgs): locationOrSpan is protocol.FileLocationRequestArgs { + return (locationOrSpan).line !== undefined; + } + + private extractPositionAndRange(args: protocol.FileLocationOrRangeRequestArgs, scriptInfo: ScriptInfo): { position: number, textRange: TextRange } { + let position: number = undefined; + let textRange: TextRange; + if (this.isLocation(args)) { + position = getPosition(args); + } + else { + const { startPosition, endPosition } = this.getStartAndEndPosition(args, scriptInfo); + textRange = { pos: startPosition, end: endPosition }; + } + return { position, textRange }; + + function getPosition(loc: protocol.FileLocationRequestArgs) { + return loc.position !== undefined ? loc.position : scriptInfo.lineOffsetToPosition(loc.line, loc.offset); + } + } + + private getApplicableRefactors(args: protocol.GetApplicableRefactorsRequestArgs): protocol.ApplicableRefactorInfo[] { + const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); + const scriptInfo = project.getScriptInfoForNormalizedPath(file); + const { position, textRange } = this.extractPositionAndRange(args, scriptInfo); + return project.getLanguageService().getApplicableRefactors(file, position || textRange); + } + + private getRefactorCodeActions(args: protocol.GetRefactorCodeActionsRequestArgs): protocol.CodeAction[] { + const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); + const scriptInfo = project.getScriptInfoForNormalizedPath(file); + const { position, textRange } = this.extractPositionAndRange(args, scriptInfo); + + const result = project.getLanguageService().getRefactorCodeActions( + file, + this.projectService.getFormatCodeOptions(), + position || textRange, + args.refactorName + ); + return result ? map(result, action => this.mapCodeAction(action, scriptInfo)) : undefined; + } + private getCodeFixes(args: protocol.CodeFixRequestArgs, simplifiedResult: boolean): protocol.CodeAction[] | CodeAction[] { if (args.errorCodes.length === 0) { return undefined; @@ -1436,8 +1491,7 @@ namespace ts.server { const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); const scriptInfo = project.getScriptInfoForNormalizedPath(file); - const startPosition = getStartPosition(); - const endPosition = getEndPosition(); + const { startPosition, endPosition } = this.getStartAndEndPosition(args, scriptInfo); const formatOptions = this.projectService.getFormatCodeOptions(file); const codeActions = project.getLanguageService().getCodeFixesAtPosition(file, startPosition, endPosition, args.errorCodes, formatOptions); @@ -1450,14 +1504,28 @@ namespace ts.server { else { return codeActions; } + } - function getStartPosition() { - return args.startPosition !== undefined ? args.startPosition : scriptInfo.lineOffsetToPosition(args.startLine, args.startOffset); + private getStartAndEndPosition(args: protocol.FileRangeRequestArgs, scriptInfo: ScriptInfo) { + let startPosition: number = undefined, endPosition: number = undefined; + if (args.startPosition !== undefined ) { + startPosition = args.startPosition; + } + else { + startPosition = scriptInfo.lineOffsetToPosition(args.startLine, args.startOffset); + // save the result so we don't always recompute + args.startPosition = startPosition; } - function getEndPosition() { - return args.endPosition !== undefined ? args.endPosition : scriptInfo.lineOffsetToPosition(args.endLine, args.endOffset); + if (args.endPosition !== undefined) { + endPosition = args.endPosition; + } + else { + endPosition = scriptInfo.lineOffsetToPosition(args.endLine, args.endOffset); + args.endPosition = endPosition; } + + return { startPosition, endPosition }; } private mapCodeAction(codeAction: CodeAction, scriptInfo: ScriptInfo): protocol.CodeAction { @@ -1488,8 +1556,8 @@ namespace ts.server { return !spans ? undefined : simplifiedResult - ? spans.map(span => this.decorateSpan(span, scriptInfo)) - : spans; + ? spans.map(span => this.decorateSpan(span, scriptInfo)) + : spans; } private getDiagnosticsForProject(next: NextStep, delay: number, fileName: string): void { @@ -1790,6 +1858,12 @@ namespace ts.server { }, [CommandNames.GetSupportedCodeFixes]: () => { return this.requiredResponse(this.getSupportedCodeFixes()); + }, + [CommandNames.GetApplicableRefactors]: (request: protocol.GetApplicableRefactorsRequest) => { + return this.requiredResponse(this.getApplicableRefactors(request.arguments)); + }, + [CommandNames.GetRefactorCodeActions]: (request: protocol.GetRefactorCodeActionsRequest) => { + return this.requiredResponse(this.getRefactorCodeActions(request.arguments)); } }); @@ -1847,7 +1921,7 @@ namespace ts.server { let request: protocol.Request; try { request = JSON.parse(message); - const {response, responseRequired} = this.executeCommand(request); + const { response, responseRequired } = this.executeCommand(request); if (this.logger.hasLevel(LogLevel.requestTime)) { const elapsedTime = hrTimeToMilliseconds(this.hrtime(start)).toFixed(4); diff --git a/src/services/codeFixProvider.ts b/src/services/codeFixProvider.ts index bab5356e99c82..a04754ba28b92 100644 --- a/src/services/codeFixProvider.ts +++ b/src/services/codeFixProvider.ts @@ -3,6 +3,14 @@ namespace ts { export interface CodeFix { errorCodes: number[]; getCodeActions(context: CodeFixContext): CodeAction[] | undefined; + createCodeFixDiagnosticIfApplicable?(node: Node, context: CodeFixDiagnoseContext): Diagnostic | undefined; + } + + export interface CodeFixDiagnoseContext { + boundSourceFile: SourceFile; + program: Program; + newLineCharacter: string; + rulesProvider: formatting.RulesProvider; } export interface CodeFixContext { @@ -18,22 +26,38 @@ namespace ts { export namespace codefix { const codeFixes: CodeFix[][] = []; + const diagnosticGeneratingCodeFixes: CodeFix[] = []; - export function registerCodeFix(action: CodeFix) { - forEach(action.errorCodes, error => { + export function registerCodeFix(codeFix: CodeFix) { + forEach(codeFix.errorCodes, error => { let fixes = codeFixes[error]; if (!fixes) { fixes = []; codeFixes[error] = fixes; } - fixes.push(action); + fixes.push(codeFix); }); + + if (codeFix.createCodeFixDiagnosticIfApplicable) { + diagnosticGeneratingCodeFixes.push(codeFix); + } } export function getSupportedErrorCodes() { return Object.keys(codeFixes); } + export function getCodeFixDiagnosticsForNode(context: CodeFixDiagnoseContext, node: Node): Diagnostic[] | undefined { + let result: Diagnostic[]; + for (const codeFix of diagnosticGeneratingCodeFixes) { + const newDiag = codeFix.createCodeFixDiagnosticIfApplicable(node, context); + if (newDiag) { + (result || (result = [])).push(newDiag); + } + } + return result; + } + export function getFixes(context: CodeFixContext): CodeAction[] { const fixes = codeFixes[context.errorCode]; let allActions: CodeAction[] = []; diff --git a/src/services/codefixes/convertFunctionToEs6Class.ts b/src/services/codefixes/convertFunctionToEs6Class.ts new file mode 100644 index 0000000000000..3e3f7ac62bf6e --- /dev/null +++ b/src/services/codefixes/convertFunctionToEs6Class.ts @@ -0,0 +1,191 @@ +/* @internal */ +namespace ts.codefix { + registerCodeFix({ + errorCodes: [Diagnostics.Convert_function_0_to_ES6_class.code], + getCodeActions, + createCodeFixDiagnosticIfApplicable + }); + + function createCodeFixDiagnosticIfApplicable(node: Node, context: CodeFixDiagnoseContext): Diagnostic | undefined { + if (!isSourceFileJavaScript(context.boundSourceFile)) { + return undefined; + } + + const checker = context.program.getTypeChecker(); + const symbol = checker.getSymbolAtLocation(node); + if (isClassLikeSymbol(symbol)) { + return createDiagnosticForNode(node, Diagnostics.Convert_function_0_to_ES6_class, symbol.name); + } + + function isClassLikeSymbol(symbol: Symbol) { + if (!symbol || !symbol.valueDeclaration) { + return false; + } + + let targetSymbol: Symbol; + if (symbol.valueDeclaration.kind === SyntaxKind.FunctionDeclaration) { + targetSymbol = symbol; + } + else if (isDeclarationOfFunctionOrClassExpression(symbol)) { + targetSymbol = (symbol.valueDeclaration as VariableDeclaration).initializer.symbol; + } + + // if there is a prototype property assignment like: + // foo.prototype.method = function () { } + // then the symbol for "foo" will have a member + return targetSymbol && targetSymbol.members && targetSymbol.members.size > 0; + } + } + + function getCodeActions(context: CodeFixContext): CodeAction[] { + const sourceFile = context.sourceFile; + const checker = context.program.getTypeChecker(); + const token = getTokenAtPosition(sourceFile, context.span.start); + const ctorSymbol = checker.getSymbolAtLocation(token); + + const deletes: (() => any)[] = []; + + if (!(ctorSymbol.flags & (SymbolFlags.Function | SymbolFlags.Variable))) { + return []; + } + + const ctorDeclaration = ctorSymbol.valueDeclaration; + const changeTracker = textChanges.ChangeTracker.fromCodeFixContext(context); + + let precedingNode: Node; + let newClassDeclaration: ClassDeclaration; + switch (ctorDeclaration.kind) { + case SyntaxKind.FunctionDeclaration: + precedingNode = ctorDeclaration; + deletes.push(() => changeTracker.deleteNode(sourceFile, ctorDeclaration)); + newClassDeclaration = createClassFromFunctionDeclaration(ctorDeclaration as FunctionDeclaration); + break; + + case SyntaxKind.VariableDeclaration: + precedingNode = ctorDeclaration.parent.parent; + if ((ctorDeclaration.parent).declarations.length === 1) { + deletes.push(() => changeTracker.deleteNode(sourceFile, precedingNode)); + } + else { + deletes.push(() => changeTracker.deleteNodeInList(sourceFile, ctorDeclaration)); + } + newClassDeclaration = createClassFromVariableDeclaration(ctorDeclaration as VariableDeclaration); + break; + } + + if (!newClassDeclaration) { + return []; + } + + // Because the preceding node could be touched, we need to insert nodes before delete nodes. + changeTracker.insertNodeAfter(sourceFile, precedingNode, newClassDeclaration, { suffix: "\n" }); + for (const deleteCallback of deletes) { + deleteCallback(); + } + + return [{ + description: `Convert function ${ctorSymbol.name} to ES6 class`, + changes: changeTracker.getChanges() + }]; + + function createClassElementsFromSymbol(symbol: Symbol) { + const memberElements: ClassElement[] = []; + // all instance members are stored in the "member" array of symbol + if (symbol.members) { + symbol.members.forEach(member => { + const memberElement = createClassElement(member, /*modifiers*/ undefined); + if (memberElement) { + memberElements.push(memberElement); + } + }); + } + + // all static members are stored in the "exports" array of symbol + if (symbol.exports) { + symbol.exports.forEach(member => { + const memberElement = createClassElement(member, [createToken(SyntaxKind.StaticKeyword)]); + if (memberElement) { + memberElements.push(memberElement); + } + }); + } + + return memberElements; + + function createClassElement(symbol: Symbol, modifiers: Modifier[]): ClassElement { + // both properties and methods are bound as property symbols + if (!(symbol.flags & SymbolFlags.Property)) { + return; + } + + const memberDeclaration = symbol.valueDeclaration as PropertyAccessExpression; + const assignmentBinaryExpression = memberDeclaration.parent as BinaryExpression; + + // delete the entire statement if this expression is the sole expression to take care of the semicolon at the end + const nodeToDelete = assignmentBinaryExpression.parent && assignmentBinaryExpression.parent.kind === SyntaxKind.ExpressionStatement + ? assignmentBinaryExpression.parent : assignmentBinaryExpression; + deletes.push(() => changeTracker.deleteNode(sourceFile, nodeToDelete)); + + if (!assignmentBinaryExpression.right) { + return createProperty([], modifiers, symbol.name, /*questionToken*/ undefined, + /*type*/ undefined, /*initializer*/ undefined); + } + + switch (assignmentBinaryExpression.right.kind) { + case SyntaxKind.FunctionExpression: + const functionExpression = assignmentBinaryExpression.right as FunctionExpression; + return createMethodDeclaration(/*decorators*/ undefined, modifiers, /*asteriskToken*/ undefined, memberDeclaration.name, /*questionToken*/ undefined, + /*typeParameters*/ undefined, functionExpression.parameters, /*type*/ undefined, functionExpression.body); + + case SyntaxKind.ArrowFunction: + const arrowFunction = assignmentBinaryExpression.right as ArrowFunction; + const arrowFunctionBody = arrowFunction.body; + let bodyBlock: Block; + + // case 1: () => { return [1,2,3] } + if (arrowFunctionBody.kind === SyntaxKind.Block) { + bodyBlock = arrowFunctionBody as Block; + } + // case 2: () => [1,2,3] + else { + const expression = arrowFunctionBody as Expression; + bodyBlock = createBlock([createReturn(expression)]); + } + return createMethodDeclaration(/*decorators*/ undefined, modifiers, /*asteriskToken*/ undefined, memberDeclaration.name, /*questionToken*/ undefined, + /*typeParameters*/ undefined, arrowFunction.parameters, /*type*/ undefined, bodyBlock); + default: + return createProperty(/*decorators*/ undefined, modifiers, memberDeclaration.name, /*questionToken*/ undefined, + /*type*/ undefined, assignmentBinaryExpression.right); + } + } + } + + function createClassFromVariableDeclaration(node: VariableDeclaration): ClassDeclaration { + const initializer = node.initializer as FunctionExpression; + if (!initializer || initializer.kind !== SyntaxKind.FunctionExpression) { + return undefined; + } + + if (node.name.kind !== SyntaxKind.Identifier) { + return undefined; + } + + const memberElements = createClassElementsFromSymbol(initializer.symbol); + if (initializer.body) { + memberElements.unshift(createConstructor(/*decorators*/ undefined, /*modifiers*/ undefined, initializer.parameters, initializer.body)); + } + + return createClassDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, node.name, + /*typeParameters*/ undefined, /*heritageClauses*/ undefined, memberElements); + } + + function createClassFromFunctionDeclaration(node: FunctionDeclaration): ClassDeclaration { + const memberElements = createClassElementsFromSymbol(ctorSymbol); + if (node.body) { + memberElements.unshift(createConstructor(/*decorators*/ undefined, /*modifiers*/ undefined, node.parameters, node.body)); + } + return createClassDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, node.name, + /*typeParameters*/ undefined, /*heritageClauses*/ undefined, memberElements); + } + } +} \ No newline at end of file diff --git a/src/services/codefixes/fixes.ts b/src/services/codefixes/fixes.ts index ae1643dfa3baa..6f58f16ea0e73 100644 --- a/src/services/codefixes/fixes.ts +++ b/src/services/codefixes/fixes.ts @@ -9,3 +9,4 @@ /// /// /// +/// diff --git a/src/services/refactorProvider.ts b/src/services/refactorProvider.ts new file mode 100644 index 0000000000000..6a13bc3c4e4d5 --- /dev/null +++ b/src/services/refactorProvider.ts @@ -0,0 +1,78 @@ +/* @internal */ +namespace ts { + export interface Refactor { + /** An unique code associated with each refactor */ + name: string; + + /** Description of the refactor to display in the UI of the editor */ + description: string; + + /** Compute the associated code actions */ + getCodeActions(context: RefactorContext, positionOrRange: number | TextRange): CodeAction[]; + + /** A fast syntactic check to see if the refactor is applicable at given position. */ + isApplicableForPositionOrRange(context: LightRefactorContext, positionOrRange: number | TextRange): boolean; + } + + /** + * The `GetApplicableRefactor` API call is supposed to be fast, therefore only syntactic checks should be conducted + * to see if a refactor is applicable. The `LightRefactorContent` limits the context information accesable to the + * refactor to enforce such design. Such context should not provide a bound source file with symbols. + */ + export interface LightRefactorContext { + /** + * The AST that was not bound, so the symbols associated with the nodes are not accessible. + * Such a source file should be cheap to get. + */ + nonBoundSourceFile: SourceFile; + newLineCharacter: string; + } + + export interface RefactorContext { + boundSourceFile: SourceFile; + program: Program; + newLineCharacter: string; + rulesProvider: formatting.RulesProvider; + } + + export namespace refactor { + // A map with the refactor code as key, the refactor itself as value + // e.g. nonSuggestableRefactors[refactorCode] -> the refactor you want + const refactors: Map = createMap(); + + export function registerRefactor(refactor: Refactor) { + refactors.set(refactor.name, refactor); + } + + export function getApplicableRefactors( + context: LightRefactorContext, + positionOrRange: number | TextRange): ApplicableRefactorInfo[] | undefined { + + let results: ApplicableRefactorInfo[]; + refactors.forEach(refactor => { + if (refactor.isApplicableForPositionOrRange(context, positionOrRange)) { + (results || (results = [])).push({ refactorName: refactor.name, description: refactor.description }); + } + }); + return results; + } + + export function getRefactorCodeActions( + context: RefactorContext, + positionOrRange: number | TextRange, + refactorName: string): CodeAction[] | undefined { + + let result: CodeAction[]; + const refactor = refactors.get(refactorName); + if (!refactor) { + return undefined; + } + + const codeActions = refactor.getCodeActions(context, positionOrRange); + if (codeActions) { + addRange((result || (result = [])), codeActions); + } + return result; + } + } +} diff --git a/src/services/refactors/addAsyncSuffix.ts b/src/services/refactors/addAsyncSuffix.ts new file mode 100644 index 0000000000000..470dbb0837bfa --- /dev/null +++ b/src/services/refactors/addAsyncSuffix.ts @@ -0,0 +1,51 @@ +/* @internal */ +namespace ts.refactor { + + const asyncSuffixRefactor: Refactor = { + name: "Add Async suffix", + description: "Add an 'Async' suffix to async function declarations", + getCodeActions, + isApplicableForPositionOrRange + }; + + registerRefactor(asyncSuffixRefactor); + + function getCodeActions(context: RefactorContext, positionOrRange: number | TextRange): CodeAction[] | undefined { + const { boundSourceFile, program } = context; + const tokenPos = typeof positionOrRange === "number" ? positionOrRange : positionOrRange.pos; + const token = getTokenAtPosition(boundSourceFile, tokenPos); + + const functionSymbol = program.getTypeChecker().getSymbolAtLocation(token); + if (!(functionSymbol.flags & SymbolFlags.Function)) { + return undefined; + } + + const oldNameNode = functionSymbol.valueDeclaration.name as Identifier; + const changeTracker = textChanges.ChangeTracker.fromCodeFixContext(context); + + changeTracker.replaceNode(boundSourceFile, oldNameNode, createIdentifier(functionSymbol.name + "Async")); + + return [{ + changes: changeTracker.getChanges(), + description: asyncSuffixRefactor.description + }]; + } + + function isApplicableForPositionOrRange(context: LightRefactorContext, positionOrRange: number | TextRange): boolean { + const { nonBoundSourceFile } = context; + const tokenPos = typeof positionOrRange === "number" ? positionOrRange : positionOrRange.pos; + const token = getTokenAtPosition(nonBoundSourceFile, tokenPos); + + let node = token; + while (node) { + if (node.kind === SyntaxKind.FunctionDeclaration) { + const nameNode = (node).name; + const modifiers = (node).modifiers; + const hasAsyncModifier = modifiers && forEach(modifiers, modifier => modifier.kind === SyntaxKind.AsyncKeyword); + return hasAsyncModifier && nameNode === token && !nameNode.text.match(/.*[aA]sync$/g); + } + node = node.parent; + } + return false; + } +} \ No newline at end of file diff --git a/src/services/refactors/refactors.ts b/src/services/refactors/refactors.ts new file mode 100644 index 0000000000000..585e4f2daf6c3 --- /dev/null +++ b/src/services/refactors/refactors.ts @@ -0,0 +1 @@ +/// diff --git a/src/services/services.ts b/src/services/services.ts index 8ccc1d379d67b..a117fbf4ec486 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -26,7 +26,9 @@ /// /// /// +/// /// +/// namespace ts { /** The version of the language service API */ @@ -1966,11 +1968,60 @@ namespace ts { return Rename.getRenameInfo(program.getTypeChecker(), defaultLibFileName, getCanonicalFileName, getValidSourceFile(fileName), position); } + function getCodeFixDiagnostics(fileName: string): Diagnostic[] { + synchronizeHostData(); + + const newLineCharacter = host.getNewLine(); + const boundSourceFile = getValidSourceFile(fileName); + const program = getProgram(); + const context: CodeFixDiagnoseContext = { boundSourceFile, newLineCharacter, program, rulesProvider: ruleProvider }; + const result: Diagnostic[] = []; + + forEachChild(boundSourceFile, visitor); + return result; + + function visitor(node: Node): void { + const diags = codefix.getCodeFixDiagnosticsForNode(context, node); + if (diags) { + addRange(result, diags); + } + + forEachChild(node, visitor); + } + } + + function getApplicableRefactors(fileName: string, positionOrRange: number | TextRange): ApplicableRefactorInfo[] { + synchronizeHostData(); + const newLineCharacter = host.getNewLine(); + const nonBoundSourceFile = getNonBoundSourceFile(fileName); + const context: LightRefactorContext = { newLineCharacter, nonBoundSourceFile }; + return refactor.getApplicableRefactors(context, positionOrRange); + } + + function getRefactorCodeActions( + fileName: string, + formatOptions: FormatCodeSettings, + positionOrRange: number | TextRange, + refactorName: string): CodeAction[] | undefined { + + const context: RefactorContext = { + boundSourceFile: getValidSourceFile(fileName), + newLineCharacter: host.getNewLine(), + program: getProgram(), + rulesProvider: getRuleProvider(formatOptions) + }; + + return refactor.getRefactorCodeActions(context, positionOrRange, refactorName); + } + return { dispose, cleanupSemanticCache, getSyntacticDiagnostics, getSemanticDiagnostics, + getCodeFixDiagnostics, + getApplicableRefactors, + getRefactorCodeActions, getCompilerOptionsDiagnostics, getSyntacticClassifications, getSemanticClassifications, diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts index d7056cfdca241..e16ae05a5c2de 100644 --- a/src/services/textChanges.ts +++ b/src/services/textChanges.ts @@ -157,7 +157,7 @@ namespace ts.textChanges { private changes: Change[] = []; private readonly newLineCharacter: string; - public static fromCodeFixContext(context: CodeFixContext) { + public static fromCodeFixContext(context: { newLineCharacter: string, rulesProvider: formatting.RulesProvider }) { return new ChangeTracker(context.newLineCharacter === "\n" ? NewLineKind.LineFeed : NewLineKind.CarriageReturnLineFeed, context.rulesProvider); } diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index 6b40c71e20488..af8fac33b9bb1 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -64,34 +64,12 @@ "signatureHelp.ts", "symbolDisplay.ts", "textChanges.ts", - "formatting/formatting.ts", - "formatting/formattingContext.ts", - "formatting/formattingRequestKind.ts", - "formatting/formattingScanner.ts", - "formatting/references.ts", - "formatting/rule.ts", - "formatting/ruleAction.ts", - "formatting/ruleDescriptor.ts", - "formatting/ruleFlag.ts", - "formatting/ruleOperation.ts", - "formatting/ruleOperationContext.ts", - "formatting/rules.ts", - "formatting/rulesMap.ts", - "formatting/rulesProvider.ts", - "formatting/smartIndenter.ts", - "formatting/tokenRange.ts", - "codeFixProvider.ts", - "codefixes/fixAddMissingMember.ts", - "codefixes/fixExtendsInterfaceBecomesImplements.ts", - "codefixes/fixClassIncorrectlyImplementsInterface.ts", - "codefixes/fixClassDoesntImplementInheritedAbstractMember.ts", - "codefixes/fixClassSuperMustPrecedeThisAccess.ts", - "codefixes/fixConstructorForDerivedNeedSuperCall.ts", - "codefixes/fixForgottenThisPropertyAccess.ts", - "codefixes/fixes.ts", - "codefixes/helpers.ts", - "codefixes/importFixes.ts", - "codefixes/unusedIdentifierFixes.ts", - "codefixes/disableJsDiagnostics.ts" + "refactorProvider.ts", + "codeFixProvider.ts" + ], + "include": [ + "formatting/*", + "codefixes/*", + "refactors/*" ] } \ No newline at end of file diff --git a/src/services/types.ts b/src/services/types.ts index 62fbf18cddf3e..2e2eb547152f2 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -198,6 +198,7 @@ namespace ts { getSyntacticDiagnostics(fileName: string): Diagnostic[]; getSemanticDiagnostics(fileName: string): Diagnostic[]; + getCodeFixDiagnostics(fileName: string): Diagnostic[]; // TODO: Rename this to getProgramDiagnostics to better indicate that these are any // diagnostics present for the program level, and not just 'options' diagnostics. @@ -262,6 +263,9 @@ namespace ts { getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[], formatOptions: FormatCodeSettings): CodeAction[]; + getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange): ApplicableRefactorInfo[]; + getRefactorCodeActions(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string): CodeAction[] | undefined; + getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput; getProgram(): Program; @@ -352,6 +356,11 @@ namespace ts { changes: FileTextChanges[]; } + export interface ApplicableRefactorInfo { + refactorName: string; + description: string; + } + export interface TextInsertion { newText: string; /** The position in newText the caret should point to after the insertion. */ diff --git a/tests/cases/fourslash/addAsyncSuffixRefactor.ts b/tests/cases/fourslash/addAsyncSuffixRefactor.ts new file mode 100644 index 0000000000000..28c4da83c66e1 --- /dev/null +++ b/tests/cases/fourslash/addAsyncSuffixRefactor.ts @@ -0,0 +1,16 @@ +/// + +////function /*1*/test() { } +////async function /*2*/test2async() { } +////async function /*3*/test3Async() { } +////async function /*4*/test4() { } + +verify.not.applicableRefactorAvailableAtMarker('1'); +verify.not.applicableRefactorAvailableAtMarker('2'); +verify.not.applicableRefactorAvailableAtMarker('3'); +verify.applicableRefactorAvailableAtMarker('4'); +verify.fileAfterApplyingRefactorAtMarker('4', + `function test() { } +async function test2async() { } +async function test3Async() { } +async function test4Async() { }`, "Add Async suffix"); \ No newline at end of file diff --git a/tests/cases/fourslash/convertFunctionToEs6Class1.ts b/tests/cases/fourslash/convertFunctionToEs6Class1.ts new file mode 100644 index 0000000000000..2d5df7eb6265d --- /dev/null +++ b/tests/cases/fourslash/convertFunctionToEs6Class1.ts @@ -0,0 +1,27 @@ +/// + +// @allowNonTsExtensions: true +// @Filename: test123.js +//// [|function /*1*/foo() { } +//// /*2*/foo.prototype.instanceMethod1 = function() { return "this is name"; }; +//// /*3*/foo.prototype.instanceMethod2 = () => { return "this is name"; }; +//// /*4*/foo.prototype.instanceProp1 = "hello"; +//// /*5*/foo.prototype.instanceProp2 = undefined; +//// /*6*/foo.staticProp = "world"; +//// /*7*/foo.staticMethod1 = function() { return "this is static name"; }; +//// /*8*/foo.staticMethod2 = () => "this is static name";|] + + +verify.codeFixDiagnosticsAvailableAtMarkers(['1', '2', '3', '4', '5', '6', '7', '8']); +verify.rangeAfterCodeFix( +`class foo { + constructor() { } + instanceMethod1() { return "this is name"; } + instanceMethod2() { return "this is name"; } + instanceProp1 = "hello"; + instanceProp2 = undefined; + static staticProp = "world"; + static staticMethod1() { return "this is static name"; } + static staticMethod2() { return "this is static name"; } +} +`, /*includeWhiteSpace*/ true, /*errorCode*/ undefined, /*index*/ 0); \ No newline at end of file diff --git a/tests/cases/fourslash/convertFunctionToEs6Class2.ts b/tests/cases/fourslash/convertFunctionToEs6Class2.ts new file mode 100644 index 0000000000000..1c216c9e2280f --- /dev/null +++ b/tests/cases/fourslash/convertFunctionToEs6Class2.ts @@ -0,0 +1,27 @@ +/// + +// @allowNonTsExtensions: true +// @Filename: test123.js +//// [|var /*1*/foo = function() { }; +//// /*2*/foo.prototype.instanceMethod1 = function() { return "this is name"; }; +//// /*3*/foo.prototype.instanceMethod2 = () => { return "this is name"; }; +//// /*4*/foo.prototype.instanceProp1 = "hello"; +//// /*5*/foo.prototype.instanceProp2 = undefined; +//// /*6*/foo.staticProp = "world"; +//// /*7*/foo.staticMethod1 = function() { return "this is static name"; }; +//// /*8*/foo.staticMethod2 = () => "this is static name";|] + + +verify.codeFixDiagnosticsAvailableAtMarkers(['1', '2', '3', '4', '5', '6', '7', '8']); +verify.rangeAfterCodeFix( +`class foo { + constructor() { } + instanceMethod1() { return "this is name"; } + instanceMethod2() { return "this is name"; } + instanceProp1 = "hello"; + instanceProp2 = undefined; + static staticProp = "world"; + static staticMethod1() { return "this is static name"; } + static staticMethod2() { return "this is static name"; } +} +`, /*includeWhiteSpace*/ true, /*errorCode*/ undefined, /*index*/ 0); \ No newline at end of file diff --git a/tests/cases/fourslash/convertFunctionToEs6Class3.ts b/tests/cases/fourslash/convertFunctionToEs6Class3.ts new file mode 100644 index 0000000000000..ae81aba31f43b --- /dev/null +++ b/tests/cases/fourslash/convertFunctionToEs6Class3.ts @@ -0,0 +1,28 @@ +/// + +// @allowNonTsExtensions: true +// @Filename: test123.js +//// [|var bar = 10, /*1*/foo = function() { }; +//// /*2*/foo.prototype.instanceMethod1 = function() { return "this is name"; }; +//// /*3*/foo.prototype.instanceMethod2 = () => { return "this is name"; }; +//// /*4*/foo.prototype.instanceProp1 = "hello"; +//// /*5*/foo.prototype.instanceProp2 = undefined; +//// /*6*/foo.staticProp = "world"; +//// /*7*/foo.staticMethod1 = function() { return "this is static name"; }; +//// /*8*/foo.staticMethod2 = () => "this is static name";|] + + +verify.codeFixDiagnosticsAvailableAtMarkers(['1', '2', '3', '4', '5', '6', '7', '8']); +verify.rangeAfterCodeFix( +`var bar = 10; +class foo { + constructor() { } + instanceMethod1() { return "this is name"; } + instanceMethod2() { return "this is name"; } + instanceProp1 = "hello"; + instanceProp2 = undefined; + static staticProp = "world"; + static staticMethod1() { return "this is static name"; } + static staticMethod2() { return "this is static name"; } +} +`, /*includeWhiteSpace*/ true, /*errorCode*/ undefined, /*index*/ 0); \ No newline at end of file diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 0e302a649e609..02dbbc41a5d5e 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -148,6 +148,9 @@ declare namespace FourSlashInterface { implementationListIsEmpty(): void; isValidBraceCompletionAtPosition(openingBrace?: string): void; codeFixAvailable(): void; + applicableRefactorAvailableAtMarker(markerName: string): void; + codeFixDiagnosticsAvailableAtMarkers(markerNames: string[], diagnosticCode?: number): void; + applicableRefactorAvailableForRange(): void; } class verify extends verifyNegatable { assertHasRanges(ranges: Range[]): void; @@ -228,6 +231,7 @@ declare namespace FourSlashInterface { DocCommentTemplate(expectedText: string, expectedOffset: number, empty?: boolean): void; noDocCommentTemplate(): void; rangeAfterCodeFix(expectedText: string, includeWhiteSpace?: boolean, errorCode?: number, index?: number): void; + fileAfterApplyingRefactorAtMarker(markerName: string, expectedContent: string, refactorNameToApply: string, formattingOptions?: FormatCodeOptions): void; importFixAtPosition(expectedTextArray: string[], errorCode?: number): void; navigationBar(json: any): void;