mirror of
https://github.com/zebrajr/node.git
synced 2025-12-06 12:20:27 +01:00
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 in07220230d9and the problem exacerbated in8ba66c5e7b. Our team noticed this because our tests started failing when attempting to update to Node.js 20.19.5. Some recent commits, such as1093f38c43or69453378fc, 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>
170 lines
5.7 KiB
JavaScript
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"].', []],
|
|
]);
|
|
});
|
|
});
|
|
});
|