• 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.valueDeclaration || !(ctorSymbol.flags & (SymbolFlags.Function | SymbolFlags.Variable))) {
20            // Bad input
21            return undefined;
22        }
23
24        const ctorDeclaration = ctorSymbol.valueDeclaration;
25        if (isFunctionDeclaration(ctorDeclaration) || isFunctionExpression(ctorDeclaration)) {
26            changes.replaceNode(sourceFile, ctorDeclaration, createClassFromFunction(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 static members are stored in the "exports" array of symbol
47            if (symbol.exports) {
48                symbol.exports.forEach(member => {
49                    if (member.name === "prototype" && member.declarations) {
50                        const firstDeclaration = member.declarations[0];
51                        // only one "x.prototype = { ... }" will pass
52                        if (member.declarations.length === 1 &&
53                            isPropertyAccessExpression(firstDeclaration) &&
54                            isBinaryExpression(firstDeclaration.parent) &&
55                            firstDeclaration.parent.operatorToken.kind === SyntaxKind.EqualsToken &&
56                            isObjectLiteralExpression(firstDeclaration.parent.right)
57                        ) {
58                            const prototypes = firstDeclaration.parent.right;
59                            createClassElement(prototypes.symbol, /** modifiers */ undefined, memberElements);
60                        }
61                    }
62                    else {
63                        createClassElement(member, [factory.createToken(SyntaxKind.StaticKeyword)], memberElements);
64                    }
65                });
66            }
67
68            // all instance members are stored in the "member" array of symbol (done last so instance members pulled from prototype assignments have priority)
69            if (symbol.members) {
70                symbol.members.forEach((member, key) => {
71                    if (key === "constructor" && member.valueDeclaration) {
72                        const prototypeAssignment = symbol.exports?.get("prototype" as __String)?.declarations?.[0]?.parent;
73                        if (prototypeAssignment && isBinaryExpression(prototypeAssignment) && isObjectLiteralExpression(prototypeAssignment.right) && some(prototypeAssignment.right.properties, isConstructorAssignment)) {
74                            // fn.prototype = { constructor: fn }
75                            // Already deleted in `createClassElement` in first pass
76                        }
77                        else {
78                            // fn.prototype.constructor = fn
79                            changes.delete(sourceFile, member.valueDeclaration.parent);
80                        }
81                        return;
82                    }
83                    createClassElement(member, /*modifiers*/ undefined, memberElements);
84                });
85            }
86
87            return memberElements;
88
89            function shouldConvertDeclaration(_target: AccessExpression | ObjectLiteralExpression, source: Expression) {
90                // Right now the only thing we can convert are function expressions, get/set accessors and methods
91                // other values like normal value fields ({a: 1}) shouldn't get transformed.
92                // We can update this once ES public class properties are available.
93                if (isAccessExpression(_target)) {
94                    if (isPropertyAccessExpression(_target) && isConstructorAssignment(_target)) return true;
95                    return isFunctionLike(source);
96                }
97                else {
98                    return every(_target.properties, property => {
99                        // a() {}
100                        if (isMethodDeclaration(property) || isGetOrSetAccessorDeclaration(property)) return true;
101                        // a: function() {}
102                        if (isPropertyAssignment(property) && isFunctionExpression(property.initializer) && !!property.name) return true;
103                        // x.prototype.constructor = fn
104                        if (isConstructorAssignment(property)) return true;
105                        return false;
106                    });
107                }
108            }
109
110            function createClassElement(symbol: Symbol, modifiers: Modifier[] | undefined, members: ClassElement[]): void {
111                // Right now the only thing we can convert are function expressions, which are marked as methods
112                // or { x: y } type prototype assignments, which are marked as ObjectLiteral
113                if (!(symbol.flags & SymbolFlags.Method) && !(symbol.flags & SymbolFlags.ObjectLiteral)) {
114                    return;
115                }
116
117                const memberDeclaration = symbol.valueDeclaration as AccessExpression | ObjectLiteralExpression;
118                const assignmentBinaryExpression = memberDeclaration.parent as BinaryExpression;
119                const assignmentExpr = assignmentBinaryExpression.right;
120                if (!shouldConvertDeclaration(memberDeclaration, assignmentExpr)) {
121                    return;
122                }
123
124                if (some(members, m => {
125                    const name = getNameOfDeclaration(m);
126                    if (name && isIdentifier(name) && idText(name) === symbolName(symbol)) {
127                        return true; // class member already made for this name
128                    }
129                    return false;
130                })) {
131                    return;
132                }
133
134                // delete the entire statement if this expression is the sole expression to take care of the semicolon at the end
135                const nodeToDelete = assignmentBinaryExpression.parent && assignmentBinaryExpression.parent.kind === SyntaxKind.ExpressionStatement
136                    ? assignmentBinaryExpression.parent : assignmentBinaryExpression;
137                changes.delete(sourceFile, nodeToDelete);
138
139                if (!assignmentExpr) {
140                    members.push(factory.createPropertyDeclaration(modifiers, symbol.name, /*questionToken*/ undefined,
141                        /*type*/ undefined, /*initializer*/ undefined));
142                    return;
143                }
144
145                // f.x = expr
146                if (isAccessExpression(memberDeclaration) && (isFunctionExpression(assignmentExpr) || isArrowFunction(assignmentExpr))) {
147                    const quotePreference = getQuotePreference(sourceFile, preferences);
148                    const name = tryGetPropertyName(memberDeclaration, compilerOptions, quotePreference);
149                    if (name) {
150                        createFunctionLikeExpressionMember(members, assignmentExpr, name);
151                    }
152                    return;
153                }
154                // f.prototype = { ... }
155                else if (isObjectLiteralExpression(assignmentExpr)) {
156                    forEach(
157                        assignmentExpr.properties,
158                        property => {
159                            if (isMethodDeclaration(property) || isGetOrSetAccessorDeclaration(property)) {
160                                // MethodDeclaration and AccessorDeclaration can appear in a class directly
161                                members.push(property);
162                            }
163                            if (isPropertyAssignment(property) && isFunctionExpression(property.initializer)) {
164                                createFunctionLikeExpressionMember(members, property.initializer, property.name);
165                            }
166                            // Drop constructor assignments
167                            if (isConstructorAssignment(property)) return;
168                            return;
169                        }
170                    );
171                    return;
172                }
173                else {
174                    // Don't try to declare members in JavaScript files
175                    if (isSourceFileJS(sourceFile)) return;
176                    if (!isPropertyAccessExpression(memberDeclaration)) return;
177                    const prop = factory.createPropertyDeclaration(modifiers, memberDeclaration.name, /*questionToken*/ undefined, /*type*/ undefined, assignmentExpr);
178                    copyLeadingComments(assignmentBinaryExpression.parent, prop, sourceFile);
179                    members.push(prop);
180                    return;
181                }
182
183                function createFunctionLikeExpressionMember(members: ClassElement[], expression: FunctionExpression | ArrowFunction, name: PropertyName) {
184                    if (isFunctionExpression(expression)) return createFunctionExpressionMember(members, expression, name);
185                    else return createArrowFunctionExpressionMember(members, expression, name);
186                }
187
188                function createFunctionExpressionMember(members: ClassElement[], functionExpression: FunctionExpression, name: PropertyName) {
189                    const fullModifiers = concatenate(modifiers, getModifierKindFromSource(functionExpression, SyntaxKind.AsyncKeyword));
190                    const method = factory.createMethodDeclaration(fullModifiers, /*asteriskToken*/ undefined, name, /*questionToken*/ undefined,
191                        /*typeParameters*/ undefined, functionExpression.parameters, /*type*/ undefined, functionExpression.body);
192                    copyLeadingComments(assignmentBinaryExpression, method, sourceFile);
193                    members.push(method);
194                    return;
195                }
196
197                function createArrowFunctionExpressionMember(members: ClassElement[], arrowFunction: ArrowFunction, name: PropertyName) {
198                    const arrowFunctionBody = arrowFunction.body;
199                    let bodyBlock: Block;
200
201                    // case 1: () => { return [1,2,3] }
202                    if (arrowFunctionBody.kind === SyntaxKind.Block) {
203                        bodyBlock = arrowFunctionBody as Block;
204                    }
205                    // case 2: () => [1,2,3]
206                    else {
207                        bodyBlock = factory.createBlock([factory.createReturnStatement(arrowFunctionBody)]);
208                    }
209                    const fullModifiers = concatenate(modifiers, getModifierKindFromSource(arrowFunction, SyntaxKind.AsyncKeyword));
210                    const method = factory.createMethodDeclaration(fullModifiers, /*asteriskToken*/ undefined, name, /*questionToken*/ undefined,
211                        /*typeParameters*/ undefined, arrowFunction.parameters, /*type*/ undefined, bodyBlock);
212                    copyLeadingComments(assignmentBinaryExpression, method, sourceFile);
213                    members.push(method);
214                }
215            }
216        }
217
218        function createClassFromVariableDeclaration(node: VariableDeclaration): ClassDeclaration | undefined {
219            const initializer = node.initializer;
220            if (!initializer || !isFunctionExpression(initializer) || !isIdentifier(node.name)) {
221                return undefined;
222            }
223
224            const memberElements = createClassElementsFromSymbol(node.symbol);
225            if (initializer.body) {
226                memberElements.unshift(factory.createConstructorDeclaration(/*modifiers*/ undefined, initializer.parameters, initializer.body));
227            }
228
229            const modifiers = getModifierKindFromSource(node.parent.parent, SyntaxKind.ExportKeyword);
230            const cls = factory.createClassDeclaration(modifiers, node.name,
231                /*typeParameters*/ undefined, /*heritageClauses*/ undefined, memberElements);
232            // Don't call copyComments here because we'll already leave them in place
233            return cls;
234        }
235
236        function createClassFromFunction(node: FunctionDeclaration | FunctionExpression): ClassDeclaration {
237            const memberElements = createClassElementsFromSymbol(ctorSymbol);
238            if (node.body) {
239                memberElements.unshift(factory.createConstructorDeclaration(/*modifiers*/ undefined, node.parameters, node.body));
240            }
241
242            const modifiers = getModifierKindFromSource(node, SyntaxKind.ExportKeyword);
243            const cls = factory.createClassDeclaration(modifiers, node.name,
244                /*typeParameters*/ undefined, /*heritageClauses*/ undefined, memberElements);
245            // Don't call copyComments here because we'll already leave them in place
246            return cls;
247        }
248    }
249
250    function getModifierKindFromSource(source: Node, kind: Modifier["kind"]): readonly Modifier[] | undefined {
251        return canHaveModifiers(source) ? filter(source.modifiers, (modifier): modifier is Modifier => modifier.kind === kind) : undefined;
252    }
253
254    function isConstructorAssignment(x: ObjectLiteralElementLike | PropertyAccessExpression) {
255        if (!x.name) return false;
256        if (isIdentifier(x.name) && x.name.text === "constructor") return true;
257        return false;
258    }
259
260    function tryGetPropertyName(node: AccessExpression, compilerOptions: CompilerOptions, quotePreference: QuotePreference): PropertyName | undefined {
261        if (isPropertyAccessExpression(node)) {
262            return node.name;
263        }
264
265        const propName = node.argumentExpression;
266        if (isNumericLiteral(propName)) {
267            return propName;
268        }
269
270        if (isStringLiteralLike(propName)) {
271            return isIdentifierText(propName.text, getEmitScriptTarget(compilerOptions)) ? factory.createIdentifier(propName.text)
272                : isNoSubstitutionTemplateLiteral(propName) ? factory.createStringLiteral(propName.text, quotePreference === QuotePreference.Single)
273                : propName;
274        }
275
276        return undefined;
277    }
278}
279