Skip to content

Commit

Permalink
repl: ensure correct syntax err for await parsing
Browse files Browse the repository at this point in the history
PR-URL: #39154
Reviewed-By: Anna Henningsen <anna@addaleax.net>
  • Loading branch information
guybedford authored and targos committed Sep 4, 2021
1 parent 761dafa commit 9966449
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 45 deletions.
42 changes: 39 additions & 3 deletions lib/internal/repl/await.js
Expand Up @@ -7,10 +7,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 @@ -79,13 +88,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
Expand Up @@ -431,59 +431,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
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

0 comments on commit 9966449

Please sign in to comment.