forked from sindresorhus/eslint-plugin-unicorn
-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
prefer-string-replace-all.js
135 lines (113 loc) · 3.21 KB
/
prefer-string-replace-all.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
'use strict';
const {getStaticValue} = require('eslint-utils');
const {parse: parseRegExp} = require('regjsparser');
const escapeString = require('./utils/escape-string.js');
const {methodCallSelector} = require('./selectors/index.js');
const {isRegexLiteral, isNewExpression} = require('./ast/index.js');
const MESSAGE_ID_USE_REPLACE_ALL = 'method';
const MESSAGE_ID_USE_STRING = 'pattern';
const messages = {
[MESSAGE_ID_USE_REPLACE_ALL]: 'Prefer `String#replaceAll()` over `String#replace()`.',
[MESSAGE_ID_USE_STRING]: 'This pattern can be replaced with {{replacement}}.',
};
const selector = methodCallSelector({
methods: ['replace', 'replaceAll'],
argumentsLength: 2,
});
function getPatternReplacement(node) {
if (!isRegexLiteral(node)) {
return;
}
const {pattern, flags} = node.regex;
if (flags.replace('u', '') !== 'g') {
return;
}
const tree = parseRegExp(pattern, flags, {
unicodePropertyEscape: true,
namedGroups: true,
lookbehind: true,
});
const parts = tree.type === 'alternative' ? tree.body : [tree];
if (parts.some(part => part.type !== 'value')) {
return;
}
// TODO: Preserve escape
const string = String.fromCodePoint(...parts.map(part => part.codePoint));
return escapeString(string);
}
const isRegExpWithGlobalFlag = (node, scope) => {
if (isRegexLiteral(node)) {
return node.regex.flags.includes('g');
}
if (
isNewExpression(node, {name: 'RegExp'})
&& node.arguments[0]?.type !== 'SpreadElement'
&& node.arguments[1]?.type === 'Literal'
&& typeof node.arguments[1].value === 'string'
) {
return node.arguments[1].value.includes('g');
}
const staticResult = getStaticValue(node, scope);
// Don't know if there is `g` flag
if (!staticResult) {
return false;
}
const {value} = staticResult;
return (
Object.prototype.toString.call(value) === '[object RegExp]'
&& value.global
);
};
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => ({
[selector](node) {
const {
arguments: [pattern],
callee: {property},
} = node;
if (!isRegExpWithGlobalFlag(pattern, context.getScope())) {
return;
}
const methodName = property.name;
const patternReplacement = getPatternReplacement(pattern);
if (methodName === 'replaceAll') {
if (!patternReplacement) {
return;
}
return {
node: pattern,
messageId: MESSAGE_ID_USE_STRING,
data: {
// Show `This pattern can be replaced with a string literal.` for long strings
replacement: patternReplacement.length < 20 ? patternReplacement : 'a string literal',
},
/** @param {import('eslint').Rule.RuleFixer} fixer */
fix: fixer => fixer.replaceText(pattern, patternReplacement),
};
}
return {
node: property,
messageId: MESSAGE_ID_USE_REPLACE_ALL,
/** @param {import('eslint').Rule.RuleFixer} fixer */
* fix(fixer) {
yield fixer.insertTextAfter(property, 'All');
if (!patternReplacement) {
return;
}
yield fixer.replaceText(pattern, patternReplacement);
},
};
},
});
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Prefer `String#replaceAll()` over regex searches with the global flag.',
},
fixable: 'code',
messages,
},
};