@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
text/typescript
// @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;
}