Skip to content

Commit f0bd4e8

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

2 files changed

Lines changed: 152 additions & 12 deletions

File tree

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

Lines changed: 91 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import java.util.ArrayList;
1010
import java.util.List;
11+
import java.util.Objects;
1112
import org.mozilla.javascript.ScriptRuntime.StringIdOrIndex;
1213

1314
/**
@@ -214,23 +215,101 @@ private static Object deleteProperty(
214215
return false;
215216
}
216217

218+
/*
219+
* https://tc39.es/ecma262/#sec-reflect.get
220+
* 1. If target is not an Object, throw a TypeError exception.
221+
* 2. Let key be ? ToPropertyKey(propertyKey).
222+
* 3. If receiver is not present, then
223+
* a. Set receiver to target.
224+
* 4. Return ? target.[[Get]](key, receiver).
225+
*/
217226
private static Object get(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
218-
ScriptableObject target = checkTarget(args);
219-
220-
if (args.length > 1) {
221-
if (ScriptRuntime.isSymbol(args[1])) {
222-
Object prop = ScriptableObject.getProperty(target, (Symbol) args[1]);
223-
return prop == Scriptable.NOT_FOUND ? Undefined.SCRIPTABLE_UNDEFINED : prop;
227+
final ScriptableObject target = checkTarget(args);
228+
final Object propertyKey = args.length > 1 ? args[1] : Undefined.instance;
229+
final Object receiver = args.length > 2 ? args[2] : target;
230+
231+
// If target is a proxy, delegate to the proxy handler
232+
if (target instanceof NativeProxy) {
233+
final NativeProxy proxy = (NativeProxy) target;
234+
final Function trap = proxy.getTrap("get");
235+
if (trap != null) {
236+
final ScriptableObject proxyTarget = proxy.getTargetThrowIfRevoked();
237+
final Object[] trapArgs = {proxyTarget, propertyKey, receiver};
238+
final Object trapResult = proxy.callTrap(trap, trapArgs);
239+
240+
// checks for non-configurable properties
241+
// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-get-p-receiver steps 9
242+
final DescriptorInfo targetDesc = proxyTarget.getOwnPropertyDescriptor(cx, propertyKey);
243+
if (targetDesc != null && targetDesc.isConfigurable(false)) {
244+
if (targetDesc.isDataDescriptor() && targetDesc.isWritable(false)) {
245+
if (!Objects.equals(trapResult, targetDesc.value)) {
246+
throw ScriptRuntime.typeError(
247+
"proxy must report the same value for the non-writable,"
248+
+ " non-configurable property '\"" + propertyKey + "\"'");
249+
}
250+
}
251+
if (targetDesc.isAccessorDescriptor()
252+
&& (targetDesc.getter == null
253+
|| targetDesc.getter == Scriptable.NOT_FOUND
254+
|| Undefined.isUndefined(targetDesc.getter))) {
255+
if (!Undefined.isUndefined(trapResult)) {
256+
throw ScriptRuntime.typeError(
257+
"proxy must report the same value for the non-writable,"
258+
+ " non-configurable property '\"" + propertyKey + "\"'");
259+
}
260+
}
261+
}
262+
return trapResult;
224263
}
225-
if (args[1] instanceof Number) {
226-
Object prop = ScriptableObject.getProperty(target, ScriptRuntime.toIndex(args[1]));
227-
return prop == Scriptable.NOT_FOUND ? Undefined.SCRIPTABLE_UNDEFINED : prop;
264+
}
265+
266+
return internalGet(cx, target, propertyKey, receiver);
267+
}
268+
269+
/*
270+
* https://tc39.es/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots-get-p-receiver
271+
* 1. Let desc be ? O.[[GetOwnProperty]](P).
272+
* 2. If desc is undefined, then
273+
* a. Let parent be ? O.[[GetPrototypeOf]]().
274+
* b. If parent is null, return undefined.
275+
* c. Return ? parent.[[Get]](P, Receiver).
276+
* 3. If IsDataDescriptor(desc) is true, return desc.[[Value]].
277+
* 4. Assert: IsAccessorDescriptor(desc) is true.
278+
* 5. Let getter be desc.[[Get]].
279+
* 6. If getter is undefined, return undefined.
280+
* 7. Return ? Call(getter, Receiver).
281+
*/
282+
private static Object internalGet(
283+
Context cx, ScriptableObject target, Object propertyKey, Object receiver) {
284+
final DescriptorInfo desc = target.getOwnPropertyDescriptor(cx, propertyKey);
285+
if (desc == null) {
286+
final Scriptable parent = target.getPrototype();
287+
if (parent == null) {
288+
return Undefined.SCRIPTABLE_UNDEFINED;
228289
}
290+
return internalGet(cx, ScriptableObject.ensureScriptableObject(parent), propertyKey, receiver);
291+
}
229292

230-
Object prop = ScriptableObject.getProperty(target, ScriptRuntime.toString(args[1]));
231-
return prop == Scriptable.NOT_FOUND ? Undefined.SCRIPTABLE_UNDEFINED : prop;
293+
if (desc.isDataDescriptor()) {
294+
return desc.value == Scriptable.NOT_FOUND
295+
? Undefined.SCRIPTABLE_UNDEFINED
296+
: desc.value;
232297
}
233-
return Undefined.SCRIPTABLE_UNDEFINED;
298+
299+
final Object getter = desc.getter;
300+
if (getter == null || getter == Scriptable.NOT_FOUND || Undefined.isUndefined(getter)) {
301+
return Undefined.SCRIPTABLE_UNDEFINED;
302+
}
303+
304+
final Scriptable receiverForCall;
305+
if (receiver == null || Undefined.isUndefined(receiver)) {
306+
receiverForCall = cx.isStrictMode()
307+
? null
308+
: ScriptableObject.getTopLevelScope(target);
309+
} else {
310+
receiverForCall = ScriptableObject.ensureScriptable(receiver);
311+
}
312+
return ((Function) getter).call(cx, target, receiverForCall, ScriptRuntime.emptyArgs);
234313
}
235314

236315
private static Scriptable getOwnPropertyDescriptor(

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

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,4 +477,65 @@ public void constructSubclassBuiltin() {
477477
+ " + ' ' + (Object.getPrototypeOf(set) === CustomSet.prototype)";
478478
Utils.assertWithAllModes_ES6("1,2,3,4 true true true", js);
479479
}
480+
481+
@Test
482+
public void getWithReceiver() {
483+
String js =
484+
// accessor: receiver is used as 'this' in getter
485+
"var target = {};\n"
486+
+ "Object.defineProperty(target, 'x', {\n"
487+
+ " get: function() { return this.value; }\n"
488+
+ "});\n"
489+
+ "var accessorResult = Reflect.get(target, 'x', { value: 42 });\n"
490+
// inherited accessor: receiver is still used as 'this'
491+
+ "var child = Object.create(target);\n"
492+
+ "var protoResult = Reflect.get(child, 'x', { value: 'hello' });\n"
493+
// data property: receiver is ignored
494+
+ "var dataResult = Reflect.get({ x: 10 }, 'x', { x: 99 });\n"
495+
+ "accessorResult + ' ' + protoResult + ' ' + dataResult";
496+
Utils.assertWithAllModes_ES6("42 hello 10", js);
497+
}
498+
499+
@Test
500+
public void getWithProxyTarget() {
501+
String js =
502+
"var target = { x: 1 };\n"
503+
+ "var proxy = new Proxy(target, {\n"
504+
+ " get: function(target, prop, receiver) {\n"
505+
+ " return 'trapped:' + prop;\n"
506+
+ " }\n"
507+
+ "});\n"
508+
+ "var trapResult = Reflect.get(proxy, 'x');\n"
509+
+ "var frozenTarget = {};\n"
510+
+ "Object.defineProperty(frozenTarget, 'x',"
511+
+ " { value: 42, writable: false, configurable: false });\n"
512+
+ "var nonConfigurableProxy = new Proxy(frozenTarget, {\n"
513+
+ " get: function(target, prop, receiver) { return 'wrong'; }\n"
514+
+ "});\n"
515+
+ "var nonConfigurableResult;\n"
516+
+ "try { Reflect.get(nonConfigurableProxy, 'x'); nonConfigurableResult = 'no error'; }"
517+
+ " catch (e) { nonConfigurableResult = 'threw'; }\n"
518+
+ "trapResult + ' ' + nonConfigurableResult";
519+
Utils.assertWithAllModes_ES6("trapped:x threw", js);
520+
}
521+
522+
@Test
523+
public void proxyTrapForwardsViaReflect() {
524+
String js =
525+
"var target = { x: 'hello' };\n"
526+
+ "Object.defineProperty(target, 'context', {\n"
527+
+ " get: function() { return 'context-' + (this.id || 'default'); }\n"
528+
+ "});\n"
529+
+ "var accessLog = [];\n"
530+
+ "var proxy = new Proxy(target, {\n"
531+
+ " get: function(target, key, receiver) {\n"
532+
+ " accessLog.push('get:' + key);\n"
533+
+ " return Reflect.get(target, key, receiver);\n"
534+
+ " }\n"
535+
+ "});\n"
536+
+ "var getResult = proxy.x;\n"
537+
+ "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);
540+
}
480541
}

0 commit comments

Comments
 (0)