node/test/parallel/test-repl-completion-on-getters-disabled.js
Dario Piotrowicz aad8c05595
repl: fix getters triggering side effects during completion
PR-URL: https://github.com/nodejs/node/pull/61043
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: Aviv Keller <me@aviv.sh>
2025-12-20 18:54:12 +00:00

189 lines
6.5 KiB
JavaScript

'use strict';
const common = require('../common');
const assert = require('node:assert');
const { describe, test } = require('node:test');
const { startNewREPLServer } = require('../common/repl');
function runCompletionTests(replInit, tests) {
const { replServer: testRepl, input } = startNewREPLServer();
input.run([replInit]);
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', []],
]);
});
test('no side effects are triggered for getters during completion', async () => {
const { replServer } = startNewREPLServer();
await new Promise((resolve, reject) => {
replServer.eval('const foo = { get name() { globalThis.nameGetterRun = true; throw new Error(); } };',
replServer.context, '', (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
['foo.name.', 'foo["name"].'].forEach((test) => {
replServer.complete(
test,
common.mustCall((error, data) => {
// The context's nameGetterRun variable hasn't been set
assert.strictEqual(replServer.context.nameGetterRun, undefined);
// No errors has been thrown
assert.strictEqual(error, null);
})
);
});
});
});
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"].', []],
]);
});
});
});