From 07492b5f4c12bdb5a6fd6888f36133fe582677ec Mon Sep 17 00:00:00 2001 From: drubanovich-soti <76963728+drubanovich-soti@users.noreply.github.com> Date: Fri, 31 Jan 2025 01:31:21 -0500 Subject: [PATCH] Add nullable JS API parameter support This change allows to define JS APIs which receive nullable primitive type parameters, by overriding NullabilityDetector interface. E.g. clients can introduce Nullable-like annotation which they can read at runtime, thus enabling passing null to JS function with string parameter (by default such function receives a string "null") One of the following commits will introduce KotlinNullabilityDetector, which will rely on parsing Kotlin metadata and will allow a better inter-operation of Rhino with Kotlin. KotlinNullabilityDetector relies on Kotlin Metadata to detect parameter nullability. The limitation of this detector is that it cannot distinguish between overloads with the same number of parameters. To avoid false positives, KotlinNullabilityDetector returns array of 'false' values for methods or constructors with such overloads. --- README.md | 4 +- rhino-kotlin/README.md | 44 +++++++++ rhino-kotlin/build.gradle | 48 ++++++++++ rhino-kotlin/src/main/java/module-info.java | 11 +++ .../kotlin/KotlinNullabilityDetector.java | 89 +++++++++++++++++++ ...org.mozilla.javascript.NullabilityDetector | 1 + .../java/org/mozilla/kotlin/JavaClass.java | 15 ++++ .../java/org/mozilla/kotlin/KotlinClass.kt | 14 +++ .../KotlinClassWithOverloadedFunction.kt | 11 +++ .../kotlin/KotlinNullabilityDetectorTest.java | 89 +++++++++++++++++++ rhino/src/main/java/module-info.java | 2 + .../org/mozilla/javascript/AccessorSlot.java | 3 +- .../mozilla/javascript/FunctionObject.java | 31 +++++-- .../org/mozilla/javascript/MemberBox.java | 16 +++- .../javascript/NullabilityDetector.java | 10 +++ .../javascript/NullabilityDetectorTest.java | 61 +++++++++++++ .../NullabilityDetectorTestClass.java | 27 ++++++ .../NullableArgumentsConversionTest.java | 72 +++++++++++++++ .../javascript/TestNullabilityDetector.java | 27 ++++++ ...org.mozilla.javascript.NullabilityDetector | 1 + settings.gradle | 5 ++ 21 files changed, 570 insertions(+), 11 deletions(-) create mode 100644 rhino-kotlin/README.md create mode 100644 rhino-kotlin/build.gradle create mode 100644 rhino-kotlin/src/main/java/module-info.java create mode 100644 rhino-kotlin/src/main/java/org/mozilla/kotlin/KotlinNullabilityDetector.java create mode 100644 rhino-kotlin/src/main/resources/META-INF/services/org.mozilla.javascript.NullabilityDetector create mode 100644 rhino-kotlin/src/test/java/org/mozilla/kotlin/JavaClass.java create mode 100644 rhino-kotlin/src/test/java/org/mozilla/kotlin/KotlinClass.kt create mode 100644 rhino-kotlin/src/test/java/org/mozilla/kotlin/KotlinClassWithOverloadedFunction.kt create mode 100644 rhino-kotlin/src/test/java/org/mozilla/kotlin/KotlinNullabilityDetectorTest.java create mode 100644 rhino/src/main/java/org/mozilla/javascript/NullabilityDetector.java create mode 100644 rhino/src/test/java/org/mozilla/javascript/NullabilityDetectorTest.java create mode 100644 rhino/src/test/java/org/mozilla/javascript/NullabilityDetectorTestClass.java create mode 100644 rhino/src/test/java/org/mozilla/javascript/NullableArgumentsConversionTest.java create mode 100644 rhino/src/test/java/org/mozilla/javascript/TestNullabilityDetector.java create mode 100644 rhino/src/test/resources/META-INF/services/org.mozilla.javascript.NullabilityDetector diff --git a/README.md b/README.md index b234c16fe9..bb05c1ffc9 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ JavaDoc for all the APIs: Rhino 1.7.15 and before were primarily used in a single JAR called "rhino.jar". -Newer releases now organize the code using Java modules. There are four primary modules: +Newer releases now organize the code using Java modules. There are four primary modules and one auxiliary module for Kotlin developers: * **rhino**: The primary codebase necessary and sufficient to run JavaScript code. Required by everything that uses Rhino. In releases *after* 1.7.15, this module does not contain the "tools" or the XML implementation. * **rhino-tools**: Contains the shell, debugger, and the "Global" object, which many tests and other Rhino-based tools use. Note that adding Global gives Rhino the ability to print to stdout, open files, and do other things that may be considered dangerous in a sensitive environment, so it only makes sense to include if you will use it. @@ -58,6 +58,8 @@ Newer releases now organize the code using Java modules. There are four primary * **rhino-engine**: Adds the Rhino implementation of the standard Java *ScriptEngine* interface. Some projects use this to be able to switch between script execution engines, but for anything even moderately complex it is almost always easier and always more flexible to use Rhino's API directly. * **rhino-all**: This creates an "all-in-one" JAR that includes *rhino-runtime*, *rhino-tools*, and *rhino-xml*. This is what's used if you want to run Rhino using "java jar". +* **rhino-kotlin**: Enhanced support for code written in Kotlin, [see the details.](./rhino-kotlin/README.md) + The release contains the following other modules, which are used while building and testing but which are not published to Maven Central: diff --git a/rhino-kotlin/README.md b/rhino-kotlin/README.md new file mode 100644 index 0000000000..a909cfdcd0 --- /dev/null +++ b/rhino-kotlin/README.md @@ -0,0 +1,44 @@ +# Kotlin Support + +The rhino-kotlin module uses Kotlin metadata to augment function and property information, +making JavaScript APIs more nuanced. + +For example, the following code exposes setValue JavaScript function: +``` +defineProperty( + topLevelScope, + "setValue", + Bundle::class.java.methods.find { + it.name == "setValue" + }, + DONT_ENUM +) +``` +which is backed by the following Kotlin class: +``` +class Bundle() : ScriptableObject { + val valueMap = emptyMap() + + fun setValue(key: String, value: String?) { + valueMap[key] = value + } +} +``` +Imagine rhino-kotlin is not used and JavaScript code tries to call setValue with `null` value parameter: +``` +setValue("key", null) +``` +This will lead to unexpected result - a 4-char long string "null" will be inserted into the backing map. +This happens because Rhino engine doesn't know how to infer parameter nullability from pure Java code, +so it tries to convert `null` to String. + +Adding rhino-kotlin dependency fixes the problem, allowing JavaScript functions to have nullable parameters. + +At the moment, only parameter nullability is supported, but we might add more Kotlin-specific support in future. + +**Note:** If building for Android, make sure the kotlin.Metadata class is not obfuscated by adding the following line +into the proguard configuration: +``` +-keep class kotlin.Metadata +``` +Without this line Rhino Kotlin support won't work in release apk. diff --git a/rhino-kotlin/build.gradle b/rhino-kotlin/build.gradle new file mode 100644 index 0000000000..d09d38673f --- /dev/null +++ b/rhino-kotlin/build.gradle @@ -0,0 +1,48 @@ +plugins { + id 'rhino.library-conventions' + id 'org.jetbrains.kotlin.jvm' +} + +dependencies { + implementation project(':rhino') + implementation "org.jetbrains.kotlin:kotlin-metadata-jvm:2.1.0" + + testImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.1.0" +} + +kotlin { + jvmToolchain(11) +} + +publishing { + publications { + rhinokotlin(MavenPublication) { + from components.java + artifacts = [jar, sourceJar] + pom.withXml { + def root = asNode() + + root.appendNode('description', "Rhino reflection interfaces implementations for Kotlin") + root.appendNode("url", "https://mozilla.github.io/rhino/") + + def p = root.appendNode("parent") + p.appendNode("groupId", "org.sonatype.oss") + p.appendNode("artifactId", "oss-parent") + p.appendNode("version", "7") + + def l = root.appendNode("licenses").appendNode("license") + l.appendNode("name", "Mozilla Public License, Version 2.0") + l.appendNode("url", "http://www.mozilla.org/MPL/2.0/index.txt") + + def scm = root.appendNode("scm") + scm.appendNode("connection", "scm:git:git@github.com:mozilla/rhino.git") + scm.appendNode("developerConnection", "scm:git:git@github.com:mozilla/rhino.git") + scm.appendNode("url", "git@github.com:mozilla/rhino.git") + + def o = root.appendNode("organization") + o.appendNode("name", "The Mozilla Foundation") + o.appendNode("url", "http://www.mozilla.org") + } + } + } +} diff --git a/rhino-kotlin/src/main/java/module-info.java b/rhino-kotlin/src/main/java/module-info.java new file mode 100644 index 0000000000..a95cd913f8 --- /dev/null +++ b/rhino-kotlin/src/main/java/module-info.java @@ -0,0 +1,11 @@ +import org.mozilla.javascript.NullabilityDetector; +import org.mozilla.kotlin.KotlinNullabilityDetector; + +module org.mozilla.rhino.kotlin { + requires kotlin.metadata.jvm; + requires kotlin.stdlib; + requires org.mozilla.rhino; + + provides NullabilityDetector with + KotlinNullabilityDetector; +} diff --git a/rhino-kotlin/src/main/java/org/mozilla/kotlin/KotlinNullabilityDetector.java b/rhino-kotlin/src/main/java/org/mozilla/kotlin/KotlinNullabilityDetector.java new file mode 100644 index 0000000000..a6df5d24f7 --- /dev/null +++ b/rhino-kotlin/src/main/java/org/mozilla/kotlin/KotlinNullabilityDetector.java @@ -0,0 +1,89 @@ +package org.mozilla.kotlin; + +import static kotlin.metadata.Attributes.isNullable; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.List; +import java.util.stream.Collectors; +import kotlin.Metadata; +import kotlin.metadata.KmClass; +import kotlin.metadata.KmConstructor; +import kotlin.metadata.KmFunction; +import kotlin.metadata.KmValueParameter; +import kotlin.metadata.jvm.KotlinClassMetadata; +import org.mozilla.javascript.NullabilityDetector; + +public class KotlinNullabilityDetector implements NullabilityDetector { + @Override + public boolean[] getParameterNullability(Method method) { + int paramCount = method.getParameterTypes().length; + KmClass kmClass = getKmClassForJavaClass(method.getDeclaringClass()); + return getMethodParameterNullabilityFromKotlinMetadata( + kmClass, method.getName(), paramCount); + } + + @Override + public boolean[] getParameterNullability(Constructor constructor) { + int paramCount = constructor.getParameterTypes().length; + KmClass kmClass = getKmClassForJavaClass(constructor.getDeclaringClass()); + return getConstructorParameterNullabilityFromKotlinMetadata(kmClass, paramCount); + } + + private KmClass getKmClassForJavaClass(Class javaClass) { + Metadata metadata = javaClass.getAnnotation(Metadata.class); + if (metadata != null) { + KotlinClassMetadata.Class kMetadata = + (KotlinClassMetadata.Class) KotlinClassMetadata.readLenient(metadata); + return kMetadata.getKmClass(); + } else { + return null; + } + } + + private boolean[] getMethodParameterNullabilityFromKotlinMetadata( + KmClass clazz, String methodName, int paramCount) { + boolean[] fallback = createFallbackNullabilityArray(paramCount); + if (clazz == null) { + return fallback; + } + List candidates = + clazz.getFunctions().stream() + .filter( + f -> + f.getName().equals(methodName) + && f.getValueParameters().size() == paramCount) + .collect(Collectors.toList()); + return candidates.size() == 1 + ? createNullabilityArray(candidates.get(0).getValueParameters()) + : fallback; + } + + private boolean[] getConstructorParameterNullabilityFromKotlinMetadata( + KmClass clazz, int paramCount) { + boolean[] fallback = createFallbackNullabilityArray(paramCount); + if (clazz == null) { + return fallback; + } + List candidates = + clazz.getConstructors().stream() + .filter(c -> c.getValueParameters().size() == paramCount) + .collect(Collectors.toList()); + return candidates.size() == 1 + ? createNullabilityArray(candidates.get(0).getValueParameters()) + : fallback; + } + + private boolean[] createNullabilityArray(List params) { + boolean[] result = new boolean[params.size()]; + int index = 0; + for (KmValueParameter parameter : params) { + result[index++] = isNullable(parameter.getType()); + } + return result; + } + + private boolean[] createFallbackNullabilityArray(int paramCount) { + return new boolean[paramCount]; + } +} diff --git a/rhino-kotlin/src/main/resources/META-INF/services/org.mozilla.javascript.NullabilityDetector b/rhino-kotlin/src/main/resources/META-INF/services/org.mozilla.javascript.NullabilityDetector new file mode 100644 index 0000000000..ed01479b13 --- /dev/null +++ b/rhino-kotlin/src/main/resources/META-INF/services/org.mozilla.javascript.NullabilityDetector @@ -0,0 +1 @@ +org.mozilla.kotlin.KotlinNullabilityDetector diff --git a/rhino-kotlin/src/test/java/org/mozilla/kotlin/JavaClass.java b/rhino-kotlin/src/test/java/org/mozilla/kotlin/JavaClass.java new file mode 100644 index 0000000000..d99a813e7e --- /dev/null +++ b/rhino-kotlin/src/test/java/org/mozilla/kotlin/JavaClass.java @@ -0,0 +1,15 @@ +package org.mozilla.kotlin; + +public class JavaClass { + private final String property1; + private final String property2; + + public JavaClass(String param1, String param2) { + property1 = param1; + property2 = param2; + } + + public void function(Integer param1, Long param2) { + // Do nothing + } +} diff --git a/rhino-kotlin/src/test/java/org/mozilla/kotlin/KotlinClass.kt b/rhino-kotlin/src/test/java/org/mozilla/kotlin/KotlinClass.kt new file mode 100644 index 0000000000..15f410d87e --- /dev/null +++ b/rhino-kotlin/src/test/java/org/mozilla/kotlin/KotlinClass.kt @@ -0,0 +1,14 @@ +package org.mozilla.kotlin + +class KotlinClass( + val nonNullProperty: String, + val nullableProperty: String? +) { + fun function(nullableParam: Int?, nonNullParam: Int, anotherNullableParam: KotlinClass?) { + // Do nothing + } + + fun function(nonNullParam: Int, anotherNonNullParam: Int) { + // Do nothing + } +} diff --git a/rhino-kotlin/src/test/java/org/mozilla/kotlin/KotlinClassWithOverloadedFunction.kt b/rhino-kotlin/src/test/java/org/mozilla/kotlin/KotlinClassWithOverloadedFunction.kt new file mode 100644 index 0000000000..66429fde99 --- /dev/null +++ b/rhino-kotlin/src/test/java/org/mozilla/kotlin/KotlinClassWithOverloadedFunction.kt @@ -0,0 +1,11 @@ +package org.mozilla.kotlin + +class KotlinClassWithOverloadedFunction { + fun function(nullableParam: Int?, nonNullParam: Int, anotherNullableParam: KotlinClass?) { + // Do nothing + } + + fun function(nullableParam: Int?, nonNullParam: Int, anotherNonNullParam: String) { + // Do nothing + } +} diff --git a/rhino-kotlin/src/test/java/org/mozilla/kotlin/KotlinNullabilityDetectorTest.java b/rhino-kotlin/src/test/java/org/mozilla/kotlin/KotlinNullabilityDetectorTest.java new file mode 100644 index 0000000000..754760f197 --- /dev/null +++ b/rhino-kotlin/src/test/java/org/mozilla/kotlin/KotlinNullabilityDetectorTest.java @@ -0,0 +1,89 @@ +package org.mozilla.kotlin; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.Test; + +public class KotlinNullabilityDetectorTest { + private final KotlinNullabilityDetector detector = new KotlinNullabilityDetector(); + + @Test + public void testKotlinFunction() { + boolean[] nullability = + detector.getParameterNullability(findMethod(KotlinClass.class, "function", 3)); + + boolean[] expectedNullability = new boolean[] {true, false, true}; + assertThat(nullability, is(expectedNullability)); + } + + @Test + public void testKotlinConstructor() { + boolean[] nullability = + detector.getParameterNullability(findConstructor(KotlinClass.class, 2)); + + boolean[] expectedNullability = new boolean[] {false, true}; + assertThat(nullability, is(expectedNullability)); + } + + @Test + public void testJavaFunction() { + boolean[] nullability = + detector.getParameterNullability(findMethod(JavaClass.class, "function", 2)); + + boolean[] expectedNullability = new boolean[] {false, false}; + assertThat(nullability, is(expectedNullability)); + } + + @Test + public void testJavaConstructor() { + boolean[] nullability = + detector.getParameterNullability(findConstructor(JavaClass.class, 2)); + + boolean[] expectedNullability = new boolean[] {false, false}; + assertThat(nullability, is(expectedNullability)); + } + + @Test + public void testKotlinOverloadedFunction() { + List overloadedMethods = + findMethods(KotlinClassWithOverloadedFunction.class, "function"); + + assertThat(overloadedMethods.size(), is(2)); + // Since we cannot distinguish overloads with same number of params, we have to fallback to + // no-op + boolean[] expectedNullability = new boolean[] {false, false, false}; + overloadedMethods.forEach( + method -> + assertThat( + detector.getParameterNullability(method), is(expectedNullability))); + } + + private Method findMethod(Class clazz, String methodName, int paramCount) { + return Arrays.stream(clazz.getMethods()) + .filter( + method -> + method.getName().equals(methodName) + && method.getParameterTypes().length == paramCount) + .findFirst() + .orElse(null); + } + + private Constructor findConstructor(Class clazz, int paramCount) { + return Arrays.stream(clazz.getConstructors()) + .filter(constructor -> constructor.getParameterTypes().length == paramCount) + .findFirst() + .orElse(null); + } + + private List findMethods(Class clazz, String methodName) { + return Arrays.stream(clazz.getMethods()) + .filter(method -> method.getName().equals(methodName)) + .collect(Collectors.toList()); + } +} diff --git a/rhino/src/main/java/module-info.java b/rhino/src/main/java/module-info.java index 44383d7437..32b36988d2 100644 --- a/rhino/src/main/java/module-info.java +++ b/rhino/src/main/java/module-info.java @@ -1,4 +1,6 @@ module org.mozilla.rhino { + uses org.mozilla.javascript.NullabilityDetector; + exports org.mozilla.classfile; exports org.mozilla.javascript; exports org.mozilla.javascript.annotations; 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..b63710e2ef 100644 --- a/rhino/src/main/java/org/mozilla/javascript/FunctionObject.java +++ b/rhino/src/main/java/org/mozilla/javascript/FunctionObject.java @@ -164,19 +164,32 @@ public static int getTypeTag(Class type) { } public static Object convertArg(Context cx, Scriptable scope, Object arg, int typeTag) { + return convertArg(cx, scope, arg, typeTag, false); + } + + 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 +334,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 +342,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 +414,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 +429,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..34b9773783 100644 --- a/rhino/src/main/java/org/mozilla/javascript/MemberBox.java +++ b/rhino/src/main/java/org/mozilla/javascript/MemberBox.java @@ -27,6 +27,7 @@ final class MemberBox implements Serializable { private transient Member memberObject; transient Class[] argTypes; + transient boolean[] argNullability; transient boolean vararg; transient Function asGetterFunction; @@ -44,12 +45,24 @@ final class MemberBox implements Serializable { private void init(Method method) { this.memberObject = method; this.argTypes = method.getParameterTypes(); + NullabilityDetector detector = + ScriptRuntime.loadOneServiceImplementation(NullabilityDetector.class); + this.argNullability = + detector == null + ? new boolean[method.getParameters().length] + : detector.getParameterNullability(method); this.vararg = method.isVarArgs(); } private void init(Constructor constructor) { this.memberObject = constructor; this.argTypes = constructor.getParameterTypes(); + NullabilityDetector detector = + ScriptRuntime.loadOneServiceImplementation(NullabilityDetector.class); + this.argNullability = + detector == null + ? new boolean[constructor.getParameters().length] + : detector.getParameterNullability(constructor); this.vararg = constructor.isVarArgs(); } @@ -184,7 +197,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; diff --git a/rhino/src/main/java/org/mozilla/javascript/NullabilityDetector.java b/rhino/src/main/java/org/mozilla/javascript/NullabilityDetector.java new file mode 100644 index 0000000000..f649b0449a --- /dev/null +++ b/rhino/src/main/java/org/mozilla/javascript/NullabilityDetector.java @@ -0,0 +1,10 @@ +package org.mozilla.javascript; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +public interface NullabilityDetector { + boolean[] getParameterNullability(Method method); + + boolean[] getParameterNullability(Constructor constructor); +} diff --git a/rhino/src/test/java/org/mozilla/javascript/NullabilityDetectorTest.java b/rhino/src/test/java/org/mozilla/javascript/NullabilityDetectorTest.java new file mode 100644 index 0000000000..4b905de5f6 --- /dev/null +++ b/rhino/src/test/java/org/mozilla/javascript/NullabilityDetectorTest.java @@ -0,0 +1,61 @@ +package org.mozilla.javascript; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.Arrays; +import org.junit.Test; + +public class NullabilityDetectorTest { + @Test + public void testNullableDetectorForMethodWithoutArgs() { + MemberBox memberBox = new MemberBox(getTestClassMethod("function1")); + assertThat(memberBox.argNullability, is(new boolean[] {})); + } + + @Test + public void testNullableDetectorForMethodWithOneArg() { + MemberBox memberBox = new MemberBox(getTestClassMethod("function2")); + assertThat(memberBox.argNullability, is(new boolean[] {true})); + } + + @Test + public void testNullableDetectorForMethodWithSeveralArgs() { + MemberBox memberBox = new MemberBox(getTestClassMethod("function3")); + assertThat(memberBox.argNullability, is(new boolean[] {true, true, true, true})); + } + + @Test + public void testNullableDetectorForConstructorWithoutArgs() { + MemberBox memberBox = new MemberBox(getTestClassConstructor(0)); + assertThat(memberBox.argNullability, is(new boolean[] {})); + } + + @Test + public void testNullableDetectorForConstructorWithOneArg() { + MemberBox memberBox = new MemberBox(getTestClassConstructor(1)); + assertThat(memberBox.argNullability, is(new boolean[] {true})); + } + + @Test + public void testNullableDetectorForConstructorWithSeveralArgs() { + MemberBox memberBox = new MemberBox(getTestClassConstructor(4)); + assertThat(memberBox.argNullability, is(new boolean[] {true, false, true, false})); + } + + private Method getTestClassMethod(String methodName) { + return Arrays.stream(NullabilityDetectorTestClass.class.getMethods()) + .filter(method -> method.getName().equals(methodName)) + .findFirst() + .orElse(null); + } + + private Constructor getTestClassConstructor(int paramCount) { + return Arrays.stream(NullabilityDetectorTestClass.class.getConstructors()) + .filter(constructor -> constructor.getParameterTypes().length == paramCount) + .findFirst() + .orElse(null); + } +} diff --git a/rhino/src/test/java/org/mozilla/javascript/NullabilityDetectorTestClass.java b/rhino/src/test/java/org/mozilla/javascript/NullabilityDetectorTestClass.java new file mode 100644 index 0000000000..02094e2754 --- /dev/null +++ b/rhino/src/test/java/org/mozilla/javascript/NullabilityDetectorTestClass.java @@ -0,0 +1,27 @@ +package org.mozilla.javascript; + +class NullabilityDetectorTestClass { + public NullabilityDetectorTestClass() { + // Do nothing + } + + public NullabilityDetectorTestClass(Integer arg) { + // Do nothing + } + + public NullabilityDetectorTestClass(Integer arg1, String arg2, Object arg3, Long arg4) { + // Do nothing + } + + public void function1() { + // Do nothing + } + + public void function2(Integer arg) { + // Do nothing + } + + public void function3(Integer arg1, String arg2, Object arg3, Long arg4) { + // Do nothing + } +} diff --git a/rhino/src/test/java/org/mozilla/javascript/NullableArgumentsConversionTest.java b/rhino/src/test/java/org/mozilla/javascript/NullableArgumentsConversionTest.java new file mode 100644 index 0000000000..28362a3a7f --- /dev/null +++ b/rhino/src/test/java/org/mozilla/javascript/NullableArgumentsConversionTest.java @@ -0,0 +1,72 @@ +package org.mozilla.javascript; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mozilla.javascript.FunctionObject.*; + +import java.util.Arrays; +import java.util.Collection; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.mozilla.javascript.tests.Utils; + +@RunWith(Parameterized.class) +public class NullableArgumentsConversionTest { + + @Parameterized.Parameters + public static Collection data() { + return Arrays.asList( + new Object[][] { + {"string", JAVA_STRING_TYPE, false, "string"}, + {"string", JAVA_STRING_TYPE, true, "string"}, + {null, JAVA_STRING_TYPE, false, "null"}, + {null, JAVA_STRING_TYPE, true, null}, + {2, JAVA_INT_TYPE, false, 2}, + {2, JAVA_INT_TYPE, true, 2}, + {null, JAVA_INT_TYPE, false, 0}, + {null, JAVA_INT_TYPE, true, null}, + {2.0, JAVA_DOUBLE_TYPE, false, 2.0}, + {2.0, JAVA_DOUBLE_TYPE, true, 2.0}, + {null, JAVA_DOUBLE_TYPE, false, 0.0}, + {null, JAVA_DOUBLE_TYPE, true, null}, + {true, JAVA_BOOLEAN_TYPE, false, true}, + {true, JAVA_BOOLEAN_TYPE, true, true}, + {null, JAVA_BOOLEAN_TYPE, false, false}, + {null, JAVA_BOOLEAN_TYPE, true, null} + }); + } + + private final Object arg; + private final int typeTag; + private final boolean isNullable; + private final Object expectedConvertedArg; + + public NullableArgumentsConversionTest( + Object arg, int typeTag, boolean isNullable, Object expectedConvertedArg) { + this.arg = arg; + this.typeTag = typeTag; + this.isNullable = isNullable; + this.expectedConvertedArg = expectedConvertedArg; + } + + @Test + public void checkArgumentConversion() { + Utils.runWithAllModes( + context -> { + Scriptable scriptable = + new ScriptableObject() { + @Override + public String getClassName() { + return ""; + } + }; + Object convertedArg = + FunctionObject.convertArg( + context, scriptable, arg, typeTag, isNullable); + + assertThat(convertedArg, is(expectedConvertedArg)); + return null; + }); + } +} diff --git a/rhino/src/test/java/org/mozilla/javascript/TestNullabilityDetector.java b/rhino/src/test/java/org/mozilla/javascript/TestNullabilityDetector.java new file mode 100644 index 0000000000..592861d7d2 --- /dev/null +++ b/rhino/src/test/java/org/mozilla/javascript/TestNullabilityDetector.java @@ -0,0 +1,27 @@ +package org.mozilla.javascript; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.Arrays; + +public class TestNullabilityDetector implements NullabilityDetector { + @Override + public boolean[] getParameterNullability(Method method) { + int paramCount = method.getParameters().length; + boolean[] result = new boolean[paramCount]; + // All arguments are nullable + Arrays.fill(result, true); + return result; + } + + @Override + public boolean[] getParameterNullability(Constructor constructor) { + int paramCount = constructor.getParameters().length; + boolean[] result = new boolean[paramCount]; + for (int i = 0; i < paramCount; i++) { + // Even arguments are nullable + result[i] = i % 2 == 0; + } + return result; + } +} diff --git a/rhino/src/test/resources/META-INF/services/org.mozilla.javascript.NullabilityDetector b/rhino/src/test/resources/META-INF/services/org.mozilla.javascript.NullabilityDetector new file mode 100644 index 0000000000..b209831afb --- /dev/null +++ b/rhino/src/test/resources/META-INF/services/org.mozilla.javascript.NullabilityDetector @@ -0,0 +1 @@ +org.mozilla.javascript.TestNullabilityDetector \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 31d0175025..02739a50a0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,7 @@ +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' + id 'org.jetbrains.kotlin.jvm' version '2.1.0' apply false +} rootProject.name = 'rhino-root' include 'rhino', 'rhino-engine', 'rhino-tools', 'rhino-xml', 'rhino-all', 'examples', 'testutils', 'tests', 'benchmarks' +include 'rhino-kotlin'