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, 28 getAvailableActions 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 getAvailableActions(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 getEditsForAction(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)) { 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 ) { 155 if (isFunctionExpression(maybeFunc) && isFunctionReferencedInFile(file, typeChecker, maybeFunc)) return undefined; 156 return { selectedVariableDeclaration: false, func: maybeFunc }; 157 } 158 159 return undefined; 160 } 161 162 function isSingleVariableDeclaration(parent: Node): parent is VariableDeclarationList { 163 return isVariableDeclaration(parent) || (isVariableDeclarationList(parent) && parent.declarations.length === 1); 164 } 165 166 function tryGetFunctionFromVariableDeclaration(sourceFile: SourceFile, typeChecker: TypeChecker, parent: Node): ArrowFunction | FunctionExpression | undefined { 167 if (!isSingleVariableDeclaration(parent)) { 168 return undefined; 169 } 170 const variableDeclaration = isVariableDeclaration(parent) ? parent : first(parent.declarations); 171 const initializer = variableDeclaration.initializer; 172 if (initializer && (isArrowFunction(initializer) || isFunctionExpression(initializer) && !isFunctionReferencedInFile(sourceFile, typeChecker, initializer))) { 173 return initializer; 174 } 175 return undefined; 176 } 177 178 function convertToBlock(body: ConciseBody): Block { 179 if (isExpression(body)) { 180 return factory.createBlock([factory.createReturnStatement(body)], /* multiLine */ true); 181 } 182 else { 183 return body; 184 } 185 } 186 187 function getVariableInfo(func: FunctionExpression | ArrowFunction): VariableInfo | undefined { 188 const variableDeclaration = func.parent; 189 if (!isVariableDeclaration(variableDeclaration) || !isVariableDeclarationInVariableStatement(variableDeclaration)) return undefined; 190 191 const variableDeclarationList = variableDeclaration.parent; 192 const statement = variableDeclarationList.parent; 193 if (!isVariableDeclarationList(variableDeclarationList) || !isVariableStatement(statement) || !isIdentifier(variableDeclaration.name)) return undefined; 194 195 return { variableDeclaration, variableDeclarationList, statement, name: variableDeclaration.name }; 196 } 197 198 function getEditInfoForConvertToAnonymousFunction(context: RefactorContext, func: FunctionExpression | ArrowFunction): FileTextChanges[] { 199 const { file } = context; 200 const body = convertToBlock(func.body); 201 const newNode = factory.createFunctionExpression(func.modifiers, func.asteriskToken, /* name */ undefined, func.typeParameters, func.parameters, func.type, body); 202 return textChanges.ChangeTracker.with(context, t => t.replaceNode(file, func, newNode)); 203 } 204 205 function getEditInfoForConvertToNamedFunction(context: RefactorContext, func: FunctionExpression | ArrowFunction, variableInfo: VariableInfo): FileTextChanges[] { 206 const { file } = context; 207 const body = convertToBlock(func.body); 208 209 const { variableDeclaration, variableDeclarationList, statement, name } = variableInfo; 210 suppressLeadingTrivia(statement); 211 const newNode = factory.createFunctionDeclaration(func.decorators, statement.modifiers, func.asteriskToken, name, func.typeParameters, func.parameters, func.type, body); 212 213 if (variableDeclarationList.declarations.length === 1) { 214 return textChanges.ChangeTracker.with(context, t => t.replaceNode(file, statement, newNode)); 215 } 216 else { 217 return textChanges.ChangeTracker.with(context, t => { 218 t.delete(file, variableDeclaration); 219 t.insertNodeAfter(file, statement, newNode); 220 }); 221 } 222 } 223 224 function getEditInfoForConvertToArrowFunction(context: RefactorContext, func: FunctionExpression): FileTextChanges[] { 225 const { file } = context; 226 const statements = func.body.statements; 227 const head = statements[0]; 228 let body: ConciseBody; 229 230 if (canBeConvertedToExpression(func.body, head)) { 231 body = head.expression!; 232 suppressLeadingAndTrailingTrivia(body); 233 copyComments(head, body); 234 } 235 else { 236 body = func.body; 237 } 238 239 const newNode = factory.createArrowFunction(func.modifiers, func.typeParameters, func.parameters, func.type, factory.createToken(SyntaxKind.EqualsGreaterThanToken), body); 240 return textChanges.ChangeTracker.with(context, t => t.replaceNode(file, func, newNode)); 241 } 242 243 function canBeConvertedToExpression(body: Block, head: Statement): head is ReturnStatement { 244 return body.statements.length === 1 && ((isReturnStatement(head) && !!head.expression)); 245 } 246 247 function isFunctionReferencedInFile(sourceFile: SourceFile, typeChecker: TypeChecker, node: FunctionExpression): boolean { 248 return !!node.name && FindAllReferences.Core.isSymbolReferencedInFile(node.name, typeChecker, sourceFile); 249 } 250} 251