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