From c165ac23173b23f65a8dbcef5969e0f9d9feef03 Mon Sep 17 00:00:00 2001 From: Guillaume Martigny Date: Wed, 29 May 2019 16:58:42 +0200 Subject: [PATCH] Add `prefer-t-regex` rule (#247) Co-authored-by: Sindre Sorhus --- docs/rules/prefer-t-regex.md | 37 ++++++++++++++ index.js | 1 + readme.md | 2 + rules/prefer-t-regex.js | 98 ++++++++++++++++++++++++++++++++++++ test/prefer-t-regex.js | 66 ++++++++++++++++++++++++ 5 files changed, 204 insertions(+) create mode 100644 docs/rules/prefer-t-regex.md create mode 100644 rules/prefer-t-regex.js create mode 100644 test/prefer-t-regex.js diff --git a/docs/rules/prefer-t-regex.md b/docs/rules/prefer-t-regex.md new file mode 100644 index 00000000..af3d318d --- /dev/null +++ b/docs/rules/prefer-t-regex.md @@ -0,0 +1,37 @@ +# Prefer using `t.regex()` to test regular expressions + +The AVA [`t.regex()` assertion](https://github.com/avajs/ava/blob/master/docs/03-assertions.md#regexcontents-regex-message) can test a string against a regular expression. + +This rule will enforce the use of `t.regex()` instead of manually using `RegExp#test()`, which will make your code look clearer and produce better failure output. + +This rule is fixable. It will replace the use of `RegExp#test()`, `String#match()`, or `String#search()` with `t.regex()`. + + +## Fail + +```js +import test from 'ava'; + +test('main', t => { + t.true(/\w+/.test('foo')); +}); +``` + +```js +import test from 'ava'; + +test('main', t => { + t.truthy('foo'.match(/\w+/)); +}); +``` + + +## Pass + +```js +import test from 'ava'; + +test('main', async t => { + t.regex('foo', /\w+/); +}); +``` diff --git a/index.js b/index.js index 86484312..2cd3aec1 100644 --- a/index.js +++ b/index.js @@ -39,6 +39,7 @@ module.exports = { 'ava/no-unknown-modifiers': 'error', 'ava/prefer-async-await': 'error', 'ava/prefer-power-assert': 'off', + 'ava/prefer-t-regex': 'error', 'ava/test-ended': 'error', 'ava/test-title': 'error', 'ava/test-title-format': 'off', diff --git a/readme.md b/readme.md index 6505290e..24813d19 100644 --- a/readme.md +++ b/readme.md @@ -57,6 +57,7 @@ Configure it in `package.json`. "ava/no-unknown-modifiers": "error", "ava/prefer-async-await": "error", "ava/prefer-power-assert": "off", + "ava/prefer-t-regex": "error", "ava/test-ended": "error", "ava/test-title": "error", "ava/test-title-format": "off", @@ -93,6 +94,7 @@ The rules will only activate in test files. - [no-unknown-modifiers](docs/rules/no-unknown-modifiers.md) - Prevent the use of unknown test modifiers. - [prefer-async-await](docs/rules/prefer-async-await.md) - Prefer using async/await instead of returning a Promise. - [prefer-power-assert](docs/rules/prefer-power-assert.md) - Allow only use of the asserts that have no [power-assert](https://github.com/power-assert-js/power-assert) alternative. +- [prefer-t-regex](docs/rules/prefer-t-regex.md) - Prefer using `t.regex()` to test regular expressions. *(fixable)* - [test-ended](docs/rules/test-ended.md) - Ensure callback tests are explicitly ended. - [test-title](docs/rules/test-title.md) - Ensure tests have a title. - [test-title-format](docs/rules/test-title-format.md) - Ensure test titles have a certain format. diff --git a/rules/prefer-t-regex.js b/rules/prefer-t-regex.js new file mode 100644 index 00000000..bde6ab09 --- /dev/null +++ b/rules/prefer-t-regex.js @@ -0,0 +1,98 @@ +'use strict'; +const {visitIf} = require('enhance-visitors'); +const createAvaRule = require('../create-ava-rule'); +const util = require('../util'); + +const create = context => { + const ava = createAvaRule(); + + const booleanTests = [ + 'true', + 'false', + 'truthy', + 'falsy' + ]; + + const findReference = name => { + const reference = context.getScope().references.find(reference => reference.identifier.name === name); + const definitions = reference.resolved.defs; + return definitions[definitions.length - 1].node; + }; + + return ava.merge({ + CallExpression: visitIf([ + ava.isInTestFile, + ava.isInTestNode + ])(node => { + // Call a boolean assertion, for example, `t.true`, `t.false`, … + const isBooleanAssertion = node.callee.type === 'MemberExpression' && + booleanTests.includes(node.callee.property.name) && + util.getNameOfRootNodeObject(node.callee) === 't'; + + if (!isBooleanAssertion) { + return; + } + + const firstArg = node.arguments[0]; + + // First argument is a call expression + const isFunctionCall = firstArg.type === 'CallExpression'; + if (!isFunctionCall) { + return; + } + + const {name} = firstArg.callee.property; + let lookup = {}; + let variable = {}; + + if (name === 'test') { + // `lookup.test(variable)` + lookup = firstArg.callee.object; + variable = firstArg.arguments[0]; + } else if (['search', 'match'].includes(name)) { + // `variable.match(lookup)` + lookup = firstArg.arguments[0]; + variable = firstArg.callee.object; + } + + let isRegExp = lookup.regex; + + // It's not a regexp but an identifier + if (!isRegExp && lookup.type === 'Identifier') { + const reference = findReference(lookup.name); + isRegExp = reference.init.regex; + } + + if (!isRegExp) { + return; + } + + const assertion = ['true', 'truthy'].includes(node.callee.property.name) ? 'regex' : 'notRegex'; + + const fix = fixer => { + const source = context.getSourceCode(); + return [ + fixer.replaceText(node.callee.property, assertion), + fixer.replaceText(firstArg, `${source.getText(variable)}, ${source.getText(lookup)}`) + ]; + }; + + context.report({ + node, + message: `Prefer using the \`t.${assertion}()\` assertion.`, + fix + }); + }) + }); +}; + +module.exports = { + create, + meta: { + docs: { + url: util.getDocsUrl(__filename) + }, + fixable: 'code', + type: 'suggestion' + } +}; diff --git a/test/prefer-t-regex.js b/test/prefer-t-regex.js new file mode 100644 index 00000000..77a44de0 --- /dev/null +++ b/test/prefer-t-regex.js @@ -0,0 +1,66 @@ +import test from 'ava'; +import avaRuleTester from 'eslint-ava-rule-tester'; +import rule from '../rules/prefer-t-regex'; + +const ruleTester = avaRuleTester(test, { + env: { + es6: true + } +}); + +const errors = assertion => [{ + ruleId: 'prefer-t-regex', + message: `Prefer using the \`t.${assertion}()\` assertion.` +}]; +const header = 'const test = require(\'ava\');\n'; + +ruleTester.run('prefer-t-regex', rule, { + valid: [ + header + 'test(t => t.regex("foo", /\\d+/));', + header + 'test(t => t.regex(foo(), /\\d+/));', + header + 'test(t => t.is(/\\d+/.test("foo")), true);', + header + 'test(t => t.true(1 === 1));', + header + 'test(t => t.true(foo.bar()));', + header + 'const a = /\\d+/;\ntest(t => t.truthy(a));', + header + 'const a = "not a regexp";\ntest(t => t.true(a.test("foo")));', + // Shouldn't be triggered since it's not a test file + 'test(t => t.true(/\\d+/.test("foo")));' + ], + invalid: [ + { + code: header + 'test(t => t.true(/\\d+/.test("foo")));', + output: header + 'test(t => t.regex("foo", /\\d+/));', + errors: errors('regex') + }, + { + code: header + 'test(t => t.false(foo.search(/\\d+/)));', + output: header + 'test(t => t.notRegex(foo, /\\d+/));', + errors: errors('notRegex') + }, + { + code: header + 'const regexp = /\\d+/;\ntest(t => t.true(foo.search(regexp)));', + output: header + 'const regexp = /\\d+/;\ntest(t => t.regex(foo, regexp));', + errors: errors('regex') + }, + { + code: header + 'test(t => t.truthy(foo.match(/\\d+/)));', + output: header + 'test(t => t.regex(foo, /\\d+/));', + errors: errors('regex') + }, + { + code: header + 'test(t => t.false(/\\d+/.test("foo")));', + output: header + 'test(t => t.notRegex("foo", /\\d+/));', + errors: errors('notRegex') + }, + { + code: header + 'test(t => t.true(/\\d+/.test(foo())));', + output: header + 'test(t => t.regex(foo(), /\\d+/));', + errors: errors('regex') + }, + { + code: header + 'const reg = /\\d+/;\ntest(t => t.true(reg.test(foo.bar())));', + output: header + 'const reg = /\\d+/;\ntest(t => t.regex(foo.bar(), reg));', + errors: errors('regex') + } + ] +});