Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
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 129 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 119 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
Morty Proxy This is a proxified and sanitized view of the page, visit original site.