Skip to content

Fix object spread runtime semantics #32514

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

Merged
merged 1 commit into from
Jul 22, 2019
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions src/compiler/transformers/es2018.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,14 +229,39 @@ namespace ts {
if (node.transformFlags & TransformFlags.ContainsObjectRestOrSpread) {
// spread elements emit like so:
// non-spread elements are chunked together into object literals, and then all are passed to __assign:
// { a, ...o, b } => __assign({a}, o, {b});
// { a, ...o, b } => __assign(__assign({a}, o), {b});
// If the first element is a spread element, then the first argument to __assign is {}:
// { ...o, a, b, ...o2 } => __assign({}, o, {a, b}, o2)
// { ...o, a, b, ...o2 } => __assign(__assign(__assign({}, o), {a, b}), o2)
//
// We cannot call __assign with more than two elements, since any element could cause side effects. For
// example:
// var k = { a: 1, b: 2 };
// var o = { a: 3, ...k, b: k.a++ };
// // expected: { a: 1, b: 1 }
// If we translate the above to `__assign({ a: 3 }, k, { b: k.a++ })`, the `k.a++` will evaluate before
// `k` is spread and we end up with `{ a: 2, b: 1 }`.
//
// This also occurs for spread elements, not just property assignments:
// var k = { a: 1, get b() { l = { z: 9 }; return 2; } };
// var l = { c: 3 };
// var o = { ...k, ...l };
// // expected: { a: 1, b: 2, z: 9 }
// If we translate the above to `__assign({}, k, l)`, the `l` will evaluate before `k` is spread and we
// end up with `{ a: 1, b: 2, c: 3 }`
const objects = chunkObjectLiteralElements(node.properties);
if (objects.length && objects[0].kind !== SyntaxKind.ObjectLiteralExpression) {
objects.unshift(createObjectLiteral());
}
return createAssignHelper(context, objects);
let expression: Expression = objects[0];
if (objects.length > 1) {
for (let i = 1; i < objects.length; i++) {
expression = createAssignHelper(context, [expression, objects[i]]);
}
return expression;
}
else {
return createAssignHelper(context, objects);
}
}
return visitEachChild(node, visitor, context);
}
Expand Down
1 change: 1 addition & 0 deletions src/testRunner/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
"unittests/evaluation/asyncArrow.ts",
"unittests/evaluation/asyncGenerator.ts",
"unittests/evaluation/forAwaitOf.ts",
"unittests/evaluation/objectRest.ts",
"unittests/services/cancellableLanguageServiceOperations.ts",
"unittests/services/colorization.ts",
"unittests/services/convertToAsyncFunction.ts",
Expand Down
28 changes: 28 additions & 0 deletions src/testRunner/unittests/evaluation/objectRest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
describe("unittests:: evaluation:: objectRest", () => {
// https://github.com/microsoft/TypeScript/issues/31469
it("side effects in property assignment", async () => {
const result = evaluator.evaluateTypeScript(`
const k = { a: 1, b: 2 };
const o = { a: 3, ...k, b: k.a++ };
export const output = o;
`);
assert.deepEqual(result.output, { a: 1, b: 1 });
});
it("side effects in during spread", async () => {
const result = evaluator.evaluateTypeScript(`
const k = { a: 1, get b() { l = { c: 9 }; return 2; } };
let l = { c: 3 };
const o = { ...k, ...l };
export const output = o;
`);
assert.deepEqual(result.output, { a: 1, b: 2, c: 9 });
});
it("trailing literal-valued object-literal", async () => {
const result = evaluator.evaluateTypeScript(`
const k = { a: 1 }
const o = { ...k, ...{ b: 2 } };
export const output = o;
`);
assert.deepEqual(result.output, { a: 1, b: 2 });
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ exports.testRecFun = function (parent) {
return {
result: parent,
deeper: function (child) {
return exports.testRecFun(__assign({}, parent, child));
return exports.testRecFun(__assign(__assign({}, parent), child));
}
};
};
Expand Down
2 changes: 1 addition & 1 deletion tests/baselines/reference/excessPropertyCheckWithSpread.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ var __assign = (this && this.__assign) || function () {
return __assign.apply(this, arguments);
};
f(__assign({ a: 1 }, i));
f(__assign({ a: 1 }, l, r));
f(__assign(__assign({ a: 1 }, l), r));
2 changes: 1 addition & 1 deletion tests/baselines/reference/genericIsNeverEmptyObject.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ var __rest = (this && this.__rest) || function (s, e) {
};
function test(obj) {
var a = obj.a, rest = __rest(obj, ["a"]);
return __assign({}, rest, { b: a });
return __assign(__assign({}, rest), { b: a });
}
var o1 = { a: 'hello', x: 42 };
var o2 = test(o1);
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ var __assign = (this && this.__assign) || function () {
return __assign.apply(this, arguments);
};
var x = { b: 1, extra: 2 };
var xx = __assign({ a: 1 }, x, { z: 3 }); // error for 'z', no error for 'extra'
var xx = __assign(__assign({ a: 1 }, x), { z: 3 }); // error for 'z', no error for 'extra'
2 changes: 1 addition & 1 deletion tests/baselines/reference/objectLiteralNormalization.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ a2 = { a: "def" };
a2 = {};
a2 = { a: "def", b: 20 }; // Error
a2 = { a: 1 }; // Error
var b2 = __assign({}, b1, { z: 55 });
var b2 = __assign(__assign({}, b1), { z: 55 });
var b3 = __assign({}, b2);
var c1 = !true ? {} : opts;
var c2 = !true ? opts : {};
Expand Down
2 changes: 1 addition & 1 deletion tests/baselines/reference/objectRestForOf.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ for (let _b of array) {
({ x: xx } = _b, rrestOff = __rest(_b, ["x"]));
[xx, rrestOff];
}
for (const norest of array.map(a => (Object.assign({}, a, { x: 'a string' })))) {
for (const norest of array.map(a => (Object.assign(Object.assign({}, a), { x: 'a string' })))) {
[norest.x, norest.y];
// x is now a string. who knows why.
}
76 changes: 38 additions & 38 deletions tests/baselines/reference/objectSpread.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,45 +171,45 @@ var __assign = (this && this.__assign) || function () {
var o = { a: 1, b: 'no' };
var o2 = { b: 'yes', c: true };
var swap = { a: 'yes', b: -1 };
var addAfter = __assign({}, o, { c: false });
var addAfter = __assign(__assign({}, o), { c: false });
var addBefore = __assign({ c: false }, o);
// Note: ignore still changes the order that properties are printed
var ignore = __assign({ b: 'ignored' }, o);
var override = __assign({}, o, { b: 'override' });
var nested = __assign({}, __assign({ a: 3 }, { b: false, c: 'overriden' }), { c: 'whatever' });
var combined = __assign({}, o, o2);
var combinedBefore = __assign({ b: 'ok' }, o, o2);
var combinedMid = __assign({}, o, { b: 'ok' }, o2);
var combinedAfter = __assign({}, o, o2, { b: 'ok' });
var combinedNested = __assign({}, __assign({ a: 4 }, { b: false, c: 'overriden' }), { d: 'actually new' }, { a: 5, d: 'maybe new' });
var combinedNestedChangeType = __assign({}, __assign({ a: 1 }, { b: false, c: 'overriden' }), { c: -1 });
var override = __assign(__assign({}, o), { b: 'override' });
var nested = __assign(__assign({}, __assign({ a: 3 }, { b: false, c: 'overriden' })), { c: 'whatever' });
var combined = __assign(__assign({}, o), o2);
var combinedBefore = __assign(__assign({ b: 'ok' }, o), o2);
var combinedMid = __assign(__assign(__assign({}, o), { b: 'ok' }), o2);
var combinedAfter = __assign(__assign(__assign({}, o), o2), { b: 'ok' });
var combinedNested = __assign(__assign(__assign({}, __assign({ a: 4 }, { b: false, c: 'overriden' })), { d: 'actually new' }), { a: 5, d: 'maybe new' });
var combinedNestedChangeType = __assign(__assign({}, __assign({ a: 1 }, { b: false, c: 'overriden' })), { c: -1 });
var propertyNested = { a: __assign({}, o) };
// accessors don't copy the descriptor
// (which means that readonly getters become read/write properties)
var op = { get a() { return 6; } };
var getter = __assign({}, op, { c: 7 });
var getter = __assign(__assign({}, op), { c: 7 });
getter.a = 12;
// functions result in { }
var spreadFunc = __assign({}, (function () { }));
function from16326(header, authToken) {
return __assign({}, this.header, header, authToken && { authToken: authToken });
return __assign(__assign(__assign({}, this.header), header), authToken && { authToken: authToken });
}
// boolean && T results in Partial<T>
function conditionalSpreadBoolean(b) {
var o = { x: 12, y: 13 };
o = __assign({}, o, b && { x: 14 });
o = __assign(__assign({}, o), b && { x: 14 });
var o2 = __assign({}, b && { x: 21 });
return o;
}
function conditionalSpreadNumber(nt) {
var o = { x: 15, y: 16 };
o = __assign({}, o, nt && { x: nt });
o = __assign(__assign({}, o), nt && { x: nt });
var o2 = __assign({}, nt && { x: nt });
return o;
}
function conditionalSpreadString(st) {
var o = { x: 'hi', y: 17 };
o = __assign({}, o, st && { x: st });
o = __assign(__assign({}, o), st && { x: st });
var o2 = __assign({}, st && { x: st });
return o;
}
Expand All @@ -227,52 +227,52 @@ var C = /** @class */ (function () {
var c = new C();
var spreadC = __assign({}, c);
// own methods are enumerable
var cplus = __assign({}, c, { plus: function () { return this.p + 1; } });
var cplus = __assign(__assign({}, c), { plus: function () { return this.p + 1; } });
cplus.plus();
// new field's type conflicting with existing field is OK
var changeTypeAfter = __assign({}, o, { a: 'wrong type?' });
var changeTypeAfter = __assign(__assign({}, o), { a: 'wrong type?' });
var changeTypeBefore = __assign({ a: 'wrong type?' }, o);
var changeTypeBoth = __assign({}, o, swap);
var changeTypeBoth = __assign(__assign({}, o), swap);
// optional
function container(definiteBoolean, definiteString, optionalString, optionalNumber) {
var _a, _b, _c;
var optionalUnionStops = __assign({}, definiteBoolean, definiteString, optionalNumber);
var optionalUnionDuplicates = __assign({}, definiteBoolean, definiteString, optionalString, optionalNumber);
var allOptional = __assign({}, optionalString, optionalNumber);
var optionalUnionStops = __assign(__assign(__assign({}, definiteBoolean), definiteString), optionalNumber);
var optionalUnionDuplicates = __assign(__assign(__assign(__assign({}, definiteBoolean), definiteString), optionalString), optionalNumber);
var allOptional = __assign(__assign({}, optionalString), optionalNumber);
// computed property
var computedFirst = __assign((_a = {}, _a['before everything'] = 12, _a), o, { b: 'yes' });
var computedMiddle = __assign({}, o, (_b = {}, _b['in the middle'] = 13, _b.b = 'maybe?', _b), o2);
var computedAfter = __assign({}, o, (_c = { b: 'yeah' }, _c['at the end'] = 14, _c));
var computedFirst = __assign(__assign((_a = {}, _a['before everything'] = 12, _a), o), { b: 'yes' });
var computedMiddle = __assign(__assign(__assign({}, o), (_b = {}, _b['in the middle'] = 13, _b.b = 'maybe?', _b)), o2);
var computedAfter = __assign(__assign({}, o), (_c = { b: 'yeah' }, _c['at the end'] = 14, _c));
}
// shortcut syntax
var a = 12;
var shortCutted = __assign({}, o, { a: a });
var shortCutted = __assign(__assign({}, o), { a: a });
// non primitive
var spreadNonPrimitive = __assign({}, {});
// generic spreads
function f(t, u) {
return __assign({}, t, u, { id: 'id' });
return __assign(__assign(__assign({}, t), u), { id: 'id' });
}
var exclusive = f({ a: 1, b: 'yes' }, { c: 'no', d: false });
var overlap = f({ a: 1 }, { a: 2, b: 'extra' });
var overlapConflict = f({ a: 1 }, { a: 'mismatch' });
var overwriteId = f({ a: 1, id: true }, { c: 1, d: 'no' });
function genericSpread(t, u, v, w, obj) {
var x01 = __assign({}, t);
var x02 = __assign({}, t, t);
var x03 = __assign({}, t, u);
var x04 = __assign({}, u, t);
var x02 = __assign(__assign({}, t), t);
var x03 = __assign(__assign({}, t), u);
var x04 = __assign(__assign({}, u), t);
var x05 = __assign({ a: 5, b: 'hi' }, t);
var x06 = __assign({}, t, { a: 5, b: 'hi' });
var x07 = __assign({ a: 5, b: 'hi' }, t, { c: true }, obj);
var x09 = __assign({ a: 5 }, t, { b: 'hi', c: true }, obj);
var x10 = __assign({ a: 5 }, t, { b: 'hi' }, u, obj);
var x06 = __assign(__assign({}, t), { a: 5, b: 'hi' });
var x07 = __assign(__assign(__assign({ a: 5, b: 'hi' }, t), { c: true }), obj);
var x09 = __assign(__assign(__assign({ a: 5 }, t), { b: 'hi', c: true }), obj);
var x10 = __assign(__assign(__assign(__assign({ a: 5 }, t), { b: 'hi' }), u), obj);
var x11 = __assign({}, v);
var x12 = __assign({}, v, obj);
var x12 = __assign(__assign({}, v), obj);
var x13 = __assign({}, w);
var x14 = __assign({}, w, obj);
var x15 = __assign({}, t, v);
var x16 = __assign({}, t, w);
var x17 = __assign({}, t, w, obj);
var x18 = __assign({}, t, v, w);
var x14 = __assign(__assign({}, w), obj);
var x15 = __assign(__assign({}, t), v);
var x16 = __assign(__assign({}, t), w);
var x17 = __assign(__assign(__assign({}, t), w), obj);
var x18 = __assign(__assign(__assign({}, t), v), w);
}
2 changes: 1 addition & 1 deletion tests/baselines/reference/objectSpreadComputedProperty.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,5 @@ function f() {
var a = null;
var o1 = __assign({}, (_a = {}, _a[n] = n, _a));
var o2 = __assign({}, (_b = {}, _b[a] = n, _b));
var o3 = __assign((_c = {}, _c[a] = n, _c), {}, (_d = {}, _d[n] = n, _d), {}, (_e = {}, _e[m] = m, _e));
var o3 = __assign(__assign(__assign(__assign((_c = {}, _c[a] = n, _c), {}), (_d = {}, _d[n] = n, _d)), {}), (_e = {}, _e[m] = m, _e));
}
4 changes: 2 additions & 2 deletions tests/baselines/reference/objectSpreadIndexSignature.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ var __assign = (this && this.__assign) || function () {
};
return __assign.apply(this, arguments);
};
var i = __assign({}, indexed1, { b: 11 });
var i = __assign(__assign({}, indexed1), { b: 11 });
// only indexed has indexer, so i[101]: any
i[101];
var ii = __assign({}, indexed1, indexed2);
var ii = __assign(__assign({}, indexed1), indexed2);
// both have indexer, so i[1001]: number | boolean
ii[1001];
indexed3 = __assign({}, b ? indexed3 : undefined);
Expand Down
8 changes: 4 additions & 4 deletions tests/baselines/reference/objectSpreadNegative.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,20 +85,20 @@ var PublicX = /** @class */ (function () {
}());
var publicX;
var privateOptionalX;
var o2 = __assign({}, publicX, privateOptionalX);
var o2 = __assign(__assign({}, publicX), privateOptionalX);
var sn = o2.x; // error, x is private
var optionalString;
var optionalNumber;
var allOptional = __assign({}, optionalString, optionalNumber);
var allOptional = __assign(__assign({}, optionalString), optionalNumber);
;
;
var spread = __assign({ b: true }, { s: "foo" });
spread = { s: "foo" }; // error, missing 'b'
var b = { b: false };
spread = b; // error, missing 's'
// literal repeats are not allowed, but spread repeats are fine
var duplicated = __assign({ b: 'bad' }, o, { b: 'bad' }, o2, { b: 'bad' });
var duplicatedSpread = __assign({}, o, o);
var duplicated = __assign(__assign(__assign(__assign({ b: 'bad' }, o), { b: 'bad' }), o2), { b: 'bad' });
var duplicatedSpread = __assign(__assign({}, o), o);
// primitives are not allowed, except for falsy ones
var spreadNum = __assign({}, 12);
var spreadSum = __assign({}, 1 + 1);
Expand Down
2 changes: 1 addition & 1 deletion tests/baselines/reference/objectSpreadNegativeParse.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ var o7 = __assign({}, o ? : );
var o8 = __assign({}, * o);
var o9 = __assign({}, matchMedia()), _a = void 0;
;
var o10 = __assign({}, get, { x: function () { return 12; } });
var o10 = __assign(__assign({}, get), { x: function () { return 12; } });
20 changes: 10 additions & 10 deletions tests/baselines/reference/objectSpreadStrictNull.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,21 +58,21 @@ var __assign = (this && this.__assign) || function () {
};
function f(definiteBoolean, definiteString, optionalString, optionalNumber, undefinedString, undefinedNumber) {
// optional
var optionalUnionStops = __assign({}, definiteBoolean, definiteString, optionalNumber);
var optionalUnionDuplicates = __assign({}, definiteBoolean, definiteString, optionalString, optionalNumber);
var allOptional = __assign({}, optionalString, optionalNumber);
var optionalUnionStops = __assign(__assign(__assign({}, definiteBoolean), definiteString), optionalNumber);
var optionalUnionDuplicates = __assign(__assign(__assign(__assign({}, definiteBoolean), definiteString), optionalString), optionalNumber);
var allOptional = __assign(__assign({}, optionalString), optionalNumber);
// undefined
var undefinedUnionStops = __assign({}, definiteBoolean, definiteString, undefinedNumber);
var undefinedUnionDuplicates = __assign({}, definiteBoolean, definiteString, undefinedString, undefinedNumber);
var allUndefined = __assign({}, undefinedString, undefinedNumber);
var undefinedWithOptionalContinues = __assign({}, definiteBoolean, undefinedString, optionalNumber);
var undefinedUnionStops = __assign(__assign(__assign({}, definiteBoolean), definiteString), undefinedNumber);
var undefinedUnionDuplicates = __assign(__assign(__assign(__assign({}, definiteBoolean), definiteString), undefinedString), undefinedNumber);
var allUndefined = __assign(__assign({}, undefinedString), undefinedNumber);
var undefinedWithOptionalContinues = __assign(__assign(__assign({}, definiteBoolean), undefinedString), optionalNumber);
}
var m = { title: "The Matrix", yearReleased: 1999 };
// should error here because title: undefined is not assignable to string
var x = __assign({}, m, { title: undefined });
var x = __assign(__assign({}, m), { title: undefined });
function g(fields, partialFields, nearlyPartialFields) {
// ok, undefined is stripped from optional properties when spread
fields = __assign({}, fields, partialFields);
fields = __assign(__assign({}, fields), partialFields);
// error: not optional, undefined remains
fields = __assign({}, fields, nearlyPartialFields);
fields = __assign(__assign({}, fields), nearlyPartialFields);
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@ var __assign = (this && this.__assign) || function () {
return __assign.apply(this, arguments);
};
var obj = {};
var a = __assign({}, obj, { prop: function () {
return __assign({}, obj, { metadata: 213 });
var a = __assign(__assign({}, obj), { prop: function () {
return __assign(__assign({}, obj), { metadata: 213 });
} });
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ var __assign = (this && this.__assign) || function () {
return __assign.apply(this, arguments);
};
// [ts] Initializer provides no value for this binding element and the binding element has no default value.
var _a = __assign({}, bob, alice), naam = _a.naam, age = _a.age;
var _a = __assign(__assign({}, bob), alice), naam = _a.naam, age = _a.age;
2 changes: 1 addition & 1 deletion tests/baselines/reference/spreadIntersection.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ var intersection;
var o1;
var o1 = __assign({}, intersection);
var o2;
var o2 = __assign({}, intersection, { c: false });
var o2 = __assign(__assign({}, intersection), { c: false });
2 changes: 1 addition & 1 deletion tests/baselines/reference/spreadNonPrimitive.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ var __assign = (this && this.__assign) || function () {
};
return __assign.apply(this, arguments);
};
var x = __assign({ a: 1 }, o, { b: 2 });
var x = __assign(__assign({ a: 1 }, o), { b: 2 });
4 changes: 2 additions & 2 deletions tests/baselines/reference/spreadUnion.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ var union;
var o3;
var o3 = __assign({}, union);
var o4;
var o4 = __assign({}, union, { a: false });
var o4 = __assign(__assign({}, union), { a: false });
var o5;
var o5 = __assign({}, union, union);
var o5 = __assign(__assign({}, union), union);
8 changes: 4 additions & 4 deletions tests/baselines/reference/spreadUnion2.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ var o1 = __assign({}, undefinedUnion);
var o2;
var o2 = __assign({}, nullUnion);
var o3;
var o3 = __assign({}, undefinedUnion, nullUnion);
var o3 = __assign({}, nullUnion, undefinedUnion);
var o3 = __assign(__assign({}, undefinedUnion), nullUnion);
var o3 = __assign(__assign({}, nullUnion), undefinedUnion);
var o4;
var o4 = __assign({}, undefinedUnion, undefinedUnion);
var o4 = __assign(__assign({}, undefinedUnion), undefinedUnion);
var o5;
var o5 = __assign({}, nullUnion, nullUnion);
var o5 = __assign(__assign({}, nullUnion), nullUnion);
Loading