nodejs/test/parallel/test-repl-completion-on-getters-disabled.js
Dario Piotrowicz 1093f38c43
lib: disable REPL completion on proxies and getters
the REPL completion logic evaluates code, this is generally ok
but it can be problematic when there are objects with nested
properties since the code evaluation would trigger their potential
getters (e.g. the `obj.foo.b` line would trigger the getter of
`foo`), the changes here disable the completion logic when proxies
and getters are involved thus making sure that code evaluation does
not take place when it can potentially (and surprisingly to the user)
trigger side effectful behaviors

PR-URL: https://github.com/nodejs/node/pull/57909
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
2025-06-04 12:13:41 +00:00

120 lines
4.0 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 objWithGetters = {
foo: { bar: { baz: {} }, get gBar() { return { baz: {} } } },
get gFoo() { return { bar: { baz: {} } }; }
};
`, [
['objWithGetters.', ['objWithGetters.foo']],
['objWithGetters.f', ['objWithGetters.foo']],
['objWithGetters.foo', ['objWithGetters.foo']],
['objWithGetters.foo.', ['objWithGetters.foo.bar']],
['objWithGetters.foo.bar.b', ['objWithGetters.foo.bar.baz']],
['objWithGetters.gFo', ['objWithGetters.gFoo']],
['objWithGetters.foo.gB', ['objWithGetters.foo.gBar']],
]);
});
test('no completions are generated for properties that trigger getters', () => {
runCompletionTests(
`
const objWithGetters = {
foo: { bar: { baz: {} }, get gBar() { return { baz: {} } } },
get gFoo() { return { bar: { baz: {} } }; }
};
`,
[
['objWithGetters.gFoo.', []],
['objWithGetters.gFoo.b', []],
['objWithGetters.gFoo.bar.b', []],
['objWithGetters.foo.gBar.', []],
['objWithGetters.foo.gBar.b', []],
]);
});
});
describe('completions on proxies', () => {
test('no completions are generated for a proxy object', () => {
runCompletionTests('const proxyObj = new Proxy({ foo: { bar: { baz: {} } } }, {});', [
['proxyObj.', []],
['proxyObj.f', []],
['proxyObj.foo', []],
['proxyObj.foo.', []],
['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.', []],
]);
});
});
});