diff --git a/lib/repl.js b/lib/repl.js index 3d66e928601f07..b95beb45643eb2 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -1744,6 +1744,16 @@ function findExpressionCompleteTarget(code) { return findExpressionCompleteTarget(lastDeclarationInitCode); } + // If the last statement is an expression statement with a unary operator (delete, typeof, etc.) + // we want to extract the argument for completion (e.g. for `delete obj.prop` we want `obj.prop`) + if (lastBodyStatement.type === 'ExpressionStatement' && + lastBodyStatement.expression.type === 'UnaryExpression' && + lastBodyStatement.expression.argument) { + const argument = lastBodyStatement.expression.argument; + const argumentCode = code.slice(argument.start, argument.end); + return findExpressionCompleteTarget(argumentCode); + } + // If any of the above early returns haven't activated then it means that // the potential complete target is the full code (e.g. the code represents // a simple partial identifier, a member expression, etc...) diff --git a/test/parallel/test-repl-tab-complete-unary-expressions.js b/test/parallel/test-repl-tab-complete-unary-expressions.js new file mode 100644 index 00000000000000..d84f0672b98151 --- /dev/null +++ b/test/parallel/test-repl-tab-complete-unary-expressions.js @@ -0,0 +1,141 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const repl = require('repl'); +const { describe, it } = require('node:test'); + +// This test verifies that tab completion works correctly with unary expressions +// like delete, typeof, void, etc. This is a regression test for the issue where +// typing "delete globalThis._" and then backspacing and typing "globalThis" +// would cause "globalThis is not defined" error. + +describe('REPL tab completion with unary expressions', () => { + it('should handle delete operator correctly', (t, done) => { + const r = repl.start({ + prompt: '', + input: process.stdin, + output: process.stdout, + terminal: false, + }); + + // Test delete with member expression + r.complete( + 'delete globalThis._', + common.mustSucceed((completions) => { + assert.strictEqual(completions[1], 'globalThis._'); + + // Test delete with identifier + r.complete( + 'delete globalThis', + common.mustSucceed((completions) => { + assert.strictEqual(completions[1], 'globalThis'); + r.close(); + done(); + }) + ); + }) + ); + }); + + it('should handle typeof operator correctly', (t, done) => { + const r = repl.start({ + prompt: '', + input: process.stdin, + output: process.stdout, + terminal: false, + }); + + r.complete( + 'typeof globalThis', + common.mustSucceed((completions) => { + assert.strictEqual(completions[1], 'globalThis'); + r.close(); + done(); + }) + ); + }); + + it('should handle void operator correctly', (t, done) => { + const r = repl.start({ + prompt: '', + input: process.stdin, + output: process.stdout, + terminal: false, + }); + + r.complete( + 'void globalThis', + common.mustSucceed((completions) => { + assert.strictEqual(completions[1], 'globalThis'); + r.close(); + done(); + }) + ); + }); + + it('should handle other unary operators correctly', (t, done) => { + const r = repl.start({ + prompt: '', + input: process.stdin, + output: process.stdout, + terminal: false, + }); + + const unaryOperators = [ + '!globalThis', + '+globalThis', + '-globalThis', + '~globalThis', + ]; + + let testIndex = 0; + + function testNext() { + if (testIndex >= unaryOperators.length) { + r.close(); + done(); + return; + } + + const testCase = unaryOperators[testIndex++]; + r.complete( + testCase, + common.mustSucceed((completions) => { + assert.strictEqual(completions[1], 'globalThis'); + testNext(); + }) + ); + } + + testNext(); + }); + + it('should still evaluate globalThis correctly after unary expression completion', (t, done) => { + const r = repl.start({ + prompt: '', + input: process.stdin, + output: process.stdout, + terminal: false, + }); + + // First trigger completion with delete + r.complete( + 'delete globalThis._', + common.mustSucceed(() => { + // Then evaluate globalThis + r.eval( + 'globalThis', + r.context, + 'test.js', + common.mustSucceed((result) => { + assert.strictEqual(typeof result, 'object'); + assert.ok(result !== null); + r.close(); + done(); + }) + ); + }) + ); + }); +});