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