diff --git a/src/main/java/dev/latvian/mods/rhino/Context.java b/src/main/java/dev/latvian/mods/rhino/Context.java index 1854741a..37b2c6e7 100644 --- a/src/main/java/dev/latvian/mods/rhino/Context.java +++ b/src/main/java/dev/latvian/mods/rhino/Context.java @@ -15,81 +15,48 @@ import dev.latvian.mods.rhino.util.ClassVisibilityContext; import dev.latvian.mods.rhino.util.CustomJavaToJsWrapper; import dev.latvian.mods.rhino.util.JavaSetWrapper; +import dev.latvian.mods.rhino.util.wrap.TypeWrapperFactory; import java.io.IOException; import java.io.Reader; +import java.lang.reflect.Array; import java.lang.reflect.Field; +import java.lang.reflect.GenericArrayType; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Type; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -/** - * This class represents the runtime context of an executing script. - *

- * Before executing a script, an instance of Context must be created - * and associated with the thread that will be executing the script. - * The Context will be used to store information about the executing - * of the script such as the call stack. Contexts are associated with - * the current thread using the {@link #enter()} method.

- *

- * Different forms of script execution are supported. Scripts may be - * evaluated from the source directly, or first compiled and then later - * executed. Interactive execution is also supported.

- *

- * Some aspects of script execution, such as type conversions and - * object creation, may be accessed directly through methods of - * Context. - * - * @author Norris Boyd - * @author Brendan Eich - * @see Scriptable - */ - @SuppressWarnings("ThrowableNotThrown") public class Context { - /** - * Report a warning using the error reporter for the current thread. - * - * @param message the warning message to report - * @param sourceName a string describing the source, such as a filename - * @param lineno the starting line number - * @param lineSource the text of the line (may be null) - * @param lineOffset the offset into lineSource where problem was detected - * @see ErrorReporter - */ + public static final int CONVERSION_TRIVIAL = 1; + public static final int CONVERSION_NONTRIVIAL = 0; + public static final int CONVERSION_NONE = 99; + public static final int JSTYPE_UNDEFINED = 0; // undefined type + public static final int JSTYPE_NULL = 1; // null + public static final int JSTYPE_BOOLEAN = 2; // boolean + public static final int JSTYPE_NUMBER = 3; // number + public static final int JSTYPE_STRING = 4; // string + public static final int JSTYPE_JAVA_CLASS = 5; // JavaClass + public static final int JSTYPE_JAVA_OBJECT = 6; // JavaObject + public static final int JSTYPE_JAVA_ARRAY = 7; // JavaArray + public static final int JSTYPE_OBJECT = 8; // Scriptable + public static void reportWarning(Context cx, String message, String sourceName, int lineno, String lineSource, int lineOffset) { cx.getErrorReporter().warning(message, sourceName, lineno, lineSource, lineOffset); } - /** - * Report a warning using the error reporter for the current thread. - * - * @param message the warning message to report - * @param cx - * @see ErrorReporter - */ public static void reportWarning(String message, Context cx) { int[] linep = {0}; String filename = getSourcePositionFromStack(cx, linep); Context.reportWarning(cx, message, filename, linep[0], null, 0); } - /** - * Report an error using the error reporter for the current thread. - * - * @param cx - * @param message the error message to report - * @param lineno the starting line number - * @param lineSource the text of the line (may be null) - * @param lineOffset the offset into lineSource where problem was detected - * @param sourceName a string describing the source, such as a filename - * @see ErrorReporter - */ public static void reportError(Context cx, String message, int lineno, String lineSource, int lineOffset, String sourceName) { if (cx != null) { cx.getErrorReporter().error(cx, message, sourceName, lineno, lineSource, lineOffset); @@ -98,12 +65,6 @@ public static void reportError(Context cx, String message, int lineno, String li } } - /** - * Report an error using the error reporter for the current thread. - * - * @param message the error message to report - * @see ErrorReporter - */ public static void reportError(Context cx, String message) { int[] linep = {0}; String filename = getSourcePositionFromStack(cx, linep); @@ -174,69 +135,6 @@ public static Object getUndefinedValue() { return Undefined.INSTANCE; } - /** - * Convenient method to convert java value to its closest representation - * in JavaScript. - *

- * If value is an instance of String, Number, Boolean, Function or - * Scriptable, it is returned as it and will be treated as the corresponding - * JavaScript type of string, number, boolean, function and object. - *

- * Note that for Number instances during any arithmetic operation in - * JavaScript the engine will always use the result of - * Number.doubleValue() resulting in a precision loss if - * the number can not fit into double. - *

- * If value is an instance of Character, it will be converted to string of - * length 1 and its JavaScript type will be string. - *

- * The rest of values will be wrapped as LiveConnect objects - * by calling {@link Scriptable#getPrototype(Context)} as in: - *

-	 *    Context cx = Context.getCurrentContext();
-	 *    return cx.getWrapFactory().wrap(cx, scope, value, null);
-	 * 
- * - * @param value any Java object - * @param scope top scope object - * @return value suitable to pass to any API that takes JavaScript values. - */ - public Object javaToJS(Object value, Scriptable scope) { - if (value instanceof String || value instanceof Number || value instanceof Boolean || value instanceof Scriptable) { - return value; - } else if (value instanceof Character) { - return String.valueOf(((Character) value).charValue()); - } else { - return wrap(scope, value, null, null); - } - } - - /** - * Convert a JavaScript value into the desired type. - * Uses the semantics defined with LiveConnect3 and throws an - * Illegal argument exception if the conversion cannot be performed. - * - * @param value the JavaScript value to convert - * @param desiredType the Java type to convert to. Primitive Java - * types are represented using the TYPE fields in the corresponding - * wrapper class in java.lang. - * @return the converted value - * @throws EvaluatorException if the conversion cannot be performed - */ - public Object jsToJava(Object value, Class desiredType, Type desiredGenericType) throws EvaluatorException { - if (desiredType == null) { - return value; - } else if (desiredType == Object.class) { - return Wrapper.unwrapped(value); - } else { - return NativeJavaObject.coerceTypeImpl(factory.getTypeWrappers(), desiredType, value, this); - } - } - - public Object jsToJava(Object value, Class desiredType) throws EvaluatorException { - return jsToJava(value, desiredType, desiredType); - } - /** * Rethrow the exception wrapping it as the script runtime exception. * Unless the exception is instance of {@link EcmaError} or @@ -311,10 +209,6 @@ public static String getSourcePositionFromStack(Context cx, int[] linep) { boolean isContinuationsTopCall; NativeCall currentActivationCall; BaseFunction typeErrorThrower; - // for Objects, Arrays to tag themselves as being printed out, - // so they don't print themselves out recursively. - // Use ObjToIntMap instead of java.util.HashSet for JDK 1.1 compatibility - ObjToIntMap iterating; RegExp regExp; // For the interpreter to store the last frame for error reports etc. Object lastInterpreterFrame; @@ -866,7 +760,6 @@ public void setGenerateObserverCount(boolean generateObserverCount) { protected void observeInstructionCount(int instructionCount) { } - /********** end of API **********/ public final ClassLoader getApplicationClassLoader() { if (applicationClassLoader == null) { @@ -1271,12 +1164,12 @@ public Scriptable wrapNewObject(Scriptable scope, Object obj) { return wrapAsJavaObject(scope, obj, null, null); } - public int getConversionWeight(Object fromObj, Class to) { - if (factory.getTypeWrappers().getWrapperFactory(to, fromObj) != null) { - return NativeJavaObject.CONVERSION_NONTRIVIAL; + public int internalConversionWeight(Object fromObj, Class target, Type genericTarget) { + if (factory.getTypeWrappers().hasWrapper(fromObj, target, genericTarget)) { + return CONVERSION_NONTRIVIAL; } - return NativeJavaObject.CONVERSION_NONE; + return CONVERSION_NONE; } /** @@ -1305,4 +1198,585 @@ public String getMappedMethod(Class from, Method method) { public int getMaximumInterpreterStackDepth() { return Integer.MAX_VALUE; } + + public Object createInterfaceAdapter(Class type, Type genericType, ScriptableObject so) { + // XXX: Currently only instances of ScriptableObject are + // supported since the resulting interface proxies should + // be reused next time conversion is made and generic + // Callable has no storage for it. Weak references can + // address it but for now use this restriction. + + Object key = Kit.makeHashKeyFromPair("Coerced Interface", type); + Object old = so.getAssociatedValue(key); + if (old != null) { + // Function was already wrapped + return old; + } + Object glue = InterfaceAdapter.create(this, type, so); + // Store for later retrieval + glue = so.associateValue(key, glue); + return glue; + } + + public Object javaToJS(Object value, Scriptable scope) { + if (value instanceof String || value instanceof Number || value instanceof Boolean || value instanceof Scriptable) { + return value; + } else if (value instanceof Character) { + return String.valueOf(((Character) value).charValue()); + } else { + return wrap(scope, value, null, null); + } + } + + public final Object jsToJava(Object value, Class desiredType, Type desiredGenericType) throws EvaluatorException { + if (desiredType == null) { + return value; + } else if (desiredType == Object.class) { + return Wrapper.unwrapped(value); + } else if (desiredType.isArray() && value instanceof NativeArray array) { + if (desiredGenericType instanceof GenericArrayType type) { + var componentType = type.getGenericComponentType(); + + if (componentType instanceof Class arrayType) { + // Make a new java array, and coerce the JS array components + // to the target (component) type. + long length = array.getLength(); + var result = Array.newInstance(arrayType, (int) length); + for (int i = 0; i < length; ++i) { + try { + Array.set(result, i, jsToJava(array.get(this, i, array), arrayType, type.getGenericComponentType())); + } catch (EvaluatorException ee) { + return reportConversionError(value, arrayType); + } + } + + return result; + } else if (componentType instanceof GenericArrayType subtype) { + System.out.println("uhhh"); + } + } + } + + return internalJsToJava(value, desiredType, desiredGenericType); + } + + public final Object jsToJava(Object value, Class desiredType) throws EvaluatorException { + return jsToJava(value, desiredType, desiredType); + } + + private static int getJSTypeCode(Object value) { + if (value == null) { + return JSTYPE_NULL; + } else if (value == Undefined.INSTANCE) { + return JSTYPE_UNDEFINED; + } else if (value instanceof CharSequence) { + return JSTYPE_STRING; + } else if (value instanceof Number) { + return JSTYPE_NUMBER; + } else if (value instanceof Boolean) { + return JSTYPE_BOOLEAN; + } else if (value instanceof Scriptable) { + if (value instanceof NativeJavaClass) { + return JSTYPE_JAVA_CLASS; + } else if (value instanceof NativeJavaArray) { + return JSTYPE_JAVA_ARRAY; + } else if (value instanceof Wrapper) { + return JSTYPE_JAVA_OBJECT; + } else { + return JSTYPE_OBJECT; + } + } else if (value instanceof Class) { + return JSTYPE_JAVA_CLASS; + } else { + Class valueClass = value.getClass(); + if (valueClass.isArray()) { + return JSTYPE_JAVA_ARRAY; + } + return JSTYPE_JAVA_OBJECT; + } + } + + protected Object internalJsToJava(Object value, Class target, Type genericTarget) { + var typeWrappers = factory.getTypeWrappers(); + + if (value == null || value.getClass() == target) { + return value; + } + + if (target.isArray()) { + // Make a new java array, and coerce the JS array components to the target (component) type. + Class arrayType = target.getComponentType(); + var arrayGenericType = genericTarget instanceof GenericArrayType arr ? arr.getGenericComponentType() : arrayType; + + if (value instanceof NativeArray array) { + long length = array.getLength(); + + Object result = Array.newInstance(arrayType, (int) length); + for (int i = 0; i < length; ++i) { + try { + Array.set(result, i, jsToJava(array.get(this, i, array), arrayType, arrayGenericType)); + } catch (EvaluatorException ee) { + return reportConversionError(value, target); + } + } + + return result; + } else { + // Convert a single value to an array + Object result = Array.newInstance(arrayType, 1); + Array.set(result, 0, jsToJava(value, arrayType, arrayGenericType)); + return result; + } + } + /*else if (o instanceof Iterable) { + int size; + + if (o instanceof Collection) { + size = ((Collection) o).size(); + } else { + size = 0; + + for (Object o1 : (Iterable) o) { + size++; + } + } + + if (size == 0) { + return emptyArray; + } + + T[] array = (T[]) Array.newInstance(target, size); + int index = 0; + + for (Object o1 : (Iterable) o) { + if (typeWrapper.validator.test(o1)) { + array[index] = typeWrapper.factory.wrap(cx, o1); + index++; + } + } + + return index == 0 ? emptyArray : index == array.length ? array : Arrays.copyOf(array, index, arrayTarget); + } + */ + + Object unwrappedValue = Wrapper.unwrapped(value); + + + TypeWrapperFactory typeWrapper = typeWrappers == null ? null : typeWrappers.getWrapperFactory(unwrappedValue, target, genericTarget); + + if (typeWrapper != null) { + return typeWrapper.wrap(this, unwrappedValue, target, genericTarget); + } + + switch (getJSTypeCode(value)) { + case JSTYPE_NULL -> { + // raise error if type.isPrimitive() + if (target.isPrimitive()) { + return reportConversionError(value, target); + } + return null; + } + case JSTYPE_UNDEFINED -> { + if (target == ScriptRuntime.StringClass || target == ScriptRuntime.ObjectClass) { + return "undefined"; + } + return reportConversionError("undefined", target, value); + } + case JSTYPE_BOOLEAN -> { + // Under LC3, only JS Booleans can be coerced into a Boolean value + if (target == Boolean.TYPE || target == ScriptRuntime.BooleanClass || target == ScriptRuntime.ObjectClass) { + return value; + } else if (target == ScriptRuntime.StringClass) { + return value.toString(); + } else { + return reportConversionError(value, target); + } + } + case JSTYPE_NUMBER -> { + if (target == ScriptRuntime.StringClass) { + return ScriptRuntime.toString(this, value); + } else if (target == ScriptRuntime.ObjectClass) { + /* + if (cx.hasFeature(Context.FEATURE_INTEGER_WITHOUT_DECIMAL_PLACE)) { + //to process numbers like 2.0 as 2 without decimal place + long roundedValue = Math.round(toDouble(value)); + if (roundedValue == toDouble(value)) { + return coerceToNumber(Long.TYPE, value); + } + } + */ + return coerceToNumber(Double.TYPE, value); + } else if ((target.isPrimitive() && target != Boolean.TYPE) || ScriptRuntime.NumberClass.isAssignableFrom(target)) { + return coerceToNumber(target, value); + } else { + return reportConversionError(value, target); + } + } + case JSTYPE_STRING -> { + if (target == ScriptRuntime.StringClass || target.isInstance(value)) { + return value.toString(); + } else if (target == Character.TYPE || target == ScriptRuntime.CharacterClass) { + // Special case for converting a single char string to a + // character + // Placed here because it applies *only* to JS strings, + // not other JS objects converted to strings + if (((CharSequence) value).length() == 1) { + return ((CharSequence) value).charAt(0); + } + return coerceToNumber(target, value); + } else if ((target.isPrimitive() && target != Boolean.TYPE) || ScriptRuntime.NumberClass.isAssignableFrom(target)) { + return coerceToNumber(target, value); + } else { + return reportConversionError(value, target); + } + } + case JSTYPE_JAVA_CLASS -> { + if (target == ScriptRuntime.ClassClass || target == ScriptRuntime.ObjectClass) { + return unwrappedValue; + } else if (target == ScriptRuntime.StringClass) { + return unwrappedValue.toString(); + } else { + return reportConversionError(unwrappedValue, target); + } + } + case JSTYPE_JAVA_OBJECT, JSTYPE_JAVA_ARRAY -> { + if (target.isPrimitive()) { + if (target == Boolean.TYPE) { + return reportConversionError(unwrappedValue, target); + } + return coerceToNumber(target, unwrappedValue); + } + if (target == ScriptRuntime.StringClass) { + return unwrappedValue.toString(); + } + if (target.isInstance(unwrappedValue)) { + return unwrappedValue; + } + return reportConversionError(unwrappedValue, target); + } + case JSTYPE_OBJECT -> { + if (target == ScriptRuntime.StringClass) { + return ScriptRuntime.toString(this, value); + } else if (target.isPrimitive()) { + if (target == Boolean.TYPE) { + return reportConversionError(value, target); + } + return coerceToNumber(target, value); + } else if (target.isInstance(value)) { + return value; + } else if (target == ScriptRuntime.DateClass && value instanceof NativeDate) { + double time = ((NativeDate) value).getJSTimeValue(); + // XXX: This will replace NaN by 0 + return new Date((long) time); + } else if (value instanceof Wrapper) { + if (target.isInstance(unwrappedValue)) { + return unwrappedValue; + } + return reportConversionError(unwrappedValue, target); + } else if (target.isInterface() && (value instanceof NativeObject || value instanceof NativeFunction || value instanceof ArrowFunction)) { + // Try to use function/object as implementation of Java interface. + return createInterfaceAdapter(target, genericTarget, (ScriptableObject) value); + } else { + return reportConversionError(value, target); + } + } + } + + return value; + } + + public final boolean canConvert(Object from, Class target, Type genericTarget) { + return getConversionWeight(from, target, genericTarget) < CONVERSION_NONE; + } + + public final int getConversionWeight(Object from, Class target, Type genericTarget) { + int fcw = internalConversionWeight(from, target, genericTarget); + + if (fcw != CONVERSION_NONE) { + return fcw; + } + + int fromCode = getJSTypeCode(from); + + switch (fromCode) { + case JSTYPE_UNDEFINED -> { + if (target == ScriptRuntime.StringClass || target == ScriptRuntime.ObjectClass) { + return 1; + } + } + case JSTYPE_NULL -> { + if (!target.isPrimitive()) { + return 1; + } + } + case JSTYPE_BOOLEAN -> { + // "boolean" is #1 + if (target == Boolean.TYPE) { + return 1; + } else if (target == ScriptRuntime.BooleanClass) { + return 2; + } else if (target == ScriptRuntime.ObjectClass) { + return 3; + } else if (target == ScriptRuntime.StringClass) { + return 4; + } + } + case JSTYPE_NUMBER -> { + if (target.isPrimitive()) { + if (target == Double.TYPE) { + return 1; + } else if (target != Boolean.TYPE) { + return 1 + getSizeRank(target); + } + } else { + if (target == ScriptRuntime.StringClass) { + // native numbers are #1-8 + return 9; + } else if (target == ScriptRuntime.ObjectClass) { + return 10; + } else if (ScriptRuntime.NumberClass.isAssignableFrom(target)) { + // "double" is #1 + return 2; + } + } + } + case JSTYPE_STRING -> { + if (target == ScriptRuntime.StringClass) { + return 1; + } else if (target.isInstance(from)) { + return 2; + } else if (target.isPrimitive()) { + if (target == Character.TYPE) { + return 3; + } else if (target != Boolean.TYPE) { + return 4; + } + } + } + case JSTYPE_JAVA_CLASS -> { + if (target == ScriptRuntime.ClassClass) { + return 1; + } else if (target == ScriptRuntime.ObjectClass) { + return 3; + } else if (target == ScriptRuntime.StringClass) { + return 4; + } + } + case JSTYPE_JAVA_OBJECT, JSTYPE_JAVA_ARRAY -> { + Object javaObj = from; + if (javaObj instanceof Wrapper) { + javaObj = ((Wrapper) javaObj).unwrap(); + } + if (target.isInstance(javaObj)) { + return CONVERSION_NONTRIVIAL; + } + if (target == ScriptRuntime.StringClass) { + return 2; + } else if (target.isPrimitive() && target != Boolean.TYPE) { + return (fromCode == JSTYPE_JAVA_ARRAY) ? CONVERSION_NONE : 2 + getSizeRank(target); + } + } + case JSTYPE_OBJECT -> { + // Other objects takes #1-#3 spots + if (target != ScriptRuntime.ObjectClass && target.isInstance(from)) { + // No conversion required, but don't apply for java.lang.Object + return 1; + } + if (target.isArray()) { + if (from instanceof NativeArray) { + // This is a native array conversion to a java array + // Array conversions are all equal, and preferable to object + // and string conversion, per LC3. + return 2; + } + } else if (target == ScriptRuntime.ObjectClass) { + return 3; + } else if (target == ScriptRuntime.StringClass) { + return 4; + } else if (target == ScriptRuntime.DateClass) { + if (from instanceof NativeDate) { + // This is a native date to java date conversion + return 1; + } + } else if (target.isInterface()) { + + if (from instanceof NativeFunction) { + // See comments in createInterfaceAdapter + return 1; + } + if (from instanceof NativeObject) { + return 2; + } + return 12; + } else if (target.isPrimitive() && target != Boolean.TYPE) { + return 4 + getSizeRank(target); + } + } + } + + return CONVERSION_NONE; + } + + public static int getSizeRank(Class aType) { + if (aType == Double.TYPE) { + return 1; + } else if (aType == Float.TYPE) { + return 2; + } else if (aType == Long.TYPE) { + return 3; + } else if (aType == Integer.TYPE) { + return 4; + } else if (aType == Short.TYPE) { + return 5; + } else if (aType == Character.TYPE) { + return 6; + } else if (aType == Byte.TYPE) { + return 7; + } else if (aType == Boolean.TYPE) { + return CONVERSION_NONE; + } else { + return 8; + } + } + + protected Object coerceToNumber(Class type, Object value) { + Class valueClass = value.getClass(); + + // Character + if (type == Character.TYPE || type == ScriptRuntime.CharacterClass) { + if (valueClass == ScriptRuntime.CharacterClass) { + return value; + } + return (char) toInteger(value, ScriptRuntime.CharacterClass, Character.MIN_VALUE, Character.MAX_VALUE); + } + + // Double, Float + if (type == ScriptRuntime.ObjectClass || type == ScriptRuntime.DoubleClass || type == Double.TYPE) { + return valueClass == ScriptRuntime.DoubleClass ? value : Double.valueOf(toDouble(value)); + } + + if (type == ScriptRuntime.FloatClass || type == Float.TYPE) { + if (valueClass == ScriptRuntime.FloatClass) { + return value; + } + double number = toDouble(value); + if (Double.isInfinite(number) || Double.isNaN(number) || number == 0.0) { + return (float) number; + } + + double absNumber = Math.abs(number); + if (absNumber < Float.MIN_VALUE) { + return (number > 0.0) ? +0.0f : -0.0f; + } else if (absNumber > Float.MAX_VALUE) { + return (number > 0.0) ? Float.POSITIVE_INFINITY : Float.NEGATIVE_INFINITY; + } else { + return (float) number; + } + } + + // Integer, Long, Short, Byte + if (type == ScriptRuntime.IntegerClass || type == Integer.TYPE) { + if (valueClass == ScriptRuntime.IntegerClass) { + return value; + } + return (int) toInteger(value, ScriptRuntime.IntegerClass, Integer.MIN_VALUE, Integer.MAX_VALUE); + } + + if (type == ScriptRuntime.LongClass || type == Long.TYPE) { + if (valueClass == ScriptRuntime.LongClass) { + return value; + } + /* Long values cannot be expressed exactly in doubles. + * We thus use the largest and smallest double value that + * has a value expressible as a long value. We build these + * numerical values from their hexidecimal representations + * to avoid any problems caused by attempting to parse a + * decimal representation. + */ + final double max = Double.longBitsToDouble(0x43dfffffffffffffL); + final double min = Double.longBitsToDouble(0xc3e0000000000000L); + return toInteger(value, ScriptRuntime.LongClass, min, max); + } + + if (type == ScriptRuntime.ShortClass || type == Short.TYPE) { + if (valueClass == ScriptRuntime.ShortClass) { + return value; + } + return (short) toInteger(value, ScriptRuntime.ShortClass, Short.MIN_VALUE, Short.MAX_VALUE); + } + + if (type == ScriptRuntime.ByteClass || type == Byte.TYPE) { + if (valueClass == ScriptRuntime.ByteClass) { + return value; + } + return (byte) toInteger(value, ScriptRuntime.ByteClass, Byte.MIN_VALUE, Byte.MAX_VALUE); + } + + return toDouble(value); + } + + protected double toDouble(Object value) { + if (value instanceof Number) { + return ((Number) value).doubleValue(); + } else if (value instanceof String) { + return ScriptRuntime.toNumber(this, (String) value); + } else if (value instanceof Scriptable) { + if (value instanceof Wrapper) { + // XXX: optimize tail-recursion? + return toDouble(((Wrapper) value).unwrap()); + } + return ScriptRuntime.toNumber(this, value); + } else { + Method meth; + try { + meth = value.getClass().getMethod("doubleValue", (Class[]) null); + } catch (NoSuchMethodException | SecurityException e) { + meth = null; + } + if (meth != null) { + try { + return ((Number) meth.invoke(value, (Object[]) null)).doubleValue(); + } catch (IllegalAccessException | InvocationTargetException e) { + // XXX: ignore, or error message? + reportConversionError(value, Double.TYPE); + } + } + return ScriptRuntime.toNumber(this, value.toString()); + } + } + + protected long toInteger(Object value, Class type, double min, double max) { + double d = toDouble(value); + + if (Double.isInfinite(d) || Double.isNaN(d)) { + // Convert to string first, for more readable message + reportConversionError(ScriptRuntime.toString(this, value), type); + } + + if (d > 0.0) { + d = Math.floor(d); + } else { + d = Math.ceil(d); + } + + if (d < min || d > max) { + // Convert to string first, for more readable message + reportConversionError(ScriptRuntime.toString(this, value), type); + } + return (long) d; + } + + private Object reportConversionError(Object value, Class type) { + return reportConversionError(value, type, value); + } + + private Object reportConversionError(Object value, Class type, Object stringValue) { + // It uses String.valueOf(value), not value.toString() since + // value can be null, bug 282447. + throw Context.reportRuntimeError2("msg.conversion.not.allowed", String.valueOf(stringValue), JavaMembers.javaSignature(type), this); + } + + public String defaultObjectToSource(Scriptable scope, Scriptable thisObj, Object[] args) { + return "not_supported"; + } } diff --git a/src/main/java/dev/latvian/mods/rhino/NativeArray.java b/src/main/java/dev/latvian/mods/rhino/NativeArray.java index f36df5a6..5c98a7f7 100644 --- a/src/main/java/dev/latvian/mods/rhino/NativeArray.java +++ b/src/main/java/dev/latvian/mods/rhino/NativeArray.java @@ -407,86 +407,58 @@ private static void setRawElem(Context cx, Scriptable target, long index, Object private static String toStringHelper(Context cx, Scriptable scope, Scriptable thisObj, boolean toLocale) { Scriptable o = ScriptRuntime.toObject(cx, scope, thisObj); - /* It's probably redundant to handle long lengths in this - * function; StringBuilders are limited to 2^31 in java. - */ - long length = getLengthProperty(cx, o, false); + int length = (int) getLengthProperty(cx, o, false); - StringBuilder result = new StringBuilder(256); - - // whether to return '4,unquoted,5' or '[4, "quoted", 5]' - String separator; + if (length == 0) { + return "[]"; + } - separator = ","; + StringBuilder result = new StringBuilder(256); + result.append('['); - boolean haslast = false; - long i = 0; + // make toSource print null and undefined values in recent versions + for (int i = 0; i < length; i++) { + if (i > 0) { + result.append(", "); + } + Object elem = getRawElem(o, i, cx); + if (elem == NOT_FOUND || elem == null || elem == Undefined.INSTANCE) { + continue; + } - boolean toplevel, iterating; - if (cx.iterating == null) { - toplevel = true; - iterating = false; - cx.iterating = new ObjToIntMap(31); - } else { - toplevel = false; - iterating = cx.iterating.has(o); + result.append(ScriptRuntime.uneval(cx, scope, elem)); } - // Make sure cx.iterating is set to null when done - // so we don't leak memory - try { - if (!iterating) { - // stop recursion - cx.iterating.put(o, 0); + result.append(']'); - // make toSource print null and undefined values in recent versions - for (i = 0; i < length; i++) { - if (i > 0) { - result.append(separator); - } - Object elem = getRawElem(o, i, cx); - if (elem == NOT_FOUND || elem == null || elem == Undefined.INSTANCE) { - haslast = false; - continue; - } - haslast = true; + return result.toString(); + } - if (false) { - result.append(ScriptRuntime.uneval(cx, scope, elem)); + private static String toSource(Context cx, Scriptable scope, Scriptable thisObj) { + Scriptable o = ScriptRuntime.toObject(cx, scope, thisObj); - } else if (elem instanceof String) { - result.append((String) elem); + int length = (int) getLengthProperty(cx, o, false); - } else { - if (toLocale) { - Callable fun; - Scriptable funThis; - fun = ScriptRuntime.getPropFunctionAndThis(cx, scope, elem, "toLocaleString"); - funThis = cx.lastStoredScriptable(); - elem = fun.call(cx, scope, funThis, ScriptRuntime.EMPTY_OBJECTS); - } - result.append(ScriptRuntime.toString(cx, elem)); - } - } + if (length == 0) { + return "[]"; + } + + StringBuilder result = new StringBuilder(256); + result.append('['); - // processing of thisObj done, remove it from the recursion detector - // to allow thisObj to be again in the array later on - cx.iterating.remove(o); + for (int i = 0; i < length; i++) { + if (i > 0) { + result.append(", "); } - } finally { - if (toplevel) { - cx.iterating = null; + Object elem = getRawElem(o, i, cx); + if (elem == NOT_FOUND || elem == null || elem == Undefined.INSTANCE) { + continue; } - } - if (false) { - //for [,,].length behavior; we want toString to be symmetric. - if (!haslast && i > 0) { - result.append(", ]"); - } else { - result.append(']'); - } + result.append(ScriptRuntime.uneval(cx, scope, elem)); } + + result.append(']'); return result.toString(); } @@ -1741,7 +1713,7 @@ public Object execIdCall(IdFunctionObject f, Context cx, Scriptable scope, Scrip return toStringHelper(cx, scope, thisObj, true); case Id_toSource: - return "not_supported"; + return toSource(cx, scope, thisObj); case Id_join: return js_join(cx, scope, thisObj, args); diff --git a/src/main/java/dev/latvian/mods/rhino/NativeBoolean.java b/src/main/java/dev/latvian/mods/rhino/NativeBoolean.java index 440b2a38..54bdc2bd 100644 --- a/src/main/java/dev/latvian/mods/rhino/NativeBoolean.java +++ b/src/main/java/dev/latvian/mods/rhino/NativeBoolean.java @@ -92,18 +92,11 @@ public Object execIdCall(IdFunctionObject f, Context cx, Scriptable scope, Scrip } boolean value = ((NativeBoolean) thisObj).booleanValue; - switch (id) { - - case Id_toString: - return value ? "true" : "false"; - - case Id_toSource: - return "not_supported"; - - case Id_valueOf: - return value; - } - throw new IllegalArgumentException(String.valueOf(id)); + return switch (id) { + case Id_toString, Id_toSource -> value ? "true" : "false"; + case Id_valueOf -> value; + default -> throw new IllegalArgumentException(String.valueOf(id)); + }; } @Override diff --git a/src/main/java/dev/latvian/mods/rhino/NativeDate.java b/src/main/java/dev/latvian/mods/rhino/NativeDate.java index 85bf6265..f068d2a1 100644 --- a/src/main/java/dev/latvian/mods/rhino/NativeDate.java +++ b/src/main/java/dev/latvian/mods/rhino/NativeDate.java @@ -1642,7 +1642,7 @@ public Object execIdCall(IdFunctionObject f, Context cx, Scriptable scope, Scrip return js_NaN_date_str; case Id_toSource: - return "not_supported"; + return "Date"; case Id_valueOf: case Id_getTime: diff --git a/src/main/java/dev/latvian/mods/rhino/NativeError.java b/src/main/java/dev/latvian/mods/rhino/NativeError.java index 8e68ffac..a1d64e5f 100644 --- a/src/main/java/dev/latvian/mods/rhino/NativeError.java +++ b/src/main/java/dev/latvian/mods/rhino/NativeError.java @@ -239,7 +239,7 @@ public Object execIdCall(IdFunctionObject f, Context cx, Scriptable scope, Scrip return js_toString(cx, thisObj); case Id_toSource: - return "not_supported"; + return "Error"; case ConstructorId_captureStackTrace: js_captureStackTrace(cx, thisObj, args); diff --git a/src/main/java/dev/latvian/mods/rhino/NativeJavaClass.java b/src/main/java/dev/latvian/mods/rhino/NativeJavaClass.java index 376aaca1..ecf105c4 100644 --- a/src/main/java/dev/latvian/mods/rhino/NativeJavaClass.java +++ b/src/main/java/dev/latvian/mods/rhino/NativeJavaClass.java @@ -103,9 +103,6 @@ private static Class findNestedClass(Class parentClass, String name) { private Map staticFieldAndMethods; - public NativeJavaClass() { - } - public NativeJavaClass(Context cx, Scriptable scope, Class cl) { this(cx, scope, cl, false); } @@ -242,7 +239,7 @@ public Scriptable construct(Context cx, Scriptable scope, Object[] args) { // When running on Android create an InterfaceAdapter since our // bytecode generation won't work on Dalvik VM. if ("Dalvik".equals(System.getProperty("java.vm.name")) && classObject.isInterface()) { - Object obj = createInterfaceAdapter(cx, classObject, ScriptableObject.ensureScriptableObject(args[0], cx)); + Object obj = cx.createInterfaceAdapter(classObject, classObject, ScriptableObject.ensureScriptableObject(args[0], cx)); return cx.wrapAsJavaObject(scope, obj, null, null); } // use JavaAdapter to construct a new class on the fly that diff --git a/src/main/java/dev/latvian/mods/rhino/NativeJavaMethod.java b/src/main/java/dev/latvian/mods/rhino/NativeJavaMethod.java index 3917e816..eed3ec48 100644 --- a/src/main/java/dev/latvian/mods/rhino/NativeJavaMethod.java +++ b/src/main/java/dev/latvian/mods/rhino/NativeJavaMethod.java @@ -8,6 +8,7 @@ import java.lang.reflect.Array; import java.lang.reflect.Method; +import java.lang.reflect.Type; import java.util.concurrent.CopyOnWriteArrayList; /** @@ -92,7 +93,7 @@ static int findFunction(Context cx, MemberBox[] methodsOrCtors, Object[] args) { } } for (int j = 0; j != alength; ++j) { - if (!NativeJavaObject.canConvert(cx, args[j], member.argTypes[j])) { + if (!cx.canConvert(args[j], member.argTypes[j], member.genericArgTypes[j])) { if (debug) { printDebug("Rejecting (args can't convert) ", member, args); } @@ -124,7 +125,7 @@ static int findFunction(Context cx, MemberBox[] methodsOrCtors, Object[] args) { } } for (int j = 0; j < alength; j++) { - if (!NativeJavaObject.canConvert(cx, args[j], member.argTypes[j])) { + if (!cx.canConvert(args[j], member.argTypes[j], member.genericArgTypes[j])) { if (debug) { printDebug("Rejecting (args can't convert) ", member, args); } @@ -153,7 +154,7 @@ static int findFunction(Context cx, MemberBox[] methodsOrCtors, Object[] args) { bestFitIndex = extraBestFits[j]; } MemberBox bestFit = methodsOrCtors[bestFitIndex]; - int preference = preferSignature(cx, args, member.argTypes, member.vararg, bestFit.argTypes, bestFit.vararg); + int preference = preferSignature(cx, args, member.argTypes, member.genericArgTypes, member.vararg, bestFit.argTypes, bestFit.genericArgTypes, bestFit.vararg); if (preference == PREFERENCE_AMBIGUOUS) { break; } else if (preference == PREFERENCE_FIRST_ARG) { @@ -253,20 +254,25 @@ static int findFunction(Context cx, MemberBox[] methodsOrCtors, Object[] args) { * Returns one of PREFERENCE_EQUAL, PREFERENCE_FIRST_ARG, * PREFERENCE_SECOND_ARG, or PREFERENCE_AMBIGUOUS. */ - private static int preferSignature(Context cx, Object[] args, Class[] sig1, boolean vararg1, Class[] sig2, boolean vararg2) { + private static int preferSignature(Context cx, Object[] args, Class[] sig1, Type[] gsig1, boolean vararg1, Class[] sig2, Type[] gsig2, boolean vararg2) { int totalPreference = 0; for (int j = 0; j < args.length; j++) { Class type1 = vararg1 && j >= sig1.length ? sig1[sig1.length - 1] : sig1[j]; Class type2 = vararg2 && j >= sig2.length ? sig2[sig2.length - 1] : sig2[j]; + if (type1 == type2) { continue; } + + Type gType1 = vararg1 && j >= gsig1.length ? gsig1[gsig1.length - 1] : gsig1[j]; + Type gType2 = vararg2 && j >= gsig2.length ? gsig2[gsig2.length - 1] : gsig2[j]; + Object arg = args[j]; // Determine which of type1, type2 is easier to convert from arg. - int rank1 = NativeJavaObject.getConversionWeight(cx, arg, type1); - int rank2 = NativeJavaObject.getConversionWeight(cx, arg, type2); + int rank1 = cx.getConversionWeight(arg, type1, gType1); + int rank2 = cx.getConversionWeight(arg, type2, gType2); int preference; if (rank1 < rank2) { @@ -275,7 +281,7 @@ private static int preferSignature(Context cx, Object[] args, Class[] sig1, b preference = PREFERENCE_SECOND_ARG; } else { // Equal ranks - if (rank1 == NativeJavaObject.CONVERSION_NONTRIVIAL) { + if (rank1 == Context.CONVERSION_NONTRIVIAL) { if (type1.isAssignableFrom(type2)) { preference = PREFERENCE_SECOND_ARG; } else if (type2.isAssignableFrom(type1)) { diff --git a/src/main/java/dev/latvian/mods/rhino/NativeJavaObject.java b/src/main/java/dev/latvian/mods/rhino/NativeJavaObject.java index eaca1f20..09dd4f35 100644 --- a/src/main/java/dev/latvian/mods/rhino/NativeJavaObject.java +++ b/src/main/java/dev/latvian/mods/rhino/NativeJavaObject.java @@ -9,15 +9,8 @@ import dev.latvian.mods.rhino.util.DefaultValueTypeHint; import dev.latvian.mods.rhino.util.Deletable; import dev.latvian.mods.rhino.util.JavaIteratorWrapper; -import dev.latvian.mods.rhino.util.wrap.TypeWrapperFactory; -import dev.latvian.mods.rhino.util.wrap.TypeWrappers; -import org.jetbrains.annotations.Nullable; import org.openjdk.nashorn.internal.runtime.NativeJavaPackage; -import java.lang.reflect.Array; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.Date; import java.util.HashMap; import java.util.Map; @@ -33,539 +26,6 @@ */ public class NativeJavaObject implements Scriptable, SymbolScriptable, Wrapper { - public static final int CONVERSION_TRIVIAL = 1; - public static final int CONVERSION_NONTRIVIAL = 0; - public static final int CONVERSION_NONE = 99; - private static final Object COERCED_INTERFACE_KEY = "Coerced Interface"; - private static final int JSTYPE_UNDEFINED = 0; // undefined type - private static final int JSTYPE_NULL = 1; // null - private static final int JSTYPE_BOOLEAN = 2; // boolean - private static final int JSTYPE_NUMBER = 3; // number - private static final int JSTYPE_STRING = 4; // string - private static final int JSTYPE_JAVA_CLASS = 5; // JavaClass - private static final int JSTYPE_JAVA_OBJECT = 6; // JavaObject - private static final int JSTYPE_JAVA_ARRAY = 7; // JavaArray - private static final int JSTYPE_OBJECT = 8; // Scriptable - - /** - * Determine whether we can/should convert between the given type and the - * desired one. This should be superceded by a conversion-cost calculation - * function, but for now I'll hide behind precedent. - */ - public static boolean canConvert(Context cx, Object fromObj, Class to) { - return getConversionWeight(cx, fromObj, to) < CONVERSION_NONE; - } - - /** - * Derive a ranking based on how "natural" the conversion is. - * The special value CONVERSION_NONE means no conversion is possible, - * and CONVERSION_NONTRIVIAL signals that more type conformance testing - * is required. - * Based on - * - * "preferred method conversions" from Live Connect 3 - */ - static int getConversionWeight(Context cx, Object fromObj, Class to) { - int fcw = cx.getConversionWeight(fromObj, to); - - if (fcw != CONVERSION_NONE) { - return fcw; - } - - int fromCode = getJSTypeCode(fromObj); - - switch (fromCode) { - - case JSTYPE_UNDEFINED: - if (to == ScriptRuntime.StringClass || to == ScriptRuntime.ObjectClass) { - return 1; - } - break; - - case JSTYPE_NULL: - if (!to.isPrimitive()) { - return 1; - } - break; - - case JSTYPE_BOOLEAN: - // "boolean" is #1 - if (to == Boolean.TYPE) { - return 1; - } else if (to == ScriptRuntime.BooleanClass) { - return 2; - } else if (to == ScriptRuntime.ObjectClass) { - return 3; - } else if (to == ScriptRuntime.StringClass) { - return 4; - } - break; - - case JSTYPE_NUMBER: - if (to.isPrimitive()) { - if (to == Double.TYPE) { - return 1; - } else if (to != Boolean.TYPE) { - return 1 + getSizeRank(to); - } - } else { - if (to == ScriptRuntime.StringClass) { - // native numbers are #1-8 - return 9; - } else if (to == ScriptRuntime.ObjectClass) { - return 10; - } else if (ScriptRuntime.NumberClass.isAssignableFrom(to)) { - // "double" is #1 - return 2; - } - } - break; - - case JSTYPE_STRING: - if (to == ScriptRuntime.StringClass) { - return 1; - } else if (to.isInstance(fromObj)) { - return 2; - } else if (to.isPrimitive()) { - if (to == Character.TYPE) { - return 3; - } else if (to != Boolean.TYPE) { - return 4; - } - } - break; - - case JSTYPE_JAVA_CLASS: - if (to == ScriptRuntime.ClassClass) { - return 1; - } else if (to == ScriptRuntime.ObjectClass) { - return 3; - } else if (to == ScriptRuntime.StringClass) { - return 4; - } - break; - - case JSTYPE_JAVA_OBJECT: - case JSTYPE_JAVA_ARRAY: - Object javaObj = fromObj; - if (javaObj instanceof Wrapper) { - javaObj = ((Wrapper) javaObj).unwrap(); - } - if (to.isInstance(javaObj)) { - return CONVERSION_NONTRIVIAL; - } - if (to == ScriptRuntime.StringClass) { - return 2; - } else if (to.isPrimitive() && to != Boolean.TYPE) { - return (fromCode == JSTYPE_JAVA_ARRAY) ? CONVERSION_NONE : 2 + getSizeRank(to); - } - break; - - case JSTYPE_OBJECT: - // Other objects takes #1-#3 spots - if (to != ScriptRuntime.ObjectClass && to.isInstance(fromObj)) { - // No conversion required, but don't apply for java.lang.Object - return 1; - } - if (to.isArray()) { - if (fromObj instanceof NativeArray) { - // This is a native array conversion to a java array - // Array conversions are all equal, and preferable to object - // and string conversion, per LC3. - return 2; - } - } else if (to == ScriptRuntime.ObjectClass) { - return 3; - } else if (to == ScriptRuntime.StringClass) { - return 4; - } else if (to == ScriptRuntime.DateClass) { - if (fromObj instanceof NativeDate) { - // This is a native date to java date conversion - return 1; - } - } else if (to.isInterface()) { - - if (fromObj instanceof NativeFunction) { - // See comments in createInterfaceAdapter - return 1; - } - if (fromObj instanceof NativeObject) { - return 2; - } - return 12; - } else if (to.isPrimitive() && to != Boolean.TYPE) { - return 4 + getSizeRank(to); - } - break; - } - - return CONVERSION_NONE; - } - - static int getSizeRank(Class aType) { - if (aType == Double.TYPE) { - return 1; - } else if (aType == Float.TYPE) { - return 2; - } else if (aType == Long.TYPE) { - return 3; - } else if (aType == Integer.TYPE) { - return 4; - } else if (aType == Short.TYPE) { - return 5; - } else if (aType == Character.TYPE) { - return 6; - } else if (aType == Byte.TYPE) { - return 7; - } else if (aType == Boolean.TYPE) { - return CONVERSION_NONE; - } else { - return 8; - } - } - - private static int getJSTypeCode(Object value) { - if (value == null) { - return JSTYPE_NULL; - } else if (value == Undefined.INSTANCE) { - return JSTYPE_UNDEFINED; - } else if (value instanceof CharSequence) { - return JSTYPE_STRING; - } else if (value instanceof Number) { - return JSTYPE_NUMBER; - } else if (value instanceof Boolean) { - return JSTYPE_BOOLEAN; - } else if (value instanceof Scriptable) { - if (value instanceof NativeJavaClass) { - return JSTYPE_JAVA_CLASS; - } else if (value instanceof NativeJavaArray) { - return JSTYPE_JAVA_ARRAY; - } else if (value instanceof Wrapper) { - return JSTYPE_JAVA_OBJECT; - } else { - return JSTYPE_OBJECT; - } - } else if (value instanceof Class) { - return JSTYPE_JAVA_CLASS; - } else { - Class valueClass = value.getClass(); - if (valueClass.isArray()) { - return JSTYPE_JAVA_ARRAY; - } - return JSTYPE_JAVA_OBJECT; - } - } - - /** - * Type-munging for field setting and method invocation. - * Conforms to LC3 specification - */ - static Object coerceTypeImpl(@Nullable TypeWrappers typeWrappers, Class type, Object value, Context cx) { - if (value == null || value.getClass() == type) { - return value; - } - - Object unwrappedValue = Wrapper.unwrapped(value); - TypeWrapperFactory typeWrapper = typeWrappers == null ? null : typeWrappers.getWrapperFactory(type, unwrappedValue); - - if (typeWrapper != null) { - return typeWrapper.wrap(cx, unwrappedValue); - } - - switch (getJSTypeCode(value)) { - case JSTYPE_NULL -> { - // raise error if type.isPrimitive() - if (type.isPrimitive()) { - return reportConversionError(value, type, cx); - } - return null; - } - case JSTYPE_UNDEFINED -> { - if (type == ScriptRuntime.StringClass || type == ScriptRuntime.ObjectClass) { - return "undefined"; - } - return reportConversionError("undefined", type, value, cx); - } - case JSTYPE_BOOLEAN -> { - // Under LC3, only JS Booleans can be coerced into a Boolean value - if (type == Boolean.TYPE || type == ScriptRuntime.BooleanClass || type == ScriptRuntime.ObjectClass) { - return value; - } else if (type == ScriptRuntime.StringClass) { - return value.toString(); - } else { - return reportConversionError(value, type, cx); - } - } - case JSTYPE_NUMBER -> { - if (type == ScriptRuntime.StringClass) { - return ScriptRuntime.toString(cx, value); - } else if (type == ScriptRuntime.ObjectClass) { - /* - if (cx.hasFeature(Context.FEATURE_INTEGER_WITHOUT_DECIMAL_PLACE)) { - //to process numbers like 2.0 as 2 without decimal place - long roundedValue = Math.round(toDouble(value)); - if (roundedValue == toDouble(value)) { - return coerceToNumber(Long.TYPE, value); - } - } - */ - return coerceToNumber(Double.TYPE, value, cx); - } else if ((type.isPrimitive() && type != Boolean.TYPE) || ScriptRuntime.NumberClass.isAssignableFrom(type)) { - return coerceToNumber(type, value, cx); - } else { - return reportConversionError(value, type, cx); - } - } - case JSTYPE_STRING -> { - if (type == ScriptRuntime.StringClass || type.isInstance(value)) { - return value.toString(); - } else if (type == Character.TYPE || type == ScriptRuntime.CharacterClass) { - // Special case for converting a single char string to a - // character - // Placed here because it applies *only* to JS strings, - // not other JS objects converted to strings - if (((CharSequence) value).length() == 1) { - return ((CharSequence) value).charAt(0); - } - return coerceToNumber(type, value, cx); - } else if ((type.isPrimitive() && type != Boolean.TYPE) || ScriptRuntime.NumberClass.isAssignableFrom(type)) { - return coerceToNumber(type, value, cx); - } else { - return reportConversionError(value, type, cx); - } - } - case JSTYPE_JAVA_CLASS -> { - if (type == ScriptRuntime.ClassClass || type == ScriptRuntime.ObjectClass) { - return unwrappedValue; - } else if (type == ScriptRuntime.StringClass) { - return unwrappedValue.toString(); - } else { - return reportConversionError(unwrappedValue, type, cx); - } - } - case JSTYPE_JAVA_OBJECT, JSTYPE_JAVA_ARRAY -> { - if (type.isPrimitive()) { - if (type == Boolean.TYPE) { - return reportConversionError(unwrappedValue, type, cx); - } - return coerceToNumber(type, unwrappedValue, cx); - } - if (type == ScriptRuntime.StringClass) { - return unwrappedValue.toString(); - } - if (type.isInstance(unwrappedValue)) { - return unwrappedValue; - } - return reportConversionError(unwrappedValue, type, cx); - } - case JSTYPE_OBJECT -> { - if (type == ScriptRuntime.StringClass) { - return ScriptRuntime.toString(cx, value); - } else if (type.isPrimitive()) { - if (type == Boolean.TYPE) { - return reportConversionError(value, type, cx); - } - return coerceToNumber(type, value, cx); - } else if (type.isInstance(value)) { - return value; - } else if (type == ScriptRuntime.DateClass && value instanceof NativeDate) { - double time = ((NativeDate) value).getJSTimeValue(); - // XXX: This will replace NaN by 0 - return new Date((long) time); - } else if (type.isArray() && value instanceof NativeArray array) { - // Make a new java array, and coerce the JS array components - // to the target (component) type. - long length = array.getLength(); - Class arrayType = type.getComponentType(); - Object Result = Array.newInstance(arrayType, (int) length); - for (int i = 0; i < length; ++i) { - try { - Array.set(Result, i, coerceTypeImpl(typeWrappers, arrayType, array.get(cx, i, array), cx)); - } catch (EvaluatorException ee) { - return reportConversionError(value, type, cx); - } - } - - return Result; - } else if (value instanceof Wrapper) { - if (type.isInstance(unwrappedValue)) { - return unwrappedValue; - } - return reportConversionError(unwrappedValue, type, cx); - } else if (type.isInterface() && (value instanceof NativeObject || value instanceof NativeFunction || value instanceof ArrowFunction)) { - // Try to use function/object as implementation of Java interface. - return createInterfaceAdapter(cx, type, (ScriptableObject) value); - } else { - return reportConversionError(value, type, cx); - } - } - } - - - return value; - } - - public static Object createInterfaceAdapter(Context cx, Class type, ScriptableObject so) { - // XXX: Currently only instances of ScriptableObject are - // supported since the resulting interface proxies should - // be reused next time conversion is made and generic - // Callable has no storage for it. Weak references can - // address it but for now use this restriction. - - Object key = Kit.makeHashKeyFromPair(COERCED_INTERFACE_KEY, type); - Object old = so.getAssociatedValue(key); - if (old != null) { - // Function was already wrapped - return old; - } - Object glue = InterfaceAdapter.create(cx, type, so); - // Store for later retrieval - glue = so.associateValue(key, glue); - return glue; - } - - private static Object coerceToNumber(Class type, Object value, Context cx) { - Class valueClass = value.getClass(); - - // Character - if (type == Character.TYPE || type == ScriptRuntime.CharacterClass) { - if (valueClass == ScriptRuntime.CharacterClass) { - return value; - } - return (char) toInteger(value, ScriptRuntime.CharacterClass, Character.MIN_VALUE, Character.MAX_VALUE, cx); - } - - // Double, Float - if (type == ScriptRuntime.ObjectClass || type == ScriptRuntime.DoubleClass || type == Double.TYPE) { - return valueClass == ScriptRuntime.DoubleClass ? value : Double.valueOf(toDouble(value, cx)); - } - - if (type == ScriptRuntime.FloatClass || type == Float.TYPE) { - if (valueClass == ScriptRuntime.FloatClass) { - return value; - } - double number = toDouble(value, cx); - if (Double.isInfinite(number) || Double.isNaN(number) || number == 0.0) { - return (float) number; - } - - double absNumber = Math.abs(number); - if (absNumber < Float.MIN_VALUE) { - return (number > 0.0) ? +0.0f : -0.0f; - } else if (absNumber > Float.MAX_VALUE) { - return (number > 0.0) ? Float.POSITIVE_INFINITY : Float.NEGATIVE_INFINITY; - } else { - return (float) number; - } - } - - // Integer, Long, Short, Byte - if (type == ScriptRuntime.IntegerClass || type == Integer.TYPE) { - if (valueClass == ScriptRuntime.IntegerClass) { - return value; - } - return (int) toInteger(value, ScriptRuntime.IntegerClass, Integer.MIN_VALUE, Integer.MAX_VALUE, cx); - } - - if (type == ScriptRuntime.LongClass || type == Long.TYPE) { - if (valueClass == ScriptRuntime.LongClass) { - return value; - } - /* Long values cannot be expressed exactly in doubles. - * We thus use the largest and smallest double value that - * has a value expressible as a long value. We build these - * numerical values from their hexidecimal representations - * to avoid any problems caused by attempting to parse a - * decimal representation. - */ - final double max = Double.longBitsToDouble(0x43dfffffffffffffL); - final double min = Double.longBitsToDouble(0xc3e0000000000000L); - return toInteger(value, ScriptRuntime.LongClass, min, max, cx); - } - - if (type == ScriptRuntime.ShortClass || type == Short.TYPE) { - if (valueClass == ScriptRuntime.ShortClass) { - return value; - } - return (short) toInteger(value, ScriptRuntime.ShortClass, Short.MIN_VALUE, Short.MAX_VALUE, cx); - } - - if (type == ScriptRuntime.ByteClass || type == Byte.TYPE) { - if (valueClass == ScriptRuntime.ByteClass) { - return value; - } - return (byte) toInteger(value, ScriptRuntime.ByteClass, Byte.MIN_VALUE, Byte.MAX_VALUE, cx); - } - - return toDouble(value, cx); - } - - private static double toDouble(Object value, Context cx) { - if (value instanceof Number) { - return ((Number) value).doubleValue(); - } else if (value instanceof String) { - return ScriptRuntime.toNumber(cx, (String) value); - } else if (value instanceof Scriptable) { - if (value instanceof Wrapper) { - // XXX: optimize tail-recursion? - return toDouble(((Wrapper) value).unwrap(), cx); - } - return ScriptRuntime.toNumber(cx, value); - } else { - Method meth; - try { - meth = value.getClass().getMethod("doubleValue", (Class[]) null); - } catch (NoSuchMethodException e) { - meth = null; - } catch (SecurityException e) { - meth = null; - } - if (meth != null) { - try { - return ((Number) meth.invoke(value, (Object[]) null)).doubleValue(); - } catch (IllegalAccessException e) { - // XXX: ignore, or error message? - reportConversionError(value, Double.TYPE, cx); - } catch (InvocationTargetException e) { - // XXX: ignore, or error message? - reportConversionError(value, Double.TYPE, cx); - } - } - return ScriptRuntime.toNumber(cx, value.toString()); - } - } - - private static long toInteger(Object value, Class type, double min, double max, Context cx) { - double d = toDouble(value, cx); - - if (Double.isInfinite(d) || Double.isNaN(d)) { - // Convert to string first, for more readable message - reportConversionError(ScriptRuntime.toString(cx, value), type, cx); - } - - if (d > 0.0) { - d = Math.floor(d); - } else { - d = Math.ceil(d); - } - - if (d < min || d > max) { - // Convert to string first, for more readable message - reportConversionError(ScriptRuntime.toString(cx, value), type, cx); - } - return (long) d; - } - - static Object reportConversionError(Object value, Class type, Context cx) { - return reportConversionError(value, type, value, cx); - } - - static Object reportConversionError(Object value, Class type, Object stringValue, Context cx) { - // It uses String.valueOf(value), not value.toString() since - // value can be null, bug 282447. - throw Context.reportRuntimeError2("msg.conversion.not.allowed", String.valueOf(stringValue), JavaMembers.javaSignature(type), cx); - } - /** * The prototype of this object. */ @@ -581,9 +41,6 @@ static Object reportConversionError(Object value, Class type, Object stringVa protected transient Map customMembers; protected transient boolean isAdapter; - public NativeJavaObject() { - } - public NativeJavaObject(Scriptable scope, Object javaObject, Class staticType, Context cx) { this(scope, javaObject, staticType, false, cx); } diff --git a/src/main/java/dev/latvian/mods/rhino/NativeNumber.java b/src/main/java/dev/latvian/mods/rhino/NativeNumber.java index 83757dfc..c645b071 100644 --- a/src/main/java/dev/latvian/mods/rhino/NativeNumber.java +++ b/src/main/java/dev/latvian/mods/rhino/NativeNumber.java @@ -270,9 +270,8 @@ public Object execIdCall(IdFunctionObject f, Context cx, Scriptable scope, Scrip int base = (args.length == 0 || args[0] == Undefined.INSTANCE) ? 10 : ScriptRuntime.toInt32(cx, args[0]); return ScriptRuntime.numberToString(cx, value, base); } - case Id_toSource: - return "not_supported"; + return ScriptRuntime.numberToString(cx, value, 10); case Id_valueOf: return ScriptRuntime.wrapNumber(value); diff --git a/src/main/java/dev/latvian/mods/rhino/NativeObject.java b/src/main/java/dev/latvian/mods/rhino/NativeObject.java index 9db3d326..79f47d66 100644 --- a/src/main/java/dev/latvian/mods/rhino/NativeObject.java +++ b/src/main/java/dev/latvian/mods/rhino/NativeObject.java @@ -314,7 +314,7 @@ public Object execIdCall(IdFunctionObject f, Context cx, Scriptable scope, Scrip } case Id_toSource: - return ScriptRuntime.defaultObjectToSource(cx, scope, thisObj, args); + return cx.defaultObjectToSource(scope, thisObj, args); case Id___defineGetter__: case Id___defineSetter__: { if (args.length < 2 || !(args[1] instanceof Callable getterOrSetter)) { diff --git a/src/main/java/dev/latvian/mods/rhino/NativeString.java b/src/main/java/dev/latvian/mods/rhino/NativeString.java index a3af050e..e75e8f21 100644 --- a/src/main/java/dev/latvian/mods/rhino/NativeString.java +++ b/src/main/java/dev/latvian/mods/rhino/NativeString.java @@ -836,11 +836,9 @@ public Object execIdCall(IdFunctionObject f, Context cx, Scriptable scope, Scrip case Id_toString: case Id_valueOf: // ECMA 15.5.4.2: 'the toString function is not generic. - CharSequence cs = realThis(thisObj, f, cx).string; - return cs instanceof String ? cs : cs.toString(); - + return realThis(thisObj, f, cx).string.toString(); case Id_toSource: { - return "not_supported"; + return ScriptRuntime.escapeAndWrapString(realThis(thisObj, f, cx).string.toString()); } case Id_charAt: diff --git a/src/main/java/dev/latvian/mods/rhino/NativeSymbol.java b/src/main/java/dev/latvian/mods/rhino/NativeSymbol.java index d252e806..bc0c6ce9 100644 --- a/src/main/java/dev/latvian/mods/rhino/NativeSymbol.java +++ b/src/main/java/dev/latvian/mods/rhino/NativeSymbol.java @@ -203,31 +203,26 @@ public Object execIdCall(IdFunctionObject f, Context cx, Scriptable scope, Scrip return super.execIdCall(f, cx, scope, thisObj, args); } int id = f.methodId(); - switch (id) { - case ConstructorId_for: - return js_for(cx, scope, args); - case ConstructorId_keyFor: - return js_keyFor(cx, scope, args); - - case Id_constructor: + return switch (id) { + case ConstructorId_for -> js_for(cx, scope, args); + case ConstructorId_keyFor -> js_keyFor(cx, scope, args); + case Id_constructor -> { if (thisObj == null) { if (cx.getThreadLocal(CONSTRUCTOR_SLOT) == null) { // We should never get to this via "new". throw ScriptRuntime.typeError0(cx, "msg.no.symbol.new"); } // Unless we are being called by our own internal "new" - return js_constructor(cx, args); + yield js_constructor(cx, args); } - return construct(cx, scope, args); - - case Id_toString: - return getSelf(cx, thisObj).toString(); - case Id_valueOf: - case SymbolId_toPrimitive: - return getSelf(cx, thisObj).js_valueOf(); - default: - return super.execIdCall(f, cx, scope, thisObj, args); - } + yield construct(cx, scope, args); + // We should never get to this via "new". + // Unless we are being called by our own internal "new" + } + case Id_toString -> getSelf(cx, thisObj).toString(); + case Id_valueOf, SymbolId_toPrimitive -> getSelf(cx, thisObj).js_valueOf(); + default -> super.execIdCall(f, cx, scope, thisObj, args); + }; } private Object js_valueOf() { diff --git a/src/main/java/dev/latvian/mods/rhino/ScriptRuntime.java b/src/main/java/dev/latvian/mods/rhino/ScriptRuntime.java index 68c1f4ec..6de928bd 100644 --- a/src/main/java/dev/latvian/mods/rhino/ScriptRuntime.java +++ b/src/main/java/dev/latvian/mods/rhino/ScriptRuntime.java @@ -16,6 +16,7 @@ import dev.latvian.mods.rhino.v8dtoa.DoubleConversion; import dev.latvian.mods.rhino.v8dtoa.FastDtoa; +import java.lang.reflect.Array; import java.text.MessageFormat; import java.util.Arrays; import java.util.Date; @@ -652,8 +653,10 @@ public static Object[] padArguments(Object[] args, int count) { return result; } - public static String escapeString(String s) { - return escapeString(s, '"'); + public static String escapeAndWrapString(String s) { + var c = s.indexOf('\'') == -1 ? '\'' : '"'; + var escaped = escapeString(s, c); + return c + escaped + c; } /** @@ -779,6 +782,28 @@ public static String toString(Context cx, Object val) { } return toString(cx, val); } + if (val.getClass().isArray()) { + var builder = new StringBuilder(); + int length = Array.getLength(val); + + if (length == 0) { + builder.append("[]"); + } else { + builder.append('['); + + for (int i = 0; i < length; i++) { + if (i > 0) { + builder.append(", "); + } + + builder.append(toString(cx, Array.get(val, i))); + } + + builder.append(']'); + } + + return builder.toString(); + } return ToStringJS.toStringJS(cx, val); } @@ -843,8 +868,7 @@ static String uneval(Context cx, Scriptable scope, Object value) { return "undefined"; } if (value instanceof CharSequence) { - String escaped = escapeString(value.toString()); - return '\"' + escaped + '\"'; + return escapeAndWrapString(value.toString()); } if (value instanceof Number) { double d = ((Number) value).doubleValue(); @@ -871,10 +895,6 @@ static String uneval(Context cx, Scriptable scope, Object value) { return value.toString(); } - static String defaultObjectToSource(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { - return "not_supported"; - } - /** * Warning: This doesn't allow to resolve primitive * prototype properly when many top scopes are involved diff --git a/src/main/java/dev/latvian/mods/rhino/mod/core/mixin/common/StringRepresentableMixin.java b/src/main/java/dev/latvian/mods/rhino/mod/core/mixin/common/StringRepresentableMixin.java new file mode 100644 index 00000000..660e82f9 --- /dev/null +++ b/src/main/java/dev/latvian/mods/rhino/mod/core/mixin/common/StringRepresentableMixin.java @@ -0,0 +1,13 @@ +package dev.latvian.mods.rhino.mod.core.mixin.common; + +import dev.latvian.mods.rhino.util.RemappedEnumConstant; +import net.minecraft.util.StringRepresentable; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin(StringRepresentable.class) +public interface StringRepresentableMixin extends RemappedEnumConstant { + @Override + default String getRemappedEnumConstantName() { + return ((StringRepresentable) this).getSerializedName(); + } +} diff --git a/src/main/java/dev/latvian/mods/rhino/util/EnumTypeWrapper.java b/src/main/java/dev/latvian/mods/rhino/util/EnumTypeWrapper.java index 228fe89b..69719642 100644 --- a/src/main/java/dev/latvian/mods/rhino/util/EnumTypeWrapper.java +++ b/src/main/java/dev/latvian/mods/rhino/util/EnumTypeWrapper.java @@ -3,12 +3,14 @@ import dev.latvian.mods.rhino.Context; import dev.latvian.mods.rhino.util.wrap.TypeWrapperFactory; +import java.lang.reflect.Type; import java.util.HashMap; +import java.util.IdentityHashMap; import java.util.Map; import java.util.stream.Collectors; public class EnumTypeWrapper implements TypeWrapperFactory { - private static final Map, EnumTypeWrapper> WRAPPERS = new HashMap<>(); + private static final Map, EnumTypeWrapper> WRAPPERS = new IdentityHashMap<>(); @SuppressWarnings("unchecked") public static EnumTypeWrapper get(Class enumType) { @@ -16,7 +18,9 @@ public static EnumTypeWrapper get(Class enumType) { throw new IllegalArgumentException("Class " + enumType.getName() + " is not an enum!"); } - return (EnumTypeWrapper) WRAPPERS.computeIfAbsent(enumType, EnumTypeWrapper::new); + synchronized (WRAPPERS) { + return (EnumTypeWrapper) WRAPPERS.computeIfAbsent(enumType, EnumTypeWrapper::new); + } } public static String getName(Class enumType, Enum e, boolean cache) { @@ -46,7 +50,7 @@ private EnumTypeWrapper(Class enumType) { this.enumType = enumType; this.indexValues = enumType.getEnumConstants(); this.nameValues = new HashMap<>(); - this.valueNames = new HashMap<>(); + this.valueNames = new IdentityHashMap<>(); for (T t : indexValues) { String name = getName(enumType, (Enum) t, false).toLowerCase(); @@ -56,9 +60,9 @@ private EnumTypeWrapper(Class enumType) { } @Override - public T wrap(Context cx, Object o) { - if (o instanceof CharSequence) { - String s = o.toString().toLowerCase(); + public T wrap(Context cx, Object from, Class toType, Type toGenericType) { + if (from instanceof CharSequence) { + String s = from.toString().toLowerCase(); if (s.isEmpty()) { return null; @@ -71,8 +75,8 @@ public T wrap(Context cx, Object o) { } return t; - } else if (o instanceof Number) { - int index = ((Number) o).intValue(); + } else if (from instanceof Number) { + int index = ((Number) from).intValue(); if (index < 0 || index >= indexValues.length) { throw new IllegalArgumentException(index + " is not a valid enum index! Valid values are: 0 - " + (indexValues.length - 1)); @@ -81,6 +85,6 @@ public T wrap(Context cx, Object o) { return indexValues[index]; } - return (T) o; + return (T) from; } } diff --git a/src/main/java/dev/latvian/mods/rhino/util/TypeUtils.java b/src/main/java/dev/latvian/mods/rhino/util/TypeUtils.java new file mode 100644 index 00000000..d033a002 --- /dev/null +++ b/src/main/java/dev/latvian/mods/rhino/util/TypeUtils.java @@ -0,0 +1,43 @@ +package dev.latvian.mods.rhino.util; + +import java.lang.reflect.Array; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; + +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; + } else if (type instanceof ParameterizedType paramType) { + var rawType = paramType.getRawType(); + + if (rawType instanceof Class clz) { + return clz; + } + } else if (type instanceof GenericArrayType arrType) { + var componentType = arrType.getGenericComponentType(); + return Array.newInstance(getRawType(componentType), 0).getClass(); + } else if (type instanceof TypeVariable) { + return Object.class; + } else if (type instanceof WildcardType wildcard) { + 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); + } +} diff --git a/src/main/java/dev/latvian/mods/rhino/util/wrap/ArrayTypeWrapperFactory.java b/src/main/java/dev/latvian/mods/rhino/util/wrap/ArrayTypeWrapperFactory.java deleted file mode 100644 index 3fc0230e..00000000 --- a/src/main/java/dev/latvian/mods/rhino/util/wrap/ArrayTypeWrapperFactory.java +++ /dev/null @@ -1,87 +0,0 @@ -package dev.latvian.mods.rhino.util.wrap; - -import dev.latvian.mods.rhino.Context; - -import java.lang.reflect.Array; -import java.util.Arrays; -import java.util.Collection; - -/** - * @author LatvianModder - */ -public class ArrayTypeWrapperFactory implements TypeWrapperFactory { - public final TypeWrapper typeWrapper; - public final Class target; - public final Class arrayTarget; - private final T[] emptyArray; - - @SuppressWarnings("unchecked") - public ArrayTypeWrapperFactory(TypeWrapper tw, Class t, Class at) { - typeWrapper = tw; - target = t; - arrayTarget = at; - emptyArray = (T[]) Array.newInstance(target, 0); - } - - @Override - @SuppressWarnings("all") - public T[] wrap(Context cx, Object o) { - if (o == null) { - return emptyArray; - } else if (o instanceof Iterable) { - int size; - - if (o instanceof Collection) { - size = ((Collection) o).size(); - } else { - size = 0; - - for (Object o1 : (Iterable) o) { - size++; - } - } - - if (size == 0) { - return emptyArray; - } - - T[] array = (T[]) Array.newInstance(target, size); - int index = 0; - - for (Object o1 : (Iterable) o) { - if (typeWrapper.validator.test(o1)) { - array[index] = typeWrapper.factory.wrap(cx, o1); - index++; - } - } - - return index == 0 ? emptyArray : index == array.length ? array : Arrays.copyOf(array, index, arrayTarget); - } else if (o.getClass().isArray()) { - int size = Array.getLength(o); - - if (size == 0) { - return emptyArray; - } - - T[] array = (T[]) Array.newInstance(target, size); - int index = 0; - - for (int i = 0; i < array.length; i++) { - Object o1 = Array.get(o, i); - - if (typeWrapper.validator.test(o1)) { - array[index] = typeWrapper.factory.wrap(cx, o1); - index++; - } - } - - return index == 0 ? emptyArray : index == array.length ? array : Arrays.copyOf(array, index, arrayTarget); - } else if (typeWrapper.validator.test(o)) { - T[] array = (T[]) Array.newInstance(target, 1); - array[0] = typeWrapper.factory.wrap(cx, o); - return array; - } - - return emptyArray; - } -} diff --git a/src/main/java/dev/latvian/mods/rhino/util/wrap/TypeWrapper.java b/src/main/java/dev/latvian/mods/rhino/util/wrap/TypeWrapper.java index fdcda6a1..232b2447 100644 --- a/src/main/java/dev/latvian/mods/rhino/util/wrap/TypeWrapper.java +++ b/src/main/java/dev/latvian/mods/rhino/util/wrap/TypeWrapper.java @@ -1,20 +1,7 @@ package dev.latvian.mods.rhino.util.wrap; -import java.util.function.Predicate; - /** * @author LatvianModder */ -public class TypeWrapper { - public static final Predicate ALWAYS_VALID = o -> true; - - public final Class target; - public final Predicate validator; - public final TypeWrapperFactory factory; - - TypeWrapper(Class t, Predicate v, TypeWrapperFactory f) { - target = t; - validator = v; - factory = f; - } +public record TypeWrapper(Class target, TypeWrapperValidator validator, TypeWrapperFactory factory) { } diff --git a/src/main/java/dev/latvian/mods/rhino/util/wrap/TypeWrapperFactory.java b/src/main/java/dev/latvian/mods/rhino/util/wrap/TypeWrapperFactory.java index a0ce7ed6..9f87d909 100644 --- a/src/main/java/dev/latvian/mods/rhino/util/wrap/TypeWrapperFactory.java +++ b/src/main/java/dev/latvian/mods/rhino/util/wrap/TypeWrapperFactory.java @@ -2,19 +2,12 @@ import dev.latvian.mods.rhino.Context; +import java.lang.reflect.Type; + /** * @author LatvianModder */ @FunctionalInterface public interface TypeWrapperFactory { - interface Simple extends TypeWrapperFactory { - T wrapSimple(Object o); - - @Override - default T wrap(Context cx, Object o) { - return wrapSimple(o); - } - } - - T wrap(Context cx, Object o); + T wrap(Context cx, Object from, Class toType, Type toGenericType); } diff --git a/src/main/java/dev/latvian/mods/rhino/util/wrap/TypeWrapperValidator.java b/src/main/java/dev/latvian/mods/rhino/util/wrap/TypeWrapperValidator.java new file mode 100644 index 00000000..7e4a2fa2 --- /dev/null +++ b/src/main/java/dev/latvian/mods/rhino/util/wrap/TypeWrapperValidator.java @@ -0,0 +1,10 @@ +package dev.latvian.mods.rhino.util.wrap; + +import java.lang.reflect.Type; + +@FunctionalInterface +public interface TypeWrapperValidator { + TypeWrapperValidator ALWAYS_VALID = (from, target, genericTarget) -> true; + + boolean isValid(Object from, Class target, Type genericTarget); +} diff --git a/src/main/java/dev/latvian/mods/rhino/util/wrap/TypeWrappers.java b/src/main/java/dev/latvian/mods/rhino/util/wrap/TypeWrappers.java index 137de2c6..db9f2e8b 100644 --- a/src/main/java/dev/latvian/mods/rhino/util/wrap/TypeWrappers.java +++ b/src/main/java/dev/latvian/mods/rhino/util/wrap/TypeWrappers.java @@ -3,75 +3,55 @@ import dev.latvian.mods.rhino.util.EnumTypeWrapper; import org.jetbrains.annotations.Nullable; -import java.lang.reflect.Array; +import java.lang.reflect.Type; import java.util.LinkedHashMap; import java.util.Map; -import java.util.function.Predicate; /** * @author LatvianModder */ public class TypeWrappers { - private final Map, TypeWrapper> wrappers = new LinkedHashMap<>(); + public final Map, TypeWrapper> wrappers = new LinkedHashMap<>(); - @SuppressWarnings("unchecked") - public void register(Class target, Predicate validator, TypeWrapperFactory factory) { + public void register(Class target, TypeWrapperValidator validator, TypeWrapperFactory factory) { if (target == null || target == Object.class) { throw new IllegalArgumentException("target can't be Object.class!"); } else if (target.isArray()) { throw new IllegalArgumentException("target can't be an array!"); } else if (wrappers.containsKey(target)) { throw new IllegalArgumentException("Wrapper for class " + target.getName() + " already exists!"); + } else { + wrappers.put(target, new TypeWrapper<>(target, validator, factory)); } - - TypeWrapper typeWrapper0 = new TypeWrapper<>(target, validator, factory); - wrappers.put(target, typeWrapper0); - - // I know this looks like cancer but it's actually pretty simple - grab T[].class, register ArrayTypeWrapperFactory - // You may say that it would be better to just implement N-sized array checking directly in java parser, but this is way more efficient - - // 1D - Class target1 = (Class) Array.newInstance(target, 0).getClass(); - TypeWrapper typeWrapper1 = new TypeWrapper<>(target1, validator, new ArrayTypeWrapperFactory<>(typeWrapper0, target, target1)); - wrappers.put(target1, typeWrapper1); - - // 2D - Class target2 = (Class) Array.newInstance(target1, 0).getClass(); - TypeWrapper typeWrapper2 = new TypeWrapper<>(target2, validator, new ArrayTypeWrapperFactory<>(typeWrapper1, target1, target2)); - wrappers.put(target2, typeWrapper2); - - // 3D - Class target3 = (Class) Array.newInstance(target2, 0).getClass(); - TypeWrapper typeWrapper3 = new TypeWrapper<>(target3, validator, new ArrayTypeWrapperFactory<>(typeWrapper2, target2, target3)); - wrappers.put(target3, typeWrapper3); - - // 4D.. yeah no. 3D already is an overkill } public void register(Class target, TypeWrapperFactory factory) { - register(target, TypeWrapper.ALWAYS_VALID, factory); + register(target, TypeWrapperValidator.ALWAYS_VALID, factory); } - public void registerSimple(Class target, Predicate validator, TypeWrapperFactory.Simple factory) { - register(target, validator, factory); - } + public boolean hasWrapper(Object from, Class target, Type genericTarget) { + if (target.isEnum() || target.isRecord()) { + return true; + } - public void registerSimple(Class target, TypeWrapperFactory.Simple factory) { - register(target, TypeWrapper.ALWAYS_VALID, factory); + var wrapper = wrappers.get(target); + return wrapper != null && wrapper.validator().isValid(from, target, genericTarget); } @Nullable - public TypeWrapperFactory getWrapperFactory(Class target, @Nullable Object from) { + public TypeWrapperFactory getWrapperFactory(@Nullable Object from, Class target, Type genericTarget) { if (target == Object.class) { return null; } - TypeWrapper wrapper = wrappers.get(target); + var wrapper = wrappers.get(target); - if (wrapper != null && wrapper.validator.test(from)) { - return wrapper.factory; + if (wrapper != null && wrapper.validator().isValid(from, target, genericTarget)) { + return wrapper.factory(); } else if (target.isEnum()) { return EnumTypeWrapper.get(target); + } else if (target.isRecord()) { + } //else if (from != null && target.isArray() && !from.getClass().isArray() && target.getComponentType() == from.getClass() && !target.isPrimitive()) diff --git a/src/main/resources/rhino.mixins.json b/src/main/resources/rhino.mixins.json index 6ffab0a2..a3d1d0a8 100644 --- a/src/main/resources/rhino.mixins.json +++ b/src/main/resources/rhino.mixins.json @@ -9,6 +9,7 @@ "NumericTagMixin", "ResourceKeyMixin", "ResourceLocationMixin", + "StringRepresentableMixin", "StringTagMixin", "TextColorMixin" ], diff --git a/src/test/java/dev/latvian/mods/rhino/test/MiscTests.java b/src/test/java/dev/latvian/mods/rhino/test/MiscTests.java index f72446f7..82d06049 100644 --- a/src/test/java/dev/latvian/mods/rhino/test/MiscTests.java +++ b/src/test/java/dev/latvian/mods/rhino/test/MiscTests.java @@ -18,7 +18,7 @@ public void testFunctionAssignment() { x.abc = 1; console.info(x.abc); """, - "1.0" + "1" ); } @@ -112,9 +112,9 @@ public void keysValuesEntries() { console.info(Object.values(shared.testObject)) console.info(Object.entries(shared.testObject)) """, """ - [a, b, c] - [-39.0, 2.0, 3439438.0] - [[a, -39.0], [b, 2.0], [c, 3439438.0]] + ['a', 'b', 'c'] + [-39, 2, 3439438] + [['a', -39], ['b', 2], ['c', 3439438]] """); } @@ -147,10 +147,25 @@ public void typeWrappers() { } @Test - public void testJsonStringifyWithNestedArrays() { + public void jsonStringifyWithNestedArrays() { TEST.test("jsonStringifyWithNestedArrays", """ const thing = {nested: [1, 2, 3]}; console.info(JSON.stringify(thing)); """, "{\"nested\":[1.0,2.0,3.0]}"); } + + @Test + public void genericTypes() { + TEST.test("genericTypes", "console.genericType(['a', 'b']);", "Generic type:\n[WithContext[a], WithContext[b]]"); + } + + @Test + public void genericTypesUnwrapped() { + TEST.test("genericTypesUnwrapped", "console.genericType('a');", "Generic type:\n[WithContext[a]]"); + } + + @Test + public void genericTypesFromList() { + TEST.test("genericTypesFromList", "console.genericType('a');", "Generic type:\n[WithContext[a]]"); + } } diff --git a/src/test/java/dev/latvian/mods/rhino/test/NullishCoalescingTests.java b/src/test/java/dev/latvian/mods/rhino/test/NullishCoalescingTests.java index 1cf126a8..20575179 100644 --- a/src/test/java/dev/latvian/mods/rhino/test/NullishCoalescingTests.java +++ b/src/test/java/dev/latvian/mods/rhino/test/NullishCoalescingTests.java @@ -14,7 +14,7 @@ public void bothNonNull() { let c = a ?? b console.info(c) """, """ - 10.0 + 10 """); } @@ -26,7 +26,7 @@ public void firstNull() { let c = a ?? b console.info(c) """, """ - 20.0 + 20 """); } @@ -38,7 +38,7 @@ public void firstUndefined() { let c = a ?? b console.info(c) """, """ - 20.0 + 20 """); } @@ -62,7 +62,7 @@ public void firstZero() { let c = a ?? b console.info(c) """, """ - 0.0 + 0 """); } @@ -74,7 +74,7 @@ public void secondNull() { let c = a ?? b console.info(c) """, """ - 10.0 + 10 """); } } diff --git a/src/test/java/dev/latvian/mods/rhino/test/PowTests.java b/src/test/java/dev/latvian/mods/rhino/test/PowTests.java index 52b208d6..686abd59 100644 --- a/src/test/java/dev/latvian/mods/rhino/test/PowTests.java +++ b/src/test/java/dev/latvian/mods/rhino/test/PowTests.java @@ -14,7 +14,7 @@ public void bothWhole() { let c = a ** b console.info(c) """, """ - 1000.0 + 1000 """); } @@ -50,7 +50,7 @@ public void zeroExponent() { let c = a ** b console.info(c) """, """ - 1.0 + 1 """); } diff --git a/src/test/java/dev/latvian/mods/rhino/test/RhinoTest.java b/src/test/java/dev/latvian/mods/rhino/test/RhinoTest.java index 769ea1c6..b84e8178 100644 --- a/src/test/java/dev/latvian/mods/rhino/test/RhinoTest.java +++ b/src/test/java/dev/latvian/mods/rhino/test/RhinoTest.java @@ -19,7 +19,7 @@ public RhinoTest(String n) { this.shared = new HashMap<>(); var typeWrappers = factory.getTypeWrappers(); - typeWrappers.registerSimple(TestMaterial.class, TestMaterial::get); + typeWrappers.register(TestMaterial.class, (cx, from, target, genericTarget) -> TestMaterial.get(from)); } public void test(String name, String script, String match) { @@ -28,6 +28,7 @@ public void test(String name, String script, String match) { var rootScope = context.initStandardObjects(); context.addToScope(rootScope, "console", console); context.addToScope(rootScope, "shared", shared); + context.addToScope(rootScope, "testName", name); context.evaluateString(rootScope, script, testName + "/" + name, 1, null); } catch (Exception ex) { ex.printStackTrace(); 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 d6e44dee..25a29e64 100644 --- a/src/test/java/dev/latvian/mods/rhino/test/TestConsole.java +++ b/src/test/java/dev/latvian/mods/rhino/test/TestConsole.java @@ -2,6 +2,7 @@ import dev.latvian.mods.rhino.Context; import dev.latvian.mods.rhino.ContextFactory; +import dev.latvian.mods.rhino.ScriptRuntime; import dev.latvian.mods.rhino.util.RemapPrefixForJS; import dev.latvian.mods.unit.UnitContext; @@ -21,7 +22,7 @@ public TestConsole(ContextFactory factory) { } public void info(Object o) { - String s = String.valueOf(o); + String s = ScriptRuntime.toString(factory.enter(), o); StringBuilder builder = new StringBuilder(); @@ -83,4 +84,9 @@ public void printUnit(String input) { public void printMaterial(TestMaterial material) { info("%s#%08x".formatted(material.name(), material.hashCode())); } + + public void genericType(WithContext[] test) { + info("Generic type:"); + info(test); + } } diff --git a/src/test/java/dev/latvian/mods/rhino/test/TestContext.java b/src/test/java/dev/latvian/mods/rhino/test/TestContext.java new file mode 100644 index 00000000..9ffc2f6d --- /dev/null +++ b/src/test/java/dev/latvian/mods/rhino/test/TestContext.java @@ -0,0 +1,40 @@ +package dev.latvian.mods.rhino.test; + +import dev.latvian.mods.rhino.Context; +import dev.latvian.mods.rhino.EvaluatorException; +import dev.latvian.mods.rhino.util.TypeUtils; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +public class TestContext extends Context { + public TestContext(TestContextFactory factory) { + super(factory); + } + + @Override + public int internalConversionWeight(Object fromObj, Class target, Type genericTarget) { + if (target == WithContext.class) { + return CONVERSION_NONTRIVIAL; + } + + return super.internalConversionWeight(fromObj, target, genericTarget); + } + + @Override + protected Object internalJsToJava(Object value, Class target, Type genericTarget) throws EvaluatorException { + if (genericTarget instanceof ParameterizedType parameterizedType) { + if (target == WithContext.class) { + var types = parameterizedType.getActualTypeArguments(); + + if (types.length == 1) { + return new WithContext<>(this, jsToJava(value, TypeUtils.getRawType(types[0]), types[0])); + } + + return new WithContext<>(this, value); + } + } + + return super.internalJsToJava(value, target, genericTarget); + } +} diff --git a/src/test/java/dev/latvian/mods/rhino/test/TestContextFactory.java b/src/test/java/dev/latvian/mods/rhino/test/TestContextFactory.java index d4145742..23e1be39 100644 --- a/src/test/java/dev/latvian/mods/rhino/test/TestContextFactory.java +++ b/src/test/java/dev/latvian/mods/rhino/test/TestContextFactory.java @@ -1,6 +1,11 @@ package dev.latvian.mods.rhino.test; +import dev.latvian.mods.rhino.Context; import dev.latvian.mods.rhino.ContextFactory; public class TestContextFactory extends ContextFactory { + @Override + protected Context createContext() { + return new TestContext(this); + } } diff --git a/src/test/java/dev/latvian/mods/rhino/test/WithContext.java b/src/test/java/dev/latvian/mods/rhino/test/WithContext.java new file mode 100644 index 00000000..e2e73d03 --- /dev/null +++ b/src/test/java/dev/latvian/mods/rhino/test/WithContext.java @@ -0,0 +1,22 @@ +package dev.latvian.mods.rhino.test; + +import dev.latvian.mods.rhino.Context; + +import java.util.Objects; + +public record WithContext(Context cx, T value) { + @Override + public int hashCode() { + return value == null ? 0 : value.hashCode(); + } + + @Override + public boolean equals(Object o) { + return o instanceof WithContext wc && Objects.equals(value, wc.value); + } + + @Override + public String toString() { + return "WithContext[" + value + "]"; + } +}