• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3const {
4  ArrayPrototypeForEach,
5  ArrayPrototypeIncludes,
6  ArrayPrototypeJoin,
7  ArrayPrototypePop,
8  ArrayPrototypePush,
9  FunctionPrototype,
10  ObjectKeys,
11  RegExpPrototypeSymbolReplace,
12  StringPrototypeEndsWith,
13  StringPrototypeIncludes,
14  StringPrototypeIndexOf,
15  StringPrototypeRepeat,
16  StringPrototypeSplit,
17  StringPrototypeStartsWith,
18  SyntaxError,
19} = primordials;
20
21const parser = require('internal/deps/acorn/acorn/dist/acorn').Parser;
22const walk = require('internal/deps/acorn/acorn-walk/dist/walk');
23const { Recoverable } = require('internal/repl');
24
25function isTopLevelDeclaration(state) {
26  return state.ancestors[state.ancestors.length - 2] === state.body;
27}
28
29const noop = FunctionPrototype;
30const visitorsWithoutAncestors = {
31  ClassDeclaration(node, state, c) {
32    if (isTopLevelDeclaration(state)) {
33      state.prepend(node, `${node.id.name}=`);
34      ArrayPrototypePush(
35        state.hoistedDeclarationStatements,
36        `let ${node.id.name}; `,
37      );
38    }
39
40    walk.base.ClassDeclaration(node, state, c);
41  },
42  ForOfStatement(node, state, c) {
43    if (node.await === true) {
44      state.containsAwait = true;
45    }
46    walk.base.ForOfStatement(node, state, c);
47  },
48  FunctionDeclaration(node, state, c) {
49    state.prepend(node, `this.${node.id.name} = ${node.id.name}; `);
50    ArrayPrototypePush(
51      state.hoistedDeclarationStatements,
52      `var ${node.id.name}; `,
53    );
54  },
55  FunctionExpression: noop,
56  ArrowFunctionExpression: noop,
57  MethodDefinition: noop,
58  AwaitExpression(node, state, c) {
59    state.containsAwait = true;
60    walk.base.AwaitExpression(node, state, c);
61  },
62  ReturnStatement(node, state, c) {
63    state.containsReturn = true;
64    walk.base.ReturnStatement(node, state, c);
65  },
66  VariableDeclaration(node, state, c) {
67    const variableKind = node.kind;
68    const isIterableForDeclaration = ArrayPrototypeIncludes(
69      ['ForOfStatement', 'ForInStatement'],
70      state.ancestors[state.ancestors.length - 2].type,
71    );
72
73    if (variableKind === 'var' || isTopLevelDeclaration(state)) {
74      state.replace(
75        node.start,
76        node.start + variableKind.length + (isIterableForDeclaration ? 1 : 0),
77        variableKind === 'var' && isIterableForDeclaration ?
78          '' :
79          'void' + (node.declarations.length === 1 ? '' : ' ('),
80      );
81
82      if (!isIterableForDeclaration) {
83        ArrayPrototypeForEach(node.declarations, (decl) => {
84          state.prepend(decl, '(');
85          state.append(decl, decl.init ? ')' : '=undefined)');
86        });
87
88        if (node.declarations.length !== 1) {
89          state.append(node.declarations[node.declarations.length - 1], ')');
90        }
91      }
92
93      const variableIdentifiersToHoist = [
94        ['var', []],
95        ['let', []],
96      ];
97      function registerVariableDeclarationIdentifiers(node) {
98        switch (node.type) {
99          case 'Identifier':
100            ArrayPrototypePush(
101              variableIdentifiersToHoist[variableKind === 'var' ? 0 : 1][1],
102              node.name,
103            );
104            break;
105          case 'ObjectPattern':
106            ArrayPrototypeForEach(node.properties, (property) => {
107              registerVariableDeclarationIdentifiers(property.value);
108            });
109            break;
110          case 'ArrayPattern':
111            ArrayPrototypeForEach(node.elements, (element) => {
112              registerVariableDeclarationIdentifiers(element);
113            });
114            break;
115        }
116      }
117
118      ArrayPrototypeForEach(node.declarations, (decl) => {
119        registerVariableDeclarationIdentifiers(decl.id);
120      });
121
122      ArrayPrototypeForEach(
123        variableIdentifiersToHoist,
124        ({ 0: kind, 1: identifiers }) => {
125          if (identifiers.length > 0) {
126            ArrayPrototypePush(
127              state.hoistedDeclarationStatements,
128              `${kind} ${ArrayPrototypeJoin(identifiers, ', ')}; `,
129            );
130          }
131        },
132      );
133    }
134
135    walk.base.VariableDeclaration(node, state, c);
136  },
137};
138
139const visitors = {};
140for (const nodeType of ObjectKeys(walk.base)) {
141  const callback = visitorsWithoutAncestors[nodeType] || walk.base[nodeType];
142  visitors[nodeType] = (node, state, c) => {
143    const isNew = node !== state.ancestors[state.ancestors.length - 1];
144    if (isNew) {
145      ArrayPrototypePush(state.ancestors, node);
146    }
147    callback(node, state, c);
148    if (isNew) {
149      ArrayPrototypePop(state.ancestors);
150    }
151  };
152}
153
154function processTopLevelAwait(src) {
155  const wrapPrefix = '(async () => { ';
156  const wrapped = `${wrapPrefix}${src} })()`;
157  const wrappedArray = StringPrototypeSplit(wrapped, '');
158  let root;
159  try {
160    root = parser.parse(wrapped, { ecmaVersion: 'latest' });
161  } catch (e) {
162    if (StringPrototypeStartsWith(e.message, 'Unterminated '))
163      throw new Recoverable(e);
164    // If the parse error is before the first "await", then use the execution
165    // error. Otherwise we must emit this parse error, making it look like a
166    // proper syntax error.
167    const awaitPos = StringPrototypeIndexOf(src, 'await');
168    const errPos = e.pos - wrapPrefix.length;
169    if (awaitPos > errPos)
170      return null;
171    // Convert keyword parse errors on await into their original errors when
172    // possible.
173    if (errPos === awaitPos + 6 &&
174        StringPrototypeIncludes(e.message, 'Expecting Unicode escape sequence'))
175      return null;
176    if (errPos === awaitPos + 7 &&
177        StringPrototypeIncludes(e.message, 'Unexpected token'))
178      return null;
179    const line = e.loc.line;
180    const column = line === 1 ? e.loc.column - wrapPrefix.length : e.loc.column;
181    let message = '\n' + StringPrototypeSplit(src, '\n')[line - 1] + '\n' +
182        StringPrototypeRepeat(' ', column) +
183        '^\n\n' + RegExpPrototypeSymbolReplace(/ \([^)]+\)/, e.message, '');
184    // V8 unexpected token errors include the token string.
185    if (StringPrototypeEndsWith(message, 'Unexpected token'))
186      message += " '" +
187        // Wrapper end may cause acorn to report error position after the source
188        (src[e.pos - wrapPrefix.length] ?? src[src.length - 1]) +
189        "'";
190    // eslint-disable-next-line no-restricted-syntax
191    throw new SyntaxError(message);
192  }
193  const body = root.body[0].expression.callee.body;
194  const state = {
195    body,
196    ancestors: [],
197    hoistedDeclarationStatements: [],
198    replace(from, to, str) {
199      for (let i = from; i < to; i++) {
200        wrappedArray[i] = '';
201      }
202      if (from === to) str += wrappedArray[from];
203      wrappedArray[from] = str;
204    },
205    prepend(node, str) {
206      wrappedArray[node.start] = str + wrappedArray[node.start];
207    },
208    append(node, str) {
209      wrappedArray[node.end - 1] += str;
210    },
211    containsAwait: false,
212    containsReturn: false,
213  };
214
215  walk.recursive(body, state, visitors);
216
217  // Do not transform if
218  // 1. False alarm: there isn't actually an await expression.
219  // 2. There is a top-level return, which is not allowed.
220  if (!state.containsAwait || state.containsReturn) {
221    return null;
222  }
223
224  for (let i = body.body.length - 1; i >= 0; i--) {
225    const node = body.body[i];
226    if (node.type === 'EmptyStatement') continue;
227    if (node.type === 'ExpressionStatement') {
228      // For an expression statement of the form
229      // ( expr ) ;
230      // ^^^^^^^^^^   // node
231      //   ^^^^       // node.expression
232      //
233      // We do not want the left parenthesis before the `return` keyword;
234      // therefore we prepend the `return (` to `node`.
235      //
236      // On the other hand, we do not want the right parenthesis after the
237      // semicolon. Since there can only be more right parentheses between
238      // node.expression.end and the semicolon, appending one more to
239      // node.expression should be fine.
240      //
241      // We also create a wrapper object around the result of the expression.
242      // Consider an expression of the form `(await x).y`. If we just return
243      // this expression from an async function, the caller will await `y`, too,
244      // if it evaluates to a Promise. Instead, we return
245      // `{ value: ((await x).y) }`, which allows the caller to retrieve the
246      // awaited value correctly.
247      state.prepend(node.expression, '{ value: (');
248      state.prepend(node, 'return ');
249      state.append(node.expression, ') }');
250    }
251    break;
252  }
253
254  return (
255    ArrayPrototypeJoin(state.hoistedDeclarationStatements, '') +
256    ArrayPrototypeJoin(wrappedArray, '')
257  );
258}
259
260module.exports = {
261  processTopLevelAwait,
262};
263