Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

repl: ensure correct syntax error for await parsing #39154

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 39 additions & 3 deletions lib/internal/repl/await.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,19 @@ const {
ArrayPrototypePush,
FunctionPrototype,
ObjectKeys,
RegExpPrototypeSymbolReplace,
StringPrototypeEndsWith,
StringPrototypeIncludes,
StringPrototypeIndexOf,
StringPrototypeRepeat,
StringPrototypeSplit,
StringPrototypeStartsWith,
SyntaxError,
} = primordials;

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

const noop = FunctionPrototype;
const visitorsWithoutAncestors = {
Expand Down Expand Up @@ -80,13 +89,40 @@ for (const nodeType of ObjectKeys(walk.base)) {
}

function processTopLevelAwait(src) {
const wrapped = `(async () => { ${src} })()`;
const wrapPrefix = '(async () => { ';
const wrapped = `${wrapPrefix}${src} })()`;
const wrappedArray = ArrayFrom(wrapped);
let root;
try {
root = parser.parse(wrapped, { ecmaVersion: 'latest' });
} catch {
return null;
} 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 += " '" + src[e.pos - wrapPrefix.length] + "'";
// eslint-disable-next-line no-restricted-syntax
throw new SyntaxError(message);
}
const body = root.body[0].expression.callee.body;
const state = {
Expand Down
91 changes: 49 additions & 42 deletions lib/repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -426,59 +426,66 @@ function REPLServer(prompt,
({ processTopLevelAwait } = require('internal/repl/await'));
}

const potentialWrappedCode = processTopLevelAwait(code);
if (potentialWrappedCode !== null) {
code = potentialWrappedCode;
wrappedCmd = true;
awaitPromise = true;
try {
const potentialWrappedCode = processTopLevelAwait(code);
if (potentialWrappedCode !== null) {
code = potentialWrappedCode;
wrappedCmd = true;
awaitPromise = true;
}
} catch (e) {
decorateErrorStack(e);
err = e;
}
}

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

let parentURL;
try {
const { pathToFileURL } = require('url');
// Adding `/repl` prevents dynamic imports from loading relative
// to the parent of `process.cwd()`.
parentURL = pathToFileURL(path.join(process.cwd(), 'repl')).href;
} catch {
}
while (true) {
if (err === null) {
let parentURL;
try {
if (self.replMode === module.exports.REPL_MODE_STRICT &&
!RegExpPrototypeTest(/^\s*$/, code)) {
// "void 0" keeps the repl from returning "use strict" as the result
// value for statements and declarations that don't return a value.
code = `'use strict'; void 0;\n${code}`;
}
script = vm.createScript(code, {
filename: file,
displayErrors: true,
importModuleDynamically: async (specifier) => {
return asyncESM.ESMLoader.import(specifier, parentURL);
const { pathToFileURL } = require('url');
// Adding `/repl` prevents dynamic imports from loading relative
// to the parent of `process.cwd()`.
parentURL = pathToFileURL(path.join(process.cwd(), 'repl')).href;
} catch {
}
while (true) {
try {
if (self.replMode === module.exports.REPL_MODE_STRICT &&
!RegExpPrototypeTest(/^\s*$/, code)) {
// "void 0" keeps the repl from returning "use strict" as the result
// value for statements and declarations that don't return a value.
code = `'use strict'; void 0;\n${code}`;
}
});
} catch (e) {
debug('parse error %j', code, e);
if (wrappedCmd) {
// Unwrap and try again
wrappedCmd = false;
awaitPromise = false;
code = input;
wrappedErr = e;
continue;
script = vm.createScript(code, {
filename: file,
displayErrors: true,
importModuleDynamically: async (specifier) => {
return asyncESM.ESMLoader.import(specifier, parentURL);
}
});
} catch (e) {
debug('parse error %j', code, e);
if (wrappedCmd) {
// Unwrap and try again
wrappedCmd = false;
awaitPromise = false;
code = input;
wrappedErr = e;
continue;
}
// Preserve original error for wrapped command
const error = wrappedErr || e;
if (isRecoverableError(error, code))
err = new Recoverable(error);
else
err = error;
}
// Preserve original error for wrapped command
const error = wrappedErr || e;
if (isRecoverableError(error, code))
err = new Recoverable(error);
else
err = error;
break;
}
break;
}

// This will set the values from `savedRegExMatches` to corresponding
Expand Down
10 changes: 10 additions & 0 deletions test/parallel/test-repl-top-level-await.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,16 @@ async function ordinaryTests() {
'undefined',
],
],
['await Promise..resolve()',
[
'await Promise..resolve()\r',
'Uncaught SyntaxError: ',
'await Promise..resolve()',
' ^',
'',
'Unexpected token \'.\'',
],
],
];

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