Skip to content

Commit da44309

Browse files
committed
NativeReflect: re-implement Reflect.set()
1 parent f0bd4e8 commit da44309

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
@@ -387,41 +387,165 @@ private static Object preventExtensions(
387387
return target.preventExtensions();
388388
}
389389

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

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

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

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

424-
return true;
544+
return true;
545+
546+
} catch (EcmaError e) {
547+
return false;
548+
}
425549
}
426550

427551
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)