Skip to content

Commit 8d97beb

Browse files
authored
prefer-regexp-test: Do not fix if regexp has g flag (sindresorhus#1173)
1 parent 0d4fc8b commit 8d97beb

File tree

5 files changed

+366
-75
lines changed

5 files changed

+366
-75
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@
124124
"rules": {
125125
"strict": "error",
126126
"node/no-unsupported-features/node-builtins": [
127-
"off",
127+
"error",
128128
{
129129
"ignores": [
130130
"module.createRequire"

rules/prefer-regexp-test.js

Lines changed: 105 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,133 @@
11
'use strict';
22
const {isParenthesized, getStaticValue} = require('eslint-utils');
3+
const {fromPairs} = require('lodash');
34
const getDocumentationUrl = require('./utils/get-documentation-url');
45
const methodSelector = require('./utils/method-selector');
56
const {isBooleanNode} = require('./utils/boolean');
67
const shouldAddParenthesesToMemberExpressionObject = require('./utils/should-add-parentheses-to-member-expression-object');
78

8-
const MESSAGE_ID_REGEXP_EXEC = 'regexp-exec';
9-
const MESSAGE_ID_STRING_MATCH = 'string-match';
9+
const REGEXP_EXEC = 'regexp-exec';
10+
const STRING_MATCH = 'string-match';
1011
const messages = {
11-
[MESSAGE_ID_REGEXP_EXEC]: 'Prefer `.test(…)` over `.exec(…)`.',
12-
[MESSAGE_ID_STRING_MATCH]: 'Prefer `RegExp#test(…)` over `String#match(…)`.'
12+
[REGEXP_EXEC]: 'Prefer `.test(…)` over `.exec(…)`.',
13+
[STRING_MATCH]: 'Prefer `RegExp#test(…)` over `String#match(…)`.'
1314
};
1415

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;
16+
const cases = [
17+
{
18+
type: REGEXP_EXEC,
19+
selector: methodSelector({
20+
name: 'exec',
21+
length: 1
22+
}),
23+
getNodes: node => ({
24+
stringNode: node.arguments[0],
25+
methodNode: node.callee.property,
26+
regexpNode: node.callee.object
27+
}),
28+
fix: (fixer, {methodNode}) => fixer.replaceText(methodNode, 'test')
29+
},
30+
{
31+
type: STRING_MATCH,
32+
selector: methodSelector({
33+
name: 'match',
34+
length: 1
35+
}),
36+
getNodes: node => ({
37+
stringNode: node.callee.object,
38+
methodNode: node.callee.property,
39+
regexpNode: node.arguments[0]
40+
}),
41+
* fix(fixer, {stringNode, methodNode, regexpNode}, sourceCode) {
42+
yield fixer.replaceText(methodNode, 'test');
43+
44+
let stringText = sourceCode.getText(stringNode);
45+
if (
46+
!isParenthesized(regexpNode, sourceCode) &&
47+
// Only `SequenceExpression` need add parentheses
48+
stringNode.type === 'SequenceExpression'
49+
) {
50+
stringText = `(${stringText})`;
3251
}
3352

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-
}
53+
yield fixer.replaceText(regexpNode, stringText);
4554

46-
const regexpNode = node.arguments[0];
47-
if (regexpNode.type === 'Literal' && !regexpNode.regex) {
48-
return;
55+
let regexpText = sourceCode.getText(regexpNode);
56+
if (
57+
!isParenthesized(stringNode, sourceCode) &&
58+
shouldAddParenthesesToMemberExpressionObject(regexpNode, sourceCode)
59+
) {
60+
regexpText = `(${regexpText})`;
4961
}
5062

51-
const problem = {
52-
node,
53-
messageId: MESSAGE_ID_STRING_MATCH
54-
};
55-
56-
const staticResult = getStaticValue(regexpNode, context.getScope());
57-
if (staticResult) {
58-
const {value} = staticResult;
63+
// The nodes that pass `isBooleanNode` cannot have an ASI problem.
5964

60-
if (Object.prototype.toString.call(value) !== '[object RegExp]') {
61-
context.report(problem);
62-
return;
63-
}
64-
}
65+
yield fixer.replaceText(stringNode, regexpText);
66+
}
67+
}
68+
];
6569

66-
const stringNode = node.callee.object;
70+
const isRegExpNode = node => {
71+
if (node.type === 'Literal' && node.regex) {
72+
return true;
73+
}
6774

68-
problem.fix = function * (fixer) {
69-
yield fixer.replaceText(node.callee.property, 'test');
75+
if (
76+
node.type === 'NewExpression' &&
77+
node.callee.type === 'Identifier' &&
78+
node.callee.name === 'RegExp'
79+
) {
80+
return true;
81+
}
7082

71-
let stringText = sourceCode.getText(stringNode);
72-
if (
73-
!isParenthesized(regexpNode, sourceCode) &&
74-
// Only `SequenceExpression` need add parentheses
75-
stringNode.type === 'SequenceExpression'
76-
) {
77-
stringText = `(${stringText})`;
78-
}
83+
return false;
84+
};
7985

80-
yield fixer.replaceText(regexpNode, stringText);
86+
function getProblem(node, checkCase, context) {
87+
if (!isBooleanNode(node)) {
88+
return;
89+
}
8190

82-
let regexpText = sourceCode.getText(regexpNode);
83-
if (
84-
!isParenthesized(stringNode, sourceCode) &&
85-
shouldAddParenthesesToMemberExpressionObject(regexpNode, sourceCode)
86-
) {
87-
regexpText = `(${regexpText})`;
88-
}
91+
const {type, getNodes, fix} = checkCase;
92+
const nodes = getNodes(node);
93+
const {methodNode, regexpNode} = nodes;
94+
const problem = {
95+
node: type === REGEXP_EXEC ? methodNode : node,
96+
messageId: type
97+
};
8998

90-
// The nodes that pass `isBooleanNode` cannot have an ASI problem.
99+
if (regexpNode.type === 'Literal' && !regexpNode.regex) {
100+
return;
101+
}
91102

92-
yield fixer.replaceText(stringNode, regexpText);
93-
};
103+
if (!isRegExpNode(regexpNode)) {
104+
const staticResult = getStaticValue(regexpNode, context.getScope());
105+
if (staticResult) {
106+
const {value} = staticResult;
107+
if (
108+
Object.prototype.toString.call(value) !== '[object RegExp]' ||
109+
value.flags.includes('g')
110+
) {
111+
return problem;
112+
}
113+
}
114+
}
94115

95-
context.report(problem);
116+
problem.fix = fixer => fix(fixer, nodes, context.getSourceCode());
117+
return problem;
118+
}
119+
120+
const create = context => fromPairs(
121+
cases.map(checkCase => [
122+
checkCase.selector,
123+
node => {
124+
const problem = getProblem(node, checkCase, context);
125+
if (problem) {
126+
context.report(problem);
127+
}
96128
}
97-
};
98-
};
129+
])
130+
);
99131

100132
module.exports = {
101133
create,

test/prefer-regexp-test.mjs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,34 @@ test.snapshot({
122122
const regex = new RegExp('[.!?]\\s*$');
123123
if (foo.match(regex)) {}
124124
`,
125-
'if (foo.match(unknown)) {}'
125+
'if (foo.match(unknown)) {}',
126+
// `g` and `y` flags
127+
'if (foo.match(/a/g));',
128+
'if (foo.match(/a/y));',
129+
'if (foo.match(/a/gy));',
130+
'if (foo.match(/a/ig));',
131+
'if (foo.match(new RegExp("a", "g")));',
132+
'if (/a/g.exec(foo));',
133+
'if (/a/y.exec(foo));',
134+
'if (/a/gy.exec(foo));',
135+
'if (/a/yi.exec(foo));',
136+
'if (new RegExp("a", "g").exec(foo));',
137+
'if (new RegExp("a", "y").exec(foo));',
138+
outdent`
139+
const regex = /weird/g;
140+
if (foo.match(regex));
141+
`,
142+
outdent`
143+
const regex = /weird/g;
144+
if (regex.exec(foo));
145+
`,
146+
outdent`
147+
const regex = /weird/y;
148+
if (regex.exec(foo));
149+
`,
150+
outdent`
151+
const regex = /weird/gyi;
152+
if (regex.exec(foo));
153+
`
126154
]
127155
});

0 commit comments

Comments
 (0)