Skip to content

Commit

Permalink
ref. mozilla#780 fix Object.assign() -- and [[Set]] in general -- for…
Browse files Browse the repository at this point in the history
… the case where the target object inherits from a prototype that already contains the property being set
  • Loading branch information
dcitron committed Jul 3, 2022
1 parent 7a94441 commit 3b0002a
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 4 deletions.
9 changes: 9 additions & 0 deletions src/org/mozilla/javascript/ScriptableObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Supplier;

import org.mozilla.javascript.ScriptRuntime.StringIdOrIndex;
import org.mozilla.javascript.annotations.JSConstructor;
import org.mozilla.javascript.annotations.JSFunction;
Expand Down Expand Up @@ -2483,6 +2484,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
Expand Down
23 changes: 19 additions & 4 deletions src/org/mozilla/javascript/Slot.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand Down
71 changes: 71 additions & 0 deletions testsrc/org/mozilla/javascript/tests/es6/NativeObjectTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import java.util.HashMap;
import java.util.Map;

import org.junit.Test;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Scriptable;
Expand Down Expand Up @@ -148,6 +149,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(
Expand Down

0 comments on commit 3b0002a

Please sign in to comment.