import { injectable } from 'inversify'; import * as estraverse from '@javascript-obfuscator/estraverse'; import * as ESTree from 'estree'; import { TObjectMembersCallsChain } from '../../../types/analyzers/calls-graph-analyzer/TObjectMembersCallsChain'; import { ICalleeData } from '../../../interfaces/analyzers/calls-graph-analyzer/ICalleeData'; import { AbstractCalleeDataExtractor } from './AbstractCalleeDataExtractor'; import { NodeGuards } from '../../../node/NodeGuards'; import { NodeStatementUtils } from '../../../node/NodeStatementUtils'; @injectable() export class ObjectExpressionCalleeDataExtractor extends AbstractCalleeDataExtractor { /** * @param {Property} propertyNode * @param {string | number} nextItemInCallsChain * @returns {boolean} */ private static isValidTargetPropertyNode (propertyNode: ESTree.Property, nextItemInCallsChain: string | number): boolean { if (!propertyNode.key) { return false; } const isTargetPropertyNodeWithIdentifierKey: boolean = NodeGuards.isIdentifierNode(propertyNode.key) && propertyNode.key.name === nextItemInCallsChain; const isTargetPropertyNodeWithLiteralKey: boolean = NodeGuards.isLiteralNode(propertyNode.key) && Boolean(propertyNode.key.value) && propertyNode.key.value === nextItemInCallsChain; return isTargetPropertyNodeWithIdentifierKey || isTargetPropertyNodeWithLiteralKey; } /** * @param {NodeGuards[]} blockScopeBody * @param {MemberExpression} callee * @returns {ICalleeData} */ public extract (blockScopeBody: ESTree.Node[], callee: ESTree.MemberExpression): ICalleeData | null { if (!NodeGuards.isMemberExpressionNode(callee)) { return null; } const objectMembersCallsChain: TObjectMembersCallsChain = this.createObjectMembersCallsChain([], callee); if (!objectMembersCallsChain.length) { return null; } const functionExpressionName: string | number | null = objectMembersCallsChain[objectMembersCallsChain.length - 1]; const calleeBlockStatement: ESTree.BlockStatement | null = this.getCalleeBlockStatement( NodeStatementUtils.getParentNodeWithStatements(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 {TObjectMembersCallsChain} currentChain * @param {MemberExpression} memberExpression * @returns {TObjectMembersCallsChain} */ private createObjectMembersCallsChain ( currentChain: TObjectMembersCallsChain, memberExpression: ESTree.MemberExpression ): TObjectMembersCallsChain { // first step: processing memberExpression `property` property if (NodeGuards.isIdentifierNode(memberExpression.property) && !memberExpression.computed) { currentChain.unshift(memberExpression.property.name); } else if ( NodeGuards.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 (NodeGuards.isMemberExpressionNode(memberExpression.object)) { return this.createObjectMembersCallsChain(currentChain, memberExpression.object); } else if (NodeGuards.isIdentifierNode(memberExpression.object)) { currentChain.unshift(memberExpression.object.name); } return currentChain; } /** * @param {NodeGuards} targetNode * @param {TObjectMembersCallsChain} objectMembersCallsChain * @returns {BlockStatement} */ private getCalleeBlockStatement ( targetNode: ESTree.Node, objectMembersCallsChain: TObjectMembersCallsChain ): ESTree.BlockStatement | null { const objectName: string | number | undefined = objectMembersCallsChain.shift(); if (!objectName) { return null; } let calleeBlockStatement: ESTree.BlockStatement | null = null; estraverse.traverse(targetNode, { enter: (node: ESTree.Node): estraverse.VisitorOption | void => { if ( NodeGuards.isVariableDeclaratorNode(node) && NodeGuards.isIdentifierNode(node.id) && node.init && NodeGuards.isObjectExpressionNode(node.init) && node.id.name === objectName ) { calleeBlockStatement = this.findCalleeBlockStatement(node.init.properties, objectMembersCallsChain); return estraverse.VisitorOption.Break; } } }); return calleeBlockStatement; } /** * @param {Property[]} objectExpressionProperties * @param {TObjectMembersCallsChain} objectMembersCallsChain * @returns {BlockStatement} */ private findCalleeBlockStatement ( objectExpressionProperties: (ESTree.Property | ESTree.SpreadElement)[], objectMembersCallsChain: TObjectMembersCallsChain ): ESTree.BlockStatement | null { const nextItemInCallsChain: string | number | undefined = objectMembersCallsChain.shift(); if (!nextItemInCallsChain) { return null; } for (const propertyNode of objectExpressionProperties) { if (!NodeGuards.isPropertyNode(propertyNode)) { continue; } if (!ObjectExpressionCalleeDataExtractor.isValidTargetPropertyNode(propertyNode, nextItemInCallsChain)) { continue; } if (NodeGuards.isObjectExpressionNode(propertyNode.value)) { return this.findCalleeBlockStatement(propertyNode.value.properties, objectMembersCallsChain); } if (NodeGuards.isFunctionExpressionNode(propertyNode.value)) { return propertyNode.value.body; } } return null; } }