Skip to content

Commit 87c3cca

Browse files
author
Andy
authored
Make convertFunctionToEs6Class a codefix (#22241)
* Make convertFunctionToEs6Class a codefix * Change diagnostic message
1 parent 81c313e commit 87c3cca

16 files changed

+160
-161
lines changed

src/compiler/diagnosticMessages.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3822,6 +3822,10 @@
38223822
"category": "Suggestion",
38233823
"code": 80001
38243824
},
3825+
"This constructor function may be converted to a class declaration.": {
3826+
"category": "Suggestion",
3827+
"code": 80002
3828+
},
38253829

38263830
"Add missing 'super()' call": {
38273831
"category": "Message",

src/services/refactors/convertFunctionToEs6Class.ts renamed to src/services/codefixes/convertFunctionToEs6Class.ts

Lines changed: 41 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,28 @@
11
/* @internal */
2-
3-
namespace ts.refactor.convertFunctionToES6Class {
4-
const refactorName = "Convert to ES2015 class";
5-
const actionName = "convert";
6-
const description = Diagnostics.Convert_function_to_an_ES2015_class.message;
7-
registerRefactor(refactorName, { getEditsForAction, getAvailableActions });
8-
9-
function getAvailableActions(context: RefactorContext): ApplicableRefactorInfo[] | undefined {
10-
if (!isInJavaScriptFile(context.file)) {
11-
return undefined;
12-
}
13-
14-
let symbol = getConstructorSymbol(context);
15-
if (!symbol) {
16-
return undefined;
17-
}
18-
19-
if (isDeclarationOfFunctionOrClassExpression(symbol)) {
20-
symbol = (symbol.valueDeclaration as VariableDeclaration).initializer.symbol;
21-
}
22-
23-
if ((symbol.flags & SymbolFlags.Function) && symbol.members && (symbol.members.size > 0)) {
24-
return [
25-
{
26-
name: refactorName,
27-
description,
28-
actions: [
29-
{
30-
description,
31-
name: actionName
32-
}
33-
]
34-
}
35-
];
36-
}
37-
}
38-
39-
function getEditsForAction(context: RefactorContext, action: string): RefactorEditInfo | undefined {
40-
// Somehow wrong action got invoked?
41-
if (actionName !== action) {
42-
return undefined;
43-
}
44-
45-
const { file: sourceFile } = context;
46-
const ctorSymbol = getConstructorSymbol(context);
47-
2+
namespace ts.codefix {
3+
const fixId = "convertFunctionToEs6Class";
4+
const errorCodes = [Diagnostics.This_constructor_function_may_be_converted_to_a_class_declaration.code];
5+
registerCodeFix({
6+
errorCodes,
7+
getCodeActions(context: CodeFixContext) {
8+
const changes = textChanges.ChangeTracker.with(context, t => doChange(t, context.sourceFile, context.span.start, context.program.getTypeChecker()));
9+
return [{ description: getLocaleSpecificMessage(Diagnostics.Convert_function_to_an_ES2015_class), changes, fixId }];
10+
},
11+
fixIds: [fixId],
12+
getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, err) => doChange(changes, err.file!, err.start, context.program.getTypeChecker())),
13+
});
14+
15+
function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, position: number, checker: TypeChecker): void {
4816
const deletedNodes: Node[] = [];
49-
const deletes: (() => any)[] = [];
17+
const deletes: (() => void)[] = [];
18+
const ctorSymbol = checker.getSymbolAtLocation(getTokenAtPosition(sourceFile, position, /*includeJsDocComment*/ false));
5019

51-
if (!(ctorSymbol.flags & (SymbolFlags.Function | SymbolFlags.Variable))) {
20+
if (!ctorSymbol || !(ctorSymbol.flags & (SymbolFlags.Function | SymbolFlags.Variable))) {
21+
// Bad input
5222
return undefined;
5323
}
5424

5525
const ctorDeclaration = ctorSymbol.valueDeclaration;
56-
const changeTracker = textChanges.ChangeTracker.fromContext(context);
5726

5827
let precedingNode: Node;
5928
let newClassDeclaration: ClassDeclaration;
@@ -81,28 +50,22 @@ namespace ts.refactor.convertFunctionToES6Class {
8150
}
8251

8352
// Because the preceding node could be touched, we need to insert nodes before delete nodes.
84-
changeTracker.insertNodeAfter(sourceFile, precedingNode, newClassDeclaration);
53+
changes.insertNodeAfter(sourceFile, precedingNode, newClassDeclaration);
8554
for (const deleteCallback of deletes) {
8655
deleteCallback();
8756
}
8857

89-
return {
90-
edits: changeTracker.getChanges(),
91-
renameFilename: undefined,
92-
renameLocation: undefined,
93-
};
94-
9558
function deleteNode(node: Node, inList = false) {
9659
if (deletedNodes.some(n => isNodeDescendantOf(node, n))) {
9760
// Parent node has already been deleted; do nothing
9861
return;
9962
}
10063
deletedNodes.push(node);
10164
if (inList) {
102-
deletes.push(() => changeTracker.deleteNodeInList(sourceFile, node));
65+
deletes.push(() => changes.deleteNodeInList(sourceFile, node));
10366
}
10467
else {
105-
deletes.push(() => changeTracker.deleteNode(sourceFile, node));
68+
deletes.push(() => changes.deleteNode(sourceFile, node));
10669
}
10770
}
10871

@@ -165,7 +128,7 @@ namespace ts.refactor.convertFunctionToES6Class {
165128
const fullModifiers = concatenate(modifiers, getModifierKindFromSource(functionExpression, SyntaxKind.AsyncKeyword));
166129
const method = createMethod(/*decorators*/ undefined, fullModifiers, /*asteriskToken*/ undefined, memberDeclaration.name, /*questionToken*/ undefined,
167130
/*typeParameters*/ undefined, functionExpression.parameters, /*type*/ undefined, functionExpression.body);
168-
copyComments(assignmentBinaryExpression, method);
131+
copyComments(assignmentBinaryExpression, method, sourceFile);
169132
return method;
170133
}
171134

@@ -185,7 +148,7 @@ namespace ts.refactor.convertFunctionToES6Class {
185148
const fullModifiers = concatenate(modifiers, getModifierKindFromSource(arrowFunction, SyntaxKind.AsyncKeyword));
186149
const method = createMethod(/*decorators*/ undefined, fullModifiers, /*asteriskToken*/ undefined, memberDeclaration.name, /*questionToken*/ undefined,
187150
/*typeParameters*/ undefined, arrowFunction.parameters, /*type*/ undefined, bodyBlock);
188-
copyComments(assignmentBinaryExpression, method);
151+
copyComments(assignmentBinaryExpression, method, sourceFile);
189152
return method;
190153
}
191154

@@ -196,29 +159,13 @@ namespace ts.refactor.convertFunctionToES6Class {
196159
}
197160
const prop = createProperty(/*decorators*/ undefined, modifiers, memberDeclaration.name, /*questionToken*/ undefined,
198161
/*type*/ undefined, assignmentBinaryExpression.right);
199-
copyComments(assignmentBinaryExpression.parent, prop);
162+
copyComments(assignmentBinaryExpression.parent, prop, sourceFile);
200163
return prop;
201164
}
202165
}
203166
}
204167
}
205168

