From 294a06a298aab574606e0117774f4936cfdba0ca Mon Sep 17 00:00:00 2001 From: linkgoron Date: Sun, 12 May 2024 12:15:25 +0300 Subject: [PATCH] repl: fix disruptive autocomplete without inspector Fix an issue where the autocomplete wrongly autocompletes a value from a correct value to an undefined value when `node` is built without an inspector by disabling the preview view. fixes: https://github.com/nodejs/node/issues/40635 PR-URL: https://github.com/nodejs/node/pull/40661 Fixes: https://github.com/nodejs/node/issues/40635 Reviewed-By: Rich Trott Reviewed-By: Ruben Bridgewater Reviewed-By: James M Snell --- lib/internal/repl/utils.js | 2 +- test/parallel/test-repl-history-navigation.js | 4 +- .../test-repl-preview-without-inspector.js | 161 ++++++++++++++++++ 3 files changed, 164 insertions(+), 3 deletions(-) create mode 100644 test/parallel/test-repl-preview-without-inspector.js diff --git a/lib/internal/repl/utils.js b/lib/internal/repl/utils.js index f3697aa9b68ae2..ce0caf6d2a3eb7 100644 --- a/lib/internal/repl/utils.js +++ b/lib/internal/repl/utils.js @@ -365,7 +365,7 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { const showPreview = (showCompletion = true) => { // Prevent duplicated previews after a refresh. - if (inputPreview !== null || !repl.isCompletionEnabled) { + if (inputPreview !== null || !repl.isCompletionEnabled || !process.features.inspector) { return; } diff --git a/test/parallel/test-repl-history-navigation.js b/test/parallel/test-repl-history-navigation.js index 64236ec734923d..4df120d7cb9eae 100644 --- a/test/parallel/test-repl-history-navigation.js +++ b/test/parallel/test-repl-history-navigation.js @@ -585,7 +585,7 @@ const tests = [ prompt, ...'const util = {}', 'undefined\n', prompt, ...'ut', ...(prev ? [' // il', '\n// {}', - 'il', '\n// {}'] : [' // il', 'il']), + 'il', '\n// {}'] : ['il']), '{}\n', prompt, ], @@ -605,7 +605,7 @@ const tests = [ 'undefined\n', prompt, ...'globalThis.util = {}', '{}\n', - prompt, ...'ut', ' // il', 'il', + prompt, ...'ut', ...(prev ? [' // il', 'il' ] : ['il']), '{}\n', prompt, ...'Reflect.defineProperty(globalThis, "util", utilDesc)', 'true\n', diff --git a/test/parallel/test-repl-preview-without-inspector.js b/test/parallel/test-repl-preview-without-inspector.js new file mode 100644 index 00000000000000..8905d214836c64 --- /dev/null +++ b/test/parallel/test-repl-preview-without-inspector.js @@ -0,0 +1,161 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { REPLServer } = require('repl'); +const { Stream } = require('stream'); + +if (process.features.inspector) + common.skip('test is for node compiled with --without-inspector only'); + +// Ignore terminal settings. This is so the test can be run intact if TERM=dumb. +process.env.TERM = ''; +const PROMPT = 'repl > '; + +class REPLStream extends Stream { + readable = true; + writable = true; + + constructor() { + super(); + this.lines = ['']; + } + run(data) { + for (const entry of data) { + this.emit('data', entry); + } + this.emit('data', '\n'); + } + write(chunk) { + const chunkLines = chunk.toString('utf8').split('\n'); + this.lines[this.lines.length - 1] += chunkLines[0]; + if (chunkLines.length > 1) { + this.lines.push(...chunkLines.slice(1)); + } + this.emit('line'); + return true; + } + wait() { + this.lines = ['']; + return new Promise((resolve, reject) => { + const onError = (err) => { + this.removeListener('line', onLine); + reject(err); + }; + const onLine = () => { + if (this.lines[this.lines.length - 1].includes(PROMPT)) { + this.removeListener('error', onError); + this.removeListener('line', onLine); + resolve(this.lines); + } + }; + this.once('error', onError); + this.on('line', onLine); + }); + } + pause() { } + resume() { } +} + +function runAndWait(cmds, repl) { + const promise = repl.inputStream.wait(); + for (const cmd of cmds) { + repl.inputStream.run(cmd); + } + return promise; +} + +const repl = REPLServer({ + prompt: PROMPT, + stream: new REPLStream(), + ignoreUndefined: true, + useColors: true, + terminal: true, +}); + +repl.inputStream.run([ + 'function foo(x) { return x; }', + 'function koo() { console.log("abc"); }', + 'a = undefined;', + 'const r = 5;', +]); + +const testCases = [{ + input: 'foo', + preview: [ + 'foo\r', + '\x1B[36m[Function: foo]\x1B[39m', + ] +}, { + input: 'r', + preview: [ + 'r\r', + '\x1B[33m5\x1B[39m', + ] +}, { + input: 'koo', + preview: [ + 'koo\r', + '\x1B[36m[Function: koo]\x1B[39m', + ] +}, { + input: 'a', + preview: ['a\r'] // No "undefined" preview. +}, { + input: " { b: 1 }['b'] === 1", + preview: [ + " { b: 1 }['b'] === 1\r", + '\x1B[33mtrue\x1B[39m', + ] +}, { + input: "{ b: 1 }['b'] === 1;", + preview: [ + "{ b: 1 }['b'] === 1;\r", + '\x1B[33mfalse\x1B[39m', + ] +}, { + input: '{ a: true }', + preview: [ + '{ a: true }\r', + '{ a: \x1B[33mtrue\x1B[39m }', + ] +}, { + input: '{ a: true };', + preview: [ + '{ a: true };\r', + '\x1B[33mtrue\x1B[39m', + ] +}, { + input: ' \t { a: true};', + preview: [ + ' { a: true};\r', + '\x1B[33mtrue\x1B[39m', + ] +}, { + input: '1n + 2n', + preview: [ + '1n + 2n\r', + '\x1B[33m3n\x1B[39m', + ] +}, { + input: '{};1', + preview: [ + '{};1\r', + '\x1B[33m1\x1B[39m', + ], +}]; + +async function runTest() { + for (const { input, preview } of testCases) { + const toBeRun = input.split('\n'); + let lines = await runAndWait(toBeRun, repl); + // Remove error messages. That allows the code to run in different + // engines. + // eslint-disable-next-line no-control-regex + lines = lines.map((line) => line.replace(/Error: .+?\x1B/, '')); + assert.strictEqual(lines.pop(), '\x1B[1G\x1B[0Jrepl > \x1B[8G'); + assert.deepStrictEqual(lines, preview); + } +} + +runTest().then(common.mustCall());