Skip to content

Commit d441d91

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

File tree

3 files changed

+98
-45
lines changed

3 files changed

+98
-45
lines changed

Diff for: lib/internal/repl/await.js

+39-3
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,19 @@ const {
88
ArrayPrototypePush,
99
FunctionPrototype,
1010
ObjectKeys,
11+
RegExpPrototypeSymbolReplace,
12+
StringPrototypeEndsWith,
13+
StringPrototypeIncludes,
14+
StringPrototypeIndexOf,
15+
StringPrototypeRepeat,
16+
StringPrototypeSplit,
17+
StringPrototypeStartsWith,
18+
SyntaxError,
1119
} = primordials;
1220

1321
const parser = require('internal/deps/acorn/acorn/dist/acorn').Parser;
1422
const walk = require('internal/deps/acorn/acorn-walk/dist/walk');
23+
const { Recoverable } = require('internal/repl');
1524

1625
const noop = FunctionPrototype;
1726
const visitorsWithoutAncestors = {
@@ -80,13 +89,40 @@ for (const nodeType of ObjectKeys(walk.base)) {
8089
}
8190

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

Diff for: lib/repl.js

+49-42
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

Diff for: test/parallel/test-repl-top-level-await.js

+10
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)