Skip to content

Commit 26c8bb5

Browse files
committed
NativeReflect: re-implement Reflect.set()
1 parent d566364 commit 26c8bb5

2 files changed

Lines changed: 218 additions & 26 deletions

File tree

rhino/src/main/java/org/mozilla/javascript/NativeReflect.java

Lines changed: 148 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -386,41 +386,165 @@ private static Object preventExtensions(
386386
return target.preventExtensions();
387387
}
388388

389+
/*
390+
* https://tc39.es/ecma262/#sec-reflect.set
391+
* 1. If target is not an Object, throw a TypeError exception.
392+
* 2. Let key be ? ToPropertyKey(propertyKey).
393+
* 3. If receiver is not present, then
394+
* a. Set receiver to target.
395+
* 4. Return ? target.[[Set]](key, V, receiver).
396+
*/
389397
private static Object set(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
390-
ScriptableObject target = checkTarget(args);
391-
if (args.length < 2) {
392-
return true;
398+
final ScriptableObject target = checkTarget(args);
399+
final Object propertyKey = args.length > 1 ? args[1] : Undefined.instance;
400+
final Object value = args.length > 2 ? args[2] : Undefined.instance;
401+
final Object receiver = args.length > 3 ? args[3] : target;
402+
403+
// If target is a proxy, delegate to the proxy handler
404+
if (target instanceof NativeProxy) {
405+
final NativeProxy proxy = (NativeProxy) target;
406+
final Function trap = proxy.getTrap("set");
407+
if (trap != null) {
408+
final ScriptableObject proxyTarget = proxy.getTargetThrowIfRevoked();
409+
final Object[] trapArgs = {proxyTarget, propertyKey, value, receiver};
410+
final boolean booleanTrapResult = ScriptRuntime.toBoolean(proxy.callTrap(trap, trapArgs));
411+
if (!booleanTrapResult) {
412+
return false;
413+
}
414+
415+
// checks for non-configurable properties
416+
// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-set-p-v-receiver steps 10
417+
final DescriptorInfo targetDesc = proxyTarget.getOwnPropertyDescriptor(cx, propertyKey);
418+
if (targetDesc != null && targetDesc.isConfigurable(false)) {
419+
if (targetDesc.isDataDescriptor() && targetDesc.isWritable(false)) {
420+
if (!Objects.equals(value, targetDesc.value)) {
421+
throw ScriptRuntime.typeError(
422+
"proxy can't successfully set a non-writable,"
423+
+ " non-configurable property '\"" + propertyKey + "\"'");
424+
}
425+
}
426+
if (targetDesc.isAccessorDescriptor()
427+
&& (targetDesc.setter == null
428+
|| targetDesc.setter == Scriptable.NOT_FOUND
429+
|| Undefined.isUndefined(targetDesc.setter))) {
430+
throw ScriptRuntime.typeError(
431+
"proxy can't successfully set a non-writable,"
432+
+ " non-configurable property '\"" + propertyKey + "\"'");
433+
}
434+
}
435+
return true;
436+
}
393437
}
394438

395-
ScriptableObject receiver =
396-
args.length > 3 ? ScriptableObject.ensureScriptableObject(args[3]) : target;
397-
if (receiver != target) {
398-
DescriptorInfo descriptor = target.getOwnPropertyDescriptor(cx, args[1]);
399-
if (descriptor != null) {
400-
Object setter = descriptor.setter;
401-
if (setter != null && setter != NOT_FOUND) {
402-
((Function) setter).call(cx, scope, receiver, new Object[] {args[2]});
403-
return true;
439+
return internalSet(cx, target, propertyKey, value, receiver);
440+
}
441+
442+
/*
443+
* https://tc39.es/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots-set-p-v-receiver
444+
* 1. Let ownDesc be ? O.[[GetOwnProperty]](P).
445+
* 2. If ownDesc is undefined, then
446+
* a. Let parent be ? O.[[GetPrototypeOf]]().
447+
* b. If parent is not null, then
448+
* i. Return ? parent.[[Set]](P, V, Receiver).
449+
* c. Else,
450+
* i. Set ownDesc to the PropertyDescriptor
451+
* { [[Value]]: undefined, [[Writable]]: true,
452+
* [[Enumerable]]: true, [[Configurable]]: true }.
453+
* 3. If IsDataDescriptor(ownDesc) is true, then
454+
* a. If ownDesc.[[Writable]] is false, return false.
455+
* b. If Receiver is not an Object, return false.
456+
* c. Let existingDescriptor be ? Receiver.[[GetOwnProperty]](P).
457+
* d. If existingDescriptor is not undefined, then
458+
* i. If IsAccessorDescriptor(existingDescriptor) is true, return false.
459+
* ii. If existingDescriptor.[[Writable]] is false, return false.
460+
* iii. Let valueDesc be the PropertyDescriptor { [[Value]]: V }.
461+
* iv. Return ? Receiver.[[DefineOwnProperty]](P, valueDesc).
462+
* e. Else,
463+
* i. Assert: Receiver does not currently have a property P.
464+
* ii. Return ? CreateDataProperty(Receiver, P, V).
465+
* 4. Assert: IsAccessorDescriptor(ownDesc) is true.
466+
* 5. Let setter be ownDesc.[[Set]].
467+
* 6. If setter is undefined, return false.
468+
* 7. Perform ? Call(setter, Receiver, « V »).
469+
* 8. Return true.
470+
*/
471+
private static boolean internalSet(Context cx, ScriptableObject target, Object propertyKey,
472+
Object value, Object receiver) {
473+
try {
474+
DescriptorInfo ownDesc = target.getOwnPropertyDescriptor(cx, propertyKey);
475+
if (ownDesc == null) {
476+
final Scriptable parent = target.getPrototype();
477+
if (parent != null) {
478+
return internalSet(cx, ScriptableObject.ensureScriptableObject(parent), propertyKey, value, receiver);
479+
}
480+
ownDesc = new DescriptorInfo(true, true, true, Undefined.instance);
481+
}
482+
483+
if (ownDesc.isDataDescriptor()) {
484+
if (ownDesc.isWritable(false)) {
485+
return false;
486+
}
487+
if (!ScriptRuntime.isObject(receiver)) {
488+
return false;
404489
}
405490

406-
if (descriptor.isConfigurable(false)) {
491+
final ScriptableObject receiverObj = ScriptableObject.ensureScriptableObject(receiver);
492+
final DescriptorInfo existingDescriptor = receiverObj.getOwnPropertyDescriptor(cx, propertyKey);
493+
if (existingDescriptor != null) {
494+
if (existingDescriptor.isAccessorDescriptor()) {
495+
return false;
496+
}
497+
if (existingDescriptor.isWritable(false)) {
498+
return false;
499+
}
500+
} else if (!receiverObj.isExtensible()) {
407501
return false;
408502
}
503+
504+
// If receiver is a proxy, set property directly on the proxy's target
505+
// to avoid recursion (reflect <-> proxy)
506+
final ScriptableObject realReceiverObj = receiverObj instanceof NativeProxy
507+
? ((NativeProxy) receiverObj).getTargetThrowIfRevoked()
508+
: receiverObj;
509+
510+
if (ScriptRuntime.isSymbol(propertyKey)) {
511+
realReceiverObj.put((Symbol) propertyKey, realReceiverObj, value);
512+
} else {
513+
final StringIdOrIndex s = ScriptRuntime.toStringIdOrIndex(propertyKey);
514+
if (s.stringId == null) {
515+
realReceiverObj.put(s.index, realReceiverObj, value);
516+
} else {
517+
realReceiverObj.put(s.stringId, realReceiverObj, value);
518+
}
519+
}
520+
521+
return true;
409522
}
410-
}
411523

412-
if (ScriptRuntime.isSymbol(args[1])) {
413-
receiver.put((Symbol) args[1], receiver, args[2]);
414-
} else {
415-
StringIdOrIndex s = ScriptRuntime.toStringIdOrIndex(args[1]);
416-
if (s.stringId == null) {
417-
receiver.put(s.index, receiver, args[2]);
418-
} else {
419-
receiver.put(s.stringId, receiver, args[2]);
524+
if (ownDesc.isAccessorDescriptor()) {
525+
final Object setter = ownDesc.setter;
526+
if (setter == null
527+
|| setter == Scriptable.NOT_FOUND
528+
|| Undefined.isUndefined(setter)) {
529+
return false;
530+
}
531+
final Scriptable receiverForCall;
532+
if (receiver == null || Undefined.isUndefined(receiver)) {
533+
receiverForCall = cx.isStrictMode()
534+
? null
535+
: ScriptableObject.getTopLevelScope(target);
536+
} else {
537+
receiverForCall = ScriptableObject.ensureScriptable(receiver);
538+
}
539+
540+
((Function) setter).call(cx, target, receiverForCall, new Object[] {value});
420541
}
421-
}
422542

423-
return true;
543+
return true;
544+
545+
} catch (EcmaError e) {
546+
return false;
547+
}
424548
}
425549

426550
private static Object setPrototypeOf(

tests/src/test/java/org/mozilla/javascript/tests/es6/NativeReflectTest.java

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,40 @@ public void getWithReceiver() {
496496
Utils.assertWithAllModes_ES6("42 hello 10", js);
497497
}
498498

499+
@Test
500+
public void setWithReceiver() {
501+
String js =
502+
// accessor: receiver is used as 'this' in setter
503+
"var target = {};\n"
504+
+ "Object.defineProperty(target, 'x', {\n"
505+
+ " set: function(v) { this.result = v; },\n"
506+
+ " get: function() { return this.result; }\n"
507+
+ "});\n"
508+
+ "var receiver = {};\n"
509+
+ "Reflect.set(target, 'x', 42, receiver);\n"
510+
+ "var accessorResult = receiver.result + ' ' + (target.result === undefined);\n"
511+
// non-writable target: returns false
512+
+ "var nonWritableTarget = {};\n"
513+
+ "Object.defineProperty(nonWritableTarget, 'x',"
514+
+ " { value: 1, writable: false, configurable: true });\n"
515+
+ "var nonWritableResult = Reflect.set(nonWritableTarget, 'x', 2);\n"
516+
// receiver constraints: accessor, non-writable, non-extensible all return false
517+
+ "var accessorReceiver = {};\n"
518+
+ "Object.defineProperty(accessorReceiver, 'x',"
519+
+ " { get: function() {}, set: function() {} });\n"
520+
+ "var readonlyReceiver = {};\n"
521+
+ "Object.defineProperty(readonlyReceiver, 'x',"
522+
+ " { value: 99, writable: false });\n"
523+
+ "var sealedReceiver = {}; Object.preventExtensions(sealedReceiver);\n"
524+
+ "var receiverResults = Reflect.set({ x: 1 }, 'x', 2, accessorReceiver)"
525+
+ " + ' ' + Reflect.set({ x: 1 }, 'x', 2, readonlyReceiver)"
526+
+ " + ' ' + Reflect.set(Object.create({ x: 1 }), 'x', 2, sealedReceiver);\n"
527+
+ "accessorResult"
528+
+ " + ' ' + nonWritableResult"
529+
+ " + ' ' + receiverResults";
530+
Utils.assertWithAllModes_ES6("42 true false false false false", js);
531+
}
532+
499533
@Test
500534
public void getWithProxyTarget() {
501535
String js =
@@ -519,6 +553,33 @@ public void getWithProxyTarget() {
519553
Utils.assertWithAllModes_ES6("trapped:x threw", js);
520554
}
521555

556+
@Test
557+
public void setWithProxyTarget() {
558+
String js =
559+
"var trapLog = '';\n"
560+
+ "var target = {};\n"
561+
+ "var proxy = new Proxy(target, {\n"
562+
+ " set: function(target, prop, value, receiver) {\n"
563+
+ " trapLog = prop + '=' + value;\n"
564+
+ " target[prop] = value;\n"
565+
+ " return true;\n"
566+
+ " }\n"
567+
+ "});\n"
568+
+ "var trapResult = Reflect.set(proxy, 'x', 42);\n"
569+
+ "var frozenTarget = {};\n"
570+
+ "Object.defineProperty(frozenTarget, 'x',"
571+
+ " { value: 42, writable: false, configurable: false });\n"
572+
+ "var nonConfigurableProxy = new Proxy(frozenTarget, {\n"
573+
+ " set: function(target, prop, value, receiver) { return true; }\n"
574+
+ "});\n"
575+
+ "var nonConfigurableResult;\n"
576+
+ "try { Reflect.set(nonConfigurableProxy, 'x', 99); nonConfigurableResult = 'no error'; }"
577+
+ " catch (e) { nonConfigurableResult = 'threw'; }\n"
578+
+ "trapResult + ' ' + trapLog + ' ' + target.x"
579+
+ " + ' ' + nonConfigurableResult";
580+
Utils.assertWithAllModes_ES6("true x=42 42 threw", js);
581+
}
582+
522583
@Test
523584
public void proxyTrapForwardsViaReflect() {
524585
String js =
@@ -531,11 +592,18 @@ public void proxyTrapForwardsViaReflect() {
531592
+ " get: function(target, key, receiver) {\n"
532593
+ " accessLog.push('get:' + key);\n"
533594
+ " return Reflect.get(target, key, receiver);\n"
595+
+ " },\n"
596+
+ " set: function(target, key, value, receiver) {\n"
597+
+ " accessLog.push('set:' + key);\n"
598+
+ " return Reflect.set(target, key, value, receiver);\n"
534599
+ " }\n"
535600
+ "});\n"
536601
+ "var getResult = proxy.x;\n"
537602
+ "var accessorResult = Reflect.get(proxy, 'context', { id: 'custom' });\n"
538-
+ "getResult + ' ' + accessorResult + ' | ' + accessLog";
539-
Utils.assertWithAllModes_ES6("hello context-custom | get:x,get:context", js);
603+
+ "proxy.y = 99;\n"
604+
+ "var setResult = target.y;\n"
605+
+ "getResult + ' ' + accessorResult + ' ' + setResult"
606+
+ " + ' | ' + accessLog";
607+
Utils.assertWithAllModes_ES6("hello context-custom 99 | get:x,get:context,set:y", js);
540608
}
541609
}

0 commit comments

Comments
 (0)