repl: support top-level await
Much of the AST visitor code was ported from Chrome DevTools code
written by Aleksey Kozyatinskiy <kozyatinskiy@chromium.org>.
PR-URL: https://github.com/nodejs/node/pull/15566
Fixes: https://github.com/nodejs/node/issues/13209
Refs: e8111c396f
Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
This commit is contained in:
parent
ab64b6d799
commit
eeab7bc068
128
lib/internal/repl/await.js
Normal file
128
lib/internal/repl/await.js
Normal file
@ -0,0 +1,128 @@
|
||||
'use strict';
|
||||
|
||||
const acorn = require('internal/deps/acorn/dist/acorn');
|
||||
const walk = require('internal/deps/acorn/dist/walk');
|
||||
|
||||
const noop = () => {};
|
||||
const visitorsWithoutAncestors = {
|
||||
ClassDeclaration(node, state, c) {
|
||||
if (state.ancestors[state.ancestors.length - 2] === state.body) {
|
||||
state.prepend(node, `${node.id.name}=`);
|
||||
}
|
||||
walk.base.ClassDeclaration(node, state, c);
|
||||
},
|
||||
FunctionDeclaration(node, state, c) {
|
||||
state.prepend(node, `${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) {
|
||||
if (node.kind === 'var' ||
|
||||
state.ancestors[state.ancestors.length - 2] === state.body) {
|
||||
if (node.declarations.length === 1) {
|
||||
state.replace(node.start, node.start + node.kind.length, 'void');
|
||||
} else {
|
||||
state.replace(node.start, node.start + node.kind.length, 'void (');
|
||||
}
|
||||
|
||||
for (const decl of node.declarations) {
|
||||
state.prepend(decl, '(');
|
||||
state.append(decl, decl.init ? ')' : '=undefined)');
|
||||
}
|
||||
|
||||
if (node.declarations.length !== 1) {
|
||||
state.append(node.declarations[node.declarations.length - 1], ')');
|
||||
}
|
||||
}
|
||||
|
||||
walk.base.VariableDeclaration(node, state, c);
|
||||
}
|
||||
};
|
||||
|
||||
const visitors = {};
|
||||
for (const nodeType of Object.keys(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) {
|
||||
state.ancestors.push(node);
|
||||
}
|
||||
callback(node, state, c);
|
||||
if (isNew) {
|
||||
state.ancestors.pop();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function processTopLevelAwait(src) {
|
||||
const wrapped = `(async () => { ${src} })()`;
|
||||
const wrappedArray = wrapped.split('');
|
||||
let root;
|
||||
try {
|
||||
root = acorn.parse(wrapped, { ecmaVersion: 8 });
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
const body = root.body[0].expression.callee.body;
|
||||
const state = {
|
||||
body,
|
||||
ancestors: [],
|
||||
replace(from, to, str) {
|
||||
for (var 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 wrappedArray.join('');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
processTopLevelAwait
|
||||
};
|
86
lib/repl.js
86
lib/repl.js
@ -43,6 +43,7 @@
|
||||
'use strict';
|
||||
|
||||
const internalModule = require('internal/module');
|
||||
const { processTopLevelAwait } = require('internal/repl/await');
|
||||
const internalUtil = require('internal/util');
|
||||
const { isTypedArray } = require('internal/util/types');
|
||||
const util = require('util');
|
||||
@ -200,6 +201,7 @@ function REPLServer(prompt,
|
||||
function defaultEval(code, context, file, cb) {
|
||||
var err, result, script, wrappedErr;
|
||||
var wrappedCmd = false;
|
||||
var awaitPromise = false;
|
||||
var input = code;
|
||||
|
||||
if (/^\s*\{/.test(code) && /\}\s*$/.test(code)) {
|
||||
@ -211,6 +213,15 @@ function REPLServer(prompt,
|
||||
wrappedCmd = true;
|
||||
}
|
||||
|
||||
if (code.includes('await')) {
|
||||
const potentialWrappedCode = processTopLevelAwait(code);
|
||||
if (potentialWrappedCode !== null) {
|
||||
code = potentialWrappedCode;
|
||||
wrappedCmd = true;
|
||||
awaitPromise = true;
|
||||
}
|
||||
}
|
||||
|
||||
// first, create the Script object to check the syntax
|
||||
|
||||
if (code === '\n')
|
||||
@ -231,8 +242,9 @@ function REPLServer(prompt,
|
||||
} catch (e) {
|
||||
debug('parse error %j', code, e);
|
||||
if (wrappedCmd) {
|
||||
wrappedCmd = false;
|
||||
// unwrap and try again
|
||||
wrappedCmd = false;
|
||||
awaitPromise = false;
|
||||
code = input;
|
||||
wrappedErr = e;
|
||||
continue;
|
||||
@ -251,6 +263,20 @@ function REPLServer(prompt,
|
||||
// predefined RegExp properties `RegExp.$1`, `RegExp.$2` ... `RegExp.$9`
|
||||
regExMatcher.test(savedRegExMatches.join(sep));
|
||||
|
||||
let finished = false;
|
||||
function finishExecution(err, result) {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
|
||||
// After executing the current expression, store the values of RegExp
|
||||
// predefined properties back in `savedRegExMatches`
|
||||
for (var idx = 1; idx < savedRegExMatches.length; idx += 1) {
|
||||
savedRegExMatches[idx] = RegExp[`$${idx}`];
|
||||
}
|
||||
|
||||
cb(err, result);
|
||||
}
|
||||
|
||||
if (!err) {
|
||||
// Unset raw mode during evaluation so that Ctrl+C raises a signal.
|
||||
let previouslyInRawMode;
|
||||
@ -301,15 +327,53 @@ function REPLServer(prompt,
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (awaitPromise && !err) {
|
||||
let sigintListener;
|
||||
pause();
|
||||
let promise = result;
|
||||
if (self.breakEvalOnSigint) {
|
||||
const interrupt = new Promise((resolve, reject) => {
|
||||
sigintListener = () => {
|
||||
reject(new Error('Script execution interrupted.'));
|
||||
};
|
||||
prioritizedSigintQueue.add(sigintListener);
|
||||
});
|
||||
promise = Promise.race([promise, interrupt]);
|
||||
}
|
||||
|
||||
promise.then((result) => {
|
||||
// Remove prioritized SIGINT listener if it was not called.
|
||||
// TODO(TimothyGu): Use Promise.prototype.finally when it becomes
|
||||
// available.
|
||||
prioritizedSigintQueue.delete(sigintListener);
|
||||
|
||||
finishExecution(undefined, result);
|
||||
unpause();
|
||||
}, (err) => {
|
||||
// Remove prioritized SIGINT listener if it was not called.
|
||||
prioritizedSigintQueue.delete(sigintListener);
|
||||
|
||||
if (err.message === 'Script execution interrupted.') {
|
||||
// The stack trace for this case is not very useful anyway.
|
||||
Object.defineProperty(err, 'stack', { value: '' });
|
||||
}
|
||||
|
||||
unpause();
|
||||
if (err && process.domain) {
|
||||
debug('not recoverable, send to domain');
|
||||
process.domain.emit('error', err);
|
||||
process.domain.exit();
|
||||
return;
|
||||
}
|
||||
finishExecution(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// After executing the current expression, store the values of RegExp
|
||||
// predefined properties back in `savedRegExMatches`
|
||||
for (var idx = 1; idx < savedRegExMatches.length; idx += 1) {
|
||||
savedRegExMatches[idx] = RegExp[`$${idx}`];
|
||||
if (!awaitPromise || err) {
|
||||
finishExecution(err, result);
|
||||
}
|
||||
|
||||
cb(err, result);
|
||||
}
|
||||
|
||||
self.eval = self._domain.bind(eval_);
|
||||
@ -457,7 +521,15 @@ function REPLServer(prompt,
|
||||
|
||||
var sawSIGINT = false;
|
||||
var sawCtrlD = false;
|
||||
const prioritizedSigintQueue = new Set();
|
||||
self.on('SIGINT', function onSigInt() {
|
||||
if (prioritizedSigintQueue.size > 0) {
|
||||
for (const task of prioritizedSigintQueue) {
|
||||
task();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var empty = self.line.length === 0;
|
||||
self.clearLine();
|
||||
_turnOffEditorMode(self);
|
||||
|
1
node.gyp
1
node.gyp
@ -118,6 +118,7 @@
|
||||
'lib/internal/process/write-coverage.js',
|
||||
'lib/internal/readline.js',
|
||||
'lib/internal/repl.js',
|
||||
'lib/internal/repl/await.js',
|
||||
'lib/internal/socket_list.js',
|
||||
'lib/internal/test/unicode.js',
|
||||
'lib/internal/tls.js',
|
||||
|
68
test/parallel/test-repl-preprocess-top-level-await.js
Normal file
68
test/parallel/test-repl-preprocess-top-level-await.js
Normal file
@ -0,0 +1,68 @@
|
||||
'use strict';
|
||||
|
||||
require('../common');
|
||||
const assert = require('assert');
|
||||
const { processTopLevelAwait } = require('internal/repl/await');
|
||||
|
||||
// Flags: --expose-internals
|
||||
|
||||
// This test was created based on
|
||||
// https://cs.chromium.org/chromium/src/third_party/WebKit/LayoutTests/http/tests/inspector-unit/preprocess-top-level-awaits.js?rcl=358caaba5e763e71c4abb9ada2d9cd8b1188cac9
|
||||
|
||||
const testCases = [
|
||||
[ '0',
|
||||
null ],
|
||||
[ 'await 0',
|
||||
'(async () => { return (await 0) })()' ],
|
||||
[ 'await 0;',
|
||||
'(async () => { return (await 0); })()' ],
|
||||
[ '(await 0)',
|
||||
'(async () => { return ((await 0)) })()' ],
|
||||
[ '(await 0);',
|
||||
'(async () => { return ((await 0)); })()' ],
|
||||
[ 'async function foo() { await 0; }',
|
||||
null ],
|
||||
[ 'async () => await 0',
|
||||
null ],
|
||||
[ 'class A { async method() { await 0 } }',
|
||||
null ],
|
||||
[ 'await 0; return 0;',
|
||||
null ],
|
||||
[ 'var a = await 1',
|
||||
'(async () => { void (a = await 1) })()' ],
|
||||
[ 'let a = await 1',
|
||||
'(async () => { void (a = await 1) })()' ],
|
||||
[ 'const a = await 1',
|
||||
'(async () => { void (a = await 1) })()' ],
|
||||
[ 'for (var i = 0; i < 1; ++i) { await i }',
|
||||
'(async () => { for (void (i = 0); i < 1; ++i) { await i } })()' ],
|
||||
[ 'for (let i = 0; i < 1; ++i) { await i }',
|
||||
'(async () => { for (let i = 0; i < 1; ++i) { await i } })()' ],
|
||||
[ 'var {a} = {a:1}, [b] = [1], {c:{d}} = {c:{d: await 1}}',
|
||||
'(async () => { void ( ({a} = {a:1}), ([b] = [1]), ' +
|
||||
'({c:{d}} = {c:{d: await 1}})) })()' ],
|
||||
/* eslint-disable no-template-curly-in-string */
|
||||
[ 'console.log(`${(await { a: 1 }).a}`)',
|
||||
'(async () => { return (console.log(`${(await { a: 1 }).a}`)) })()' ],
|
||||
/* eslint-enable no-template-curly-in-string */
|
||||
[ 'await 0; function foo() {}',
|
||||
'(async () => { await 0; foo=function foo() {} })()' ],
|
||||
[ 'await 0; class Foo {}',
|
||||
'(async () => { await 0; Foo=class Foo {} })()' ],
|
||||
[ 'if (await true) { function foo() {} }',
|
||||
'(async () => { if (await true) { foo=function foo() {} } })()' ],
|
||||
[ 'if (await true) { class Foo{} }',
|
||||
'(async () => { if (await true) { class Foo{} } })()' ],
|
||||
[ 'if (await true) { var a = 1; }',
|
||||
'(async () => { if (await true) { void (a = 1); } })()' ],
|
||||
[ 'if (await true) { let a = 1; }',
|
||||
'(async () => { if (await true) { let a = 1; } })()' ],
|
||||
[ 'var a = await 1; let b = 2; const c = 3;',
|
||||
'(async () => { void (a = await 1); void (b = 2); void (c = 3); })()' ],
|
||||
[ 'let o = await 1, p',
|
||||
'(async () => { void ( (o = await 1), (p=undefined)) })()' ]
|
||||
];
|
||||
|
||||
for (const [input, expected] of testCases) {
|
||||
assert.strictEqual(processTopLevelAwait(input), expected);
|
||||
}
|
173
test/parallel/test-repl-top-level-await.js
Normal file
173
test/parallel/test-repl-top-level-await.js
Normal file
@ -0,0 +1,173 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const assert = require('assert');
|
||||
const { stripVTControlCharacters } = require('internal/readline');
|
||||
const repl = require('repl');
|
||||
|
||||
common.crashOnUnhandledRejection();
|
||||
|
||||
// Flags: --expose-internals
|
||||
|
||||
const PROMPT = 'await repl > ';
|
||||
|
||||
class REPLStream extends common.ArrayStream {
|
||||
constructor() {
|
||||
super();
|
||||
this.waitingForResponse = false;
|
||||
this.lines = [''];
|
||||
}
|
||||
write(chunk, encoding, callback) {
|
||||
if (Buffer.isBuffer(chunk)) {
|
||||
chunk = chunk.toString(encoding);
|
||||
}
|
||||
const chunkLines = stripVTControlCharacters(chunk).split('\n');
|
||||
this.lines[this.lines.length - 1] += chunkLines[0];
|
||||
if (chunkLines.length > 1) {
|
||||
this.lines.push(...chunkLines.slice(1));
|
||||
}
|
||||
this.emit('line');
|
||||
if (callback) callback();
|
||||
return true;
|
||||
}
|
||||
|
||||
wait(lookFor = PROMPT) {
|
||||
if (this.waitingForResponse) {
|
||||
throw new Error('Currently waiting for response to another command');
|
||||
}
|
||||
this.lines = [''];
|
||||
return common.fires(new Promise((resolve, reject) => {
|
||||
const onError = (err) => {
|
||||
this.removeListener('line', onLine);
|
||||
reject(err);
|
||||
};
|
||||
const onLine = () => {
|
||||
if (this.lines[this.lines.length - 1].includes(lookFor)) {
|
||||
this.removeListener('error', onError);
|
||||
this.removeListener('line', onLine);
|
||||
resolve(this.lines);
|
||||
}
|
||||
};
|
||||
this.once('error', onError);
|
||||
this.on('line', onLine);
|
||||
}), new Error(), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
const putIn = new REPLStream();
|
||||
const testMe = repl.start({
|
||||
prompt: PROMPT,
|
||||
stream: putIn,
|
||||
terminal: true,
|
||||
useColors: false,
|
||||
breakEvalOnSigint: true
|
||||
});
|
||||
|
||||
function runAndWait(cmds, lookFor) {
|
||||
const promise = putIn.wait(lookFor);
|
||||
for (const cmd of cmds) {
|
||||
if (typeof cmd === 'string') {
|
||||
putIn.run([cmd]);
|
||||
} else {
|
||||
testMe.write('', cmd);
|
||||
}
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
|
||||
async function ordinaryTests() {
|
||||
// These tests were created based on
|
||||
// https://cs.chromium.org/chromium/src/third_party/WebKit/LayoutTests/http/tests/devtools/console/console-top-level-await.js?rcl=5d0ea979f0ba87655b7ef0e03b58fa3c04986ba6
|
||||
putIn.run([
|
||||
'function foo(x) { return x; }',
|
||||
'function koo() { return Promise.resolve(4); }'
|
||||
]);
|
||||
const testCases = [
|
||||
[ 'await Promise.resolve(0)', '0' ],
|
||||
[ '{ a: await Promise.resolve(1) }', '{ a: 1 }' ],
|
||||
[ '_', '{ a: 1 }' ],
|
||||
[ 'let { a, b } = await Promise.resolve({ a: 1, b: 2 }), f = 5;',
|
||||
'undefined' ],
|
||||
[ 'a', '1' ],
|
||||
[ 'b', '2' ],
|
||||
[ 'f', '5' ],
|
||||
[ 'let c = await Promise.resolve(2)', 'undefined' ],
|
||||
[ 'c', '2' ],
|
||||
[ 'let d;', 'undefined' ],
|
||||
[ 'd', 'undefined' ],
|
||||
[ 'let [i, { abc: { k } }] = [0, { abc: { k: 1 } }];', 'undefined' ],
|
||||
[ 'i', '0' ],
|
||||
[ 'k', '1' ],
|
||||
[ 'var l = await Promise.resolve(2);', 'undefined' ],
|
||||
[ 'l', '2' ],
|
||||
[ 'foo(await koo())', '4' ],
|
||||
[ '_', '4' ],
|
||||
[ 'const m = foo(await koo());', 'undefined' ],
|
||||
[ 'm', '4' ],
|
||||
[ 'const n = foo(await\nkoo());', 'undefined' ],
|
||||
[ 'n', '4' ],
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
[ '`status: ${(await Promise.resolve({ status: 200 })).status}`',
|
||||
"'status: 200'"],
|
||||
[ 'for (let i = 0; i < 2; ++i) await i', 'undefined' ],
|
||||
[ 'for (let i = 0; i < 2; ++i) { await i }', 'undefined' ],
|
||||
[ 'await 0', '0' ],
|
||||
[ 'await 0; function foo() {}', 'undefined' ],
|
||||
[ 'foo', '[Function: foo]' ],
|
||||
[ 'class Foo {}; await 1;', '1' ],
|
||||
[ 'Foo', '[Function: Foo]' ],
|
||||
[ 'if (await true) { function bar() {}; }', 'undefined' ],
|
||||
[ 'bar', '[Function: bar]' ],
|
||||
[ 'if (await true) { class Bar {}; }', 'undefined' ],
|
||||
[ 'Bar', 'ReferenceError: Bar is not defined', { line: 0 } ],
|
||||
[ 'await 0; function* gen(){}', 'undefined' ],
|
||||
[ 'for (var i = 0; i < 10; ++i) { await i; }', 'undefined' ],
|
||||
[ 'i', '10' ],
|
||||
[ 'for (let j = 0; j < 5; ++j) { await j; }', 'undefined' ],
|
||||
[ 'j', 'ReferenceError: j is not defined', { line: 0 } ],
|
||||
[ 'gen', '[GeneratorFunction: gen]' ],
|
||||
[ 'return 42; await 5;', 'SyntaxError: Illegal return statement',
|
||||
{ line: 3 } ],
|
||||
[ 'let o = await 1, p', 'undefined' ],
|
||||
[ 'p', 'undefined' ],
|
||||
[ 'let q = 1, s = await 2', 'undefined' ],
|
||||
[ 's', '2' ]
|
||||
];
|
||||
|
||||
for (const [input, expected, options = {}] of testCases) {
|
||||
console.log(`Testing ${input}`);
|
||||
const toBeRun = input.split('\n');
|
||||
const lines = await runAndWait(toBeRun);
|
||||
if ('line' in options) {
|
||||
assert.strictEqual(lines[toBeRun.length + options.line], expected);
|
||||
} else {
|
||||
const echoed = toBeRun.map((a, i) => `${i > 0 ? '... ' : ''}${a}\r`);
|
||||
assert.deepStrictEqual(lines, [...echoed, expected, PROMPT]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function ctrlCTest() {
|
||||
putIn.run([
|
||||
`const timeout = (msecs) => new Promise((resolve) => {
|
||||
setTimeout(resolve, msecs).unref();
|
||||
});`
|
||||
]);
|
||||
|
||||
console.log('Testing Ctrl+C');
|
||||
assert.deepStrictEqual(await runAndWait([
|
||||
'await timeout(100000)',
|
||||
{ ctrl: true, name: 'c' }
|
||||
]), [
|
||||
'await timeout(100000)\r',
|
||||
'Thrown: Error: Script execution interrupted.',
|
||||
PROMPT
|
||||
]);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await ordinaryTests();
|
||||
await ctrlCTest();
|
||||
}
|
||||
|
||||
main();
|
Loading…
x
Reference in New Issue
Block a user