Skip to content

Commit

Permalink
Add prefer-t-regex rule (#247)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
GMartigny and sindresorhus committed May 29, 2019
1 parent 330af0e commit c165ac2
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 0 deletions.
37 changes: 37 additions & 0 deletions docs/rules/prefer-t-regex.md
Original file line number Diff line number Diff line change
@@ -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+/);
});
```
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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.
Expand Down
98 changes: 98 additions & 0 deletions rules/prefer-t-regex.js
Original file line number Diff line number Diff line change
@@ -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'
}
};
66 changes: 66 additions & 0 deletions test/prefer-t-regex.js
Original file line number Diff line number Diff line change
@@ -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')
}
]
});

0 comments on commit c165ac2

Please sign in to comment.