From 99664494ffc2465e81d9bc49cb176ea745e59de7 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Fri, 25 Jun 2021 11:12:21 -0700 Subject: [PATCH] repl: ensure correct syntax err for await parsing PR-URL: https://github.com/nodejs/node/pull/39154 Reviewed-By: Anna Henningsen --- lib/internal/repl/await.js | 42 +++++++++- lib/repl.js | 91 ++++++++++++---------- test/parallel/test-repl-top-level-await.js | 10 +++ 3 files changed, 98 insertions(+), 45 deletions(-) diff --git a/lib/internal/repl/await.js b/lib/internal/repl/await.js index f42885fec6843c..ab2af85a7a0157 100644 --- a/lib/internal/repl/await.js +++ b/lib/internal/repl/await.js @@ -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 = { @@ -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 = { diff --git a/lib/repl.js b/lib/repl.js index b6884d5b7160d5..4a08036a44d010 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -431,11 +431,16 @@ 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; } } @@ -443,47 +448,49 @@ function REPLServer(prompt, 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 diff --git a/test/parallel/test-repl-top-level-await.js b/test/parallel/test-repl-top-level-await.js index 39227d4f8bad29..319633838bd358 100644 --- a/test/parallel/test-repl-top-level-await.js +++ b/test/parallel/test-repl-top-level-await.js @@ -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) {