Skip to content

Commit 9966449

Browse files
guybedfordtargos
authored andcommitted
repl: ensure correct syntax err for await parsing
PR-URL: #39154 Reviewed-By: Anna Henningsen <[email protected]>
1 parent 761dafa commit 9966449

File tree

3 files changed

+98
-45
lines changed

3 files changed

+98
-45
lines changed

lib/internal/repl/await.js

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,19 @@ const {
77
ArrayPrototypePush,
88
FunctionPrototype,
99
ObjectKeys,
10+
RegExpPrototypeSymbolReplace,
11+
StringPrototypeEndsWith,
12+
StringPrototypeIncludes,
13+
StringPrototypeIndexOf,
14+
StringPrototypeRepeat,
15+
StringPrototypeSplit,
16+
StringPrototypeStartsWith,
17+
SyntaxError,
1018
} = primordials;
1119

1220
const parser = require('internal/deps/acorn/acorn/dist/acorn').Parser;
1321
const walk = require('internal/deps/acorn/acorn-walk/dist/walk');
22+
const { Recoverable } = require('internal/repl');
1423

1524
const noop = FunctionPrototype;
1625
const visitorsWithoutAncestors = {
@@ -79,13 +88,40 @@ for (const nodeType of ObjectKeys(walk.base)) {
7988
}
8089

8190
function processTopLevelAwait(src) {
82-
const wrapped = `(async () => { ${src} })()`;
91+
const wrapPrefix = '(async () => { ';
92+
const wrapped = `${wrapPrefix}${src} })()`;
8393
const wrappedArray = ArrayFrom(wrapped);
8494
let root;
8595
try {
8696
root = parser.parse(wrapped, { ecmaVersion: 'latest' });
87-
} catch {
88-
return null;
97+
} catch (e) {
98+
if (StringPrototypeStartsWith(e.message, 'Unterminated '))
99+
throw new Recoverable(e);
100+
// If the parse error is before the first "await", then use the execution
101+
// error. Otherwise we must emit this parse error, making it look like a
102+
// proper syntax error.
103+
const awaitPos = StringPrototypeIndexOf(src, 'await');
104+
const errPos = e.pos - wrapPrefix.length;
105+
if (awaitPos > errPos)
106+
return null;
107+
// Convert keyword parse errors on await into their original errors when
108+
// possible.
109+
if (errPos === awaitPos + 6 &&
110+
StringPrototypeIncludes(e.message, 'Expecting Unicode escape sequence'))
111+
return null;
112+
if (errPos === awaitPos + 7 &&
113+
StringPrototypeIncludes(e.message, 'Unexpected token'))
114+
return null;
115+
const line = e.loc.line;
116+
const column = line === 1 ? e.loc.column - wrapPrefix.length : e.loc.column;
117+
let message = '\n' + StringPrototypeSplit(src, '\n')[line - 1] + '\n' +
118+
StringPrototypeRepeat(' ', column) +
119+
'^\n\n' + RegExpPrototypeSymbolReplace(/ \([^)]+\)/, e.message, '');
120+
// V8 unexpected token errors include the token string.
121+
if (StringPrototypeEndsWith(message, 'Unexpected token'))
122+
message += " '" + src[e.pos - wrapPrefix.length] + "'";
123+
// eslint-disable-next-line no-restricted-syntax
124+
throw new SyntaxError(message);
89125
}
90126
const body = root.body[0].expression.callee.body;
91127
const state = {

lib/repl.js

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

434-
const potentialWrappedCode = processTopLevelAwait(code);
435-
if (potentialWrappedCode !== null) {
436-
code = potentialWrappedCode;
437-
wrappedCmd = true;
438-
awaitPromise = true;
434+
try {
435+
const potentialWrappedCode = processTopLevelAwait(code);
436+
if (potentialWrappedCode !== null) {
437+
code = potentialWrappedCode;
438+
wrappedCmd = true;
439+
awaitPromise = true;
440+
}
441+
} catch (e) {
442+
decorateErrorStack(e);
443+
err = e;
439444
}
440445
}
441446

442447
// First, create the Script object to check the syntax
443448
if (code === '\n')
444449
return cb(null);
445450

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

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

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)