IR Scripting: allow to specify nullable types for provided props...
but only explicitly. This does not fix a breaking change described in #KT-52120, because it seems the correct behavior, but it allows to "workaround" the problem by specifying nullability explicitly. Also improve handling of nullable bindings in JSR-223. #KT-49173 fixed #KT-51213 fixed
This commit is contained in:
@@ -37,6 +37,7 @@ import org.jetbrains.kotlin.psi2ir.intermediate.createTemporaryVariableInBlock
|
|||||||
import org.jetbrains.kotlin.psi2ir.intermediate.setExplicitReceiverValue
|
import org.jetbrains.kotlin.psi2ir.intermediate.setExplicitReceiverValue
|
||||||
import org.jetbrains.kotlin.resolve.BindingContext
|
import org.jetbrains.kotlin.resolve.BindingContext
|
||||||
import org.jetbrains.kotlin.resolve.calls.util.isSingleUnderscore
|
import org.jetbrains.kotlin.resolve.calls.util.isSingleUnderscore
|
||||||
|
import org.jetbrains.kotlin.types.typeUtil.makeNullable
|
||||||
import org.jetbrains.kotlin.utils.addIfNotNull
|
import org.jetbrains.kotlin.utils.addIfNotNull
|
||||||
|
|
||||||
class ScriptGenerator(declarationGenerator: DeclarationGenerator) : DeclarationGeneratorExtension(declarationGenerator) {
|
class ScriptGenerator(declarationGenerator: DeclarationGenerator) : DeclarationGeneratorExtension(declarationGenerator) {
|
||||||
|
|||||||
@@ -14,31 +14,37 @@ import kotlin.reflect.KType
|
|||||||
*/
|
*/
|
||||||
class KotlinType private constructor(
|
class KotlinType private constructor(
|
||||||
val typeName: String,
|
val typeName: String,
|
||||||
@Transient val fromClass: KClass<*>? = null
|
@Transient val fromClass: KClass<*>?,
|
||||||
|
val isNullable: Boolean
|
||||||
// TODO: copy properties from KType
|
// TODO: copy properties from KType
|
||||||
) : Serializable {
|
) : Serializable {
|
||||||
/**
|
/**
|
||||||
* Constructs KotlinType from fully-qualified [qualifiedTypeName] in a dot-separated form, e.g. "org.acme.Outer.Inner"
|
* 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]
|
* 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
|
// 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 =
|
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 {
|
companion object {
|
||||||
private const val serialVersionUID: Long = 1L
|
private const val serialVersionUID: Long = 2L
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+8
@@ -261,6 +261,10 @@ obj
|
|||||||
put("boundValue", 100)
|
put("boundValue", 100)
|
||||||
})
|
})
|
||||||
Assert.assertEquals(111, result2)
|
Assert.assertEquals(111, result2)
|
||||||
|
|
||||||
|
engine.put("nullable", null)
|
||||||
|
val result3 = engine.eval("bindings[\"nullable\"]?.let { it as Int } ?: -1")
|
||||||
|
Assert.assertEquals(-1, result3)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -280,6 +284,10 @@ obj
|
|||||||
put("boundValue", 100)
|
put("boundValue", 100)
|
||||||
})
|
})
|
||||||
Assert.assertEquals(111, result2)
|
Assert.assertEquals(111, result2)
|
||||||
|
|
||||||
|
engine.put("nullable", null)
|
||||||
|
val result3 = engine.eval("nullable?.let { it as Int } ?: -1")
|
||||||
|
Assert.assertEquals(-1, result3)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+49
@@ -223,6 +223,55 @@ class ScriptingHostTest : TestCase() {
|
|||||||
Assert.assertEquals(greeting, output)
|
Assert.assertEquals(greeting, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testProvidedPropertiesNullability() {
|
||||||
|
val stringType = KotlinType(String::class)
|
||||||
|
val definition = createJvmScriptDefinitionFromTemplate<SimpleScriptTemplate>(
|
||||||
|
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
|
@Test
|
||||||
fun testDiamondImportWithoutSharing() {
|
fun testDiamondImportWithoutSharing() {
|
||||||
val greeting = listOf("Hi from common", "Hi from middle", "Hi from common", "sharedVar == 3")
|
val greeting = listOf("Hi from common", "Hi from middle", "Hi from common", "sharedVar == 3")
|
||||||
|
|||||||
+2
-2
@@ -20,10 +20,10 @@ fun configureProvidedPropertiesFromJsr223Context(context: ScriptConfigurationRef
|
|||||||
}
|
}
|
||||||
for ((k, v) in allBindings) {
|
for ((k, v) in allBindings) {
|
||||||
// only adding bindings that are not already defined and also skip local classes
|
// 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: 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
|
// 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) {
|
ScriptCompilationConfiguration(context.compilationConfiguration) {
|
||||||
|
|||||||
+3
-2
@@ -13,6 +13,7 @@ import org.jetbrains.kotlin.psi.KtScript
|
|||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
import kotlin.reflect.KType
|
import kotlin.reflect.KType
|
||||||
import kotlin.reflect.full.starProjectedType
|
import kotlin.reflect.full.starProjectedType
|
||||||
|
import kotlin.reflect.full.withNullability
|
||||||
import kotlin.script.experimental.api.*
|
import kotlin.script.experimental.api.*
|
||||||
import kotlin.script.experimental.dependencies.DependenciesResolver
|
import kotlin.script.experimental.dependencies.DependenciesResolver
|
||||||
import kotlin.script.experimental.host.ScriptingHostConfiguration
|
import kotlin.script.experimental.host.ScriptingHostConfiguration
|
||||||
@@ -68,12 +69,12 @@ abstract class KotlinScriptDefinitionAdapterFromNewAPIBase : KotlinScriptDefinit
|
|||||||
override val implicitReceivers: List<KType> by lazy(LazyThreadSafetyMode.PUBLICATION) {
|
override val implicitReceivers: List<KType> by lazy(LazyThreadSafetyMode.PUBLICATION) {
|
||||||
scriptCompilationConfiguration[ScriptCompilationConfiguration.implicitReceivers]
|
scriptCompilationConfiguration[ScriptCompilationConfiguration.implicitReceivers]
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
.map { getScriptingClass(it).starProjectedType }
|
.map { getScriptingClass(it).starProjectedType.withNullability(it.isNullable) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override val providedProperties: List<Pair<String, KType>> by lazy(LazyThreadSafetyMode.PUBLICATION) {
|
override val providedProperties: List<Pair<String, KType>> by lazy(LazyThreadSafetyMode.PUBLICATION) {
|
||||||
scriptCompilationConfiguration[ScriptCompilationConfiguration.providedProperties]
|
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")
|
@Deprecated("temporary workaround for missing functionality, will be replaced by the new API soon")
|
||||||
|
|||||||
+1
-1
@@ -274,7 +274,7 @@ class LazyScriptDescriptor(
|
|||||||
scriptCompilationConfiguration()[ScriptCompilationConfiguration.providedProperties].orEmpty()
|
scriptCompilationConfiguration()[ScriptCompilationConfiguration.providedProperties].orEmpty()
|
||||||
.mapNotNull { (name, type) ->
|
.mapNotNull { (name, type) ->
|
||||||
findTypeDescriptor(getScriptingClass(type), Errors.MISSING_SCRIPT_PROVIDED_PROPERTY_CLASS)
|
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) ->
|
}.map { (name, classDescriptor) ->
|
||||||
ScriptProvidedPropertyDescriptor(
|
ScriptProvidedPropertyDescriptor(
|
||||||
Name.identifier(name),
|
Name.identifier(name),
|
||||||
|
|||||||
+3
-2
@@ -9,10 +9,11 @@ import org.jetbrains.kotlin.descriptors.*
|
|||||||
import org.jetbrains.kotlin.descriptors.annotations.Annotations
|
import org.jetbrains.kotlin.descriptors.annotations.Annotations
|
||||||
import org.jetbrains.kotlin.descriptors.impl.PropertyDescriptorImpl
|
import org.jetbrains.kotlin.descriptors.impl.PropertyDescriptorImpl
|
||||||
import org.jetbrains.kotlin.name.Name
|
import org.jetbrains.kotlin.name.Name
|
||||||
|
import org.jetbrains.kotlin.types.KotlinType
|
||||||
|
|
||||||
class ScriptProvidedPropertyDescriptor(
|
class ScriptProvidedPropertyDescriptor(
|
||||||
name: Name,
|
name: Name,
|
||||||
typeDescriptor: ClassDescriptor,
|
type: KotlinType,
|
||||||
receiver: ReceiverParameterDescriptor?,
|
receiver: ReceiverParameterDescriptor?,
|
||||||
isVar: Boolean,
|
isVar: Boolean,
|
||||||
script: ScriptDescriptor
|
script: ScriptDescriptor
|
||||||
@@ -30,7 +31,7 @@ class ScriptProvidedPropertyDescriptor(
|
|||||||
/* isDelegated = */ false
|
/* isDelegated = */ false
|
||||||
) {
|
) {
|
||||||
init {
|
init {
|
||||||
setType(typeDescriptor.defaultType, emptyList(), receiver, null, emptyList())
|
setType(type, emptyList(), receiver, null, emptyList())
|
||||||
// TODO: consider delegation instead
|
// TODO: consider delegation instead
|
||||||
initialize(null, null, null, null)
|
initialize(null, null, null, null)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user