diff --git a/docs/rules/prefer-regex-literals.md b/docs/rules/prefer-regex-literals.md index 01d4149cf207..f4dab73ff6f3 100644 --- a/docs/rules/prefer-regex-literals.md +++ b/docs/rules/prefer-regex-literals.md @@ -84,6 +84,36 @@ RegExp(`${prefix}abc`); new RegExp(String.raw`^\d\. ${sufix}`); ``` +## 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-literal: ["error", {"disallowRedundantWrapping": true}]*/ +new RegExp(/abc/); + +new RegExp(/abc/, 'u'); +``` + +Examples of `correct` code for `{ "disallowRedundantWrapping": true }` + +```js +/*eslint prefer-regex-literal: ["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) diff --git a/lib/rules/prefer-regex-literals.js b/lib/rules/prefer-regex-literals.js index 47b2b090f829..02b8a2d18568 100644 --- a/lib/rules/prefer-regex-literals.js +++ b/lib/rules/prefer-regex-literals.js @@ -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. @@ -50,14 +59,27 @@ 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." } }, create(context) { + const [{ disallowRedundantWrapping = false } = {}] = context.options; /** * Determines whether the given identifier node is a reference to a global variable. @@ -98,6 +120,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(); @@ -110,12 +166,9 @@ 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)) { + context.report({ node, messageId: "unexpectedRedundantRegExp" }); + } else if (hasOnlyStaticStringArguments(node)) { context.report({ node, messageId: "unexpectedRegExp" }); } } diff --git a/tests/lib/rules/prefer-regex-literals.js b/tests/lib/rules/prefer-regex-literals.js index e263ef03dc7a..8cde127c7057 100644 --- a/tests/lib/rules/prefer-regex-literals.js +++ b/tests/lib/rules/prefer-regex-literals.js @@ -41,6 +41,12 @@ 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 }] + }, // invalid number of arguments "new RegExp;", @@ -177,6 +183,16 @@ ruleTester.run("prefer-regex-literals", rule, { { code: "RegExp('a', String.raw`g`);", 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: "unexpectedRedundantRegExp", type: "NewExpression", line: 1, column: 1 }] } ] });