[Gradle] KotlinProjectExtension: use castIsolatedKotlinPluginClassLoaderAware

Using this special cast function for accessing any Kotlin extension
will ensure that an isolated classpath will be reported to the user
when the casting fails because of it.

^KT-50592 WIP
This commit is contained in:
sebastian.sellmair
2021-12-30 10:22:13 +01:00
committed by Space
parent 32177b2cd2
commit a355fefd05
3 changed files with 165 additions and 8 deletions
@@ -24,6 +24,7 @@ import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrSingleTargetPreset
import org.jetbrains.kotlin.gradle.tasks.CompileUsingKotlinDaemon
import org.jetbrains.kotlin.gradle.tasks.withType
import org.jetbrains.kotlin.gradle.utils.SingleWarningPerBuild
import org.jetbrains.kotlin.gradle.utils.castIsolatedKotlinPluginClassLoaderAware
import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName
import org.jetbrains.kotlin.konan.target.CompilerOutputKind
import org.jetbrains.kotlin.statistics.metrics.StringMetrics
@@ -39,27 +40,27 @@ internal fun Project.createKotlinExtension(extensionClass: KClass<out KotlinTopL
}
internal val Project.topLevelExtension: KotlinTopLevelExtension
get() = extensions.getByName(KOTLIN_PROJECT_EXTENSION_NAME) as KotlinTopLevelExtension
get() = extensions.getByName(KOTLIN_PROJECT_EXTENSION_NAME).castIsolatedKotlinPluginClassLoaderAware()
internal val Project.topLevelExtensionOrNull: KotlinTopLevelExtension?
get() = extensions.findByName(KOTLIN_PROJECT_EXTENSION_NAME) as KotlinTopLevelExtension?
get() = extensions.findByName(KOTLIN_PROJECT_EXTENSION_NAME)?.castIsolatedKotlinPluginClassLoaderAware<KotlinTopLevelExtension>()
internal val Project.kotlinExtensionOrNull: KotlinProjectExtension?
get() = extensions.findByName(KOTLIN_PROJECT_EXTENSION_NAME) as? KotlinProjectExtension
get() = extensions.findByName(KOTLIN_PROJECT_EXTENSION_NAME)?.castIsolatedKotlinPluginClassLoaderAware()
val Project.kotlinExtension: KotlinProjectExtension
get() = extensions.getByName(KOTLIN_PROJECT_EXTENSION_NAME) as KotlinProjectExtension
get() = extensions.getByName(KOTLIN_PROJECT_EXTENSION_NAME).castIsolatedKotlinPluginClassLoaderAware()
internal val Project.multiplatformExtensionOrNull: KotlinMultiplatformExtension?
get() = extensions.findByName(KOTLIN_PROJECT_EXTENSION_NAME) as? KotlinMultiplatformExtension
get() = extensions.findByName(KOTLIN_PROJECT_EXTENSION_NAME)?.castIsolatedKotlinPluginClassLoaderAware()
internal val Project.multiplatformExtension: KotlinMultiplatformExtension
get() = extensions.getByName(KOTLIN_PROJECT_EXTENSION_NAME) as KotlinMultiplatformExtension
get() = extensions.getByName(KOTLIN_PROJECT_EXTENSION_NAME).castIsolatedKotlinPluginClassLoaderAware()
internal val Project.pm20Extension: KotlinPm20ProjectExtension
get() = extensions.getByName(KOTLIN_PROJECT_EXTENSION_NAME) as KotlinPm20ProjectExtension
get() = extensions.getByName(KOTLIN_PROJECT_EXTENSION_NAME).castIsolatedKotlinPluginClassLoaderAware()
open class KotlinTopLevelExtension (internal val project: Project) {
open class KotlinTopLevelExtension(internal val project: Project) {
val experimental: ExperimentalExtension
get() = DslObject(this).extensions.getByType(ExperimentalExtension::class.java)
@@ -0,0 +1,51 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle.utils
import org.jetbrains.kotlin.gradle.plugin.MULTIPLE_KOTLIN_PLUGINS_LOADED_WARNING
class IsolatedKotlinClasspathClassCastException : ClassCastException(MULTIPLE_KOTLIN_PLUGINS_LOADED_WARNING)
/**
* Behaves like a regular cast function, but will be able to detect cast failures because
* of an isolated classpath. In this case a more detailed error message will be emitted.
*
* ```
* "".castIsolatedKotlinPluginClassLoaderAware<Int>() // fails like "" as Int
* "".castIsolatedKotlinPluginClassLoaderAware<Int?>() // returns null like "" as? Int
* ```
* @return [this] as T if possible (regular cast)
* @throws ClassCastException is not castable
* @throws IsolatedKotlinClasspathClassCastException when a separated classpath is detected. See [MULTIPLE_KOTLIN_PLUGINS_LOADED_WARNING]
*/
internal inline fun <reified T> Any.castIsolatedKotlinPluginClassLoaderAware(): T {
/* Fast path */
if (this is T) return this
val targetClassFromReceiverClassLoader = try {
this::class.java.classLoader.loadClass(T::class.java.name)
} catch (_: ClassNotFoundException) {
null
}
/*
User setup lead to Gradle separating the classpath between two projects applying the Kotlin Gradle plugin.
This is not a supported setup and the ClassCastException should report this.
*/
if (targetClassFromReceiverClassLoader != null && targetClassFromReceiverClassLoader != T::class.java) {
/*
We can be lenient if
1) T is marked nullable (`null is T`), which implies a safe cast (behavior like `as?`)
2) We know that the cast is impossible from the perspective of 'this'
*/
if (null is T && !targetClassFromReceiverClassLoader.isInstance(this)) return null as T
throw IsolatedKotlinClasspathClassCastException()
}
/* Will throw "regular" ClassCastException OR return null if T is marked nullable */
return if (null is T) null as T
else this as T
}
@@ -0,0 +1,105 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle.utils
import org.jetbrains.kotlin.gradle.plugin.MULTIPLE_KOTLIN_PLUGINS_LOADED_WARNING
import org.junit.AssumptionViolatedException
import org.junit.jupiter.api.assertThrows
import java.net.URLClassLoader
import kotlin.test.Test
import kotlin.test.assertNull
import kotlin.test.assertSame
import kotlin.test.assertTrue
class CastIsolatedKotlinPluginClassLoaderAwareTest {
interface A
interface B : A
private val isolatedClassLoader by lazy {
val thisClassLoader = this::class.java.classLoader as? URLClassLoader
?: throw AssumptionViolatedException(
"Test required to load classes with ${URLClassLoader::class.java.name}. " +
"Found ${this::class.java.classLoader.javaClass.name}"
)
URLClassLoader(thisClassLoader.urLs, ClassLoader.getSystemClassLoader().parent)
}
@Test
fun `test downcast`() {
val b = object : B {}
val a = b.castIsolatedKotlinPluginClassLoaderAware<A>()
assertSame(b, a)
}
@Test
fun `test impossible cast`() {
val a = object : A {}
assertNull(
a.castIsolatedKotlinPluginClassLoaderAware<B?>(),
"Expected impossible cast to return null since the type parameter is marked nullable"
)
val exception = assertThrows<ClassCastException>(
"Expected impossible cast to throw 'ClassCastException' because type parameter is not marked nullable"
) { a.castIsolatedKotlinPluginClassLoaderAware<B>() }
assertTrue(
MULTIPLE_KOTLIN_PLUGINS_LOADED_WARNING !in exception.message.orEmpty(),
"Expected no classpath warning in the error message, since this cast was not failing because of iosolated classloaders"
)
}
@Test
fun `test downcast - with isolated classpath`() {
class BImpl : B
val isolatedBInstance = isolatedClassLoader.loadClass(BImpl::class.java.name).constructors.single().newInstance()
run {
val exception = assertThrows<IsolatedKotlinClasspathClassCastException>(
"Expected 'ClassCastException' because of classloader isolation"
) { isolatedBInstance.castIsolatedKotlinPluginClassLoaderAware<A>() }
assertTrue(
MULTIPLE_KOTLIN_PLUGINS_LOADED_WARNING in exception.message.orEmpty(),
"Expected classpath warning in the error message"
)
}
run {
val exception = assertThrows<IsolatedKotlinClasspathClassCastException> {
isolatedBInstance.castIsolatedKotlinPluginClassLoaderAware<A?>()
}
assertTrue(MULTIPLE_KOTLIN_PLUGINS_LOADED_WARNING in exception.message.orEmpty())
}
}
@Test
fun `test impossible cast - with isolated classpath`() {
class AImpl : A
val isolatedAInstance = isolatedClassLoader.loadClass(AImpl::class.java.name).constructors.single().newInstance()
run {
val exception = assertThrows<IsolatedKotlinClasspathClassCastException>(
"Expected 'ClassCastException' because of classloader isolation"
) { isolatedAInstance.castIsolatedKotlinPluginClassLoaderAware<B>() }
assertTrue(
MULTIPLE_KOTLIN_PLUGINS_LOADED_WARNING in exception.message.orEmpty(),
"Expected classpath warning in the error message"
)
}
assertNull(
isolatedAInstance.castIsolatedKotlinPluginClassLoaderAware<B?>(),
"Expected 'null', since the type parameter was marked optional and the " +
"isolated classpath was able to detect the impossible cast"
)
}
}