Skip to content

Commit 7a4b912

Browse files
committed
repl: catch promise errors during eval in completion
Fixes: #58903
1 parent ab694d5 commit 7a4b912

File tree

4 files changed

+137
-5
lines changed

4 files changed

+137
-5
lines changed

lib/repl.js

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
'use strict';
4444

4545
const {
46+
ArrayIsArray,
4647
ArrayPrototypeAt,
4748
ArrayPrototypeFilter,
4849
ArrayPrototypeFindLastIndex,
@@ -98,6 +99,7 @@ const {
9899

99100
const {
100101
isProxy,
102+
isPromise,
101103
} = require('internal/util/types');
102104

103105
const { BuiltinModule } = require('internal/bootstrap/realm');
@@ -351,6 +353,7 @@ function REPLServer(prompt,
351353
this.allowBlockingCompletions = !!options.allowBlockingCompletions;
352354
this.useColors = !!options.useColors;
353355
this._domain = options.domain || domain.create();
356+
this._completeDomain = domain.create();
354357
this.useGlobal = !!useGlobal;
355358
this.ignoreUndefined = !!ignoreUndefined;
356359
this.replMode = replMode || module.exports.REPL_MODE_SLOPPY;
@@ -668,6 +671,8 @@ function REPLServer(prompt,
668671
}
669672

670673
self.eval = self._domain.bind(eval_);
674+
self.completeEval = self._completeDomain.bind(eval_);
675+
self._completeDomain.on('error', (err) => { });
671676

672677
self._domain.on('error', function debugDomainError(e) {
673678
debug('domain error');
@@ -1541,7 +1546,7 @@ function complete(line, callback) {
15411546
return includesProxiesOrGetters(
15421547
completeTargetAst.body[0].expression,
15431548
parsableCompleteTarget,
1544-
this.eval,
1549+
this.completeEval,
15451550
this.context,
15461551
(includes) => {
15471552
if (includes) {
@@ -1558,8 +1563,10 @@ function complete(line, callback) {
15581563

15591564
const memberGroups = [];
15601565
const evalExpr = `try { ${expr} } catch {}`;
1561-
this.eval(evalExpr, this.context, getREPLResourceName(), (e, obj) => {
1566+
this.completeEval(evalExpr, this.context, getREPLResourceName(), (e, obj) => {
15621567
try {
1568+
reclusiveCatchPromise(obj);
1569+
15631570
let p;
15641571
if ((typeof obj === 'object' && obj !== null) ||
15651572
typeof obj === 'function') {
@@ -1800,6 +1807,7 @@ function includesProxiesOrGetters(expr, exprStr, evalFn, ctx, callback) {
18001807
// is the property identifier/literal)
18011808
if (expr.object.type === 'Identifier') {
18021809
return evalFn(`try { ${expr.object.name} } catch {}`, ctx, getREPLResourceName(), (err, obj) => {
1810+
reclusiveCatchPromise(obj);
18031811
if (err) {
18041812
return callback(false);
18051813
}
@@ -1815,6 +1823,7 @@ function includesProxiesOrGetters(expr, exprStr, evalFn, ctx, callback) {
18151823

18161824
return evalFn(
18171825
`try { ${exprStr} } catch {} `, ctx, getREPLResourceName(), (err, obj) => {
1826+
reclusiveCatchPromise(obj);
18181827
if (err) {
18191828
return callback(false);
18201829
}
@@ -1877,6 +1886,7 @@ function includesProxiesOrGetters(expr, exprStr, evalFn, ctx, callback) {
18771886
ctx,
18781887
getREPLResourceName(),
18791888
(err, evaledProp) => {
1889+
reclusiveCatchPromise(evaledProp);
18801890
if (err) {
18811891
return callback(false);
18821892
}
@@ -1902,7 +1912,9 @@ function includesProxiesOrGetters(expr, exprStr, evalFn, ctx, callback) {
19021912
function safeIsProxyAccess(obj, prop) {
19031913
// Accessing `prop` may trigger a getter that throws, so we use try-catch to guard against it
19041914
try {
1905-
return isProxy(obj[prop]);
1915+
const value = obj[prop];
1916+
reclusiveCatchPromise(value);
1917+
return isProxy(value);
19061918
} catch {
19071919
return false;
19081920
}
@@ -1911,6 +1923,29 @@ function includesProxiesOrGetters(expr, exprStr, evalFn, ctx, callback) {
19111923
return callback(false);
19121924
}
19131925

1926+
function reclusiveCatchPromise(obj, seen = new SafeWeakSet()) {
1927+
if (isPromise(obj)) {
1928+
return obj.catch(() => {});
1929+
} else if (ArrayIsArray(obj)) {
1930+
obj.forEach((item) => {
1931+
reclusiveCatchPromise(item, seen);
1932+
});
1933+
} else if (obj && typeof obj === 'object') {
1934+
if (seen.has(obj)) return;
1935+
seen.add(obj);
1936+
1937+
let props;
1938+
try {
1939+
props = ObjectGetOwnPropertyNames(obj);
1940+
} catch {
1941+
return;
1942+
}
1943+
props.forEach((key) => {
1944+
reclusiveCatchPromise(obj[key], seen);
1945+
});
1946+
}
1947+
}
1948+
19141949
REPLServer.prototype.completeOnEditorMode = (callback) => (err, results) => {
19151950
if (err) return callback(err);
19161951

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._domain.on('error', function(err) {
35+
testMe._completeDomain.on('error', function(err) {
3636
throw err;
3737
});
3838

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const repl = require('repl');
5+
const ArrayStream = require('../common/arraystream');
6+
const assert = require('assert');
7+
8+
const completionTests = [
9+
{ send: 'Promise.reject().' },
10+
{ send: 'let p = Promise.reject().' },
11+
{ send: 'Promise.resolve().' },
12+
{ send: 'Promise.resolve().then(() => {}).' },
13+
{ send: `async function f() {throw new Error('test');}; f().` },
14+
{ send: `async function f() {}; f().` },
15+
{ send: 'const foo = { bar: Promise.reject() }; foo.bar.' },
16+
// Test for that reclusiveCatchPromise does not infinitely recurse
17+
// see lib/repl.js:reclusiveCatchPromise
18+
{ send: 'const a = {}; a.self = a; a.self.' },
19+
{ run: `const foo = { get name() { return Promise.reject(); } };`,
20+
send: `foo.name` },
21+
{ run: 'const baz = { get bar() { return ""; } }; const getPropText = () => Promise.reject();',
22+
send: 'baz[getPropText()].',
23+
completeError: true },
24+
{
25+
send: 'const quux = { bar: { return Promise.reject(); } }; const getPropText = () => "bar"; quux[getPropText()].',
26+
},
27+
];
28+
29+
(async function() {
30+
await runReplCompleteTests(completionTests);
31+
})().then(common.mustCall());
32+
33+
async function runReplCompleteTests(tests) {
34+
const input = new ArrayStream();
35+
const output = new ArrayStream();
36+
37+
const replServer = repl.start({
38+
prompt: '',
39+
input,
40+
output: output,
41+
allowBlockingCompletions: true,
42+
terminal: true
43+
});
44+
45+
for (const { send, run, completeError = false } of tests) {
46+
if (run) {
47+
await new Promise((resolve, reject) => {
48+
replServer.eval(run, replServer.context, '', (err) => {
49+
if (err) {
50+
reject(err);
51+
} else {
52+
resolve();
53+
}
54+
});
55+
});
56+
}
57+
58+
const onError = (e) => {
59+
assert.fail(`Unexpected error: ${e.message}`);
60+
};
61+
62+
let completeErrorPromise = Promise.resolve();
63+
64+
if (completeError) {
65+
completeErrorPromise = new Promise((resolve) => {
66+
const handleError = () => {
67+
replServer._completeDomain.removeListener('error', handleError);
68+
resolve();
69+
};
70+
replServer._completeDomain.on('error', handleError);
71+
});
72+
} else {
73+
replServer._completeDomain.on('error', onError);
74+
}
75+
76+
await replServer.complete(
77+
send,
78+
common.mustCall((error, data) => {
79+
assert.strictEqual(error, null);
80+
assert.strictEqual(data.length, 2);
81+
assert.strictEqual(typeof data[1], 'string');
82+
assert.ok(send.includes(data[1]));
83+
})
84+
);
85+
86+
await completeErrorPromise;
87+
88+
if (!completeError) {
89+
await new Promise((resolve) => {
90+
setImmediate(() => {
91+
replServer._completeDomain.removeListener('error', onError);
92+
resolve();
93+
});
94+
});
95+
}
96+
}
97+
}

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._domain.on('error', assert.ifError);
47+
replServer._completeDomain.on('error', assert.ifError);
4848

4949
return { replServer, input };
5050
}

0 commit comments

Comments
 (0)