diff --git a/docs/rules/prefer-regex-literals.md b/docs/rules/prefer-regex-literals.md index fea589d3412..2ba8cacc08d 100644 --- a/docs/rules/prefer-regex-literals.md +++ b/docs/rules/prefer-regex-literals.md @@ -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) diff --git a/lib/rules/prefer-regex-literals.js b/lib/rules/prefer-regex-literals.js index 47b2b090f82..8a5d209c1e2 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,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. @@ -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(); @@ -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" }); } } diff --git a/tests/lib/rules/prefer-regex-literals.js b/tests/lib/rules/prefer-regex-literals.js index 65979de83f9..9f6a2e6fbae 100644 --- a/tests/lib/rules/prefer-regex-literals.js +++ b/tests/lib/rules/prefer-regex-literals.js @@ -23,6 +23,7 @@ ruleTester.run("prefer-regex-literals", rule, { "/abc/", "/abc/g", + // considered as dynamic "new RegExp(pattern)", "RegExp(pattern, 'g')", @@ -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;", @@ -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`);", @@ -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 }] } ] });