Skip to content

Commit 1d1c167

Browse files
jessetrinitybigaruJesse Trinity
authored
add support to convert lambda to function and vice-versa (#28250)
* add skeleton * add getAvailableActions * add working getEditsForAction * add multi vardecl * fix multi decl bug * change refactor name * add tests for ToAnon, ToArrow and available arrow * add tests for ToNamed and available anon * add tests for ReturnType and available Arrow as FnParam * fix bug modifiers by toNamed * add tests for modifiers * fix for tslint error * adapt one test case * refactor getInfo getAvailableActions * refactor small progress * extract creation of block * extract creation of funcDeclaration * make guideline compliant * apply feedback from pr * add testcase and apply feedback from pr * apply feedback from pr * add newline * rename testcases * Make conditions more expressive * fix for unnecessary duplication of comment * apply feedback from pr * update getAvailableActions * check if functionExpression name is used * add more testcases * do not provide refactoring when it contains this because this behaves differently in arrow than in function * exclude nested functions and classes at containingThis check * fix linting error * fix line endings Co-authored-by: BigAru <[email protected]> Co-authored-by: bigaru <[email protected]> Co-authored-by: Jesse Trinity <[email protected]> Co-authored-by: Jesse Trinity <[email protected]>
2 parents f57d66b + fb3a84c commit 1d1c167

File tree

47 files changed

+1010
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1010
-0
lines changed

src/compiler/diagnosticMessages.json

+16
Original file line numberDiff line numberDiff line change
@@ -5717,6 +5717,22 @@
57175717
"category": "Message",
57185718
"code": 95121
57195719
},
5720+
"Convert arrow function or function expression": {
5721+
"category": "Message",
5722+
"code": 95122
5723+
},
5724+
"Convert to anonymous function": {
5725+
"category": "Message",
5726+
"code": 95123
5727+
},
5728+
"Convert to named function": {
5729+
"category": "Message",
5730+
"code": 95124
5731+
},
5732+
"Convert to arrow function": {
5733+
"category": "Message",
5734+
"code": 95125
5735+
},
57205736

