Skip to content

Commit c165ac2

Browse files
Add prefer-t-regex rule (#247)
Co-authored-by: Sindre Sorhus <[email protected]>
1 parent 330af0e commit c165ac2

File tree

5 files changed

+204
-0
lines changed

5 files changed

+204
-0
lines changed

docs/rules/prefer-t-regex.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Prefer using `t.regex()` to test regular expressions
2+
3+
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.
4+
5+
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.
6+
7+
This rule is fixable. It will replace the use of `RegExp#test()`, `String#match()`, or `String#search()` with `t.regex()`.
8+
9+
10+
## Fail
11+
12+
```js
13+
import test from 'ava';
14+
15+
test('main', t => {
16+
t.true(/\w+/.test('foo'));
17+
});
18+
```
19+
20+
```js
21+
import test from 'ava';
22+
23+
test('main', t => {
24+
t.truthy('foo'.match(/\w+/));
25+
});
26+
```
27+
28+
29+
## Pass
30+
31+
```js
32+
import test from 'ava';
33+
34+
test('main', async t => {
35+
t.regex('foo', /\w+/);
36+
});
37+
```

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ module.exports = {
3939
'ava/no-unknown-modifiers': 'error',
4040
'ava/prefer-async-await': 'error',
4141
'ava/prefer-power-assert': 'off',
42+
'ava/prefer-t-regex': 'error',
4243
'ava/test-ended': 'error',
4344
'ava/test-title': 'error',
4445
'ava/test-title-format': 'off',

readme.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ Configure it in `package.json`.
5757
"ava/no-unknown-modifiers": "error",
5858
"ava/prefer-async-await": "error",
5959
"ava/prefer-power-assert": "off",
60+
"ava/prefer-t-regex": "error",
6061
"ava/test-ended": "error",
6162
"ava/test-title": "error",
6263
"ava/test-title-format": "off",
@@ -93,6 +94,7 @@ The rules will only activate in test files.
9394
- [no-unknown-modifiers](docs/rules/no-unknown-modifiers.md) - Prevent the use of unknown test modifiers.
9495
- [prefer-async-await](docs/rules/prefer-async-await.md) - Prefer using async/await instead of returning a Promise.
9596
- [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.
97+
- [prefer-t-regex](docs/rules/prefer-t-regex.md) - Prefer using `t.regex()` to test regular expressions. *(fixable)*
9698
- [test-ended](docs/rules/test-ended.md) - Ensure callback tests are explicitly ended.
9799
- [test-title](docs/rules/test-title.md) - Ensure tests have a title.
98100
- [test-title-format](docs/rules/test-title-format.md) - Ensure test titles have a certain format.

rules/prefer-t-regex.js

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
'use strict';
2+
const {visitIf} = require('enhance-visitors');
3+
const createAvaRule = require('../create-ava-rule');
4+
const util = require('../util');
5+
6+
const create = context => {
7+
const ava = createAvaRule();
8+
9+
const booleanTests = [
10+
'true',
11+
'false',
12+
'truthy',
13+
'falsy'
14+
];
15+
16+
const findReference = name => {
17+
const reference = context.getScope().references.find(reference => reference.identifier.name === name);
18+
const definitions = reference.resolved.defs;
19+
return definitions[definitions.length - 1].node;
20+
};
21+
22+
return ava.merge({
23+
CallExpression: visitIf([
24+
ava.isInTestFile,
25+
ava.isInTestNode
26+
])(node => {
27+
// Call a boolean assertion, for example, `t.true`, `t.false`, …
28+
const isBooleanAssertion = node.callee.type === 'MemberExpression' &&
29+
booleanTests.includes(node.callee.property.name) &&
30+
util.getNameOfRootNodeObject(node.callee) === 't';
31+
32+
if (!isBooleanAssertion) {
33+
return;
34+
}
35+
36+
const firstArg = node.arguments[0];
37+
38+
// First argument is a call expression
39+
const isFunctionCall = firstArg.type === 'CallExpression';
40+
if (!isFunctionCall) {
41+
return;
42+
}
43+
44+
const {name} = firstArg.callee.property;
45+
let lookup = {};
46+
let variable = {};
47+
48+
if (name === 'test') {
49+
// `lookup.test(variable)`
50+
lookup = firstArg.callee.object;
51+
variable = firstArg.arguments[0];
52+
} else if (['search', 'match'].includes(name)) {
53+
// `variable.match(lookup)`
54+
lookup = firstArg.arguments[0];
55+
variable = firstArg.callee.object;
56+
}
57+
58+
let isRegExp = lookup.regex;
59+
60+
// It's not a regexp but an identifier
61+
if (!isRegExp && lookup.type === 'Identifier') {
62+
const reference = findReference(lookup.name);
63+
isRegExp = reference.init.regex;
64+
}
65+
66+
if (!isRegExp) {
67+
return;
68+
}
69+
70+
const assertion = ['true', 'truthy'].includes(node.callee.property.name) ? 'regex' : 'notRegex';
71+
72+
const fix = fixer => {
73+
const source = context.getSourceCode();
74+
return [
75+
fixer.replaceText(node.callee.property, assertion),
76+
fixer.replaceText(firstArg, `${source.getText(variable)}, ${source.getText(lookup)}`)
77+
];
78+
};
79+
80+
context.report({
81+
node,
82+
message: `Prefer using the \`t.${assertion}()\` assertion.`,
83+
fix
84+
});
85+
})
86+
});
87+
};
88+
89+
module.exports = {
90+
create,
91+
meta: {
92+
docs: {
93+
url: util.getDocsUrl(__filename)
94+
},
95+
fixable: 'code',
96+
type: 'suggestion'
97+
}
98+
};

test/prefer-t-regex.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import test from 'ava';
2+
import avaRuleTester from 'eslint-ava-rule-tester';
3+
import rule from '../rules/prefer-t-regex';
4+
5+
const ruleTester = avaRuleTester(test, {
6+
env: {
7+
es6: true
8+
}
9+
});
10+
11+
const errors = assertion => [{
12+
ruleId: 'prefer-t-regex',
13+
message: `Prefer using the \`t.${assertion}()\` assertion.`
14+
}];
15+
const header = 'const test = require(\'ava\');\n';
16+
17+
ruleTester.run('prefer-t-regex', rule, {
18+
valid: [
19+
header + 'test(t => t.regex("foo", /\\d+/));',
20+
header + 'test(t => t.regex(foo(), /\\d+/));',
21+
header + 'test(t => t.is(/\\d+/.test("foo")), true);',
22+
header + 'test(t => t.true(1 === 1));',
23+
header + 'test(t => t.true(foo.bar()));',
24+
header + 'const a = /\\d+/;\ntest(t => t.truthy(a));',
25+
header + 'const a = "not a regexp";\ntest(t => t.true(a.test("foo")));',
26+
// Shouldn't be triggered since it's not a test file
27+
'test(t => t.true(/\\d+/.test("foo")));'
28+
],
29+
invalid: [
30+
{
31+
code: header + 'test(t => t.true(/\\d+/.test("foo")));',
32+
output: header + 'test(t => t.regex("foo", /\\d+/));',
33+
errors: errors('regex')
34+
},
35+
{
36+
code: header + 'test(t => t.false(foo.search(/\\d+/)));',
37+
output: header + 'test(t => t.notRegex(foo, /\\d+/));',
38+
errors: errors('notRegex')
39+
},
40+
{
41+
code: header + 'const regexp = /\\d+/;\ntest(t => t.true(foo.search(regexp)));',
42+
output: header + 'const regexp = /\\d+/;\ntest(t => t.regex(foo, regexp));',
43+
errors: errors('regex')
44+
},
45+
{
46+
code: header + 'test(t => t.truthy(foo.match(/\\d+/)));',
47+
output: header + 'test(t => t.regex(foo, /\\d+/));',
48+
errors: errors('regex')
49+
},
50+
{
51+
code: header + 'test(t => t.false(/\\d+/.test("foo")));',
52+
output: header + 'test(t => t.notRegex("foo", /\\d+/));',
53+
errors: errors('notRegex')
54+
},
55+
{
56+
code: header + 'test(t => t.true(/\\d+/.test(foo())));',
57+
output: header + 'test(t => t.regex(foo(), /\\d+/));',
58+
errors: errors('regex')
59+
},
60+
{
61+
code: header + 'const reg = /\\d+/;\ntest(t => t.true(reg.test(foo.bar())));',
62+
output: header + 'const reg = /\\d+/;\ntest(t => t.regex(foo.bar(), reg));',
63+
errors: errors('regex')
64+
}
65+
]
66+
});

0 commit comments

Comments
 (0)