Skip to content

Commit

Permalink
Add nullable JS API parameter support
Browse files Browse the repository at this point in the history
This change allows to define JS APIs which receive nullable primitive
type parameters. E.g. before the change it was not possible to pass
null to JS function with string parameter, instead the function
received a string "null".

As standard Java doesn't have nullable types, this feature currently
can only be used if the files are written in Kotlin. Kotlin metadata
library is used to detect nullable parameter types.
  • Loading branch information
drubanovich-soti committed Dec 13, 2024
1 parent 9f217f1 commit 7b00220
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 11 deletions.
3 changes: 3 additions & 0 deletions rhino/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ plugins {
id 'rhino.library-conventions'
}

dependencies {
implementation "org.jetbrains.kotlin:kotlin-metadata-jvm:2.1.0"
}

publishing {
publications {
Expand Down
2 changes: 2 additions & 0 deletions rhino/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@
requires java.compiler;
requires jdk.dynalink;
requires transitive java.desktop;
requires kotlin.metadata.jvm;
requires kotlin.stdlib;
}
3 changes: 2 additions & 1 deletion rhino/src/main/java/org/mozilla/javascript/AccessorSlot.java
Original file line number Diff line number Diff line change
Expand Up @@ -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});
Expand Down
29 changes: 20 additions & 9 deletions rhino/src/main/java/org/mozilla/javascript/FunctionObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -321,15 +330,15 @@ 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) {
int tag = getTypeTag(desired);
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);
}

/**
Expand Down Expand Up @@ -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();
Expand All @@ -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]);
}
}

Expand Down
57 changes: 56 additions & 1 deletion rhino/src/main/java/org/mozilla/javascript/MemberBox.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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;
Expand All @@ -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<KmFunction> function =
clazz.getFunctions().stream()
.filter(f -> f.getName().equals(methodName))
.findFirst();
if (function.isPresent()) {
List<KmValueParameter> 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();
}

Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit 7b00220

Please sign in to comment.