diff --git a/docs/rules/no-incorrect-deep-equal.md b/docs/rules/no-incorrect-deep-equal.md new file mode 100644 index 00000000..c5c43122 --- /dev/null +++ b/docs/rules/no-incorrect-deep-equal.md @@ -0,0 +1,29 @@ +# Avoid using `deepEqual` with primitives + +The `deepEqual` and `notDeepEqual` assertions are unnecessary when comparing primitives. Use `is` or `not` instead. + +This rule is fixable. + + +## Fail + +```js +t.deepEqual(expression, 'foo'); +t.deepEqual(expression, 1); +t.deepEqual(expression, `foo${bar}`); +t.deepEqual(expression, null); +t.deepEqual(expression, undefined); +t.notDeepEqual(expression, undefined); +``` + + +## Pass + +```js +t.is(expression, 'foo'); + +t.deepEqual(expression, otherExpression); +t.deepEqual(expression, {}); +t.deepEqual(expression, []); +t.notDeepEqual(expression, []); +``` diff --git a/index.js b/index.js index ef5418f2..16f27d5f 100644 --- a/index.js +++ b/index.js @@ -29,6 +29,7 @@ module.exports = { 'ava/no-identical-title': 'error', 'ava/no-ignored-test-files': 'error', 'ava/no-import-test-files': 'error', + 'ava/no-incorrect-deep-equal': 'error', 'ava/no-inline-assertions': 'error', 'ava/no-invalid-end': 'error', 'ava/no-nested-tests': 'error', diff --git a/readme.md b/readme.md index 74397b03..c532cf54 100644 --- a/readme.md +++ b/readme.md @@ -47,6 +47,7 @@ Configure it in `package.json`. "ava/no-identical-title": "error", "ava/no-ignored-test-files": "error", "ava/no-import-test-files": "error", + "ava/no-incorrect-deep-equal": "error", "ava/no-inline-assertions": "error", "ava/no-invalid-end": "error", "ava/no-nested-tests": "error", @@ -86,6 +87,7 @@ The rules will only activate in test files. - [no-identical-title](docs/rules/no-identical-title.md) - Ensure no tests have the same title. - [no-ignored-test-files](docs/rules/no-ignored-test-files.md) - Ensure no tests are written in ignored files. - [no-import-test-files](docs/rules/no-import-test-files.md) - Ensure no test files are imported anywhere. +- [no-incorrect-deep-equal](docs/rules/no-incorrect-deep-equal.md) - Avoid using `deepEqual` with primitives. *(fixable)* - [no-inline-assertions](docs/rules/no-inline-assertions.md) - Ensure assertions are not called from inline arrow functions. *(fixable)* - [no-invalid-end](docs/rules/no-invalid-end.md) - Ensure `t.end()` is only called inside `test.cb()`. - [no-nested-tests](docs/rules/no-nested-tests.md) - Ensure no tests are nested. diff --git a/rules/no-incorrect-deep-equal.js b/rules/no-incorrect-deep-equal.js new file mode 100644 index 00000000..4441b963 --- /dev/null +++ b/rules/no-incorrect-deep-equal.js @@ -0,0 +1,93 @@ +'use strict'; +const {visitIf} = require('enhance-visitors'); +const util = require('../util'); +const createAvaRule = require('../create-ava-rule'); + +const MESSAGE_ID = 'no-deep-equal-with-primative'; + +const buildDeepEqualMessage = (context, node) => { + context.report({ + node, + messageId: MESSAGE_ID, + data: { + callee: node.callee.property.name + }, + fix: fixer => fixer.replaceText(node.callee.property, 'is') + }); +}; + +const buildNotDeepEqualMessage = (context, node) => { + context.report({ + node, + messageId: MESSAGE_ID, + data: { + callee: node.callee.property.name + }, + fix: fixer => fixer.replaceText(node.callee.property, 'not') + }); +}; + +const create = context => { + const ava = createAvaRule(); + + const callExpression = 'CallExpression'; + const deepEqual = '[callee.property.name="deepEqual"]'; + const notDeepEqual = '[callee.property.name="notDeepEqual"]'; + + const argumentsLiteral = ':matches([arguments.0.type="Literal"],[arguments.1.type="Literal"])'; + const argumentsUndefined = ':matches([arguments.0.type="Identifier"][arguments.0.name="undefined"],[arguments.1.type="Identifier"][arguments.1.name="undefined"])'; + const argumentsTemplate = ':matches([arguments.0.type="TemplateLiteral"],[arguments.1.type="TemplateLiteral"])'; + + return ava.merge({ + [`${callExpression}${deepEqual}${argumentsLiteral}`]: visitIf([ + ava.isInTestFile, + ava.isInTestNode + ])(node => { + buildDeepEqualMessage(context, node); + }), + [`${callExpression}${deepEqual}${argumentsUndefined}`]: visitIf([ + ava.isInTestFile, + ava.isInTestNode + ])(node => { + buildDeepEqualMessage(context, node); + }), + [`${callExpression}${deepEqual}${argumentsTemplate}`]: visitIf([ + ava.isInTestFile, + ava.isInTestNode + ])(node => { + buildDeepEqualMessage(context, node); + }), + [`${callExpression}${notDeepEqual}${argumentsLiteral}`]: visitIf([ + ava.isInTestFile, + ava.isInTestNode + ])(node => { + buildNotDeepEqualMessage(context, node); + }), + [`${callExpression}${notDeepEqual}${argumentsUndefined}`]: visitIf([ + ava.isInTestFile, + ava.isInTestNode + ])(node => { + buildNotDeepEqualMessage(context, node); + }), + [`${callExpression}${notDeepEqual}${argumentsTemplate}`]: visitIf([ + ava.isInTestFile, + ava.isInTestNode + ])(node => { + buildNotDeepEqualMessage(context, node); + }) + }); +}; + +module.exports = { + create, + meta: { + docs: { + url: util.getDocsUrl(__filename) + }, + fixable: true, + messages: { + [MESSAGE_ID]: 'Avoid using `{{callee}}` with literal primitives' + }, + type: 'suggestion' + } +}; diff --git a/test/no-incorrect-deep-equal.js b/test/no-incorrect-deep-equal.js new file mode 100644 index 00000000..8a2f1859 --- /dev/null +++ b/test/no-incorrect-deep-equal.js @@ -0,0 +1,302 @@ +import test from 'ava'; +import avaRuleTester from 'eslint-ava-rule-tester'; +import rule from '../rules/no-incorrect-deep-equal'; + +const ruleTester = avaRuleTester(test, { + env: { + es6: true + } +}); + +const error = { + ruleId: 'no-incorrect-deep-equal', + messageId: 'no-deep-equal-with-primative' +}; + +const header = 'const test = require(\'ava\');\n'; + +ruleTester.run('no-incorrect-deep-equal', rule, { + valid: [ + ` + ${header} + test('x', t => { + t.deepEqual(expression, otherExpression); + }); + `, + ` + ${header} + test('x', t => { + t.deepEqual(expression, {}); + }); + `, + ` + ${header} + test('x', t => { + t.deepEqual(expression, []); + }); + `, + ` + ${header} + test('x', t => { + t.notDeepEqual(expression, []); + }); + ` + ], + invalid: [ + { + code: ` + ${header} + test('x', t => { + t.deepEqual(expression, 'foo'); + }); + `, + output: ` + ${header} + test('x', t => { + t.is(expression, 'foo'); + }); + `, + errors: [error] + }, + { + code: ` + ${header} + test('x', t => { + t.deepEqual('foo', expression); + }); + `, + output: ` + ${header} + test('x', t => { + t.is('foo', expression); + }); + `, + errors: [error] + }, + { + code: ` + ${header} + test('x', t => { + t.notDeepEqual(expression, 'foo'); + }); + `, + output: ` + ${header} + test('x', t => { + t.not(expression, 'foo'); + }); + `, + errors: [error] + }, + { + code: ` + ${header} + test('x', t => { + t.notDeepEqual('foo', expression); + }); + `, + output: ` + ${header} + test('x', t => { + t.not('foo', expression); + }); + `, + errors: [error] + }, + { + code: ` + ${header} + test('x', t => { + t.deepEqual(expression, 1); + }); + `, + output: ` + ${header} + test('x', t => { + t.is(expression, 1); + }); + `, + errors: [error] + }, + { + code: ` + ${header} + test('x', t => { + t.deepEqual(expression, \`foo\${bar}\`); + }); + `, + output: ` + ${header} + test('x', t => { + t.is(expression, \`foo\${bar}\`); + }); + `, + errors: [error] + }, + { + code: ` + ${header} + test('x', t => { + t.deepEqual(\`foo\${bar}\`, expression); + }); + `, + output: ` + ${header} + test('x', t => { + t.is(\`foo\${bar}\`, expression); + }); + `, + errors: [error] + }, + { + code: ` + ${header} + test('x', t => { + t.notDeepEqual(expression, \`foo\${bar}\`); + }); + `, + output: ` + ${header} + test('x', t => { + t.not(expression, \`foo\${bar}\`); + }); + `, + errors: [error] + }, + { + code: ` + ${header} + test('x', t => { + t.notDeepEqual(\`foo\${bar}\`, expression); + }); + `, + output: ` + ${header} + test('x', t => { + t.not(\`foo\${bar}\`, expression); + }); + `, + errors: [error] + }, + { + code: ` + ${header} + test('x', t => { + t.deepEqual(expression, null); + }); + `, + output: ` + ${header} + test('x', t => { + t.is(expression, null); + }); + `, + errors: [error] + }, + { + code: ` + ${header} + test('x', t => { + t.deepEqual(null, expression); + }); + `, + output: ` + ${header} + test('x', t => { + t.is(null, expression); + }); + `, + errors: [error] + }, + { + code: ` + ${header} + test('x', t => { + t.notDeepEqual(expression, null); + }); + `, + output: ` + ${header} + test('x', t => { + t.not(expression, null); + }); + `, + errors: [error] + }, + { + code: ` + ${header} + test('x', t => { + t.notDeepEqual(null, expression); + }); + `, + output: ` + ${header} + test('x', t => { + t.not(null, expression); + }); + `, + errors: [error] + }, + { + code: ` + ${header} + test('x', t => { + t.deepEqual(expression, undefined); + }); + `, + output: ` + ${header} + test('x', t => { + t.is(expression, undefined); + }); + `, + errors: [error] + }, + { + code: ` + ${header} + test('x', t => { + t.deepEqual(undefined, expression); + }); + `, + output: ` + ${header} + test('x', t => { + t.is(undefined, expression); + }); + `, + errors: [error] + }, + { + code: ` + ${header} + test('x', t => { + t.notDeepEqual(expression, undefined); + }); + `, + output: ` + ${header} + test('x', t => { + t.not(expression, undefined); + }); + `, + errors: [error] + }, + { + code: ` + ${header} + test('x', t => { + t.notDeepEqual(undefined, expression); + }); + `, + output: ` + ${header} + test('x', t => { + t.not(undefined, expression); + }); + `, + errors: [error] + } + ] +});