All files / src/stack-trace-analyzer/callee-data-extractors ObjectExpressionCalleeDataExtractor.ts

94% Statements 47/50
88.37% Branches 38/43
100% Functions 2/2
93.88% Lines 46/49
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 1791x               1x 1x   1x                               15606x 15606x             15606x 15606x   15606x 8889x         8889x       8889x 8889x           15606x 14093x     1513x                                       8895x 6869x 2026x             2025x   1x       8894x 6x 8888x 8888x     8888x                       8889x   8889x       8889x   8889x   855204x             2015x   2015x         8889x                       2020x   2020x       6542x   6542x   6542x       6542x 5024x     1518x 5x     1513x 1513x       502x      
import * as estraverse from 'estraverse';
import * as ESTree from 'estree';
 
import { TObjectMembersCallsChain } from '../../types/TObjectMembersCallsChain';
 
import { ICalleeData } from '../../interfaces/stack-trace-analyzer/ICalleeData';
import { ICalleeDataExtractor } from '../../interfaces/stack-trace-analyzer/ICalleeDataExtractor';
 
import { Node } from '../../node/Node';
import { NodeUtils } from '../../node/NodeUtils';
 
export class ObjectExpressionCalleeDataExtractor implements ICalleeDataExtractor {
    /**
     * @type {ESTree.Node[]}
     */
    private blockScopeBody: ESTree.Node[];
 
    /**
     * @type {ESTree.MemberExpression}
     */
    private callee: ESTree.MemberExpression;
 
    /**
     * @param blockScopeBody
     * @param callee
     */
    constructor (blockScopeBody: ESTree.Node[], callee: ESTree.MemberExpression) {
        this.blockScopeBody = blockScopeBody;
        this.callee = callee;
    }
 
    /**
     * @returns {ICalleeData|null}
     */
    public extract (): ICalleeData|null {
        let calleeBlockStatement: ESTree.BlockStatement|null = null,
            functionExpressionName: string|number|null = null;
 
        if (Node.isMemberExpressionNode(this.callee)) {
            const objectMembersCallsChain: TObjectMembersCallsChain = this.createObjectMembersCallsChain(
                [],
                this.callee
            );
 
            Iif (!objectMembersCallsChain.length) {
                return null;
            }
 
            functionExpressionName = objectMembersCallsChain[objectMembersCallsChain.length - 1];
            calleeBlockStatement = this.getCalleeBlockStatement(
                NodeUtils.getBlockScopeOfNode(this.blockScopeBody[0]),
                objectMembersCallsChain
            );
        }
 
        if (!calleeBlockStatement) {
            return null;
        }
 
        return {
            callee: calleeBlockStatement,
            name: functionExpressionName
        };
    }
 
    /**
     * Creates array with MemberExpression calls chain.
     *
     * Example: object.foo.bar(); // ['object', 'foo', 'bar']
     *
     * @param currentChain
     * @param memberExpression
     * @returns {TObjectMembersCallsChain}
     */
    private createObjectMembersCallsChain (
        currentChain: TObjectMembersCallsChain,
        memberExpression: ESTree.MemberExpression
    ): TObjectMembersCallsChain {
        // first step: processing memberExpression `property` property
        if (Node.isIdentifierNode(memberExpression.property) && memberExpression.computed === false) {
            currentChain.unshift(memberExpression.property.name);
        } else if (
            Node.isLiteralNode(memberExpression.property) &&
            (
                typeof memberExpression.property.value === 'string' ||
                typeof memberExpression.property.value === 'number'
            )
        ) {
            currentChain.unshift(memberExpression.property.value);
        } else {
            return currentChain;
        }
 
        // second step: processing memberExpression `object` property
        if (Node.isMemberExpressionNode(memberExpression.object)) {
            return this.createObjectMembersCallsChain(currentChain, memberExpression.object);
        } else Eif (Node.isIdentifierNode(memberExpression.object)) {
            currentChain.unshift(memberExpression.object.name);
        }
 
        return currentChain;
    }
 
    /**
     * @param node
     * @param objectMembersCallsChain
     * @returns {ESTree.BlockStatement|null}
     */
    private getCalleeBlockStatement (
        node: ESTree.Node,
        objectMembersCallsChain: TObjectMembersCallsChain
    ): ESTree.BlockStatement|null {
        const objectName: string|number|undefined = objectMembersCallsChain.shift();
 
        Iif (!objectName) {
            return null;
        }
 
        let calleeBlockStatement: ESTree.BlockStatement|null = null;
 
        estraverse.traverse(node, {
            enter: (node: ESTree.Node, parentNode: ESTree.Node): any => {
                if (
                    Node.isVariableDeclaratorNode(node) &&
                    Node.isIdentifierNode(node.id) &&
                    node.init &&
                    Node.isObjectExpressionNode(node.init) &&
                    node.id.name === objectName
                ) {
                    calleeBlockStatement = this.findCalleeBlockStatement(node.init.properties, objectMembersCallsChain);
 
                    return estraverse.VisitorOption.Break;
                }
            }
        });
 
        return calleeBlockStatement;
    }
 
    /**
     * @param objectExpressionProperties
     * @param objectMembersCallsChain
     * @returns {ESTree.BlockStatement|null}
     */
    private findCalleeBlockStatement (
        objectExpressionProperties: ESTree.Property[],
        objectMembersCallsChain: TObjectMembersCallsChain
    ): ESTree.BlockStatement|null {
        const nextItemInCallsChain: string|number|undefined = objectMembersCallsChain.shift();
 
        Iif (!nextItemInCallsChain) {
            return null;
        }
 
        for (const propertyNode of objectExpressionProperties) {
            const isTargetPropertyNodeWithIdentifierKey: boolean =
                Node.isIdentifierNode(propertyNode.key) && propertyNode.key.name === nextItemInCallsChain;
            const isTargetPropertyNodeWithLiteralKey: boolean =
                Node.isLiteralNode(propertyNode.key) &&
                Boolean(propertyNode.key.value) &&
                propertyNode.key.value === nextItemInCallsChain;
 
            if (!isTargetPropertyNodeWithIdentifierKey && !isTargetPropertyNodeWithLiteralKey) {
                continue;
            }
 
            if (Node.isObjectExpressionNode(propertyNode.value)) {
                return this.findCalleeBlockStatement(propertyNode.value.properties, objectMembersCallsChain);
            }
 
            Eif (Node.isFunctionExpressionNode(propertyNode.value)) {
                return propertyNode.value.body;
            }
        }
 
        return null;
    }
}