lib: disable REPL completion on proxies and getters · nodejs/node@29f34a7
@@ -96,6 +96,10 @@ const {
9696 globalThis,
9797} = primordials;
989899+const {
100+ isProxy,
101+} = require('internal/util/types');
102+99103const { BuiltinModule } = require('internal/bootstrap/realm');
100104const {
101105 makeRequireFunction,
@@ -1328,8 +1332,10 @@ function completeFSFunctions(match) {
13281332// -> [['util.print', 'util.debug', 'util.log', 'util.inspect'],
13291333// 'util.' ]
13301334//
1331-// Warning: This eval's code like "foo.bar.baz", so it will run property
1332-// getter code.
1335+// Warning: This evals code like "foo.bar.baz", so it could run property
1336+// getter code. To avoid potential triggering side-effects with getters the completion
1337+// logic is skipped when getters or proxies are involved in the expression.
1338+// (see: https://github.com/nodejs/node/issues/57829).
13331339function complete(line, callback) {
13341340// List of completion lists, one for each inheritance "level"
13351341let completionGroups = [];
@@ -1525,50 +1531,61 @@ function complete(line, callback) {
15251531return;
15261532}
152715331528-let chaining = '.';
1529-if (StringPrototypeEndsWith(expr, '?')) {
1530-expr = StringPrototypeSlice(expr, 0, -1);
1531-chaining = '?.';
1532-}
1533-1534-const memberGroups = [];
1535-const evalExpr = `try { ${expr} } catch {}`;
1536-this.eval(evalExpr, this.context, getREPLResourceName(), (e, obj) => {
1537-try {
1538-let p;
1539-if ((typeof obj === 'object' && obj !== null) ||
1540-typeof obj === 'function') {
1541-ArrayPrototypePush(memberGroups, filteredOwnPropertyNames(obj));
1542-p = ObjectGetPrototypeOf(obj);
1543-} else {
1544-p = obj.constructor ? obj.constructor.prototype : null;
1534+return includesProxiesOrGetters(
1535+StringPrototypeSplit(match, '.'),
1536+this.eval,
1537+this.context,
1538+(includes) => {
1539+if (includes) {
1540+// The expression involves proxies or getters, meaning that it
1541+// can trigger side-effectful behaviors, so bail out
1542+return completionGroupsLoaded();
15451543}
1546- // Circular refs possible? Let's guard against that.
1547-let sentinel = 5;
1548-while (p !== null && sentinel-- !== 0) {
1549-ArrayPrototypePush(memberGroups, filteredOwnPropertyNames(p));
1550-p = ObjectGetPrototypeOf(p);
1544+1545+let chaining = '.';
1546+if (StringPrototypeEndsWith(expr, '?')) {
1547+expr = StringPrototypeSlice(expr, 0, -1);
1548+chaining = '?.';
15511549}
1552-} catch {
1553-// Maybe a Proxy object without `getOwnPropertyNames` trap.
1554-// We simply ignore it here, as we don't want to break the
1555-// autocompletion. Fixes the bug
1556-// https://github.com/nodejs/node/issues/2119
1557-}
155815501559-if (memberGroups.length) {
1560-expr += chaining;
1561-ArrayPrototypeForEach(memberGroups, (group) => {
1562-ArrayPrototypePush(completionGroups,
1563-ArrayPrototypeMap(group,
1564-(member) => `${expr}${member}`));
1565-});
1566-filter &&= `${expr}${filter}`;
1567-}
1551+const memberGroups = [];
1552+const evalExpr = `try { ${expr} } catch {}`;
1553+this.eval(evalExpr, this.context, getREPLResourceName(), (e, obj) => {
1554+try {
1555+let p;
1556+if ((typeof obj === 'object' && obj !== null) ||
1557+typeof obj === 'function') {
1558+ArrayPrototypePush(memberGroups, filteredOwnPropertyNames(obj));
1559+p = ObjectGetPrototypeOf(obj);
1560+} else {
1561+p = obj.constructor ? obj.constructor.prototype : null;
1562+}
1563+// Circular refs possible? Let's guard against that.
1564+let sentinel = 5;
1565+while (p !== null && sentinel-- !== 0) {
1566+ArrayPrototypePush(memberGroups, filteredOwnPropertyNames(p));
1567+p = ObjectGetPrototypeOf(p);
1568+}
1569+} catch {
1570+// Maybe a Proxy object without `getOwnPropertyNames` trap.
1571+// We simply ignore it here, as we don't want to break the
1572+// autocompletion. Fixes the bug
1573+// https://github.com/nodejs/node/issues/2119
1574+}
156815751569-completionGroupsLoaded();
1570-});
1571-return;
1576+if (memberGroups.length) {
1577+expr += chaining;
1578+ArrayPrototypeForEach(memberGroups, (group) => {
1579+ArrayPrototypePush(completionGroups,
1580+ArrayPrototypeMap(group,
1581+(member) => `${expr}${member}`));
1582+});
1583+filter &&= `${expr}${filter}`;
1584+}
1585+1586+completionGroupsLoaded();
1587+});
1588+});
15721589}
1573159015741591return completionGroupsLoaded();
@@ -1626,6 +1643,34 @@ function complete(line, callback) {
16261643}
16271644}
162816451646+function includesProxiesOrGetters(exprSegments, evalFn, context, callback, currentExpr = '', idx = 0) {
1647+const currentSegment = exprSegments[idx];
1648+currentExpr += `${currentExpr.length === 0 ? '' : '.'}${currentSegment}`;
1649+evalFn(`try { ${currentExpr} } catch { }`, context, getREPLResourceName(), (_, currentObj) => {
1650+if (typeof currentObj !== 'object' || currentObj === null) {
1651+return callback(false);
1652+}
1653+1654+if (isProxy(currentObj)) {
1655+return callback(true);
1656+}
1657+1658+const nextIdx = idx + 1;
1659+1660+if (nextIdx >= exprSegments.length) {
1661+return callback(false);
1662+}
1663+1664+const nextSegmentProp = ObjectGetOwnPropertyDescriptor(currentObj, exprSegments[nextIdx]);
1665+const nextSegmentPropHasGetter = typeof nextSegmentProp?.get === 'function';
1666+if (nextSegmentPropHasGetter) {
1667+return callback(true);
1668+}
1669+1670+return includesProxiesOrGetters(exprSegments, evalFn, context, callback, currentExpr, nextIdx);
1671+});
1672+}
1673+16291674REPLServer.prototype.completeOnEditorMode = (callback) => (err, results) => {
16301675if (err) return callback(err);
16311676