Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Update: prefer-regex-literal detect regex literals (fixes #12840) (#1…
…2842)

The rule `prefer-regex-literal` now detects when regex literals are
unnecessarily passed to the `RegExp` constructor.
  • Loading branch information
lo1tuma committed Jun 25, 2020
1 parent 004adae commit d53d69a
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 8 deletions.
32 changes: 32 additions & 0 deletions docs/rules/prefer-regex-literals.md
Expand Up @@ -88,6 +88,38 @@ RegExp(`${prefix}abc`);
new RegExp(String.raw`^\d\. ${suffix}`);
```

## Options

This rule has an object option:

* `disallowRedundantWrapping` set to `true` additionally checks for unnecessarily wrapped regex literals (Default `false`).

### `disallowRedundantWrapping`

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.

Examples of `incorrect` code for `{ "disallowRedundantWrapping": true }`

```js
/*eslint prefer-regex-literals: ["error", {"disallowRedundantWrapping": true}]*/

new RegExp(/abc/);

new RegExp(/abc/, 'u');
```

Examples of `correct` code for `{ "disallowRedundantWrapping": true }`

```js
/*eslint prefer-regex-literals: ["error", {"disallowRedundantWrapping": true}]*/

/abc/;

/abc/u;

new RegExp(/abc/, flags);
```

## Further Reading

* [MDN: Regular Expressions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions)
Expand Down
74 changes: 66 additions & 8 deletions lib/rules/prefer-regex-literals.js
Expand Up @@ -25,6 +25,15 @@ function isStringLiteral(node) {
return node.type === "Literal" && typeof node.value === "string";
}

/**
* Determines whether the given node is a regex literal.
* @param {ASTNode} node Node to check.
* @returns {boolean} True if the node is a regex literal.
*/
function isRegexLiteral(node) {
return node.type === "Literal" && Object.prototype.hasOwnProperty.call(node, "regex");
}

/**
* Determines whether the given node is a template literal without expressions.
* @param {ASTNode} node Node to check.
Expand All @@ -50,14 +59,28 @@ module.exports = {
url: "https://eslint.org/docs/rules/prefer-regex-literals"
},

schema: [],
schema: [
{
type: "object",
properties: {
disallowRedundantWrapping: {
type: "boolean",
default: false
}
},
additionalProperties: false
}
],

messages: {
unexpectedRegExp: "Use a regular expression literal instead of the 'RegExp' constructor."
unexpectedRegExp: "Use a regular expression literal instead of the 'RegExp' constructor.",
unexpectedRedundantRegExp: "Regular expression literal is unnecessarily wrapped within a 'RegExp' constructor.",
unexpectedRedundantRegExpWithFlags: "Use regular expression literal with flags instead of the 'RegExp' constructor."
}
},

create(context) {
const [{ disallowRedundantWrapping = false } = {}] = context.options;

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

/**
* Determines whether the relevant arguments of the given are all static string literals.
* @param {ASTNode} node Node to check.
* @returns {boolean} True if all arguments are static strings.
*/
function hasOnlyStaticStringArguments(node) {
const args = node.arguments;

if ((args.length === 1 || args.length === 2) && args.every(isStaticString)) {
return true;
}

return false;
}

/**
* Determines whether the arguments of the given node indicate that a regex literal is unnecessarily wrapped.
* @param {ASTNode} node Node to check.
* @returns {boolean} True if the node already contains a regex literal argument.
*/
function isUnnecessarilyWrappedRegexLiteral(node) {
const args = node.arguments;

if (args.length === 1 && isRegexLiteral(args[0])) {
return true;
}

if (args.length === 2 && isRegexLiteral(args[0]) && isStaticString(args[1])) {
return true;
}

return false;
}

return {
Program() {
const scope = context.getScope();
Expand All @@ -110,12 +167,13 @@ module.exports = {
};

for (const { node } of tracker.iterateGlobalReferences(traceMap)) {
const args = node.arguments;

if (
(args.length === 1 || args.length === 2) &&
args.every(isStaticString)
) {
if (disallowRedundantWrapping && isUnnecessarilyWrappedRegexLiteral(node)) {
if (node.arguments.length === 2) {
context.report({ node, messageId: "unexpectedRedundantRegExpWithFlags" });
} else {
context.report({ node, messageId: "unexpectedRedundantRegExp" });
}
} else if (hasOnlyStaticStringArguments(node)) {
context.report({ node, messageId: "unexpectedRegExp" });
}
}
Expand Down
45 changes: 45 additions & 0 deletions tests/lib/rules/prefer-regex-literals.js
Expand Up @@ -23,6 +23,7 @@ ruleTester.run("prefer-regex-literals", rule, {
"/abc/",
"/abc/g",


// considered as dynamic
"new RegExp(pattern)",
"RegExp(pattern, 'g')",
Expand All @@ -41,6 +42,26 @@ ruleTester.run("prefer-regex-literals", rule, {
"new RegExp(String.raw`a${''}c`);",
"new RegExp('a' + 'b')",
"RegExp(1)",
"new RegExp(/a/, 'u');",
"new RegExp(/a/);",
{
code: "new RegExp(/a/, flags);",
options: [{ disallowRedundantWrapping: true }]
},
{
code: "new RegExp(/a/, `u${flags}`);",
options: [{ disallowRedundantWrapping: true }]
},

// redundant wrapping is allowed
{
code: "new RegExp(/a/);",
options: [{}]
},
{
code: "new RegExp(/a/);",
options: [{ disallowRedundantWrapping: false }]
},

// invalid number of arguments
"new RegExp;",
Expand All @@ -52,6 +73,10 @@ ruleTester.run("prefer-regex-literals", rule, {
"RegExp(`a`, `g`, `b`);",
"new RegExp(String.raw`a`, String.raw`g`, String.raw`b`);",
"RegExp(String.raw`a`, String.raw`g`, String.raw`b`);",
{
code: "new RegExp(/a/, 'u', 'foo');",
options: [{ disallowRedundantWrapping: true }]
},

// not String.raw``
"new RegExp(String`a`);",
Expand Down Expand Up @@ -196,6 +221,26 @@ ruleTester.run("prefer-regex-literals", rule, {
code: "globalThis.RegExp('a');",
env: { es2020: true },
errors: [{ messageId: "unexpectedRegExp", type: "CallExpression" }]
},
{
code: "new RegExp(/a/);",
options: [{ disallowRedundantWrapping: true }],
errors: [{ messageId: "unexpectedRedundantRegExp", type: "NewExpression", line: 1, column: 1 }]
},
{
code: "new RegExp(/a/, 'u');",
options: [{ disallowRedundantWrapping: true }],
errors: [{ messageId: "unexpectedRedundantRegExpWithFlags", type: "NewExpression", line: 1, column: 1 }]
},
{
code: "new RegExp(/a/, `u`);",
options: [{ disallowRedundantWrapping: true }],
errors: [{ messageId: "unexpectedRedundantRegExpWithFlags", type: "NewExpression", line: 1, column: 1 }]
},
{
code: "new RegExp('a');",
options: [{ disallowRedundantWrapping: true }],
errors: [{ messageId: "unexpectedRegExp", type: "NewExpression", line: 1, column: 1 }]
}
]
});

0 comments on commit d53d69a

Please sign in to comment.