Skip to content

Commit d98c277

Browse files
jmoore914fiskersindresorhus
authored
Add prefer-replace-all rule (#488)
Co-authored-by: fisker Cheung <[email protected]> Co-authored-by: Sindre Sorhus <[email protected]>
1 parent 096fead commit d98c277

File tree

5 files changed

+180
-0
lines changed

5 files changed

+180
-0
lines changed

docs/rules/prefer-replace-all.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Prefer `String#replaceAll()` over regex searches with the global flag
2+
3+
The [`String#replaceAll()`](https://github.com/tc39/proposal-string-replaceall) method is both faster and safer as you don't have to escape the regex if the string is not a literal.
4+
5+
This rule is fixable.
6+
7+
## Fail
8+
9+
```js
10+
string.replace(/This has no special regex symbols/g, '');
11+
string.replace(/\(It also checks for escaped regex symbols\)/g, '');
12+
```
13+
14+
## Pass
15+
16+
```js
17+
string.replace(/Non-literal characters .*/g, '');
18+
string.replace(/Extra flags/gi, '');
19+
string.replace('Not a regex expression', '')
20+
string.replaceAll('Literal characters only', '');
21+
```

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ module.exports = {
5454
'unicorn/prefer-node-remove': 'error',
5555
'unicorn/prefer-query-selector': 'error',
5656
'unicorn/prefer-reflect-apply': 'error',
57+
'unicorn/prefer-replace-all': 'off',
5758
'unicorn/prefer-spread': 'error',
5859
'unicorn/prefer-starts-ends-with': 'error',
5960
'unicorn/prefer-string-slice': 'error',

readme.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ Configure it in `package.json`.
7272
"unicorn/prefer-node-remove": "error",
7373
"unicorn/prefer-query-selector": "error",
7474
"unicorn/prefer-reflect-apply": "error",
75+
"unicorn/prefer-replace-all": "off",
7576
"unicorn/prefer-spread": "error",
7677
"unicorn/prefer-starts-ends-with": "error",
7778
"unicorn/prefer-string-slice": "error",
@@ -125,6 +126,7 @@ Configure it in `package.json`.
125126
- [prefer-node-remove](docs/rules/prefer-node-remove.md) - Prefer `node.remove()` over `parentNode.removeChild(node)` and `parentElement.removeChild(node)`. *(fixable)*
126127
- [prefer-query-selector](docs/rules/prefer-query-selector.md) - Prefer `.querySelector()` over `.getElementById()`, `.querySelectorAll()` over `.getElementsByClassName()` and `.getElementsByTagName()`. *(partly fixable)*
127128
- [prefer-reflect-apply](docs/rules/prefer-reflect-apply.md) - Prefer `Reflect.apply()` over `Function#apply()`. *(fixable)*
129+
- [prefer-replace-all](docs/rules/prefer-replace-all.md) - Prefer `String#replaceAll()` over regex searches with the global flag. *(fixable)*
128130
- [prefer-spread](docs/rules/prefer-spread.md) - Prefer the spread operator over `Array.from()`. *(fixable)*
129131
- [prefer-starts-ends-with](docs/rules/prefer-starts-ends-with.md) - Prefer `String#startsWith()` & `String#endsWith()` over more complex alternatives.
130132
- [prefer-string-slice](docs/rules/prefer-string-slice.md) - Prefer `String#slice()` over `String#substr()` and `String#substring()`. *(partly fixable)*

rules/prefer-replace-all.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
'use strict';
2+
const getDocumentationUrl = require('./utils/get-documentation-url');
3+
const quoteString = require('./utils/quote-string');
4+
5+
function isRegexWithGlobalFlag(node) {
6+
const {type, regex} = node;
7+
return type === 'Literal' && regex && regex.flags === 'g';
8+
}
9+
10+
function isLiteralCharactersOnly(node) {
11+
const searchPattern = node.regex.pattern;
12+
return !/[$()*+.?[\\\]^{}]/.test(searchPattern.replace(/\\[$()*+.?[\\\]^{}]/g, ''));
13+
}
14+
15+
function removeEscapeCharacters(regexString) {
16+
let fixedString = regexString;
17+
let index = 0;
18+
do {
19+
index = fixedString.indexOf('\\', index);
20+
21+
if (index >= 0) {
22+
fixedString = fixedString.slice(0, index) + fixedString.slice(index + 1);
23+
index++;
24+
}
25+
} while (index >= 0);
26+
27+
return fixedString;
28+
}
29+
30+
const create = context => {
31+
return {
32+
'CallExpression[callee.property.name="replace"]': node => {
33+
const {arguments: arguments_} = node;
34+
35+
if (arguments_.length !== 2) {
36+
return;
37+
}
38+
39+
const [search] = arguments_;
40+
41+
if (!isRegexWithGlobalFlag(search) || !isLiteralCharactersOnly(search)) {
42+
return;
43+
}
44+
45+
context.report({
46+
node,
47+
message: 'Prefer `String#replaceAll()` over `String#replace()`.',
48+
fix: fixer =>
49+
[
50+
fixer.insertTextAfter(node.callee, 'All'),
51+
fixer.replaceText(search, quoteString(removeEscapeCharacters(search.regex.pattern)))
52+
]
53+
});
54+
}
55+
};
56+
};
57+
58+
module.exports = {
59+
create,
60+
meta: {
61+
type: 'suggestion',
62+
docs: {
63+
url: getDocumentationUrl(__filename)
64+
},
65+
fixable: 'code'
66+
}
67+
};

test/prefer-replace-all.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import test from 'ava';
2+
import avaRuleTester from 'eslint-ava-rule-tester';
3+
const rule = require('../rules/prefer-replace-all');
4+
5+
const ruleTester = avaRuleTester(test, {
6+
env: {
7+
es6: true
8+
}
9+
});
10+
11+
const error = {
12+
ruleId: 'prefer-replace-all',
13+
message: 'Prefer `String#replaceAll()` over `String#replace()`.'
14+
};
15+
16+
ruleTester.run('prefer-replace-all', rule, {
17+
valid: [
18+
// No global flag
19+
'foo.replace(/a/, bar)',
20+
// Special characters
21+
'foo.replace(/[a]/g, bar)',
22+
'foo.replace(/a?/g, bar)',
23+
'foo.replace(/.*/g, bar)',
24+
'foo.replace(/\\W/g, bar)',
25+
// Extra flag
26+
'foo.replace(/a/gi, bar)',
27+
// Not regex literal
28+
'foo.replace(\'string\', bar)',
29+
// Not 2 arguments
30+
'foo.replace(/a/g)',
31+
'foo.replace(/\\\\./g)',
32+
// New
33+
'new foo.replace(/a/g, bar)',
34+
// Function call
35+
'replace(/a/g, bar)',
36+
// Not call
37+
'foo.replace',
38+
// Not replace
39+
'foo.methodNotReplace(/a/g, bar);',
40+
// `replace` is not Identifier
41+
'foo[\'replace\'](/a/g, bar)'
42+
],
43+
invalid: [
44+
{
45+
code: 'foo.replace(/a/g, bar)',
46+
output: 'foo.replaceAll(\'a\', bar)',
47+
errors: [error]
48+
},
49+
// Comments
50+
{
51+
code: `
52+
foo/* comment 1 */
53+
.replace/* comment 2 */(
54+
/* comment 3 */
55+
/a/g // comment 4
56+
,
57+
bar
58+
)
59+
`,
60+
output: `
61+
foo/* comment 1 */
62+
.replaceAll/* comment 2 */(
63+
/* comment 3 */
64+
'a' // comment 4
65+
,
66+
bar
67+
)
68+
`,
69+
errors: [error]
70+
},
71+
// Quotes
72+
{
73+
code: 'foo.replace(/"\'/g, \'\\\'\')',
74+
output: 'foo.replaceAll(\'"\\\'\', \'\\\'\')',
75+
errors: [error]
76+
},
77+
// Escaped symbols
78+
{
79+
code: 'foo.replace(/\\./g, bar)',
80+
output: 'foo.replaceAll(\'.\', bar)',
81+
errors: [error]
82+
},
83+
{
84+
code: 'foo.replace(/\\\\\\./g, bar)',
85+
output: 'foo.replaceAll(\'\\.\', bar)',
86+
errors: [error]
87+
}
88+
]
89+
});

0 commit comments

Comments
 (0)