Skip to content

Instantly share code, notes, and snippets.

@timofei-iatsenko
Last active August 7, 2017 15:39
Show Gist options
  • Save timofei-iatsenko/91bbe7192c213f0a0076aaab5d1b7e15 to your computer and use it in GitHub Desktop.
Save timofei-iatsenko/91bbe7192c213f0a0076aaab5d1b7e15 to your computer and use it in GitHub Desktop.
Codemod: rewrite ES5 classes to ES6.
'use strict';
module.exports = function (file, api, options) {
const j = api.jscodeshift;
const source = j(file.source);
const body = source.find(j.Program).get('body');
delete body.value[0].comments;
let jobDone = false;
const classConstructor = source.find(j.FunctionDeclaration,
(node) => source.find(j.AssignmentExpression, isProtoAssignExp.bind(null, node.id.name)).length);
const superDeclaration = source.find(j.VariableDeclarator, (node) => node.id.name === '_super');
let parentClassName = null;
superDeclaration.forEach((ast) => {
if (ast.scope.isGlobal) {
parentClassName = ast.value.init.object.name;
j(ast).remove();
}
});
classConstructor.forEach((ast) => {
const prototypeAssignment = source.find(j.AssignmentExpression,
isProtoAssignExp.bind(null, ast.value.id.name))
.paths()[0];
if (!prototypeAssignment) {
return;
}
const properties = getPrototypeProperties(prototypeAssignment.value);
const clDeclaration = createClass(j, properties, ast.value, parentClassName);
const originalConstructorPos = ast.value.start;
j(ast).replaceWith(clDeclaration);
j(prototypeAssignment).remove();
const potentialUsages = source.find(j.Identifier, (node) => node.name === ast.value.id.name).paths();
// rearranging
if (!options.noRearranging && potentialUsages.length > 1 &&
potentialUsages[0].value.start < originalConstructorPos) {
const statement = getClosestForRearranging(j, potentialUsages[0]);
j(ast).insertAfter(statement.value);
j(statement).remove();
}
jobDone = true;
});
if (classConstructor.length === 0 || classConstructor.length > 1 || !jobDone) {
return null;
}
// replace call to super contructor
source.find(j.CallExpression, (node) => {
return node.callee.object && node.callee.object.name === parentClassName;
}).forEach((ast) => {
const exp = j.callExpression(j.super(), ast.value.arguments.slice(1));
j(ast).replaceWith(exp);
});
// replace all calls to _super
source.find(j.CallExpression, (node) => (
node.callee.object &&
node.callee.object.object &&
node.callee.object.object.name === '_super'
)).forEach((ast) => {
const callExp = j.callExpression(
j.memberExpression(j.super(), ast.value.callee.object.property),
ast.value.arguments.slice(1)
);
j(ast).replaceWith(callExp);
});
// remove extendHelper import declaration
source.find(j.ImportDeclaration, isExtendHelperImport).forEach((ast) => j(ast).remove());
return source.toSource({
arrowParensAlways: true,
quote: 'single',
trailingComma: {
objects: false,
arrays: false,
parameters: false,
},
});
};
function getClosestForRearranging(j, path) {
const types = [j.ExpressionStatement, j.FunctionDeclaration];
for(let type of types) {
const expression = j(path).closest(type);
if (expression.length) {
return expression.paths()[0];
}
}
}
function getPrototypeProperties(ast) {
if (ast.right.properties) {
return ast.right.properties;
}
if (ast.right.type === 'CallExpression' &&
ast.right.callee.name === 'extendPrototype') {
return ast.right.arguments[1].properties;
}
}
function createClass(j, properties, constructorFn, parentClass) {
let superClass = null;
if (parentClass) {
superClass = j.identifier(parentClass);
}
const members = properties.map((old) => createClassMember(j, old));
const constructor = createConstructor(j, constructorFn);
const bodyDef = j.classBody([constructor, ...members].filter((f) => f));
return j.classDeclaration(constructorFn.id, bodyDef, superClass);
}
function createConstructor(j, fn) {
if (!fn.body.body.length) {
return;
}
// delete redundant properies from jsdoc
if (fn.comments) {
const jsdoc = fn.comments[fn.comments.length - 1];
jsdoc.value = jsdoc.value.replace(/\s\* (@extends|@constructor).*\s/g, '');
}
return withComments(j.methodDefinition('constructor',
j.identifier('constructor'),
j.functionExpression(null, fn.params, fn.body)
), fn);
}
function createClassMember(j, old) {
if (old.key.name === 'constructor') {
return;
}
if (old.kind === 'init' && old.value.type === 'FunctionExpression') {
return withComments(j.methodDefinition('method', old.key, old.value), old);
}
if (old.kind === 'init') {
return withComments(j.classProperty(old.key, old.value, null), old);
}
return withComments(j.methodDefinition(old.kind, old.key, old.value), old);
}
function withComments(to, from) {
to.comments = from.comments;
return to;
}
function isExtendHelperImport(node) {
return (
node.type === 'ImportDeclaration' &&
node.specifiers.length > 0 &&
node.specifiers[0].imported &&
node.specifiers[0].imported.name === 'extendPrototype'
);
}
function isProtoAssignExp(whom, node) {
return (
node.left.object &&
node.left.object.name === whom &&
node.left.property.name === 'prototype'
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment