Skip to content

Commit b33aa00

Browse files
authored
feat: Detect nested test cases (#249)
* 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
1 parent b685b2a commit b33aa00

File tree

4 files changed

+343
-18
lines changed

4 files changed

+343
-18
lines changed

Diff for: lib/utils.js

+118-18
Original file line numberDiff line numberDiff line change
@@ -377,20 +377,59 @@ module.exports = {
377377
},
378378

379379
/**
380-
* Performs static analysis on an AST to try to find test cases
380+
* Extracts the body of a function if the given node is a function
381+
*
382+
* @param {ASTNode} node
383+
* @returns {ExpressionStatement[]}
384+
*/
385+
extractFunctionBody(node) {
386+
if (
387+
node.type === 'ArrowFunctionExpression' ||
388+
node.type === 'FunctionExpression'
389+
) {
390+
if (node.body.type === 'BlockStatement') {
391+
return node.body.body;
392+
}
393+
394+
return [node.body];
395+
}
396+
397+
return [];
398+
},
399+
400+
/**
401+
* Checks the given statements for possible test info
402+
*
381403
* @param {RuleContext} context The `context` variable for the source file itself
382-
* @param {ASTNode} ast The `Program` node for the file.
383-
* @returns {object} An object with `valid` and `invalid` keys containing a list of AST nodes corresponding to tests
404+
* @param {ASTNode[]} statements The statements to check
405+
* @param {Set<ASTNode>} variableIdentifiers
406+
* @returns {CallExpression[]}
384407
*/
385-
getTestInfo(context, ast) {
408+
checkStatementsForTestInfo(
409+
context,
410+
statements,
411+
variableIdentifiers = new Set()
412+
) {
386413
const runCalls = [];
387-
const variableIdentifiers = new Set();
388414

389-
ast.body.forEach((statement) => {
415+
for (const statement of statements) {
390416
if (statement.type === 'VariableDeclaration') {
391-
statement.declarations.forEach((declarator) => {
417+
for (const declarator of statement.declarations) {
418+
if (!declarator.init) {
419+
continue;
420+
}
421+
422+
const extracted = module.exports.extractFunctionBody(declarator.init);
423+
424+
runCalls.push(
425+
...module.exports.checkStatementsForTestInfo(
426+
context,
427+
extracted,
428+
variableIdentifiers
429+
)
430+
);
431+
392432
if (
393-
declarator.init &&
394433
isRuleTesterConstruction(declarator.init) &&
395434
declarator.id.type === 'Identifier'
396435
) {
@@ -400,21 +439,82 @@ module.exports = {
400439
.forEach((ref) => variableIdentifiers.add(ref.identifier));
401440
});
402441
}
403-
});
442+
}
443+
}
444+
445+
if (statement.type === 'FunctionDeclaration') {
446+
runCalls.push(
447+
...module.exports.checkStatementsForTestInfo(
448+
context,
449+
statement.body.body,
450+
variableIdentifiers
451+
)
452+
);
453+
}
454+
455+
if (statement.type === 'IfStatement') {
456+
const body =
457+
statement.consequent.type === 'BlockStatement'
458+
? statement.consequent.body
459+
: [statement.consequent];
460+
461+
runCalls.push(
462+
...module.exports.checkStatementsForTestInfo(
463+
context,
464+
body,
465+
variableIdentifiers
466+
)
467+
);
468+
469+
continue;
470+
}
471+
472+
const expression =
473+
statement.type === 'ExpressionStatement'
474+
? statement.expression
475+
: statement;
476+
477+
if (expression.type !== 'CallExpression') {
478+
continue;
479+
}
480+
481+
for (const arg of expression.arguments) {
482+
const extracted = module.exports.extractFunctionBody(arg);
483+
484+
runCalls.push(
485+
...module.exports.checkStatementsForTestInfo(
486+
context,
487+
extracted,
488+
variableIdentifiers
489+
)
490+
);
404491
}
405492

406493
if (
407-
statement.type === 'ExpressionStatement' &&
408-
statement.expression.type === 'CallExpression' &&
409-
statement.expression.callee.type === 'MemberExpression' &&
410-
(isRuleTesterConstruction(statement.expression.callee.object) ||
411-
variableIdentifiers.has(statement.expression.callee.object)) &&
412-
statement.expression.callee.property.type === 'Identifier' &&
413-
statement.expression.callee.property.name === 'run'
494+
expression.callee.type === 'MemberExpression' &&
495+
(isRuleTesterConstruction(expression.callee.object) ||
496+
variableIdentifiers.has(expression.callee.object)) &&
497+
expression.callee.property.type === 'Identifier' &&
498+
expression.callee.property.name === 'run'
414499
) {
415-
runCalls.push(statement.expression);
500+
runCalls.push(expression);
416501
}
417-
});
502+
}
503+
504+
return runCalls;
505+
},
506+
507+
/**
508+
* Performs static analysis on an AST to try to find test cases
509+
* @param {RuleContext} context The `context` variable for the source file itself
510+
* @param {ASTNode} ast The `Program` node for the file.
511+
* @returns {object} An object with `valid` and `invalid` keys containing a list of AST nodes corresponding to tests
512+
*/
513+
getTestInfo(context, ast) {
514+
const runCalls = module.exports.checkStatementsForTestInfo(
515+
context,
516+
ast.body
517+
);
418518

419519
return runCalls
420520
.filter(

Diff for: tests/lib/rules/no-identical-tests.js

+29
Original file line numberDiff line numberDiff line change
@@ -196,5 +196,34 @@ ruleTester.run('no-identical-tests', rule, {
196196
`,
197197
errors: [ERROR_STRING_TEST],
198198
},
199+
{
200+
code: `
201+
var foo = new RuleTester();
202+
203+
function testOperator(operator) {
204+
foo.run('foo', bar, {
205+
valid: [
206+
\`$\{operator}\`,
207+
\`$\{operator}\`,
208+
],
209+
invalid: []
210+
});
211+
}
212+
`,
213+
output: `
214+
var foo = new RuleTester();
215+
216+
function testOperator(operator) {
217+
foo.run('foo', bar, {
218+
valid: [
219+
\`$\{operator}\`,
220+
],
221+
invalid: []
222+
});
223+
}
224+
`,
225+
parserOptions: { ecmaVersion: 2015 },
226+
errors: [{ messageId: 'identical', type: 'TemplateLiteral' }],
227+
},
199228
],
200229
});

Diff for: tests/lib/rules/test-case-property-ordering.js

+30
Original file line numberDiff line numberDiff line change
@@ -172,5 +172,35 @@ ruleTester.run('test-case-property-ordering', rule, {
172172
},
173173
],
174174
},
175+
{
176+
code: `
177+
var tester = new RuleTester();
178+
179+
describe('my tests', function() {
180+
tester.run('foo', bar, {
181+
valid: [
182+
{\ncode: "foo",\noutput: "",\nerrors: ["baz"],\nparserOptions: "",\n},
183+
]
184+
});
185+
});
186+
`,
187+
output: `
188+
var tester = new RuleTester();
189+
190+
describe('my tests', function() {
191+
tester.run('foo', bar, {
192+
valid: [
193+
{\ncode: "foo",\noutput: "",\nparserOptions: "",\nerrors: ["baz"],\n},
194+
]
195+
});
196+
});
197+
`,
198+
errors: [
199+
{
200+
message:
201+
'The properties of a test case should be placed in a consistent order: [code, output, parserOptions, errors].',
202+
},
203+
],
204+
},
175205
],
176206
});

0 commit comments

Comments
 (0)