adding new type of Slot called OwnerAwareLambdaSlot, that allows defining getter / setter properties using Lambda functions that accept owner object ('this') as one of parameters, thus allowing to implement properties that require access to instance fields etc.

This commit is contained in:
nabacg 2024-08-07 12:05:03 +01:00 коммит произвёл Greg Brail
Родитель f63941c7e4
Коммит 6e4fe32c59
4 изменённых файлов: 461 добавлений и 0 удалений

Просмотреть файл

@ -6,6 +6,9 @@
package org.mozilla.javascript;
import java.util.function.BiConsumer;
import java.util.function.Function;
/**
* This class implements a JavaScript function that may be used as a constructor by delegating to an
* interface that can be easily implemented as a lambda. The LambdaFunction class may be used to add
@ -120,6 +123,17 @@ public class LambdaConstructor extends LambdaFunction {
proto.defineProperty(key, value, attributes);
}
public void definePrototypeProperty(String name, java.util.function.Function<Scriptable, Object> getter, int attributes) {
ScriptableObject proto = getPrototypeScriptable();
proto.defineProperty(name, getter, null, attributes );
}
public void definePrototypeProperty(String name, Function<Scriptable, Object> getter, BiConsumer<Scriptable, Object> setter, int attributes) {
ScriptableObject proto = getPrototypeScriptable();
proto.defineProperty(name, getter, setter, attributes );
}
/**
* Define a function property directly on the constructor that is implemented under the covers
* by a LambdaFunction.

Просмотреть файл

@ -0,0 +1,88 @@
package org.mozilla.javascript;
import java.util.IdentityHashMap;
import java.util.function.BiConsumer;
import java.util.function.Function;
/**
* A specialized property accessor using lambda functions, similar to {@link LambdaSlot},
* but allows defining properties with getter and setter lambdas that require access to the owner object ('this').
* This enables the implementation of properties that can access instance fields of the owner.
*
* Unlike {@link LambdaSlot}, Lambda functions used to define getter and setter logic require the owner's
* `Scriptable` object as one of the parameters.
* This is particularly useful for implementing properties that behave like standard JavaScript properties,
* but are implemented with native functionality without the need for reflection.
*/
public class OwnerAwareLambdaSlot extends Slot {
private transient Function<Scriptable, Object> getter;
private transient BiConsumer<Scriptable, Object> setter;
OwnerAwareLambdaSlot(Object name, int index) {
super(name, index, 0);
}
OwnerAwareLambdaSlot(Slot oldSlot) {
super(oldSlot);
}
@Override
boolean isValueSlot() {
return false;
}
@Override
ScriptableObject getPropertyDescriptor(Context cx, Scriptable scope) {
ScriptableObject desc = (ScriptableObject) cx.newObject(scope);
if (getter != null) {
desc.defineProperty("get",
new LambdaFunction(scope, "get " + super.name, 0, (cx1, scope1, thisObj, args) ->
getter.apply(thisObj)), ScriptableObject.EMPTY);
}
if (setter != null) {
desc.defineProperty("set",
new LambdaFunction(scope, "set " + super.name, 1, (cx1, scope1, thisObj, args) ->
{
setter.accept(thisObj, args[0]);
return Undefined.instance;
}), ScriptableObject.EMPTY);
}
desc.setCommonDescriptorProperties(getAttributes(), false);
return desc;
}
@Override
public boolean setValue(Object value, Scriptable scope, Scriptable owner, boolean isStrict) {
if (setter != null) {
setter.accept(owner, value);
return true;
}
if (isStrict) {
// in strict mode
throw ScriptRuntime.typeErrorById("msg.modify.readonly", name);
} else {
super.setValue(value, scope, owner, false);
}
return true;
}
@Override
public Object getValue(Scriptable owner) {
if (getter != null) {
return getter.apply(owner);
}
Object v = super.getValue(owner);
return v == null ? Undefined.instance : v;
}
public void setGetter(Function<Scriptable, Object> getter) {
this.getter = getter;
}
public void setSetter(BiConsumer<Scriptable, Object> setter) {
this.setter = setter;
}
}

