Skip to content

Commit 422d0e3

Browse files
committed
unuse completeDomain and disable completion on call expression
1 parent 86b5a9b commit 422d0e3

7 files changed

+69
-43
lines changed

lib/repl.js

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,6 @@ function REPLServer(prompt,
353353
this.allowBlockingCompletions = !!options.allowBlockingCompletions;
354354
this.useColors = !!options.useColors;
355355
this._domain = options.domain || domain.create();
356-
this._completeDomain = domain.create();
357356
this.useGlobal = !!useGlobal;
358357
this.ignoreUndefined = !!ignoreUndefined;
359358
this.replMode = replMode || module.exports.REPL_MODE_SLOPPY;
@@ -671,8 +670,6 @@ function REPLServer(prompt,
671670
}
672671

673672
self.eval = self._domain.bind(eval_);
674-
self.completeEval = self._completeDomain.bind(eval_);
675-
self._completeDomain.on('error', (err) => { });
676673

677674
self._domain.on('error', function debugDomainError(e) {
678675
debug('domain error');
@@ -1543,10 +1540,10 @@ function complete(line, callback) {
15431540
return completionGroupsLoaded();
15441541
}
15451542

1546-
return includesProxiesOrGetters(
1543+
return potentiallySideEffectfulAccess(
15471544
completeTargetAst.body[0].expression,
15481545
parsableCompleteTarget,
1549-
this.completeEval,
1546+
this.eval,
15501547
this.context,
15511548
(includes) => {
15521549
if (includes) {
@@ -1563,7 +1560,7 @@ function complete(line, callback) {
15631560

15641561
const memberGroups = [];
15651562
const evalExpr = `try { ${expr} } catch {}`;
1566-
this.completeEval(evalExpr, this.context, getREPLResourceName(), (e, obj) => {
1563+
this.eval(evalExpr, this.context, getREPLResourceName(), (e, obj) => {
15671564
try {
15681565
reclusiveCatchPromise(obj);
15691566
let p;
@@ -1756,10 +1753,11 @@ function findExpressionCompleteTarget(code) {
17561753
}
17571754

17581755
/**
1759-
* Utility used to determine if an expression includes object getters or proxies.
1756+
* Utility to determine if accessing a given expression could have side effects.
17601757
*
1761-
* Example: given `obj.foo`, the function lets you know if `foo` has a getter function
1762-
* associated to it, or if `obj` is a proxy
1758+
* Example: given `obj.foo`, this function checks if accessing `foo` may trigger a getter,
1759+
* or if any part of the chain is a Proxy, or if evaluating the property could cause side effects.
1760+
* This is used to avoid triggering user code or side effects during tab completion.
17631761
* @param {any} expr The expression, in AST format to analyze
17641762
* @param {string} exprStr The string representation of the expression
17651763
* @param {(str: string, ctx: any, resourceName: string, cb: (error, evaled) => void) => void} evalFn
@@ -1768,15 +1766,19 @@ function findExpressionCompleteTarget(code) {
17681766
* @param {(includes: boolean) => void} callback Callback that will be called with the result of the operation
17691767
* @returns {void}
17701768
*/
1771-
function includesProxiesOrGetters(expr, exprStr, evalFn, ctx, callback) {
1769+
function potentiallySideEffectfulAccess(expr, exprStr, evalFn, ctx, callback) {
17721770
if (expr?.type !== 'MemberExpression') {
17731771
// If the expression is not a member one for obvious reasons no getters are involved
17741772
return callback(false);
17751773
}
17761774

1775+
if (expr.object.type === 'CallExpression' || expr.property.type === 'CallExpression') {
1776+
return callback(true);
1777+
}
1778+
17771779
if (expr.object.type === 'MemberExpression') {
17781780
// The object itself is a member expression, so we need to recurse (e.g. the expression is `obj.foo.bar`)
1779-
return includesProxiesOrGetters(
1781+
return potentiallySideEffectfulAccess(
17801782
expr.object,
17811783
exprStr.slice(0, expr.object.end),
17821784
evalFn,

test/fixtures/repl-tab-completion-nested-repls.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const putIn = new ArrayStream();
3232
const testMe = repl.start('', putIn);
3333

3434
// Some errors are passed to the domain, but do not callback.
35-
testMe._completeDomain.on('error', function(err) {
35+
testMe._domain.on('error', function(err) {
3636
throw err;
3737
});
3838

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('node:assert');
5+
const { test } = require('node:test');
6+
7+
8+
const ArrayStream = require('../common/arraystream');
9+
const repl = require('node:repl');
10+
11+
function runCompletionTests(replInit, tests) {
12+
const stream = new ArrayStream();
13+
const testRepl = repl.start({ stream });
14+
15+
// Some errors are passed to the domain
16+
testRepl._domain.on('error', assert.ifError);
17+
18+
testRepl.write(replInit);
19+
testRepl.write('\n');
20+
21+
tests.forEach(([query, expectedCompletions]) => {
22+
testRepl.complete(query, common.mustCall((error, data) => {
23+
const actualCompletions = data[0];
24+
if (expectedCompletions.length === 0) {
25+
assert.deepStrictEqual(actualCompletions, []);
26+
} else {
27+
expectedCompletions.forEach((expectedCompletion) =>
28+
assert(actualCompletions.includes(expectedCompletion), `completion '${expectedCompletion}' not found`)
29+
);
30+
}
31+
}));
32+
});
33+
}
34+
35+
test('REPL completion on function disabled', () => {
36+
runCompletionTests(`
37+
function foo() { return { a: { b: 5 } } }
38+
const obj = { a: 5 }
39+
const getKey = () => 'a';
40+
`, [
41+
['foo().', []],
42+
['foo().a', []],
43+
['foo().a.', []],
44+
['foo()["a"]', []],
45+
['foo()["a"].', []],
46+
['foo()["a"].b', []],
47+
['obj[getKey()].', []],
48+
]);
49+
50+
});

test/parallel/test-repl-completion-on-getters-disabled.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ describe('REPL completion in relation of getters', () => {
9090
["objWithGetters[keys['foo key']].b", ["objWithGetters[keys['foo key']].bar"]],
9191
['objWithGetters[fooKey].b', ['objWithGetters[fooKey].bar']],
9292
["objWithGetters['f' + 'oo'].b", ["objWithGetters['f' + 'oo'].bar"]],
93-
['objWithGetters[getFooKey()].b', ['objWithGetters[getFooKey()].bar']],
9493
]);
9594
});
9695

test/parallel/test-repl-tab-complete-getter-error.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ async function runTest() {
2121
terminal: true
2222
});
2323

24-
replServer._completeDomain.on('error', (e) => {
24+
replServer._domain.on('error', (e) => {
2525
assert.fail(`Error in REPL domain: ${e}`);
2626
});
2727

test/parallel/test-repl-tab-complete-promises.js

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ const completionTests = [
1919
{ run: `const foo = { get name() { return Promise.reject(); } };`,
2020
send: `foo.name` },
2121
{ run: 'const baz = { get bar() { return ""; } }; const getPropText = () => Promise.reject();',
22-
send: 'baz[getPropText()].',
23-
completeError: true },
22+
send: 'baz[getPropText()].' },
2423
{
2524
send: 'const quux = { bar: { return Promise.reject(); } }; const getPropText = () => "bar"; quux[getPropText()].',
2625
},
@@ -46,7 +45,7 @@ async function runReplCompleteTests(tests) {
4645
assert.fail(`Unexpected domain error: ${err.message}`);
4746
});
4847

49-
for (const { send, run, completeError = false } of tests) {
48+
for (const { send, run } of tests) {
5049
if (run) {
5150
await new Promise((resolve, reject) => {
5251
replServer.eval(run, replServer.context, '', (err) => {
@@ -59,23 +58,7 @@ async function runReplCompleteTests(tests) {
5958
});
6059
}
6160

62-
const onError = (e) => {
63-
assert.fail(`Unexpected error: ${e.message}`);
64-
};
65-
66-
let completeErrorPromise = Promise.resolve();
67-
68-
if (completeError) {
69-
completeErrorPromise = new Promise((resolve) => {
70-
const handleError = () => {
71-
replServer._completeDomain.removeListener('error', handleError);
72-
resolve();
73-
};
74-
replServer._completeDomain.on('error', common.mustCall(handleError));
75-
});
76-
} else {
77-
replServer._completeDomain.on('error', onError);
78-
}
61+
const completeErrorPromise = Promise.resolve();
7962

8063
await replServer.complete(
8164
send,
@@ -89,13 +72,5 @@ async function runReplCompleteTests(tests) {
8972

9073
await completeErrorPromise;
9174

92-
if (!completeError) {
93-
await new Promise((resolve) => {
94-
setImmediate(() => {
95-
replServer._completeDomain.removeListener('error', onError);
96-
resolve();
97-
});
98-
});
99-
}
10075
}
10176
}

test/parallel/test-repl-tab-complete.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ function prepareREPL() {
4444
});
4545

4646
// Some errors are passed to the domain, but do not callback
47-
replServer._completeDomain.on('error', assert.ifError);
47+
replServer._domain.on('error', assert.ifError);
4848

4949
return { replServer, input };
5050
}

0 commit comments

Comments
 (0)