]> git.proxmox.com Git - pve-eslint.git/blobdiff - eslint/lib/rules/prefer-regex-literals.js
import 8.41.0 source
[pve-eslint.git] / eslint / lib / rules / prefer-regex-literals.js
index f30eddbf8c503a9c91cd6e8965ddaf88930aedeb..39e29506421c2b0ca67b6eb75b55fec1a91b2cdd 100644 (file)
 //------------------------------------------------------------------------------
 
 const astUtils = require("./utils/ast-utils");
-const { CALL, CONSTRUCT, ReferenceTracker, findVariable } = require("eslint-utils");
-const { RegExpValidator, visitRegExpAST, RegExpParser } = require("regexpp");
+const { CALL, CONSTRUCT, ReferenceTracker, findVariable } = require("@eslint-community/eslint-utils");
+const { RegExpValidator, visitRegExpAST, RegExpParser } = require("@eslint-community/regexpp");
 const { canTokensBeAdjacent } = require("./utils/ast-utils");
+const { REGEXPP_LATEST_ECMA_VERSION } = require("./utils/regular-expressions");
 
 //------------------------------------------------------------------------------
 // Helpers
 //------------------------------------------------------------------------------
 
-const REGEXPP_LATEST_ECMA_VERSION = 2022;
-
 /**
  * Determines whether the given node is a string literal.
  * @param {ASTNode} node Node to check.
@@ -125,7 +124,7 @@ module.exports = {
         docs: {
             description: "Disallow use of the `RegExp` constructor in favor of regular expression literals",
             recommended: false,
-            url: "https://eslint.org/docs/rules/prefer-regex-literals"
+            url: "https://eslint.org/docs/latest/rules/prefer-regex-literals"
         },
 
         hasSuggestions: true,
@@ -146,6 +145,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."
         }
@@ -153,7 +154,7 @@ module.exports = {
 
     create(context) {
         const [{ disallowRedundantWrapping = false } = {}] = context.options;
-        const sourceCode = context.getSourceCode();
+        const sourceCode = context.sourceCode;
 
         /**
          * Determines whether the given identifier node is a reference to a global variable.
@@ -161,7 +162,7 @@ module.exports = {
          * @returns {boolean} True if the identifier is a reference to a global variable.
          */
         function isGlobalReference(node) {
-            const scope = context.getScope();
+            const scope = sourceCode.getScope(node);
             const variable = findVariable(scope, node);
 
             return variable !== null && variable.scope.type === "global" && variable.defs.length === 0;
@@ -248,16 +249,18 @@ module.exports = {
 
         /**
          * Returns a ecmaVersion compatible for regexpp.
-         * @param {any} ecmaVersion The ecmaVersion to convert.
+         * @param {number} ecmaVersion The ecmaVersion to convert.
          * @returns {import("regexpp/ecma-versions").EcmaVersion} The resulting ecmaVersion compatible for regexpp.
          */
         function getRegexppEcmaVersion(ecmaVersion) {
-            if (typeof ecmaVersion !== "number" || ecmaVersion <= 5) {
+            if (ecmaVersion <= 5) {
                 return 5;
             }
-            return Math.min(ecmaVersion + 2009, REGEXPP_LATEST_ECMA_VERSION);
+            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,9 +296,86 @@ 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();
+            Program(node) {
+                const scope = sourceCode.getScope(node);
                 const tracker = new ReferenceTracker(scope);
                 const traceMap = {
                     RegExp: {
@@ -304,45 +384,86 @@ module.exports = {
                     }
                 };
 
-                for (const { node } of tracker.iterateGlobalReferences(traceMap)) {
-                    if (disallowRedundantWrapping && isUnnecessarilyWrappedRegexLiteral(node)) {
-                        if (node.arguments.length === 2) {
-                            context.report({ node, messageId: "unexpectedRedundantRegExpWithFlags" });
-                        } else {
-                            context.report({ node, messageId: "unexpectedRedundantRegExp" });
-                        }
-                    } else if (hasOnlyStaticStringArguments(node)) {
-                        let regexContent = getStringValue(node.arguments[0]);
-                        let noFix = false;
-                        let flags;
+                for (const { node: refNode } of tracker.iterateGlobalReferences(traceMap)) {
+                    if (disallowRedundantWrapping && isUnnecessarilyWrappedRegexLiteral(refNode)) {
+                        const regexNode = refNode.arguments[0];
 
-                        if (node.arguments[1]) {
-                            flags = getStringValue(node.arguments[1]);
-                        }
+                        if (refNode.arguments.length === 2) {
+                            const suggests = [];
 
-                        const regexppEcmaVersion = getRegexppEcmaVersion(context.parserOptions.ecmaVersion);
-                        const RegExpValidatorInstance = new RegExpValidator({ ecmaVersion: regexppEcmaVersion });
+                            const argFlags = getStringValue(refNode.arguments[1]) || "";
 
-                        try {
-                            RegExpValidatorInstance.validatePattern(regexContent, 0, regexContent.length, flags ? flags.includes("u") : false);
-                            if (flags) {
-                                RegExpValidatorInstance.validateFlags(flags);
+                            if (canFixTo(refNode, regexNode.regex.pattern, argFlags)) {
+                                suggests.push({
+                                    messageId: "replaceWithLiteralAndFlags",
+                                    pattern: regexNode.regex.pattern,
+                                    flags: argFlags
+                                });
                             }
-                        } catch {
-                            noFix = true;
-                        }
 
-                        const tokenBefore = sourceCode.getTokenBefore(node);
+                            const literalFlags = regexNode.regex.flags || "";
+                            const mergedFlags = mergeRegexFlags(literalFlags, argFlags);
+
+                            if (
+                                !areFlagsEqual(mergedFlags, argFlags) &&
+                                canFixTo(refNode, regexNode.regex.pattern, mergedFlags)
+                            ) {
+                                suggests.push({
+                                    messageId: "replaceWithIntendedLiteralAndFlags",
+                                    pattern: regexNode.regex.pattern,
+                                    flags: mergedFlags
+                                });
+                            }
 
-                        if (tokenBefore && !validPrecedingTokens.has(tokenBefore.value)) {
-                            noFix = true;
+                            context.report({
+                                node: refNode,
+                                messageId: "unexpectedRedundantRegExpWithFlags",
+                                suggest: suggests.map(({ flags, pattern, messageId }) => ({
+                                    messageId,
+                                    data: {
+                                        flags
+                                    },
+                                    fix(fixer) {
+                                        return fixer.replaceText(refNode, getSafeOutput(refNode, `/${pattern}/${flags}`));
+                                    }
+                                }))
+                            });
+                        } else {
+                            const outputs = [];
+
+                            if (canFixTo(refNode, regexNode.regex.pattern, regexNode.regex.flags)) {
+                                outputs.push(sourceCode.getText(regexNode));
+                            }
+
+
+                            context.report({
+                                node: refNode,
+                                messageId: "unexpectedRedundantRegExp",
+                                suggest: outputs.map(output => ({
+                                    messageId: "replaceWithLiteral",
+                                    fix(fixer) {
+                                        return fixer.replaceText(
+                                            refNode,
+                                            getSafeOutput(refNode, output)
+                                        );
+                                    }
+                                }))
+                            });
                         }
+                    } else if (hasOnlyStaticStringArguments(refNode)) {
+                        let regexContent = getStringValue(refNode.arguments[0]);
+                        let noFix = false;
+                        let flags;
 
-                        if (!/^[-a-zA-Z0-9\\[\](){} \t\r\n\v\f!@#$%^&*+^_=/~`.><?,'"|:;]*$/u.test(regexContent)) {
+                        if (refNode.arguments[1]) {
+                            flags = getStringValue(refNode.arguments[1]);
+                        }
+
+                        if (!canFixTo(refNode, regexContent, flags)) {
                             noFix = true;
                         }
 
-                        if (sourceCode.getCommentsInside(node).length > 0) {
+                        if (!/^[-a-zA-Z0-9\\[\](){} \t\r\n\v\f!@#$%^&*+^_=/~`.><?,'"|:;]*$/u.test(regexContent)) {
                             noFix = true;
                         }
 
@@ -372,19 +493,12 @@ module.exports = {
                         const newRegExpValue = `/${regexContent || "(?:)"}/${flags || ""}`;
 
                         context.report({
-                            node,
+                            node: refNode,
                             messageId: "unexpectedRegExp",
                             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(refNode, getSafeOutput(refNode, newRegExpValue));
                                 }
                             }]
                         });