Repository: nodejs/node (JavaScript)

Issue:
`.load` infinite loop in REPL
### Version

v19.6.0

### Platform

Darwin <hostname> 22.3.0 Darwin Kernel Version 22.3.0: Mon Jan 30 20:38:43 PST 2023; root:xnu-8792.81.3~2/RELEASE_ARM64_T8112 arm64

### Subsystem

repl

### What steps will reproduce the bug?

```
$ cat foo.js
function a(b) {
  return b }
a(1)
$ node
Welcome to Node.js v19.6.0.
Type ".help" for more information.
> .load foo.js
  function a(b) {
  function a(b) {
  function a(b) {
  ...
```

This prints `function a(b) {` forever until killing the process (it doesn't respond to `Ctrl-c`)

### How often does it reproduce? Is there a required condition?

Every time, with the correct file.

### What is the expected behavior?

```
$ cat foo.js
function a(b) {
  return b }
a(1)
$ node
Welcome to Node.js v19.6.0.
Type ".help" for more information.
> .load foo.js
function a(b) {
  return b }
a(1)

1
>
```

### What do you see instead?

```
  function a(b) {
  function a(b) {
  function a(b) {
  ...
```

### Additional information

I'm struggling to figure out what's happening here. If the file contents don't include a new line after the first `{`, i.e.

```
function a(b) { return b }
a(1)
```

everything works fine. This lead me to believe it was somehow a parsing issue, but there are a number of iterations of the file that either do or do not work that don't seem to line up. The following files _do_ work (separated by lines only containing //):

```
// same as above
function a(b) { return b }
a(1)

// not calling a
function a(b) {
  return b }

// using arrow function
a = b => { return b }

// random example that works fine
a = 1 + 1

console.log(a)
```

The following _do not_ work:

```
// same as above
function a(b) {
  return b }
a(1)

// using arrow function (not calling a)
a = b => {
  return b }

// using arrow function (calling a)
a = b => {
  return b }
a(1)
```

I believe there were a few other weird things that worked and didn't work.

The odd thing is that reverting 6874aa1, lines 1554-1559 (https://github.com/nodejs/node/commit/6874aa1fb15e815bacf0b0f7e18a434d72de0b4d#diff-392b628f0a5eb047b1117351e6eedcefb8ec70a48933479e0e9bdc711a04f047L1554-L1559), seems to fix this. I tried a few other things, such as loading the file into a buffer before writing it out, `toString()`ing it after `Buffer.from()`ing it), changing the encoding, and changing the line endings. None of these worked.

Also, the tests run fine, even when modifying them to include the text that breaks outside of the tests.


Discussion: The behavior is the same with v18.14.1 and v18.13.0, but works fine with v18.12.1. The only relevant change I can see between the two is from #45539, but I'm not sure why that would be causing this issue. It's also a little too in the weeds for me to spend much more time on it right now, unfortunately. Hopefully this information helps at least.
I did some testing and this seems to also relate to newline characters:
The infinite loop does not occur if the loaded file does not end with a newline character.

<table><tr><th></th><th>Node 18.12.0</th><th>Node 20.0.0-pre</th></tr>
<tr>
<td>without ending newline character</td>
<td>
<pre lang="js">
> const testFile = `function a(b) {\n  return b }\na(1)`
> fs.writeFileSync('foo.js', testFile)
> .load foo.js
function a(b) {
  return b }
  a(1)
1
</pre>
</td>
<td>
<pre lang="js">
> const testFile = `function a(b) {\n  return b }\na(1)`
> fs.writeFileSync('foo.js', testFile)
> .load foo.js
function a(b) {
  return b }
undefined
</pre>
</td>
</tr>
<tr><td>with ending newline character</td>
<td>
<pre lang="js">
> const testFile = `function a(b) {\n  return b }\na(1)\n`
> fs.writeFileSync('foo.js', testFile)
> .load foo.js
function a(b) {
  return b }
  a(1)

1
</pre>
</td>
<td>
<pre lang="js">
> const testFile = `function a(b) {\n  return b }\na(1)\n`
> fs.writeFileSync('foo.js', testFile)
> .load foo.js
  function a(b) {
  function a(b) {
  function a(b) {
  ...
</pre>
</td>
</tr>
</table>

Note that even without the ending newline character, the output between v20.0.0-pre and 18.12.0 is different.
Bisected to https://github.com/nodejs/node/pull/45614

/cc @aduh95 



Fix (patch):
diff --git a/lib/internal/readline/interface.js b/lib/internal/readline/interface.js
index 45524612aea37b..4a5ec4973695fa 100644
--- a/lib/internal/readline/interface.js
+++ b/lib/internal/readline/interface.js
@@ -1322,18 +1322,22 @@ class Interface extends InterfaceConstructor {
         // falls through
         default:
           if (typeof s === 'string' && s) {
+            // Erase state of previous searches.
+            lineEnding.lastIndex = 0;
             let nextMatch = RegExpPrototypeExec(lineEnding, s);
-            if (nextMatch !== null) {
-              this[kInsertString](StringPrototypeSlice(s, 0, nextMatch.index));
-              let { lastIndex } = lineEnding;
-              while ((nextMatch = RegExpPrototypeExec(lineEnding, s)) !== null) {
-                this[kLine]();
+            // If no line endings are found, just insert the string as is.
+            if (nextMatch === null) {
+              this[kInsertString](s);
+            } else {
+              // Keep track of the end of the last match.
+              let lastIndex = 0;
+              do {
                 this[kInsertString](StringPrototypeSlice(s, lastIndex, nextMatch.index));
                 ({ lastIndex } = lineEnding);
-              }
-              if (lastIndex === s.length) this[kLine]();
-            } else {
-              this[kInsertString](s);
+                this[kLine]();
+                // Restore lastIndex as the call to kLine could have mutated it.
+                lineEnding.lastIndex = lastIndex;
+              } while ((nextMatch = RegExpPrototypeExec(lineEnding, s)) !== null);
             }
           }
       }