Skip to content

disable REPL completion on proxies and getters #57909

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 87 additions & 42 deletions lib/repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ const {
globalThis,
} = primordials;

const {
isProxy,
} = require('internal/util/types');

const { BuiltinModule } = require('internal/bootstrap/realm');
const {
makeRequireFunction,
Expand Down Expand Up @@ -1328,8 +1332,10 @@ function completeFSFunctions(match) {
// -> [['util.print', 'util.debug', 'util.log', 'util.inspect'],
// 'util.' ]
//
// Warning: This eval's code like "foo.bar.baz", so it will run property
// getter code.
// Warning: This evals code like "foo.bar.baz", so it could run property
// getter code. To avoid potential triggering side-effects with getters the completion
// logic is skipped when getters or proxies are involved in the expression.
// (see: https://github.com/nodejs/node/issues/57829).
function complete(line, callback) {
// List of completion lists, one for each inheritance "level"
let completionGroups = [];
Expand Down Expand Up @@ -1525,50 +1531,61 @@ function complete(line, callback) {
return;
}

let chaining = '.';
if (StringPrototypeEndsWith(expr, '?')) {
expr = StringPrototypeSlice(expr, 0, -1);
chaining = '?.';
}

const memberGroups = [];
const evalExpr = `try { ${expr} } catch {}`;
this.eval(evalExpr, this.context, getREPLResourceName(), (e, obj) => {
try {
let p;
if ((typeof obj === 'object' && obj !== null) ||
typeof obj === 'function') {
ArrayPrototypePush(memberGroups, filteredOwnPropertyNames(obj));
p = ObjectGetPrototypeOf(obj);
} else {
p = obj.constructor ? obj.constructor.prototype : null;
return includesProxiesOrGetters(
StringPrototypeSplit(match, '.'),
this.eval,
this.context,
(includes) => {
if (includes) {
// The expression involves proxies or getters, meaning that it
// can trigger side-effectful behaviors, so bail out
return completionGroupsLoaded();
}
// Circular refs possible? Let's guard against that.
let sentinel = 5;
while (p !== null && sentinel-- !== 0) {
ArrayPrototypePush(memberGroups, filteredOwnPropertyNames(p));
p = ObjectGetPrototypeOf(p);

let chaining = '.';
if (StringPrototypeEndsWith(expr, '?')) {
expr = StringPrototypeSlice(expr, 0, -1);
chaining = '?.';
}
} catch {
// Maybe a Proxy object without `getOwnPropertyNames` trap.
// We simply ignore it here, as we don't want to break the
// autocompletion. Fixes the bug
// https://github.com/nodejs/node/issues/2119
}

if (memberGroups.length) {
expr += chaining;
ArrayPrototypeForEach(memberGroups, (group) => {
ArrayPrototypePush(completionGroups,
ArrayPrototypeMap(group,
(member) => `${expr}${member}`));
});
filter &&= `${expr}${filter}`;
}
const memberGroups = [];
const evalExpr = `try { ${expr} } catch {}`;
this.eval(evalExpr, this.context, getREPLResourceName(), (e, obj) => {
try {
let p;
if ((typeof obj === 'object' && obj !== null) ||
typeof obj === 'function') {
ArrayPrototypePush(memberGroups, filteredOwnPropertyNames(obj));
p = ObjectGetPrototypeOf(obj);
} else {
p = obj.constructor ? obj.constructor.prototype : null;
}
// Circular refs possible? Let's guard against that.
let sentinel = 5;
while (p !== null && sentinel-- !== 0) {
ArrayPrototypePush(memberGroups, filteredOwnPropertyNames(p));
p = ObjectGetPrototypeOf(p);
}
} catch {
// Maybe a Proxy object without `getOwnPropertyNames` trap.
// We simply ignore it here, as we don't want to break the
// autocompletion. Fixes the bug
// https://github.com/nodejs/node/issues/2119
}

completionGroupsLoaded();
});
return;
if (memberGroups.length) {
expr += chaining;
ArrayPrototypeForEach(memberGroups, (group) => {
ArrayPrototypePush(completionGroups,
ArrayPrototypeMap(group,
(member) => `${expr}${member}`));
});
filter &&= `${expr}${filter}`;
}

completionGroupsLoaded();
});
});
}

return completionGroupsLoaded();
Expand Down Expand Up @@ -1626,6 +1643,34 @@ function complete(line, callback) {
}
}

function includesProxiesOrGetters(exprSegments, evalFn, context, callback, currentExpr = '', idx = 0) {
const currentSegment = exprSegments[idx];
currentExpr += `${currentExpr.length === 0 ? '' : '.'}${currentSegment}`;
evalFn(`try { ${currentExpr} } catch { }`, context, getREPLResourceName(), (_, currentObj) => {
if (typeof currentObj !== 'object' || currentObj === null) {
return callback(false);
}

if (isProxy(currentObj)) {
return callback(true);
}

const nextIdx = idx + 1;

if (nextIdx >= exprSegments.length) {
return callback(false);
}

const nextSegmentProp = ObjectGetOwnPropertyDescriptor(currentObj, exprSegments[nextIdx]);
const nextSegmentPropHasGetter = typeof nextSegmentProp?.get === 'function';
if (nextSegmentPropHasGetter) {
return callback(true);
}

return includesProxiesOrGetters(exprSegments, evalFn, context, callback, currentExpr, nextIdx);
});
}

REPLServer.prototype.completeOnEditorMode = (callback) => (err, results) => {
if (err) return callback(err);

Expand Down
119 changes: 119 additions & 0 deletions test/parallel/test-repl-completion-on-getters-disabled.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
'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.', []],
]);
});
});
});
Loading
Loading