From 4294e10163f20b04ee2c8195eb8f462863bb09ae Mon Sep 17 00:00:00 2001 From: Xuguang Mei Date: Thu, 28 Aug 2025 01:32:01 +0800 Subject: [PATCH] repl: add isValidParentheses check before wrap input PR-URL: https://github.com/nodejs/node/pull/59607 Reviewed-By: Ruben Bridgewater Reviewed-By: Antoine du Hamel --- lib/internal/repl/utils.js | 22 +++++++- lib/repl.js | 4 +- test/parallel/test-repl-preview.js | 86 +++++++++++++++++++++++++++++- test/parallel/test-repl.js | 13 +++++ 4 files changed, 121 insertions(+), 4 deletions(-) diff --git a/lib/internal/repl/utils.js b/lib/internal/repl/utils.js index e0327ee5655af8..6c69a4f1490ce3 100644 --- a/lib/internal/repl/utils.js +++ b/lib/internal/repl/utils.js @@ -292,7 +292,7 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { function getInputPreview(input, callback) { // For similar reasons as `defaultEval`, wrap expressions starting with a // curly brace with parenthesis. - if (!wrapped && input[0] === '{' && input[input.length - 1] !== ';') { + if (!wrapped && input[0] === '{' && input[input.length - 1] !== ';' && isValidSyntax(input)) { input = `(${input})`; wrapped = true; } @@ -737,6 +737,25 @@ function setupReverseSearch(repl) { const startsWithBraceRegExp = /^\s*{/; const endsWithSemicolonRegExp = /;\s*$/; +function isValidSyntax(input) { + try { + AcornParser.parse(input, { + ecmaVersion: 'latest', + allowAwaitOutsideFunction: true, + }); + return true; + } catch { + try { + AcornParser.parse(`_=${input}`, { + ecmaVersion: 'latest', + allowAwaitOutsideFunction: true, + }); + return true; + } catch { + return false; + } + } +} /** * Checks if some provided code represents an object literal. @@ -759,4 +778,5 @@ module.exports = { setupPreview, setupReverseSearch, isObjectLiteral, + isValidSyntax, }; diff --git a/lib/repl.js b/lib/repl.js index 0557affb4693f6..30a11a167e1288 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -172,6 +172,7 @@ const { setupPreview, setupReverseSearch, isObjectLiteral, + isValidSyntax, } = require('internal/repl/utils'); const { constants: { @@ -440,7 +441,7 @@ function REPLServer(prompt, let awaitPromise = false; const input = code; - if (isObjectLiteral(code)) { + if (isObjectLiteral(code) && isValidSyntax(code)) { // Add parentheses to make sure `code` is parsed as an expression code = `(${StringPrototypeTrim(code)})\n`; wrappedCmd = true; @@ -1859,6 +1860,7 @@ module.exports = { REPL_MODE_SLOPPY, REPL_MODE_STRICT, Recoverable, + isValidSyntax, }; ObjectDefineProperty(module.exports, 'builtinModules', { diff --git a/test/parallel/test-repl-preview.js b/test/parallel/test-repl-preview.js index 6eb2a169918a51..7518673ce2e8b4 100644 --- a/test/parallel/test-repl-preview.js +++ b/test/parallel/test-repl-preview.js @@ -157,6 +157,83 @@ async function tests(options) { '\x1B[90m1\x1B[39m\x1B[12G\x1B[1A\x1B[1B\x1B[2K\x1B[1A\r', '\x1B[33m1\x1B[39m', ] + }, { + input: 'aaaa', + noPreview: 'Uncaught ReferenceError: aaaa is not defined', + preview: [ + 'aaaa\r', + 'Uncaught ReferenceError: aaaa is not defined', + ] + }, { + input: '/0', + noPreview: '/0', + preview: [ + '/0\r', + '/0', + '^', + '', + 'Uncaught SyntaxError: Invalid regular expression: missing /', + ] + }, { + input: '{})', + noPreview: '{})', + preview: [ + '{})\r', + '{})', + ' ^', + '', + "Uncaught SyntaxError: Unexpected token ')'", + ], + }, { + input: "{ a: '{' }", + noPreview: "{ a: \x1B[32m'{'\x1B[39m }", + preview: [ + "{ a: '{' }\r", + "{ a: \x1B[32m'{'\x1B[39m }", + ], + }, { + input: "{'{':0}", + noPreview: "{ \x1B[32m'{'\x1B[39m: \x1B[33m0\x1B[39m }", + preview: [ + "{'{':0}", + "\x1B[90m{ '{': 0 }\x1B[39m\x1B[15G\x1B[1A\x1B[1B\x1B[2K\x1B[1A\r", + "{ \x1B[32m'{'\x1B[39m: \x1B[33m0\x1B[39m }", + ], + }, { + input: '{[Symbol.for("{")]: 0 }', + noPreview: '{ [\x1B[32mSymbol({)\x1B[39m]: \x1B[33m0\x1B[39m }', + preview: [ + '{[Symbol.for("{")]: 0 }\r', + '{ [\x1B[32mSymbol({)\x1B[39m]: \x1B[33m0\x1B[39m }', + ], + }, { + input: '{},{}', + noPreview: '{}', + preview: [ + '{},{}', + '\x1B[90m{}\x1B[39m\x1B[13G\x1B[1A\x1B[1B\x1B[2K\x1B[1A\r', + '{}', + ], + }, { + input: '{} //', + noPreview: 'repl > ', + preview: [ + '{} //\r', + ], + }, { + input: '{} //;', + noPreview: 'repl > ', + preview: [ + '{} //;\r', + ], + }, { + input: '{throw 0}', + noPreview: 'Uncaught \x1B[33m0\x1B[39m', + preview: [ + '{throw 0}', + '\x1B[90m0\x1B[39m\x1B[17G\x1B[1A\x1B[1B\x1B[2K\x1B[1A\r', + 'Uncaught \x1B[33m0\x1B[39m', + ], }]; const hasPreview = repl.terminal && @@ -177,8 +254,13 @@ async function tests(options) { assert.deepStrictEqual(lines, preview); } else { assert.ok(lines[0].includes(noPreview), lines.map(inspect)); - if (preview.length !== 1 || preview[0] !== `${input}\r`) - assert.strictEqual(lines.length, 2); + if (preview.length !== 1 || preview[0] !== `${input}\r`) { + if (preview[preview.length - 1].includes('Uncaught SyntaxError')) { + assert.strictEqual(lines.length, 5); + } else { + assert.strictEqual(lines.length, 2); + } + } } } } diff --git a/test/parallel/test-repl.js b/test/parallel/test-repl.js index 16cead7c7251dc..6a02bb6563e293 100644 --- a/test/parallel/test-repl.js +++ b/test/parallel/test-repl.js @@ -328,6 +328,19 @@ const errorTests = [ expect: '[Function (anonymous)]' }, // Multiline object + { + send: '{}),({}', + expect: '... ', + }, + { + send: '}', + expect: [ + '{}),({}', + kArrow, + '', + /^Uncaught SyntaxError: /, + ] + }, { send: '{ a: ', expect: '... '