node/test/parallel/test-repl-completion-on-getters-disabled.js
Anna Henningsen 6cf64af44d
repl: do not cause side effects in tab completion
A number of recent changes to the REPL tab completion logic have
introduced the ability for completion to cause side effects,
specifically, calling arbitrary functions or variable
assignments/updates.

This was first introduced in 07220230d9 and the problem exacerbated in
8ba66c5e7b. Our team noticed this because our tests started failing
when attempting to update to Node.js 20.19.5.

Some recent commits, such as 1093f38c43 or 69453378fc, have
messages or PR descriptions that imply the intention to avoid side
effects, which I can can generally be agreed upon is in line with the
expectations that a user has of autocomplete functionality.
However, some of the tests introduced in those commts specifically
verify that side effects *can* happen under specific circunmstances.
I am assuming here that this is unintentional, and the corresponding
tests have been removed/replaced in this commit.

Fixes: https://github.com/nodejs/node/issues/59731
Fixes: https://github.com/nodejs/node/issues/58903
Refs: https://github.com/nodejs/node/pull/58709
Refs: https://github.com/nodejs/node/pull/58775
Refs: https://github.com/nodejs/node/pull/57909
Refs: https://github.com/nodejs/node/pull/58891
PR-URL: https://github.com/nodejs/node/pull/59774
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Dario Piotrowicz <dario.piotrowicz@gmail.com>
2025-09-08 13:54:45 +00:00

170 lines
5.7 KiB
JavaScript

'use strict';
const common = require('../common');
const assert = require('node:assert');
const { describe, test } = require('node:test');
const ArrayStream = require('../common/arraystream');
const repl = require('node:repl');
function runCompletionTests(replInit, tests) {
const stream = new ArrayStream();
const testRepl = repl.start({ stream });
// Some errors are passed to the domain
testRepl._domain.on('error', assert.ifError);
testRepl.write(replInit);
testRepl.write('\n');
tests.forEach(([query, expectedCompletions]) => {
testRepl.complete(query, common.mustCall((error, data) => {
const actualCompletions = data[0];
if (expectedCompletions.length === 0) {
assert.deepStrictEqual(actualCompletions, []);
} else {
expectedCompletions.forEach((expectedCompletion) =>
assert(actualCompletions.includes(expectedCompletion), `completion '${expectedCompletion}' not found`)
);
}
}));
});
}
describe('REPL completion in relation of getters', () => {
describe('standard behavior without proxies/getters', () => {
test('completion of nested properties of an undeclared objects', () => {
runCompletionTests('', [
['nonExisting.', []],
['nonExisting.f', []],
['nonExisting.foo', []],
['nonExisting.foo.', []],
['nonExisting.foo.bar.b', []],
]);
});
test('completion of nested properties on plain objects', () => {
runCompletionTests('const plainObj = { foo: { bar: { baz: {} } } };', [
['plainObj.', ['plainObj.foo']],
['plainObj.f', ['plainObj.foo']],
['plainObj.foo', ['plainObj.foo']],
['plainObj.foo.', ['plainObj.foo.bar']],
['plainObj.foo.bar.b', ['plainObj.foo.bar.baz']],
['plainObj.fooBar.', []],
['plainObj.fooBar.baz', []],
]);
});
});
describe('completions on an object with getters', () => {
test(`completions are generated for properties that don't trigger getters`, () => {
runCompletionTests(
`
const fooKey = "foo";
const keys = {
"foo key": "foo",
};
const objWithGetters = {
foo: { bar: { baz: { buz: {} } }, get gBar() { return { baz: {} } } },
get gFoo() { return { bar: { baz: {} } }; }
};
`, [
['objWithGetters.', ['objWithGetters.foo']],
['objWithGetters.f', ['objWithGetters.foo']],
['objWithGetters.foo', ['objWithGetters.foo']],
['objWithGetters["foo"].b', ['objWithGetters["foo"].bar']],
['objWithGetters.foo.', ['objWithGetters.foo.bar']],
['objWithGetters.foo.bar.b', ['objWithGetters.foo.bar.baz']],
['objWithGetters.gFo', ['objWithGetters.gFoo']],
['objWithGetters.foo.gB', ['objWithGetters.foo.gBar']],
["objWithGetters.foo['bar'].b", ["objWithGetters.foo['bar'].baz"]],
["objWithGetters['foo']['bar'].b", ["objWithGetters['foo']['bar'].baz"]],
["objWithGetters['foo']['bar']['baz'].b", ["objWithGetters['foo']['bar']['baz'].buz"]],
["objWithGetters[keys['foo key']].b", ["objWithGetters[keys['foo key']].bar"]],
['objWithGetters[fooKey].b', ['objWithGetters[fooKey].bar']],
["objWithGetters['f' + 'oo'].b", ["objWithGetters['f' + 'oo'].bar"]],
]);
});
test('no completions are generated for properties that trigger getters', () => {
runCompletionTests(
`
function getGFooKey() {
return "g" + "Foo";
}
const gFooKey = "gFoo";
const keys = {
"g-foo key": "gFoo",
};
const objWithGetters = {
foo: { bar: { baz: {} }, get gBar() { return { baz: {}, get gBuz() { return 5; } } } },
get gFoo() { return { bar: { baz: {} } }; }
};
`,
[
['objWithGetters.gFoo.', []],
['objWithGetters.gFoo.b', []],
['objWithGetters["gFoo"].b', []],
['objWithGetters.gFoo.bar.b', []],
['objWithGetters.foo.gBar.', []],
['objWithGetters.foo.gBar.b', []],
["objWithGetters.foo['gBar'].b", []],
["objWithGetters['foo']['gBar'].b", []],
["objWithGetters['foo']['gBar']['gBuz'].", []],
["objWithGetters[keys['g-foo key']].b", []],
['objWithGetters[gFooKey].b', []],
["objWithGetters['g' + 'Foo'].b", []],
['objWithGetters[getGFooKey()].b', []],
]);
});
});
describe('completions on proxies', () => {
test('no completions are generated for a proxy object', () => {
runCompletionTests(
`
function getFooKey() {
return "foo";
}
const fooKey = "foo";
const keys = {
"foo key": "foo",
};
const proxyObj = new Proxy({ foo: { bar: { baz: {} } } }, {});
`, [
['proxyObj.', []],
['proxyObj.f', []],
['proxyObj.foo', []],
['proxyObj.foo.', []],
['proxyObj.["foo"].', []],
['proxyObj.["f" + "oo"].', []],
['proxyObj.[fooKey].', []],
['proxyObj.[getFooKey()].', []],
['proxyObj.[keys["foo key"]].', []],
['proxyObj.foo.bar.b', []],
]);
});
test('no completions are generated for a proxy present in a standard object', () => {
runCompletionTests(
'const objWithProxy = { foo: { bar: new Proxy({ baz: {} }, {}) } };', [
['objWithProxy.', ['objWithProxy.foo']],
['objWithProxy.foo', ['objWithProxy.foo']],
['objWithProxy.foo.', ['objWithProxy.foo.bar']],
['objWithProxy.foo.b', ['objWithProxy.foo.bar']],
['objWithProxy.foo.bar.', []],
['objWithProxy.foo["b" + "ar"].', []],
]);
});
});
});