Fix object spread runtime semantics (#32514)

This commit is contained in:
Ron Buckton
2019-07-22 16:46:09 -07:00
committed by GitHub
parent 70b7bd5a1b
commit 47e3fedb5d
25 changed files with 135 additions and 81 deletions
+28 -3
View File
@@ -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);
}
+1
View File
@@ -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",
@@ -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 });
});
});
@@ -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));
}
};
};
@@ -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));
@@ -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);
@@ -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'
@@ -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 : {};
+1 -1
View File
@@ -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.
}
+38 -38
View File
@@ -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;
}
@@ -227,31 +227,31 @@ 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' });
@@ -259,20 +259,20 @@ 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);
}
@@ -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));
}
@@ -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);
@@ -85,11 +85,11 @@ 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" });
@@ -97,8 +97,8 @@ 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);
@@ -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; } });
@@ -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);
}
@@ -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 });
} });
@@ -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;
@@ -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 });
@@ -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 });
+2 -2
View File
@@ -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);
+4 -4
View File
@@ -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);
+1 -1
View File
@@ -42,5 +42,5 @@ function g(t) {
g();
g(undefined);
g(null);
var x = __assign({}, nullAndUndefinedUnion, nullAndUndefinedUnion);
var x = __assign(__assign({}, nullAndUndefinedUnion), nullAndUndefinedUnion);
var y = __assign({}, nullAndUndefinedUnion);
@@ -28,4 +28,4 @@ var __assign = (this && this.__assign) || function () {
};
return __assign.apply(this, arguments);
};
ab = __assign({}, a, { y: null }); // Should be allowed, since `y` is missing on `A`
ab = __assign(__assign({}, a), { y: null }); // Should be allowed, since `y` is missing on `A`
+2 -2
View File
@@ -279,8 +279,8 @@ function f25() {
// Spread of unknown causes result to be unknown
function f26(x, y, z) {
var o1 = __assign({ a: 42 }, x); // { a: number }
var o2 = __assign({ a: 42 }, x, y); // unknown
var o3 = __assign({ a: 42 }, x, y, z); // any
var o2 = __assign(__assign({ a: 42 }, x), y); // unknown
var o3 = __assign(__assign(__assign({ a: 42 }, x), y), z); // any
}
// Functions with unknown return type don't need return expressions
function f27() {
@@ -20,7 +20,7 @@ class D {
//// [useBeforeDeclaration_propertyAssignment.js]
export class C {
constructor() {
this.a = Object.assign({ b: this.b }, this.c, { [this.b]: `${this.c}` });
this.a = Object.assign(Object.assign({ b: this.b }, this.c), { [this.b]: `${this.c}` });
this.b = 0;
this.c = { c: this.b };
}