UNPKG

@rocket.chat/apps-engine

Version:

The engine code for the Rocket.Chat Apps which manages, runs, translates, coordinates and all of that.

240 lines (176 loc) 9.11 kB
// @deno-types="../../acorn.d.ts" import { AnyNode, AssignmentExpression, AwaitExpression, Expression, Function, Identifier, MethodDefinition, Property } from 'acorn'; // @deno-types="../../acorn-walk.d.ts" import { FullAncestorWalkerCallback } from 'acorn-walk'; export type WalkerState = { isModified: boolean; functionIdentifiers: Set<string>; }; export function getFunctionIdentifier(ancestors: AnyNode[], functionNodeIndex: number) { const parent = ancestors[functionNodeIndex - 1]; // If there is a parent node and it's not a computed property, we can try to // extract an identifier for our function from it. This needs to be done first // because when functions are assigned to named symbols, this will be the only // way to call it, even if the function itself has an identifier // Consider the following block: // // const foo = function bar() {} // // Even though the function itself has a name, the only way to call it in the // program is wiht `foo()` if (parent && !(parent as Property | MethodDefinition).computed) { // Several node types can have an id prop of type Identifier const { id } = parent as unknown as { id?: Identifier }; if (id?.type === 'Identifier') { return id.name; } // Usually assignments to object properties (MethodDefinition, Property) const { key } = parent as MethodDefinition | Property; if (key?.type === 'Identifier') { return key.name; } // Variable assignments have left hand side that can be used as Identifier const { left } = parent as AssignmentExpression; // Simple assignment: `const fn = () => {}` if (left?.type === 'Identifier') { return left.name; } // Object property assignment: `obj.fn = () => {}` if (left?.type === 'MemberExpression' && !left.computed) { return (left.property as Identifier).name; } } // nodeIndex needs to be the index of a Function node (either FunctionDeclaration or FunctionExpression) const currentNode = ancestors[functionNodeIndex] as Function; // Function declarations or expressions can be directly named if (currentNode.id?.type === 'Identifier') { return currentNode.id.name; } } export function wrapWithAwait(node: Expression) { if (!node.type.endsWith('Expression')) { throw new Error(`Can't wrap "${node.type}" with await`); } const innerNode: Expression = { ...node }; node.type = 'AwaitExpression'; // starting here node has become an AwaitExpression (node as AwaitExpression).argument = innerNode; Object.keys(node).forEach((key) => !['type', 'argument'].includes(key) && delete node[key as keyof AnyNode]); } export function asyncifyScope(ancestors: AnyNode[], state: WalkerState) { const functionNodeIndex = ancestors.findLastIndex((n) => 'async' in n); if (functionNodeIndex === -1) return; // At this point this is a node with an "async" property, so it has to be // of type Function - let TS know about that const functionScopeNode = ancestors[functionNodeIndex] as Function; if (functionScopeNode.async) { return; } functionScopeNode.async = true; // If the parent of a function node is a call expression, we're talking about an IIFE // Should we care about this case as well? // const parentNode = ancestors[functionScopeIndex-1]; // if (parentNode?.type === 'CallExpression' && ancestors[functionScopeIndex-2] && ancestors[functionScopeIndex-2].type !== 'AwaitExpression') { // pendingOperations.push(buildFunctionPredicate(getFunctionIdentifier(ancestors, functionScopeIndex-2))); // } const identifier = getFunctionIdentifier(ancestors, functionNodeIndex); // We can't fix calls of functions which name we can't determine at compile time if (!identifier) return; state.functionIdentifiers.add(identifier); } export function buildFixModifiedFunctionsOperation(functionIdentifiers: Set<string>): FullAncestorWalkerCallback<WalkerState> { return function _fixModifiedFunctionsOperation(node, state, ancestors) { if (node.type !== 'CallExpression') return; let isWrappable = false; // This node is a simple call to a function, like `fn()` isWrappable = node.callee.type === 'Identifier' && functionIdentifiers.has(node.callee.name); // This node is a call to an object property or instance method, like `obj.fn()`, but not computed like `obj[fn]()` isWrappable ||= node.callee.type === 'MemberExpression' && !node.callee.computed && node.callee.property?.type === 'Identifier' && functionIdentifiers.has(node.callee.property.name); // This is a weird dereferencing technique used by bundlers, and since we'll be dealing with bundled sources we have to check for it // e.g. `r=(0,fn)(e)` if (!isWrappable && node.callee.type === 'SequenceExpression') { const [, secondExpression] = node.callee.expressions; isWrappable = secondExpression?.type === 'Identifier' && functionIdentifiers.has(secondExpression.name); isWrappable ||= secondExpression?.type === 'MemberExpression' && !secondExpression.computed && secondExpression.property.type === 'Identifier' && functionIdentifiers.has(secondExpression.property.name); } if (!isWrappable) return; // ancestors[ancestors.length-1] === node, so here we're checking for parent node const parentNode = ancestors[ancestors.length - 2]; if (!parentNode || parentNode.type === 'AwaitExpression') return; wrapWithAwait(node); asyncifyScope(ancestors, state); state.isModified = true; }; } export const checkReassignmentOfModifiedIdentifiers: FullAncestorWalkerCallback<WalkerState> = (node, { functionIdentifiers }, _ancestors) => { if (node.type === 'AssignmentExpression') { if (node.operator !== '=') return; let identifier = ''; if (node.left.type === 'Identifier') identifier = node.left.name; if (node.left.type === 'MemberExpression' && !node.left.computed) { identifier = (node.left.property as Identifier).name; } if (!identifier || node.right.type !== 'Identifier' || !functionIdentifiers.has(node.right.name)) return; functionIdentifiers.add(identifier); return; } if (node.type === 'VariableDeclarator') { if (node.id.type !== 'Identifier' || functionIdentifiers.has(node.id.name)) return; if (node.init?.type !== 'Identifier' || !functionIdentifiers.has(node.init?.name)) return; functionIdentifiers.add(node.id.name); return; } // "Property" is for plain objects, "PropertyDefinition" is for classes // but both share the same structure if (node.type === 'Property' || node.type === 'PropertyDefinition') { if (node.key.type !== 'Identifier' || functionIdentifiers.has(node.key.name)) return; if (node.value?.type !== 'Identifier' || !functionIdentifiers.has(node.value.name)) return; functionIdentifiers.add(node.key.name); return; } }; export const fixLivechatIsOnlineCalls: FullAncestorWalkerCallback<WalkerState> = (node, state, ancestors) => { if (node.type !== 'MemberExpression' || node.computed) return; if ((node.property as Identifier).name !== 'isOnline') return; if (node.object.type !== 'CallExpression') return; if (node.object.callee.type !== 'MemberExpression') return; if ((node.object.callee.property as Identifier).name !== 'getLivechatReader') return; let parentIndex = ancestors.length - 2; let targetNode = ancestors[parentIndex]; if (targetNode.type !== 'CallExpression') { targetNode = node; } else { parentIndex--; } // If we're already wrapped with an await, nothing to do if (ancestors[parentIndex].type === 'AwaitExpression') return; // If we're in the middle of a chained member access, we can't wrap with await if (ancestors[parentIndex].type === 'MemberExpression') return; wrapWithAwait(targetNode); asyncifyScope(ancestors, state); state.isModified = true; }; export const fixRoomUsernamesCalls: FullAncestorWalkerCallback<WalkerState> = (node, state, ancestors) => { if (node.type !== 'MemberExpression' || node.computed) return; if ((node.property as Identifier).name !== 'usernames') return; let parentIndex = ancestors.length - 2; let targetNode = ancestors[parentIndex]; if (targetNode.type !== 'CallExpression') { targetNode = node; } else { parentIndex--; } // If we're already wrapped with an await, nothing to do if (ancestors[parentIndex].type === 'AwaitExpression') return; wrapWithAwait(targetNode); asyncifyScope(ancestors, state); state.isModified = true; }