57215737
"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
57225738
"category": "Error",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
/* @internal */
2+
namespace ts.refactor.convertArrowFunctionOrFunctionExpression {
3+
const refactorName = "Convert arrow function or function expression";
4+
const refactorDescription = getLocaleSpecificMessage(Diagnostics.Convert_arrow_function_or_function_expression);
5+
6+
const toAnonymousFunctionActionName = "Convert to anonymous function";
7+
const toNamedFunctionActionName = "Convert to named function";
8+
const toArrowFunctionActionName = "Convert to arrow function";
9+
10+
const toAnonymousFunctionActionDescription = getLocaleSpecificMessage(Diagnostics.Convert_to_anonymous_function);
11+
const toNamedFunctionActionDescription = getLocaleSpecificMessage(Diagnostics.Convert_to_named_function);
12+
const toArrowFunctionActionDescription = getLocaleSpecificMessage(Diagnostics.Convert_to_arrow_function);
13+
14+
registerRefactor(refactorName, { getEditsForAction, getAvailableActions });
15+
16+
interface FunctionInfo {
17+
readonly selectedVariableDeclaration: boolean;
18+
readonly func: FunctionExpression | ArrowFunction;
19+
}
20+
21+
interface VariableInfo {
22+
readonly variableDeclaration: VariableDeclaration;
23+
readonly variableDeclarationList: VariableDeclarationList;
24+
readonly statement: VariableStatement;
25+
readonly name: Identifier;
26+
}
27+
28+
function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] {
29+
const { file, startPosition, program } = context;
30+
const info = getFunctionInfo(file, startPosition, program);
31+
32+
if (!info) return emptyArray;
33+
const { selectedVariableDeclaration, func } = info;
34+
const possibleActions: RefactorActionInfo[] = [];
35+
36+
if (selectedVariableDeclaration || (isArrowFunction(func) && isVariableDeclaration(func.parent))) {
37+
possibleActions.push({
38+
name: toNamedFunctionActionName,
39+
description: toNamedFunctionActionDescription
40+
});
41+
}
42+
43+
if (!selectedVariableDeclaration && isArrowFunction(func)) {
44+
possibleActions.push({
45+
name: toAnonymousFunctionActionName,
46+
description: toAnonymousFunctionActionDescription
47+
});
48+
}
49+
50+
if (isFunctionExpression(func)) {
51+
possibleActions.push({
52+
name: toArrowFunctionActionName,
53+
description: toArrowFunctionActionDescription
54+
});
55+
}
56+
57+
return [{
58+
name: refactorName,
59+
description: refactorDescription,
60+
actions: possibleActions
61+
}];
62+
}
63+
64+
function getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined {
65+
const { file, startPosition, program } = context;
66+
const info = getFunctionInfo(file, startPosition, program);
67+
68+
if (!info) return undefined;
69+
const { func } = info;
70+
const edits: FileTextChanges[] = [];
71+
72+
switch (actionName) {
73+
case toAnonymousFunctionActionName:
74+
edits.push(...getEditInfoForConvertToAnonymousFunction(context, func));
75+
break;
76+
77+
case toNamedFunctionActionName:
78+
const variableInfo = getVariableInfo(func);
79+
if (!variableInfo) return undefined;
80+
81+
edits.push(...getEditInfoForConvertToNamedFunction(context, func, variableInfo));
82+
break;
83+
84+
case toArrowFunctionActionName:
85+
if (!isFunctionExpression(func)) return undefined;
86+
edits.push(...getEditInfoForConvertToArrowFunction(context, func));
87+
break;
88+
89+
default:
90+
return Debug.fail("invalid action");
91+
}
92+
93+
return { renameFilename: undefined, renameLocation: undefined, edits };
94+
}
95+
96+
function containingThis(node: Node): boolean {
97+
let containsThis = false;
98+
node.forEachChild(function checkThis(child) {
99+
100+
if (isThis(child)) {
101+
containsThis = true;
102+
return;
103+
}
104+
105+
if (!isClassLike(child) && !isFunctionDeclaration(child) && !isFunctionExpression(child)) {
106+
forEachChild(child, checkThis);
107+
}
108+
});
109+
110+
return containsThis;
111+
}
112+
113+
function getFunctionInfo(file: SourceFile, startPosition: number, program: Program): FunctionInfo | undefined {
114+
const token = getTokenAtPosition(file, startPosition);
115+
116+
const arrowFunc = getArrowFunctionFromVariableDeclaration(token.parent);
117+
if (arrowFunc && !containingThis(arrowFunc.body)) return { selectedVariableDeclaration: true, func: arrowFunc };
118+
119+
const maybeFunc = getContainingFunction(token);
120+
const typeChecker = program.getTypeChecker();
121+
122+
if (
123+
maybeFunc &&
124+
(isFunctionExpression(maybeFunc) || isArrowFunction(maybeFunc)) &&
125+
!rangeContainsRange(maybeFunc.body, token) &&
126+
!containingThis(maybeFunc.body)
127+
) {
128+
if ((isFunctionExpression(maybeFunc) && maybeFunc.name && FindAllReferences.Core.isSymbolReferencedInFile(maybeFunc.name, typeChecker, file))) return undefined;
129+
return { selectedVariableDeclaration: false, func: maybeFunc };
130+
}
131+
132+
return undefined;
133+
}
134+
135+
function isSingleVariableDeclaration(parent: Node): parent is VariableDeclarationList {
136+
return isVariableDeclaration(parent) || (isVariableDeclarationList(parent) && parent.declarations.length === 1);
137+
}
138+
139+
function getArrowFunctionFromVariableDeclaration(parent: Node): ArrowFunction | undefined {
140+
if (!isSingleVariableDeclaration(parent)) return undefined;
141+
const variableDeclaration = isVariableDeclaration(parent) ? parent : parent.declarations[0];
142+
143+
const initializer = variableDeclaration.initializer;
144+
if (!initializer || !isArrowFunction(initializer)) return undefined;
145+
return initializer;
146+
}
147+
148+
function convertToBlock(body: ConciseBody): Block {
149+
if (isExpression(body)) {
150+
return createBlock([createReturn(body)], /* multiLine */ true);
151+
}
152+
else {
153+
return body;
154+
}
155+
}
156+
157+
function getVariableInfo(func: FunctionExpression | ArrowFunction): VariableInfo | undefined {
158+
const variableDeclaration = func.parent;
159+
if (!isVariableDeclaration(variableDeclaration) || !isVariableDeclarationInVariableStatement(variableDeclaration)) return undefined;
160+
161+
const variableDeclarationList = variableDeclaration.parent;
162+
const statement = variableDeclarationList.parent;
163+
if (!isVariableDeclarationList(variableDeclarationList) || !isVariableStatement(statement) || !isIdentifier(variableDeclaration.name)) return undefined;
164+
165+
return { variableDeclaration, variableDeclarationList, statement, name: variableDeclaration.name };
166+
}
167+
168+
function getEditInfoForConvertToAnonymousFunction(context: RefactorContext, func: FunctionExpression | ArrowFunction): FileTextChanges[] {
169+
const { file } = context;
170+
const body = convertToBlock(func.body);
171+
const newNode = createFunctionExpression(func.modifiers, func.asteriskToken, /* name */ undefined, func.typeParameters, func.parameters, func.type, body);
172+
return textChanges.ChangeTracker.with(context, t => t.replaceNode(file, func, newNode));
173+
}
174+
175+
function getEditInfoForConvertToNamedFunction(context: RefactorContext, func: FunctionExpression | ArrowFunction, variableInfo: VariableInfo): FileTextChanges[] {
176+
const { file } = context;
177+
const body = convertToBlock(func.body);
178+
179+
const { variableDeclaration, variableDeclarationList, statement, name } = variableInfo;
180+
suppressLeadingTrivia(statement);
181+
const newNode = createFunctionDeclaration(func.decorators, statement.modifiers, func.asteriskToken, name, func.typeParameters, func.parameters, func.type, body);
182+
183+
if (variableDeclarationList.declarations.length === 1) {
184+
return textChanges.ChangeTracker.with(context, t => t.replaceNode(file, statement, newNode));
185+
}
186+
else {
187+
return textChanges.ChangeTracker.with(context, t => {
188+
t.delete(file, variableDeclaration);
189+
t.insertNodeAfter(file, statement, newNode);
190+
});
191+
}
192+
}
193+
194+
function getEditInfoForConvertToArrowFunction(context: RefactorContext, func: FunctionExpression): FileTextChanges[] {
195+
const { file } = context;
196+
const statements = func.body.statements;
197+
const head = statements[0];
198+
let body: ConciseBody;
199+
200+
if (canBeConvertedToExpression(func.body, head)) {
201+
body = head.expression!;
202+
suppressLeadingAndTrailingTrivia(body);
203+
copyComments(head, body);
204+
}
205+
else {
206+
body = func.body;
207+
}
208+
209+
const newNode = createArrowFunction(func.modifiers, func.typeParameters, func.parameters, func.type, createToken(SyntaxKind.EqualsGreaterThanToken), body);
210+
return textChanges.ChangeTracker.with(context, t => t.replaceNode(file, func, newNode));
211+
}
212+
213+
function canBeConvertedToExpression(body: Block, head: Statement): head is ReturnStatement {
214+
return body.statements.length === 1 && ((isReturnStatement(head) && !!head.expression));
215+
}
216+
}

