From 63203d7d205687d6099411819c1ef2c8dcf05690 Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Sun, 15 May 2022 09:52:19 +1200 Subject: [PATCH 1/8] Feat: improve detection of `RuleTester` usage --- lib/utils.js | 73 ++++++++++++++++++++++++++++++++++++++++------ tests/lib/utils.js | 6 ++++ 2 files changed, 70 insertions(+), 9 deletions(-) diff --git a/lib/utils.js b/lib/utils.js index 646b4720..3b7b863d 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -377,16 +377,39 @@ 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') && + node.body.type === 'BlockStatement' + ) { + return node.body.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) => { if ( @@ -404,8 +427,25 @@ module.exports = { } if ( - statement.type === 'ExpressionStatement' && - statement.expression.type === 'CallExpression' && + statement.type !== 'ExpressionStatement' || + statement.expression.type !== 'CallExpression' + ) { + continue; + } + + for (const arg of statement.expression.arguments) { + const extracted = module.exports.extractFunctionBody(arg); + + runCalls.push( + ...module.exports.checkStatementsForTestInfo( + context, + extracted, + variableIdentifiers + ) + ); + } + + if ( statement.expression.callee.type === 'MemberExpression' && (isRuleTesterConstruction(statement.expression.callee.object) || variableIdentifiers.has(statement.expression.callee.object)) && @@ -414,7 +454,22 @@ module.exports = { ) { runCalls.push(statement.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/utils.js b/tests/lib/utils.js index 5b9063f9..41b1a4f7 100644 --- a/tests/lib/utils.js +++ b/tests/lib/utils.js @@ -684,6 +684,12 @@ 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', () => { + foo.run(bar, baz, { valid: [,], invalid: [bar, , baz] }) + }); + `]: { valid: 0, invalid: 2 }, }; Object.keys(CASES).forEach((testSource) => { From 679f8cde63a518f3b590291b5e199e904c40a1fc Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Sun, 15 May 2022 10:04:24 +1200 Subject: [PATCH 2/8] Fix: cover arrow function with no body --- lib/utils.js | 35 ++++++++++++++++++++--------------- tests/lib/utils.js | 7 ++++++- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/lib/utils.js b/lib/utils.js index 3b7b863d..19a698bd 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -384,11 +384,14 @@ module.exports = { */ extractFunctionBody(node) { if ( - (node.type === 'ArrowFunctionExpression' || - node.type === 'FunctionExpression') && - node.body.type === 'BlockStatement' + node.type === 'ArrowFunctionExpression' || + node.type === 'FunctionExpression' ) { - return node.body.body; + if (node.body.type === 'BlockStatement') { + return node.body.body; + } + + return [node.body]; } return []; @@ -426,14 +429,16 @@ module.exports = { }); } - if ( - statement.type !== 'ExpressionStatement' || - statement.expression.type !== 'CallExpression' - ) { + const expression = + statement.type === 'ExpressionStatement' + ? statement.expression + : statement; + + if (expression.type !== 'CallExpression') { continue; } - for (const arg of statement.expression.arguments) { + for (const arg of expression.arguments) { const extracted = module.exports.extractFunctionBody(arg); runCalls.push( @@ -446,13 +451,13 @@ module.exports = { } if ( - 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); } } diff --git a/tests/lib/utils.js b/tests/lib/utils.js index 41b1a4f7..4deefb4d 100644 --- a/tests/lib/utils.js +++ b/tests/lib/utils.js @@ -672,7 +672,7 @@ describe('utils', () => { }); }); - describe('the file has valid tests', () => { + describe.only('the file has valid tests', () => { const CASES = { 'new RuleTester().run(bar, baz, { valid: [foo], invalid: [bar, baz] })': { valid: 1, invalid: 2 }, @@ -690,6 +690,11 @@ describe('utils', () => { 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 }, }; Object.keys(CASES).forEach((testSource) => { From 37ce4669731982c2e87f9d5fb781e4833e4fcccb Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Sun, 15 May 2022 10:11:04 +1200 Subject: [PATCH 3/8] Test: add more cases --- tests/lib/utils.js | 61 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/tests/lib/utils.js b/tests/lib/utils.js index 4deefb4d..03aa83be 100644 --- a/tests/lib/utils.js +++ b/tests/lib/utils.js @@ -672,7 +672,7 @@ describe('utils', () => { }); }); - describe.only('the file has valid tests', () => { + describe('the file has valid tests', () => { const CASES = { 'new RuleTester().run(bar, baz, { valid: [foo], invalid: [bar, baz] })': { valid: 1, invalid: 2 }, @@ -684,12 +684,30 @@ 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', () => @@ -739,6 +757,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; @@ -749,6 +780,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] }); @@ -757,6 +801,21 @@ 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 }, + ], }; Object.keys(CASES).forEach((testSource) => { From d34612f2e19314956b1f7144a435808f629f479e Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Sun, 15 May 2022 10:22:21 +1200 Subject: [PATCH 4/8] Fix: support if conditions --- lib/utils.js | 17 +++++++++++++++++ tests/lib/utils.js | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/lib/utils.js b/lib/utils.js index 19a698bd..44618b2f 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -429,6 +429,23 @@ module.exports = { }); } + 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 diff --git a/tests/lib/utils.js b/tests/lib/utils.js index 03aa83be..cc3be5d7 100644 --- a/tests/lib/utils.js +++ b/tests/lib/utils.js @@ -713,6 +713,11 @@ describe('utils', () => { 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) => { @@ -816,6 +821,40 @@ describe('utils', () => { { 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 }, + ], }; Object.keys(CASES).forEach((testSource) => { From 7328e80f5714fab916ee3c0e2d05a34e82633f2b Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Sun, 15 May 2022 10:35:35 +1200 Subject: [PATCH 5/8] Fix: support variable-defined functions --- lib/utils.js | 19 ++++++++++++++++--- tests/lib/utils.js | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/lib/utils.js b/lib/utils.js index 44618b2f..e1d6bd9e 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -414,9 +414,22 @@ module.exports = { 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' ) { @@ -426,7 +439,7 @@ module.exports = { .forEach((ref) => variableIdentifiers.add(ref.identifier)); }); } - }); + } } if (statement.type === 'IfStatement') { diff --git a/tests/lib/utils.js b/tests/lib/utils.js index cc3be5d7..52edc0dd 100644 --- a/tests/lib/utils.js +++ b/tests/lib/utils.js @@ -855,6 +855,44 @@ describe('utils', () => { { 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) => { From 7b560eb8f905fb8bd38e9e034311b1150688ddd7 Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Sun, 15 May 2022 10:47:04 +1200 Subject: [PATCH 6/8] Fix: support functions --- lib/utils.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/utils.js b/lib/utils.js index e1d6bd9e..6eef18ad 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -442,6 +442,16 @@ module.exports = { } } + if (statement.type === 'FunctionDeclaration') { + runCalls.push( + ...module.exports.checkStatementsForTestInfo( + context, + statement.body.body, + variableIdentifiers + ) + ); + } + if (statement.type === 'IfStatement') { const body = statement.consequent.type === 'BlockStatement' From a76ca140cf18dc44a002f6e4e9a6136dd1cdefb4 Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Sun, 15 May 2022 10:47:21 +1200 Subject: [PATCH 7/8] Fix: support functions --- tests/lib/utils.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/lib/utils.js b/tests/lib/utils.js index 52edc0dd..83a57d50 100644 --- a/tests/lib/utils.js +++ b/tests/lib/utils.js @@ -856,6 +856,25 @@ describe('utils', () => { { 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; From 7689861c87d61a6c2fb69bb651a87059bd3d69bb Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Sun, 15 May 2022 10:58:31 +1200 Subject: [PATCH 8/8] Test: add a few more cases --- tests/lib/rules/no-identical-tests.js | 29 ++++++++++++++++++ .../lib/rules/test-case-property-ordering.js | 30 +++++++++++++++++++ 2 files changed, 59 insertions(+) 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].', + }, + ], + }, ], });