Skip to content

Commit

Permalink
Jasmine/Jest v20+: this syntax codemod
Browse files Browse the repository at this point in the history
Closes #57

Context: jestjs/jest#3553
  • Loading branch information
avaly committed Jul 24, 2017
1 parent 0fcfb00 commit 6240017
Show file tree
Hide file tree
Showing 4 changed files with 578 additions and 1 deletion.
6 changes: 6 additions & 0 deletions src/cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const TRANSFORMER_CHAI_SHOULD = 'chai-should';
const TRANSFORMER_MOCHA = 'mocha';
const TRANSFORMER_SHOULD = 'should';
const TRANSFORMER_TAPE = 'tape';
const TRANSFORMER_JASMINE_THIS = 'jasmine-this';

const ALL_TRANSFORMERS = [
TRANSFORMER_AVA,
Expand All @@ -50,6 +51,7 @@ const ALL_TRANSFORMERS = [
// TODO: waiting for expect@20+ release: TRANSFORMER_EXPECT,
TRANSFORMER_MOCHA,
TRANSFORMER_TAPE,
TRANSFORMER_JASMINE_THIS,
];

function supportFailure(supportedItems) {
Expand Down Expand Up @@ -85,6 +87,10 @@ inquirer
value: TRANSFORMER_EXPECT,
},
*/
{
name: 'Jasmine: this usage',
value: TRANSFORMER_JASMINE_THIS,
},
{
name: 'Mocha',
value: TRANSFORMER_MOCHA,
Expand Down
200 changes: 200 additions & 0 deletions src/transformers/jasmine-this.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/**
* Codemod for transforming Jasmine `this` context into Jest v20+ compatible syntax.
*/

import finale from '../utils/finale';

const testFunctionNames = ['after', 'afterEach', 'before', 'beforeEach', 'it', 'test'];
const allFunctionNames = ['describe'].concat(testFunctionNames);
const ignoredIdentifiers = ['retries', 'skip', 'slow', 'timeout'];

function isFunctionExpressionWithinSpecificFunctions(path, acceptedFunctionNames) {
if (!path || !path.parentPath || !Array.isArray(path.parentPath.value)) {
return false;
}

const callExpressionPath = path.parentPath.parentPath;

return (
!!callExpressionPath &&
!!callExpressionPath.value &&
callExpressionPath.value.callee.type === 'Identifier' &&
acceptedFunctionNames.indexOf(callExpressionPath.value.callee.name) > -1
);
}

function isWithinObjectOrClass(path) {
const invalidParentTypes = ['Property', 'MethodDefinition'];
let currentPath = path;

while (
currentPath &&
currentPath.value &&
invalidParentTypes.indexOf(currentPath.value.type) === -1
) {
currentPath = currentPath.parentPath;
}
return currentPath ? invalidParentTypes.indexOf(currentPath.value.type) > -1 : false;
}

function isWithinSpecificFunctions(path, acceptedFunctionNames, matchAll) {
if (!matchAll) {
// Do not replace within functions declared as object properties or class methods
// See `transforms plain functions within lifecycle methods` test
if (isWithinObjectOrClass(path)) {
return false;
}
}
let currentPath = path;

while (
currentPath &&
currentPath.value &&
currentPath.value.type !== 'FunctionExpression'
) {
currentPath = currentPath.parentPath;
}

return (
isFunctionExpressionWithinSpecificFunctions(currentPath, acceptedFunctionNames) ||
(currentPath
? isWithinSpecificFunctions(currentPath.parentPath, testFunctionNames, false)
: false)
);
}

export default function jasmineThis(fileInfo, api, options) {
const j = api.jscodeshift;
const root = j(fileInfo.source);

const getValidThisExpressions = node => {
return j(node)
.find(j.MemberExpression, {
object: {
type: j.ThisExpression.name,
},
property: {
name: name => ignoredIdentifiers.indexOf(name) === -1,
},
})
.filter(path => isWithinSpecificFunctions(path, allFunctionNames, true));
};

const mutateScope = (ast, body) => {
const replacedIdentifiersMap = {};

const updateThisExpressions = () => {
return ast
.find(j.MemberExpression)
.filter(path => path.value.object.type === 'ThisExpression')
.filter(path => isWithinSpecificFunctions(path, allFunctionNames, true))
.replaceWith(replaceThisExpression)
.size();
};

const replaceThisExpression = path => {
const originalName = path.value.property.name;
const newName = originalName + 'Context';
replacedIdentifiersMap[originalName] = newName;
return j.identifier(newName);
};

const addLetDeclarations = () => {
Object.keys(replacedIdentifiersMap)
// Reverse the keys because we're adding them one by one to the front of the body array
.sort()
.reverse()
.forEach(originalName => {
body.unshift(
j.variableDeclaration('let', [
j.variableDeclarator(
j.identifier(replacedIdentifiersMap[originalName]),
null
),
])
);
});
};

updateThisExpressions();
addLetDeclarations();
};

const mutateDescribe = path => {
const functionExpression = path.value.arguments.find(
node =>
node.type === 'FunctionExpression' ||
node.type === 'ArrowFunctionExpression'
);
const functionBody = functionExpression.body;
const ast = j(functionBody);

mutateScope(ast, functionBody.body);
};

const updateRoot = () => {
const topLevelLifecycleMethods = root
.find(j.CallExpression, {
callee: {
type: j.Identifier.name,
name: name => testFunctionNames.indexOf(name) > -1,
},
})
// Find only lifecyle methods which are in the root scope
.filter(
path =>
path.parentPath.value.type === j.ExpressionStatement.name &&
Array.isArray(path.parentPath.parentPath.value) &&
path.parentPath.parentPath.parentPath.value.type === j.Program.name
)
.filter(path => getValidThisExpressions(path.value).size() > 0)
.size();

if (topLevelLifecycleMethods > 0) {
const path = root.get();
mutateScope(root, path.value.program.body);
return 1;
}

return 0;
};

const updateDescribes = () => {
return root
.find(j.CallExpression, {
callee: {
type: j.Identifier.name,
name: 'describe',
},
})
.filter(path => getValidThisExpressions(path.value).size() > 0)
.forEach(mutateDescribe)
.size();
};

const updateFunctionExpressions = () => {
return root
.find(j.FunctionExpression)
.filter(path =>
isFunctionExpressionWithinSpecificFunctions(path, allFunctionNames)
)
.replaceWith(path => {
const newFn = j.arrowFunctionExpression(
path.value.params,
path.value.body,
path.value.expression
);
newFn.async = path.value.async;
return newFn;
})
.size();
};

const mutations = updateRoot() + updateDescribes() + updateFunctionExpressions();

if (!mutations) {
return null;
}

return finale(fileInfo, j, root, options);
}
Loading

0 comments on commit 6240017

Please sign in to comment.