diff --git a/rhino/build.gradle b/rhino/build.gradle index 9cab2d4527..92d76d949f 100644 --- a/rhino/build.gradle +++ b/rhino/build.gradle @@ -2,6 +2,9 @@ plugins { id 'rhino.library-conventions' } +dependencies { + implementation "org.jetbrains.kotlin:kotlin-metadata-jvm:2.1.0" +} publishing { publications { diff --git a/rhino/src/main/java/module-info.java b/rhino/src/main/java/module-info.java index 90cad354c6..642727d763 100644 --- a/rhino/src/main/java/module-info.java +++ b/rhino/src/main/java/module-info.java @@ -14,4 +14,6 @@ requires java.compiler; requires jdk.dynalink; requires transitive java.desktop; + requires kotlin.metadata.jvm; + requires kotlin.stdlib; } diff --git a/rhino/src/main/java/org/mozilla/javascript/AccessorSlot.java b/rhino/src/main/java/org/mozilla/javascript/AccessorSlot.java index 02f26b2f6a..f8607967bc 100644 --- a/rhino/src/main/java/org/mozilla/javascript/AccessorSlot.java +++ b/rhino/src/main/java/org/mozilla/javascript/AccessorSlot.java @@ -235,8 +235,9 @@ public boolean setValue(Object value, Scriptable owner, Scriptable start) { // XXX: cache tag since it is already calculated in // defineProperty ? Class valueType = pTypes[pTypes.length - 1]; + boolean isNullable = member.argNullability[pTypes.length - 1]; int tag = FunctionObject.getTypeTag(valueType); - Object actualArg = FunctionObject.convertArg(cx, start, value, tag); + Object actualArg = FunctionObject.convertArg(cx, start, value, tag, isNullable); if (member.delegateTo == null) { member.invoke(start, new Object[] {actualArg}); diff --git a/rhino/src/main/java/org/mozilla/javascript/FunctionObject.java b/rhino/src/main/java/org/mozilla/javascript/FunctionObject.java index e5fa959d15..e297ac5653 100644 --- a/rhino/src/main/java/org/mozilla/javascript/FunctionObject.java +++ b/rhino/src/main/java/org/mozilla/javascript/FunctionObject.java @@ -163,20 +163,29 @@ public static int getTypeTag(Class type) { return JAVA_UNSUPPORTED_TYPE; } - public static Object convertArg(Context cx, Scriptable scope, Object arg, int typeTag) { + public static Object convertArg( + Context cx, Scriptable scope, Object arg, int typeTag, boolean isNullable) { switch (typeTag) { case JAVA_STRING_TYPE: if (arg instanceof String) return arg; - return ScriptRuntime.toString(arg); + return (arg == null && isNullable) ? null : ScriptRuntime.toString(arg); case JAVA_INT_TYPE: if (arg instanceof Integer) return arg; - return Integer.valueOf(ScriptRuntime.toInt32(arg)); + return (arg == null && isNullable) + ? null + : Integer.valueOf(ScriptRuntime.toInt32(arg)); case JAVA_BOOLEAN_TYPE: if (arg instanceof Boolean) return arg; - return ScriptRuntime.toBoolean(arg) ? Boolean.TRUE : Boolean.FALSE; + if (arg == null && isNullable) { + return null; + } else { + return ScriptRuntime.toBoolean(arg) ? Boolean.TRUE : Boolean.FALSE; + } case JAVA_DOUBLE_TYPE: if (arg instanceof Double) return arg; - return Double.valueOf(ScriptRuntime.toNumber(arg)); + return (arg == null && isNullable) + ? null + : Double.valueOf(ScriptRuntime.toNumber(arg)); case JAVA_SCRIPTABLE_TYPE: return ScriptRuntime.toObjectOrNull(cx, arg, scope); case JAVA_OBJECT_TYPE: @@ -321,7 +330,7 @@ void initAsConstructor(Scriptable scope, Scriptable prototype, int attributes) { /** * @deprecated Use {@link #getTypeTag(Class)} and {@link #convertArg(Context, Scriptable, - * Object, int)} for type conversion. + * Object, int, boolean)} for type conversion. */ @Deprecated public static Object convertArg(Context cx, Scriptable scope, Object arg, Class desired) { @@ -329,7 +338,7 @@ public static Object convertArg(Context cx, Scriptable scope, Object arg, Class< if (tag == JAVA_UNSUPPORTED_TYPE) { throw Context.reportRuntimeErrorById("msg.cant.convert", desired.getName()); } - return convertArg(cx, scope, arg, tag); + return convertArg(cx, scope, arg, tag, false); } /** @@ -401,7 +410,8 @@ public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] ar invokeArgs = args; for (int i = 0; i != parmsLength; ++i) { Object arg = args[i]; - Object converted = convertArg(cx, scope, arg, typeTags[i]); + Object converted = + convertArg(cx, scope, arg, typeTags[i], member.argNullability[i]); if (arg != converted) { if (invokeArgs == args) { invokeArgs = args.clone(); @@ -415,7 +425,8 @@ public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] ar invokeArgs = new Object[parmsLength]; for (int i = 0; i != parmsLength; ++i) { Object arg = (i < argsLength) ? args[i] : Undefined.instance; - invokeArgs[i] = convertArg(cx, scope, arg, typeTags[i]); + invokeArgs[i] = + convertArg(cx, scope, arg, typeTags[i], member.argNullability[i]); } } diff --git a/rhino/src/main/java/org/mozilla/javascript/MemberBox.java b/rhino/src/main/java/org/mozilla/javascript/MemberBox.java index 3035950a2b..6ccc3093ff 100644 --- a/rhino/src/main/java/org/mozilla/javascript/MemberBox.java +++ b/rhino/src/main/java/org/mozilla/javascript/MemberBox.java @@ -6,6 +6,8 @@ package org.mozilla.javascript; +import static kotlin.metadata.Attributes.isNullable; + import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; @@ -15,6 +17,13 @@ import java.lang.reflect.Member; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.util.List; +import java.util.Optional; +import kotlin.Metadata; +import kotlin.metadata.KmClass; +import kotlin.metadata.KmFunction; +import kotlin.metadata.KmValueParameter; +import kotlin.metadata.jvm.KotlinClassMetadata; /** * Wrapper class for Method and Constructor instances to cache getParameterTypes() results, recover @@ -27,6 +36,7 @@ final class MemberBox implements Serializable { private transient Member memberObject; transient Class[] argTypes; + transient boolean[] argNullability; transient boolean vararg; transient Function asGetterFunction; @@ -44,12 +54,56 @@ final class MemberBox implements Serializable { private void init(Method method) { this.memberObject = method; this.argTypes = method.getParameterTypes(); + this.argNullability = getParameterNullability(method); this.vararg = method.isVarArgs(); } + private boolean[] getParameterNullability(Method method) { + boolean[] result = new boolean[method.getParameters().length]; + Metadata metadata = method.getDeclaringClass().getAnnotation(Metadata.class); + if (metadata != null) { + result = getParameterNullabilityFromKotlinMetadata(metadata, method.getName(), result); + } + return result; + } + + private boolean[] getParameterNullability(Constructor constructor) { + boolean[] result = new boolean[constructor.getParameters().length]; + Metadata metadata = constructor.getDeclaringClass().getAnnotation(Metadata.class); + if (metadata != null) { + result = + getParameterNullabilityFromKotlinMetadata( + metadata, constructor.getName(), result); + } + return result; + } + + private boolean[] getParameterNullabilityFromKotlinMetadata( + Metadata metadata, String methodName, boolean[] fallback) { + KotlinClassMetadata.Class kMetadata = + (KotlinClassMetadata.Class) KotlinClassMetadata.readLenient(metadata); + KmClass clazz = kMetadata.getKmClass(); + Optional function = + clazz.getFunctions().stream() + .filter(f -> f.getName().equals(methodName)) + .findFirst(); + if (function.isPresent()) { + List params = function.get().getValueParameters(); + boolean[] result = new boolean[params.size()]; + int index = 0; + for (KmValueParameter parameter : params) { + result[index++] = isNullable(parameter.getType()); + } + return result; + } else { + return fallback; + } + } + private void init(Constructor constructor) { this.memberObject = constructor; this.argTypes = constructor.getParameterTypes(); + this.argNullability = getParameterNullability(constructor); this.vararg = constructor.isVarArgs(); } @@ -184,7 +238,8 @@ public Object call( thisObj, originalArgs[0], FunctionObject.getTypeTag( - nativeSetter.argTypes[0])) + nativeSetter.argTypes[0]), + nativeSetter.argNullability[0]) : Undefined.instance; if (nativeSetter.delegateTo == null) { setterThis = thisObj;