Skip to content
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

Implement objectRestNoSymbols, setSpreadProperties and pureGetters for object rest/spread #12505

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/babel-core/src/config/validation/options.js
Expand Up @@ -338,10 +338,12 @@ export const assumptionsNames = new Set<string>([
"iterableIsArray",
"mutableTemplateObject",
"noDocumentAll",
"objectRestNoSymbols",
"pureGetters",
"setClassMethods",
"setComputedProperties",
"setPublicClassFields",
"setSpreadProperties",
"skipForOfIteratorClosing",
]);

Expand Down
27 changes: 15 additions & 12 deletions packages/babel-plugin-proposal-object-rest-spread/src/index.js
Expand Up @@ -23,8 +23,10 @@ export default declare((api, opts) => {
throw new Error(".loose must be a boolean, or undefined");
}

const ignoreFunctionLength =
api.assumption("ignoreFunctionLength") ?? opts.loose;
const ignoreFunctionLength = api.assumption("ignoreFunctionLength") ?? loose;
const objectRestNoSymbols = api.assumption("objectRestNoSymbols") ?? loose;
const pureGetters = api.assumption("pureGetters") ?? loose;
const setSpreadProperties = api.assumption("setSpreadProperties") ?? loose;

function getExtendsHelper(file) {
return useBuiltIns
Expand Down Expand Up @@ -136,7 +138,7 @@ export default declare((api, opts) => {
}

//expects path to an object pattern
function createObjectSpread(path, file, objRef) {
function createObjectRest(path, file, objRef) {
const props = path.get("properties");
const last = props[props.length - 1];
t.assertRestElement(last.node);
Expand Down Expand Up @@ -175,7 +177,9 @@ export default declare((api, opts) => {
impureComputedPropertyDeclarators,
restElement.argument,
t.callExpression(
file.addHelper(`objectWithoutProperties${loose ? "Loose" : ""}`),
file.addHelper(
`objectWithoutProperties${objectRestNoSymbols ? "Loose" : ""}`,
),
[t.cloneNode(objRef), keyExpression],
),
];
Expand Down Expand Up @@ -364,9 +368,9 @@ export default declare((api, opts) => {
impureComputedPropertyDeclarators,
argument,
callExpression,
] = createObjectSpread(objectPatternPath, file, ref);
] = createObjectRest(objectPatternPath, file, ref);

if (loose) {
if (pureGetters) {
removeUnusedExcludedKeys(objectPatternPath);
}

Expand Down Expand Up @@ -447,7 +451,7 @@ export default declare((api, opts) => {
impureComputedPropertyDeclarators,
argument,
callExpression,
] = createObjectSpread(leftPath, file, t.identifier(refName));
] = createObjectRest(leftPath, file, t.identifier(refName));

if (impureComputedPropertyDeclarators.length > 0) {
nodes.push(
Expand Down Expand Up @@ -556,7 +560,7 @@ export default declare((api, opts) => {
if (!hasSpread(path.node)) return;

let helper;
if (loose) {
if (setSpreadProperties) {
helper = getExtendsHelper(file);
} else {
try {
Expand Down Expand Up @@ -586,10 +590,9 @@ export default declare((api, opts) => {
return;
}

// In loose mode, we don't want to make multiple calls. We're assuming
// that the spread objects either don't use getters, or that the
// getters are pure and don't depend on the order of evaluation.
if (loose) {
// When we can assume that getters are pure and don't depend on
// the order of evaluation, we can avoid making multiple calls.
if (pureGetters) {
if (hadProps) {
exp.arguments.push(obj);
}
Expand Down
@@ -0,0 +1,9 @@
{
"plugins": [
["external-helpers", { "helperVersion": "7.100.0"}],
"proposal-object-rest-spread"
],
"assumptions": {
"objectRestNoSymbols": true
}
}
@@ -0,0 +1 @@
({ a, b, ...c } = obj);
@@ -0,0 +1,7 @@
var _obj = obj;
({
a,
b
} = _obj);
c = babelHelpers.objectWithoutPropertiesLoose(_obj, ["a", "b"]);
_obj;
@@ -0,0 +1 @@
let { [a]: b, ...c } = obj;
@@ -0,0 +1,5 @@
let _a = a,
{
[_a]: b
} = obj,
c = babelHelpers.objectWithoutPropertiesLoose(obj, [_a].map(babelHelpers.toPropertyKey));
@@ -0,0 +1,7 @@
let sym = Symbol();

let { a, ...r } = { a: 1, b: 2, [sym]: 3 };

expect(a).toBe(1);
expect(r.b).toBe(2);
expect(sym in r).toBe(false);
@@ -0,0 +1 @@
let { a, nested: { b, c, ...d }, e } = obj;
@@ -0,0 +1,9 @@
let {
a,
nested: {
b,
c
},
e
} = obj,
d = babelHelpers.objectWithoutPropertiesLoose(obj.nested, ["b", "c"]);
@@ -0,0 +1 @@
var { a, b, ...c } = obj;
@@ -0,0 +1,5 @@
var {
a,
b
} = obj,
c = babelHelpers.objectWithoutPropertiesLoose(obj, ["a", "b"]);
@@ -0,0 +1,9 @@
{
"plugins": [
["external-helpers", { "helperVersion": "7.100.0"}],
"proposal-object-rest-spread"
],
"assumptions": {
"pureGetters": true
}
}
@@ -0,0 +1,11 @@
let called = false;
let obj = {
get foo() { called = true }
};

let { foo, ...rest } = obj;

expect("foo" in rest).toBe(false);

// Without assuming that getters are pure (in this case it isn't), this should be true
expect(called).toBe(false);
@@ -0,0 +1,20 @@
// should not remove when destructuring into existing bindings
({ a2, ...b2 } = c2);

function render() {
const {
excluded,
excluded2: excludedRenamed,
used,
used2: usedRenamed,
...props
} = this.props;

console.log(used, usedRenamed);

return React.createElement("input", props);
}

function smth({ unused, ...rest }) {
call(rest);
}
@@ -0,0 +1,23 @@
// should not remove when destructuring into existing bindings
var _c = c2;
({
a2
} = _c);
b2 = babelHelpers.objectWithoutProperties(_c, ["a2"]);
_c;

function render() {
const _this$props = this.props,
{
used,
used2: usedRenamed
} = _this$props,
props = babelHelpers.objectWithoutProperties(_this$props, ["excluded", "excluded2", "used", "used2"]);
console.log(used, usedRenamed);
return React.createElement("input", props);
}

function smth(_ref) {
let rest = babelHelpers.objectWithoutProperties(_ref, ["unused"]);
call(rest);
}
@@ -0,0 +1,10 @@
let count = 0;

let withFoo = { get foo() { return count++; } };
let withBar = { get bar() { return count++; } };

let res = { ...withFoo, middle: count, ...withBar };

// Without assuming that getters are pure (in this case it isn't),
// the result should be { foo: 0, middle: 1, bar: 1 }
expect(res).toEqual({ foo: 0, middle: 0, bar: 1 });
@@ -0,0 +1 @@
let obj = { a, ...b, c, ...d, e };
@@ -0,0 +1,7 @@
let obj = babelHelpers.objectSpread2({
a
}, b, {
c
}, d, {
e
});
@@ -0,0 +1,7 @@
var x;
var y;
var z;

z = { x, ...y };

z = { x, w: { ...y } };
@@ -0,0 +1,10 @@
var x;
var y;
var z;
z = Object.assign({
x
}, y);
z = {
x,
w: Object.assign({}, y)
};
@@ -0,0 +1,9 @@
var log = [];

var a = {
...{ get foo() { log.push(1); } },
get bar() { log.push(2); }
};

// Loose mode uses regular Get, not GetOwnProperty.
expect(log).toEqual([1, 2]);
@@ -0,0 +1,16 @@
var a;
var b;
var c;
var d;
var x;
var y;

({ x, ...y, a, ...b, c });

({ ...Object.prototype });

({ ...{ foo: 'bar' } });

({ ...{ foo: 'bar' }, ...{ bar: 'baz' } });

({ ...{ get foo () { return 'foo' } } });
@@ -0,0 +1,28 @@
var a;
var b;
var c;
var d;
var x;
var y;
Object.assign(Object.assign(Object.assign({
x
}, y), {}, {
a
}, b), {}, {
c
});
Object.assign({}, Object.prototype);
Object.assign({}, {
foo: 'bar'
});
Object.assign(Object.assign({}, {
foo: 'bar'
}), {
bar: 'baz'
});
Object.assign({}, {
get foo() {
return 'foo';
}

});
@@ -0,0 +1,9 @@
const oldGOPDs = Object.getOwnPropertyDescriptors;
Object.getOwnPropertyDescriptors = null;

try {
({ ...{ a: 1 }, b: 1, ...{} });
} finally {
Object.getOwnPropertyDescriptors = oldGOPDs;
}

@@ -0,0 +1,43 @@
"use strict";
Object.defineProperty(Object.prototype, 'NOSET', {
get(value) {
// noop
},
});

Object.defineProperty(Object.prototype, 'NOWRITE', {
writable: false,
value: 'abc',
});

const obj = { 'NOSET': 123 };
// this won't work as expected if transformed as Object.assign (or equivalent)
// because those trigger object setters (spread don't)
expect(() => {
const objSpread = { ...obj };
}).toThrow();

const obj2 = { 'NOWRITE': 456 };
// this throws `TypeError: Cannot assign to read only property 'NOWRITE'`
// if transformed as Object.assign (or equivalent) because those use *assignment* for creating properties
// (spread defines them)
expect(() => {
const obj2Spread = { ...obj2 };
}).toThrow();

const KEY = Symbol('key');
const obj3Spread = { ...{ get foo () { return 'bar' } }, [KEY]: 'symbol' };
expect(Object.getOwnPropertyDescriptor(obj3Spread, 'foo').value).toBe('bar');
expect(Object.getOwnPropertyDescriptor(obj3Spread, KEY).value).toBe('symbol');

const obj4Spread = { ...Object.prototype };
expect(Object.getOwnPropertyDescriptor(obj4Spread, 'hasOwnProperty')).toBeUndefined();

expect(() => ({ ...null, ...undefined })).not.toThrow();

const o = Object.create(null);
o.a = 'foo';
o.__proto__ = [];
const o2 = { ...o };
// Loose will do o2.__proto__ = []
expect(Array.isArray(Object.getPrototypeOf(o2))).toBe(true);
@@ -0,0 +1,9 @@
{
"plugins": [
["external-helpers", { "helperVersion": "7.100.0"}],
["proposal-object-rest-spread", { "useBuiltIns": true }]
],
"assumptions": {
"setSpreadProperties": true
}
}
@@ -0,0 +1,7 @@
var x;
var y;
var z;

z = { x, ...y };

z = { x, w: { ...y } };
@@ -0,0 +1,10 @@
var x;
var y;
var z;
z = babelHelpers.extends({
x
}, y);
z = {
x,
w: babelHelpers.extends({}, y)
};