babel: Optional Chaining does not propagate receiver when parenthesized
Bug Report
Current Behavior
Optional Calls allows you to parenthesize the expression to break the optional-ness of the chain. For instance, (foo?.bar).baz requires that an object be returned to access .baz. This also applies to call expressions: (foo?.bar)().
The problem is, that (foo?.bar)() must propagate foo as the this receiver to the call expression. We currently do not do this.
Input Code
const foo = { bar() { return this } };
console.assert(foo === (foo?.bar)());
Expected behavior/code
The foo object should be used as the receiver during the foo.bar method call.
Babel Configuration (babel.config.js, .babelrc, package.json#babel, cli command, .eslintrc)
- Filename:
babel.config.js
{
"plugins": [ "@babel/plugin-proposal-optional-chaining" ]
}
Possible Solution
We need to detect parenthesized call expressions, and use .call to properly propagate the receiver.
Additional context/Screenshots
This is likely more difficult good-first-issue. I’m still prototyping a fix to see whether we should recommend it to a first time contributor.
About this issue
- Original URL
- State: closed
- Created 5 years ago
- Comments: 19 (9 by maintainers)
I think it’s easiest if we transform to:
Doing a
bindis super expensive, and using.callis moderately expensive.@iansu
is equivalent to
if you apply the call expression on the conditional branches.
You can construct
(void 0)()fromvoid 0(scope.buildUndefinedNode()) viat.callExpression.@iansu I come up with a possible solution inspired by @jridgewell 's tips. Let’s see if we can sort it out.
Brief summary of optional-chaining plugin The optional-chaining transform plugin can be roughly divided into two parts: i) The first part is to collect an array of to-be-processed AST nodes (
optionals) and determine the AST node that we want to transform (replacementPath). ii) The second part is to apply transforms to theoptionalsand replacereplacementPathby the de-sugared member expressions or call expressions.Justin’s approach @jridgewell 's approach is to reuse the logic that we choose
parentPathto be thereplacementPath, that said,so we can imagine that
In this case
foois correctly propagated to be thethisvalue offoo.bar()member expression. However the semantic of(foo?.bar)()is changed: iffooorfoo?.baris nullish,(foo?.bar)()must throwundefined is not a function. However here we have replaced the un-optional call expression(foo?.bar)()by optional chains which returnsundefinedwhenfooorfoo?.baris nullish.My approach Here I propose we use
.bindto pass the this value and preserve the non-optional call expression. In the example aboveSo we still 1) preserve the call expression out of the optional member
foo?.bar2) pass this valuefooto the replacement nodefoo.barviafunction#bind.As I said before the plugin can be divided into two parts. In this approach we only need focus on how
replacementis generated.https://github.com/babel/babel/blob/3960f4de647e3c81b3c0d3020de8ef156c188944/packages/babel-plugin-proposal-optional-chaining/src/index.js#L121
we pass
replacement.node(which isfoo.barin the above example) to be the non-nullish conditional alternate. We can check if thereplacementPathis parenthesized and its parent path is a call expressionAnd it is detected we can append
.bind(foo)tofoo.bar:where
.get("object")extractsfoofromfoo.barandfoo.bar.bindis constructed fromt.memberExpression.@jridgewell Please let me know if I miss some semantic differences between
function#bindandfunction#call, thank you!@sidntrivedi012 I am still working on this. I’ve written some failing tests to cover this case. I’m just working on actually fixing it. I don’t have a ton of experience working with Babel’s AST so it’s taking me a little longer than I hoped but I am still trying to work through it.
@jakewilson I am still working on this. I just haven’t had much time over the holidays. I’ll pick it up again this week.
Hi. I will try to get a PR up for this today. Apologies for the delays.
I’m interested in working on this issue if no one else is.