estools / escodegen

ECMAScript code generator
BSD 2-Clause "Simplified" License
2.65k stars 335 forks source link

omitting parentheses from the argument of an await expression can change its meaning #384

Closed cixel closed 5 years ago

cixel commented 5 years ago
await (q() ? a() : b());

This expression needs the parentheses to work as intended. Without them, await will wait on q(), rather than on a() or b().

The AST representing this expression is:

const expr = {
  type: 'AwaitExpression',
  argument: {
    type: 'ConditionalExpression',
    test: {
      type: 'CallExpression',
      callee: {
        type: 'Identifier',
        name: 'q'
      },
      arguments: []
    },
    consequent: {
      type: 'CallExpression',
      callee: {
        type: 'Identifier',
        name: 'a'
      },
      arguments: []
    },
    alternate: {
      type: 'CallExpression',
      callee: {
        type: 'Identifier',
        name: 'b'
      },
      arguments: []
    }
  }
};

Without the parentheses, the AST is this:

{
  type: 'ConditionalExpression',
  test: {
    type: 'AwaitExpression',
    argument: {
      type: 'CallExpression',
      callee: {
        type: 'Identifier',
        name: 'q'
      },
      arguments: []
    }
  },
  consequent: {
    type: 'CallExpression',
    callee: {
      type: 'Identifier',
      name: 'a'
    },
    arguments: []
  },
  alternate: {
    type: 'CallExpression',
    callee: {
      type: 'Identifier',
      name: 'b'
    },
    arguments: []
  }
}

The first AST generates code which parses into the second, and these are not equivalent.

To reproduce

Paste the following code snippet into the tool here:

async function test() {
    await (q() ? a() : b());
}

Click rewrite and observe the parentheses have been removed. Going back and forth between AST and code with this snippet will not preserve the semantics of the expression.