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
bind
is super expensive, and using.call
is 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 theoptionals
and replacereplacementPath
by the de-sugared member expressions or call expressions.Justin’s approach @jridgewell 's approach is to reuse the logic that we choose
parentPath
to be thereplacementPath
, that said,so we can imagine that
In this case
foo
is correctly propagated to be thethis
value offoo.bar()
member expression. However the semantic of(foo?.bar)()
is changed: iffoo
orfoo?.bar
is 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 returnsundefined
whenfoo
orfoo?.bar
is nullish.My approach Here I propose we use
.bind
to 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?.bar
2) pass this valuefoo
to the replacement nodefoo.bar
viafunction#bind
.As I said before the plugin can be divided into two parts. In this approach we only need focus on how
replacement
is generated.https://github.com/babel/babel/blob/3960f4de647e3c81b3c0d3020de8ef156c188944/packages/babel-plugin-proposal-optional-chaining/src/index.js#L121
we pass
replacement.node
(which isfoo.bar
in the above example) to be the non-nullish conditional alternate. We can check if thereplacementPath
is parenthesized and its parent path is a call expressionAnd it is detected we can append
.bind(foo)
tofoo.bar
:where
.get("object")
extractsfoo
fromfoo.bar
andfoo.bar.bind
is constructed fromt.memberExpression
.@jridgewell Please let me know if I miss some semantic differences between
function#bind
andfunction#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.