Skip to content

Commit

Permalink
feat: Detect nested test cases (#249)
Browse files Browse the repository at this point in the history
* Feat: improve detection of `RuleTester` usage

* Fix: cover arrow function with no body

* Test: add more cases

* Fix: support if conditions

* Fix: support variable-defined functions

* Fix: support functions

* Fix: support functions

* Test: add a few more cases
  • Loading branch information
G-Rath authored May 16, 2022
1 parent b685b2a commit b33aa00
Show file tree
Hide file tree
Showing 4 changed files with 343 additions and 18 deletions.
136 changes: 118 additions & 18 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -377,20 +377,59 @@ module.exports = {
},

/**
* Performs static analysis on an AST to try to find test cases
* Extracts the body of a function if the given node is a function
*
* @param {ASTNode} node
* @returns {ExpressionStatement[]}
*/
extractFunctionBody(node) {
if (
node.type === 'ArrowFunctionExpression' ||
node.type === 'FunctionExpression'
) {
if (node.body.type === 'BlockStatement') {
return node.body.body;
}

return [node.body];
}

return [];
},

/**
* Checks the given statements for possible test info
*
* @param {RuleContext} context The `context` variable for the source file itself
* @param {ASTNode} ast The `Program` node for the file.
* @returns {object} An object with `valid` and `invalid` keys containing a list of AST nodes corresponding to tests
* @param {ASTNode[]} statements The statements to check
* @param {Set<ASTNode>} variableIdentifiers
* @returns {CallExpression[]}
*/
getTestInfo(context, ast) {
checkStatementsForTestInfo(
context,
statements,
variableIdentifiers = new Set()
) {
const runCalls = [];
const variableIdentifiers = new Set();

ast.body.forEach((statement) => {
for (const statement of statements) {
if (statement.type === 'VariableDeclaration') {
statement.declarations.forEach((declarator) => {
for (const declarator of statement.declarations) {
if (!declarator.init) {
continue;
}

const extracted = module.exports.extractFunctionBody(declarator.init);

runCalls.push(
...module.exports.checkStatementsForTestInfo(
context,
extracted,
variableIdentifiers
)
);

if (
declarator.init &&
isRuleTesterConstruction(declarator.init) &&
declarator.id.type === 'Identifier'
) {
Expand All @@ -400,21 +439,82 @@ module.exports = {
.forEach((ref) => variableIdentifiers.add(ref.identifier));
});
}
});
}
}

if (statement.type === 'FunctionDeclaration') {
runCalls.push(
...module.exports.checkStatementsForTestInfo(
context,
statement.body.body,
variableIdentifiers
)
);
}

if (statement.type === 'IfStatement') {
const body =
statement.consequent.type === 'BlockStatement'
? statement.consequent.body
: [statement.consequent];

runCalls.push(
...module.exports.checkStatementsForTestInfo(
context,
body,
variableIdentifiers
)
);

continue;
}

const expression =
statement.type === 'ExpressionStatement'
? statement.expression
: statement;

if (expression.type !== 'CallExpression') {
continue;
}

for (const arg of expression.arguments) {
const extracted = module.exports.extractFunctionBody(arg);

runCalls.push(
...module.exports.checkStatementsForTestInfo(
context,
extracted,
variableIdentifiers
)
);
}

if (
statement.type === 'ExpressionStatement' &&
statement.expression.type === 'CallExpression' &&
statement.expression.callee.type === 'MemberExpression' &&
(isRuleTesterConstruction(statement.expression.callee.object) ||
variableIdentifiers.has(statement.expression.callee.object)) &&
statement.expression.callee.property.type === 'Identifier' &&
statement.expression.callee.property.name === 'run'
expression.callee.type === 'MemberExpression' &&
(isRuleTesterConstruction(expression.callee.object) ||
variableIdentifiers.has(expression.callee.object)) &&
expression.callee.property.type === 'Identifier' &&
expression.callee.property.name === 'run'
) {
runCalls.push(statement.expression);
runCalls.push(expression);
}
});
}

return runCalls;
},

/**
* Performs static analysis on an AST to try to find test cases
* @param {RuleContext} context The `context` variable for the source file itself
* @param {ASTNode} ast The `Program` node for the file.
* @returns {object} An object with `valid` and `invalid` keys containing a list of AST nodes corresponding to tests
*/
getTestInfo(context, ast) {
const runCalls = module.exports.checkStatementsForTestInfo(
context,
ast.body
);

return runCalls
.filter(
Expand Down
29 changes: 29 additions & 0 deletions tests/lib/rules/no-identical-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,5 +196,34 @@ ruleTester.run('no-identical-tests', rule, {
`,
errors: [ERROR_STRING_TEST],
},
{
code: `
var foo = new RuleTester();
function testOperator(operator) {
foo.run('foo', bar, {
valid: [
\`$\{operator}\`,
\`$\{operator}\`,
],
invalid: []
});
}
`,
output: `
var foo = new RuleTester();
function testOperator(operator) {
foo.run('foo', bar, {
valid: [
\`$\{operator}\`,
],
invalid: []
});
}
`,
parserOptions: { ecmaVersion: 2015 },
errors: [{ messageId: 'identical', type: 'TemplateLiteral' }],
},
],
});
30 changes: 30 additions & 0 deletions tests/lib/rules/test-case-property-ordering.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,5 +172,35 @@ ruleTester.run('test-case-property-ordering', rule, {
},
],
},
{
code: `
var tester = new RuleTester();
describe('my tests', function() {
tester.run('foo', bar, {
valid: [
{\ncode: "foo",\noutput: "",\nerrors: ["baz"],\nparserOptions: "",\n},
]
});
});
`,
output: `
var tester = new RuleTester();
describe('my tests', function() {
tester.run('foo', bar, {
valid: [
{\ncode: "foo",\noutput: "",\nparserOptions: "",\nerrors: ["baz"],\n},
]
});
});
`,
errors: [
{
message:
'The properties of a test case should be placed in a consistent order: [code, output, parserOptions, errors].',
},
],
},
],
});
Loading

0 comments on commit b33aa00

Please sign in to comment.