• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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