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