206-
function copyComments(sourceNode: Node, targetNode: Node) {
207-
forEachLeadingCommentRange(sourceFile.text, sourceNode.pos, (pos, end, kind, htnl) => {
208-
if (kind === SyntaxKind.MultiLineCommentTrivia) {
209-
// Remove leading /*
210-
pos += 2;
211-
// Remove trailing */
212-
end -= 2;
213-
}
214-
else {
215-
// Remove leading //
216-
pos += 2;
217-
}
218-
addSyntheticLeadingComment(targetNode, kind, sourceFile.text.slice(pos, end), htnl);
219-
});
220-
}
221-
222169
function createClassFromVariableDeclaration(node: VariableDeclaration): ClassDeclaration {
223170
const initializer = node.initializer as FunctionExpression;
224171
if (!initializer || initializer.kind !== SyntaxKind.FunctionExpression) {
@@ -253,15 +200,25 @@ namespace ts.refactor.convertFunctionToES6Class {
253200
// Don't call copyComments here because we'll already leave them in place
254201
return cls;
255202
}
203+
}
256204

257-
function getModifierKindFromSource(source: Node, kind: SyntaxKind) {
258-
return filter(source.modifiers, modifier => modifier.kind === kind);
259-
}
205+
function copyComments(sourceNode: Node, targetNode: Node, sourceFile: SourceFile) {
206+
forEachLeadingCommentRange(sourceFile.text, sourceNode.pos, (pos, end, kind, htnl) => {
207+
if (kind === SyntaxKind.MultiLineCommentTrivia) {
208+
// Remove leading /*
209+
pos += 2;
210+
// Remove trailing */
211+
end -= 2;
212+
}
213+
else {
214+
// Remove leading //
215+
pos += 2;
216+
}
217+
addSyntheticLeadingComment(targetNode, kind, sourceFile.text.slice(pos, end), htnl);
218+
});
260219
}
261220

262-
function getConstructorSymbol({ startPosition, file, program }: RefactorContext): Symbol {
263-
const checker = program.getTypeChecker();
264-
const token = getTokenAtPosition(file, startPosition, /*includeJsDocComment*/ false);
265-
return checker.getSymbolAtLocation(token);
221+
function getModifierKindFromSource(source: Node, kind: SyntaxKind): ReadonlyArray<Modifier> {
222+
return filter(source.modifiers, modifier => modifier.kind === kind);
266223
}
267224
}

src/services/codefixes/fixes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/// <reference path="addMissingInvocationForDecorator.ts" />
2+
/// <reference path="convertFunctionToEs6Class.ts" />
23
/// <reference path="convertToEs6Module.ts" />
34
/// <reference path="correctQualifiedNameToIndexedAccessType.ts" />
45
/// <reference path="fixClassIncorrectlyImplementsInterface.ts" />

src/services/refactors/refactors.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
/// <reference path="annotateWithTypeFromJSDoc.ts" />
2-
/// <reference path="convertFunctionToEs6Class.ts" />
32
/// <reference path="extractSymbol.ts" />
43
/// <reference path="useDefaultImport.ts" />

src/services/suggestionDiagnostics.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,22 @@ namespace ts {
99
diags.push(createDiagnosticForNode(sourceFile.commonJsModuleIndicator, Diagnostics.File_is_a_CommonJS_module_it_may_be_converted_to_an_ES6_module));
1010
}
1111

12+
function check(node: Node) {
13+
switch (node.kind) {
14+
case SyntaxKind.FunctionDeclaration:
15+
case SyntaxKind.FunctionExpression:
16+
const symbol = node.symbol;
17+
if (symbol.members && (symbol.members.size > 0)) {
18+
diags.push(createDiagnosticForNode(isVariableDeclaration(node.parent) ? node.parent.name : node, Diagnostics.This_constructor_function_may_be_converted_to_a_class_declaration));
19+
}
20+
break;
21+
}
22+
node.forEachChild(check);
23+
}
24+
if (isInJavaScriptFile(sourceFile)) {
25+
check(sourceFile);
26+
}
27+
1228
return diags.concat(checker.getSuggestionDiagnostics(sourceFile));
1329
}
1430
}

