Skip to content

Commit 3a3eb68

Browse files
dario-piotrowicztargos
authored andcommitted
repl: improve REPL disabling completion on proxies and getters
#57909 introduced the disabling of REPL tab completion on object containing proxies and getters (since such completion triggers code evaluation which can be unexpected/disruptive for the user) the solution in 57909 did not address all possible such cases, the changes here improve on such solution by using acorn and AST analysis to cover most if not all possible cases PR-URL: #58891 Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 515b581 commit 3a3eb68

File tree

2 files changed

+223
-28
lines changed

2 files changed

+223
-28
lines changed

lib/repl.js

Lines changed: 159 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1523,8 +1523,24 @@ function complete(line, callback) {
15231523
return;
15241524
}
15251525

1526+
// If the target ends with a dot (e.g. `obj.foo.`) such code won't be valid for AST parsing
1527+
// so in order to make it correct we add an identifier to its end (e.g. `obj.foo.x`)
1528+
const parsableCompleteTarget = completeTarget.endsWith('.') ? `${completeTarget}x` : completeTarget;
1529+
1530+
let completeTargetAst;
1531+
try {
1532+
completeTargetAst = acornParse(
1533+
parsableCompleteTarget, { __proto__: null, sourceType: 'module', ecmaVersion: 'latest' },
1534+
);
1535+
} catch { /* No need to specifically handle parse errors */ }
1536+
1537+
if (!completeTargetAst) {
1538+
return completionGroupsLoaded();
1539+
}
1540+
15261541
return includesProxiesOrGetters(
1527-
StringPrototypeSplit(completeTarget, '.'),
1542+
completeTargetAst.body[0].expression,
1543+
parsableCompleteTarget,
15281544
this.eval,
15291545
this.context,
15301546
(includes) => {
@@ -1729,32 +1745,156 @@ function findExpressionCompleteTarget(code) {
17291745
return code;
17301746
}
17311747

1732-
function includesProxiesOrGetters(exprSegments, evalFn, context, callback, currentExpr = '', idx = 0) {
1733-
const currentSegment = exprSegments[idx];
1734-
currentExpr += `${currentExpr.length === 0 ? '' : '.'}${currentSegment}`;
1735-
evalFn(`try { ${currentExpr} } catch { }`, context, getREPLResourceName(), (_, currentObj) => {
1736-
if (typeof currentObj !== 'object' || currentObj === null) {
1737-
return callback(false);
1738-
}
1748+
/**
1749+
* Utility used to determine if an expression includes object getters or proxies.
1750+
*
1751+
* Example: given `obj.foo`, the function lets you know if `foo` has a getter function
1752+
* associated to it, or if `obj` is a proxy
1753+
* @param {any} expr The expression, in AST format to analyze
1754+
* @param {string} exprStr The string representation of the expression
1755+
* @param {(str: string, ctx: any, resourceName: string, cb: (error, evaled) => void) => void} evalFn
1756+
* Eval function to use
1757+
* @param {any} ctx The context to use for any code evaluation
1758+
* @param {(includes: boolean) => void} callback Callback that will be called with the result of the operation
1759+
* @returns {void}
1760+
*/
1761+
function includesProxiesOrGetters(expr, exprStr, evalFn, ctx, callback) {
1762+
if (expr?.type !== 'MemberExpression') {
1763+
// If the expression is not a member one for obvious reasons no getters are involved
1764+
return callback(false);
1765+
}
17391766

1740-
if (isProxy(currentObj)) {
1741-
return callback(true);
1767+
if (expr.object.type === 'MemberExpression') {
1768+
// The object itself is a member expression, so we need to recurse (e.g. the expression is `obj.foo.bar`)
1769+
return includesProxiesOrGetters(
1770+
expr.object,
1771+
exprStr.slice(0, expr.object.end),
1772+
evalFn,
1773+
ctx,
1774+
(includes, lastEvaledObj) => {
1775+
if (includes) {
1776+
// If the recurred call found a getter we can also terminate
1777+
return callback(includes);
1778+
}
1779+
1780+
if (isProxy(lastEvaledObj)) {
1781+
return callback(true);
1782+
}
1783+
1784+
// If a getter/proxy hasn't been found by the recursion call we need to check if maybe a getter/proxy
1785+
// is present here (e.g. in `obj.foo.bar` we found that `obj.foo` doesn't involve any getters so we now
1786+
// need to check if `bar` on `obj.foo` (i.e. `lastEvaledObj`) has a getter or if `obj.foo.bar` is a proxy)
1787+
return hasGetterOrIsProxy(lastEvaledObj, expr.property, (doesHaveGetterOrIsProxy) => {
1788+
return callback(doesHaveGetterOrIsProxy);
1789+
});
1790+
},
1791+
);
1792+
}
1793+
1794+
// This is the base of the recursion we have an identifier for the object and an identifier or literal
1795+
// for the property (e.g. we have `obj.foo` or `obj['foo']`, `obj` is the object identifier and `foo`
1796+
// is the property identifier/literal)
1797+
if (expr.object.type === 'Identifier') {
1798+
return evalFn(`try { ${expr.object.name} } catch {}`, ctx, getREPLResourceName(), (err, obj) => {
1799+
if (err) {
1800+
return callback(false);
1801+
}
1802+
1803+
if (isProxy(obj)) {
1804+
return callback(true);
1805+
}
1806+
1807+
return hasGetterOrIsProxy(obj, expr.property, (doesHaveGetterOrIsProxy) => {
1808+
if (doesHaveGetterOrIsProxy) {
1809+
return callback(true);
1810+
}
1811+
1812+
return evalFn(
1813+
`try { ${exprStr} } catch {} `, ctx, getREPLResourceName(), (err, obj) => {
1814+
if (err) {
1815+
return callback(false);
1816+
}
1817+
return callback(false, obj);
1818+
});
1819+
});
1820+
});
1821+
}
1822+
1823+
/**
1824+
* Utility to see if a property has a getter associated to it or if
1825+
* the property itself is a proxy object.
1826+
*/
1827+
function hasGetterOrIsProxy(obj, astProp, cb) {
1828+
if (!obj || !astProp) {
1829+
return cb(false);
17421830
}
17431831

1744-
const nextIdx = idx + 1;
1832+
if (astProp.type === 'Literal') {
1833+
// We have something like `obj['foo'].x` where `x` is the literal
17451834

1746-
if (nextIdx >= exprSegments.length) {
1747-
return callback(false);
1835+
if (isProxy(obj[astProp.value])) {
1836+
return cb(true);
1837+
}
1838+
1839+
const propDescriptor = ObjectGetOwnPropertyDescriptor(
1840+
obj,
1841+
`${astProp.value}`,
1842+
);
1843+
const propHasGetter = typeof propDescriptor?.get === 'function';
1844+
return cb(propHasGetter);
17481845
}
17491846

1750-
const nextSegmentProp = ObjectGetOwnPropertyDescriptor(currentObj, exprSegments[nextIdx]);
1751-
const nextSegmentPropHasGetter = typeof nextSegmentProp?.get === 'function';
1752-
if (nextSegmentPropHasGetter) {
1753-
return callback(true);
1847+
if (
1848+
astProp.type === 'Identifier' &&
1849+
exprStr.at(astProp.start - 1) === '.'
1850+
) {
1851+
// We have something like `obj.foo.x` where `foo` is the identifier
1852+
1853+
if (isProxy(obj[astProp.name])) {
1854+
return cb(true);
1855+
}
1856+
1857+
const propDescriptor = ObjectGetOwnPropertyDescriptor(
1858+
obj,
1859+
`${astProp.name}`,
1860+
);
1861+
const propHasGetter = typeof propDescriptor?.get === 'function';
1862+
return cb(propHasGetter);
17541863
}
17551864

1756-
return includesProxiesOrGetters(exprSegments, evalFn, context, callback, currentExpr, nextIdx);
1757-
});
1865+
return evalFn(
1866+
// Note: this eval runs the property expression, which might be side-effectful, for example
1867+
// the user could be running `obj[getKey()].` where `getKey()` has some side effects.
1868+
// Arguably this behavior should not be too surprising, but if it turns out that it is,
1869+
// then we can revisit this behavior and add logic to analyze the property expression
1870+
// and eval it only if we can confidently say that it can't have any side effects
1871+
`try { ${exprStr.slice(astProp.start, astProp.end)} } catch {} `,
1872+
ctx,
1873+
getREPLResourceName(),
1874+
(err, evaledProp) => {
1875+
if (err) {
1876+
return callback(false);
1877+
}
1878+
1879+
if (typeof evaledProp === 'string') {
1880+
if (isProxy(obj[evaledProp])) {
1881+
return cb(true);
1882+
}
1883+
1884+
const propDescriptor = ObjectGetOwnPropertyDescriptor(
1885+
obj,
1886+
evaledProp,
1887+
);
1888+
const propHasGetter = typeof propDescriptor?.get === 'function';
1889+
return cb(propHasGetter);
1890+
}
1891+
1892+
return callback(false);
1893+
},
1894+
);
1895+
}
1896+
1897+
return callback(false);
17581898
}
17591899

17601900
REPLServer.prototype.completeOnEditorMode = (callback) => (err, results) => {

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

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,48 +61,102 @@ describe('REPL completion in relation of getters', () => {
6161
test(`completions are generated for properties that don't trigger getters`, () => {
6262
runCompletionTests(
6363
`
64+
function getFooKey() {
65+
return "foo";
66+
}
67+
68+
const fooKey = "foo";
69+
70+
const keys = {
71+
"foo key": "foo",
72+
};
73+
6474
const objWithGetters = {
65-
foo: { bar: { baz: {} }, get gBar() { return { baz: {} } } },
75+
foo: { bar: { baz: { buz: {} } }, get gBar() { return { baz: {} } } },
6676
get gFoo() { return { bar: { baz: {} } }; }
6777
};
6878
`, [
6979
['objWithGetters.', ['objWithGetters.foo']],
7080
['objWithGetters.f', ['objWithGetters.foo']],
7181
['objWithGetters.foo', ['objWithGetters.foo']],
82+
['objWithGetters["foo"].b', ['objWithGetters["foo"].bar']],
7283
['objWithGetters.foo.', ['objWithGetters.foo.bar']],
7384
['objWithGetters.foo.bar.b', ['objWithGetters.foo.bar.baz']],
7485
['objWithGetters.gFo', ['objWithGetters.gFoo']],
7586
['objWithGetters.foo.gB', ['objWithGetters.foo.gBar']],
87+
["objWithGetters.foo['bar'].b", ["objWithGetters.foo['bar'].baz"]],
88+
["objWithGetters['foo']['bar'].b", ["objWithGetters['foo']['bar'].baz"]],
89+
["objWithGetters['foo']['bar']['baz'].b", ["objWithGetters['foo']['bar']['baz'].buz"]],
90+
["objWithGetters[keys['foo key']].b", ["objWithGetters[keys['foo key']].bar"]],
91+
['objWithGetters[fooKey].b', ['objWithGetters[fooKey].bar']],
92+
["objWithGetters['f' + 'oo'].b", ["objWithGetters['f' + 'oo'].bar"]],
93+
['objWithGetters[getFooKey()].b', ['objWithGetters[getFooKey()].bar']],
7694
]);
7795
});
7896

7997
test('no completions are generated for properties that trigger getters', () => {
8098
runCompletionTests(
8199
`
100+
function getGFooKey() {
101+
return "g" + "Foo";
102+
}
103+
104+
const gFooKey = "gFoo";
105+
106+
const keys = {
107+
"g-foo key": "gFoo",
108+
};
109+
82110
const objWithGetters = {
83-
foo: { bar: { baz: {} }, get gBar() { return { baz: {} } } },
111+
foo: { bar: { baz: {} }, get gBar() { return { baz: {}, get gBuz() { return 5; } } } },
84112
get gFoo() { return { bar: { baz: {} } }; }
85113
};
86114
`,
87115
[
88116
['objWithGetters.gFoo.', []],
89117
['objWithGetters.gFoo.b', []],
118+
['objWithGetters["gFoo"].b', []],
90119
['objWithGetters.gFoo.bar.b', []],
91120
['objWithGetters.foo.gBar.', []],
92121
['objWithGetters.foo.gBar.b', []],
122+
["objWithGetters.foo['gBar'].b", []],
123+
["objWithGetters['foo']['gBar'].b", []],
124+
["objWithGetters['foo']['gBar']['gBuz'].", []],
125+
["objWithGetters[keys['g-foo key']].b", []],
126+
['objWithGetters[gFooKey].b', []],
127+
["objWithGetters['g' + 'Foo'].b", []],
128+
['objWithGetters[getGFooKey()].b', []],
93129
]);
94130
});
95131
});
96132

97133
describe('completions on proxies', () => {
98134
test('no completions are generated for a proxy object', () => {
99-
runCompletionTests('const proxyObj = new Proxy({ foo: { bar: { baz: {} } } }, {});', [
100-
['proxyObj.', []],
101-
['proxyObj.f', []],
102-
['proxyObj.foo', []],
103-
['proxyObj.foo.', []],
104-
['proxyObj.foo.bar.b', []],
105-
]);
135+
runCompletionTests(
136+
`
137+
function getFooKey() {
138+
return "foo";
139+
}
140+
141+
const fooKey = "foo";
142+
143+
const keys = {
144+
"foo key": "foo",
145+
};
146+
147+
const proxyObj = new Proxy({ foo: { bar: { baz: {} } } }, {});
148+
`, [
149+
['proxyObj.', []],
150+
['proxyObj.f', []],
151+
['proxyObj.foo', []],
152+
['proxyObj.foo.', []],
153+
['proxyObj.["foo"].', []],
154+
['proxyObj.["f" + "oo"].', []],
155+
['proxyObj.[fooKey].', []],
156+
['proxyObj.[getFooKey()].', []],
157+
['proxyObj.[keys["foo key"]].', []],
158+
['proxyObj.foo.bar.b', []],
159+
]);
106160
});
107161

108162
test('no completions are generated for a proxy present in a standard object', () => {
@@ -113,6 +167,7 @@ describe('REPL completion in relation of getters', () => {
113167
['objWithProxy.foo.', ['objWithProxy.foo.bar']],
114168
['objWithProxy.foo.b', ['objWithProxy.foo.bar']],
115169
['objWithProxy.foo.bar.', []],
170+
['objWithProxy.foo["b" + "ar"].', []],
116171
]);
117172
});
118173
});

0 commit comments

Comments
 (0)