Skip to content

Commit 7b74b40

Browse files
fiskersindresorhus
andauthored
Add prefer-regexp-test rule (#970)
Co-authored-by: Sindre Sorhus <[email protected]>
1 parent e2f94fe commit 7b74b40

File tree

7 files changed

+873
-0
lines changed

7 files changed

+873
-0
lines changed

docs/rules/prefer-regexp-test.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Prefer `RegExp#test()` over `String#match()` and `RegExp#exec()`
2+
3+
When you want to know whether a pattern is found in a string, use [`RegExp#test()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test) instead of [`String#match()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match) and [`RegExp#exec()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec).
4+
5+
This rule is fixable.
6+
7+
## Fail
8+
9+
```js
10+
if (string.match(/unicorn/)) {}
11+
```
12+
13+
```js
14+
if (/unicorn/.exec(string)) {}
15+
```
16+
17+
## Pass
18+
19+
```js
20+
if (/unicorn/.test(string)) {}
21+
```

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ module.exports = {
9797
'unicorn/prefer-optional-catch-binding': 'error',
9898
'unicorn/prefer-query-selector': 'error',
9999
'unicorn/prefer-reflect-apply': 'error',
100+
'unicorn/prefer-regexp-test': 'error',
100101
'unicorn/prefer-set-has': 'error',
101102
'unicorn/prefer-spread': 'error',
102103
// TODO: Enable this by default when targeting Node.js 16.

readme.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ Configure it in `package.json`.
8989
"unicorn/prefer-optional-catch-binding": "error",
9090
"unicorn/prefer-query-selector": "error",
9191
"unicorn/prefer-reflect-apply": "error",
92+
"unicorn/prefer-regexp-test": "error",
9293
"unicorn/prefer-set-has": "error",
9394
"unicorn/prefer-spread": "error",
9495
"unicorn/prefer-string-replace-all": "off",
@@ -162,6 +163,7 @@ Configure it in `package.json`.
162163
- [prefer-optional-catch-binding](docs/rules/prefer-optional-catch-binding.md) - Prefer omitting the `catch` binding parameter. *(fixable)*
163164
- [prefer-query-selector](docs/rules/prefer-query-selector.md) - Prefer `.querySelector()` over `.getElementById()`, `.querySelectorAll()` over `.getElementsByClassName()` and `.getElementsByTagName()`. *(partly fixable)*
164165
- [prefer-reflect-apply](docs/rules/prefer-reflect-apply.md) - Prefer `Reflect.apply()` over `Function#apply()`. *(fixable)*
166+
- [prefer-regexp-test](docs/rules/prefer-regexp-test.md) - Prefer `RegExp#test()` over `String#match()` and `RegExp#exec()`. *(fixable)*
165167
- [prefer-set-has](docs/rules/prefer-set-has.md) - Prefer `Set#has()` over `Array#includes()` when checking for existence or non-existence. *(fixable)*
166168
- [prefer-spread](docs/rules/prefer-spread.md) - Prefer the spread operator over `Array.from()`. *(fixable)*
167169
- [prefer-string-replace-all](docs/rules/prefer-string-replace-all.md) - Prefer `String#replaceAll()` over regex searches with the global flag. *(fixable)*

rules/prefer-regexp-test.js

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
'use strict';
2+
const {isParenthesized} = require('eslint-utils');
3+
const getDocumentationUrl = require('./utils/get-documentation-url');
4+
const methodSelector = require('./utils/method-selector');
5+
const {isBooleanNode} = require('./utils/boolean');
6+
const shouldAddParenthesesToMemberExpressionObject = require('./utils/should-add-parentheses-to-member-expression-object');
7+
8+
const MESSAGE_ID_REGEXP_EXEC = 'regexp-exec';
9+
const MESSAGE_ID_STRING_MATCH = 'string-match';
10+
const messages = {
11+
[MESSAGE_ID_REGEXP_EXEC]: 'Prefer `.test(…)` over `.exec(…)`.',
12+
[MESSAGE_ID_STRING_MATCH]: 'Prefer `RegExp#test(…)` over `String#match(…)`.'
13+
};
14+
15+
const regExpExecCallSelector = methodSelector({
16+
name: 'exec',
17+
length: 1
18+
});
19+
20+
const stringMatchCallSelector = methodSelector({
21+
name: 'match',
22+
length: 1
23+
});
24+
25+
const create = context => {
26+
const sourceCode = context.getSourceCode();
27+
28+
return {
29+
[regExpExecCallSelector](node) {
30+
if (!isBooleanNode(node)) {
31+
return;
32+
}
33+
34+
node = node.callee.property;
35+
context.report({
36+
node,
37+
messageId: MESSAGE_ID_REGEXP_EXEC,
38+
fix: fixer => fixer.replaceText(node, 'test')
39+
});
40+
},
41+
[stringMatchCallSelector](node) {
42+
if (!isBooleanNode(node)) {
43+
return;
44+
}
45+
46+
const regexpNode = node.arguments[0];
47+
48+
if (regexpNode.type === 'Literal' && !regexpNode.regex) {
49+
return;
50+
}
51+
52+
const stringNode = node.callee.object;
53+
54+
context.report({
55+
node,
56+
messageId: MESSAGE_ID_STRING_MATCH,
57+
* fix(fixer) {
58+
yield fixer.replaceText(node.callee.property, 'test');
59+
60+
let stringText = sourceCode.getText(stringNode);
61+
if (
62+
!isParenthesized(regexpNode, sourceCode) &&
63+
// Only `SequenceExpression` need add parentheses
64+
stringNode.type === 'SequenceExpression'
65+
) {
66+
stringText = `(${stringText})`;
67+
}
68+
69+
yield fixer.replaceText(regexpNode, stringText);
70+
71+
let regexpText = sourceCode.getText(regexpNode);
72+
if (
73+
!isParenthesized(stringNode, sourceCode) &&
74+
shouldAddParenthesesToMemberExpressionObject(regexpNode, sourceCode)
75+
) {
76+
regexpText = `(${regexpText})`;
77+
}
78+
79+
// The nodes that pass `isBooleanNode` cannot have an ASI problem.
80+
81+
yield fixer.replaceText(stringNode, regexpText);
82+
}
83+
});
84+
}
85+
};
86+
};
87+
88+
module.exports = {
89+
create,
90+
meta: {
91+
type: 'suggestion',
92+
docs: {
93+
url: getDocumentationUrl(__filename)
94+
},
95+
fixable: 'code',
96+
messages
97+
}
98+
};

test/prefer-regexp-test.js

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import {outdent} from 'outdent';
2+
import {test} from './utils/test';
3+
4+
test({
5+
valid: [
6+
'const bar = !re.test(foo)',
7+
// Not `boolean`
8+
'const matches = foo.match(re) || []',
9+
'const matches = foo.match(re)',
10+
'const matches = re.exec(foo)',
11+
'while (foo = re.exec(bar)) {}',
12+
'while ((foo = re.exec(bar))) {}',
13+
14+
// Method not match
15+
'if (foo.notMatch(re)) {}',
16+
'if (re.notExec(foo)) {}',
17+
// Not `CallExpression`
18+
'if (foo.match) {}',
19+
'if (re.exec) {}',
20+
// Computed
21+
'if (foo[match](re)) {}',
22+
'if (re[exec](foo)) {}',
23+
'if (foo["match"](re)) {}',
24+
'if (re["exec"](foo)) {}',
25+
// Not `MemberExpression`
26+
'if (match(re)) {}',
27+
'if (exec(foo)) {}',
28+
// More/Less arguments
29+
'if (foo.match()) {}',
30+
'if (re.exec()) {}',
31+
'if (foo.match(re, another)) {}',
32+
'if (re.exec(foo, another)) {}',
33+
'if (foo.match(...[regexp])) {}',
34+
'if (re.exec(...[string])) {}',
35+
// Not regex
36+
'if (foo.match(1)) {}',
37+
'if (foo.match("1")) {}',
38+
'if (foo.match(null)) {}',
39+
'if (foo.match(1n)) {}',
40+
'if (foo.match(true)) {}'
41+
],
42+
invalid: []
43+
});
44+
45+
test.visualize([
46+
// `String#match()`
47+
'const bar = !foo.match(re)',
48+
'const bar = Boolean(foo.match(re))',
49+
'if (foo.match(re)) {}',
50+
'const bar = foo.match(re) ? 1 : 2',
51+
'while (foo.match(re)) foo = foo.slice(1);',
52+
'do {foo = foo.slice(1)} while (foo.match(re));',
53+
'for (; foo.match(re); ) foo = foo.slice(1);',
54+
55+
// `RegExp#exec()`
56+
'const bar = !re.exec(foo)',
57+
'const bar = Boolean(re.exec(foo))',
58+
'if (re.exec(foo)) {}',
59+
'const bar = re.exec(foo) ? 1 : 2',
60+
'while (re.exec(foo)) foo = foo.slice(1);',
61+
'do {foo = foo.slice(1)} while (re.exec(foo));',
62+
'for (; re.exec(foo); ) foo = foo.slice(1);',
63+
64+
// Parentheses
65+
'if ((0, foo).match(re)) {}',
66+
'if ((0, foo).match((re))) {}',
67+
'if ((foo).match(re)) {}',
68+
'if ((foo).match((re))) {}',
69+
'if (foo.match(/re/)) {}',
70+
'if (foo.match(bar)) {}',
71+
'if (foo.match(bar.baz)) {}',
72+
'if (foo.match(bar.baz())) {}',
73+
'if (foo.match(new RegExp("re", "g"))) {}',
74+
'if (foo.match(new SomeRegExp())) {}',
75+
'if (foo.match(new SomeRegExp)) {}',
76+
'if (foo.match(bar?.baz)) {}',
77+
'if (foo.match(bar?.baz())) {}',
78+
'if (foo.match(bar || baz)) {}',
79+
outdent`
80+
async function a() {
81+
if (foo.match(await bar())) {}
82+
}
83+
`,
84+
'if ((foo).match(/re/)) {}',
85+
'if ((foo).match(new SomeRegExp)) {}',
86+
'if ((foo).match(bar?.baz)) {}',
87+
'if ((foo).match(bar?.baz())) {}',
88+
'if ((foo).match(bar || baz)) {}',
89+
outdent`
90+
async function a() {
91+
if ((foo).match(await bar())) {}
92+
}
93+
`,
94+
// Should not need handle ASI problem
95+
'if (foo.match([re][0])) {}',
96+
97+
// Comments
98+
outdent`
99+
async function a() {
100+
if (
101+
/* 1 */ foo() /* 2 */
102+
./* 3 */ match /* 4 */ (
103+
/* 5 */ await /* 6 */ bar() /* 7 */
104+
,
105+
/* 8 */
106+
)
107+
) {}
108+
}
109+
`
110+
]);

0 commit comments

Comments
 (0)