• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/* @internal */
2namespace ts.codefix {
3    const fixId = "convertFunctionToEs6Class";
4    const errorCodes = [Diagnostics.This_constructor_function_may_be_converted_to_a_class_declaration.code];
5    registerCodeFix({
6        errorCodes,
7        getCodeActions(context: CodeFixContext) {
8            const changes = textChanges.ChangeTracker.with(context, t =>
9                doChange(t, context.sourceFile, context.span.start, context.program.getTypeChecker(), context.preferences, context.program.getCompilerOptions()));
10            return [createCodeFixAction(fixId, changes, Diagnostics.Convert_function_to_an_ES2015_class, fixId, Diagnostics.Convert_all_constructor_functions_to_classes)];
11        },
12        fixIds: [fixId],
13        getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, err) =>
14            doChange(changes, err.file, err.start, context.program.getTypeChecker(), context.preferences, context.program.getCompilerOptions())),
15    });
16
17    function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, position: number, checker: TypeChecker, preferences: UserPreferences, compilerOptions: CompilerOptions): void {
18        const ctorSymbol = checker.getSymbolAtLocation(getTokenAtPosition(sourceFile, position))!;
19        if (!ctorSymbol || !(ctorSymbol.flags & (SymbolFlags.Function | SymbolFlags.Variable))) {
20            // Bad input
21            return undefined;
22        }
23
24        const ctorDeclaration = ctorSymbol.valueDeclaration;
25        if (isFunctionDeclaration(ctorDeclaration)) {
26            changes.replaceNode(sourceFile, ctorDeclaration, createClassFromFunctionDeclaration(ctorDeclaration));
27        }
28        else if (isVariableDeclaration(ctorDeclaration)) {
29            const classDeclaration = createClassFromVariableDeclaration(ctorDeclaration);
30            if (!classDeclaration) {
31                return undefined;
32            }
33
34            const ancestor = ctorDeclaration.parent.parent;
35            if (isVariableDeclarationList(ctorDeclaration.parent) && ctorDeclaration.parent.declarations.length > 1) {
36                changes.delete(sourceFile, ctorDeclaration);
37                changes.insertNodeAfter(sourceFile, ancestor, classDeclaration);
38            }
39            else {
40                changes.replaceNode(sourceFile, ancestor, classDeclaration);
41            }
42        }
43
44        function createClassElementsFromSymbol(symbol: Symbol) {
45            const memberElements: ClassElement[] = [];
46            // all instance members are stored in the "member" array of symbol
47            if (symbol.members) {
48                symbol.members.forEach((member, key) => {
49                    if (key === "constructor") {
50                        // fn.prototype.constructor = fn
51                        changes.delete(sourceFile, member.valueDeclaration.parent);
52                        return;
53                    }
54                    const memberElement = createClassElement(member, /*modifiers*/ undefined);
55                    if (memberElement) {
56                        memberElements.push(...memberElement);
57                    }
58                });
59            }
60
61            // all static members are stored in the "exports" array of symbol
62            if (symbol.exports) {
63                symbol.exports.forEach(member => {
64                    if (member.name === "prototype") {
65                        const firstDeclaration = member.declarations[0];
66                        // only one "x.prototype = { ... }" will pass
67                        if (member.declarations.length === 1 &&
68                            isPropertyAccessExpression(firstDeclaration) &&
69                            isBinaryExpression(firstDeclaration.parent) &&
70                            firstDeclaration.parent.operatorToken.kind === SyntaxKind.EqualsToken &&
71                            isObjectLiteralExpression(firstDeclaration.parent.right)
72                        ) {
73                            const prototypes = firstDeclaration.parent.right;
74                            const memberElement = createClassElement(prototypes.symbol, /** modifiers */ undefined);
75                            if (memberElement) {
76                                memberElements.push(...memberElement);
77                            }
78                        }
79                    }
80                    else {
81                        const memberElement = createClassElement(member, [factory.createToken(SyntaxKind.StaticKeyword)]);
82                        if (memberElement) {
83                            memberElements.push(...memberElement);
84                        }
85                    }
86                });
87            }
88
89            return memberElements;
90
91            function shouldConvertDeclaration(_target: AccessExpression | ObjectLiteralExpression, source: Expression) {
92                // Right now the only thing we can convert are function expressions, get/set accessors and methods
93                // other values like normal value fields ({a: 1}) shouldn't get transformed.
94                // We can update this once ES public class properties are available.
95                if (isAccessExpression(_target)) {
96                    if (isPropertyAccessExpression(_target) && isConstructorAssignment(_target)) return true;
97                    return isFunctionLike(source);
98                }
99                else {
100                    return every(_target.properties, property => {
101                        // a() {}
102                        if (isMethodDeclaration(property) || isGetOrSetAccessorDeclaration(property)) return true;
103                        // a: function() {}
104                        if (isPropertyAssignment(property) && isFunctionExpression(property.initializer) && !!property.name) return true;
105                        // x.prototype.constructor = fn
106                        if (isConstructorAssignment(property)) return true;
107                        return false;
108                    });
109                }
110            }
111
112            function createClassElement(symbol: Symbol, modifiers: Modifier[] | undefined): readonly ClassElement[] {
113                // Right now the only thing we can convert are function expressions, which are marked as methods
114                // or { x: y } type prototype assignments, which are marked as ObjectLiteral
115                const members: ClassElement[] = [];
116                if (!(symbol.flags & SymbolFlags.Method) && !(symbol.flags & SymbolFlags.ObjectLiteral)) {
117                    return members;
118                }
119
120                const memberDeclaration = symbol.valueDeclaration as AccessExpression | ObjectLiteralExpression;
121                const assignmentBinaryExpression = memberDeclaration.parent as BinaryExpression;
122                const assignmentExpr = assignmentBinaryExpression.right;
123                if (!shouldConvertDeclaration(memberDeclaration, assignmentExpr)) {
124                    return members;
125                }
126
127                // delete the entire statement if this expression is the sole expression to take care of the semicolon at the end
128                const nodeToDelete = assignmentBinaryExpression.parent && assignmentBinaryExpression.parent.kind === SyntaxKind.ExpressionStatement
129                    ? assignmentBinaryExpression.parent : assignmentBinaryExpression;
130                changes.delete(sourceFile, nodeToDelete);
131
132                if (!assignmentExpr) {
133                    members.push(factory.createPropertyDeclaration([], modifiers, symbol.name, /*questionToken*/ undefined,
134                        /*type*/ undefined, /*initializer*/ undefined));
135                    return members;
136                }
137
138                // f.x = expr
139                if (isAccessExpression(memberDeclaration) && (isFunctionExpression(assignmentExpr) || isArrowFunction(assignmentExpr))) {
140                    const quotePreference = getQuotePreference(sourceFile, preferences);
141                    const name = tryGetPropertyName(memberDeclaration, compilerOptions, quotePreference);
142                    if (name) {
143                        return createFunctionLikeExpressionMember(members, assignmentExpr, name);
144                    }
145                    return members;
146                }
147                // f.prototype = { ... }
148                else if (isObjectLiteralExpression(assignmentExpr)) {
149                    return flatMap(
150                        assignmentExpr.properties,
151                        property => {
152                            if (isMethodDeclaration(property) || isGetOrSetAccessorDeclaration(property)) {
153                                // MethodDeclaration and AccessorDeclaration can appear in a class directly
154                                return members.concat(property);
155                            }
156                            if (isPropertyAssignment(property) && isFunctionExpression(property.initializer)) {
157                                return createFunctionLikeExpressionMember(members, property.initializer, property.name);
158                            }
159                            // Drop constructor assignments
160                            if (isConstructorAssignment(property)) return members;
161                            return [];
162                        }
163                    );
164                }
165                else {
166                    // Don't try to declare members in JavaScript files
167                    if (isSourceFileJS(sourceFile)) return members;
168                    if (!isPropertyAccessExpression(memberDeclaration)) return members;
169                    const prop = factory.createPropertyDeclaration(/*decorators*/ undefined, modifiers, memberDeclaration.name, /*questionToken*/ undefined, /*type*/ undefined, assignmentExpr);
170                    copyLeadingComments(assignmentBinaryExpression.parent, prop, sourceFile);
171                    members.push(prop);
172                    return members;
173                }
174
175                function createFunctionLikeExpressionMember(members: readonly ClassElement[], expression: FunctionExpression | ArrowFunction, name: PropertyName) {
176                    if (isFunctionExpression(expression)) return createFunctionExpressionMember(members, expression, name);
177                    else return createArrowFunctionExpressionMember(members, expression, name);
178                }
179
180                function createFunctionExpressionMember(members: readonly ClassElement[], functionExpression: FunctionExpression, name: PropertyName) {
181                    const fullModifiers = concatenate(modifiers, getModifierKindFromSource(functionExpression, SyntaxKind.AsyncKeyword));
182                    const method = factory.createMethodDeclaration(/*decorators*/ undefined, fullModifiers, /*asteriskToken*/ undefined, name, /*questionToken*/ undefined,
183                        /*typeParameters*/ undefined, functionExpression.parameters, /*type*/ undefined, functionExpression.body);
184                    copyLeadingComments(assignmentBinaryExpression, method, sourceFile);
185                    return members.concat(method);
186                }
187
188                function createArrowFunctionExpressionMember(members: readonly ClassElement[], arrowFunction: ArrowFunction, name: PropertyName) {
189                    const arrowFunctionBody = arrowFunction.body;
190                    let bodyBlock: Block;
191
192                    // case 1: () => { return [1,2,3] }
193                    if (arrowFunctionBody.kind === SyntaxKind.Block) {
194                        bodyBlock = arrowFunctionBody as Block;
195                    }
196                    // case 2: () => [1,2,3]
197                    else {
198                        bodyBlock = factory.createBlock([factory.createReturnStatement(arrowFunctionBody)]);
199                    }
200                    const fullModifiers = concatenate(modifiers, getModifierKindFromSource(arrowFunction, SyntaxKind.AsyncKeyword));
201                    const method = factory.createMethodDeclaration(/*decorators*/ undefined, fullModifiers, /*asteriskToken*/ undefined, name, /*questionToken*/ undefined,
202                        /*typeParameters*/ undefined, arrowFunction.parameters, /*type*/ undefined, bodyBlock);
203                    copyLeadingComments(assignmentBinaryExpression, method, sourceFile);
204                    return members.concat(method);
205                }
206            }
207        }
208
209        function createClassFromVariableDeclaration(node: VariableDeclaration): ClassDeclaration | undefined {
210            const initializer = node.initializer;
211            if (!initializer || !isFunctionExpression(initializer) || !isIdentifier(node.name)) {
212                return undefined;
213            }
214
215            const memberElements = createClassElementsFromSymbol(node.symbol);
216            if (initializer.body) {
217                memberElements.unshift(factory.createConstructorDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, initializer.parameters, initializer.body));
218            }
219
220            const modifiers = getModifierKindFromSource(node.parent.parent, SyntaxKind.ExportKeyword);
221            const cls = factory.createClassDeclaration(/*decorators*/ undefined, modifiers, node.name,
222                /*typeParameters*/ undefined, /*heritageClauses*/ undefined, memberElements);
223            // Don't call copyComments here because we'll already leave them in place
224            return cls;
225        }
226
227        function createClassFromFunctionDeclaration(node: FunctionDeclaration): ClassDeclaration {
228            const memberElements = createClassElementsFromSymbol(ctorSymbol);
229            if (node.body) {
230                memberElements.unshift(factory.createConstructorDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, node.parameters, node.body));
231            }
232
233            const modifiers = getModifierKindFromSource(node, SyntaxKind.ExportKeyword);
234            const cls = factory.createClassDeclaration(/*decorators*/ undefined, modifiers, node.name,
235                /*typeParameters*/ undefined, /*heritageClauses*/ undefined, memberElements);
236            // Don't call copyComments here because we'll already leave them in place
237            return cls;
238        }
239    }
240
241    function getModifierKindFromSource(source: Node, kind: SyntaxKind): readonly Modifier[] | undefined {
242        return filter(source.modifiers, modifier => modifier.kind === kind);
243    }
244
245    function isConstructorAssignment(x: ObjectLiteralElementLike | PropertyAccessExpression) {
246        if (!x.name) return false;
247        if (isIdentifier(x.name) && x.name.text === "constructor") return true;
248        return false;
249    }
250
251    function tryGetPropertyName(node: AccessExpression, compilerOptions: CompilerOptions, quotePreference: QuotePreference): PropertyName | undefined {
252        if (isPropertyAccessExpression(node)) {
253            return node.name;
254        }
255
256        const propName = node.argumentExpression;
257        if (isNumericLiteral(propName)) {
258            return propName;
259        }
260
261        if (isStringLiteralLike(propName)) {
262            return isIdentifierText(propName.text, compilerOptions.target) ? factory.createIdentifier(propName.text)
263                : isNoSubstitutionTemplateLiteral(propName) ? factory.createStringLiteral(propName.text, quotePreference === QuotePreference.Single)
264                : propName;
265        }
266
267        return undefined;
268    }
269}
270