diff --git a/compiler/ir/ir.psi2ir/src/org/jetbrains/kotlin/psi2ir/generators/ScriptGenerator.kt b/compiler/ir/ir.psi2ir/src/org/jetbrains/kotlin/psi2ir/generators/ScriptGenerator.kt index 1195e823570..861cbb08d17 100644 --- a/compiler/ir/ir.psi2ir/src/org/jetbrains/kotlin/psi2ir/generators/ScriptGenerator.kt +++ b/compiler/ir/ir.psi2ir/src/org/jetbrains/kotlin/psi2ir/generators/ScriptGenerator.kt @@ -37,6 +37,7 @@ import org.jetbrains.kotlin.psi2ir.intermediate.createTemporaryVariableInBlock import org.jetbrains.kotlin.psi2ir.intermediate.setExplicitReceiverValue import org.jetbrains.kotlin.resolve.BindingContext import org.jetbrains.kotlin.resolve.calls.util.isSingleUnderscore +import org.jetbrains.kotlin.types.typeUtil.makeNullable import org.jetbrains.kotlin.utils.addIfNotNull class ScriptGenerator(declarationGenerator: DeclarationGenerator) : DeclarationGeneratorExtension(declarationGenerator) { diff --git a/libraries/scripting/common/src/kotlin/script/experimental/api/kotlinType.kt b/libraries/scripting/common/src/kotlin/script/experimental/api/kotlinType.kt index 8e09c1de48f..cae589959aa 100644 --- a/libraries/scripting/common/src/kotlin/script/experimental/api/kotlinType.kt +++ b/libraries/scripting/common/src/kotlin/script/experimental/api/kotlinType.kt @@ -14,31 +14,37 @@ import kotlin.reflect.KType */ class KotlinType private constructor( val typeName: String, - @Transient val fromClass: KClass<*>? = null + @Transient val fromClass: KClass<*>?, + val isNullable: Boolean // TODO: copy properties from KType ) : Serializable { /** * Constructs KotlinType from fully-qualified [qualifiedTypeName] in a dot-separated form, e.g. "org.acme.Outer.Inner" */ - constructor(qualifiedTypeName: String) : this(qualifiedTypeName, null) + @JvmOverloads + constructor(qualifiedTypeName: String, isNullable: Boolean = false) + : this(qualifiedTypeName.removeSuffix("?"), null, isNullable = isNullable || qualifiedTypeName.endsWith('?')) /** * Constructs KotlinType from reflected [kclass] */ - constructor(kclass: KClass<*>) : this(kclass.java.name, kclass) + @JvmOverloads + constructor(kclass: KClass<*>, isNullable: Boolean = false) : this(kclass.java.name, kclass, isNullable) // TODO: implement other approach for non-class types /** - * Constructs KotlinType from reflected [ktype] + * Constructs KotlinType from reflected [type] */ - constructor(type: KType) : this(type.classifier as KClass<*>) + constructor(type: KType) : this(type.classifier as KClass<*>, type.isMarkedNullable) override fun equals(other: Any?): Boolean = - (other as? KotlinType)?.let { typeName == it.typeName } == true + (other as? KotlinType)?.let { typeName == it.typeName && isNullable == it.isNullable } == true - override fun hashCode(): Int = typeName.hashCode() + override fun hashCode(): Int = typeName.hashCode() + 31 * isNullable.hashCode() + + fun withNullability(isNullable: Boolean): KotlinType = KotlinType(typeName, fromClass, isNullable) companion object { - private const val serialVersionUID: Long = 1L + private const val serialVersionUID: Long = 2L } -} +} \ No newline at end of file diff --git a/libraries/scripting/jsr223-test/test/kotlin/script/experimental/jsr223/test/KotlinJsr223ScriptEngineIT.kt b/libraries/scripting/jsr223-test/test/kotlin/script/experimental/jsr223/test/KotlinJsr223ScriptEngineIT.kt index 11fe6c451fe..30381f6aaae 100644 --- a/libraries/scripting/jsr223-test/test/kotlin/script/experimental/jsr223/test/KotlinJsr223ScriptEngineIT.kt +++ b/libraries/scripting/jsr223-test/test/kotlin/script/experimental/jsr223/test/KotlinJsr223ScriptEngineIT.kt @@ -261,6 +261,10 @@ obj put("boundValue", 100) }) Assert.assertEquals(111, result2) + + engine.put("nullable", null) + val result3 = engine.eval("bindings[\"nullable\"]?.let { it as Int } ?: -1") + Assert.assertEquals(-1, result3) } @Test @@ -280,6 +284,10 @@ obj put("boundValue", 100) }) Assert.assertEquals(111, result2) + + engine.put("nullable", null) + val result3 = engine.eval("nullable?.let { it as Int } ?: -1") + Assert.assertEquals(-1, result3) } @Test diff --git a/libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/ScriptingHostTest.kt b/libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/ScriptingHostTest.kt index eb1a2b6223f..f46d5e7dedb 100644 --- a/libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/ScriptingHostTest.kt +++ b/libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/ScriptingHostTest.kt @@ -223,6 +223,55 @@ class ScriptingHostTest : TestCase() { Assert.assertEquals(greeting, output) } + @Test + fun testProvidedPropertiesNullability() { + val stringType = KotlinType(String::class) + val definition = createJvmScriptDefinitionFromTemplate( + compilation = { + providedProperties( + "notNullable" to stringType, + "nullable" to stringType.withNullability(true) + ) + }, + evaluation = { + providedProperties( + "notNullable" to "something", + "nullable" to null + ) + } + ) + val defaultEvalConfig = definition.evaluationConfiguration + val notNullEvalConfig = defaultEvalConfig.with { + providedProperties("nullable" to "!") + } + val wrongNullEvalConfig = defaultEvalConfig.with { + providedProperties("notNullable" to null) + } + + with(BasicJvmScriptingHost()) { + // compile time + val comp0 = runBlocking { + compiler("nullable.length".toScriptSource(), definition.compilationConfiguration) + } + assertTrue(comp0 is ResultWithDiagnostics.Failure) + val errors = comp0.reports.filter { it.severity == ScriptDiagnostic.Severity.ERROR } + assertTrue( errors.any { it.message == "Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?" }) + + // runtime + fun evalWith(evalConfig: ScriptEvaluationConfiguration) = + eval("notNullable+(nullable ?: \"0\")".toScriptSource(), definition.compilationConfiguration, evalConfig).valueOrThrow().returnValue + + val ret0 = evalWith(defaultEvalConfig) + assertEquals("something0", (ret0 as? ResultValue.Value)?.value) + + val ret1 = evalWith(notNullEvalConfig) + assertEquals("something!", (ret1 as? ResultValue.Value)?.value) + + val ret2 = evalWith(wrongNullEvalConfig) + assertTrue((ret2 as? ResultValue.Error)?.error is java.lang.NullPointerException) + } + } + @Test fun testDiamondImportWithoutSharing() { val greeting = listOf("Hi from common", "Hi from middle", "Hi from common", "sharedVar == 3") diff --git a/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/jsr223/propertiesFromContext.kt b/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/jsr223/propertiesFromContext.kt index fd0153f9030..043ac85acfc 100644 --- a/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/jsr223/propertiesFromContext.kt +++ b/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/jsr223/propertiesFromContext.kt @@ -20,10 +20,10 @@ fun configureProvidedPropertiesFromJsr223Context(context: ScriptConfigurationRef } for ((k, v) in allBindings) { // only adding bindings that are not already defined and also skip local classes - if (!updatedProperties.containsKey(k) && v::class.qualifiedName != null) { + if (!updatedProperties.containsKey(k) && (v == null || v::class.qualifiedName != null)) { // TODO: add only valid names // TODO: find out how it's implemented in other jsr223 engines for typed languages, since this approach prevent certain usage scenarios, e.g. assigning back value of a "sibling" type - updatedProperties[k] = KotlinType(v::class) + updatedProperties[k] = if (v == null) KotlinType(Any::class, isNullable = true) else KotlinType(v::class) } } ScriptCompilationConfiguration(context.compilationConfiguration) { diff --git a/plugins/scripting/scripting-compiler-impl/src/org/jetbrains/kotlin/scripting/definitions/KotlinScriptDefinitionAdapterFromNewAPI.kt b/plugins/scripting/scripting-compiler-impl/src/org/jetbrains/kotlin/scripting/definitions/KotlinScriptDefinitionAdapterFromNewAPI.kt index 2c16b000805..a78ece3ecc6 100644 --- a/plugins/scripting/scripting-compiler-impl/src/org/jetbrains/kotlin/scripting/definitions/KotlinScriptDefinitionAdapterFromNewAPI.kt +++ b/plugins/scripting/scripting-compiler-impl/src/org/jetbrains/kotlin/scripting/definitions/KotlinScriptDefinitionAdapterFromNewAPI.kt @@ -13,6 +13,7 @@ import org.jetbrains.kotlin.psi.KtScript import kotlin.reflect.KClass import kotlin.reflect.KType import kotlin.reflect.full.starProjectedType +import kotlin.reflect.full.withNullability import kotlin.script.experimental.api.* import kotlin.script.experimental.dependencies.DependenciesResolver import kotlin.script.experimental.host.ScriptingHostConfiguration @@ -68,12 +69,12 @@ abstract class KotlinScriptDefinitionAdapterFromNewAPIBase : KotlinScriptDefinit override val implicitReceivers: List by lazy(LazyThreadSafetyMode.PUBLICATION) { scriptCompilationConfiguration[ScriptCompilationConfiguration.implicitReceivers] .orEmpty() - .map { getScriptingClass(it).starProjectedType } + .map { getScriptingClass(it).starProjectedType.withNullability(it.isNullable) } } override val providedProperties: List> by lazy(LazyThreadSafetyMode.PUBLICATION) { scriptCompilationConfiguration[ScriptCompilationConfiguration.providedProperties] - ?.map { (k, v) -> k to getScriptingClass(v).starProjectedType }.orEmpty() + ?.map { (k, v) -> k to getScriptingClass(v).starProjectedType.withNullability(v.isNullable) }.orEmpty() } @Deprecated("temporary workaround for missing functionality, will be replaced by the new API soon") diff --git a/plugins/scripting/scripting-compiler-impl/src/org/jetbrains/kotlin/scripting/resolve/LazyScriptDescriptor.kt b/plugins/scripting/scripting-compiler-impl/src/org/jetbrains/kotlin/scripting/resolve/LazyScriptDescriptor.kt index 22d5096c225..d428f61a470 100644 --- a/plugins/scripting/scripting-compiler-impl/src/org/jetbrains/kotlin/scripting/resolve/LazyScriptDescriptor.kt +++ b/plugins/scripting/scripting-compiler-impl/src/org/jetbrains/kotlin/scripting/resolve/LazyScriptDescriptor.kt @@ -274,7 +274,7 @@ class LazyScriptDescriptor( scriptCompilationConfiguration()[ScriptCompilationConfiguration.providedProperties].orEmpty() .mapNotNull { (name, type) -> findTypeDescriptor(getScriptingClass(type), Errors.MISSING_SCRIPT_PROVIDED_PROPERTY_CLASS) - ?.let { name.toValidJvmIdentifier() to it } + ?.let { name.toValidJvmIdentifier() to it.defaultType.makeNullableAsSpecified(type.isNullable) } }.map { (name, classDescriptor) -> ScriptProvidedPropertyDescriptor( Name.identifier(name), diff --git a/plugins/scripting/scripting-compiler-impl/src/org/jetbrains/kotlin/scripting/resolve/ScriptProvidedPropertyDescriptor.kt b/plugins/scripting/scripting-compiler-impl/src/org/jetbrains/kotlin/scripting/resolve/ScriptProvidedPropertyDescriptor.kt index 4a250243271..1ed523882ed 100644 --- a/plugins/scripting/scripting-compiler-impl/src/org/jetbrains/kotlin/scripting/resolve/ScriptProvidedPropertyDescriptor.kt +++ b/plugins/scripting/scripting-compiler-impl/src/org/jetbrains/kotlin/scripting/resolve/ScriptProvidedPropertyDescriptor.kt @@ -9,10 +9,11 @@ import org.jetbrains.kotlin.descriptors.* import org.jetbrains.kotlin.descriptors.annotations.Annotations import org.jetbrains.kotlin.descriptors.impl.PropertyDescriptorImpl import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.types.KotlinType class ScriptProvidedPropertyDescriptor( name: Name, - typeDescriptor: ClassDescriptor, + type: KotlinType, receiver: ReceiverParameterDescriptor?, isVar: Boolean, script: ScriptDescriptor @@ -30,7 +31,7 @@ class ScriptProvidedPropertyDescriptor( /* isDelegated = */ false ) { init { - setType(typeDescriptor.defaultType, emptyList(), receiver, null, emptyList()) + setType(type, emptyList(), receiver, null, emptyList()) // TODO: consider delegation instead initialize(null, null, null, null) }