/**
 * @fileoverview Rule to flag use of eval() statement
 * @author Nicholas C. Zakas
 * @copyright 2015 Toru Nagashima. All rights reserved.
 * @copyright 2015 Mathias Schreck. All rights reserved.
 * @copyright 2013 Nicholas C. Zakas. All rights reserved.
 */

"use strict";

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

var astUtils = require("../ast-utils");

//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------

var candidatesOfGlobalObject = Object.freeze([
    "global",
    "window"
]);

/**
 * Checks a given node is a Identifier node of the specified name.
 *
 * @param {ASTNode} node - A node to check.
 * @param {string} name - A name to check.
 * @returns {boolean} `true` if the node is a Identifier node of the name.
 */
function isIdentifier(node, name) {
    return node.type === "Identifier" && node.name === name;
}

/**
 * Checks a given node is a Literal node of the specified string value.
 *
 * @param {ASTNode} node - A node to check.
 * @param {string} name - A name to check.
 * @returns {boolean} `true` if the node is a Literal node of the name.
 */
function isConstant(node, name) {
    switch (node.type) {
        case "Literal":
            return node.value === name;

        case "TemplateLiteral":
            return (
                node.expressions.length === 0 &&
                node.quasis[0].value.cooked === name
            );

        default:
            return false;
    }
}

/**
 * Checks a given node is a MemberExpression node which has the specified name's
 * property.
 *
 * @param {ASTNode} node - A node to check.
 * @param {string} name - A name to check.
 * @returns {boolean} `true` if the node is a MemberExpression node which has
 *      the specified name's property
 */
function isMember(node, name) {
    return (
        node.type === "MemberExpression" &&
        (node.computed ? isConstant : isIdentifier)(node.property, name)
    );
}

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

module.exports = function(context) {
    var allowIndirect = Boolean(
        context.options[0] &&
        context.options[0].allowIndirect
    );
    var sourceCode = context.getSourceCode();
    var funcInfo = null;

    /**
     * Pushs a variable scope (Program or Function) information to the stack.
     *
     * This is used in order to check whether or not `this` binding is a
     * reference to the global object.
     *
     * @param {ASTNode} node - A node of the scope. This is one of Program,
     *      FunctionDeclaration, FunctionExpression, and ArrowFunctionExpression.
     * @returns {void}
     */
    function enterVarScope(node) {
        var strict = context.getScope().isStrict;

        funcInfo = {
            upper: funcInfo,
            node: node,
            strict: strict,
            defaultThis: false,
            initialized: strict
        };
    }

    /**
     * Pops a variable scope from the stack.
     *
     * @returns {void}
     */
    function exitVarScope() {
        funcInfo = funcInfo.upper;
    }

    /**
     * Reports a given node.
     *
     * `node` is `Identifier` or `MemberExpression`.
     * The parent of `node` might be `CallExpression`.
     *
     * The location of the report is always `eval` `Identifier` (or possibly
     * `Literal`). The type of the report is `CallExpression` if the parent is
     * `CallExpression`. Otherwise, it's the given node type.
     *
     * @param {ASTNode} node - A node to report.
     * @returns {void}
     */
    function report(node) {
        var locationNode = node;
        var parent = node.parent;

        if (node.type === "MemberExpression") {
            locationNode = node.property;
        }
        if (parent.type === "CallExpression" && parent.callee === node) {
            node = parent;
        }

        context.report({
            node: node,
            loc: locationNode.loc.start,
            message: "eval can be harmful."
        });
    }

    /**
     * Reports accesses of `eval` via the global object.
     *
     * @param {escope.Scope} globalScope - The global scope.
     * @returns {void}
     */
    function reportAccessingEvalViaGlobalObject(globalScope) {
        for (var i = 0; i < candidatesOfGlobalObject.length; ++i) {
            var name = candidatesOfGlobalObject[i];
            var variable = astUtils.getVariableByName(globalScope, name);
            if (!variable) {
                continue;
            }

            var references = variable.references;
            for (var j = 0; j < references.length; ++j) {
                var identifier = references[j].identifier;
                var node = identifier.parent;

                // To detect code like `window.window.eval`.
                while (isMember(node, name)) {
                    node = node.parent;
                }

                // Reports.
                if (isMember(node, "eval")) {
                    report(node);
                }
            }
        }
    }

    /**
     * Reports all accesses of `eval` (excludes direct calls to eval).
     *
     * @param {escope.Scope} globalScope - The global scope.
     * @returns {void}
     */
    function reportAccessingEval(globalScope) {
        var variable = astUtils.getVariableByName(globalScope, "eval");
        if (!variable) {
            return;
        }

        var references = variable.references;
        for (var i = 0; i < references.length; ++i) {
            var reference = references[i];
            var id = reference.identifier;

            if (id.name === "eval" && !astUtils.isCallee(id)) {
                // Is accessing to eval (excludes direct calls to eval)
                report(id);
            }
        }
    }

    if (allowIndirect) {
        // Checks only direct calls to eval.
        // It's simple!
        return {
            "CallExpression:exit": function(node) {
                var callee = node.callee;
                if (isIdentifier(callee, "eval")) {
                    report(callee);
                }
            }
        };
    }

    return {
        "CallExpression:exit": function(node) {
            var callee = node.callee;
            if (isIdentifier(callee, "eval")) {
                report(callee);
            }
        },

        "Program": function(node) {
            var scope = context.getScope(),
                features = context.parserOptions.ecmaFeatures || {},
                strict =
                    scope.isStrict ||
                    node.sourceType === "module" ||
                    (features.globalReturn && scope.childScopes[0].isStrict);

            funcInfo = {
                upper: null,
                node: node,
                strict: strict,
                defaultThis: true,
                initialized: true
            };
        },

        "Program:exit": function() {
            var globalScope = context.getScope();

            exitVarScope();
            reportAccessingEval(globalScope);
            reportAccessingEvalViaGlobalObject(globalScope);
        },

        "FunctionDeclaration": enterVarScope,
        "FunctionDeclaration:exit": exitVarScope,
        "FunctionExpression": enterVarScope,
        "FunctionExpression:exit": exitVarScope,
        "ArrowFunctionExpression": enterVarScope,
        "ArrowFunctionExpression:exit": exitVarScope,

        "ThisExpression": function(node) {
            if (!isMember(node.parent, "eval")) {
                return;
            }

            // `this.eval` is found.
            // Checks whether or not the value of `this` is the global object.
            if (!funcInfo.initialized) {
                funcInfo.initialized = true;
                funcInfo.defaultThis = astUtils.isDefaultThisBinding(
                    funcInfo.node,
                    sourceCode
                );
            }
            if (!funcInfo.strict && funcInfo.defaultThis) {
                // `this.eval` is possible built-in `eval`.
                report(node.parent);
            }
        }
    };

};

module.exports.schema = [
    {
        "type": "object",
        "properties": {
            "allowIndirect": {"type": "boolean"}
        },
        "additionalProperties": false
    }
];
