Skip to content

Commit 949f08a

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

File tree

5 files changed

+141
-6
lines changed

5 files changed

+141
-6
lines changed

lib/repl.js

Lines changed: 41 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,9 @@ 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);
15631569
let p;
15641570
if ((typeof obj === 'object' && obj !== null) ||
15651571
typeof obj === 'function') {
@@ -1800,6 +1806,7 @@ function includesProxiesOrGetters(expr, exprStr, evalFn, ctx, callback) {
18001806
// is the property identifier/literal)
18011807
if (expr.object.type === 'Identifier') {
18021808
return evalFn(`try { ${expr.object.name} } catch {}`, ctx, getREPLResourceName(), (err, obj) => {
1809+
reclusiveCatchPromise(obj);
18031810
if (err) {
18041811
return callback(false);
18051812
}
@@ -1815,6 +1822,7 @@ function includesProxiesOrGetters(expr, exprStr, evalFn, ctx, callback) {
18151822

18161823
return evalFn(
18171824
`try { ${exprStr} } catch {} `, ctx, getREPLResourceName(), (err, obj) => {
1825+
reclusiveCatchPromise(obj);
18181826
if (err) {
18191827
return callback(false);
18201828
}
@@ -1877,6 +1885,7 @@ function includesProxiesOrGetters(expr, exprStr, evalFn, ctx, callback) {
18771885
ctx,
18781886
getREPLResourceName(),
18791887
(err, evaledProp) => {
1888+
reclusiveCatchPromise(evaledProp);
18801889
if (err) {
18811890
return callback(false);
18821891
}
@@ -1902,7 +1911,9 @@ function includesProxiesOrGetters(expr, exprStr, evalFn, ctx, callback) {
19021911
function safeIsProxyAccess(obj, prop) {
19031912
// Accessing `prop` may trigger a getter that throws, so we use try-catch to guard against it
19041913
try {
1905-
return isProxy(obj[prop]);
1914+
const value = obj[prop];
1915+
reclusiveCatchPromise(value);
1916+
return isProxy(value);
19061917
} catch {
19071918
return false;
19081919
}
@@ -1911,6 +1922,33 @@ function includesProxiesOrGetters(expr, exprStr, evalFn, ctx, callback) {
19111922
return callback(false);
19121923
}
19131924

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

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

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

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)