Skip to content

Commit d53d69a

Browse files
authored
Update: prefer-regex-literal detect regex literals (fixes #12840) (#12842)
The rule `prefer-regex-literal` now detects when regex literals are unnecessarily passed to the `RegExp` constructor.
1 parent 004adae commit d53d69a

3 files changed

Lines changed: 143 additions & 8 deletions

File tree

docs/rules/prefer-regex-literals.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,38 @@ RegExp(`${prefix}abc`);
8888
new RegExp(String.raw`^\d\. ${suffix}`);
8989
```
9090

91+
## Options
92+
93+
This rule has an object option:
94+
95+
* `disallowRedundantWrapping` set to `true` additionally checks for unnecessarily wrapped regex literals (Default `false`).
96+
97+
### `disallowRedundantWrapping`
98+
99+
By default, this rule doesn’t check when a regex literal is unnecessarily wrapped in a `RegExp` constructor call. When the option `disallowRedundantWrapping` is set to `true`, the rule will also disallow such unnecessary patterns.
100+
101+
Examples of `incorrect` code for `{ "disallowRedundantWrapping": true }`
102+
103+
```js
104+
/*eslint prefer-regex-literals: ["error", {"disallowRedundantWrapping": true}]*/
105+
106+
new RegExp(/abc/);
107+
108+
new RegExp(/abc/, 'u');
109+
```
110+
111+
Examples of `correct` code for `{ "disallowRedundantWrapping": true }`
112+
113+
```js
114+
/*eslint prefer-regex-literals: ["error", {"disallowRedundantWrapping": true}]*/
115+
116+
/abc/;
117+
118+
/abc/u;
119+
120+
new RegExp(/abc/, flags);
121+
```
122+
91123
## Further Reading
92124

93125
* [MDN: Regular Expressions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions)

lib/rules/prefer-regex-literals.js

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ function isStringLiteral(node) {
2525
return node.type === "Literal" && typeof node.value === "string";
2626
}
2727

28+
/**
29+
* Determines whether the given node is a regex literal.
30+
* @param {ASTNode} node Node to check.
31+
* @returns {boolean} True if the node is a regex literal.
32+
*/
33+
function isRegexLiteral(node) {
34+
return node.type === "Literal" && Object.prototype.hasOwnProperty.call(node, "regex");
35+
}
36+
2837
/**
2938
* Determines whether the given node is a template literal without expressions.
3039
* @param {ASTNode} node Node to check.
@@ -50,14 +59,28 @@ module.exports = {
5059
url: "https://eslint.org/docs/rules/prefer-regex-literals"
5160
},
5261

53-
schema: [],
62+
schema: [
63+
{
64+
type: "object",
65+
properties: {
66+
disallowRedundantWrapping: {
67+
type: "boolean",
68+
default: false
69+
}
70+
},
71+
additionalProperties: false
72+
}
73+
],
5474

5575
messages: {
56-
unexpectedRegExp: "Use a regular expression literal instead of the 'RegExp' constructor."
76+
unexpectedRegExp: "Use a regular expression literal instead of the 'RegExp' constructor.",
77+
unexpectedRedundantRegExp: "Regular expression literal is unnecessarily wrapped within a 'RegExp' constructor.",
78+
unexpectedRedundantRegExpWithFlags: "Use regular expression literal with flags instead of the 'RegExp' constructor."
5779
}
5880
},
5981

6082
create(context) {
83+
const [{ disallowRedundantWrapping = false } = {}] = context.options;
6184

6285
/**
6386
* Determines whether the given identifier node is a reference to a global variable.
@@ -98,6 +121,40 @@ module.exports = {
98121
isStringRawTaggedStaticTemplateLiteral(node);
99122
}
100123

124+
/**
125+
* Determines whether the relevant arguments of the given are all static string literals.
126+
* @param {ASTNode} node Node to check.
127+
* @returns {boolean} True if all arguments are static strings.
128+
*/
129+
function hasOnlyStaticStringArguments(node) {
130+
const args = node.arguments;
131+
132+
if ((args.length === 1 || args.length === 2) && args.every(isStaticString)) {
133+
return true;
134+
}
135+
136+
return false;
137+
}
138+
139+
/**
140+
* Determines whether the arguments of the given node indicate that a regex literal is unnecessarily wrapped.
141+
* @param {ASTNode} node Node to check.
142+
* @returns {boolean} True if the node already contains a regex literal argument.
143+
*/
144+
function isUnnecessarilyWrappedRegexLiteral(node) {
145+
const args = node.arguments;
146+
147+
if (args.length === 1 && isRegexLiteral(args[0])) {
148+
return true;
149+
}
150+
151+
if (args.length === 2 && isRegexLiteral(args[0]) && isStaticString(args[1])) {
152+
return true;
153+
}
154+
155+
return false;
156+
}
157+
101158
return {
102159
Program() {
103160
const scope = context.getScope();
@@ -110,12 +167,13 @@ module.exports = {
110167
};
111168

112169
for (const { node } of tracker.iterateGlobalReferences(traceMap)) {
113-
const args = node.arguments;
114-
115-
if (
116-
(args.length === 1 || args.length === 2) &&
117-
args.every(isStaticString)
118-
) {
170+
if (disallowRedundantWrapping && isUnnecessarilyWrappedRegexLiteral(node)) {
171+
if (node.arguments.length === 2) {
172+
context.report({ node, messageId: "unexpectedRedundantRegExpWithFlags" });
173+
} else {
174+
context.report({ node, messageId: "unexpectedRedundantRegExp" });
175+
}
176+
} else if (hasOnlyStaticStringArguments(node)) {
119177
context.report({ node, messageId: "unexpectedRegExp" });
120178
}
121179
}

tests/lib/rules/prefer-regex-literals.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ ruleTester.run("prefer-regex-literals", rule, {
2323
"/abc/",
2424
"/abc/g",
2525

26+
2627
// considered as dynamic
2728
"new RegExp(pattern)",
2829
"RegExp(pattern, 'g')",
@@ -41,6 +42,26 @@ ruleTester.run("prefer-regex-literals", rule, {
4142
"new RegExp(String.raw`a${''}c`);",
4243
"new RegExp('a' + 'b')",
4344
"RegExp(1)",
45+
"new RegExp(/a/, 'u');",
46+
"new RegExp(/a/);",
47+
{
48+
code: "new RegExp(/a/, flags);",
49+
options: [{ disallowRedundantWrapping: true }]
50+
},
51+
{
52+
code: "new RegExp(/a/, `u${flags}`);",
53+
options: [{ disallowRedundantWrapping: true }]
54+
},
55+
56+
// redundant wrapping is allowed
57+
{
58+
code: "new RegExp(/a/);",
59+
options: [{}]
60+
},
61+
{
62+
code: "new RegExp(/a/);",
63+
options: [{ disallowRedundantWrapping: false }]
64+
},
4465

4566
// invalid number of arguments
4667
"new RegExp;",
@@ -52,6 +73,10 @@ ruleTester.run("prefer-regex-literals", rule, {
5273
"RegExp(`a`, `g`, `b`);",
5374
"new RegExp(String.raw`a`, String.raw`g`, String.raw`b`);",
5475
"RegExp(String.raw`a`, String.raw`g`, String.raw`b`);",
76+
{
77+
code: "new RegExp(/a/, 'u', 'foo');",
78+
options: [{ disallowRedundantWrapping: true }]
79+
},
5580

5681
// not String.raw``
5782
"new RegExp(String`a`);",
@@ -196,6 +221,26 @@ ruleTester.run("prefer-regex-literals", rule, {
196221
code: "globalThis.RegExp('a');",
197222
env: { es2020: true },
198223
errors: [{ messageId: "unexpectedRegExp", type: "CallExpression" }]
224+
},
225+
{
226+
code: "new RegExp(/a/);",
227+
options: [{ disallowRedundantWrapping: true }],
228+
errors: [{ messageId: "unexpectedRedundantRegExp", type: "NewExpression", line: 1, column: 1 }]
229+
},
230+
{
231+
code: "new RegExp(/a/, 'u');",
232+
options: [{ disallowRedundantWrapping: true }],
233+
errors: [{ messageId: "unexpectedRedundantRegExpWithFlags", type: "NewExpression", line: 1, column: 1 }]
234+
},
235+
{
236+
code: "new RegExp(/a/, `u`);",
237+
options: [{ disallowRedundantWrapping: true }],
238+
errors: [{ messageId: "unexpectedRedundantRegExpWithFlags", type: "NewExpression", line: 1, column: 1 }]
239+
},
240+
{
241+
code: "new RegExp('a');",
242+
options: [{ disallowRedundantWrapping: true }],
243+
errors: [{ messageId: "unexpectedRegExp", type: "NewExpression", line: 1, column: 1 }]
199244
}
200245
]
201246
});

0 commit comments

Comments
 (0)