Skip to content

Commit a6f34a9

Browse files
committed
add codefix to convert function to es6 class
1 parent 683a0a0 commit a6f34a9

File tree

10 files changed

+297
-10
lines changed

10 files changed

+297
-10
lines changed

src/compiler/diagnosticMessages.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3500,8 +3500,6 @@
35003500
"category": "Message",
35013501
"code": 90021
35023502
},
3503-
3504-
35053503
"Octal literal types must use ES2015 syntax. Use the syntax '{0}'.": {
35063504
"category": "Error",
35073505
"code": 8017
@@ -3513,5 +3511,9 @@
35133511
"Report errors in .js files.": {
35143512
"category": "Message",
35153513
"code": 8019
3514+
},
3515+
"Convert function '{0}' to ES6 class.": {
3516+
"category": "CodeFix",
3517+
"code": 100000
35163518
}
35173519
}

src/harness/fourslash.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -481,16 +481,26 @@ namespace FourSlash {
481481
private getDiagnostics(fileName: string): ts.Diagnostic[] {
482482
const syntacticErrors = this.languageService.getSyntacticDiagnostics(fileName);
483483
const semanticErrors = this.languageService.getSemanticDiagnostics(fileName);
484+
const codeFixDiagnostics = this.getCodeFixDiagnostics(fileName);
484485

485486
const diagnostics: ts.Diagnostic[] = [];
486487
diagnostics.push.apply(diagnostics, syntacticErrors);
487488
diagnostics.push.apply(diagnostics, semanticErrors);
489+
diagnostics.push.apply(diagnostics, codeFixDiagnostics);
488490

489491
return diagnostics;
490492
}
491493

492494
private getCodeFixDiagnostics(fileName: string): ts.Diagnostic[] {
493-
return this.languageService.getCodeFixDiagnostics(fileName);
495+
let result: ts.Diagnostic[];
496+
497+
try {
498+
result = this.languageService.getCodeFixDiagnostics(fileName);
499+
}
500+
catch(e) {
501+
result = [];
502+
}
503+
return result;
494504
}
495505

496506
private getAllDiagnostics(): ts.Diagnostic[] {

src/harness/unittests/tsserverProjectSystem.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3631,7 +3631,7 @@ namespace ts.projectSystem {
36313631
host.runQueuedImmediateCallbacks();
36323632
assert.equal(host.getOutput().length, 2, "expect 2 messages");
36333633
const e3 = <protocol.Event>getMessage(0);
3634-
assert.equal(e3.event, "refactorDiag");
3634+
assert.equal(e3.event, "codeFixDiag");
36353635
verifyRequestCompleted(getErrId, 1);
36363636

36373637
cancellationToken.resetToken();

src/server/session.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,7 @@ namespace ts.server {
506506
next.immediate(() => {
507507
this.semanticCheck(checkSpec.fileName, checkSpec.project);
508508
next.immediate(() => {
509-
this.refactorDiagnosticsCheck(checkSpec.fileName, checkSpec.project);
509+
this.codeFixDiagnosticsCheck(checkSpec.fileName, checkSpec.project);
510510
if (checkList.length > index) {
511511
next.delay(followMs, checkOne);
512512
}
@@ -1435,11 +1435,11 @@ namespace ts.server {
14351435
return ts.getSupportedCodeFixes();
14361436
}
14371437

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

1442-
this.event<protocol.DiagnosticEventBody>({ file, diagnostics }, "refactorDiag");
1442+
this.event<protocol.DiagnosticEventBody>({ file, diagnostics }, "codeFixDiag");
14431443
}
14441444

14451445
private isLocation(locationOrSpan: protocol.FileLocationOrRangeRequestArgs): locationOrSpan is protocol.FileLocationRequestArgs {
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/* @internal */
2+
namespace ts.codefix {
3+
registerCodeFix({
4+
errorCodes: [Diagnostics.Convert_function_0_to_ES6_class.code],
5+
getCodeActions,
6+
createCodeFixDiagnosticIfApplicable
7+
});
8+
9+
function createCodeFixDiagnosticIfApplicable(node: Node, context: CodeFixDiagnoseContext): Diagnostic | undefined {
10+
if (!isSourceFileJavaScript(context.boundSourceFile)) {
11+
return undefined;
12+
}
13+
14+
const checker = context.program.getTypeChecker();
15+
const symbol = checker.getSymbolAtLocation(node);
16+
if (isClassLikeSymbol(symbol)) {
17+
return createDiagnosticForNode(node, Diagnostics.Convert_function_0_to_ES6_class, symbol.name);
18+
}
19+
20+
function isClassLikeSymbol(symbol: Symbol) {
21+
if (!symbol || !symbol.valueDeclaration) {
22+
return false;
23+
}
24+
25+
let targetSymbol: Symbol;
26+
if (symbol.valueDeclaration.kind === SyntaxKind.FunctionDeclaration) {
27+
targetSymbol = symbol;
28+
}
29+
else if (isDeclarationOfFunctionOrClassExpression(symbol)) {
30+
targetSymbol = (symbol.valueDeclaration as VariableDeclaration).initializer.symbol;
31+
}
32+
33+
// if there is a prototype property assignment like:
34+
// foo.prototype.method = function () { }
35+
// then the symbol for "foo" will have a member
36+
return targetSymbol && targetSymbol.members && targetSymbol.members.size > 0;
37+
}
38+
}
39+
40+
function getCodeActions(context: CodeFixContext): CodeAction[] {
41+
const sourceFile = context.sourceFile;
42+
const checker = context.program.getTypeChecker();
43+
const token = getTokenAtPosition(sourceFile, context.span.start);
44+
const ctorSymbol = checker.getSymbolAtLocation(token);
45+
46+
const deletes: (() => any)[] = [];
47+
48+
if (!(ctorSymbol.flags & (SymbolFlags.Function | SymbolFlags.Variable))) {
49+
return [];
50+
}
51+
52+
const ctorDeclaration = ctorSymbol.valueDeclaration;
53+
const changeTracker = textChanges.ChangeTracker.fromCodeFixContext(context);
54+
55+
let precedingNode: Node;
56+
let newClassDeclaration: ClassDeclaration;
57+
switch (ctorDeclaration.kind) {
58+
case SyntaxKind.FunctionDeclaration:
59+
precedingNode = ctorDeclaration;
60+
deletes.push(() => changeTracker.deleteNode(sourceFile, ctorDeclaration));
61+
newClassDeclaration = createClassFromFunctionDeclaration(ctorDeclaration as FunctionDeclaration);
62+
break;
63+
64+
case SyntaxKind.VariableDeclaration:
65+
precedingNode = ctorDeclaration.parent.parent;
66+
if ((<VariableDeclarationList>ctorDeclaration.parent).declarations.length === 1) {
67+
deletes.push(() => changeTracker.deleteNode(sourceFile, precedingNode));
68+
}
69+
else {
70+
deletes.push(() => changeTracker.deleteNodeInList(sourceFile, ctorDeclaration));
71+
}
72+
newClassDeclaration = createClassFromVariableDeclaration(ctorDeclaration as VariableDeclaration);
73+
break;
74+
}
75+
76+
if (!newClassDeclaration) {
77+
return [];
78+
}
79+
80+
// Because the preceding node could be touched, we need to insert nodes before delete nodes.
81+
changeTracker.insertNodeAfter(sourceFile, precedingNode, newClassDeclaration, { suffix: "\n" });
82+
for (const deleteCallback of deletes) {
83+
deleteCallback();
84+
}
85+
86+
return [{
87+
description: `Convert function ${ctorSymbol.name} to ES6 class`,
88+
changes: changeTracker.getChanges()
89+
}];
90+
91+
function createClassElementsFromSymbol(symbol: Symbol) {
92+
const memberElements: ClassElement[] = [];
93+
// all instance members are stored in the "member" array of symbol
94+
if (symbol.members) {
95+
symbol.members.forEach(member => {
96+
const memberElement = createClassElement(member, /*modifiers*/ undefined);
97+
if (memberElement) {
98+
memberElements.push(memberElement);
99+
}
100+
});
101+
}
102+
103+
// all static members are stored in the "exports" array of symbol
104+
if (symbol.exports) {
105+
symbol.exports.forEach(member => {
106+
const memberElement = createClassElement(member, [createToken(SyntaxKind.StaticKeyword)]);
107+
if (memberElement) {
108+
memberElements.push(memberElement);
109+
}
110+
});
111+
}
112+
113+
return memberElements;
114+
115+
function createClassElement(symbol: Symbol, modifiers: Modifier[]): ClassElement {
116+
// both properties and methods are bound as property symbols
117+
if (!(symbol.flags & SymbolFlags.Property)) {
118+
return;
119+
}
120+
121+
const memberDeclaration = symbol.valueDeclaration as PropertyAccessExpression;
122+
const assignmentBinaryExpression = memberDeclaration.parent as BinaryExpression;
123+
124+
// delete the entire statement if this expression is the sole expression to take care of the semicolon at the end
125+
const nodeToDelete = assignmentBinaryExpression.parent && assignmentBinaryExpression.parent.kind === SyntaxKind.ExpressionStatement
126+
? assignmentBinaryExpression.parent : assignmentBinaryExpression;
127+
deletes.push(() => changeTracker.deleteNode(sourceFile, nodeToDelete));
128+
129+
if (!assignmentBinaryExpression.right) {
130+
return createProperty([], modifiers, symbol.name, /*questionToken*/ undefined,
131+
/*type*/ undefined, /*initializer*/ undefined);
132+
}
133+
134+
switch (assignmentBinaryExpression.right.kind) {
135+
case SyntaxKind.FunctionExpression:
136+
const functionExpression = assignmentBinaryExpression.right as FunctionExpression;
137+
return createMethodDeclaration(/*decorators*/ undefined, modifiers, /*asteriskToken*/ undefined, memberDeclaration.name, /*questionToken*/ undefined,
138+
/*typeParameters*/ undefined, functionExpression.parameters, /*type*/ undefined, functionExpression.body);
139+
140+
case SyntaxKind.ArrowFunction:
141+
const arrowFunction = assignmentBinaryExpression.right as ArrowFunction;
142+
const arrowFunctionBody = arrowFunction.body;
143+
let bodyBlock: Block;
144+
145+
// case 1: () => { return [1,2,3] }
146+
if (arrowFunctionBody.kind === SyntaxKind.Block) {
147+
bodyBlock = arrowFunctionBody as Block;
148+
}
149+
// case 2: () => [1,2,3]
150+
else {
151+
const expression = arrowFunctionBody as Expression;
152+
bodyBlock = createBlock([createReturn(expression)]);
153+
}
154+
return createMethodDeclaration(/*decorators*/ undefined, modifiers, /*asteriskToken*/ undefined, memberDeclaration.name, /*questionToken*/ undefined,
155+
/*typeParameters*/ undefined, arrowFunction.parameters, /*type*/ undefined, bodyBlock);
156+
default:
157+
return createProperty(/*decorators*/ undefined, modifiers, memberDeclaration.name, /*questionToken*/ undefined,
158+
/*type*/ undefined, assignmentBinaryExpression.right);
159+
}
160+
}
161+
}
162+
163+
function createClassFromVariableDeclaration(node: VariableDeclaration): ClassDeclaration {
164+
const initializer = node.initializer as FunctionExpression;
165+
if (!initializer || initializer.kind !== SyntaxKind.FunctionExpression) {
166+
return undefined;
167+
}
168+
169+
if (node.name.kind !== SyntaxKind.Identifier) {
170+
return undefined;
171+
}
172+
173+
const memberElements = createClassElementsFromSymbol(initializer.symbol);
174+
if (initializer.body) {
175+
memberElements.unshift(createConstructor(/*decorators*/ undefined, /*modifiers*/ undefined, initializer.parameters, initializer.body));
176+
}
177+
178+
return createClassDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, node.name,
179+
/*typeParameters*/ undefined, /*heritageClauses*/ undefined, memberElements);
180+
}
181+
182+
function createClassFromFunctionDeclaration(node: FunctionDeclaration): ClassDeclaration {
183+
const memberElements = createClassElementsFromSymbol(ctorSymbol);
184+
if (node.body) {
185+
memberElements.unshift(createConstructor(/*decorators*/ undefined, /*modifiers*/ undefined, node.parameters, node.body));
186+
}
187+
return createClassDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, node.name,
188+
/*typeParameters*/ undefined, /*heritageClauses*/ undefined, memberElements);
189+
}
190+
}
191+
}

src/services/codefixes/fixes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@
99
/// <reference path='importFixes.ts' />
1010
/// <reference path='disableJsDiagnostics.ts' />
1111
/// <reference path='helpers.ts' />
12+
/// <reference path="convertFunctionToEs6Class.ts" />

src/services/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
"codefixes/helpers.ts",
9494
"codefixes/importFixes.ts",
9595
"codefixes/unusedIdentifierFixes.ts",
96-
"codefixes/disableJsDiagnostics.ts"
96+
"codefixes/disableJsDiagnostics.ts",
97+
"codefixes/convertFunctionToEs6Class.ts"
9798
]
9899
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @allowNonTsExtensions: true
4+
// @Filename: test123.js
5+
//// [|function foo() { }
6+
//// foo.prototype.instanceMethod1 = function() { return "this is name"; };
7+
//// foo.prototype.instanceMethod2 = () => { return "this is name"; };
8+
//// foo.prototype.instanceProp1 = "hello";
9+
//// foo.prototype.instanceProp2 = undefined;
10+
//// foo.staticProp = "world";
11+
//// foo.staticMethod1 = function() { return "this is static name"; };
12+
//// foo.staticMethod2 = () => "this is static name";|]
13+
14+
15+
verify.codeFixAvailable();
16+
verify.rangeAfterCodeFix(
17+
`class foo {
18+
constructor() { }
19+
instanceMethod1() { return "this is name"; }
20+
instanceMethod2() { return "this is name"; }
21+
instanceProp1 = "hello";
22+
instanceProp2 = undefined;
23+
static staticProp = "world";
24+
static staticMethod1() { return "this is static name"; }
25+
static staticMethod2() { return "this is static name"; }
26+
}
27+
`, /*includeWhiteSpace*/ true, /*errorCode*/ undefined, /*index*/ 0);
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @allowNonTsExtensions: true
4+
// @Filename: test123.js
5+
//// [|var foo = function() { };
6+
//// foo.prototype.instanceMethod1 = function() { return "this is name"; };
7+
//// foo.prototype.instanceMethod2 = () => { return "this is name"; };
8+
//// foo.prototype.instanceProp1 = "hello";
9+
//// foo.prototype.instanceProp2 = undefined;
10+
//// foo.staticProp = "world";
11+
//// foo.staticMethod1 = function() { return "this is static name"; };
12+
//// foo.staticMethod2 = () => "this is static name";|]
13+
14+
15+
verify.codeFixAvailable();
16+
verify.rangeAfterCodeFix(
17+
`class foo {
18+
constructor() { }
19+
instanceMethod1() { return "this is name"; }
20+
instanceMethod2() { return "this is name"; }
21+
instanceProp1 = "hello";
22+
instanceProp2 = undefined;
23+
static staticProp = "world";
24+
static staticMethod1() { return "this is static name"; }
25+
static staticMethod2() { return "this is static name"; }
26+
}
27+
`, /*includeWhiteSpace*/ true, /*errorCode*/ undefined, /*index*/ 0);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @allowNonTsExtensions: true
4+
// @Filename: test123.js
5+
//// [|var bar = 10, foo = function() { };
6+
//// foo.prototype.instanceMethod1 = function() { return "this is name"; };
7+
//// foo.prototype.instanceMethod2 = () => { return "this is name"; };
8+
//// foo.prototype.instanceProp1 = "hello";
9+
//// foo.prototype.instanceProp2 = undefined;
10+
//// foo.staticProp = "world";
11+
//// foo.staticMethod1 = function() { return "this is static name"; };
12+
//// foo.staticMethod2 = () => "this is static name";|]
13+
14+
15+
verify.codeFixAvailable();
16+
verify.rangeAfterCodeFix(
17+
`var bar = 10;
18+
class foo {
19+
constructor() { }
20+
instanceMethod1() { return "this is name"; }
21+
instanceMethod2() { return "this is name"; }
22+
instanceProp1 = "hello";
23+
instanceProp2 = undefined;
24+
static staticProp = "world";
25+
static staticMethod1() { return "this is static name"; }
26+
static staticMethod2() { return "this is static name"; }
27+
}
28+
`, /*includeWhiteSpace*/ true, /*errorCode*/ undefined, /*index*/ 0);

0 commit comments

Comments
 (0)