node/test/parallel/test-repl-completion-on-getters-disabled.js
Dario Piotrowicz 69453378fc
repl: improve REPL disabling completion on proxies and getters
https://github.com/nodejs/node/pull/57909 introduced the disabling
of REPL tab completion on object containing proxies and getters
(since such completion triggers code evaluation which can be
unexpected/disruptive for the user)

the solution in 57909 did not address all possible such cases,
the changes here improve on such solution by using acorn and
AST analysis to cover most if not all possible cases

PR-URL: https://github.com/nodejs/node/pull/58891
Reviewed-By: James M Snell <jasnell@gmail.com>
2025-07-09 20:52:25 +00:00

174 lines
5.9 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(
`
function getFooKey() {
return "foo";
}
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"]],
['objWithGetters[getFooKey()].b', ['objWithGetters[getFooKey()].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"].', []],
]);
});
});
});