fix: Duplicate function call in variable destructuring (#13711)

* Add test case demonstrating invalid behavior

* Add conditional check for child properties so RHS is not duplicated

* Add recursive check to find any nested non single-property case

* Add case for array destructuring

* Add test case for a nested rest element

* More safely recurse through array destructuring

* Traverse assignment and nested rest operations

* Refactor to be more clear which case statement we are executing against

* Update missed snapshots

* Update packages/babel-plugin-proposal-object-rest-spread/src/index.js

Co-authored-by: Huáng Jùnliàng <jlhwung@gmail.com>

* Filter null array elements, add early return, remove optional chaining

* Use stronger type assertions

* Update fall through to be false; add early return to RestElement case

* Move hasMoreThanOneBinding to its own file with distinct tests

* Rename testing helper to more appropriately match business logic

* Yarn Dedup & rename hasMoreThanOneBinding to shouldStoreRHSInTemporaryVariable

Co-authored-by: Huáng Jùnliàng <jlhwung@gmail.com>
This commit is contained in:
Daniel Kezerashvili 2021-09-07 17:27:52 -04:00 committed by GitHub
parent d87a3d9e13
commit 40e43f5a14
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 295 additions and 64 deletions

View File

@ -28,7 +28,8 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "workspace:*", "@babel/core": "workspace:*",
"@babel/helper-plugin-test-runner": "workspace:*" "@babel/helper-plugin-test-runner": "workspace:*",
"@babel/parser": "workspace:*"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"

View File

@ -4,6 +4,7 @@ import { types as t } from "@babel/core";
import { convertFunctionParams } from "@babel/plugin-transform-parameters"; import { convertFunctionParams } from "@babel/plugin-transform-parameters";
import { isRequired } from "@babel/helper-compilation-targets"; import { isRequired } from "@babel/helper-compilation-targets";
import compatData from "@babel/compat-data/corejs2-built-ins"; import compatData from "@babel/compat-data/corejs2-built-ins";
import shouldStoreRHSInTemporaryVariable from "./shouldStoreRHSInTemporaryVariable";
// TODO: Remove in Babel 8 // TODO: Remove in Babel 8
// @babel/types <=7.3.3 counts FOO as referenced in var { x: FOO }. // @babel/types <=7.3.3 counts FOO as referenced in var { x: FOO }.
@ -335,7 +336,7 @@ export default declare((api, opts) => {
// skip single-property case, e.g. // skip single-property case, e.g.
// const { ...x } = foo(); // const { ...x } = foo();
// since the RHS will not be duplicated // since the RHS will not be duplicated
originalPath.node.id.properties.length > 1 && shouldStoreRHSInTemporaryVariable(originalPath.node.id) &&
!t.isIdentifier(originalPath.node.init) !t.isIdentifier(originalPath.node.init)
) { ) {
// const { a, ...b } = foo(); // const { a, ...b } = foo();

View File

@ -0,0 +1,30 @@
import { types as t } from "@babel/core";
/**
* This is a helper function to determine if we should create an intermediate variable
* such that the RHS of an assignment is not duplicated.
*
* See https://github.com/babel/babel/pull/13711#issuecomment-914388382 for discussion
* on further optimizations.
*/
export default function shouldStoreRHSInTemporaryVariable(node) {
if (t.isArrayPattern(node)) {
const nonNullElements = node.elements.filter(element => element !== null);
if (nonNullElements.length > 1) return true;
else return shouldStoreRHSInTemporaryVariable(nonNullElements[0]);
} else if (t.isObjectPattern(node)) {
if (node.properties.length > 1) return true;
else if (node.properties.length === 0) return false;
else return shouldStoreRHSInTemporaryVariable(node.properties[0]);
} else if (t.isObjectProperty(node)) {
return shouldStoreRHSInTemporaryVariable(node.value);
} else if (t.isAssignmentPattern(node)) {
return shouldStoreRHSInTemporaryVariable(node.left);
} else if (t.isRestElement(node)) {
if (t.isIdentifier(node.argument)) return true;
return shouldStoreRHSInTemporaryVariable(node.argument);
} else {
// node is Identifier or MemberExpression
return false;
}
}

View File

@ -1,9 +1,10 @@
var _ref2; var _ref3;
const { const {
[_ref => { [_ref => {
let rest = babelHelpers.extends({}, _ref); let rest = babelHelpers.extends({}, _ref);
let b = babelHelpers.extends({}, {}); let _ref2 = {},
b = babelHelpers.extends({}, _ref2);
}]: a, }]: a,
[(_ref2 = {}, ({} = _ref2), d = babelHelpers.extends({}, _ref2), _ref2)]: c [(_ref3 = {}, ({} = _ref3), d = babelHelpers.extends({}, _ref3), _ref3)]: c
} = {}; } = {};

View File

@ -1,9 +1,10 @@
var _ref2; var _ref3;
const { const {
a = _ref => { a = _ref => {
let rest = babelHelpers.extends({}, _ref); let rest = babelHelpers.extends({}, _ref);
let b = babelHelpers.extends({}, {}); let _ref2 = {},
b = babelHelpers.extends({}, _ref2);
}, },
c = (_ref2 = {}, ({} = _ref2), d = babelHelpers.extends({}, _ref2), _ref2) c = (_ref3 = {}, ({} = _ref3), d = babelHelpers.extends({}, _ref3), _ref3)
} = {}; } = {};

View File

@ -2,23 +2,19 @@ var key, x, y, z; // impure
key = 1; key = 1;
var _key = key++, var _ = {
{
[_key]: {
y
}
} = {
1: { 1: {
a: 1, a: 1,
y: 1 y: 1
} }
}, },
x = babelHelpers.objectWithoutProperties({ _key = key++,
1: { {
a: 1, [_key]: {
y: 1 y
} }
}[_key], ["y"]); } = _,
x = babelHelpers.objectWithoutProperties(_[_key], ["y"]);
expect(x).toEqual({ expect(x).toEqual({
a: 1 a: 1

View File

@ -1,14 +1,4 @@
var z = {}; const { x15: [...{ ...y15 }] } = z();
var { ...x } = z;
var { ...a } = { a: 1 };
var { ...x } = a.b;
var { ...x } = a();
var {x1, ...y1} = z;
x1++;
var { [a]: b, ...c } = z;
var {x1, ...y1} = z;
let {x2, y2, ...z2} = z;
const {w3, x3, y3, ...z4} = z;
let { let {
x: { a: xa, [d]: f, ...asdf }, x: { a: xa, [d]: f, ...asdf },
@ -16,4 +6,34 @@ let {
...g ...g
} = complex; } = complex;
let { x4: { ...y4 } } = z; let { x4: { ...y4 } } = z();
let { x5: { w5, ...y5 } } = z();
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();
let { x11: [{ w11, ...z11 }] } = z();
let { x12: [{ a12, b12 }, { c12, ...d12 }] } = z();
let { x13: [, { c13, ...d13 }] } = z();
const { x14: [...{ q14, ...y14 }] } = z();
const { x15: [...{ ...y16 }] } = z();
const { x16: [] } = z();
const [...[ ...y17 ]] = z();
const [...{ ...y18 }] = z();
const [...{ a19, ...y19 }] = z();
const { x20: { ...y20 } = { } } = z();
const { x22: { q22, ...y22 } = {} } = z();
const [[ ...y23 ] = []] = z();
const [{ ...y24 } = []] = z();
const { x25: [ ...y25 ] = []} = z();
const { x26: [ q26, ...y26 ] = []} = z();
const {} = {};
const [,,x27] = z();
const {x28: [,,{...y28}]} = z();
const {x29: [,,{q29, ...y29}]} = z();
const [,,{y30, ...x30}] = z();
const [,,{...x31}] = z();
const { x32: { }, w32: { ...y32 } } = z();
const [,,{}, {...q32}] = z();
const { ...y33 } = z();

View File

@ -1,34 +1,7 @@
var z = {}; const _z = z(),
var x = babelHelpers.extends({}, z); {} = _z,
var a = babelHelpers.extends({}, { y15 = babelHelpers.extends({}, _z.x15);
a: 1
});
var x = babelHelpers.extends({}, a.b);
var x = babelHelpers.extends({}, a());
var {
x1
} = z,
y1 = babelHelpers.objectWithoutProperties(z, ["x1"]);
x1++;
var {
[a]: b
} = z,
c = babelHelpers.objectWithoutProperties(z, [a].map(babelHelpers.toPropertyKey));
var {
x1
} = z,
y1 = babelHelpers.objectWithoutProperties(z, ["x1"]);
let {
x2,
y2
} = z,
z2 = babelHelpers.objectWithoutProperties(z, ["x2", "y2"]);
const {
w3,
x3,
y3
} = z,
z4 = babelHelpers.objectWithoutProperties(z, ["w3", "x3", "y3"]);
let { let {
x: { x: {
a: xa, a: xa,
@ -38,5 +11,166 @@ let {
asdf = babelHelpers.objectWithoutProperties(complex.x, ["a", d].map(babelHelpers.toPropertyKey)), asdf = babelHelpers.objectWithoutProperties(complex.x, ["a", d].map(babelHelpers.toPropertyKey)),
d = babelHelpers.extends({}, complex.y), d = babelHelpers.extends({}, complex.y),
g = babelHelpers.objectWithoutProperties(complex, ["x"]); g = babelHelpers.objectWithoutProperties(complex, ["x"]);
let {} = z,
y4 = babelHelpers.extends({}, z.x4); let _z2 = z(),
{} = _z2,
y4 = babelHelpers.extends({}, _z2.x4);
let _z3 = z(),
{
x5: {
w5
}
} = _z3,
y5 = babelHelpers.objectWithoutProperties(_z3.x5, ["w5"]);
let _z4 = z(),
{
x6: {
w6: {
a6
}
}
} = _z4,
y6 = babelHelpers.objectWithoutProperties(_z4.x6.w6, ["a6"]);
let _z5 = z(),
{
x7: {
e7,
r7
},
q7: {
w7: {
a7
}
}
} = _z5,
y7 = babelHelpers.objectWithoutProperties(_z5.q7.w7, ["a7"]);
let _z6 = z(),
{
x8
} = _z6,
y8 = babelHelpers.objectWithoutProperties(_z6, ["x8"]);
let _z7 = z(),
{
x9: {
w9: {
a9
}
},
x10: {
a10
}
} = _z7,
y9 = babelHelpers.objectWithoutProperties(_z7.x9.w9, ["a9"]),
y10 = babelHelpers.objectWithoutProperties(_z7.x10, ["a10"]);
let _z8 = z(),
{
x11: [{
w11
}]
} = _z8,
z11 = babelHelpers.objectWithoutProperties(_z8.x11, ["w11"]);
let _z9 = z(),
{
x12: [{
a12,
b12
}, {
c12
}]
} = _z9,
d12 = babelHelpers.objectWithoutProperties(_z9.x12, ["c12"]);
let _z10 = z(),
{
x13: [, {
c13
}]
} = _z10,
d13 = babelHelpers.objectWithoutProperties(_z10.x13, ["c13"]);
const _z11 = z(),
{
x14: [...{
q14
}]
} = _z11,
y14 = babelHelpers.objectWithoutProperties(_z11.x14, ["q14"]);
const _z12 = z(),
{} = _z12,
y16 = babelHelpers.extends({}, _z12.x15);
const {
x16: []
} = z();
const [...[...y17]] = z();
const [..._ref] = z();
const y18 = babelHelpers.extends({}, _ref);
const [..._ref2] = z();
const {
a19
} = _ref2,
y19 = babelHelpers.objectWithoutProperties(_ref2, ["a19"]);
const _z13 = z(),
{} = _z13,
y20 = babelHelpers.extends({}, _z13.x20);
const _z14 = z(),
{
x22: {
q22
} = {}
} = _z14,
y22 = babelHelpers.objectWithoutProperties(_z14.x22, ["q22"]);
const [[...y23] = []] = z();
const [_ref3 = []] = z();
const y24 = babelHelpers.extends({}, _ref3);
const {
x25: [...y25] = []
} = z();
const {
x26: [q26, ...y26] = []
} = z();
const {} = {};
const [,, x27] = z();
const _z15 = z(),
{} = _z15,
y28 = babelHelpers.extends({}, _z15.x28);
const _z16 = z(),
{
x29: [,, {
q29
}]
} = _z16,
y29 = babelHelpers.objectWithoutProperties(_z16.x29, ["q29"]);
const [,, _ref4] = z();
const {
y30
} = _ref4,
x30 = babelHelpers.objectWithoutProperties(_ref4, ["y30"]);
const [,, _ref5] = z();
const x31 = babelHelpers.extends({}, _ref5);
const _z17 = z(),
{
x32: {}
} = _z17,
y32 = babelHelpers.extends({}, _z17.w32);
const [,, {}, _ref6] = z();
const q32 = babelHelpers.extends({}, _ref6);
const _z18 = z(),
y33 = babelHelpers.extends({}, _z18);

View File

@ -0,0 +1,46 @@
import { parse } from "@babel/parser";
import shouldStoreRHSInTemporaryVariable from "../lib/shouldStoreRHSInTemporaryVariable";
function getFistObjectPattern(program) {
return parse(program, { sourceType: "module" }).program.body[0]
.declarations[0].id;
}
describe("shouldStoreRHSInTemporaryVariable", function () {
it.each([
["const { x: { ...y } } = z();", true],
["let { x4: { ...y4 } } = z();", true],
["let { x5: { w5, ...y5 } } = z();", true],
["let { x6: { w6: { a6, ...y6 } } } = z();", true],
["let { x7: { e7, r7 }, q7: { w7: { a7, ...y7 } } } = z();", true],
["let { x8, ...y8 } = z();", true],
["let { x9: { w9: { a9, ...y9 } }, x10: { a10, ...y10 }, } = z();", true],
["let { x11: [{ w11, ...z11 }] } = z();", true],
["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],
["const [...[ ...y17 ]] = z();", true],
["const [...{ ...y18 }] = z();", true],
["const [...{ a19, ...y19 }] = z();", true],
["const { x20: { ...y20 } = { } } = z();", true],
["const { x22: { q22, ...y22 } = {} } = z();", true],
["const [[ ...y23 ] = []] = z();", true],
["const [{ ...y24 } = []] = z();", true],
["const { x25: [ ...y25 ] = []} = z();", true],
["const { x26: [ q26, ...y26 ] = []} = z();", true],
["const {x28: [,,{...y28}]} = z();", true],
["const {x29: [,,{q29, ...y29}]} = z();", true],
["const [,,{y30, ...x30}] = z();", true],
["const [,,{...x31}] = z();", true],
["const { x32: { }, w32: { ...y32 } } = z();", true],
["const [,,{}, {...q32}] = z();", true],
["const { ...y33 } = z();", true],
["const { x16: [] } = z();", false],
["const {} = {};", false],
["const [,,x27] = z();", false],
])("%s", (code, expectedResult) => {
const ast = getFistObjectPattern(code);
const result = shouldStoreRHSInTemporaryVariable(ast);
expect(result).toEqual(expectedResult);
});
});

View File

@ -1412,6 +1412,7 @@ __metadata:
"@babel/helper-compilation-targets": "workspace:^7.14.5" "@babel/helper-compilation-targets": "workspace:^7.14.5"
"@babel/helper-plugin-test-runner": "workspace:*" "@babel/helper-plugin-test-runner": "workspace:*"
"@babel/helper-plugin-utils": "workspace:^7.14.5" "@babel/helper-plugin-utils": "workspace:^7.14.5"
"@babel/parser": "workspace:*"
"@babel/plugin-syntax-object-rest-spread": ^7.8.3 "@babel/plugin-syntax-object-rest-spread": ^7.8.3
"@babel/plugin-transform-parameters": "workspace:^7.14.5" "@babel/plugin-transform-parameters": "workspace:^7.14.5"
peerDependencies: peerDependencies: