Repository: nodejs/node
Pull request: [v22.x backport] repl: add isValidParentheses check before wrap input

PR-URL: https://github.com/nodejs/node/pull/59607

<!--
Before submitting a pull request, please read:

- the CONTRIBUTING guide at https://github.com/nodejs/node/blob/HEAD/CONTRIBUTING.md
- the commit message formatting guidelines at
  https://github.com/nodejs/node/blob/HEAD/doc/contributing/pull-requests.md#commit-message-guidelines

For code changes:
1. Include tests for any bug fixes or new features.
2. Update documentation if relevant.
3. Ensure that `make -j4 test` (UNIX), or `vcbuild test` (Windows) passes.

If you believe this PR should be highlighted in the Node.js CHANGELOG
please add the `notable-change` label.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.
-->


Changed files:
filename: lib/internal/repl/utils.js
patch: @@ -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,
 };

filename: lib/repl.js
patch: @@ -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', {

filename: test/parallel/test-repl-preview.js
patch: @@ -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);
+        }
+      }
     }
   }
 }

filename: test/parallel/test-repl.js
patch: @@ -328,6 +328,19 @@ const errorTests = [
     expect: '[Function (anonymous)]'
   },
   // Multiline object
+  {
+    send: '{}),({}',
+    expect: '... ',
+  },
+  {
+    send: '}',
+    expect: [
+      '{}),({}',
+      kArrow,
+      '',
+      /^Uncaught SyntaxError: /,
+    ]
+  },
   {
     send: '{ a: ',
     expect: '... '