diff --git a/lib/rules/prefer-regex-literals.js b/lib/rules/prefer-regex-literals.js index 02e2f5f44a0..fdf18874e77 100644 --- a/lib/rules/prefer-regex-literals.js +++ b/lib/rules/prefer-regex-literals.js @@ -146,6 +146,8 @@ module.exports = { messages: { unexpectedRegExp: "Use a regular expression literal instead of the 'RegExp' constructor.", replaceWithLiteral: "Replace with an equivalent regular expression literal.", + replaceWithLiteralAndFlags: "Replace with an equivalent regular expression literal with flags '{{ flags }}'.", + replaceWithIntendedLiteralAndFlags: "Replace with a regular expression literal with flags '{{ flags }}'.", unexpectedRedundantRegExp: "Regular expression literal is unnecessarily wrapped within a 'RegExp' constructor.", unexpectedRedundantRegExpWithFlags: "Use regular expression literal with flags instead of the 'RegExp' constructor." } @@ -258,6 +260,8 @@ module.exports = { return Math.min(ecmaVersion, REGEXPP_LATEST_ECMA_VERSION); } + const regexppEcmaVersion = getRegexppEcmaVersion(context.languageOptions.ecmaVersion); + /** * Makes a character escaped or else returns null. * @param {string} character The character to escape. @@ -293,6 +297,83 @@ module.exports = { } } + /** + * Checks whether the given regex and flags are valid for the ecma version or not. + * @param {string} pattern The regex pattern to check. + * @param {string | undefined} flags The regex flags to check. + * @returns {boolean} True if the given regex pattern and flags are valid for the ecma version. + */ + function isValidRegexForEcmaVersion(pattern, flags) { + const validator = new RegExpValidator({ ecmaVersion: regexppEcmaVersion }); + + try { + validator.validatePattern(pattern, 0, pattern.length, flags ? flags.includes("u") : false); + if (flags) { + validator.validateFlags(flags); + } + return true; + } catch { + return false; + } + } + + /** + * Checks whether two given regex flags contain the same flags or not. + * @param {string} flagsA The regex flags. + * @param {string} flagsB The regex flags. + * @returns {boolean} True if two regex flags contain same flags. + */ + function areFlagsEqual(flagsA, flagsB) { + return [...flagsA].sort().join("") === [...flagsB].sort().join(""); + } + + + /** + * Merges two regex flags. + * @param {string} flagsA The regex flags. + * @param {string} flagsB The regex flags. + * @returns {string} The merged regex flags. + */ + function mergeRegexFlags(flagsA, flagsB) { + const flagsSet = new Set([ + ...flagsA, + ...flagsB + ]); + + return [...flagsSet].join(""); + } + + /** + * Checks whether a give node can be fixed to the given regex pattern and flags. + * @param {ASTNode} node The node to check. + * @param {string} pattern The regex pattern to check. + * @param {string} flags The regex flags + * @returns {boolean} True if a node can be fixed to the given regex pattern and flags. + */ + function canFixTo(node, pattern, flags) { + const tokenBefore = sourceCode.getTokenBefore(node); + + return sourceCode.getCommentsInside(node).length === 0 && + (!tokenBefore || validPrecedingTokens.has(tokenBefore.value)) && + isValidRegexForEcmaVersion(pattern, flags); + } + + /** + * Returns a safe output code considering the before and after tokens. + * @param {ASTNode} node The regex node. + * @param {string} newRegExpValue The new regex expression value. + * @returns {string} The output code. + */ + function getSafeOutput(node, newRegExpValue) { + const tokenBefore = sourceCode.getTokenBefore(node); + const tokenAfter = sourceCode.getTokenAfter(node); + + return (tokenBefore && !canTokensBeAdjacent(tokenBefore, newRegExpValue) && tokenBefore.range[1] === node.range[0] ? " " : "") + + newRegExpValue + + (tokenAfter && !canTokensBeAdjacent(newRegExpValue, tokenAfter) && node.range[1] === tokenAfter.range[0] ? " " : ""); + + } + return { Program() { const scope = context.getScope(); @@ -306,10 +387,69 @@ module.exports = { for (const { node } of tracker.iterateGlobalReferences(traceMap)) { if (disallowRedundantWrapping && isUnnecessarilyWrappedRegexLiteral(node)) { + const regexNode = node.arguments[0]; + if (node.arguments.length === 2) { - context.report({ node, messageId: "unexpectedRedundantRegExpWithFlags" }); + const suggests = []; + + const argFlags = getStringValue(node.arguments[1]) || ""; + + if (canFixTo(node, regexNode.regex.pattern, argFlags)) { + suggests.push({ + messageId: "replaceWithLiteralAndFlags", + pattern: regexNode.regex.pattern, + flags: argFlags + }); + } + + const literalFlags = regexNode.regex.flags || ""; + const mergedFlags = mergeRegexFlags(literalFlags, argFlags); + + if ( + !areFlagsEqual(mergedFlags, argFlags) && + canFixTo(node, regexNode.regex.pattern, mergedFlags) + ) { + suggests.push({ + messageId: "replaceWithIntendedLiteralAndFlags", + pattern: regexNode.regex.pattern, + flags: mergedFlags + }); + } + + context.report({ + node, + messageId: "unexpectedRedundantRegExpWithFlags", + suggest: suggests.map(({ flags, pattern, messageId }) => ({ + messageId, + data: { + flags + }, + fix(fixer) { + return fixer.replaceText(node, getSafeOutput(node, `/${pattern}/${flags}`)); + } + })) + }); } else { - context.report({ node, messageId: "unexpectedRedundantRegExp" }); + const outputs = []; + + if (canFixTo(node, regexNode.regex.pattern, regexNode.regex.flags)) { + outputs.push(sourceCode.getText(regexNode)); + } + + + context.report({ + node, + messageId: "unexpectedRedundantRegExp", + suggest: outputs.map(output => ({ + messageId: "replaceWithLiteral", + fix(fixer) { + return fixer.replaceText( + node, + getSafeOutput(node, output) + ); + } + })) + }); } } else if (hasOnlyStaticStringArguments(node)) { let regexContent = getStringValue(node.arguments[0]); @@ -320,21 +460,7 @@ module.exports = { flags = getStringValue(node.arguments[1]); } - const regexppEcmaVersion = getRegexppEcmaVersion(context.languageOptions.ecmaVersion); - const RegExpValidatorInstance = new RegExpValidator({ ecmaVersion: regexppEcmaVersion }); - - try { - RegExpValidatorInstance.validatePattern(regexContent, 0, regexContent.length, flags ? flags.includes("u") : false); - if (flags) { - RegExpValidatorInstance.validateFlags(flags); - } - } catch { - noFix = true; - } - - const tokenBefore = sourceCode.getTokenBefore(node); - - if (tokenBefore && !validPrecedingTokens.has(tokenBefore.value)) { + if (!canFixTo(node, regexContent, flags)) { noFix = true; } @@ -342,10 +468,6 @@ module.exports = { noFix = true; } - if (sourceCode.getCommentsInside(node).length > 0) { - noFix = true; - } - if (regexContent && !noFix) { let charIncrease = 0; @@ -377,14 +499,7 @@ module.exports = { suggest: noFix ? [] : [{ messageId: "replaceWithLiteral", fix(fixer) { - const tokenAfter = sourceCode.getTokenAfter(node); - - return fixer.replaceText( - node, - (tokenBefore && !canTokensBeAdjacent(tokenBefore, newRegExpValue) && tokenBefore.range[1] === node.range[0] ? " " : "") + - newRegExpValue + - (tokenAfter && !canTokensBeAdjacent(newRegExpValue, tokenAfter) && node.range[1] === tokenAfter.range[0] ? " " : "") - ); + return fixer.replaceText(node, getSafeOutput(node, newRegExpValue)); } }] }); diff --git a/tests/lib/rules/prefer-regex-literals.js b/tests/lib/rules/prefer-regex-literals.js index 429df4fd46d..054d89be1d7 100644 --- a/tests/lib/rules/prefer-regex-literals.js +++ b/tests/lib/rules/prefer-regex-literals.js @@ -576,7 +576,13 @@ ruleTester.run("prefer-regex-literals", rule, { messageId: "unexpectedRedundantRegExp", type: "NewExpression", line: 1, - column: 1 + column: 1, + suggestions: [ + { + messageId: "replaceWithLiteral", + output: "/a/;" + } + ] } ] }, @@ -592,7 +598,187 @@ ruleTester.run("prefer-regex-literals", rule, { messageId: "unexpectedRedundantRegExpWithFlags", type: "NewExpression", line: 1, - column: 1 + column: 1, + suggestions: [ + { + messageId: "replaceWithLiteralAndFlags", + output: "/a/u;", + data: { + flags: "u" + } + } + ] + } + ] + }, + { + code: "new RegExp(/a/g, '');", + options: [ + { + disallowRedundantWrapping: true + } + ], + errors: [ + { + messageId: "unexpectedRedundantRegExpWithFlags", + type: "NewExpression", + line: 1, + column: 1, + suggestions: [ + { + messageId: "replaceWithLiteralAndFlags", + output: "/a/;", + data: { + flags: "" + } + }, + { + messageId: "replaceWithIntendedLiteralAndFlags", + output: "/a/g;", + data: { + flags: "g" + } + } + ] + } + ] + }, + { + code: "new RegExp(/a/g, 'g');", + options: [ + { + disallowRedundantWrapping: true + } + ], + errors: [ + { + messageId: "unexpectedRedundantRegExpWithFlags", + type: "NewExpression", + line: 1, + column: 1, + suggestions: [ + { + messageId: "replaceWithLiteralAndFlags", + output: "/a/g;", + data: { + flags: "g" + } + } + ] + } + ] + }, + { + code: "new RegExp(/a/ig, 'g');", + options: [ + { + disallowRedundantWrapping: true + } + ], + errors: [ + { + messageId: "unexpectedRedundantRegExpWithFlags", + type: "NewExpression", + line: 1, + column: 1, + suggestions: [ + { + messageId: "replaceWithLiteralAndFlags", + output: "/a/g;", + data: { + flags: "g" + } + }, + { + messageId: "replaceWithIntendedLiteralAndFlags", + output: "/a/ig;", + data: { + flags: "ig" + } + } + ] + } + ] + }, + { + code: "new RegExp(/a/g, 'ig');", + options: [ + { + disallowRedundantWrapping: true + } + ], + errors: [ + { + messageId: "unexpectedRedundantRegExpWithFlags", + type: "NewExpression", + line: 1, + column: 1, + suggestions: [ + { + messageId: "replaceWithLiteralAndFlags", + output: "/a/ig;", + data: { + flags: "ig" + } + } + ] + } + ] + }, + { + code: "new RegExp(/a/i, 'g');", + options: [ + { + disallowRedundantWrapping: true + } + ], + errors: [ + { + messageId: "unexpectedRedundantRegExpWithFlags", + type: "NewExpression", + line: 1, + column: 1, + suggestions: [ + { + messageId: "replaceWithLiteralAndFlags", + output: "/a/g;", + data: { + flags: "g" + } + }, + { + messageId: "replaceWithIntendedLiteralAndFlags", + output: "/a/ig;", + data: { + flags: "ig" + } + } + ] + } + ] + }, + { + code: "new RegExp(/a/i, 'i');", + options: [ + { + disallowRedundantWrapping: true + } + ], + errors: [ + { + messageId: "unexpectedRedundantRegExpWithFlags", + type: "NewExpression", + line: 1, + column: 1, + suggestions: [ + { + messageId: "replaceWithLiteralAndFlags", + output: "/a/i;", + data: { + flags: "i" + } + } + ] } ] }, @@ -608,12 +794,21 @@ ruleTester.run("prefer-regex-literals", rule, { messageId: "unexpectedRedundantRegExpWithFlags", type: "NewExpression", line: 1, - column: 1 + column: 1, + suggestions: [ + { + messageId: "replaceWithLiteralAndFlags", + output: "/a/u;", + data: { + flags: "u" + } + } + ] } ] }, { - code: "new RegExp(/a/, String.raw`u`);", + code: "new RegExp(/a/, `gi`);", options: [ { disallowRedundantWrapping: true @@ -624,7 +819,16 @@ ruleTester.run("prefer-regex-literals", rule, { messageId: "unexpectedRedundantRegExpWithFlags", type: "NewExpression", line: 1, - column: 1 + column: 1, + suggestions: [ + { + messageId: "replaceWithLiteralAndFlags", + output: "/a/gi;", + data: { + flags: "gi" + } + } + ] } ] }, @@ -650,6 +854,138 @@ ruleTester.run("prefer-regex-literals", rule, { } ] }, + { + code: "new RegExp(/a/, String.raw`u`);", + options: [ + { + disallowRedundantWrapping: true + } + ], + errors: [ + { + messageId: "unexpectedRedundantRegExpWithFlags", + type: "NewExpression", + line: 1, + column: 1, + suggestions: [ + { + messageId: "replaceWithLiteralAndFlags", + output: "/a/u;", + data: { + flags: "u" + } + } + ] + } + ] + }, + { + code: "new RegExp(/a/ /* comment */);", + options: [ + { + disallowRedundantWrapping: true + } + ], + errors: [ + { + messageId: "unexpectedRedundantRegExp", + type: "NewExpression", + line: 1, + column: 1, + suggestions: null + } + ] + }, + { + code: "new RegExp(/a/, 'd');", + options: [ + { + disallowRedundantWrapping: true + } + ], + parserOptions: { + ecmaVersion: 2021 + }, + errors: [ + { + messageId: "unexpectedRedundantRegExpWithFlags", + type: "NewExpression", + line: 1, + column: 1, + suggestions: null + } + ] + }, + { + code: "(a)\nnew RegExp(/b/);", + options: [{ + disallowRedundantWrapping: true + }], + errors: [ + { + messageId: "unexpectedRedundantRegExp", + type: "NewExpression", + line: 2, + column: 1, + suggestions: null + } + ] + }, + { + code: "(a)\nnew RegExp(/b/, 'g');", + options: [{ + disallowRedundantWrapping: true + }], + errors: [ + { + messageId: "unexpectedRedundantRegExpWithFlags", + type: "NewExpression", + line: 2, + column: 1, + suggestions: null + } + ] + }, + { + code: "a/RegExp(/foo/);", + options: [{ + disallowRedundantWrapping: true + }], + errors: [ + { + messageId: "unexpectedRedundantRegExp", + type: "CallExpression", + line: 1, + column: 3, + suggestions: [ + { + messageId: "replaceWithLiteral", + output: "a/ /foo/;" + } + ] + } + ] + }, + { + code: "RegExp(/foo/)in a;", + options: [{ + disallowRedundantWrapping: true + }], + errors: [ + { + messageId: "unexpectedRedundantRegExp", + type: "CallExpression", + line: 1, + column: 1, + suggestions: [ + { + messageId: "replaceWithLiteral", + output: "/foo/ in a;" + } + ] + } + ] + }, { code: "new RegExp((String?.raw)`a`);", errors: [