Skip to content

Commit 668e201

Browse files
committed
repl: ensure correct syntax error for await parsing
1 parent 84d6ce9 commit 668e201

File tree

4 files changed

+110
-65
lines changed

4 files changed

+110
-65
lines changed

lib/internal/repl/await.js

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const {
88
ArrayPrototypePush,
99
FunctionPrototype,
1010
ObjectKeys,
11+
SyntaxError,
1112
} = primordials;
1213

1314
const acorn = require('internal/deps/acorn/acorn/dist/acorn');
@@ -92,13 +93,40 @@ for (const nodeType of ObjectKeys(walk.base)) {
9293
}
9394

9495
function processTopLevelAwait(src) {
95-
const wrapped = `(async () => { ${src} })()`;
96+
const wrapPrefix = '(async () => {\n';
97+
const wrapped = `${wrapPrefix}${src}\n})()`;
9698
const wrappedArray = ArrayFrom(wrapped);
9799
let root;
98100
try {
99101
root = parser.parse(wrapped, { ecmaVersion: 'latest' });
100-
} catch {
101-
return null;
102+
} catch (e) {
103+
// If the parse error is before the first "await", then use the execution
104+
// error. Otherwise we must emit this parse error, making it look like a
105+
// proper syntax error.
106+
const awaitPos = src.indexOf('await');
107+
const errPos = e.pos - wrapPrefix.length;
108+
if (awaitPos > errPos)
109+
return null;
110+
// Convert keyword parse errors on await into their original errors when
111+
// possible.
112+
if (errPos === awaitPos + 6 &&
113+
src.slice(errPos - 6, errPos - 1) === 'await' &&
114+
e.message.includes('Expecting Unicode escape sequence'))
115+
return null;
116+
if (errPos === awaitPos + 7 &&
117+
src.slice(errPos - 7, errPos - 2) === 'await' &&
118+
e.message.includes('Unexpected token'))
119+
return null;
120+
const { line, column } = e.loc;
121+
let message = '\n' + src.split('\n')[line - 2] + '\n';
122+
let i = 0;
123+
while (i++ < column) message += ' ';
124+
message += '^\n\n' + e.message.replace(/ \([^)]+\)/, '');
125+
// V8 unexpected token errors include the token string.
126+
if (message.endsWith('Unexpected token'))
127+
message += " '" + src[e.pos - wrapPrefix.length] + "'";
128+
// eslint-disable-next-line no-restricted-syntax
129+
throw new SyntaxError(message);
102130
}
103131
const body = root.body[0].expression.callee.body;
104132
const state = {

lib/repl.js

Lines changed: 49 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -426,59 +426,66 @@ function REPLServer(prompt,
426426
({ processTopLevelAwait } = require('internal/repl/await'));
427427
}
428428

429-
const potentialWrappedCode = processTopLevelAwait(code);
430-
if (potentialWrappedCode !== null) {
431-
code = potentialWrappedCode;
432-
wrappedCmd = true;
433-
awaitPromise = true;
429+
try {
430+
const potentialWrappedCode = processTopLevelAwait(code);
431+
if (potentialWrappedCode !== null) {
432+
code = potentialWrappedCode;
433+
wrappedCmd = true;
434+
awaitPromise = true;
435+
}
436+
} catch (e) {
437+
decorateErrorStack(e);
438+
err = e;
434439
}
435440
}
436441

437442
// First, create the Script object to check the syntax
438443
if (code === '\n')
439444
return cb(null);
440445

441-
let parentURL;
442-
try {
443-
const { pathToFileURL } = require('url');
444-
// Adding `/repl` prevents dynamic imports from loading relative
445-
// to the parent of `process.cwd()`.
446-
parentURL = pathToFileURL(path.join(process.cwd(), 'repl')).href;
447-
} catch {
448-
}
449-
while (true) {
446+
if (err === null) {
447+
let parentURL;
450448
try {
451-
if (self.replMode === module.exports.REPL_MODE_STRICT &&
452-
!RegExpPrototypeTest(/^\s*$/, code)) {
453-
// "void 0" keeps the repl from returning "use strict" as the result
454-
// value for statements and declarations that don't return a value.
455-
code = `'use strict'; void 0;\n${code}`;
456-
}
457-
script = vm.createScript(code, {
458-
filename: file,
459-
displayErrors: true,
460-
importModuleDynamically: async (specifier) => {
461-
return asyncESM.ESMLoader.import(specifier, parentURL);
449+
const { pathToFileURL } = require('url');
450+
// Adding `/repl` prevents dynamic imports from loading relative
451+
// to the parent of `process.cwd()`.
452+
parentURL = pathToFileURL(path.join(process.cwd(), 'repl')).href;
453+
} catch {
454+
}
455+
while (true) {
456+
try {
457+
if (self.replMode === module.exports.REPL_MODE_STRICT &&
458+
!RegExpPrototypeTest(/^\s*$/, code)) {
459+
// "void 0" keeps the repl from returning "use strict" as the result
460+
// value for statements and declarations that don't return a value.
461+
code = `'use strict'; void 0;\n${code}`;
462462
}
463-
});
464-
} catch (e) {
465-
debug('parse error %j', code, e);
466-
if (wrappedCmd) {
467-
// Unwrap and try again
468-
wrappedCmd = false;
469-
awaitPromise = false;
470-
code = input;
471-
wrappedErr = e;
472-
continue;
463+
script = vm.createScript(code, {
464+
filename: file,
465+
displayErrors: true,
466+
importModuleDynamically: async (specifier) => {
467+
return asyncESM.ESMLoader.import(specifier, parentURL);
468+
}
469+
});
470+
} catch (e) {
471+
debug('parse error %j', code, e);
472+
if (wrappedCmd) {
473+
// Unwrap and try again
474+
wrappedCmd = false;
475+
awaitPromise = false;
476+
code = input;
477+
wrappedErr = e;
478+
continue;
479+
}
480+
// Preserve original error for wrapped command
481+
const error = wrappedErr || e;
482+
if (isRecoverableError(error, code))
483+
err = new Recoverable(error);
484+
else
485+
err = error;
473486
}
474-
// Preserve original error for wrapped command
475-
const error = wrappedErr || e;
476-
if (isRecoverableError(error, code))
477-
err = new Recoverable(error);
478-
else
479-
err = error;
487+
break;
480488
}
481-
break;
482489
}
483490

484491
// This will set the values from `savedRegExMatches` to corresponding

test/parallel/test-repl-preprocess-top-level-await.js

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ const testCases = [
1313
[ '0',
1414
null ],
1515
[ 'await 0',
16-
'(async () => { return (await 0) })()' ],
16+
'(async () => {\nreturn (await 0)\n})()' ],
1717
[ 'await 0;',
18-
'(async () => { return (await 0); })()' ],
18+
'(async () => {\nreturn (await 0);\n})()' ],
1919
[ '(await 0)',
20-
'(async () => { return ((await 0)) })()' ],
20+
'(async () => {\nreturn ((await 0))\n})()' ],
2121
[ '(await 0);',
22-
'(async () => { return ((await 0)); })()' ],
22+
'(async () => {\nreturn ((await 0));\n})()' ],
2323
[ 'async function foo() { await 0; }',
2424
null ],
2525
[ 'async () => await 0',
@@ -29,38 +29,38 @@ const testCases = [
2929
[ 'await 0; return 0;',
3030
null ],
3131
[ 'var a = await 1',
32-
'(async () => { void (a = await 1) })()' ],
32+
'(async () => {\nvoid (a = await 1)\n})()' ],
3333
[ 'let a = await 1',
34-
'(async () => { void (a = await 1) })()' ],
34+
'(async () => {\nvoid (a = await 1)\n})()' ],
3535
[ 'const a = await 1',
36-
'(async () => { void (a = await 1) })()' ],
36+
'(async () => {\nvoid (a = await 1)\n})()' ],
3737
[ 'for (var i = 0; i < 1; ++i) { await i }',
38-
'(async () => { for (void (i = 0); i < 1; ++i) { await i } })()' ],
38+
'(async () => {\nfor (void (i = 0); i < 1; ++i) { await i }\n})()' ],
3939
[ 'for (let i = 0; i < 1; ++i) { await i }',
40-
'(async () => { for (let i = 0; i < 1; ++i) { await i } })()' ],
40+
'(async () => {\nfor (let i = 0; i < 1; ++i) { await i }\n})()' ],
4141
[ 'var {a} = {a:1}, [b] = [1], {c:{d}} = {c:{d: await 1}}',
42-
'(async () => { void ( ({a} = {a:1}), ([b] = [1]), ' +
43-
'({c:{d}} = {c:{d: await 1}})) })()' ],
42+
'(async () => {\nvoid ( ({a} = {a:1}), ([b] = [1]), ' +
43+
'({c:{d}} = {c:{d: await 1}}))\n})()' ],
4444
/* eslint-disable no-template-curly-in-string */
4545
[ 'console.log(`${(await { a: 1 }).a}`)',
46-
'(async () => { return (console.log(`${(await { a: 1 }).a}`)) })()' ],
46+
'(async () => {\nreturn (console.log(`${(await { a: 1 }).a}`))\n})()' ],
4747
/* eslint-enable no-template-curly-in-string */
4848
[ 'await 0; function foo() {}',
49-
'(async () => { await 0; foo=function foo() {} })()' ],
49+
'(async () => {\nawait 0; foo=function foo() {}\n})()' ],
5050
[ 'await 0; class Foo {}',
51-
'(async () => { await 0; Foo=class Foo {} })()' ],
51+
'(async () => {\nawait 0; Foo=class Foo {}\n})()' ],
5252
[ 'if (await true) { function foo() {} }',
53-
'(async () => { if (await true) { foo=function foo() {} } })()' ],
53+
'(async () => {\nif (await true) { foo=function foo() {} }\n})()' ],
5454
[ 'if (await true) { class Foo{} }',
55-
'(async () => { if (await true) { class Foo{} } })()' ],
55+
'(async () => {\nif (await true) { class Foo{} }\n})()' ],
5656
[ 'if (await true) { var a = 1; }',
57-
'(async () => { if (await true) { void (a = 1); } })()' ],
57+
'(async () => {\nif (await true) { void (a = 1); }\n})()' ],
5858
[ 'if (await true) { let a = 1; }',
59-
'(async () => { if (await true) { let a = 1; } })()' ],
59+
'(async () => {\nif (await true) { let a = 1; }\n})()' ],
6060
[ 'var a = await 1; let b = 2; const c = 3;',
61-
'(async () => { void (a = await 1); void (b = 2); void (c = 3); })()' ],
61+
'(async () => {\nvoid (a = await 1); void (b = 2); void (c = 3);\n})()' ],
6262
[ 'let o = await 1, p',
63-
'(async () => { void ( (o = await 1), (p=undefined)) })()' ],
63+
'(async () => {\nvoid ( (o = await 1), (p=undefined))\n})()' ],
6464
];
6565

6666
for (const [input, expected] of testCases) {

test/parallel/test-repl-top-level-await.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,16 @@ async function ordinaryTests() {
142142
'undefined',
143143
],
144144
],
145+
['await Promise..resolve()',
146+
[
147+
'await Promise..resolve()\r',
148+
'Uncaught SyntaxError: ',
149+
'await Promise..resolve()',
150+
' ^',
151+
'',
152+
'Unexpected token \'.\'',
153+
],
154+
],
145155
];
146156

147157
for (const [input, expected = [`${input}\r`], options = {}] of testCases) {

0 commit comments

Comments
 (0)