From f146e9cd3cd22ea63ae924056fee374bceaf8052 Mon Sep 17 00:00:00 2001 From: jenthone Date: Mon, 6 Mar 2023 23:59:20 +0700 Subject: [PATCH 01/11] repl: add isValidParentheses check before wrap input --- lib/internal/repl/utils.js | 107 +++++++++++++++++---------- lib/repl.js | 147 +++++++++++++++++++------------------ 2 files changed, 141 insertions(+), 113 deletions(-) diff --git a/lib/internal/repl/utils.js b/lib/internal/repl/utils.js index b17635e3d42877..78b71695e20130 100644 --- a/lib/internal/repl/utils.js +++ b/lib/internal/repl/utils.js @@ -72,7 +72,7 @@ function isRecoverableError(e, code) { // here as the point is to test for potentially valid but incomplete // expressions. if (RegExpPrototypeExec(/^\s*\{/, code) !== null && - isRecoverableError(e, `(${code}`)) + isRecoverableError(e, `(${code}`)) return true; let recoverable = false; @@ -115,10 +115,10 @@ function isRecoverableError(e, code) { case 'Unterminated string constant': { const token = StringPrototypeSlice(this.input, - this.lastTokStart, this.pos); + this.lastTokStart, this.pos); // See https://www.ecma-international.org/ecma-262/#sec-line-terminators if (RegExpPrototypeExec(/\\(?:\r\n?|\n|\u2028|\u2029)$/, - token) !== null) { + token) !== null) { recoverable = true; } } @@ -146,7 +146,7 @@ function isRecoverableError(e, code) { function setupPreview(repl, contextSymbol, bufferSymbol, active) { // Simple terminals can't handle previews. if (process.env.TERM === 'dumb' || !active) { - return { showPreview() {}, clearPreview() {} }; + return { showPreview() { }, clearPreview() { } }; } let inputPreview = null; @@ -171,7 +171,7 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { function isCursorAtInputEnd() { const { cursorPos, displayPos } = getPreviewPos(); return cursorPos.rows === displayPos.rows && - cursorPos.cols === displayPos.cols; + cursorPos.cols === displayPos.cols; } const clearPreview = (key) => { @@ -212,9 +212,9 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { escaped = repl.line; } } else if ((key.name === 'return' || key.name === 'enter') && - !key.meta && - escaped !== repl.line && - isCursorAtInputEnd()) { + !key.meta && + escaped !== repl.line && + isCursorAtInputEnd()) { repl._insertString(completionPreview); } } @@ -289,11 +289,11 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { function isInStrictMode(repl) { return repl.replMode === REPL_MODE_STRICT || ArrayPrototypeIncludes( ArrayPrototypeMap(process.execArgv, - (e) => StringPrototypeReplaceAll( - StringPrototypeToLowerCase(e), - '_', - '-', - )), + (e) => StringPrototypeReplaceAll( + StringPrototypeToLowerCase(e), + '_', + '-', + )), '--use-strict'); } @@ -301,7 +301,10 @@ 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 (StringPrototypeStartsWith(input, '{') && + !StringPrototypeEndsWith(input, ';') && + isValidParentheses(input) && + !wrapped) { input = `(${input})`; wrapped = true; } @@ -319,16 +322,16 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { const { result } = preview; if (result.value !== undefined) { callback(null, inspect(result.value, previewOptions)); - // Ignore EvalErrors, SyntaxErrors and ReferenceErrors. It is not clear - // where they came from and if they are recoverable or not. Other errors - // may be inspected. + // Ignore EvalErrors, SyntaxErrors and ReferenceErrors. It is not clear + // where they came from and if they are recoverable or not. Other errors + // may be inspected. } else if (preview.exceptionDetails && - (result.className === 'EvalError' || - result.className === 'SyntaxError' || - // Report ReferenceError in case the strict mode is active - // for input that has no completions. - (result.className === 'ReferenceError' && - (hasCompletions || !isInStrictMode(repl))))) { + (result.className === 'EvalError' || + result.className === 'SyntaxError' || + // Report ReferenceError in case the strict mode is active + // for input that has no completions. + (result.className === 'ReferenceError' && + (hasCompletions || !isInStrictMode(repl))))) { callback(null, null); } else if (result.objectId) { // The writer options might change and have influence on the inspect @@ -368,9 +371,9 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { const showPreview = (showCompletion = true) => { // Prevent duplicated previews after a refresh or in a multiline command. if (inputPreview !== null || - repl[kIsMultiline] || - !repl.isCompletionEnabled || - !process.features.inspector) { + repl[kIsMultiline] || + !repl.isCompletionEnabled || + !process.features.inspector) { return; } @@ -413,7 +416,7 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { // Do not preview `undefined` if colors are deactivated or explicitly // requested. if (inspected === 'undefined' && - (!repl.useColors || repl.ignoreUndefined)) { + (!repl.useColors || repl.ignoreUndefined)) { return; } @@ -427,7 +430,7 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { // Support unicode characters of width other than one by checking the // actual width. if (inspected.length * 2 >= maxColumns && - getStringWidth(inspected) > maxColumns) { + getStringWidth(inspected) > maxColumns) { maxColumns -= 4 + (repl.useColors ? 0 : 3); let res = ''; for (const char of new SafeStringIterator(inspected)) { @@ -466,8 +469,8 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { let previewLine = line; if (completionPreview !== null && - isCursorAtInputEnd() && - escaped !== repl.line) { + isCursorAtInputEnd() && + escaped !== repl.line) { previewLine += completionPreview; } @@ -501,8 +504,8 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { const currentCursor = repl.cursor; originalMoveCursor(dx); if (currentCursor + dx > repl.line.length && - typeof repl.completer === 'function' && - insertCompletionPreview) { + typeof repl.completer === 'function' && + insertCompletionPreview) { const insertPreview = true; showCompletionPreview(repl.line, insertPreview); } @@ -597,7 +600,7 @@ function setupReverseSearch(repl) { // Match not found. if (cursor === -1) { goToNextHistoryIndex(); - // Match found. + // Match found. } else { if (repl.useColors) { const start = StringPrototypeSlice(entry, 0, cursor); @@ -610,7 +613,7 @@ function setupReverseSearch(repl) { // Explicitly go to the next history item in case no further matches are // possible with the current entry. if ((dir === 'r' && cursor === 0) || - (dir === 's' && entry.length === cursor + input.length)) { + (dir === 's' && entry.length === cursor + input.length)) { goToNextHistoryIndex(); } return; @@ -723,7 +726,7 @@ function setupReverseSearch(repl) { } else if (key.ctrl && checkAndSetDirectionKey(key.name)) { search(); } else if (key.name === 'backspace' || - (key.ctrl && (key.name === 'h' || key.name === 'w'))) { + (key.ctrl && (key.name === 'h' || key.name === 'w'))) { reset(StringPrototypeSlice(input, 0, input.length - 1)); search(); // Special handle + c and escape. Those should only cancel the @@ -735,11 +738,11 @@ function setupReverseSearch(repl) { // End search in case either enter is pressed or if any non-reverse-search // key (combination) is pressed. } else if (key.ctrl || - key.meta || - key.name === 'return' || - key.name === 'enter' || - typeof string !== 'string' || - string === '') { + key.meta || + key.name === 'return' || + key.name === 'enter' || + typeof string !== 'string' || + string === '') { reset(); repl[kSubstringSearch] = ''; } else { @@ -768,6 +771,29 @@ function isObjectLiteral(code) { RegExpPrototypeExec(endsWithSemicolonRegExp, code) === null; } +function isValidParentheses(input) { + const stack = []; + const pairs = { + '(': ')', + '[': ']', + '{': '}', + }; + + for (let i = 0; i < input.length; i++) { + const char = input[i]; + + if (pairs[char]) { + stack.push(char); + } else if (char === ')' || char === ']' || char === '}') { + if (pairs[stack.pop()] !== char) { + return false; + } + } + } + + return stack.length === 0; +} + module.exports = { REPL_MODE_SLOPPY: Symbol('repl-sloppy'), REPL_MODE_STRICT, @@ -776,4 +802,5 @@ module.exports = { setupPreview, setupReverseSearch, isObjectLiteral, + isValidParentheses, }; diff --git a/lib/repl.js b/lib/repl.js index 443971df63b0e8..0e302a982d7cda 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -177,6 +177,7 @@ const { setupPreview, setupReverseSearch, isObjectLiteral, + isValidParentheses, } = require('internal/repl/utils'); const { constants: { @@ -266,11 +267,11 @@ const toDynamicImport = (codeLine) => { }; function REPLServer(prompt, - stream, - eval_, - useGlobal, - ignoreUndefined, - replMode) { + stream, + eval_, + useGlobal, + ignoreUndefined, + replMode) { if (!(this instanceof REPLServer)) { return deprecateInstantiation(REPLServer, 'DEP0185', prompt, stream, eval_, useGlobal, ignoreUndefined, replMode); } @@ -317,15 +318,15 @@ function REPLServer(prompt, __proto__: null, get: pendingDeprecation ? deprecate(() => this.input, - 'repl.inputStream and repl.outputStream are deprecated. ' + - 'Use repl.input and repl.output instead', - 'DEP0141') : + 'repl.inputStream and repl.outputStream are deprecated. ' + + 'Use repl.input and repl.output instead', + 'DEP0141') : () => this.input, set: pendingDeprecation ? deprecate((val) => this.input = val, - 'repl.inputStream and repl.outputStream are deprecated. ' + - 'Use repl.input and repl.output instead', - 'DEP0141') : + 'repl.inputStream and repl.outputStream are deprecated. ' + + 'Use repl.input and repl.output instead', + 'DEP0141') : (val) => this.input = val, enumerable: false, configurable: true, @@ -334,15 +335,15 @@ function REPLServer(prompt, __proto__: null, get: pendingDeprecation ? deprecate(() => this.output, - 'repl.inputStream and repl.outputStream are deprecated. ' + - 'Use repl.input and repl.output instead', - 'DEP0141') : + 'repl.inputStream and repl.outputStream are deprecated. ' + + 'Use repl.input and repl.output instead', + 'DEP0141') : () => this.output, set: pendingDeprecation ? deprecate((val) => this.output = val, - 'repl.inputStream and repl.outputStream are deprecated. ' + - 'Use repl.input and repl.output instead', - 'DEP0141') : + 'repl.inputStream and repl.outputStream are deprecated. ' + + 'Use repl.input and repl.output instead', + 'DEP0141') : (val) => this.output = val, enumerable: false, configurable: true, @@ -380,9 +381,9 @@ function REPLServer(prompt, // instance and that could trigger the `MaxListenersExceededWarning`. process.prependListener('newListener', (event, listener) => { if (event === 'uncaughtException' && - process.domain && - listener.name !== 'domainUncaughtExceptionClear' && - domainSet.has(process.domain)) { + process.domain && + listener.name !== 'domainUncaughtExceptionClear' && + domainSet.has(process.domain)) { // Throw an error so that the event will not be added and the current // domain takes over. That way the user is notified about the error // and the current code evaluation is stopped, just as any other code @@ -399,8 +400,8 @@ function REPLServer(prompt, const savedRegExMatches = ['', '', '', '', '', '', '', '', '', '']; const sep = '\u0000\u0000\u0000'; const regExMatcher = new RegExp(`^${sep}(.*)${sep}(.*)${sep}(.*)${sep}(.*)` + - `${sep}(.*)${sep}(.*)${sep}(.*)${sep}(.*)` + - `${sep}(.*)$`); + `${sep}(.*)${sep}(.*)${sep}(.*)${sep}(.*)` + + `${sep}(.*)$`); eval_ ||= defaultEval; @@ -464,8 +465,8 @@ function REPLServer(prompt, async function importModuleDynamically(specifier, _, importAttributes, phase) { const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader(); return cascadedLoader.import(specifier, parentURL, importAttributes, - phase === 'evaluation' ? cascadedLoader.kEvaluationPhase : - cascadedLoader.kSourcePhase); + phase === 'evaluation' ? cascadedLoader.kEvaluationPhase : + cascadedLoader.kSourcePhase); } // `experimentalREPLAwait` is set to true by default. // Shall be false in case `--no-experimental-repl-await` flag is used. @@ -521,7 +522,7 @@ function REPLServer(prompt, while (true) { try { if (self.replMode === module.exports.REPL_MODE_STRICT && - RegExpPrototypeExec(/^\s*$/, code) === null) { + RegExpPrototypeExec(/^\s*$/, code) === null) { // "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}`; @@ -561,7 +562,7 @@ function REPLServer(prompt, // This will set the values from `savedRegExMatches` to corresponding // predefined RegExp properties `RegExp.$1`, `RegExp.$2` ... `RegExp.$9` RegExpPrototypeExec(regExMatcher, - ArrayPrototypeJoin(savedRegExMatches, sep)); + ArrayPrototypeJoin(savedRegExMatches, sep)); let finished = false; function finishExecution(err, result) { @@ -743,7 +744,7 @@ function REPLServer(prompt, } if (options[kStandaloneREPL] && - process.listenerCount('uncaughtException') !== 0) { + process.listenerCount('uncaughtException') !== 0) { process.nextTick(() => { process.emit('uncaughtException', e); self.clearBufferedCommand(); @@ -762,7 +763,7 @@ function REPLServer(prompt, errStack = ''; ArrayPrototypeForEach(lines, (line) => { if (!matched && - RegExpPrototypeExec(/^\[?([A-Z][a-z0-9_]*)*Error/, line) !== null) { + RegExpPrototypeExec(/^\[?([A-Z][a-z0-9_]*)*Error/, line) !== null) { errStack += writer.options.breakLength >= line.length ? `Uncaught ${line}` : `Uncaught:\n${line}`; @@ -790,7 +791,7 @@ function REPLServer(prompt, function completer(text, cb) { ReflectApply(complete, self, - [text, self.editorMode ? self.completeOnEditorMode(cb) : cb]); + [text, self.editorMode ? self.completeOnEditorMode(cb) : cb]); } // All the parameters in the object are defining the "input" param of the @@ -912,8 +913,8 @@ function REPLServer(prompt, // display next prompt and return. if (trimmedCmd) { if (StringPrototypeCharAt(trimmedCmd, 0) === '.' && - StringPrototypeCharAt(trimmedCmd, 1) !== '.' && - NumberIsNaN(NumberParseFloat(trimmedCmd))) { + StringPrototypeCharAt(trimmedCmd, 1) !== '.' && + NumberIsNaN(NumberParseFloat(trimmedCmd))) { const matches = RegExpPrototypeExec(/^\.([^\s]+)\s*(.*)$/, trimmedCmd); const keyword = matches?.[1]; const rest = matches?.[2]; @@ -938,12 +939,12 @@ function REPLServer(prompt, ReflectApply(_memory, self, [cmd]); if (e && !self[kBufferedCommandSymbol] && - StringPrototypeStartsWith(StringPrototypeTrim(cmd), 'npm ') && - !(e instanceof Recoverable) + StringPrototypeStartsWith(StringPrototypeTrim(cmd), 'npm ') && + !(e instanceof Recoverable) ) { self.output.write('npm should be run outside of the ' + - 'Node.js REPL, in your normal shell.\n' + - '(Press Ctrl+D to exit.)\n'); + 'Node.js REPL, in your normal shell.\n' + + '(Press Ctrl+D to exit.)\n'); self.displayPrompt(); return; } @@ -971,11 +972,11 @@ function REPLServer(prompt, // If we got any output - print it (if no error) if (!e && - // When an invalid REPL command is used, error message is printed - // immediately. We don't have to print anything else. So, only when - // the second argument to this function is there, print it. - arguments.length === 2 && - (!self.ignoreUndefined || ret !== undefined)) { + // When an invalid REPL command is used, error message is printed + // immediately. We don't have to print anything else. So, only when + // the second argument to this function is there, print it. + arguments.length === 2 && + (!self.ignoreUndefined || ret !== undefined)) { if (!self.underscoreAssigned) { self.last = ret; } @@ -1021,13 +1022,13 @@ function REPLServer(prompt, key ||= {}; if (paused && !(self.breakEvalOnSigint && key.ctrl && key.name === 'c')) { ArrayPrototypePush(pausedBuffer, - ['key', [d, key], self.isCompletionEnabled]); + ['key', [d, key], self.isCompletionEnabled]); return; } if (!self.editorMode || !self.terminal) { // Before exiting, make sure to clear the line. if (key.ctrl && key.name === 'd' && - self.cursor === 0 && self.line.length === 0) { + self.cursor === 0 && self.line.length === 0) { self.clearLine(); } clearPreview(key); @@ -1114,7 +1115,7 @@ REPLServer.prototype.close = function close() { ); }; -REPLServer.prototype.createContext = function() { +REPLServer.prototype.createContext = function () { let context; if (this.useGlobal) { context = globalThis; @@ -1133,10 +1134,10 @@ REPLServer.prototype.createContext = function() { // Only set properties that do not already exist as a global builtin. if (!globalBuiltins.has(name)) { ObjectDefineProperty(context, name, - { - __proto__: null, - ...ObjectGetOwnPropertyDescriptor(globalThis, name), - }); + { + __proto__: null, + ...ObjectGetOwnPropertyDescriptor(globalThis, name), + }); } }); context.global = context; @@ -1170,7 +1171,7 @@ REPLServer.prototype.createContext = function() { return context; }; -REPLServer.prototype.resetContext = function() { +REPLServer.prototype.resetContext = function () { this.context = this.createContext(); this.underscoreAssigned = false; this.underscoreErrAssigned = false; @@ -1209,7 +1210,7 @@ REPLServer.prototype.resetContext = function() { this.emit('reset', this.context); }; -REPLServer.prototype.displayPrompt = function(preserveCursor) { +REPLServer.prototype.displayPrompt = function (preserveCursor) { let prompt = this._initialPrompt; if (this[kBufferedCommandSymbol].length) { prompt = kMultilinePrompt.description; @@ -1291,7 +1292,7 @@ function getGlobalLexicalScopeNames(contextId) { }, () => []); } -REPLServer.prototype.complete = function() { +REPLServer.prototype.complete = function () { ReflectApply(this.completer, this, arguments); }; @@ -1363,7 +1364,7 @@ function complete(line, callback) { const subdir = match[2] || ''; const extensions = ObjectKeys(CJSModule._extensions); const indexes = ArrayPrototypeMap(extensions, - (extension) => `index${extension}`); + (extension) => `index${extension}`); ArrayPrototypePush(indexes, 'package.json', 'index'); group = []; @@ -1386,7 +1387,7 @@ function complete(line, callback) { const dirents = gracefulReaddir(dir, { withFileTypes: true }) || []; ArrayPrototypeForEach(dirents, (dirent) => { if (RegExpPrototypeExec(versionedFileNamesRe, dirent.name) !== null || - dirent.name === '.npm') { + dirent.name === '.npm') { // Exclude versioned names that 'npm' installs. return; } @@ -1394,7 +1395,7 @@ function complete(line, callback) { const base = StringPrototypeSlice(dirent.name, 0, -extension.length); if (!dirent.isDirectory()) { if (StringPrototypeIncludes(extensions, extension) && - (!subdir || base !== 'index')) { + (!subdir || base !== 'index')) { ArrayPrototypePush(group, `${subdir}${base}`); } return; @@ -1447,7 +1448,7 @@ function complete(line, callback) { ArrayPrototypeForEach(dirents, (dirent) => { const { name } = dirent; if (RegExpPrototypeExec(versionedFileNamesRe, name) !== null || - name === '.npm') { + name === '.npm') { // Exclude versioned names that 'npm' installs. return; } @@ -1480,10 +1481,10 @@ function complete(line, callback) { ArrayPrototypePush(completionGroups, _builtinLibs, nodeSchemeBuiltinLibs); } else if ((match = RegExpPrototypeExec(fsAutoCompleteRE, line)) !== null && - this.allowBlockingCompletions) { + this.allowBlockingCompletions) { ({ 0: completionGroups, 1: completeOn } = completeFSFunctions(match)); } else if (line.length === 0 || - RegExpPrototypeExec(/\w|\.|\$/, line[line.length - 1]) !== null) { + RegExpPrototypeExec(/\w|\.|\$/, line[line.length - 1]) !== null) { const completeTarget = line.length === 0 ? line : findExpressionCompleteTarget(line); if (line.length !== 0 && !completeTarget) { @@ -1504,11 +1505,11 @@ function complete(line, callback) { if (!expr) { // Get global vars synchronously ArrayPrototypePush(completionGroups, - getGlobalLexicalScopeNames(this[kContextId])); + getGlobalLexicalScopeNames(this[kContextId])); let contextProto = this.context; while ((contextProto = ObjectGetPrototypeOf(contextProto)) !== null) { ArrayPrototypePush(completionGroups, - filteredOwnPropertyNames(contextProto)); + filteredOwnPropertyNames(contextProto)); } const contextOwnNames = filteredOwnPropertyNames(this.context); if (!this.useGlobal) { @@ -1545,8 +1546,8 @@ function complete(line, callback) { this.context, (includes) => { if (includes) { - // The expression involves proxies or getters, meaning that it - // can trigger side-effectful behaviors, so bail out + // The expression involves proxies or getters, meaning that it + // can trigger side-effectful behaviors, so bail out return completionGroupsLoaded(); } @@ -1575,18 +1576,18 @@ function complete(line, callback) { p = ObjectGetPrototypeOf(p); } } catch { - // Maybe a Proxy object without `getOwnPropertyNames` trap. - // We simply ignore it here, as we don't want to break the - // autocompletion. Fixes the bug - // https://github.com/nodejs/node/issues/2119 + // Maybe a Proxy object without `getOwnPropertyNames` trap. + // We simply ignore it here, as we don't want to break the + // autocompletion. Fixes the bug + // https://github.com/nodejs/node/issues/2119 } if (memberGroups.length) { expr += chaining; ArrayPrototypeForEach(memberGroups, (group) => { ArrayPrototypePush(completionGroups, - ArrayPrototypeMap(group, - (member) => `${expr}${member}`)); + ArrayPrototypeMap(group, + (member) => `${expr}${member}`)); }); filter &&= `${expr}${filter}`; } @@ -1924,7 +1925,7 @@ REPLServer.prototype.completeOnEditorMode = (callback) => (err, results) => { callback(null, [result, completeOn]); }; -REPLServer.prototype.defineCommand = function(keyword, cmd) { +REPLServer.prototype.defineCommand = function (keyword, cmd) { if (typeof cmd === 'function') { cmd = { action: cmd }; } else { @@ -2029,7 +2030,7 @@ function _turnOffEditorMode(repl) { function defineDefaultCommands(repl) { repl.defineCommand('break', { help: 'Sometimes you get stuck, this gets you out', - action: function() { + action: function () { this.clearBufferedCommand(); this.displayPrompt(); }, @@ -2043,7 +2044,7 @@ function defineDefaultCommands(repl) { } repl.defineCommand('clear', { help: clearMessage, - action: function() { + action: function () { this.clearBufferedCommand(); if (!this.useGlobal) { this.output.write('Clearing context...\n'); @@ -2055,14 +2056,14 @@ function defineDefaultCommands(repl) { repl.defineCommand('exit', { help: 'Exit the REPL', - action: function() { + action: function () { this.close(); }, }); repl.defineCommand('help', { help: 'Print this help message', - action: function() { + action: function () { const names = ArrayPrototypeSort(ObjectKeys(this.commands)); const longestNameLength = MathMaxApply( ArrayPrototypeMap(names, (name) => name.length), @@ -2082,7 +2083,7 @@ function defineDefaultCommands(repl) { repl.defineCommand('save', { help: 'Save all evaluated commands in this REPL session to a file', - action: function(file) { + action: function (file) { try { if (file === '') { throw new ERR_MISSING_ARGS('file'); @@ -2102,7 +2103,7 @@ function defineDefaultCommands(repl) { repl.defineCommand('load', { help: 'Load JS from a file into the REPL session', - action: function(file) { + action: function (file) { try { if (file === '') { throw new ERR_MISSING_ARGS('file'); From 334b186b9323c0446b0fd1c5d855bebe5fcea754 Mon Sep 17 00:00:00 2001 From: jenthone Date: Tue, 7 Mar 2023 09:14:46 +0700 Subject: [PATCH 02/11] test: add Uncaught ReferenceError case --- test/parallel/test-repl-preview.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/parallel/test-repl-preview.js b/test/parallel/test-repl-preview.js index 06a044f5a11a13..80cd1469ca24bd 100644 --- a/test/parallel/test-repl-preview.js +++ b/test/parallel/test-repl-preview.js @@ -157,6 +157,13 @@ 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' + ] }]; const hasPreview = repl.terminal && From e367980ab4eb5a651761ae76c52ed4efd179d70c Mon Sep 17 00:00:00 2001 From: jenthone Date: Tue, 7 Mar 2023 12:39:44 +0700 Subject: [PATCH 03/11] test: add Uncaught SyntaxError case --- test/parallel/test-repl-preview.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/test/parallel/test-repl-preview.js b/test/parallel/test-repl-preview.js index 80cd1469ca24bd..ae09a0454479c4 100644 --- a/test/parallel/test-repl-preview.js +++ b/test/parallel/test-repl-preview.js @@ -164,6 +164,16 @@ async function tests(options) { 'aaaa\r', 'Uncaught ReferenceError: aaaa is not defined' ] + }, { + input: '/0', + noPreview: '/0', + preview: [ + '/0\r', + '/0', + '^', + '', + 'Uncaught SyntaxError: Invalid regular expression: missing /', + ] }]; const hasPreview = repl.terminal && @@ -184,8 +194,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')) { // Syntax error + assert.strictEqual(lines.length, 5); + } else { + assert.strictEqual(lines.length, 2); + } + } } } } From 73e23c834f9a2884474ace1f76de5c39cd3f37f8 Mon Sep 17 00:00:00 2001 From: jenthone Date: Tue, 7 Mar 2023 12:44:57 +0700 Subject: [PATCH 04/11] test: add SyntaxError: Unexpected token case --- test/parallel/test-repl-preview.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/test/parallel/test-repl-preview.js b/test/parallel/test-repl-preview.js index ae09a0454479c4..d7e446692ae617 100644 --- a/test/parallel/test-repl-preview.js +++ b/test/parallel/test-repl-preview.js @@ -162,7 +162,7 @@ async function tests(options) { noPreview: 'Uncaught ReferenceError: aaaa is not defined', preview: [ 'aaaa\r', - 'Uncaught ReferenceError: aaaa is not defined' + 'Uncaught ReferenceError: aaaa is not defined', ] }, { input: '/0', @@ -174,6 +174,16 @@ async function tests(options) { '', 'Uncaught SyntaxError: Invalid regular expression: missing /', ] + }, { + input: '{})', + noPreview: '{})', + preview: [ + '{})\r', + '{})', + ' ^', + '', + "Uncaught SyntaxError: Unexpected token ')'", + ], }]; const hasPreview = repl.terminal && @@ -195,7 +205,7 @@ async function tests(options) { } else { assert.ok(lines[0].includes(noPreview), lines.map(inspect)); if (preview.length !== 1 || preview[0] !== `${input}\r`) { - if (preview[preview.length-1].includes('Uncaught SyntaxError')) { // Syntax error + if (preview[preview.length - 1].includes('Uncaught SyntaxError')) { assert.strictEqual(lines.length, 5); } else { assert.strictEqual(lines.length, 2); From c20260e799507ea763ccc4dbc86c6a499972dec4 Mon Sep 17 00:00:00 2001 From: jenthone Date: Wed, 8 Mar 2023 16:28:39 +0700 Subject: [PATCH 05/11] repl: check valid syntax by using acorn --- lib/internal/repl/utils.js | 36 +++++++++++++++++++++--------------- lib/repl.js | 12 ++++++++++-- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/lib/internal/repl/utils.js b/lib/internal/repl/utils.js index 78b71695e20130..e22dbe46cb48af 100644 --- a/lib/internal/repl/utils.js +++ b/lib/internal/repl/utils.js @@ -303,7 +303,7 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { // curly brace with parenthesis. if (StringPrototypeStartsWith(input, '{') && !StringPrototypeEndsWith(input, ';') && - isValidParentheses(input) && + isValidSyntax(input) && !wrapped) { input = `(${input})`; wrapped = true; @@ -789,18 +789,24 @@ function isValidParentheses(input) { return false; } } - } - - return stack.length === 0; -} + function isValidSyntax(input) { + const Parser = require('internal/deps/acorn/acorn/dist/acorn').Parser; + try { + let result = Parser.parse(input, { ecmaVersion: 'latest' }); + return true; + } catch (e) { + return false; + } + } -module.exports = { - REPL_MODE_SLOPPY: Symbol('repl-sloppy'), - REPL_MODE_STRICT, - isRecoverableError, - kStandaloneREPL: Symbol('kStandaloneREPL'), - setupPreview, - setupReverseSearch, - isObjectLiteral, - isValidParentheses, -}; + module.exports = { + REPL_MODE_SLOPPY: Symbol('repl-sloppy'), + REPL_MODE_STRICT, + isRecoverableError, + kStandaloneREPL: Symbol('kStandaloneREPL'), + setupPreview, + setupReverseSearch, + isObjectLiteral, + isValidParentheses, + isValidSyntax, + }; diff --git a/lib/repl.js b/lib/repl.js index 0e302a982d7cda..33fd38d02901c9 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -178,6 +178,7 @@ const { setupReverseSearch, isObjectLiteral, isValidParentheses, + isValidSyntax, } = require('internal/repl/utils'); const { constants: { @@ -446,8 +447,14 @@ function REPLServer(prompt, let awaitPromise = false; const input = code; - if (isObjectLiteral(code)) { - // Add parentheses to make sure `code` is parsed as an expression + // It's confusing for `{ a : 1 }` to be interpreted as a block + // statement rather than an object literal. So, we first try + // to wrap it in parentheses, so that it will be interpreted as + // an expression. Note that if the above condition changes, + // lib/internal/repl/utils.js needs to be changed to match. + if (RegExpPrototypeExec(/^\s*{/, code) !== null && + RegExpPrototypeExec(/;\s*$/, code) === null && + isValidSyntax(code)) { code = `(${StringPrototypeTrim(code)})\n`; wrappedCmd = true; } @@ -2157,6 +2164,7 @@ module.exports = { REPL_MODE_SLOPPY, REPL_MODE_STRICT, Recoverable, + isValidSyntax, }; ObjectDefineProperty(module.exports, 'builtinModules', { From 94cad394fd494ad98e7a6d2a76bcc0ce38129cd1 Mon Sep 17 00:00:00 2001 From: jenthone Date: Wed, 8 Mar 2023 16:35:43 +0700 Subject: [PATCH 06/11] test: add open bracket case --- lib/internal/repl/utils.js | 78 ++++++++++++++++-------------- test/parallel/test-repl-preview.js | 7 +++ 2 files changed, 50 insertions(+), 35 deletions(-) diff --git a/lib/internal/repl/utils.js b/lib/internal/repl/utils.js index e22dbe46cb48af..abd9ffc0f815bc 100644 --- a/lib/internal/repl/utils.js +++ b/lib/internal/repl/utils.js @@ -769,44 +769,52 @@ const endsWithSemicolonRegExp = /;\s*$/; function isObjectLiteral(code) { return RegExpPrototypeExec(startsWithBraceRegExp, code) !== null && RegExpPrototypeExec(endsWithSemicolonRegExp, code) === null; -} + function isValidSyntax(input) { + const Parser = require('internal/deps/acorn/acorn/dist/acorn').Parser; + try { + Parser.parse(input, { ecmaVersion: 'latest' }); + return true; + } catch { + return false; + } + } -function isValidParentheses(input) { - const stack = []; - const pairs = { - '(': ')', - '[': ']', - '{': '}', - }; + function isValidParentheses(input) { + const stack = []; + const pairs = { + '(': ')', + '[': ']', + '{': '}', + }; - for (let i = 0; i < input.length; i++) { - const char = input[i]; + for (let i = 0; i < input.length; i++) { + const char = input[i]; - if (pairs[char]) { - stack.push(char); - } else if (char === ')' || char === ']' || char === '}') { - if (pairs[stack.pop()] !== char) { - return false; + if (pairs[char]) { + stack.push(char); + } else if (char === ')' || char === ']' || char === '}') { + if (pairs[stack.pop()] !== char) { + return false; + } } - } - function isValidSyntax(input) { - const Parser = require('internal/deps/acorn/acorn/dist/acorn').Parser; - try { - let result = Parser.parse(input, { ecmaVersion: 'latest' }); - return true; - } catch (e) { - return false; + function isValidSyntax(input) { + const Parser = require('internal/deps/acorn/acorn/dist/acorn').Parser; + try { + let result = Parser.parse(input, { ecmaVersion: 'latest' }); + return true; + } catch (e) { + return false; + } } - } - module.exports = { - REPL_MODE_SLOPPY: Symbol('repl-sloppy'), - REPL_MODE_STRICT, - isRecoverableError, - kStandaloneREPL: Symbol('kStandaloneREPL'), - setupPreview, - setupReverseSearch, - isObjectLiteral, - isValidParentheses, - isValidSyntax, - }; + module.exports = { + REPL_MODE_SLOPPY: Symbol('repl-sloppy'), + REPL_MODE_STRICT, + isRecoverableError, + kStandaloneREPL: Symbol('kStandaloneREPL'), + setupPreview, + setupReverseSearch, + isObjectLiteral, + isValidParentheses, + isValidSyntax, + }; diff --git a/test/parallel/test-repl-preview.js b/test/parallel/test-repl-preview.js index d7e446692ae617..f6156511433684 100644 --- a/test/parallel/test-repl-preview.js +++ b/test/parallel/test-repl-preview.js @@ -184,6 +184,13 @@ async function tests(options) { '', "Uncaught SyntaxError: Unexpected token ')'", ], + }, { + input: "{ a: '{' }", + noPreview: "{ a: \x1B[32m'{'\x1B[39m }", + preview: [ + "{ a: '{' }\r", + "{ a: \x1B[32m'{'\x1B[39m }", + ], }]; const hasPreview = repl.terminal && From 13b2c530a7c10c295c95db2c1f3e116d5add9fbe Mon Sep 17 00:00:00 2001 From: jenthone Date: Thu, 9 Mar 2023 09:09:28 +0700 Subject: [PATCH 07/11] repl: try parse again and allowAwaitOutsideFunction --- lib/internal/repl/utils.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lib/internal/repl/utils.js b/lib/internal/repl/utils.js index abd9ffc0f815bc..c09e54014addf9 100644 --- a/lib/internal/repl/utils.js +++ b/lib/internal/repl/utils.js @@ -757,6 +757,26 @@ function setupReverseSearch(repl) { const startsWithBraceRegExp = /^\s*{/; const endsWithSemicolonRegExp = /;\s*$/; +function isValidSyntax(input) { + const Parser = require('internal/deps/acorn/acorn/dist/acorn').Parser; + try { + Parser.parse(input, { + ecmaVersion: 'latest', + allowAwaitOutsideFunction: true, + }); + return true; + } catch { + try { + Parser.parse(`_=${input}`, { + ecmaVersion: 'latest', + allowAwaitOutsideFunction: true, + }); + return true; + } catch { + return false; + } + } +} /** * Checks if some provided code represents an object literal. From 814248a1e7b771834d534e8d3aefcecce7e176d9 Mon Sep 17 00:00:00 2001 From: jenthone Date: Thu, 9 Mar 2023 09:13:31 +0700 Subject: [PATCH 08/11] test: add string-keyed object case --- test/parallel/test-repl-preview.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/parallel/test-repl-preview.js b/test/parallel/test-repl-preview.js index f6156511433684..876005b512eb9d 100644 --- a/test/parallel/test-repl-preview.js +++ b/test/parallel/test-repl-preview.js @@ -191,6 +191,22 @@ async function tests(options) { "{ 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: [ + // eslint-disable-next-line max-len + '{[Sym\x1B[90mbol\x1B[39m\x1B[13G\x1B[0Kb\x1B[90mol\x1B[39m\x1B[14G\x1B[0Ko\x1B[90ml\x1B[39m\x1B[15G\x1B[0Kl.for("{")]: 0 }\r', + '{ [\x1B[32mSymbol({)\x1B[39m]: \x1B[33m0\x1B[39m }', + ], }]; const hasPreview = repl.terminal && From 953874ea19f0448c1e909f9177bc58630ee1e4b2 Mon Sep 17 00:00:00 2001 From: jenthone Date: Thu, 9 Mar 2023 15:49:45 +0700 Subject: [PATCH 09/11] test: add more bracket cases --- test/parallel/test-repl-preview.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/parallel/test-repl-preview.js b/test/parallel/test-repl-preview.js index 876005b512eb9d..c3140c0bb5bc1f 100644 --- a/test/parallel/test-repl-preview.js +++ b/test/parallel/test-repl-preview.js @@ -207,6 +207,28 @@ async function tests(options) { '{[Sym\x1B[90mbol\x1B[39m\x1B[13G\x1B[0Kb\x1B[90mol\x1B[39m\x1B[14G\x1B[0Ko\x1B[90ml\x1B[39m\x1B[15G\x1B[0Kl.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: '{throw 0}', + noPreview: 'Uncaught \x1B[33m0\x1B[39m', + preview: [ + '{thr\x1B[90mow\x1B[39m\x1B[12G\x1B[0Ko\x1B[90mw\x1B[39m\x1B[13G\x1B[0Kw 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 && From 0230e45fda548da94d2fa67104704d982ee9fb05 Mon Sep 17 00:00:00 2001 From: jenthone Date: Thu, 9 Mar 2023 19:39:01 +0700 Subject: [PATCH 10/11] test: add multiple object case --- test/parallel/test-repl.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/parallel/test-repl.js b/test/parallel/test-repl.js index 9f14ebb39eeac7..4c53bdfa2d4073 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: '| ' From 6093854f980c78862a2b463c9428695f4a032b93 Mon Sep 17 00:00:00 2001 From: meixg Date: Sun, 24 Aug 2025 15:12:35 +0800 Subject: [PATCH 11/11] repl: add isValidParentheses check before wrap input --- lib/internal/repl/utils.js | 147 ++++++++++----------------- lib/repl.js | 157 ++++++++++++++--------------- test/parallel/test-repl-preview.js | 15 ++- test/parallel/test-repl.js | 2 +- 4 files changed, 139 insertions(+), 182 deletions(-) diff --git a/lib/internal/repl/utils.js b/lib/internal/repl/utils.js index c09e54014addf9..88919653d26508 100644 --- a/lib/internal/repl/utils.js +++ b/lib/internal/repl/utils.js @@ -72,7 +72,7 @@ function isRecoverableError(e, code) { // here as the point is to test for potentially valid but incomplete // expressions. if (RegExpPrototypeExec(/^\s*\{/, code) !== null && - isRecoverableError(e, `(${code}`)) + isRecoverableError(e, `(${code}`)) return true; let recoverable = false; @@ -115,10 +115,10 @@ function isRecoverableError(e, code) { case 'Unterminated string constant': { const token = StringPrototypeSlice(this.input, - this.lastTokStart, this.pos); + this.lastTokStart, this.pos); // See https://www.ecma-international.org/ecma-262/#sec-line-terminators if (RegExpPrototypeExec(/\\(?:\r\n?|\n|\u2028|\u2029)$/, - token) !== null) { + token) !== null) { recoverable = true; } } @@ -146,7 +146,7 @@ function isRecoverableError(e, code) { function setupPreview(repl, contextSymbol, bufferSymbol, active) { // Simple terminals can't handle previews. if (process.env.TERM === 'dumb' || !active) { - return { showPreview() { }, clearPreview() { } }; + return { showPreview() {}, clearPreview() {} }; } let inputPreview = null; @@ -171,7 +171,7 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { function isCursorAtInputEnd() { const { cursorPos, displayPos } = getPreviewPos(); return cursorPos.rows === displayPos.rows && - cursorPos.cols === displayPos.cols; + cursorPos.cols === displayPos.cols; } const clearPreview = (key) => { @@ -212,9 +212,9 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { escaped = repl.line; } } else if ((key.name === 'return' || key.name === 'enter') && - !key.meta && - escaped !== repl.line && - isCursorAtInputEnd()) { + !key.meta && + escaped !== repl.line && + isCursorAtInputEnd()) { repl._insertString(completionPreview); } } @@ -289,11 +289,11 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { function isInStrictMode(repl) { return repl.replMode === REPL_MODE_STRICT || ArrayPrototypeIncludes( ArrayPrototypeMap(process.execArgv, - (e) => StringPrototypeReplaceAll( - StringPrototypeToLowerCase(e), - '_', - '-', - )), + (e) => StringPrototypeReplaceAll( + StringPrototypeToLowerCase(e), + '_', + '-', + )), '--use-strict'); } @@ -301,10 +301,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 (StringPrototypeStartsWith(input, '{') && - !StringPrototypeEndsWith(input, ';') && - isValidSyntax(input) && - !wrapped) { + if (!wrapped && input[0] === '{' && input[input.length - 1] !== ';' && isValidSyntax(input)) { input = `(${input})`; wrapped = true; } @@ -322,16 +319,16 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { const { result } = preview; if (result.value !== undefined) { callback(null, inspect(result.value, previewOptions)); - // Ignore EvalErrors, SyntaxErrors and ReferenceErrors. It is not clear - // where they came from and if they are recoverable or not. Other errors - // may be inspected. + // Ignore EvalErrors, SyntaxErrors and ReferenceErrors. It is not clear + // where they came from and if they are recoverable or not. Other errors + // may be inspected. } else if (preview.exceptionDetails && - (result.className === 'EvalError' || - result.className === 'SyntaxError' || - // Report ReferenceError in case the strict mode is active - // for input that has no completions. - (result.className === 'ReferenceError' && - (hasCompletions || !isInStrictMode(repl))))) { + (result.className === 'EvalError' || + result.className === 'SyntaxError' || + // Report ReferenceError in case the strict mode is active + // for input that has no completions. + (result.className === 'ReferenceError' && + (hasCompletions || !isInStrictMode(repl))))) { callback(null, null); } else if (result.objectId) { // The writer options might change and have influence on the inspect @@ -371,9 +368,9 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { const showPreview = (showCompletion = true) => { // Prevent duplicated previews after a refresh or in a multiline command. if (inputPreview !== null || - repl[kIsMultiline] || - !repl.isCompletionEnabled || - !process.features.inspector) { + repl[kIsMultiline] || + !repl.isCompletionEnabled || + !process.features.inspector) { return; } @@ -416,7 +413,7 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { // Do not preview `undefined` if colors are deactivated or explicitly // requested. if (inspected === 'undefined' && - (!repl.useColors || repl.ignoreUndefined)) { + (!repl.useColors || repl.ignoreUndefined)) { return; } @@ -430,7 +427,7 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { // Support unicode characters of width other than one by checking the // actual width. if (inspected.length * 2 >= maxColumns && - getStringWidth(inspected) > maxColumns) { + getStringWidth(inspected) > maxColumns) { maxColumns -= 4 + (repl.useColors ? 0 : 3); let res = ''; for (const char of new SafeStringIterator(inspected)) { @@ -469,8 +466,8 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { let previewLine = line; if (completionPreview !== null && - isCursorAtInputEnd() && - escaped !== repl.line) { + isCursorAtInputEnd() && + escaped !== repl.line) { previewLine += completionPreview; } @@ -504,8 +501,8 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { const currentCursor = repl.cursor; originalMoveCursor(dx); if (currentCursor + dx > repl.line.length && - typeof repl.completer === 'function' && - insertCompletionPreview) { + typeof repl.completer === 'function' && + insertCompletionPreview) { const insertPreview = true; showCompletionPreview(repl.line, insertPreview); } @@ -600,7 +597,7 @@ function setupReverseSearch(repl) { // Match not found. if (cursor === -1) { goToNextHistoryIndex(); - // Match found. + // Match found. } else { if (repl.useColors) { const start = StringPrototypeSlice(entry, 0, cursor); @@ -613,7 +610,7 @@ function setupReverseSearch(repl) { // Explicitly go to the next history item in case no further matches are // possible with the current entry. if ((dir === 'r' && cursor === 0) || - (dir === 's' && entry.length === cursor + input.length)) { + (dir === 's' && entry.length === cursor + input.length)) { goToNextHistoryIndex(); } return; @@ -726,7 +723,7 @@ function setupReverseSearch(repl) { } else if (key.ctrl && checkAndSetDirectionKey(key.name)) { search(); } else if (key.name === 'backspace' || - (key.ctrl && (key.name === 'h' || key.name === 'w'))) { + (key.ctrl && (key.name === 'h' || key.name === 'w'))) { reset(StringPrototypeSlice(input, 0, input.length - 1)); search(); // Special handle + c and escape. Those should only cancel the @@ -738,11 +735,11 @@ function setupReverseSearch(repl) { // End search in case either enter is pressed or if any non-reverse-search // key (combination) is pressed. } else if (key.ctrl || - key.meta || - key.name === 'return' || - key.name === 'enter' || - typeof string !== 'string' || - string === '') { + key.meta || + key.name === 'return' || + key.name === 'enter' || + typeof string !== 'string' || + string === '') { reset(); repl[kSubstringSearch] = ''; } else { @@ -758,16 +755,15 @@ function setupReverseSearch(repl) { const startsWithBraceRegExp = /^\s*{/; const endsWithSemicolonRegExp = /;\s*$/; function isValidSyntax(input) { - const Parser = require('internal/deps/acorn/acorn/dist/acorn').Parser; try { - Parser.parse(input, { + AcornParser.parse(input, { ecmaVersion: 'latest', allowAwaitOutsideFunction: true, }); return true; } catch { try { - Parser.parse(`_=${input}`, { + AcornParser.parse(`_=${input}`, { ecmaVersion: 'latest', allowAwaitOutsideFunction: true, }); @@ -789,52 +785,15 @@ function isValidSyntax(input) { function isObjectLiteral(code) { return RegExpPrototypeExec(startsWithBraceRegExp, code) !== null && RegExpPrototypeExec(endsWithSemicolonRegExp, code) === null; - function isValidSyntax(input) { - const Parser = require('internal/deps/acorn/acorn/dist/acorn').Parser; - try { - Parser.parse(input, { ecmaVersion: 'latest' }); - return true; - } catch { - return false; - } - } - - function isValidParentheses(input) { - const stack = []; - const pairs = { - '(': ')', - '[': ']', - '{': '}', - }; - - for (let i = 0; i < input.length; i++) { - const char = input[i]; - - if (pairs[char]) { - stack.push(char); - } else if (char === ')' || char === ']' || char === '}') { - if (pairs[stack.pop()] !== char) { - return false; - } - } - function isValidSyntax(input) { - const Parser = require('internal/deps/acorn/acorn/dist/acorn').Parser; - try { - let result = Parser.parse(input, { ecmaVersion: 'latest' }); - return true; - } catch (e) { - return false; - } - } +} - module.exports = { - REPL_MODE_SLOPPY: Symbol('repl-sloppy'), - REPL_MODE_STRICT, - isRecoverableError, - kStandaloneREPL: Symbol('kStandaloneREPL'), - setupPreview, - setupReverseSearch, - isObjectLiteral, - isValidParentheses, - isValidSyntax, - }; +module.exports = { + REPL_MODE_SLOPPY: Symbol('repl-sloppy'), + REPL_MODE_STRICT, + isRecoverableError, + kStandaloneREPL: Symbol('kStandaloneREPL'), + setupPreview, + setupReverseSearch, + isObjectLiteral, + isValidSyntax, +}; diff --git a/lib/repl.js b/lib/repl.js index 33fd38d02901c9..3d66e928601f07 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -177,7 +177,6 @@ const { setupPreview, setupReverseSearch, isObjectLiteral, - isValidParentheses, isValidSyntax, } = require('internal/repl/utils'); const { @@ -268,11 +267,11 @@ const toDynamicImport = (codeLine) => { }; function REPLServer(prompt, - stream, - eval_, - useGlobal, - ignoreUndefined, - replMode) { + stream, + eval_, + useGlobal, + ignoreUndefined, + replMode) { if (!(this instanceof REPLServer)) { return deprecateInstantiation(REPLServer, 'DEP0185', prompt, stream, eval_, useGlobal, ignoreUndefined, replMode); } @@ -319,15 +318,15 @@ function REPLServer(prompt, __proto__: null, get: pendingDeprecation ? deprecate(() => this.input, - 'repl.inputStream and repl.outputStream are deprecated. ' + - 'Use repl.input and repl.output instead', - 'DEP0141') : + 'repl.inputStream and repl.outputStream are deprecated. ' + + 'Use repl.input and repl.output instead', + 'DEP0141') : () => this.input, set: pendingDeprecation ? deprecate((val) => this.input = val, - 'repl.inputStream and repl.outputStream are deprecated. ' + - 'Use repl.input and repl.output instead', - 'DEP0141') : + 'repl.inputStream and repl.outputStream are deprecated. ' + + 'Use repl.input and repl.output instead', + 'DEP0141') : (val) => this.input = val, enumerable: false, configurable: true, @@ -336,15 +335,15 @@ function REPLServer(prompt, __proto__: null, get: pendingDeprecation ? deprecate(() => this.output, - 'repl.inputStream and repl.outputStream are deprecated. ' + - 'Use repl.input and repl.output instead', - 'DEP0141') : + 'repl.inputStream and repl.outputStream are deprecated. ' + + 'Use repl.input and repl.output instead', + 'DEP0141') : () => this.output, set: pendingDeprecation ? deprecate((val) => this.output = val, - 'repl.inputStream and repl.outputStream are deprecated. ' + - 'Use repl.input and repl.output instead', - 'DEP0141') : + 'repl.inputStream and repl.outputStream are deprecated. ' + + 'Use repl.input and repl.output instead', + 'DEP0141') : (val) => this.output = val, enumerable: false, configurable: true, @@ -382,9 +381,9 @@ function REPLServer(prompt, // instance and that could trigger the `MaxListenersExceededWarning`. process.prependListener('newListener', (event, listener) => { if (event === 'uncaughtException' && - process.domain && - listener.name !== 'domainUncaughtExceptionClear' && - domainSet.has(process.domain)) { + process.domain && + listener.name !== 'domainUncaughtExceptionClear' && + domainSet.has(process.domain)) { // Throw an error so that the event will not be added and the current // domain takes over. That way the user is notified about the error // and the current code evaluation is stopped, just as any other code @@ -401,8 +400,8 @@ function REPLServer(prompt, const savedRegExMatches = ['', '', '', '', '', '', '', '', '', '']; const sep = '\u0000\u0000\u0000'; const regExMatcher = new RegExp(`^${sep}(.*)${sep}(.*)${sep}(.*)${sep}(.*)` + - `${sep}(.*)${sep}(.*)${sep}(.*)${sep}(.*)` + - `${sep}(.*)$`); + `${sep}(.*)${sep}(.*)${sep}(.*)${sep}(.*)` + + `${sep}(.*)$`); eval_ ||= defaultEval; @@ -447,14 +446,8 @@ function REPLServer(prompt, let awaitPromise = false; const input = code; - // It's confusing for `{ a : 1 }` to be interpreted as a block - // statement rather than an object literal. So, we first try - // to wrap it in parentheses, so that it will be interpreted as - // an expression. Note that if the above condition changes, - // lib/internal/repl/utils.js needs to be changed to match. - if (RegExpPrototypeExec(/^\s*{/, code) !== null && - RegExpPrototypeExec(/;\s*$/, code) === null && - isValidSyntax(code)) { + if (isObjectLiteral(code) && isValidSyntax(code)) { + // Add parentheses to make sure `code` is parsed as an expression code = `(${StringPrototypeTrim(code)})\n`; wrappedCmd = true; } @@ -472,8 +465,8 @@ function REPLServer(prompt, async function importModuleDynamically(specifier, _, importAttributes, phase) { const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader(); return cascadedLoader.import(specifier, parentURL, importAttributes, - phase === 'evaluation' ? cascadedLoader.kEvaluationPhase : - cascadedLoader.kSourcePhase); + phase === 'evaluation' ? cascadedLoader.kEvaluationPhase : + cascadedLoader.kSourcePhase); } // `experimentalREPLAwait` is set to true by default. // Shall be false in case `--no-experimental-repl-await` flag is used. @@ -529,7 +522,7 @@ function REPLServer(prompt, while (true) { try { if (self.replMode === module.exports.REPL_MODE_STRICT && - RegExpPrototypeExec(/^\s*$/, code) === null) { + RegExpPrototypeExec(/^\s*$/, code) === null) { // "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}`; @@ -569,7 +562,7 @@ function REPLServer(prompt, // This will set the values from `savedRegExMatches` to corresponding // predefined RegExp properties `RegExp.$1`, `RegExp.$2` ... `RegExp.$9` RegExpPrototypeExec(regExMatcher, - ArrayPrototypeJoin(savedRegExMatches, sep)); + ArrayPrototypeJoin(savedRegExMatches, sep)); let finished = false; function finishExecution(err, result) { @@ -751,7 +744,7 @@ function REPLServer(prompt, } if (options[kStandaloneREPL] && - process.listenerCount('uncaughtException') !== 0) { + process.listenerCount('uncaughtException') !== 0) { process.nextTick(() => { process.emit('uncaughtException', e); self.clearBufferedCommand(); @@ -770,7 +763,7 @@ function REPLServer(prompt, errStack = ''; ArrayPrototypeForEach(lines, (line) => { if (!matched && - RegExpPrototypeExec(/^\[?([A-Z][a-z0-9_]*)*Error/, line) !== null) { + RegExpPrototypeExec(/^\[?([A-Z][a-z0-9_]*)*Error/, line) !== null) { errStack += writer.options.breakLength >= line.length ? `Uncaught ${line}` : `Uncaught:\n${line}`; @@ -798,7 +791,7 @@ function REPLServer(prompt, function completer(text, cb) { ReflectApply(complete, self, - [text, self.editorMode ? self.completeOnEditorMode(cb) : cb]); + [text, self.editorMode ? self.completeOnEditorMode(cb) : cb]); } // All the parameters in the object are defining the "input" param of the @@ -920,8 +913,8 @@ function REPLServer(prompt, // display next prompt and return. if (trimmedCmd) { if (StringPrototypeCharAt(trimmedCmd, 0) === '.' && - StringPrototypeCharAt(trimmedCmd, 1) !== '.' && - NumberIsNaN(NumberParseFloat(trimmedCmd))) { + StringPrototypeCharAt(trimmedCmd, 1) !== '.' && + NumberIsNaN(NumberParseFloat(trimmedCmd))) { const matches = RegExpPrototypeExec(/^\.([^\s]+)\s*(.*)$/, trimmedCmd); const keyword = matches?.[1]; const rest = matches?.[2]; @@ -946,12 +939,12 @@ function REPLServer(prompt, ReflectApply(_memory, self, [cmd]); if (e && !self[kBufferedCommandSymbol] && - StringPrototypeStartsWith(StringPrototypeTrim(cmd), 'npm ') && - !(e instanceof Recoverable) + StringPrototypeStartsWith(StringPrototypeTrim(cmd), 'npm ') && + !(e instanceof Recoverable) ) { self.output.write('npm should be run outside of the ' + - 'Node.js REPL, in your normal shell.\n' + - '(Press Ctrl+D to exit.)\n'); + 'Node.js REPL, in your normal shell.\n' + + '(Press Ctrl+D to exit.)\n'); self.displayPrompt(); return; } @@ -979,11 +972,11 @@ function REPLServer(prompt, // If we got any output - print it (if no error) if (!e && - // When an invalid REPL command is used, error message is printed - // immediately. We don't have to print anything else. So, only when - // the second argument to this function is there, print it. - arguments.length === 2 && - (!self.ignoreUndefined || ret !== undefined)) { + // When an invalid REPL command is used, error message is printed + // immediately. We don't have to print anything else. So, only when + // the second argument to this function is there, print it. + arguments.length === 2 && + (!self.ignoreUndefined || ret !== undefined)) { if (!self.underscoreAssigned) { self.last = ret; } @@ -1029,13 +1022,13 @@ function REPLServer(prompt, key ||= {}; if (paused && !(self.breakEvalOnSigint && key.ctrl && key.name === 'c')) { ArrayPrototypePush(pausedBuffer, - ['key', [d, key], self.isCompletionEnabled]); + ['key', [d, key], self.isCompletionEnabled]); return; } if (!self.editorMode || !self.terminal) { // Before exiting, make sure to clear the line. if (key.ctrl && key.name === 'd' && - self.cursor === 0 && self.line.length === 0) { + self.cursor === 0 && self.line.length === 0) { self.clearLine(); } clearPreview(key); @@ -1122,7 +1115,7 @@ REPLServer.prototype.close = function close() { ); }; -REPLServer.prototype.createContext = function () { +REPLServer.prototype.createContext = function() { let context; if (this.useGlobal) { context = globalThis; @@ -1141,10 +1134,10 @@ REPLServer.prototype.createContext = function () { // Only set properties that do not already exist as a global builtin. if (!globalBuiltins.has(name)) { ObjectDefineProperty(context, name, - { - __proto__: null, - ...ObjectGetOwnPropertyDescriptor(globalThis, name), - }); + { + __proto__: null, + ...ObjectGetOwnPropertyDescriptor(globalThis, name), + }); } }); context.global = context; @@ -1178,7 +1171,7 @@ REPLServer.prototype.createContext = function () { return context; }; -REPLServer.prototype.resetContext = function () { +REPLServer.prototype.resetContext = function() { this.context = this.createContext(); this.underscoreAssigned = false; this.underscoreErrAssigned = false; @@ -1217,7 +1210,7 @@ REPLServer.prototype.resetContext = function () { this.emit('reset', this.context); }; -REPLServer.prototype.displayPrompt = function (preserveCursor) { +REPLServer.prototype.displayPrompt = function(preserveCursor) { let prompt = this._initialPrompt; if (this[kBufferedCommandSymbol].length) { prompt = kMultilinePrompt.description; @@ -1299,7 +1292,7 @@ function getGlobalLexicalScopeNames(contextId) { }, () => []); } -REPLServer.prototype.complete = function () { +REPLServer.prototype.complete = function() { ReflectApply(this.completer, this, arguments); }; @@ -1371,7 +1364,7 @@ function complete(line, callback) { const subdir = match[2] || ''; const extensions = ObjectKeys(CJSModule._extensions); const indexes = ArrayPrototypeMap(extensions, - (extension) => `index${extension}`); + (extension) => `index${extension}`); ArrayPrototypePush(indexes, 'package.json', 'index'); group = []; @@ -1394,7 +1387,7 @@ function complete(line, callback) { const dirents = gracefulReaddir(dir, { withFileTypes: true }) || []; ArrayPrototypeForEach(dirents, (dirent) => { if (RegExpPrototypeExec(versionedFileNamesRe, dirent.name) !== null || - dirent.name === '.npm') { + dirent.name === '.npm') { // Exclude versioned names that 'npm' installs. return; } @@ -1402,7 +1395,7 @@ function complete(line, callback) { const base = StringPrototypeSlice(dirent.name, 0, -extension.length); if (!dirent.isDirectory()) { if (StringPrototypeIncludes(extensions, extension) && - (!subdir || base !== 'index')) { + (!subdir || base !== 'index')) { ArrayPrototypePush(group, `${subdir}${base}`); } return; @@ -1455,7 +1448,7 @@ function complete(line, callback) { ArrayPrototypeForEach(dirents, (dirent) => { const { name } = dirent; if (RegExpPrototypeExec(versionedFileNamesRe, name) !== null || - name === '.npm') { + name === '.npm') { // Exclude versioned names that 'npm' installs. return; } @@ -1488,10 +1481,10 @@ function complete(line, callback) { ArrayPrototypePush(completionGroups, _builtinLibs, nodeSchemeBuiltinLibs); } else if ((match = RegExpPrototypeExec(fsAutoCompleteRE, line)) !== null && - this.allowBlockingCompletions) { + this.allowBlockingCompletions) { ({ 0: completionGroups, 1: completeOn } = completeFSFunctions(match)); } else if (line.length === 0 || - RegExpPrototypeExec(/\w|\.|\$/, line[line.length - 1]) !== null) { + RegExpPrototypeExec(/\w|\.|\$/, line[line.length - 1]) !== null) { const completeTarget = line.length === 0 ? line : findExpressionCompleteTarget(line); if (line.length !== 0 && !completeTarget) { @@ -1512,11 +1505,11 @@ function complete(line, callback) { if (!expr) { // Get global vars synchronously ArrayPrototypePush(completionGroups, - getGlobalLexicalScopeNames(this[kContextId])); + getGlobalLexicalScopeNames(this[kContextId])); let contextProto = this.context; while ((contextProto = ObjectGetPrototypeOf(contextProto)) !== null) { ArrayPrototypePush(completionGroups, - filteredOwnPropertyNames(contextProto)); + filteredOwnPropertyNames(contextProto)); } const contextOwnNames = filteredOwnPropertyNames(this.context); if (!this.useGlobal) { @@ -1553,8 +1546,8 @@ function complete(line, callback) { this.context, (includes) => { if (includes) { - // The expression involves proxies or getters, meaning that it - // can trigger side-effectful behaviors, so bail out + // The expression involves proxies or getters, meaning that it + // can trigger side-effectful behaviors, so bail out return completionGroupsLoaded(); } @@ -1583,18 +1576,18 @@ function complete(line, callback) { p = ObjectGetPrototypeOf(p); } } catch { - // Maybe a Proxy object without `getOwnPropertyNames` trap. - // We simply ignore it here, as we don't want to break the - // autocompletion. Fixes the bug - // https://github.com/nodejs/node/issues/2119 + // Maybe a Proxy object without `getOwnPropertyNames` trap. + // We simply ignore it here, as we don't want to break the + // autocompletion. Fixes the bug + // https://github.com/nodejs/node/issues/2119 } if (memberGroups.length) { expr += chaining; ArrayPrototypeForEach(memberGroups, (group) => { ArrayPrototypePush(completionGroups, - ArrayPrototypeMap(group, - (member) => `${expr}${member}`)); + ArrayPrototypeMap(group, + (member) => `${expr}${member}`)); }); filter &&= `${expr}${filter}`; } @@ -1932,7 +1925,7 @@ REPLServer.prototype.completeOnEditorMode = (callback) => (err, results) => { callback(null, [result, completeOn]); }; -REPLServer.prototype.defineCommand = function (keyword, cmd) { +REPLServer.prototype.defineCommand = function(keyword, cmd) { if (typeof cmd === 'function') { cmd = { action: cmd }; } else { @@ -2037,7 +2030,7 @@ function _turnOffEditorMode(repl) { function defineDefaultCommands(repl) { repl.defineCommand('break', { help: 'Sometimes you get stuck, this gets you out', - action: function () { + action: function() { this.clearBufferedCommand(); this.displayPrompt(); }, @@ -2051,7 +2044,7 @@ function defineDefaultCommands(repl) { } repl.defineCommand('clear', { help: clearMessage, - action: function () { + action: function() { this.clearBufferedCommand(); if (!this.useGlobal) { this.output.write('Clearing context...\n'); @@ -2063,14 +2056,14 @@ function defineDefaultCommands(repl) { repl.defineCommand('exit', { help: 'Exit the REPL', - action: function () { + action: function() { this.close(); }, }); repl.defineCommand('help', { help: 'Print this help message', - action: function () { + action: function() { const names = ArrayPrototypeSort(ObjectKeys(this.commands)); const longestNameLength = MathMaxApply( ArrayPrototypeMap(names, (name) => name.length), @@ -2090,7 +2083,7 @@ function defineDefaultCommands(repl) { repl.defineCommand('save', { help: 'Save all evaluated commands in this REPL session to a file', - action: function (file) { + action: function(file) { try { if (file === '') { throw new ERR_MISSING_ARGS('file'); @@ -2110,7 +2103,7 @@ function defineDefaultCommands(repl) { repl.defineCommand('load', { help: 'Load JS from a file into the REPL session', - action: function (file) { + action: function(file) { try { if (file === '') { throw new ERR_MISSING_ARGS('file'); diff --git a/test/parallel/test-repl-preview.js b/test/parallel/test-repl-preview.js index c3140c0bb5bc1f..9ab84b5c9f3ae4 100644 --- a/test/parallel/test-repl-preview.js +++ b/test/parallel/test-repl-preview.js @@ -201,11 +201,10 @@ async function tests(options) { ], }, { input: '{[Symbol.for("{")]: 0 }', - noPreview: '{ [\x1B[32mSymbol({)\x1B[39m]: \x1B[33m0\x1B[39m }', + noPreview: '{ \x1B[32mSymbol({)\x1B[39m: \x1B[33m0\x1B[39m }', preview: [ - // eslint-disable-next-line max-len - '{[Sym\x1B[90mbol\x1B[39m\x1B[13G\x1B[0Kb\x1B[90mol\x1B[39m\x1B[14G\x1B[0Ko\x1B[90ml\x1B[39m\x1B[15G\x1B[0Kl.for("{")]: 0 }\r', - '{ [\x1B[32mSymbol({)\x1B[39m]: \x1B[33m0\x1B[39m }', + '{[Symbol.for("{")]: 0 }\r', + '{ \x1B[32mSymbol({)\x1B[39m: \x1B[33m0\x1B[39m }', ], }, { input: '{},{}', @@ -221,11 +220,17 @@ async function tests(options) { preview: [ '{} //\r', ], + }, { + input: '{} //;', + noPreview: 'repl > ', + preview: [ + '{} //;\r', + ], }, { input: '{throw 0}', noPreview: 'Uncaught \x1B[33m0\x1B[39m', preview: [ - '{thr\x1B[90mow\x1B[39m\x1B[12G\x1B[0Ko\x1B[90mw\x1B[39m\x1B[13G\x1B[0Kw 0}', + '{throw 0}', '\x1B[90m0\x1B[39m\x1B[17G\x1B[1A\x1B[1B\x1B[2K\x1B[1A\r', 'Uncaught \x1B[33m0\x1B[39m', ], diff --git a/test/parallel/test-repl.js b/test/parallel/test-repl.js index 4c53bdfa2d4073..a8f483e90135cb 100644 --- a/test/parallel/test-repl.js +++ b/test/parallel/test-repl.js @@ -330,7 +330,7 @@ const errorTests = [ // Multiline object { send: '{}),({}', - expect: '... ', + expect: '| ', }, { send: '}',