tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

mapAwaitExpression.js (6323B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
      4 
      5 import generate from "@babel/generator";
      6 import * as t from "@babel/types";
      7 
      8 import { replaceNode } from "./utils/ast";
      9 import { isTopLevel } from "./utils/helpers";
     10 
     11 // translates new bindings `var a = 3` into `a = 3`.
     12 function translateDeclarationIntoAssignment(node) {
     13  return node.declarations.reduce((acc, declaration) => {
     14    // Don't translate declaration without initial assignment (e.g. `var a;`)
     15    if (!declaration.init) {
     16      return acc;
     17    }
     18    acc.push(
     19      t.expressionStatement(
     20        t.assignmentExpression("=", declaration.id, declaration.init)
     21      )
     22    );
     23    return acc;
     24  }, []);
     25 }
     26 
     27 /**
     28 * Given an AST, modify it to return the last evaluated statement's expression value if possible.
     29 * This is to preserve existing console behavior of displaying the last executed expression value.
     30 */
     31 function addReturnNode(ast) {
     32  const statements = ast.program.body;
     33  const lastStatement = statements.pop();
     34 
     35  // if the last expression is an awaitExpression, strip the `await` part and directly
     36  // return the argument to avoid calling the argument's `then` function twice when the
     37  // mapped expression gets evaluated (See Bug 1771428)
     38  if (t.isAwaitExpression(lastStatement.expression)) {
     39    lastStatement.expression = lastStatement.expression.argument;
     40  }
     41 
     42  // NOTE: For more complicated cases such as an if/for statement, the last evaluated
     43  // expression value probably can not be displayed, unless doing hacky workarounds such
     44  // as returning the `eval` of the final statement (won't always work due to CSP issues?)
     45  // or SpiderMonkey support (See Bug 1839588) at which point this entire module can be removed.
     46  statements.push(
     47    t.isExpressionStatement(lastStatement)
     48      ? t.returnStatement(lastStatement.expression)
     49      : lastStatement
     50  );
     51  return statements;
     52 }
     53 
     54 function getDeclarations(node) {
     55  const { kind, declarations } = node;
     56  const declaratorNodes = declarations.reduce((acc, d) => {
     57    const declarators = getVariableDeclarators(d.id);
     58    return acc.concat(declarators);
     59  }, []);
     60 
     61  // We can't declare const variables outside of the async iife because we
     62  // wouldn't be able to re-assign them. As a workaround, we transform them
     63  // to `let` which should be good enough for those case.
     64  return t.variableDeclaration(
     65    kind === "const" ? "let" : kind,
     66    declaratorNodes
     67  );
     68 }
     69 
     70 function getVariableDeclarators(node) {
     71  if (t.isIdentifier(node)) {
     72    return t.variableDeclarator(t.identifier(node.name));
     73  }
     74 
     75  if (t.isObjectProperty(node)) {
     76    return getVariableDeclarators(node.value);
     77  }
     78  if (t.isRestElement(node)) {
     79    return getVariableDeclarators(node.argument);
     80  }
     81 
     82  if (t.isAssignmentPattern(node)) {
     83    return getVariableDeclarators(node.left);
     84  }
     85 
     86  if (t.isArrayPattern(node)) {
     87    return node.elements.reduce(
     88      (acc, element) => acc.concat(getVariableDeclarators(element)),
     89      []
     90    );
     91  }
     92  if (t.isObjectPattern(node)) {
     93    return node.properties.reduce(
     94      (acc, property) => acc.concat(getVariableDeclarators(property)),
     95      []
     96    );
     97  }
     98  return [];
     99 }
    100 
    101 /**
    102 * Given an AST and an array of variableDeclaration nodes, return a new AST with
    103 * all the declarations at the top of the AST.
    104 */
    105 function addTopDeclarationNodes(ast, declarationNodes) {
    106  const statements = [];
    107  declarationNodes.forEach(declarationNode => {
    108    statements.push(getDeclarations(declarationNode));
    109  });
    110  statements.push(ast);
    111  return t.program(statements);
    112 }
    113 
    114 /**
    115 * Given an AST, return an object of the following shape:
    116 *   - newAst: {AST} the AST where variable declarations were transformed into
    117 *             variable assignments
    118 *   - declarations: {Array<Node>} An array of all the declaration nodes needed
    119 *                   outside of the async iife.
    120 */
    121 function translateDeclarationsIntoAssignment(ast) {
    122  const declarations = [];
    123  t.traverse(ast, (node, ancestors) => {
    124    const parent = ancestors[ancestors.length - 1];
    125 
    126    if (
    127      t.isWithStatement(node) ||
    128      !isTopLevel(ancestors) ||
    129      t.isAssignmentExpression(node) ||
    130      !t.isVariableDeclaration(node) ||
    131      t.isForStatement(parent.node) ||
    132      t.isForXStatement(parent.node) ||
    133      !Array.isArray(node.declarations) ||
    134      node.declarations.length === 0
    135    ) {
    136      return;
    137    }
    138 
    139    const newNodes = translateDeclarationIntoAssignment(node);
    140    replaceNode(ancestors, newNodes);
    141    declarations.push(node);
    142  });
    143 
    144  return {
    145    newAst: ast,
    146    declarations,
    147  };
    148 }
    149 
    150 /**
    151 * Given an AST, wrap its body in an async iife, transform variable declarations
    152 * in assignments and move the variable declarations outside of the async iife.
    153 * Example: With the AST for the following expression: `let a = await 123`, the
    154 * function will return:
    155 * let a;
    156 * (async => {
    157 *   return a = await 123;
    158 * })();
    159 */
    160 function wrapExpressionFromAst(ast) {
    161  // Transform let and var declarations into assignments, and get back an array
    162  // of variable declarations.
    163  let { newAst, declarations } = translateDeclarationsIntoAssignment(ast);
    164  const body = addReturnNode(newAst);
    165 
    166  // Create the async iife.
    167  newAst = t.expressionStatement(
    168    t.callExpression(
    169      t.arrowFunctionExpression([], t.blockStatement(body), true),
    170      []
    171    )
    172  );
    173 
    174  // Now let's put all the variable declarations at the top of the async iife.
    175  newAst = addTopDeclarationNodes(newAst, declarations);
    176 
    177  return generate(newAst).code;
    178 }
    179 
    180 export default function mapTopLevelAwait(expression, ast) {
    181  if (!ast) {
    182    // If there's no ast this means the expression is malformed. And if the
    183    // expression contains the await keyword, we still want to wrap it in an
    184    // async iife in order to get a meaningful message (without this, the
    185    // engine will throw an Error stating that await keywords are only valid
    186    // in async functions and generators).
    187    if (expression.includes("await ")) {
    188      return `(async () => { ${expression} })();`;
    189    }
    190 
    191    return expression;
    192  }
    193 
    194  // Does it have top-level-await?
    195  if (!ast.program.extra.topLevelAwait) {
    196    return expression;
    197  }
    198 
    199  return wrapExpressionFromAst(ast);
    200 }