Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions lib/repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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...)
Expand Down
141 changes: 141 additions & 0 deletions test/parallel/test-repl-tab-complete-unary-expressions.js
Original file line number Diff line number Diff line change
@@ -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();
})
);
})
);
});
});
Loading