diff --git a/src/main/java/dev/latvian/mods/rhino/Context.java b/src/main/java/dev/latvian/mods/rhino/Context.java index fa669672..1f5276cd 100644 --- a/src/main/java/dev/latvian/mods/rhino/Context.java +++ b/src/main/java/dev/latvian/mods/rhino/Context.java @@ -18,6 +18,7 @@ import dev.latvian.mods.rhino.util.JavaSetWrapper; import dev.latvian.mods.rhino.util.TypeUtils; import dev.latvian.mods.rhino.util.wrap.TypeWrapperFactory; +import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.io.Reader; @@ -28,11 +29,15 @@ import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -982,7 +987,7 @@ public Object wrap(Scriptable scope, Object obj, Class staticType, Type gener Class cls = obj.getClass(); if (cls.isArray()) { - return NativeJavaArray.wrap(scope, obj, this); + return new NativeJavaArray(scope, obj, cls.getComponentType(), genericType instanceof GenericArrayType arr ? arr.getGenericComponentType() : cls.getComponentType(), this); } return wrapAsJavaObject(scope, obj, staticType, genericType); @@ -1163,7 +1168,7 @@ public Scriptable wrapNewObject(Scriptable scope, Object obj) { } Class cls = obj.getClass(); if (cls.isArray()) { - return NativeJavaArray.wrap(scope, obj, this); + return new NativeJavaArray(scope, obj, cls.getComponentType(), cls.getComponentType(), this); } return wrapAsJavaObject(scope, obj, null, null); } @@ -1203,6 +1208,134 @@ public int getMaximumInterpreterStackDepth() { return Integer.MAX_VALUE; } + protected ArrayValueProvider arrayValueProviderOf(Object value) { + if (value instanceof Object[] arr) { + return arr.length == 0 ? ArrayValueProvider.EMPTY : new ArrayValueProvider.FromPlainJavaArray(arr); + } else if (value != null && value.getClass().isArray()) { + int len = Array.getLength(value); + return len == 0 ? ArrayValueProvider.EMPTY : new ArrayValueProvider.FromJavaArray(value, len); + } + + return switch (value) { + case NativeArray array -> ArrayValueProvider.fromNativeArray(array); + case NativeJavaList list -> ArrayValueProvider.fromJavaList(list.list, list); + case List list -> ArrayValueProvider.fromJavaList(list, list); + case Iterable itr -> ArrayValueProvider.fromIterable(itr); + case null, default -> value == null ? ArrayValueProvider.FromObject.FROM_NULL : new ArrayValueProvider.FromObject(value); + }; + } + + protected Object arrayOf(@Nullable Object from, @Nullable Class target, @Nullable Type genericTarget) { + if (from instanceof Object[] arr) { + if (target == null) { + return from; + } + + return arr.length == 0 ? Array.newInstance(target, 0) : new ArrayValueProvider.FromPlainJavaArray(arr).createArray(this, target, genericTarget); + } else if (from != null && from.getClass().isArray()) { + if (target == null) { + return from; + } + + int len = Array.getLength(from); + return len == 0 ? Array.newInstance(target, 0) : new ArrayValueProvider.FromJavaArray(from, len).createArray(this, target, genericTarget); + } + + return arrayValueProviderOf(from).createArray(this, target, genericTarget); + } + + protected Object listOf(@Nullable Object from, @Nullable Class target, @Nullable Type genericTarget) { + if (from instanceof NativeJavaList n) { + if (target == null) { + // No conversion necessary + return n.list; + } else if (target == n.listType && Objects.equals(genericTarget, n.listGenericType)) { + // No conversion necessary + return n.list; + } else { + var list = new ArrayList<>(n.list.size()); + + for (var o : n.list) { + list.add(jsToJava(o, target, genericTarget)); + } + + return list; + } + } + + return arrayValueProviderOf(from).createList(this, target, genericTarget); + } + + protected Object setOf(@Nullable Object from, @Nullable Class target, @Nullable Type genericTarget) { + if (from instanceof NativeJavaList n) { + if (target == null) { + // No conversion necessary + return new LinkedHashSet<>(n.list); + } else if (target == n.listType && Objects.equals(genericTarget, n.listGenericType)) { + // No conversion necessary + return new LinkedHashSet<>(n.list); + } else { + var set = new LinkedHashSet<>(n.list.size()); + + for (var o : n.list) { + set.add(jsToJava(o, target, genericTarget)); + } + + return set; + } + } + + return arrayValueProviderOf(from).createSet(this, target, genericTarget); + } + + protected Object mapOf(@Nullable Object from, @Nullable Class kTarget, @Nullable Type kGenericTarget, @Nullable Class vTarget, @Nullable Type vGenericTarget) { + if (from instanceof NativeJavaMap n) { + if (kTarget == null && vTarget == null) { + // No conversion necessary + return n.map; + } else if (kTarget == n.mapKeyType && Objects.equals(kGenericTarget, n.mapKeyGenericType) && vTarget == n.mapValueType && Objects.equals(vGenericTarget, n.mapValueGenericType)) { + // No conversion necessary + return n.map; + } else { + if (n.map.isEmpty()) { + return Map.of(); + } + + var map = new LinkedHashMap<>(n.map.size()); + + for (var entry : ((Map) n.map).entrySet()) { + map.put(jsToJava(entry.getKey(), kTarget, kGenericTarget), jsToJava(entry.getValue(), vTarget, vGenericTarget)); + } + + return map; + } + } else if (from instanceof NativeObject obj) { + var keys = obj.getIds(this); + var map = new LinkedHashMap<>(keys.length); + + for (var key : keys) { + map.put(jsToJava(key, kTarget, kGenericTarget), jsToJava(obj.get(this, key), vTarget, vGenericTarget)); + } + + return map; + } else if (from instanceof Map m) { + if (kTarget == null && vTarget == null) { + // No conversion necessary + return m; + } + + var map = new LinkedHashMap<>(m.size()); + + for (var entry : m.entrySet()) { + map.put(jsToJava(entry.getKey(), kTarget, kGenericTarget), jsToJava(entry.getValue(), vTarget, vGenericTarget)); + } + + return map; + } else { + return reportConversionError(from, Map.class); + } + } + public Object createInterfaceAdapter(Class type, Type genericType, ScriptableObject so) { // XXX: Currently only instances of ScriptableObject are // supported since the resulting interface proxies should @@ -1232,54 +1365,65 @@ public Object javaToJS(Object value, Scriptable scope) { } } - public final Object jsToJava(Object from, Class target, Type genericTarget) throws EvaluatorException { + public final Object jsToJava(@Nullable Object from, @Nullable Class target, @Nullable Type genericTarget) throws EvaluatorException { if (target == null) { return from; } else if (target == Object.class) { return Wrapper.unwrapped(from); - } else if (target.isArray()) { - var desiredComponentType = target.componentType(); - var desiredComponentGenericType = TypeUtils.getComponentType(genericTarget, desiredComponentType); - - if (from != null && from.getClass() == target) { - return new ArrayValueProvider.FromJavaArray(from).createArray(this, desiredComponentType, desiredComponentGenericType); - } - - return ArrayValueProvider.of(from).createArray(this, desiredComponentType, desiredComponentGenericType); } else if (target == Set.class) { if (genericTarget instanceof ParameterizedType pt) { var types = pt.getActualTypeArguments(); var c = types.length == 1 ? TypeUtils.getRawType(types[0]) : Object.class; - if (c != Object.class) { - return ArrayValueProvider.of(from).createSet(this, c, types[0]); + if (c != null && c != Object.class) { + return setOf(from, c, types[0]); } } - return ArrayValueProvider.of(from).createSet(this, null, null); - } else if (List.class.isAssignableFrom(target)) { + return setOf(from, null, null); + } else if (target == Map.class) { + Class kType = null; + Type kGenericType = null; + Class vType = null; + Type vGenericType = null; + if (genericTarget instanceof ParameterizedType pt) { var types = pt.getActualTypeArguments(); - var c = types.length == 1 ? TypeUtils.getRawType(types[0]) : Object.class; + var kRaw = types.length == 2 ? TypeUtils.getRawType(types[0]) : Object.class; + var vRaw = types.length == 2 ? TypeUtils.getRawType(types[1]) : Object.class; + + if (kRaw != null && kRaw != Object.class) { + kType = kRaw; + kGenericType = types[0]; + } - if (c != Object.class) { - return ArrayValueProvider.of(from).createList(this, c, types[0]); + if (vRaw != null && vRaw != Object.class) { + vType = vRaw; + vGenericType = types[1]; } } - return ArrayValueProvider.of(from).createList(this, null, null); - } else if (target == Map.class) { + return mapOf(from, kType, kGenericType, vType, vGenericType); + } else if (target.isArray()) { + var desiredComponentType = target.componentType(); + var desiredComponentGenericType = TypeUtils.getComponentType(genericTarget, desiredComponentType); + + if (from != null && from.getClass() == target) { + return arrayOf(from, desiredComponentType, desiredComponentGenericType); + } + + return arrayOf(from, desiredComponentType, desiredComponentGenericType); + } else if (List.class.isAssignableFrom(target)) { if (genericTarget instanceof ParameterizedType pt) { var types = pt.getActualTypeArguments(); - var k = types.length == 2 ? TypeUtils.getRawType(types[0]) : Object.class; - var v = types.length == 2 ? TypeUtils.getRawType(types[1]) : Object.class; + var c = types.length == 1 ? TypeUtils.getRawType(types[0]) : Object.class; - if (k != Object.class || v != Object.class) { - return ArrayValueProvider.of(from).createSet(this, k, types[0]); + if (c != null && c != Object.class) { + return listOf(from, c, types[0]); } } - return ArrayValueProvider.of(from).createSet(this, null, null); + return listOf(from, null, null); } return internalJsToJava(from, target, genericTarget); diff --git a/src/main/java/dev/latvian/mods/rhino/NativeJavaArray.java b/src/main/java/dev/latvian/mods/rhino/NativeJavaArray.java index 0d62d13d..4c9f4235 100644 --- a/src/main/java/dev/latvian/mods/rhino/NativeJavaArray.java +++ b/src/main/java/dev/latvian/mods/rhino/NativeJavaArray.java @@ -20,25 +20,17 @@ */ public class NativeJavaArray extends NativeJavaObject implements SymbolScriptable { - public static NativeJavaArray wrap(Scriptable scope, Object array, Context cx) { - return new NativeJavaArray(scope, array, cx); - } - Object array; int length; Class componentType; Type genericComponentType; - public NativeJavaArray(Scriptable scope, Object array, Context cx) { + public NativeJavaArray(Scriptable scope, Object array, Class componentType, Type genericComponentType, Context cx) { super(scope, null, ScriptRuntime.ObjectClass, cx); - Class cl = array.getClass(); - if (!cl.isArray()) { - throw new RuntimeException("Array expected"); - } this.array = array; this.length = Array.getLength(array); - this.componentType = cl.getComponentType(); - this.genericComponentType = componentType; // fixme + this.componentType = componentType; + this.genericComponentType = genericComponentType; } @Override diff --git a/src/main/java/dev/latvian/mods/rhino/NativeJavaMap.java b/src/main/java/dev/latvian/mods/rhino/NativeJavaMap.java index c7d251fc..e6833206 100644 --- a/src/main/java/dev/latvian/mods/rhino/NativeJavaMap.java +++ b/src/main/java/dev/latvian/mods/rhino/NativeJavaMap.java @@ -15,21 +15,25 @@ @SuppressWarnings({"rawtypes", "unchecked"}) public class NativeJavaMap extends NativeJavaObject { - private final Map map; - private final Class mapValueType; - private final Type mapValueGenericType; + public final Map map; + public final Class mapKeyType; + public final Type mapKeyGenericType; + public final Class mapValueType; + public final Type mapValueGenericType; private final ValueUnwrapper valueUnwrapper; - public NativeJavaMap(Context cx, Scriptable scope, Object jo, Map map, Class mapValueType, ValueUnwrapper valueUnwrapper) { + public NativeJavaMap(Context cx, Scriptable scope, Object jo, Map map, Class mapKeyType, Type mapKeyGenericType, Class mapValueType, Type mapValueGenericType, ValueUnwrapper valueUnwrapper) { super(scope, jo, jo.getClass(), cx); this.map = map; + this.mapKeyType = mapKeyType; + this.mapKeyGenericType = mapKeyGenericType; this.mapValueType = mapValueType; - this.mapValueGenericType = mapValueType; + this.mapValueGenericType = mapValueGenericType; this.valueUnwrapper = valueUnwrapper; } public NativeJavaMap(Context cx, Scriptable scope, Object jo, Map map) { - this(cx, scope, jo, map, Object.class, ValueUnwrapper.DEFAULT); + this(cx, scope, jo, map, null, null, null, null, ValueUnwrapper.DEFAULT); } @Override diff --git a/src/main/java/dev/latvian/mods/rhino/util/ArrayValueProvider.java b/src/main/java/dev/latvian/mods/rhino/util/ArrayValueProvider.java index d342fef9..fa57a136 100644 --- a/src/main/java/dev/latvian/mods/rhino/util/ArrayValueProvider.java +++ b/src/main/java/dev/latvian/mods/rhino/util/ArrayValueProvider.java @@ -3,7 +3,6 @@ import dev.latvian.mods.rhino.Context; import dev.latvian.mods.rhino.EvaluatorException; import dev.latvian.mods.rhino.NativeArray; -import dev.latvian.mods.rhino.NativeJavaList; import java.lang.reflect.Array; import java.lang.reflect.Type; @@ -32,16 +31,6 @@ public Object getErrorSource(Context cx) { } }; - static ArrayValueProvider of(Object value) { - return switch (value) { - case NativeArray array -> fromNativeArray(array); - case NativeJavaList list -> fromJavaList(list.list, list); - case List list -> fromJavaList(list, list); - case Iterable itr -> fromIterable(itr); - case null, default -> value == null ? FromObject.FROM_NULL : new FromObject(value); - }; - } - int getLength(Context cx); Object getArrayValue(Context cx, int index); @@ -155,19 +144,32 @@ public Object getErrorSource(Context cx) { } } - static ArrayValueProvider fromJavaArray(Object array) { - return Array.getLength(array) == 0 ? EMPTY : new FromJavaArray(array); + record FromJavaArray(Object array, int length) implements ArrayValueProvider { + @Override + public int getLength(Context cx) { + return length; + } + + @Override + public Object getArrayValue(Context cx, int index) { + return Array.get(array, index); + } + + @Override + public Object getErrorSource(Context cx) { + return array; + } } - record FromJavaArray(Object array) implements ArrayValueProvider { + record FromPlainJavaArray(Object[] array) implements ArrayValueProvider { @Override public int getLength(Context cx) { - return Array.getLength(array); + return array.length; } @Override public Object getArrayValue(Context cx, int index) { - return Array.get(array, index); + return array[index]; } @Override diff --git a/src/main/java/dev/latvian/mods/rhino/util/TypeUtils.java b/src/main/java/dev/latvian/mods/rhino/util/TypeUtils.java index ef114695..6418d21c 100644 --- a/src/main/java/dev/latvian/mods/rhino/util/TypeUtils.java +++ b/src/main/java/dev/latvian/mods/rhino/util/TypeUtils.java @@ -10,15 +10,6 @@ public class TypeUtils { public static final Type[] NO_TYPES = new Type[0]; - public static Type[] getGenericTypes(Type type) { - return switch (type) { - case ParameterizedType paramType -> paramType.getActualTypeArguments(); - case GenericArrayType arrayType -> new Type[]{arrayType.getGenericComponentType()}; - case WildcardType wildcard -> new Type[]{getRawType(wildcard.getUpperBounds()[0])}; - case null, default -> NO_TYPES; - }; - } - public static Class getRawType(Type type) { if (type instanceof Class clz) { return clz; @@ -37,8 +28,7 @@ public static Class getRawType(Type type) { return getRawType(wildcard.getUpperBounds()[0]); } - var className = type == null ? "null" : type.getClass().getName(); - throw new IllegalArgumentException("Expected a Class, ParameterizedType, GenericArrayType, TypeVariable or WildcardType, but <" + type + "> is of type " + className); + return null; } public static Type getComponentType(Type type, Type fallback) { diff --git a/src/test/java/dev/latvian/mods/rhino/test/GenericTests.java b/src/test/java/dev/latvian/mods/rhino/test/GenericTests.java deleted file mode 100644 index 9e208f6d..00000000 --- a/src/test/java/dev/latvian/mods/rhino/test/GenericTests.java +++ /dev/null @@ -1,59 +0,0 @@ -package dev.latvian.mods.rhino.test; - -import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestMethodOrder; - -@SuppressWarnings("unused") -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) -public class GenericTests { - public static final RhinoTest TEST = new RhinoTest("generics"); - - @Test - @Order(1) - public void init() { - TEST.test("init", """ - shared.testObject = { - a: -39, b: 2, c: 3439438 - } - - shared.testList = console.testList - """, ""); - } - - @Test - public void arrayArg() { - TEST.test("arrayArg", "console.genericArrayArg(['a', 'b']);", "Generic array:\n[W[a], W[b]]"); - } - - @Test - public void arrayArgUnwrapped() { - TEST.test("arrayArgUnwrapped", "console.genericArrayArg('a');", "Generic array:\n[W[a]]"); - } - - @Test - public void arrayArgList() { - TEST.test("arrayArgList", "console.genericArrayArg(console.testList);", "Generic array:\n[W[abc], W[def], W[ghi]]"); - } - - @Test - public void listArg() { - TEST.test("listArg", "console.genericListArg(['a', 'b']);", "Generic list:\n[W[a], W[b]]"); - } - - @Test - public void listArgUnwrapped() { - TEST.test("listArgUnwrapped", "console.genericListArg('a');", "Generic list:\n[W[a]]"); - } - - @Test - public void listArgList() { - TEST.test("listArgList", "console.genericListArg(console.testList);", "Generic list:\n[W[abc], W[def], W[ghi]]"); - } - - @Test - public void mapArg() { - TEST.test("mapArg", "console.genericMapArg({'test': '10.5'});", "Generic map:\n{W[abc]: 10}"); - } -} diff --git a/src/test/java/dev/latvian/mods/rhino/test/GenericsTests.java b/src/test/java/dev/latvian/mods/rhino/test/GenericsTests.java new file mode 100644 index 00000000..fb131857 --- /dev/null +++ b/src/test/java/dev/latvian/mods/rhino/test/GenericsTests.java @@ -0,0 +1,79 @@ +package dev.latvian.mods.rhino.test; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +@SuppressWarnings("unused") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class GenericsTests { + public static final RhinoTest TEST = new RhinoTest("generics"); + + @Test + @Order(1) + public void init() { + TEST.test("init", """ + shared.testObject = { + a: -39, b: 2, c: 3439438 + } + + shared.testList = console.testList + """, ""); + } + + @Test + public void arrayArg() { + TEST.test("arrayArg", "console.genericsArrayArg(['a', 'b']);", "Generics array:\n[W[a], W[b]]"); + } + + @Test + public void arrayArgUnwrapped() { + TEST.test("arrayArgUnwrapped", "console.genericsArrayArg('a');", "Generics array:\n[W[a]]"); + } + + @Test + public void arrayArgList() { + TEST.test("arrayArgList", "console.genericsArrayArg(console.testList);", "Generics array:\n[W[abc], W[def], W[ghi]]"); + } + + @Test + public void listArg() { + TEST.test("listArg", "console.genericsListArg(['a', 'b']);", "Generics list:\n[W[a], W[b]]"); + } + + @Test + public void listArgUnwrapped() { + TEST.test("listArgUnwrapped", "console.genericsListArg('a');", "Generics list:\n[W[a]]"); + } + + @Test + public void listArgList() { + TEST.test("listArgList", "console.genericsListArg(console.testList);", "Generics list:\n[W[abc], W[def], W[ghi]]"); + } + + @Test + public void setArg() { + TEST.test("setArg", "console.genericsSetArg(['a', 'b']);", "Generics set:\n[W[a], W[b]]"); + } + + @Test + public void setArgUnwrapped() { + TEST.test("setArgUnwrapped", "console.genericsSetArg('a');", "Generics set:\n[W[a]]"); + } + + @Test + public void setArgList() { + TEST.test("setArgList", "console.genericsSetArg(console.testList);", "Generics set:\n[W[abc], W[def], W[ghi]]"); + } + + @Test + public void mapArg() { + TEST.test("mapArg", "console.genericsMapArg({'test': '10.5'});", "Generics map:\n{W[M[test]]: 10}"); + } + + @Test + public void mapArgMap() { + TEST.test("mapArgMap", "console.genericsMapArg(console.testMap);", "Generics map:\n{W[M[test]]: 10}"); + } +} diff --git a/src/test/java/dev/latvian/mods/rhino/test/TestConsole.java b/src/test/java/dev/latvian/mods/rhino/test/TestConsole.java index f0195fb7..d367aacf 100644 --- a/src/test/java/dev/latvian/mods/rhino/test/TestConsole.java +++ b/src/test/java/dev/latvian/mods/rhino/test/TestConsole.java @@ -70,6 +70,10 @@ public List getTestList() { return new ArrayList<>(Arrays.asList(getTestArray())); } + public Map getTestMap() { + return Map.of("test", "10.5"); + } + public void test1$setTheme(TestConsoleTheme t) { info("Set theme to " + t); theme = t; @@ -87,23 +91,23 @@ public void printMaterial(TestMaterial material) { info("%s#%08x".formatted(material.name(), material.hashCode())); } - public void genericArrayArg(WithContext[] arg) { - info("Generic array:"); + public void genericsArrayArg(WithContext[] arg) { + info("Generics array:"); info(arg); } - public void genericListArg(List> arg) { - info("Generic list:"); + public void genericsListArg(List> arg) { + info("Generics list:"); info(arg); } - public void genericSetArg(Set> arg) { - info("Generic set:"); + public void genericsSetArg(Set> arg) { + info("Generics set:"); info(arg); } - public void genericMapArg(Map, Integer> arg) { - info("Generic map:"); + public void genericsMapArg(Map, Integer> arg) { + info("Generics map:"); info(arg); } } diff --git a/src/test/java/dev/latvian/mods/rhino/test/TestMaterial.java b/src/test/java/dev/latvian/mods/rhino/test/TestMaterial.java index 2658b7e5..1d308d8f 100644 --- a/src/test/java/dev/latvian/mods/rhino/test/TestMaterial.java +++ b/src/test/java/dev/latvian/mods/rhino/test/TestMaterial.java @@ -9,4 +9,9 @@ public record TestMaterial(String name) { public static synchronized TestMaterial get(Object o) { return MATERIALS.computeIfAbsent(String.valueOf(o), TestMaterial::new); } + + @Override + public String toString() { + return "M[" + name + "]"; + } }