no-invalid-regexp.js 4.07 KB
/**
 * @fileoverview Validate strings passed to the RegExp constructor
 * @author Michael Ficarra
 */
"use strict";

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const RegExpValidator = require("regexpp").RegExpValidator;
const validator = new RegExpValidator({ ecmaVersion: 2018 });
const validFlags = /[gimuys]/gu;
const undefined1 = void 0;

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

module.exports = {
    meta: {
        type: "problem",

        docs: {
            description: "disallow invalid regular expression strings in `RegExp` constructors",
            category: "Possible Errors",
            recommended: true,
            url: "https://eslint.org/docs/rules/no-invalid-regexp"
        },

        schema: [{
            type: "object",
            properties: {
                allowConstructorFlags: {
                    type: "array",
                    items: {
                        type: "string"
                    }
                }
            },
            additionalProperties: false
        }],

        messages: {
            regexMessage: "{{message}}."
        }
    },

    create(context) {

        const options = context.options[0];
        let allowedFlags = null;

        if (options && options.allowConstructorFlags) {
            const temp = options.allowConstructorFlags.join("").replace(validFlags, "");

            if (temp) {
                allowedFlags = new RegExp(`[${temp}]`, "giu");
            }
        }

        /**
         * Check if node is a string
         * @param {ASTNode} node node to evaluate
         * @returns {boolean} True if its a string
         * @private
         */
        function isString(node) {
            return node && node.type === "Literal" && typeof node.value === "string";
        }

        /**
         * Check syntax error in a given pattern.
         * @param {string} pattern The RegExp pattern to validate.
         * @param {boolean} uFlag The Unicode flag.
         * @returns {string|null} The syntax error.
         */
        function validateRegExpPattern(pattern, uFlag) {
            try {
                validator.validatePattern(pattern, undefined1, undefined1, uFlag);
                return null;
            } catch (err) {
                return err.message;
            }
        }

        /**
         * Check syntax error in a given flags.
         * @param {string} flags The RegExp flags to validate.
         * @returns {string|null} The syntax error.
         */
        function validateRegExpFlags(flags) {
            try {
                validator.validateFlags(flags);
                return null;
            } catch {
                return `Invalid flags supplied to RegExp constructor '${flags}'`;
            }
        }

        return {
            "CallExpression, NewExpression"(node) {
                if (node.callee.type !== "Identifier" || node.callee.name !== "RegExp" || !isString(node.arguments[0])) {
                    return;
                }
                const pattern = node.arguments[0].value;
                let flags = isString(node.arguments[1]) ? node.arguments[1].value : "";

                if (allowedFlags) {
                    flags = flags.replace(allowedFlags, "");
                }

                // If flags are unknown, check both are errored or not.
                const message = validateRegExpFlags(flags) || (
                    flags
                        ? validateRegExpPattern(pattern, flags.indexOf("u") !== -1)
                        : validateRegExpPattern(pattern, true) && validateRegExpPattern(pattern, false)
                );

                if (message) {
                    context.report({
                        node,
                        messageId: "regexMessage",
                        data: { message }
                    });
                }
            }
        };
    }
};