1/* @internal */ 2namespace 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 toAnonymousFunctionAction = { 7 name: "Convert to anonymous function", 8 description: getLocaleSpecificMessage(Diagnostics.Convert_to_anonymous_function), 9 kind: "refactor.rewrite.function.anonymous", 10 }; 11 const toNamedFunctionAction = { 12 name: "Convert to named function", 13 description: getLocaleSpecificMessage(Diagnostics.Convert_to_named_function), 14 kind: "refactor.rewrite.function.named", 15 }; 16 const toArrowFunctionAction = { 17 name: "Convert to arrow function", 18 description: getLocaleSpecificMessage(Diagnostics.Convert_to_arrow_function), 19 kind: "refactor.rewrite.function.arrow", 20 }; 21 registerRefactor(refactorName, { 22 kinds: [ 23 toAnonymousFunctionAction.kind, 24 toNamedFunctionAction.kind, 25 toArrowFunctionAction.kind 26 ], 27 getEditsForAction: getRefactorEditsToConvertFunctionExpressions, 28 getAvailableActions: getRefactorActionsToConvertFunctionExpressions 29 }); 30 31 interface FunctionInfo { 32 readonly selectedVariableDeclaration: boolean; 33 readonly func: FunctionExpression | ArrowFunction; 34 } 35 36 interface VariableInfo { 37 readonly variableDeclaration: VariableDeclaration; 38 readonly variableDeclarationList: VariableDeclarationList; 39 readonly statement: VariableStatement; 40 readonly name: Identifier; 41 } 42 43 function getRefactorActionsToConvertFunctionExpressions(context: RefactorContext): readonly ApplicableRefactorInfo[] { 44 const { file, startPosition, program, kind } = context; 45 const info = getFunctionInfo(file, startPosition, program); 46 47 if (!info) return emptyArray; 48 const { selectedVariableDeclaration, func } = info; 49 const possibleActions: RefactorActionInfo[] = []; 50 const errors: RefactorActionInfo[] = []; 51 if (refactorKindBeginsWith(toNamedFunctionAction.kind, kind)) { 52 const error = selectedVariableDeclaration || (isArrowFunction(func) && isVariableDeclaration(func.parent)) ? 53 undefined : getLocaleSpecificMessage(Diagnostics.Could_not_convert_to_named_function); 54 if (error) { 55 errors.push({ ...toNamedFunctionAction, notApplicableReason: error }); 56 } 57 else { 58 possibleActions.push(toNamedFunctionAction); 59 } 60 } 61 62 if (refactorKindBeginsWith(toAnonymousFunctionAction.kind, kind)) { 63 const error = !selectedVariableDeclaration && isArrowFunction(func) ? 64 undefined: getLocaleSpecificMessage(Diagnostics.Could_not_convert_to_anonymous_function); 65 if (error) { 66 errors.push({ ...toAnonymousFunctionAction, notApplicableReason: error }); 67 } 68 else { 69 possibleActions.push(toAnonymousFunctionAction); 70 } 71 } 72 73 if (refactorKindBeginsWith(toArrowFunctionAction.kind, kind)) { 74 const error = isFunctionExpression(func) ? undefined : getLocaleSpecificMessage(Diagnostics.Could_not_convert_to_arrow_function); 75 if (error) { 76 errors.push({ ...toArrowFunctionAction, notApplicableReason: error }); 77 } 78 else { 79 possibleActions.push(toArrowFunctionAction); 80 } 81 } 82 83 return [{ 84 name: refactorName, 85 description: refactorDescription, 86 actions: possibleActions.length === 0 && context.preferences.provideRefactorNotApplicableReason ? 87 errors : possibleActions 88 }]; 89 } 90 91 function getRefactorEditsToConvertFunctionExpressions(context: RefactorContext, actionName: string): RefactorEditInfo | undefined { 92 const { file, startPosition, program } = context; 93 const info = getFunctionInfo(file, startPosition, program); 94 95 if (!info) return undefined; 96 const { func } = info; 97 const edits: FileTextChanges[] = []; 98 99 switch (actionName) { 100 case toAnonymousFunctionAction.name: 101 edits.push(...getEditInfoForConvertToAnonymousFunction(context, func)); 102 break; 103 104 case toNamedFunctionAction.name: 105 const variableInfo = getVariableInfo(func); 106 if (!variableInfo) return undefined; 107 108 edits.push(...getEditInfoForConvertToNamedFunction(context, func, variableInfo)); 109 break; 110 111 case toArrowFunctionAction.name: 112 if (!isFunctionExpression(func)) return undefined; 113 edits.push(...getEditInfoForConvertToArrowFunction(context, func)); 114 break; 115 116 default: 117 return Debug.fail("invalid action"); 118 } 119 120 return { renameFilename: undefined, renameLocation: undefined, edits }; 121 } 122 123 function containingThis(node: Node): boolean { 124 let containsThis = false; 125 node.forEachChild(function checkThis(child) { 126 127 if (isThis(child)) { 128 containsThis = true; 129 return; 130 } 131 132 if (!isClassLike(child) && !isFunctionDeclaration(child) && !isFunctionExpression(child)) { 133 forEachChild(child, checkThis); 134 } 135 }); 136 137 return containsThis; 138 } 139 140 function getFunctionInfo(file: SourceFile, startPosition: number, program: Program): FunctionInfo | undefined { 141 const token = getTokenAtPosition(file, startPosition); 142 const typeChecker = program.getTypeChecker(); 143 const func = tryGetFunctionFromVariableDeclaration(file, typeChecker, token.parent); 144 if (func && !containingThis(func.body) && !typeChecker.containsArgumentsReference(func)) { 145 return { selectedVariableDeclaration: true, func }; 146 } 147 148 const maybeFunc = getContainingFunction(token); 149 if ( 150 maybeFunc && 151 (isFunctionExpression(maybeFunc) || isArrowFunction(maybeFunc)) && 152 !rangeContainsRange(maybeFunc.body, token) && 153 !containingThis(maybeFunc.body) && 154 !typeChecker.containsArgumentsReference(maybeFunc) 155 ) { 156 if (isFunctionExpression(maybeFunc) && isFunctionReferencedInFile(file, typeChecker, maybeFunc)) return undefined; 157 return { selectedVariableDeclaration: false, func: maybeFunc }; 158 } 159 160 return undefined; 161 } 162 163 function isSingleVariableDeclaration(parent: Node): parent is VariableDeclarationList { 164 return isVariableDeclaration(parent) || (isVariableDeclarationList(parent) && parent.declarations.length === 1); 165 } 166 167 function tryGetFunctionFromVariableDeclaration(sourceFile: SourceFile, typeChecker: TypeChecker, parent: Node): ArrowFunction | FunctionExpression | undefined { 168 if (!isSingleVariableDeclaration(parent)) { 169 return undefined; 170 } 171 const variableDeclaration = isVariableDeclaration(parent) ? parent : first(parent.declarations); 172 const initializer = variableDeclaration.initializer; 173 if (initializer && (isArrowFunction(initializer) || isFunctionExpression(initializer) && !isFunctionReferencedInFile(sourceFile, typeChecker, initializer))) { 174 return initializer; 175 } 176 return undefined; 177 } 178 179 function convertToBlock(body: ConciseBody): Block { 180 if (isExpression(body)) { 181 const returnStatement = factory.createReturnStatement(body); 182 const file = body.getSourceFile(); 183 suppressLeadingAndTrailingTrivia(returnStatement); 184 copyTrailingAsLeadingComments(body, returnStatement, file, /* commentKind */ undefined, /* hasTrailingNewLine */ true); 185 return factory.createBlock([returnStatement], /* multiLine */ true); 186 } 187 else { 188 return body; 189 } 190 } 191 192 function getVariableInfo(func: FunctionExpression | ArrowFunction): VariableInfo | undefined { 193 const variableDeclaration = func.parent; 194 if (!isVariableDeclaration(variableDeclaration) || !isVariableDeclarationInVariableStatement(variableDeclaration)) return undefined; 195 196 const variableDeclarationList = variableDeclaration.parent; 197 const statement = variableDeclarationList.parent; 198 if (!isVariableDeclarationList(variableDeclarationList) || !isVariableStatement(statement) || !isIdentifier(variableDeclaration.name)) return undefined; 199 200 return { variableDeclaration, variableDeclarationList, statement, name: variableDeclaration.name }; 201 } 202 203 function getEditInfoForConvertToAnonymousFunction(context: RefactorContext, func: FunctionExpression | ArrowFunction): FileTextChanges[] { 204 const { file } = context; 205 const body = convertToBlock(func.body); 206 const newNode = factory.createFunctionExpression(func.modifiers, func.asteriskToken, /* name */ undefined, func.typeParameters, func.parameters, func.type, body); 207 return textChanges.ChangeTracker.with(context, t => t.replaceNode(file, func, newNode)); 208 } 209 210 function getEditInfoForConvertToNamedFunction(context: RefactorContext, func: FunctionExpression | ArrowFunction, variableInfo: VariableInfo): FileTextChanges[] { 211 const { file } = context; 212 const body = convertToBlock(func.body); 213 214 const { variableDeclaration, variableDeclarationList, statement, name } = variableInfo; 215 suppressLeadingTrivia(statement); 216 217 const modifiersFlags = (getCombinedModifierFlags(variableDeclaration) & ModifierFlags.Export) | getEffectiveModifierFlags(func); 218 const modifiers = factory.createModifiersFromModifierFlags(modifiersFlags); 219 const newNode = factory.createFunctionDeclaration(length(modifiers) ? modifiers : undefined, func.asteriskToken, name, func.typeParameters, func.parameters, func.type, body); 220 221 if (variableDeclarationList.declarations.length === 1) { 222 return textChanges.ChangeTracker.with(context, t => t.replaceNode(file, statement, newNode)); 223 } 224 else { 225 return textChanges.ChangeTracker.with(context, t => { 226 t.delete(file, variableDeclaration); 227 t.insertNodeAfter(file, statement, newNode); 228 }); 229 } 230 } 231 232 function getEditInfoForConvertToArrowFunction(context: RefactorContext, func: FunctionExpression): FileTextChanges[] { 233 const { file } = context; 234 const statements = func.body.statements; 235 const head = statements[0]; 236 let body: ConciseBody; 237 238 if (canBeConvertedToExpression(func.body, head)) { 239 body = head.expression!; 240 suppressLeadingAndTrailingTrivia(body); 241 copyComments(head, body); 242 } 243 else { 244 body = func.body; 245 } 246 247 const newNode = factory.createArrowFunction(func.modifiers, func.typeParameters, func.parameters, func.type, factory.createToken(SyntaxKind.EqualsGreaterThanToken), body); 248 return textChanges.ChangeTracker.with(context, t => t.replaceNode(file, func, newNode)); 249 } 250 251 function canBeConvertedToExpression(body: Block, head: Statement): head is ReturnStatement { 252 return body.statements.length === 1 && ((isReturnStatement(head) && !!head.expression)); 253 } 254 255 function isFunctionReferencedInFile(sourceFile: SourceFile, typeChecker: TypeChecker, node: FunctionExpression): boolean { 256 return !!node.name && FindAllReferences.Core.isSymbolReferencedInFile(node.name, typeChecker, sourceFile); 257 } 258} 259