255 lines
7.8 KiB
JavaScript
255 lines
7.8 KiB
JavaScript
// copied from https://github.com/nodejs/node/blob/88799930794045795e8abac874730f9eba7e2300/lib/internal/repl/await.js
|
|
'use strict';
|
|
|
|
const {
|
|
ArrayFrom,
|
|
ArrayPrototypeForEach,
|
|
ArrayPrototypeIncludes,
|
|
ArrayPrototypeJoin,
|
|
ArrayPrototypePop,
|
|
ArrayPrototypePush,
|
|
FunctionPrototype,
|
|
ObjectKeys,
|
|
RegExpPrototypeSymbolReplace,
|
|
StringPrototypeEndsWith,
|
|
StringPrototypeIncludes,
|
|
StringPrototypeIndexOf,
|
|
StringPrototypeRepeat,
|
|
StringPrototypeSplit,
|
|
StringPrototypeStartsWith,
|
|
SyntaxError,
|
|
} = require('./node-primordials');
|
|
|
|
const parser = require('acorn').Parser;
|
|
const walk = require('acorn-walk');
|
|
const { Recoverable } = require('repl');
|
|
|
|
function isTopLevelDeclaration(state) {
|
|
return state.ancestors[state.ancestors.length - 2] === state.body;
|
|
}
|
|
|
|
const noop = FunctionPrototype;
|
|
const visitorsWithoutAncestors = {
|
|
ClassDeclaration(node, state, c) {
|
|
if (isTopLevelDeclaration(state)) {
|
|
state.prepend(node, `${node.id.name}=`);
|
|
ArrayPrototypePush(
|
|
state.hoistedDeclarationStatements,
|
|
`let ${node.id.name}; `
|
|
);
|
|
}
|
|
|
|
walk.base.ClassDeclaration(node, state, c);
|
|
},
|
|
ForOfStatement(node, state, c) {
|
|
if (node.await === true) {
|
|
state.containsAwait = true;
|
|
}
|
|
walk.base.ForOfStatement(node, state, c);
|
|
},
|
|
FunctionDeclaration(node, state, c) {
|
|
state.prepend(node, `${node.id.name}=`);
|
|
ArrayPrototypePush(
|
|
state.hoistedDeclarationStatements,
|
|
`var ${node.id.name}; `
|
|
);
|
|
},
|
|
FunctionExpression: noop,
|
|
ArrowFunctionExpression: noop,
|
|
MethodDefinition: noop,
|
|
AwaitExpression(node, state, c) {
|
|
state.containsAwait = true;
|
|
walk.base.AwaitExpression(node, state, c);
|
|
},
|
|
ReturnStatement(node, state, c) {
|
|
state.containsReturn = true;
|
|
walk.base.ReturnStatement(node, state, c);
|
|
},
|
|
VariableDeclaration(node, state, c) {
|
|
const variableKind = node.kind;
|
|
const isIterableForDeclaration = ArrayPrototypeIncludes(
|
|
['ForOfStatement', 'ForInStatement'],
|
|
state.ancestors[state.ancestors.length - 2].type
|
|
);
|
|
|
|
if (variableKind === 'var' || isTopLevelDeclaration(state)) {
|
|
state.replace(
|
|
node.start,
|
|
node.start + variableKind.length + (isIterableForDeclaration ? 1 : 0),
|
|
variableKind === 'var' && isIterableForDeclaration ?
|
|
'' :
|
|
'void' + (node.declarations.length === 1 ? '' : ' (')
|
|
);
|
|
|
|
if (!isIterableForDeclaration) {
|
|
ArrayPrototypeForEach(node.declarations, (decl) => {
|
|
state.prepend(decl, '(');
|
|
state.append(decl, decl.init ? ')' : '=undefined)');
|
|
});
|
|
|
|
if (node.declarations.length !== 1) {
|
|
state.append(node.declarations[node.declarations.length - 1], ')');
|
|
}
|
|
}
|
|
|
|
const variableIdentifiersToHoist = [
|
|
['var', []],
|
|
['let', []],
|
|
];
|
|
function registerVariableDeclarationIdentifiers(node) {
|
|
switch (node.type) {
|
|
case 'Identifier':
|
|
ArrayPrototypePush(
|
|
variableIdentifiersToHoist[variableKind === 'var' ? 0 : 1][1],
|
|
node.name
|
|
);
|
|
break;
|
|
case 'ObjectPattern':
|
|
ArrayPrototypeForEach(node.properties, (property) => {
|
|
registerVariableDeclarationIdentifiers(property.value);
|
|
});
|
|
break;
|
|
case 'ArrayPattern':
|
|
ArrayPrototypeForEach(node.elements, (element) => {
|
|
registerVariableDeclarationIdentifiers(element);
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
|
|
ArrayPrototypeForEach(node.declarations, (decl) => {
|
|
registerVariableDeclarationIdentifiers(decl.id);
|
|
});
|
|
|
|
ArrayPrototypeForEach(
|
|
variableIdentifiersToHoist,
|
|
({ 0: kind, 1: identifiers }) => {
|
|
if (identifiers.length > 0) {
|
|
ArrayPrototypePush(
|
|
state.hoistedDeclarationStatements,
|
|
`${kind} ${ArrayPrototypeJoin(identifiers, ', ')}; `
|
|
);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
walk.base.VariableDeclaration(node, state, c);
|
|
}
|
|
};
|
|
|
|
const visitors = {};
|
|
for (const nodeType of ObjectKeys(walk.base)) {
|
|
const callback = visitorsWithoutAncestors[nodeType] || walk.base[nodeType];
|
|
visitors[nodeType] = (node, state, c) => {
|
|
const isNew = node !== state.ancestors[state.ancestors.length - 1];
|
|
if (isNew) {
|
|
ArrayPrototypePush(state.ancestors, node);
|
|
}
|
|
callback(node, state, c);
|
|
if (isNew) {
|
|
ArrayPrototypePop(state.ancestors);
|
|
}
|
|
};
|
|
}
|
|
|
|
function processTopLevelAwait(src) {
|
|
const wrapPrefix = '(async () => { ';
|
|
const wrapped = `${wrapPrefix}${src} })()`;
|
|
const wrappedArray = ArrayFrom(wrapped);
|
|
let root;
|
|
try {
|
|
root = parser.parse(wrapped, { ecmaVersion: 'latest' });
|
|
} catch (e) {
|
|
if (StringPrototypeStartsWith(e.message, 'Unterminated '))
|
|
throw new Recoverable(e);
|
|
// If the parse error is before the first "await", then use the execution
|
|
// error. Otherwise we must emit this parse error, making it look like a
|
|
// proper syntax error.
|
|
const awaitPos = StringPrototypeIndexOf(src, 'await');
|
|
const errPos = e.pos - wrapPrefix.length;
|
|
if (awaitPos > errPos)
|
|
return null;
|
|
// Convert keyword parse errors on await into their original errors when
|
|
// possible.
|
|
if (errPos === awaitPos + 6 &&
|
|
StringPrototypeIncludes(e.message, 'Expecting Unicode escape sequence'))
|
|
return null;
|
|
if (errPos === awaitPos + 7 &&
|
|
StringPrototypeIncludes(e.message, 'Unexpected token'))
|
|
return null;
|
|
const line = e.loc.line;
|
|
const column = line === 1 ? e.loc.column - wrapPrefix.length : e.loc.column;
|
|
let message = '\n' + StringPrototypeSplit(src, '\n')[line - 1] + '\n' +
|
|
StringPrototypeRepeat(' ', column) +
|
|
'^\n\n' + RegExpPrototypeSymbolReplace(/ \([^)]+\)/, e.message, '');
|
|
// V8 unexpected token errors include the token string.
|
|
if (StringPrototypeEndsWith(message, 'Unexpected token'))
|
|
message += " '" +
|
|
// Wrapper end may cause acorn to report error position after the source
|
|
((src.length - 1) >= (e.pos - wrapPrefix.length)
|
|
? src[e.pos - wrapPrefix.length]
|
|
: src[src.length - 1]) +
|
|
"'";
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
throw new SyntaxError(message);
|
|
}
|
|
const body = root.body[0].expression.callee.body;
|
|
const state = {
|
|
body,
|
|
ancestors: [],
|
|
hoistedDeclarationStatements: [],
|
|
replace(from, to, str) {
|
|
for (let i = from; i < to; i++) {
|
|
wrappedArray[i] = '';
|
|
}
|
|
if (from === to) str += wrappedArray[from];
|
|
wrappedArray[from] = str;
|
|
},
|
|
prepend(node, str) {
|
|
wrappedArray[node.start] = str + wrappedArray[node.start];
|
|
},
|
|
append(node, str) {
|
|
wrappedArray[node.end - 1] += str;
|
|
},
|
|
containsAwait: false,
|
|
containsReturn: false
|
|
};
|
|
|
|
walk.recursive(body, state, visitors);
|
|
|
|
// Do not transform if
|
|
// 1. False alarm: there isn't actually an await expression.
|
|
// 2. There is a top-level return, which is not allowed.
|
|
if (!state.containsAwait || state.containsReturn) {
|
|
return null;
|
|
}
|
|
|
|
const last = body.body[body.body.length - 1];
|
|
if (last.type === 'ExpressionStatement') {
|
|
// For an expression statement of the form
|
|
// ( expr ) ;
|
|
// ^^^^^^^^^^ // last
|
|
// ^^^^ // last.expression
|
|
//
|
|
// We do not want the left parenthesis before the `return` keyword;
|
|
// therefore we prepend the `return (` to `last`.
|
|
//
|
|
// On the other hand, we do not want the right parenthesis after the
|
|
// semicolon. Since there can only be more right parentheses between
|
|
// last.expression.end and the semicolon, appending one more to
|
|
// last.expression should be fine.
|
|
state.prepend(last, 'return (');
|
|
state.append(last.expression, ')');
|
|
}
|
|
|
|
return (
|
|
ArrayPrototypeJoin(state.hoistedDeclarationStatements, '') +
|
|
ArrayPrototypeJoin(wrappedArray, '')
|
|
);
|
|
}
|
|
|
|
module.exports = {
|
|
processTopLevelAwait
|
|
};
|