readline: fix issue with newline-less last line

The logic for reading lines was slightly flawed, in that it assumed
there would be a final new line. It handled the case where there are no
new lines, but this then broke if there were some new lines.

The fix in logic is basically removing the special case where there are
no new lines by changing it to always read the final line with no new
lines. This works because if a file contains no new lines, the final
line is the first line, and all is well.

There is some subtlety in this functioning, however. If the last line
contains no new lines, then `lastIndex` will be the start of the last
line, and `kInsertString` will be called from that point. If it does
contain a new line, `lastIndex` will be equal to `s.length`, so the
slice will be the empty string.

Fixes: https://github.com/nodejs/node/issues/47305
PR-URL: https://github.com/nodejs/node/pull/47317
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
This commit is contained in:
Ian Harris 2023-04-14 03:12:16 -07:00 committed by GitHub
parent c94be4125b
commit 9decb70d05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 85 additions and 14 deletions

View File

@ -1324,21 +1324,19 @@ class Interface extends InterfaceConstructor {
if (typeof s === 'string' && s) { if (typeof s === 'string' && s) {
// Erase state of previous searches. // Erase state of previous searches.
lineEnding.lastIndex = 0; lineEnding.lastIndex = 0;
let nextMatch = RegExpPrototypeExec(lineEnding, s); let nextMatch;
// If no line endings are found, just insert the string as is. // Keep track of the end of the last match.
if (nextMatch === null) { let lastIndex = 0;
this[kInsertString](s); while ((nextMatch = RegExpPrototypeExec(lineEnding, s)) !== null) {
} else { this[kInsertString](StringPrototypeSlice(s, lastIndex, nextMatch.index));
// Keep track of the end of the last match. ({ lastIndex } = lineEnding);
let lastIndex = 0; this[kLine]();
do { // Restore lastIndex as the call to kLine could have mutated it.
this[kInsertString](StringPrototypeSlice(s, lastIndex, nextMatch.index)); lineEnding.lastIndex = lastIndex;
({ lastIndex } = lineEnding);
this[kLine]();
// Restore lastIndex as the call to kLine could have mutated it.
lineEnding.lastIndex = lastIndex;
} while ((nextMatch = RegExpPrototypeExec(lineEnding, s)) !== null);
} }
// This ensures that the last line is written if it doesn't end in a newline.
// Note that the last line may be the first line, in which case this still works.
this[kInsertString](StringPrototypeSlice(s, lastIndex));
} }
} }
} }

View File

@ -0,0 +1,7 @@
// The lack of a newline at the end of this file is intentional.
const getLunch = () =>
placeOrder('tacos')
.then(eat);
const placeOrder = (order) => Promise.resolve(order);
const eat = (food) => '<nom nom nom>';

View File

@ -0,0 +1,24 @@
'use strict';
const common = require('../common');
const ArrayStream = require('../common/arraystream');
const assert = require('assert');
common.skipIfDumbTerminal();
const readline = require('readline');
const rli = new readline.Interface({
terminal: true,
input: new ArrayStream(),
output: new ArrayStream(),
});
// Minimal reproduction for #47305
const testInput = '{\n}';
let accum = '';
rli.output.write = (data) => accum += data.replace('\r', '');
rli.write(testInput);
assert.strictEqual(accum, testInput);

View File

@ -0,0 +1,42 @@
'use strict';
const common = require('../common');
const ArrayStream = require('../common/arraystream');
const fixtures = require('../common/fixtures');
const assert = require('assert');
const repl = require('repl');
common.skipIfDumbTerminal();
const command = `.load ${fixtures.path('repl-load-multiline-no-trailing-newline.js')}`;
const terminalCode = '\u001b[1G\u001b[0J \u001b[1G';
const terminalCodeRegex = new RegExp(terminalCode.replace(/\[/g, '\\['), 'g');
const expected = `${command}
// The lack of a newline at the end of this file is intentional.
const getLunch = () =>
placeOrder('tacos')
.then(eat);
const placeOrder = (order) => Promise.resolve(order);
const eat = (food) => '<nom nom nom>';
undefined
`;
let accum = '';
const inputStream = new ArrayStream();
const outputStream = new ArrayStream();
outputStream.write = (data) => accum += data.replace('\r', '');
const r = repl.start({
prompt: '',
input: inputStream,
output: outputStream,
terminal: true,
useColors: false
});
r.write(`${command}\n`);
assert.strictEqual(accum.replace(terminalCodeRegex, ''), expected);
r.close();