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