New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: Duplicate function call in variable destructuring #13711
fix: Duplicate function call in variable destructuring #13711
Conversation
Build successful! You can test your changes in the REPL here: https://babeljs.io/repl/build/48579/ |
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. Latest deployment of this branch, based on commit 4be2624:
|
let { x6: { w6: { a6, ...y6 } } } = z(); | ||
let { x7: { e7, r7 }, q7: { w7: { a7, ...y7 } } } = z(); | ||
let { x8, ...y8 } = z(); | ||
let { x9: { w9: { a9, ...y9 } }, x10: { a10, ...y10 }, } = z(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
Can you add a test case with mixed arrays?
let { x9: { w9: { a9, ...y9 } }, x10: { a10, ...y10 }, } = z(); | |
let { x9: { w9: { a9, ...y9 } }, x10: { a10, ...y10 }, } = z(); | |
let { x10: [{ w10, ...z10 }] } = z(); | |
let { x11: [{ a11, b11 }, { c11, ...d11 }] } = z(); | |
let { x12: [, { c12, ...d12 }] } = z(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just pushed up a commit. Thanks for the recommendation, it caught an edge case with the implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a new test case?
const { x: [...{ x, ...y }] } = z();
A RestElement's argument can be a pattern so we have to recurse into it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great point! I added the test case and confirmed the output looks as I'd expect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can not return early when properties
is falsy, instead we should invoke the recursive function on .argument
. For example,
const { x: [...{ ...y }] } = z();
has only one binding identifier but doesAnyChildNodeHaveMoreThanOneProperty
returns true
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @JLHwung - thanks for the the detailed case. I looked through https://astexplorer.net/ and believe I was able to diagnose the issue you called out with more robust test cases for array destructuring.
Happy to make any other adjustments. Thank you for the thorough review and prompt responses!
9a62ee3
to
4336cdb
Compare
@@ -335,7 +368,7 @@ export default declare((api, opts) => { | |||
// skip single-property case, e.g. | |||
// const { ...x } = foo(); | |||
// since the RHS will not be duplicated | |||
originalPath.node.id.properties.length > 1 && | |||
doesAnyChildNodeHaveMoreThanOneProperty(originalPath.node.id) && | |||
!t.isIdentifier(originalPath.node.init) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unrelated to this PR: I think we should use originalPath.scope.isStatic(originalPath.node.init)
here, because
- A
ThisExpression
is safe to reuse, e.g.
const { ...x } = this;
- Even if
node.init
is an identifier, it can not be reused if not a constant binding. e.g.
let z = { v: 1 };
function sideEffect() {
z = { v: 2 };
}
const { x = sideEffect(), ...y } = z;
y // { v: 1 }
y
becomes { v: 2 }
after transformed because z
can be modified in sideEffect()
.
These two issues will be fixed by scope.isStatic
, they can be addressed in a subsequent PR.
// If this is an AssignmentPattern, always return true else the RHS will be duplicated | ||
if (innerNode.type === "AssignmentPattern" && innerNode.left) { | ||
return true; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to RestElement
, we need to recurse into node.left
.
innerNode ??= node.left
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I originally added that but the output did not end up as I expected. Specifically the RHS was duplicated. I believe the way that we handle assignment patterns also duplicates the RHS.
See this diff that I branched off this branch for: https://github.com/dan-kez/babel/pull/1/files
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I was meant to replace the whole "if" branch with innerNode ??= node.left
, which is similar to how we handle the array / object patterns (note that in your posted branch it was changed to innerNode = innerNode.left
). The innerNode
is essentially the arguments of next recursive call, so the algorithm can be summarized as
function hasMoreThanOneBinding(node) {
if (node is ArrayPattern ) {
return true if node has more than one elements
else return hasMoreThanOneBinding(node's first element)
} else if (node is ObjectPattern) {
return true if node has more than one properties
else return hasMoreThanOneBinding(node's first property value)
} else if (node is AssignmentPattern) {
return hasMoreThanOneBinding(node's left)
} else if (node is RestElement) {
return hasMoreThanOneBinding(node's argument)
} else { // node is Identifier or MemberExpression
return true
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I understand now. Apologies for all the back and forth here - I really appreciate you taking the time on this PR!
I've refactored the function to be more in line with you example. I did add one additional case ObjectProperty
. This was because an ObjectPattern could have the first element a RestElement (for example const { x15: { ...y15 } } = z();
). In this case node.properties[0].value
would be undefined which would fall through the final else clause.
To be more specific I added an ObjectProperty
case where I test on the presence of node?.value
and then recurse on the given value.
If this is wrong please let me know - I'm happy to change it.
I also used optional chaining just in case I missed a case were node could be falsey (this was how I found the above issue when stepping through the test case). I am also happy to add an early return if node is undefined to return true or other cases that could be present in this LHS assignment.
// ObjectPattern | ||
} else if (node.properties) { | ||
if (node.properties.length > 1) return true; | ||
// Case of `const {} = {}`. This shouldn't be hit as |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This branch is reachable because visitRestElements
applies to rest elements in array patterns, too. E.g.
const { x: [ ...{} ] } = { x: [] }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work!
Thanks for the review @JLHwung !! I was following along with the CI steps and noticed [CI / E2E (create-react-app) (pull_request) was failing]https://github.com/babel/babel/runs/3495970358?check_suite_focus=true It looks like there is in issue in how we're invoking the build command incorrectly
I'm not sure if this error is blocking to have this fixed merged in or how I can help fixing this error. |
The react e2e CI error has been fixed in #13724, you can rebase this PR on latest |
700c692
to
ab3b603
Compare
Perfect - thank you! Just rebased |
return hasMoreThanOneBinding(node.argument); | ||
} else { | ||
// node is Identifier or MemberExpression | ||
return true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Q: Since this is a single identifier, shouldn't it return false? (it's one binding)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like this test case fails if I make this false
input:
const { x15: [...{ ...y15 }] } = z();
output:
const {} = z(),
y15 = babelHelpers.extends({}, z().x15);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I believe I have a fix. In the event node.argument
is an Identifier then we can early return true and this these test cases work out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I think we don't need to return true
for rest but the fix needs to live somewhere else.
In the const { x15: [...{ ...y15 }] } = z();
case, we don't need to cache z()
: we can just skip the {} = z()
one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be ok to consider making this change in a separate PR as an optimization? My main goal for this is to mitigate duplicating the right hand assignment. I'd prefer avoiding doing a larger refactor of the greater plugin if possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's ok!
describe("hasMoreThanOneBinding", function () { | ||
it.each([ | ||
["const { x: { ...y } } = z();", true], | ||
["let { x4: { ...y4 } } = z();", true], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should return false
, the source contains only one binding y4
. Also we don't have to add number suffix since every snippet is parsed independently.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I think hasMoreThanOneBinding
is an invalid name for the function. The end goal of this is to strictly stop the right hand side being duplicated
I am happy to rename this function or do something else but in the end the main goal is to mitigate the active bug.
["const [...[ ...y17 ]] = z();", true], | ||
["const [...{ ...y18 }] = z();", true], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These two cases are almost identical, we can remove one of them.
} | ||
describe("hasMoreThanOneBinding", function () { | ||
it.each([ | ||
["const { x: { ...y } } = z();", true], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should return false
.
["let { x12: [{ a12, b12 }, { c12, ...d12 }] } = z();", true], | ||
["let { x13: [, { c13, ...d13 }] } = z();", true], | ||
["const { x14: [...{ q14, ...y14 }] } = z();", true], | ||
["const { x15: [...{ ...y16 }] } = z();", true], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should return false
.
["const [...[ ...y17 ]] = z();", true], | ||
["const [...{ ...y18 }] = z();", true], | ||
["const [...{ a19, ...y19 }] = z();", true], | ||
["const { x20: { ...y20 } = { } } = z();", true], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should return false
.
["const [[ ...y23 ] = []] = z();", true], | ||
["const [{ ...y24 } = []] = z();", true], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should return false
and almost identical examples.
["const { x22: { q22, ...y22 } = {} } = z();", true], | ||
["const [[ ...y23 ] = []] = z();", true], | ||
["const [{ ...y24 } = []] = z();", true], | ||
["const { x25: [ ...y25 ] = []} = z();", true], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should return false
.
["const [{ ...y24 } = []] = z();", true], | ||
["const { x25: [ ...y25 ] = []} = z();", true], | ||
["const { x26: [ q26, ...y26 ] = []} = z();", true], | ||
["const {x28: [,,{...y28}]} = z();", true], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should return false
["const [,,{...x31}] = z();", true], | ||
["const { x32: { }, w32: { ...y32 } } = z();", true], | ||
["const [,,{}, {...q32}] = z();", true], | ||
["const { ...y33 } = z();", true], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These four examples also contains only one binding, and thus should return false
.
["const {x29: [,,{q29, ...y29}]} = z();", true], | ||
["const [,,{y30, ...x30}] = z();", true], | ||
["const [,,{...x31}] = z();", true], | ||
["const { x32: { }, w32: { ...y32 } } = z();", true], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a very interesting case. It shows that we can't simply return true
if node.properties > 1
, since the property value may be an empty object pattern, or nested pattern.
We may need a new algorithm similar to
function getBindingIdentifiers( |
Essentially the hasMoreThanOneBinding
should be equivalent to
Object.keys(getBindingIdentifiers(node)).length > 1
Hey @JLHwung & @nicolo-ribaudo I'd love to align on the implementation that you both would like to see to have this bug fixed. Right now I would say the acceptance criteria should likely be the following:
To accomplish the above I want to do the following
Would making both those changes be sufficient to see this PR merged? |
Since this PR fixes the bug in the generated code, I'm ok with just renaming |
Could you rebase and run |
Thanks for the prompt response @nicolo-ribaudo ! I updated the function name, added a comment, and also ran yarn dedupe. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's also wait for @JLHwung's opinion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR already fixed several cases. We can iterate later on further issues.
Excellent! Thank you both!I took a look at CI and I'm not sure if there is something a maintainer / member needs to do to enable the tests to continue. |
@dan-kez All good! Merged. |
Awesome! Thank you @JLHwung - looking forward to incorporating this at work once a new version is cut. Really appreciate the thorough review from both of you! |
I am new to babel's code base but I believe I have a fix here for an issue where the RHS of an assignment will be called duplicate times.
I've added a few test cases to validate against this potential issue and have confirmed the output looks as I would expect.
If there are any suggestions please let me know! I'm happy to make updates.
Thanks for reviewing!
Short summary
This PR adds a simple recursive check so that any multi property case with a spread operator on assignment will not duplicate the RHS of the assignment.
Prior to this PR there was a guard for this case but it only looked one level deep. Example cases of this are shown in #13710 (comment)