Skip to content

Commit

Permalink
add codefix to convert function to es6 class
Browse files Browse the repository at this point in the history
  • Loading branch information
zhengbli committed Apr 19, 2017
1 parent 683a0a0 commit dfa2506
Show file tree
Hide file tree
Showing 11 changed files with 298 additions and 11 deletions.
6 changes: 4 additions & 2 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -3500,8 +3500,6 @@
"category": "Message",
"code": 90021
},


"Octal literal types must use ES2015 syntax. Use the syntax '{0}'.": {
"category": "Error",
"code": 8017
Expand All @@ -3513,5 +3511,9 @@
"Report errors in .js files.": {
"category": "Message",
"code": 8019
},
"Convert function '{0}' to ES6 class.": {
"category": "CodeFix",
"code": 100000
}
}
2 changes: 1 addition & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3349,7 +3349,7 @@ namespace ts {
Warning,
Error,
Message,
Refactor
CodeFix
}

export enum ModuleResolutionKind {
Expand Down
12 changes: 11 additions & 1 deletion src/harness/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,16 +481,26 @@ 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[] {
return this.languageService.getCodeFixDiagnostics(fileName);
let result: ts.Diagnostic[];

try {
result = this.languageService.getCodeFixDiagnostics(fileName);
}
catch(e) {
result = [];
}
return result;
}

private getAllDiagnostics(): ts.Diagnostic[] {
Expand Down
2 changes: 1 addition & 1 deletion src/harness/unittests/tsserverProjectSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3631,7 +3631,7 @@ namespace ts.projectSystem {
host.runQueuedImmediateCallbacks();
assert.equal(host.getOutput().length, 2, "expect 2 messages");
const e3 = <protocol.Event>getMessage(0);
assert.equal(e3.event, "refactorDiag");
assert.equal(e3.event, "codeFixDiag");
verifyRequestCompleted(getErrId, 1);

cancellationToken.resetToken();
Expand Down
10 changes: 5 additions & 5 deletions src/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ namespace ts.server {
next.immediate(() => {
this.semanticCheck(checkSpec.fileName, checkSpec.project);
next.immediate(() => {
this.refactorDiagnosticsCheck(checkSpec.fileName, checkSpec.project);
this.codeFixDiagnosticsCheck(checkSpec.fileName, checkSpec.project);
if (checkList.length > index) {
next.delay(followMs, checkOne);
}
Expand Down Expand Up @@ -1435,11 +1435,11 @@ namespace ts.server {
return ts.getSupportedCodeFixes();
}

private refactorDiagnosticsCheck(file: NormalizedPath, project: Project): void {
const refactorDiags = project.getLanguageService().getCodeFixDiagnostics(file);
const diagnostics = refactorDiags.map(d => formatDiag(file, project, d));
private codeFixDiagnosticsCheck(file: NormalizedPath, project: Project): void {
const codeFixDiags = project.getLanguageService().getCodeFixDiagnostics(file);
const diagnostics = codeFixDiags.map(d => formatDiag(file, project, d));

this.event<protocol.DiagnosticEventBody>({ file, diagnostics }, "refactorDiag");
this.event<protocol.DiagnosticEventBody>({ file, diagnostics }, "codeFixDiag");
}

private isLocation(locationOrSpan: protocol.FileLocationOrRangeRequestArgs): locationOrSpan is protocol.FileLocationRequestArgs {
Expand Down
191 changes: 191 additions & 0 deletions src/services/codefixes/convertFunctionToEs6Class.ts
Original file line number Diff line number Diff line change
@@ -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 ((<VariableDeclarationList>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);
}
}
}
1 change: 1 addition & 0 deletions src/services/codefixes/fixes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
/// <reference path='importFixes.ts' />
/// <reference path='disableJsDiagnostics.ts' />
/// <reference path='helpers.ts' />
/// <reference path="convertFunctionToEs6Class.ts" />
3 changes: 2 additions & 1 deletion src/services/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
"codefixes/helpers.ts",
"codefixes/importFixes.ts",
"codefixes/unusedIdentifierFixes.ts",
"codefixes/disableJsDiagnostics.ts"
"codefixes/disableJsDiagnostics.ts",
"codefixes/convertFunctionToEs6Class.ts"
]
}
27 changes: 27 additions & 0 deletions tests/cases/fourslash/convertFunctionToEs6Class1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/// <reference path='fourslash.ts' />

// @allowNonTsExtensions: true
// @Filename: test123.js
//// [|function foo() { }
//// foo.prototype.instanceMethod1 = function() { return "this is name"; };
//// foo.prototype.instanceMethod2 = () => { return "this is name"; };
//// foo.prototype.instanceProp1 = "hello";
//// foo.prototype.instanceProp2 = undefined;
//// foo.staticProp = "world";
//// foo.staticMethod1 = function() { return "this is static name"; };
//// foo.staticMethod2 = () => "this is static name";|]


verify.codeFixAvailable();
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);
27 changes: 27 additions & 0 deletions tests/cases/fourslash/convertFunctionToEs6Class2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/// <reference path='fourslash.ts' />

// @allowNonTsExtensions: true
// @Filename: test123.js
//// [|var foo = function() { };
//// foo.prototype.instanceMethod1 = function() { return "this is name"; };
//// foo.prototype.instanceMethod2 = () => { return "this is name"; };
//// foo.prototype.instanceProp1 = "hello";
//// foo.prototype.instanceProp2 = undefined;
//// foo.staticProp = "world";
//// foo.staticMethod1 = function() { return "this is static name"; };
//// foo.staticMethod2 = () => "this is static name";|]


verify.codeFixAvailable();
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);
28 changes: 28 additions & 0 deletions tests/cases/fourslash/convertFunctionToEs6Class3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/// <reference path='fourslash.ts' />

// @allowNonTsExtensions: true
// @Filename: test123.js
//// [|var bar = 10, foo = function() { };
//// foo.prototype.instanceMethod1 = function() { return "this is name"; };
//// foo.prototype.instanceMethod2 = () => { return "this is name"; };
//// foo.prototype.instanceProp1 = "hello";
//// foo.prototype.instanceProp2 = undefined;
//// foo.staticProp = "world";
//// foo.staticMethod1 = function() { return "this is static name"; };
//// foo.staticMethod2 = () => "this is static name";|]


verify.codeFixAvailable();
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);

0 comments on commit dfa2506

Please sign in to comment.