@@ -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
17601900REPLServer . prototype . completeOnEditorMode = ( callback ) => ( err , results ) => {
0 commit comments