• 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,
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