mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-05-19 10:41:56 -05:00
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:
@@ -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",
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -113,6 +113,7 @@
|
||||
"refactors/addOrRemoveBracesToArrowFunction.ts",
|
||||
"refactors/convertParamsToDestructuredObject.ts",
|
||||
"refactors/convertStringOrTemplateLiteral.ts",
|
||||
"refactors/convertArrowFunctionOrFunctionExpression.ts",
|
||||
"services.ts",
|
||||
"breakpoints.ts",
|
||||
"transform.ts",
|
||||
|
||||
Reference in New Issue
Block a user