Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update: prefer-regex-literal detects regex literals passed to RegExp (fixes #12840) #12842

Merged
merged 1 commit into from Jun 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 }]
},
mdjermanovic marked this conversation as resolved.
Show resolved Hide resolved
{
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 }]
mdjermanovic marked this conversation as resolved.
Show resolved Hide resolved
},
{
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 }]
}
]
});