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 <arooran@indikon.ch>
Co-authored-by: bigaru <bigaru@users.noreply.github.com>
Co-authored-by: Jesse Trinity <42591254+jessetrinity@users.noreply.github.com>
Co-authored-by: Jesse Trinity <jetrinit@microsoft.com>
This commit is contained in:
Jesse Trinity
2020-06-01 15:26:45 -07:00
committed by GitHub
47 changed files with 1010 additions and 0 deletions

View File

@@ -5717,6 +5717,22 @@
"category": "Message",
"code": 95121
},
"Convert arrow function or function expression": {
"category": "Message",
"code": 95122
},
"Convert to anonymous function": {
"category": "Message",
"code": 95123
},
"Convert to named function": {
"category": "Message",
"code": 95124
},
"Convert to arrow function": {
"category": "Message",
"code": 95125
},
"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
"category": "Error",

View File

@@ -0,0 +1,216 @@
/* @internal */
namespace ts.refactor.convertArrowFunctionOrFunctionExpression {
const refactorName = "Convert arrow function or function expression";
const refactorDescription = getLocaleSpecificMessage(Diagnostics.Convert_arrow_function_or_function_expression);
const toAnonymousFunctionActionName = "Convert to anonymous function";
const toNamedFunctionActionName = "Convert to named function";
const toArrowFunctionActionName = "Convert to arrow function";
const toAnonymousFunctionActionDescription = getLocaleSpecificMessage(Diagnostics.Convert_to_anonymous_function);
const toNamedFunctionActionDescription = getLocaleSpecificMessage(Diagnostics.Convert_to_named_function);
const toArrowFunctionActionDescription = getLocaleSpecificMessage(Diagnostics.Convert_to_arrow_function);
registerRefactor(refactorName, { getEditsForAction, getAvailableActions });
interface FunctionInfo {
readonly selectedVariableDeclaration: boolean;
readonly func: FunctionExpression | ArrowFunction;
}
interface VariableInfo {
readonly variableDeclaration: VariableDeclaration;
readonly variableDeclarationList: VariableDeclarationList;
readonly statement: VariableStatement;
readonly name: Identifier;
}
function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] {
const { file, startPosition, program } = context;
const info = getFunctionInfo(file, startPosition, program);
if (!info) return emptyArray;
const { selectedVariableDeclaration, func } = info;
const possibleActions: RefactorActionInfo[] = [];
if (selectedVariableDeclaration || (isArrowFunction(func) && isVariableDeclaration(func.parent))) {
possibleActions.push({
name: toNamedFunctionActionName,
description: toNamedFunctionActionDescription
});
}
if (!selectedVariableDeclaration && isArrowFunction(func)) {
possibleActions.push({
name: toAnonymousFunctionActionName,
description: toAnonymousFunctionActionDescription
});
}
if (isFunctionExpression(func)) {
possibleActions.push({
name: toArrowFunctionActionName,
description: toArrowFunctionActionDescription
});
}
return [{
name: refactorName,
description: refactorDescription,
actions: possibleActions
}];
}
function getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined {
const { file, startPosition, program } = context;
const info = getFunctionInfo(file, startPosition, program);
if (!info) return undefined;
const { func } = info;
const edits: FileTextChanges[] = [];
switch (actionName) {
case toAnonymousFunctionActionName:
edits.push(...getEditInfoForConvertToAnonymousFunction(context, func));
break;
case toNamedFunctionActionName:
const variableInfo = getVariableInfo(func);
if (!variableInfo) return undefined;
edits.push(...getEditInfoForConvertToNamedFunction(context, func, variableInfo));
break;
case toArrowFunctionActionName:
if (!isFunctionExpression(func)) return undefined;
edits.push(...getEditInfoForConvertToArrowFunction(context, func));
break;
default:
return Debug.fail("invalid action");
}
return { renameFilename: undefined, renameLocation: undefined, edits };
}
function containingThis(node: Node): boolean {
let containsThis = false;
node.forEachChild(function checkThis(child) {
if (isThis(child)) {
containsThis = true;
return;
}
if (!isClassLike(child) && !isFunctionDeclaration(child) && !isFunctionExpression(child)) {
forEachChild(child, checkThis);
}
});
return containsThis;
}
function getFunctionInfo(file: SourceFile, startPosition: number, program: Program): FunctionInfo | undefined {
const token = getTokenAtPosition(file, startPosition);
const arrowFunc = getArrowFunctionFromVariableDeclaration(token.parent);
if (arrowFunc && !containingThis(arrowFunc.body)) return { selectedVariableDeclaration: true, func: arrowFunc };
const maybeFunc = getContainingFunction(token);
const typeChecker = program.getTypeChecker();
if (
maybeFunc &&
(isFunctionExpression(maybeFunc) || isArrowFunction(maybeFunc)) &&
!rangeContainsRange(maybeFunc.body, token) &&
!containingThis(maybeFunc.body)
) {
if ((isFunctionExpression(maybeFunc) && maybeFunc.name && FindAllReferences.Core.isSymbolReferencedInFile(maybeFunc.name, typeChecker, file))) return undefined;
return { selectedVariableDeclaration: false, func: maybeFunc };
}
return undefined;
}
function isSingleVariableDeclaration(parent: Node): parent is VariableDeclarationList {
return isVariableDeclaration(parent) || (isVariableDeclarationList(parent) && parent.declarations.length === 1);
}
function getArrowFunctionFromVariableDeclaration(parent: Node): ArrowFunction | undefined {
if (!isSingleVariableDeclaration(parent)) return undefined;
const variableDeclaration = isVariableDeclaration(parent) ? parent : parent.declarations[0];
const initializer = variableDeclaration.initializer;
if (!initializer || !isArrowFunction(initializer)) return undefined;
return initializer;
}
function convertToBlock(body: ConciseBody): Block {
if (isExpression(body)) {
return createBlock([createReturn(body)], /* multiLine */ true);
}
else {
return body;
}
}
function getVariableInfo(func: FunctionExpression | ArrowFunction): VariableInfo | undefined {
const variableDeclaration = func.parent;
if (!isVariableDeclaration(variableDeclaration) || !isVariableDeclarationInVariableStatement(variableDeclaration)) return undefined;
const variableDeclarationList = variableDeclaration.parent;
const statement = variableDeclarationList.parent;
if (!isVariableDeclarationList(variableDeclarationList) || !isVariableStatement(statement) || !isIdentifier(variableDeclaration.name)) return undefined;
return { variableDeclaration, variableDeclarationList, statement, name: variableDeclaration.name };
}
function getEditInfoForConvertToAnonymousFunction(context: RefactorContext, func: FunctionExpression | ArrowFunction): FileTextChanges[] {
const { file } = context;
const body = convertToBlock(func.body);
const newNode = createFunctionExpression(func.modifiers, func.asteriskToken, /* name */ undefined, func.typeParameters, func.parameters, func.type, body);
return textChanges.ChangeTracker.with(context, t => t.replaceNode(file, func, newNode));
}
function getEditInfoForConvertToNamedFunction(context: RefactorContext, func: FunctionExpression | ArrowFunction, variableInfo: VariableInfo): FileTextChanges[] {
const { file } = context;
const body = convertToBlock(func.body);
const { variableDeclaration, variableDeclarationList, statement, name } = variableInfo;
suppressLeadingTrivia(statement);
const newNode = createFunctionDeclaration(func.decorators, statement.modifiers, func.asteriskToken, name, func.typeParameters, func.parameters, func.type, body);
if (variableDeclarationList.declarations.length === 1) {
return textChanges.ChangeTracker.with(context, t => t.replaceNode(file, statement, newNode));
}
else {
return textChanges.ChangeTracker.with(context, t => {
t.delete(file, variableDeclaration);
t.insertNodeAfter(file, statement, newNode);
});
}
}
function getEditInfoForConvertToArrowFunction(context: RefactorContext, func: FunctionExpression): FileTextChanges[] {
const { file } = context;
const statements = func.body.statements;
const head = statements[0];
let body: ConciseBody;
if (canBeConvertedToExpression(func.body, head)) {
body = head.expression!;
suppressLeadingAndTrailingTrivia(body);
copyComments(head, body);
}
else {
body = func.body;
}
const newNode = createArrowFunction(func.modifiers, func.typeParameters, func.parameters, func.type, createToken(SyntaxKind.EqualsGreaterThanToken), body);
return textChanges.ChangeTracker.with(context, t => t.replaceNode(file, func, newNode));
}
function canBeConvertedToExpression(body: Block, head: Statement): head is ReturnStatement {
return body.statements.length === 1 && ((isReturnStatement(head) && !!head.expression));
}
}

View File

@@ -113,6 +113,7 @@
"refactors/addOrRemoveBracesToArrowFunction.ts",
"refactors/convertParamsToDestructuredObject.ts",
"refactors/convertStringOrTemplateLiteral.ts",
"refactors/convertArrowFunctionOrFunctionExpression.ts",
"services.ts",
"breakpoints.ts",
"transform.ts",