diff --git a/src/org/mozilla/javascript/ScriptableObject.java b/src/org/mozilla/javascript/ScriptableObject.java index 8259f0746f..6b60865c7c 100644 --- a/src/org/mozilla/javascript/ScriptableObject.java +++ b/src/org/mozilla/javascript/ScriptableObject.java @@ -2483,6 +2483,14 @@ boolean putImpl(Object key, int index, Scriptable start, Object value, boolean i if (slot == null) { return false; } + // If this object is not the Receiver (start) object + // and if the slot is a Writeable data descriptor + // then invoke putImpl() on the Receiver (start) object + if (slot.isValueSlot() + && !slot.isReadOnly(isThrow) + && start instanceof ScriptableObject) { + return ((ScriptableObject) start).putImpl(key, index, start, value, isThrow); + } } else if (!isExtensible) { slot = slotMap.query(key, index); if ((slot == null diff --git a/src/org/mozilla/javascript/Slot.java b/src/org/mozilla/javascript/Slot.java index ac0e445312..cc77e923b5 100644 --- a/src/org/mozilla/javascript/Slot.java +++ b/src/org/mozilla/javascript/Slot.java @@ -40,6 +40,24 @@ boolean isSetterSlot() { return false; } + /** + * Return true if this Slot has the {@link ScriptableObject#READONLY read-only} {@link + * #getAttributes() attribute}. Callers may wish to check if this is a {@link #isValueSlot() + * value slot} first. + * + * @param isThrow set to true to throw a type error instead of returning true + * @return true if this slot is read-only and {@code isThrow} is false + */ + boolean isReadOnly(boolean isThrow) { + if ((attributes & ScriptableObject.READONLY) != 0) { + if (isThrow) { + throw ScriptRuntime.typeErrorById("msg.modify.readonly", name); + } + return true; + } + return false; + } + protected Slot(Slot oldSlot) { name = oldSlot.name; indexOrHash = oldSlot.indexOrHash; @@ -61,10 +79,7 @@ public final boolean setValue(Object value, Scriptable owner, Scriptable start) } public boolean setValue(Object value, Scriptable owner, Scriptable start, boolean isThrow) { - if ((attributes & ScriptableObject.READONLY) != 0) { - if (isThrow) { - throw ScriptRuntime.typeErrorById("msg.modify.readonly", name); - } + if (isReadOnly(isThrow)) { return true; } if (owner == start) { diff --git a/testsrc/org/mozilla/javascript/tests/es6/NativeObjectTest.java b/testsrc/org/mozilla/javascript/tests/es6/NativeObjectTest.java index c15e5425d6..40d09c9d9c 100644 --- a/testsrc/org/mozilla/javascript/tests/es6/NativeObjectTest.java +++ b/testsrc/org/mozilla/javascript/tests/es6/NativeObjectTest.java @@ -148,6 +148,76 @@ public void testAssignUnwritable() { "error"); } + @Test + public void testAssignVariousKeyTypes() { + evaluateAndAssert( + "var strKeys = Object.assign({}, {a: 1, b: 2});\n" + + "var arrayKeys = Object.assign({}, ['a', 'b']);\n" + + "var res = 'strKeys: ' + JSON.stringify(strKeys) + " + + "' arrayKeys:' + JSON.stringify(arrayKeys);\n" + + "res", + "strKeys: {\"a\":1,\"b\":2} arrayKeys:{\"0\":\"a\",\"1\":\"b\"}"); + } + + @Test + public void testAssignWithPrototypeStringKeys() { + final String script = + "var targetProto = { a: 1, b: 2 };\n" + + "var target = Object.create(targetProto);\n" + + "var source = { b: 4, c: 5 };\n" + + "var assigned = Object.assign(target, source);\n" + + "var res = 'targetProto: ' + JSON.stringify(targetProto) + " + + "' target: ' + JSON.stringify(target) + " + + "' assigned: ' + JSON.stringify(assigned);\n" + + "res"; + + evaluateAndAssert( + script, + "targetProto: {\"a\":1,\"b\":2} target: {\"b\":4,\"c\":5} assigned: {\"b\":4,\"c\":5}"); + } + + @Test + public void testAssignWithPrototypeNumericKeys() { + final String script = + "var targetProto = {0: 'a', 1: 'b'};\n" + + "var target = Object.create(targetProto);\n" + + "var source = {1: 'c', 2: 'd'};\n" + + "var assigned = Object.assign(target, source);\n" + + "var res = 'targetProto: ' + JSON.stringify(targetProto) + " + + "' target: ' + JSON.stringify(target) + " + + "' assigned: ' + JSON.stringify(assigned);\n" + + "res"; + + evaluateAndAssert( + script, + "targetProto: {\"0\":\"a\",\"1\":\"b\"} target: {\"1\":\"c\",\"2\":\"d\"} assigned: {\"1\":\"c\",\"2\":\"d\"}"); + } + + @Test + public void testAssignWithPrototypeWithGettersAndSetters() { + final String script = + "var targetProto = {" + + "_a: 1, get a() { return this._a }, set a(val) { this._a = val+10 }," + + " _b: 2, get b() { return this._b }, set b(val) { this._b = val+10 }" + + "};\n" + + "var target = Object.create(targetProto);\n" + + "var source = {" + + "_b: 4, get b() { return this._b * 4 }, set b(val) { this._b = val+10 }," + + " _c: 5, get c() { return this._c }, set c(val) { this._c = val+10 }};\n" + + "var assigned = Object.assign(target, source);\n" + + "var res = 'targetProto: ' + JSON.stringify(targetProto) + " + + "' target: ' + JSON.stringify(target) + " + + "' assigned: ' + JSON.stringify(assigned) + " + + "' assigned.a: ' + assigned.a + " + + "' assigned.b: ' + assigned.b + " + + "' assigned.c: ' + assigned.c;\n" + + "res"; + + evaluateAndAssert( + script, + "targetProto: {\"_a\":1,\"a\":1,\"_b\":2,\"b\":2} target: {\"_b\":26,\"_c\":5,\"c\":5} assigned: {\"_b\":26,\"_c\":5,\"c\":5} assigned.a: 1 assigned.b: 26 assigned.c: 5"); + } + @Test public void testSetPrototypeOfNull() { evaluateAndAssert(