/**
 * @fileoverview Rule to flag use of implied eval via setTimeout and setInterval
 * @author James Allardice
 */

"use strict";

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

const astUtils = require("./utils/ast-utils");
const { getStaticValue } = require("@eslint-community/eslint-utils");

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

/** @type {import('../shared/types').Rule} */
module.exports = {
    meta: {
        type: "suggestion",

        docs: {
            description: "Disallow the use of `eval()`-like methods",
            recommended: false,
            url: "https://eslint.org/docs/rules/no-implied-eval"
        },

        schema: [],

        messages: {
            impliedEval: "Implied eval. Consider passing a function instead of a string."
        }
    },

    create(context) {
        const GLOBAL_CANDIDATES = Object.freeze(["global", "window", "globalThis"]);
        const EVAL_LIKE_FUNC_PATTERN = /^(?:set(?:Interval|Timeout)|execScript)$/u;
        const sourceCode = context.getSourceCode();

        /**
         * Checks whether a node is evaluated as a string or not.
         * @param {ASTNode} node A node to check.
         * @returns {boolean} True if the node is evaluated as a string.
         */
        function isEvaluatedString(node) {
            if (
                (node.type === "Literal" && typeof node.value === "string") ||
                node.type === "TemplateLiteral"
            ) {
                return true;
            }
            if (node.type === "BinaryExpression" && node.operator === "+") {
                return isEvaluatedString(node.left) || isEvaluatedString(node.right);
            }
            return false;
        }

        /**
         * Reports if the `CallExpression` node has evaluated argument.
         * @param {ASTNode} node A CallExpression to check.
         * @returns {void}
         */
        function reportImpliedEvalCallExpression(node) {
            const [firstArgument] = node.arguments;

            if (firstArgument) {

                const staticValue = getStaticValue(firstArgument, sourceCode.getScope(node));
                const isStaticString = staticValue && typeof staticValue.value === "string";
                const isString = isStaticString || isEvaluatedString(firstArgument);

                if (isString) {
                    context.report({
                        node,
                        messageId: "impliedEval"
                    });
                }
            }

        }

        /**
         * Reports calls of `implied eval` via the global references.
         * @param {Variable} globalVar A global variable to check.
         * @returns {void}
         */
        function reportImpliedEvalViaGlobal(globalVar) {
            const { references, name } = globalVar;

            references.forEach(ref => {
                const identifier = ref.identifier;
                let node = identifier.parent;

                while (astUtils.isSpecificMemberAccess(node, null, name)) {
                    node = node.parent;
                }

                if (astUtils.isSpecificMemberAccess(node, null, EVAL_LIKE_FUNC_PATTERN)) {
                    const calleeNode = node.parent.type === "ChainExpression" ? node.parent : node;
                    const parent = calleeNode.parent;

                    if (parent.type === "CallExpression" && parent.callee === calleeNode) {
                        reportImpliedEvalCallExpression(parent);
                    }
                }
            });
        }

        //--------------------------------------------------------------------------
        // Public
        //--------------------------------------------------------------------------

        return {
            CallExpression(node) {
                if (astUtils.isSpecificId(node.callee, EVAL_LIKE_FUNC_PATTERN)) {
                    reportImpliedEvalCallExpression(node);
                }
            },
            "Program:exit"(node) {
                const globalScope = sourceCode.getScope(node);

                GLOBAL_CANDIDATES
                    .map(candidate => astUtils.getVariableByName(globalScope, candidate))
                    .filter(globalVar => !!globalVar && globalVar.defs.length === 0)
                    .forEach(reportImpliedEvalViaGlobal);
            }
        };

    }
};
