Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve no-mocha-arrows performance #288

Merged
merged 8 commits into from
May 26, 2021
11 changes: 7 additions & 4 deletions lib/rules/no-exports.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ module.exports = {
const astUtils = createAstUtils(context.settings);
const exportNodes = [];
let hasTestCase = false;
const isTestCase = astUtils.buildIsTestCaseAnswerer();
const isDescribe = astUtils.buildIsDescribeAnswerer();
const isMochaFunctionCall = astUtils.buildIsMochaFunctionCallAnswerer(
isTestCase,
isDescribe
);

function isCommonJsExport(node) {
if (node.type === 'MemberExpression') {
Expand All @@ -37,10 +43,7 @@ module.exports = {
},

CallExpression(node) {
if (
!hasTestCase &&
astUtils.isMochaFunctionCall(node, context.getScope())
) {
if (!hasTestCase && isMochaFunctionCall(node, context)) {
hasTestCase = true;
}
},
Expand Down
154 changes: 91 additions & 63 deletions lib/rules/no-mocha-arrows.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,83 +7,111 @@

const createAstUtils = require('../util/ast');

function extractSourceTextByRange(sourceCode, start, end) {
return sourceCode.text.slice(start, end).trim();
}

// eslint-disable-next-line max-statements
function formatFunctionHead(sourceCode, fn) {
const arrow = sourceCode.getTokenBefore(fn.body);
const beforeArrowToken = sourceCode.getTokenBefore(arrow);
let firstToken = sourceCode.getFirstToken(fn);

let functionKeyword = 'function';
let params = extractSourceTextByRange(
sourceCode,
firstToken.range[0],
beforeArrowToken.range[1]
);
if (fn.async) {
// When 'async' specified strip the token from the params text
// and prepend it to the function keyword
params = params.slice(firstToken.range[1] - firstToken.range[0]).trim();
functionKeyword = 'async function';

// Advance firstToken pointer
firstToken = sourceCode.getTokenAfter(firstToken);
}

const beforeArrowComment = extractSourceTextByRange(
sourceCode,
beforeArrowToken.range[1],
arrow.range[0]
);
const afterArrowComment = extractSourceTextByRange(
sourceCode,
arrow.range[1],
fn.body.range[0]
);
let paramsFullText;
if (firstToken.type !== 'Punctuator') {
paramsFullText = `(${params}${beforeArrowComment})${afterArrowComment}`;
} else {
paramsFullText = `${params}${beforeArrowComment}${afterArrowComment}`;
}

return `${functionKeyword}${paramsFullText} `;
}

function fixArrowFunction(fixer, sourceCode, fn) {
if (fn.body.type === 'BlockStatement') {
// When it((...) => { ... }),
// simply replace '(...) => ' with 'function () '
return fixer.replaceTextRange(
[ fn.range[0], fn.body.range[0] ],
formatFunctionHead(sourceCode, fn)
);
}

const bodyText = sourceCode.getText(fn.body);
return fixer.replaceTextRange(
fn.range,
`${formatFunctionHead(sourceCode, fn)}{ return ${bodyText}; }`
);
}

module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'Disallow arrow functions as arguments to mocha functions'
description:
'Disallow arrow functions as arguments to mocha functions'
},
fixable: 'code'
},
create(context) {
const astUtils = createAstUtils(context.settings);
const sourceCode = context.getSourceCode();

function extractSourceTextByRange(start, end) {
return sourceCode.text.slice(start, end).trim();
}

// eslint-disable-next-line max-statements
function formatFunctionHead(fn) {
const arrow = sourceCode.getTokenBefore(fn.body);
const beforeArrowToken = sourceCode.getTokenBefore(arrow);
let firstToken = sourceCode.getFirstToken(fn);

let functionKeyword = 'function';
let params = extractSourceTextByRange(firstToken.range[0], beforeArrowToken.range[1]);
if (fn.async) {
// When 'async' specified strip the token from the params text
// and prepend it to the function keyword
params = params.slice(firstToken.range[1] - firstToken.range[0]).trim();
functionKeyword = 'async function';

// Advance firstToken pointer
firstToken = sourceCode.getTokenAfter(firstToken);
}

const beforeArrowComment = extractSourceTextByRange(beforeArrowToken.range[1], arrow.range[0]);
const afterArrowComment = extractSourceTextByRange(arrow.range[1], fn.body.range[0]);
let paramsFullText;
if (firstToken.type !== 'Punctuator') {
paramsFullText = `(${params}${beforeArrowComment})${afterArrowComment}`;
} else {
paramsFullText = `${params}${beforeArrowComment}${afterArrowComment}`;
}

return `${functionKeyword}${paramsFullText} `;
}

function fixArrowFunction(fixer, fn) {
if (fn.body.type === 'BlockStatement') {
// When it((...) => { ... }),
// simply replace '(...) => ' with 'function () '
return fixer.replaceTextRange(
[ fn.range[0], fn.body.range[0] ],
formatFunctionHead(fn)
);
}

const bodyText = sourceCode.text.slice(fn.body.range[0], fn.body.range[1]);
return fixer.replaceTextRange(
[ fn.range[0], fn.range[1] ],
`${formatFunctionHead(fn)}{ return ${ bodyText }; }`
);
}
const isTestCase = astUtils.buildIsTestCaseAnswerer();
const isDescribe = astUtils.buildIsDescribeAnswerer();
const isMochaFunctionCall = astUtils.buildIsMochaFunctionCallAnswerer(
isTestCase,
isDescribe
);

return {
CallExpression(node) {
const name = astUtils.getNodeName(node.callee);
if (isMochaFunctionCall(node, context)) {
const amountOfArguments = node.arguments.length;

if (amountOfArguments > 0) {
const lastArgument =
node.arguments[amountOfArguments - 1];

if (astUtils.isMochaFunctionCall(node, context.getScope())) {
const fnArg = node.arguments.slice(-1)[0];
if (fnArg && fnArg.type === 'ArrowFunctionExpression') {
context.report({
node,
message: `Do not pass arrow functions to ${ name }()`,
fix(fixer) {
return fixArrowFunction(fixer, fnArg);
}
});
if (lastArgument.type === 'ArrowFunctionExpression') {
const name = astUtils.getNodeName(node.callee);
context.report({
node,
message: `Do not pass arrow functions to ${name}()`,
fix(fixer) {
return fixArrowFunction(
fixer,
sourceCode,
lastArgument
);
}
});
}
}
}
}
Expand Down
18 changes: 15 additions & 3 deletions lib/rules/no-nested-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,20 @@ module.exports = {
return isNested && isTest;
}

function checkForAndReportErrors(node, isTestCase, isDescribe, isHookCall) {
function checkForAndReportErrors(
node,
isTestCase,
isDescribe,
isHookCall
) {
if (isNestedTest(isTestCase, isDescribe, testNestingLevel)) {
const message = isDescribe ?
'Unexpected suite nested within a test.' :
'Unexpected test nested within another test.';
report(node, message);
} else if (isNestedTest(isTestCase, isHookCall, hookCallNestingLevel)) {
} else if (
isNestedTest(isTestCase, isHookCall, hookCallNestingLevel)
) {
const message = isHookCall ?
'Unexpected test hook nested within a test hook.' :
'Unexpected test nested within a test hook.';
Expand All @@ -50,7 +57,12 @@ module.exports = {
const isHookCall = astUtils.isHookCall(node);
const isDescribe = astUtils.isDescribe(node);

checkForAndReportErrors(node, isTestCase, isDescribe, isHookCall);
checkForAndReportErrors(
node,
isTestCase,
isDescribe,
isHookCall
);

if (isTestCase) {
testNestingLevel += 1;
Expand Down