Просмотреть файл

@ -28,6 +28,7 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.mozilla.javascript.ScriptRuntime.StringIdOrIndex;
@ -1690,6 +1691,35 @@ public abstract class ScriptableObject
slot.setter = setter;
}
/**
* Define a property on this object that is implemented using lambda functions accepting Scriptable `this` object
* as first parameter. Unlike with `defineProperty(String name, Supplier<Object> getter, Consumer<Object> setter, int attributes)`
* where getter and setter need to have access to target object instance, this allows for defining properties on
* LambdaConstructor prototype providing getter and setter logic with java instance methods.
* If a property with the same name already exists, then it will be replaced. This property will appear to the
* JavaScript user exactly like any other property -- unlike Function properties and those based
* on reflection, the property descriptor will only reflect the value as defined by this
* function.
*
* @param name the name of the property
* @param getter a function that given Scriptable `this` returns the value of the property. If null, throws typeError
* @param setter a function that Scriptable `this` and a value sets the value of the property, by calling appropriate
* method on `this`. If null, then the value will be
* set directly and may not be retrieved by the getter.
* @param attributes the attributes to set on the property
*/
public void defineProperty(
String name, java.util.function.Function<Scriptable, Object> getter, BiConsumer<Scriptable, Object> setter, int attributes) {
if (getter == null && setter == null)
throw ScriptRuntime.typeError("at least one of {getter, setter} is required");
OwnerAwareLambdaSlot slot = slotMap.compute(name, 0, ScriptableObject::ensureOwnerAwareLambdaSlot);
slot.setAttributes(attributes);
slot.setGetter(getter);
slot.setSetter(setter);
}
protected void checkPropertyDefinition(ScriptableObject desc) {
Object getter = getProperty(desc, "get");
if (getter != NOT_FOUND && getter != Undefined.instance && !(getter instanceof Callable)) {
@ -2695,6 +2725,16 @@ public abstract class ScriptableObject
}
}
private static OwnerAwareLambdaSlot ensureOwnerAwareLambdaSlot(Object name, int index, Slot existing) {
if (existing == null) {
return new OwnerAwareLambdaSlot(name, index);
} else if (existing instanceof OwnerAwareLambdaSlot) {
return (OwnerAwareLambdaSlot) existing;
} else {
return new OwnerAwareLambdaSlot(existing);
}
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
final long stamp = slotMap.readLock();

Просмотреть файл

@ -0,0 +1,319 @@
package org.mozilla.javascript.tests;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.EcmaError;
import org.mozilla.javascript.LambdaConstructor;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.Undefined;
import static org.junit.Assert.*;
import static org.mozilla.javascript.ScriptableObject.DONTENUM;
import static org.mozilla.javascript.tests.OwnerAwareLambdaSlotTest.StatusHolder.self;
public class OwnerAwareLambdaSlotTest {
private Context cx;
private ScriptableObject scope;
@Before
public void setUp() throws Exception {
cx = Context.enter();
scope = cx.initStandardObjects();
}
@After
public void tearDown() throws Exception {
Context.exit();
}
@Test
public void testGetterProperty() {
StatusHolder
.init(scope)
.definePrototypeProperty("status",
(thisObj) -> self(thisObj).getStatus(),
(thisObj, value) ->
self(thisObj).setStatus(value),
DONTENUM);
Object getterResult = cx.evaluateString(scope, "s = new StatusHolder('InProgress'); s.status", "source", 1, null);
assertEquals("InProgress", getterResult);
}
@Test
public void testThrowIfNeitherGetterOrSetterAreDefined() {
var error = assertThrows(EcmaError.class, () ->
StatusHolder
.init(scope)
.definePrototypeProperty("status",
null,
null,
DONTENUM));
assertTrue(error.toString().contains("at least one of {getter, setter} is required"));
}
@Test
public void testCanUpdateValueUsingSetter() {
StatusHolder
.init(scope)
.definePrototypeProperty("status",
(thisObj) -> self(thisObj).getStatus(),
(thisObj, value) ->
self(thisObj).setStatus(value),
DONTENUM);
Object getterResult = cx.evaluateString(scope, "s = new StatusHolder('InProgress'); s.status", "source", 1, null);
assertEquals("InProgress", getterResult);
Object setResult = cx.evaluateString(scope, "s.status = 'DONE';", "source", 1, null);
Object newStatus = cx.evaluateString(scope, "s.status", "source", 1, null);
assertEquals( "NewStatus: DONE", newStatus);
}
@Test
public void testOnlyGetterCanBeAccessed() {
StatusHolder
.init(scope)
.definePrototypeProperty("status", (thisObj) -> self(thisObj).getStatus(), DONTENUM);
Object getterResult = cx.evaluateString(scope, "new StatusHolder('OK').status", "source", 1, null);
assertEquals("OK", getterResult);
Object hiddenFieldResult = cx.evaluateString(scope, "new StatusHolder('OK').hiddenStatus", "source", 1, null);
assertEquals("fields not explicitly defined as properties should return undefined", Undefined.instance, hiddenFieldResult);
}
@Test
public void testWhenNoSetterDefined_InStrictMode_WillThrowException() {
StatusHolder
.init(scope)
.definePrototypeProperty("status",
(thisObj) -> self(thisObj).getStatus(),
DONTENUM);
Object getterResult = cx.evaluateString(scope, "s = new StatusHolder('Constant'); s.status", "source", 1, null);
assertEquals("Constant", getterResult);
var error = assertThrows(EcmaError.class, () ->
cx.evaluateString(scope,
"\"use strict\"; s.status = 'DONE'; s.status", "source", 1, null));
assertTrue(error.toString().contains("Cannot modify readonly property: status."));
}
@Test
public void testWhenNoSetterDefined_InNormalMode_NoErrorButValueIsNotChanged() {
StatusHolder
.init(scope)
.definePrototypeProperty("status",
(thisObj) -> self(thisObj).getStatus(),
DONTENUM);
Object getterResult = cx.evaluateString(scope, "s = new StatusHolder('Constant'); s.status", "source", 1, null);
assertEquals("Constant", getterResult);
Object setResult = cx.evaluateString(scope, "s.status = 'DONE'; s.status", "source", 1, null);
assertEquals("status won't be changed", "Constant", setResult);
Object shObj = cx.evaluateString(scope, "s", "source", 1, null);
var statusHolder = (StatusHolder) shObj;
assertEquals("Constant", statusHolder.getStatus());
}
@Test
public void testSetterOnly_WillModifyUnderlyingValue() {
StatusHolder
.init(scope)
.definePrototypeProperty("status",
null,
(thisObj, value) ->
self(thisObj).setStatus(value),
DONTENUM);
cx.evaluateString(scope, "s = new StatusHolder('Constant')", "source", 1, null);
cx.evaluateString(scope, "s.status = 'DONE'; s.status", "source", 1, null);
Object newStatus = cx.evaluateString(scope, "s.status", "source", 1, null);
assertEquals( Undefined.instance, newStatus);
Object shObj = cx.evaluateString(scope, "s", "source", 1, null);
var statusHolder = (StatusHolder) shObj;
assertEquals("NewStatus: DONE", statusHolder.getStatus());
}
// using getOwnPropertyDescriptor to access property
@Test
public void testGetterUsing_getOwnPropertyDescriptor() {
StatusHolder
.init(scope)
.definePrototypeProperty("status",
(thisObj) -> self(thisObj).getStatus(), DONTENUM);
Object result = cx.evaluateString(scope, "s = new StatusHolder('InProgress');" +
"f = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(s), 'status');" +
"f.get.call(s)",
"source", 1, null);
assertEquals("InProgress", result);
}
@Test
public void testSetterOnlyUsing_getOwnPropertyDescriptor() {
StatusHolder
.init(scope)
.definePrototypeProperty("status",
null,
(thisObj, value) ->
self(thisObj).setStatus(value), DONTENUM);
Object shObj = cx.evaluateString(scope,
"s = new StatusHolder('InProgress');" +
"f = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(s), 'status');" +
"f.set.call(s, 'DONE');" +
"s",
"source", 1, null);
var statusHolder = (StatusHolder) shObj;
assertEquals("NewStatus: DONE", statusHolder.getStatus());
}
@Test
public void testSetValueUsing_getOwnPropertyDescriptor() {
StatusHolder
.init(scope)
.definePrototypeProperty("status",
(thisObj) -> self(thisObj).getStatus(),
(thisObj, value) ->
self(thisObj).setStatus(value), DONTENUM);
Object result = cx.evaluateString(scope,
"s = new StatusHolder('InProgress');" +
"f = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(s), 'status');" +
"f.set.call(s, 'DONE');" +
"s.status",
"source", 1, null);
assertEquals("Status with prefix", "NewStatus: DONE", result);
}
@Test
public void testSetterOnlyUsing_getOwnPropertyDescriptor_ErrorOnGet() {
StatusHolder
.init(scope)
.definePrototypeProperty("status",
null,
(thisObj, value) ->
self(thisObj).setStatus(value), DONTENUM);
var error = assertThrows(EcmaError.class, () ->
cx.evaluateString(scope,
"var s = new StatusHolder('InProgress');" +
"var f = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(s), 'status');" +
"f.get.call(s)",
"source", 1, null));
assertTrue(error.toString().contains("Cannot call method \"call\" of undefined"));
}
@Test
public void testSetterOnlyUsing_getOwnPropertyDescriptor_InStrictMode_ErrorOnGet() {
StatusHolder
.init(scope)
.definePrototypeProperty("status",
null,
(thisObj, value) ->
self(thisObj).setStatus(value), DONTENUM);
var error = assertThrows(EcmaError.class, () ->
cx.evaluateString(scope,
"\"use strict\";"+
"var s = new StatusHolder('InProgress');" +
"var f = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(s), 'status');" +
"f.get.call(s)",
"source", 1, null));
assertTrue(error.toString().contains("Cannot call method \"call\" of undefined"));
}
@Test
public void testGetterOnlyUsing_getOwnPropertyDescriptor_ErrorOnSet() {
StatusHolder
.init(scope)
.definePrototypeProperty("status",
(thisObj) -> self(thisObj).getStatus(), DONTENUM);
var error = assertThrows(EcmaError.class, () ->
cx.evaluateString(scope,
"var s = new StatusHolder('InProgress');" +
"var f = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(s), 'status');" +
"f.set.call(s, 'DONE');" +
"s.status",
"source", 1, null));
assertTrue(error.toString().contains("Cannot call method \"call\" of undefined"));
}
@Test
public void testGetterOnlyUsing_getOwnPropertyDescriptor_InStrictMode_ErrorOnSet() {
StatusHolder
.init(scope)
.definePrototypeProperty("status",
(thisObj) -> self(thisObj).getStatus(), DONTENUM);
var error = assertThrows(EcmaError.class, () ->
cx.evaluateString(scope,
"\"use strict\";" +
"var s = new StatusHolder('InProgress');" +
"var f = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(s), 'status');" +
"f.set.call(s, 'DONE');" +
"s.status",
"source", 1, null));
assertTrue(error.toString().contains("Cannot call method \"call\" of undefined"));
}
static class StatusHolder extends ScriptableObject {
private String status;
private final String hiddenStatus;
static LambdaConstructor init(Scriptable scope) {
LambdaConstructor constructor = new LambdaConstructor(scope, "StatusHolder", 1,
LambdaConstructor.CONSTRUCTOR_NEW,
(cx, scope1, args) -> new StatusHolder((String)args[0]));
ScriptableObject.defineProperty(scope, "StatusHolder", constructor, DONTENUM);
return constructor;
}
static StatusHolder self(Scriptable thisObj) {
return LambdaConstructor.convertThisObject(thisObj, StatusHolder.class);
}
StatusHolder(String status) {
this.status = status;
this.hiddenStatus = "NotQuiteReady";
}
public String getStatus() {
return status;
}
@Override
public String getClassName() {
return "StatusHolder";
}
public void setStatus(Object value) {
this.status = "NewStatus: "+ (String)value;
}
}
}