src/services/tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
"refactors/addOrRemoveBracesToArrowFunction.ts",
114114
"refactors/convertParamsToDestructuredObject.ts",
115115
"refactors/convertStringOrTemplateLiteral.ts",
116+
"refactors/convertArrowFunctionOrFunctionExpression.ts",
116117
"services.ts",
117118
"breakpoints.ts",
118119
"transform.ts",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// /*z*/c/*y*/onst /*x*/f/*w*/oo = /*v*/f/*u*/unction() /*t*/{/*s*/ /*r*/r/*q*/eturn 42;};
4+
5+
goTo.select("z", "y");
6+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to named function");
7+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to anonymous function");
8+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to arrow function");
9+
10+
goTo.select("x", "w");
11+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to named function");
12+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to anonymous function");
13+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to arrow function");
14+
15+
goTo.select("v", "u");
16+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to named function");
17+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to anonymous function");
18+
verify.refactorAvailable("Convert arrow function or function expression", "Convert to arrow function");
19+
20+
goTo.select("t", "s");
21+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to named function");
22+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to anonymous function");
23+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to arrow function");
24+
25+
goTo.select("r", "q");
26+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to named function");
27+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to anonymous function");
28+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to arrow function");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// function foo(a){}
4+
//// /*z*/f/*y*/oo/*x*/(/*w*//*v*/f/*u*/unction/*t*/(/*s*//*r*/b/*q*/,c){/*p*/r/*o*/eturn 42;})
5+
6+
goTo.select("z", "y");
7+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to named function");
8+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to anonymous function");
9+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to arrow function");
10+
11+
goTo.select("x", "w");
12+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to named function");
13+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to anonymous function");
14+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to arrow function");
15+
16+
goTo.select("v", "u");
17+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to named function");
18+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to anonymous function");
19+
verify.refactorAvailable("Convert arrow function or function expression", "Convert to arrow function");
20+
21+
goTo.select("t", "s");
22+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to named function");
23+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to anonymous function");
24+
verify.refactorAvailable("Convert arrow function or function expression", "Convert to arrow function");
25+
26+
goTo.select("r", "q");
27+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to named function");
28+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to anonymous function");
29+
verify.refactorAvailable("Convert arrow function or function expression", "Convert to arrow function");
30+
31+
goTo.select("p", "o");
32+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to named function");
33+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to anonymous function");
34+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to arrow function");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// const zoo = /*x*/f/*w*/unction () {
4+
//// class Animal {
5+
//// weight = 42
6+
//// askWeight() { return this.weight }
7+
//// }
8+
//// const Insect = class {
9+
//// weight = 42
10+
//// askWeight() { return this.weight }
11+
//// }
12+
//// function callTaxi() { this.no = "054 xxx xx xx" }
13+
//// const callPizzaDelivery = function() { this.phone = "064 yyy yy yy"}
14+
//// };
15+
16+
goTo.select("x", "w");
17+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to named function");
18+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to anonymous function");
19+
verify.refactorAvailable("Convert arrow function or function expression", "Convert to arrow function");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// const foo = /*x*/f/*w*/unction() {
4+
//// this.bar = "F-Express";
5+
//// return this.bar;
6+
//// };
7+
8+
goTo.select("x", "w");
9+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to named function");
10+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to anonymous function");
11+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to arrow function");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// /*z*/c/*y*/onst /*x*/f/*w*/oo = /*v*/f/*u*/unction bar() /*t*/{/*s*/ /*r*/r/*q*/eturn 42;};
4+
5+
goTo.select("z", "y");
6+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to named function");
7+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to anonymous function");
8+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to arrow function");
9+
10+
goTo.select("x", "w");
11+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to named function");
12+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to anonymous function");
13+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to arrow function");
14+
15+
goTo.select("v", "u");
16+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to named function");
17+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to anonymous function");
18+
verify.refactorAvailable("Convert arrow function or function expression", "Convert to arrow function");
19+
20+
goTo.select("t", "s");
21+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to named function");
22+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to anonymous function");
23+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to arrow function");
24+
25+
goTo.select("r", "q");
26+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to named function");
27+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to anonymous function");
28+
verify.not.refactorAvailable("Convert arrow function or function expression", "Convert to arrow function");

0 commit comments

Comments
 (0)