tests/cases/fourslash/convertFunctionToEs6Class1.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,24 @@
22

33
// @allowNonTsExtensions: true
44
// @Filename: test123.js
5-
//// [|function /*1*/foo() { }
6-
//// /*2*/foo.prototype.instanceMethod1 = function() { return "this is name"; };
7-
//// /*3*/foo.prototype.instanceMethod2 = () => { return "this is name"; };
8-
//// /*4*/foo.prototype.instanceProp1 = "hello";
9-
//// /*5*/foo.prototype.instanceProp2 = undefined;
10-
//// /*6*/foo.staticProp = "world";
11-
//// /*7*/foo.staticMethod1 = function() { return "this is static name"; };
12-
//// /*8*/foo.staticMethod2 = () => "this is static name";|]
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";
1313

14-
['1', '2', '3', '4', '5', '6', '7', '8'].forEach(m => verify.applicableRefactorAvailableAtMarker(m));
15-
verify.fileAfterApplyingRefactorAtMarker('1',
14+
verify.getSuggestionDiagnostics([{
15+
message: "This constructor function may be converted to a class declaration.",
16+
category: "suggestion",
17+
code: 80002,
18+
}]);
19+
20+
verify.codeFix({
21+
description: "Convert function to an ES2015 class",
22+
newFileContent:
1623
`class foo {
1724
constructor() { }
1825
instanceMethod1() { return "this is name"; }
@@ -23,4 +30,5 @@ verify.fileAfterApplyingRefactorAtMarker('1',
2330
foo.prototype.instanceProp1 = "hello";
2431
foo.prototype.instanceProp2 = undefined;
2532
foo.staticProp = "world";
26-
`, 'Convert to ES2015 class', 'convert');
33+
`,
34+
});

tests/cases/fourslash/convertFunctionToEs6Class2.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@
22

33
// @allowNonTsExtensions: true
44
// @Filename: test123.js
5-
//// [|var /*1*/foo = function() { };
6-
//// /*2*/foo.prototype.instanceMethod1 = function() { return "this is name"; };
7-
//// /*3*/foo.prototype.instanceMethod2 = () => { return "this is name"; };
8-
//// /*4*/foo.instanceProp1 = "hello";
9-
//// /*5*/foo.instanceProp2 = undefined;
10-
//// /*6*/foo.staticProp = "world";
11-
//// /*7*/foo.staticMethod1 = function() { return "this is static name"; };
12-
//// /*8*/foo.staticMethod2 = () => "this is static name";|]
5+
////var foo = function() { };
6+
////foo.prototype.instanceMethod1 = function() { return "this is name"; };
7+
////foo.prototype.instanceMethod2 = () => { return "this is name"; };
8+
////foo.instanceProp1 = "hello";
9+
////foo.instanceProp2 = undefined;
10+
////foo.staticProp = "world";
11+
////foo.staticMethod1 = function() { return "this is static name"; };
12+
////foo.staticMethod2 = () => "this is static name";
1313

14-
15-
['1', '2', '3', '4', '5', '6', '7', '8'].forEach(m => verify.applicableRefactorAvailableAtMarker(m));
16-
verify.fileAfterApplyingRefactorAtMarker('4',
14+
verify.codeFix({
15+
description: "Convert function to an ES2015 class",
16+
newFileContent:
1717
`class foo {
1818
constructor() { }
1919
instanceMethod1() { return "this is name"; }
@@ -24,4 +24,5 @@ verify.fileAfterApplyingRefactorAtMarker('4',
2424
foo.instanceProp1 = "hello";
2525
foo.instanceProp2 = undefined;
2626
foo.staticProp = "world";
27-
`, 'Convert to ES2015 class', 'convert');
27+
`,
28+
});

tests/cases/fourslash/convertFunctionToEs6Class3.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@
22

33
// @allowNonTsExtensions: true
44
// @Filename: test123.js
5-
//// var bar = 10, /*1*/foo = function() { };
6-
//// /*2*/foo.prototype.instanceMethod1 = function() { return "this is name"; };
7-
//// /*3*/foo.prototype.instanceMethod2 = () => { return "this is name"; };
8-
//// /*4*/foo.prototype.instanceProp1 = "hello";
9-
//// /*5*/foo.prototype.instanceProp2 = undefined;
10-
//// /*6*/foo.staticProp = "world";
11-
//// /*7*/foo.staticMethod1 = function() { return "this is static name"; };
12-
//// /*8*/foo.staticMethod2 = () => "this is static name";
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";
1313

14-
15-
['1', '2', '3', '4', '5', '6', '7', '8'].forEach(m => verify.applicableRefactorAvailableAtMarker(m));
16-
verify.fileAfterApplyingRefactorAtMarker('7',
14+
verify.codeFix({
15+
description: "Convert function to an ES2015 class",
16+
newFileContent:
1717
`var bar = 10;
1818
class foo {
1919
constructor() { }
@@ -25,4 +25,5 @@ class foo {
2525
foo.prototype.instanceProp1 = "hello";
2626
foo.prototype.instanceProp2 = undefined;
2727
foo.staticProp = "world";
28-
`, 'Convert to ES2015 class', 'convert');
28+
`,
29+
});

0 commit comments

Comments
 (0)