Skip to content

Commit

Permalink
Jasmine/Jest v20+: this syntax codemod
Browse files Browse the repository at this point in the history
  • Loading branch information
avaly committed Jul 18, 2017
1 parent 39d71e6 commit 1be51ac
Show file tree
Hide file tree
Showing 3 changed files with 517 additions and 0 deletions.
6 changes: 6 additions & 0 deletions src/cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@ 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,
TRANSFORMER_CHAI_ASSERT,
// TRANSFORMER_CHAI_SHOULD & TRANSFORMER_SHOULD doesn't have import detection
TRANSFORMER_MOCHA,
TRANSFORMER_TAPE,
TRANSFORMER_JASMINE_THIS,
];

function supportFailure(supportedItems) {
Expand Down Expand Up @@ -76,6 +78,10 @@ inquirer
name: 'Chai: Should/Expect BDD Syntax',
value: TRANSFORMER_CHAI_SHOULD,
},
{
name: 'Jasmine: this Syntax',
value: TRANSFORMER_JASMINE_THIS,
},
{
name: 'Mocha',
value: TRANSFORMER_MOCHA,
Expand Down
201 changes: 201 additions & 0 deletions src/transformers/jasmine-this.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/**
* Codemod for transforming Jamine `this` context into Jest v20+ compatible syntax.
*/

import detectQuoteStyle from '../utils/quote-style';

const testFunctionNames = ['after', 'afterEach', 'before', 'beforeEach', 'it', 'test'];

const allFunctionNames = ['describe'].concat(testFunctionNames);

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)
);
}

function reverseComparator(a, b) {
return a < b ? 1 : -1;
}

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

const getValidThisExpressions = node => {
return j(node)
.find(j.MemberExpression, {
object: {
type: j.ThisExpression.name,
},
})
.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(reverseComparator)
.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 =>
j.arrowFunctionExpression(
path.value.params,
path.value.body,
path.value.expression
)
)
.size();
};

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

if (!mutations) {
return null;
}

// As Recast is not preserving original quoting, we try to detect it,
// and default to something sane.
const quote = detectQuoteStyle(j, root) || 'single';
return root.toSource({ quote });
}
Loading

0 comments on commit 1be51ac

Please sign in to comment.