diff --git a/lib/utils.js b/lib/utils.js index 646b4720..6eef18ad 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -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} 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' ) { @@ -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( diff --git a/tests/lib/rules/no-identical-tests.js b/tests/lib/rules/no-identical-tests.js index d8c7bd74..6aa76be2 100644 --- a/tests/lib/rules/no-identical-tests.js +++ b/tests/lib/rules/no-identical-tests.js @@ -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' }], + }, ], }); diff --git a/tests/lib/rules/test-case-property-ordering.js b/tests/lib/rules/test-case-property-ordering.js index c46300e2..3ee1dee2 100644 --- a/tests/lib/rules/test-case-property-ordering.js +++ b/tests/lib/rules/test-case-property-ordering.js @@ -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].', + }, + ], + }, ], }); diff --git a/tests/lib/utils.js b/tests/lib/utils.js index 5b9063f9..83a57d50 100644 --- a/tests/lib/utils.js +++ b/tests/lib/utils.js @@ -684,6 +684,40 @@ describe('utils', () => { { valid: 0, invalid: 2 }, 'var foo = new bar.RuleTester; foo.run(bar, baz, { valid: [,], invalid: [bar, , baz] })': { valid: 0, invalid: 2 }, + [` + var foo = new bar.RuleTester; + describe('my tests', function () { + foo.run(bar, baz, { valid: [,], invalid: [bar, , baz] }) + }); + `]: { valid: 0, invalid: 2 }, + [` + var foo = new bar.RuleTester; + describe('my tests', () => { + foo.run(bar, baz, { valid: [,], invalid: [bar, , baz] }) + }); + `]: { valid: 0, invalid: 2 }, + [` + var foo = new bar.RuleTester; + describe('my tests', () => { + describe('my tests', () => { + describe('my tests', () => { + describe('my tests', () => { + foo.run(bar, baz, { valid: [,], invalid: [bar, , baz] }) + }); + }); + }); + }); + `]: { valid: 0, invalid: 2 }, + [` + var foo = new bar.RuleTester(); + describe('my tests', () => + foo.run(bar, baz, { valid: [,], invalid: [bar, , baz] })); + `]: { valid: 0, invalid: 2 }, + [` + var foo = new bar.RuleTester(); + if (eslintVersion >= 8) + foo.run(bar, baz, { valid: [,], invalid: [bar, , baz] }); + `]: { valid: 0, invalid: 2 }, }; Object.keys(CASES).forEach((testSource) => { @@ -728,6 +762,19 @@ describe('utils', () => { { valid: 0, invalid: 2 }, ], + [` + describe('one', function() { + new RuleTester().run(foo, bar, { valid: [foo], invalid: [] }); + }); + + describe('two', () => { + new RuleTester().run(foo, bar, { valid: [], invalid: [foo, bar] }); + }); + `]: [ + { valid: 1, invalid: 0 }, + { valid: 0, invalid: 2 }, + ], + [` var foo = new RuleTester; var bar = new RuleTester; @@ -738,6 +785,19 @@ describe('utils', () => { { valid: 0, invalid: 2 }, ], + [` + var foo = new RuleTester; + + describe('some tests', () => { + var bar = new RuleTester; + foo.run(foo, bar, { valid: [foo, bar, baz], invalid: [foo] }); + bar.run(foo, bar, { valid: [], invalid: [foo, bar] }); + }); + `]: [ + { valid: 3, invalid: 1 }, + { valid: 0, invalid: 2 }, + ], + [` var foo = new RuleTester, bar = new RuleTester; foo.run(foo, bar, { valid: [foo, bar, baz], invalid: [foo] }); @@ -746,6 +806,112 @@ describe('utils', () => { { valid: 3, invalid: 1 }, { valid: 0, invalid: 2 }, ], + + [` + var foo = new RuleTester, bar = new RuleTester; + + describe('one set of tests', () => { + foo.run(foo, bar, { valid: [foo, bar, baz], invalid: [foo] }); + }); + + describe('another set of tests', () => { + bar.run(foo, bar, { valid: [], invalid: [foo, bar] }); + }); + `]: [ + { valid: 3, invalid: 1 }, + { valid: 0, invalid: 2 }, + ], + + [` + var foo = new RuleTester, bar = new RuleTester; + + if (eslintVersion >= 8) { + describe('one set of tests', () => { + foo.run(foo, bar, { valid: [foo, bar, baz], invalid: [foo] }); + }); + } + + describe('another set of tests', () => { + bar.run(foo, bar, { valid: [], invalid: [foo, bar] }); + }); + `]: [ + { valid: 3, invalid: 1 }, + { valid: 0, invalid: 2 }, + ], + + [` + var foo = new RuleTester, bar = new RuleTester; + + describe('one set of tests', () => { + if (eslintVersion >= 8) { + foo.run(foo, bar, { valid: [foo, bar, baz], invalid: [foo] }); + } + }); + + describe('another set of tests', () => { + bar.run(foo, bar, { valid: [], invalid: [foo, bar] }); + }); + `]: [ + { valid: 3, invalid: 1 }, + { valid: 0, invalid: 2 }, + ], + + [` + var foo = new RuleTester, bar = new RuleTester; + + function testUtilsAgainst(value) { + foo.run(foo, bar, { valid: [foo, bar, baz], invalid: [foo] }); + }; + + testUtilsAgainst(1); + testUtilsAgainst(2); + testUtilsAgainst(3); + + describe('another set of tests', () => { + bar.run(foo, bar, { valid: [], invalid: [foo, bar] }); + }); + `]: [ + { valid: 3, invalid: 1 }, + { valid: 0, invalid: 2 }, + ], + + [` + var foo = new RuleTester, bar = new RuleTester; + + const testUtilsAgainst = function(value) { + foo.run(foo, bar, { valid: [foo, bar, baz], invalid: [foo] }); + }; + + testUtilsAgainst(1); + testUtilsAgainst(2); + testUtilsAgainst(3); + + describe('another set of tests', () => { + bar.run(foo, bar, { valid: [], invalid: [foo, bar] }); + }); + `]: [ + { valid: 3, invalid: 1 }, + { valid: 0, invalid: 2 }, + ], + + [` + var foo = new RuleTester, bar = new RuleTester; + + const testUtilsAgainst = (value) => { + foo.run(foo, bar, { valid: [foo, bar, baz], invalid: [foo] }); + }; + + testUtilsAgainst(1); + testUtilsAgainst(2); + testUtilsAgainst(3); + + describe('another set of tests', () => { + bar.run(foo, bar, { valid: [], invalid: [foo, bar] }); + }); + `]: [ + { valid: 3, invalid: 1 }, + { valid: 0, invalid: 2 }, + ], }; Object.keys(CASES).forEach((testSource) => {