Object.defineProperty(exports, "__esModule", { value: true });
const babelPluginHelpers_1 = require("../babelPluginHelpers");
const config_1 = require("../config");
const FunctionNames_1 = require("../FunctionNames");
const OperationLog_1 = require("../helperFunctions/OperationLog");
const logging_1 = require("../helperFunctions/logging");
const CallExpressionSpecialCases_1 = require("./CallExpressionSpecialCases");
const util_1 = require("../util");
const CallExpression = {
    argNames: ["function", "context", "arg", "evalFn"],
    argIsArray: [false, false, true, false],
    exec: (args, astArgs, ctx, logData) => {
        let [fnArg, context, argList, evalFn] = args;
        var fn = fnArg[0];
        var fnArgs = [];
        var fnArgValues = [];
        for (var i = 0; i < argList.length; i++) {
            const arg = argList[i];
            if (astArgs.spreadArgumentIndices &&
                astArgs.spreadArgumentIndices.includes(i)) {
                const argumentArray = arg[0];
                argumentArray.forEach(argument => {
                    fnArgValues.push(argument);
                    fnArgs.push(ctx.getEmptyTrackingInfo("spreadArgument", logData.loc));
                });
            }
            else {
                fnArgValues.push(arg[0]);
                fnArgs.push(arg[1]);
            }
        }
        var object = context[0];
        const functionIsCall = fn === Function.prototype.call;
        const functionIsApply = fn === Function.prototype.apply;
        const functionIsCallOrApply = functionIsCall || functionIsApply;
        // There are basically two sets of arguments:
        // 1) The args passed into callExpression.exec
        // 2) The args passed into .apply/argTrackingValues, and used for special case handlers
        let fnAtInvocation = functionIsCallOrApply ? context[0] : fn;
        let fnArgsAtInvocation = fnArgs;
        let fnArgValuesAtInvocation = fnArgValues;
        if (functionIsCall) {
            fnArgsAtInvocation = fnArgs.slice(1);
            fnArgValuesAtInvocation = fnArgValues.slice(1);
        }
        else if (functionIsApply) {
            ({
                fnArgsAtInvocation,
                fnArgValuesAtInvocation
            } = getInvocationArgsForApply(fnArgValues, fnArgsAtInvocation, fnArgValuesAtInvocation, ctx));
        }
        ctx.argTrackingInfo = fnArgsAtInvocation;
        const extraTrackingValues = {};
        const runtimeArgs = {};
        let extraState = {};
        const fnKnownValue = ctx.knownValues.getName(fnAtInvocation);
        var ret;
        let retT = null;
        let fnArgValuesForApply = fnArgValues;
        if (astArgs.isNewExpression) {
            ({ ret, retT } = handleNewExpression(getSpecialCaseArgs()));
        }
        else if (CallExpressionSpecialCases_1.specialCasesWhereWeDontCallTheOriginalFunction[fnKnownValue] &&
            (fnKnownValue !== "String.prototype.replace" ||
                ["string", "number"].includes(typeof fnArgValues[1]))) {
            [ret, retT] = CallExpressionSpecialCases_1.specialCasesWhereWeDontCallTheOriginalFunction[fnKnownValue](getSpecialCaseArgs());
        }
        else {
            if (config_1.VERIFY && fnKnownValue === "String.prototype.replace") {
                logging_1.consoleLog("unhandled string replace call");
            }
            const fnIsEval = fn === eval;
            if (fnIsEval) {
                if (ctx.hasInstrumentationFunction) {
                    if (evalFn) {
                        ctx.global["__fromJSEvalSetEvalFn"](evalFn[0]);
                    }
                    fn = ctx.global["__fromJSEval"];
                }
                else {
                    if (!ctx.global.__forTestsDontShowCantEvalLog) {
                        logging_1.consoleLog("Calling eval but can't instrument code");
                    }
                }
            }
            const knownFnProcessor = CallExpressionSpecialCases_1.knownFnProcessors[fnKnownValue];
            if (knownFnProcessor) {
                knownFnProcessor(getFnProcessorArgs());
            }
            const lastReturnStatementResultBeforeCall = ctx.lastReturnStatementResult && ctx.lastReturnStatementResult[1];
            ret = fn.apply(object, fnArgValuesForApply);
            ctx.argTrackingInfo = null;
            const lastReturnStatementResultAfterCall = ctx.lastReturnStatementResult && ctx.lastReturnStatementResult[1];
            // Don't pretend to have a tracked return value if an uninstrumented function was called
            // (not 100% reliable e.g. if the uninstrumented fn calls an instrumented fn)
            if (fnIsEval && ctx.hasInstrumentationFunction) {
                ctx.registerEvalScript(ret.evalScript);
                ret = ret.returnValue;
                retT = ctx.lastOpTrackingResultWithoutResetting;
            }
            else if (CallExpressionSpecialCases_1.specialValuesForPostprocessing[fnKnownValue]) {
                try {
                    retT = CallExpressionSpecialCases_1.specialValuesForPostprocessing[fnKnownValue](getSpecialCaseArgs());
                }
                catch (err) {
                    logging_1.consoleError("post procressing error", fnKnownValue, err);
                    debugger;
                }
            }
            else {
                if (ctx.lastOperationType === "returnStatement" &&
                    lastReturnStatementResultAfterCall !==
                        lastReturnStatementResultBeforeCall) {
                    retT =
                        ctx.lastReturnStatementResult && ctx.lastReturnStatementResult[1];
                }
            }
            if (functionIsCallOrApply && fnKnownValue) {
                let callOrApplyInvocationArgs = {};
                fnArgValuesAtInvocation.forEach((arg, i) => {
                    callOrApplyInvocationArgs["arg" + i] = [null, fnArgsAtInvocation[i]];
                });
                extraTrackingValues.call = [
                    null,
                    ctx.createOperationLog({
                        operation: ctx.operationTypes.callExpression,
                        args: callOrApplyInvocationArgs,
                        result: ret,
                        loc: logData.loc
                    })
                ];
            }
        }
        extraTrackingValues.returnValue = [ret, retT]; // pick up value from returnStatement
        if (util_1.countObjectKeys(runtimeArgs) > 0) {
            logData.runtimeArgs = runtimeArgs;
        }
        logData.extraArgs = extraTrackingValues;
        return ret;
        function getSpecialCaseArgs() {
            const specialCaseArgs = {
                fn,
                ctx,
                object,
                fnArgs: fnArgsAtInvocation,
                fnArgValues: fnArgValuesAtInvocation,
                args,
                extraTrackingValues,
                logData,
                context,
                ret,
                retT,
                extraState,
                runtimeArgs
            };
            if (functionIsCallOrApply) {
                specialCaseArgs.fn = object;
                specialCaseArgs.object = fnArgValues[0];
                specialCaseArgs.context = [fnArgValues[0], fnArgs[0]];
            }
            return specialCaseArgs;
        }
        function getFnProcessorArgs() {
            function setFnArgForApply(argIndex, argValue) {
                if (functionIsApply) {
                    const argList = fnArgValuesForApply[1].slice();
                    argList[argIndex] = argValue;
                }
                else if (functionIsCall) {
                    fnArgValuesForApply[argIndex + 1] = argValue;
                }
                else {
                    fnArgValuesForApply[argIndex] = argValue;
                }
            }
            function getFnArgForApply(argIndex) {
                if (functionIsApply) {
                    const argList = fnArgValuesForApply[1];
                    return argList[argIndex];
                }
                else if (functionIsCall) {
                    return fnArgValuesForApply[argIndex + 1];
                }
                else {
                    return fnArgValuesForApply[argIndex];
                }
            }
            function setContext(c) {
                context = c;
            }
            function setArgValuesForApply(vals) {
                fnArgValuesForApply = vals;
            }
            function setFunction(f) {
                fn = f;
            }
            const fnProcessorArgs = {
                extraState,
                setArgValuesForApply,
                fnArgValues,
                getFnArgForApply,
                setFnArgForApply,
                ctx,
                setContext,
                fnArgs,
                logData,
                object,
                setFunction
            };
            return fnProcessorArgs;
        }
    },
    traverse(operationLog, charIndex) {
        var knownFunction = operationLog.args.function &&
            operationLog.args.function.result.knownValue;
        if ((knownFunction === "Function.prototype.call" ||
            knownFunction === "Function.prototype.apply") &&
            operationLog.extraArgs.call) {
            const args = operationLog.extraArgs.call.args;
            args.function = operationLog.args.context;
            args.context = operationLog.args.arg0;
            return this.traverse(new OperationLog_1.default(Object.assign({}, operationLog, { args })), charIndex);
        }
        if (knownFunction) {
            return CallExpressionSpecialCases_1.traverseKnownFunction({ operationLog, charIndex, knownFunction });
        }
        else {
            return {
                operationLog: operationLog.extraArgs.returnValue,
                charIndex: charIndex
            };
        }
    },
    visitor(path, isNewExpression = false) {
        const { callee } = path.node;
        var isMemberExpressionCall = callee.type === "MemberExpression";
        const astArgs = {};
        var args = [];
        path.node.arguments.forEach((arg, i) => {
            if (arg.type === "SpreadElement") {
                if (!astArgs.spreadArgumentIndices) {
                    astArgs.spreadArgumentIndices = [];
                }
                astArgs.spreadArgumentIndices.push(babelPluginHelpers_1.ignoredNumericLiteral(i));
                arg = arg.argument;
            }
            args.push(babelPluginHelpers_1.ignoredArrayExpression([arg, babelPluginHelpers_1.getLastOperationTrackingResultCall()]));
        });
        let contextArg;
        let evalFn;
        const calleeType = path.node.callee.type;
        if (calleeType === "Super") {
            // Prevent syntax error: super' keyword unexpected here
            return;
        }
        if (calleeType === "Identifier") {
            const functionIdentifier = path.node.callee.name;
            if (functionIdentifier === "eval") {
                // Eval function that can be embedded in code, so that local variables are accessible in eval'd code
                const evalFnAst = this.babylon.parse(`sth = function(){return eval(arguments[0])}`).program.body[0].expression.right;
                evalFn = babelPluginHelpers_1.ignoredArrayExpression([
                    babelPluginHelpers_1.skipPath(evalFnAst),
                    this.t.nullLiteral()
                ]);
            }
        }
        if (isMemberExpressionCall) {
            contextArg = babelPluginHelpers_1.ignoredCallExpression(FunctionNames_1.getLastMemberExpressionObject, []);
        }
        else {
            contextArg = [
                babelPluginHelpers_1.ignoredIdentifier("undefined"),
                babelPluginHelpers_1.ignoreNode(this.t.nullLiteral())
            ];
        }
        const fn = [
            path.node.callee,
            isMemberExpressionCall
                ? babelPluginHelpers_1.getLastOperationTrackingResultCall()
                : babelPluginHelpers_1.getLastOperationTrackingResultCall()
        ];
        var fnArgs = [fn, contextArg, args];
        if (evalFn) {
            fnArgs.push(evalFn);
        }
        if (isNewExpression) {
            astArgs["isNewExpression"] = babelPluginHelpers_1.ignoreNode(this.t.booleanLiteral(true));
        }
        var call = this.createNode(fnArgs, astArgs, path.node.callee.loc);
        // todo: would it be better for perf if I updated existing call
        // instead of using replaceWith?
        return call;
    }
};
exports.default = CallExpression;
function getInvocationArgsForApply(fnArgValues, fnArgsAtInvocation, fnArgValuesAtInvocation, ctx) {
    const argArray = fnArgValues[1] || [];
    if (!("length" in argArray)) {
        // hmm can this even happen in a program that's not already broken?
        logging_1.consoleLog("can this even happen?");
        fnArgsAtInvocation = [];
    }
    else {
        fnArgsAtInvocation = [];
        fnArgValuesAtInvocation = [];
        for (let i = 0; i < argArray.length; i++) {
            fnArgValuesAtInvocation.push(argArray[i]);
            fnArgsAtInvocation.push(ctx.getObjectPropertyTrackingValue(argArray, i));
        }
    }
    return { fnArgsAtInvocation, fnArgValuesAtInvocation };
}
function handleNewExpression({ fn, ctx, fnArgValues, ret, retT, logData, args }) {
    const isNewFunctionCall = fn === Function;
    if (isNewFunctionCall && ctx.hasInstrumentationFunction) {
        let code = fnArgValues[fnArgValues.length - 1];
        let generatedFnArguments = fnArgValues.slice(0, -1);
        code =
            "(function(" + generatedFnArguments.join(",") + ") { " + code + " })";
        ret = ctx.global["__fromJSEval"](code);
        ctx.registerEvalScript(ret.evalScript);
        ret = ret.returnValue;
    }
    else {
        if (isNewFunctionCall) {
            logging_1.consoleLog("can't instrument new Function() code because instrumentation function is missing in context");
        }
        let thisValue = null; // overwritten inside new()
        ret = new (Function.prototype.bind.apply(fn, [
            thisValue,
            ...fnArgValues
        ]))();
    }
    const newArgs = {
        function: args[0]
    };
    // args[2] is the arg list
    args[2].forEach((arg, i) => {
        // not sure if this is needed...
        if (typeof arg[1] === "number") {
            newArgs["arg" + i] = arg;
        }
    });
    retT = ctx.createOperationLog({
        operation: ctx.operationTypes.newExpressionResult,
        args: newArgs,
        result: ret,
        loc: logData.loc
    });
    return { ret, retT };
}
//# sourceMappingURL=CallExpression.js.map