diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinProjectExtension.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinProjectExtension.kt index 434199375ea..dffea7c826d 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinProjectExtension.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinProjectExtension.kt @@ -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() 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) diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/utils/reflectionUtils.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/utils/reflectionUtils.kt new file mode 100644 index 00000000000..dfa1c052227 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/utils/reflectionUtils.kt @@ -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() // fails like "" as Int + * "".castIsolatedKotlinPluginClassLoaderAware() // 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 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 +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/utils/CastIsolatedKotlinPluginClassLoaderAwareTest.kt b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/utils/CastIsolatedKotlinPluginClassLoaderAwareTest.kt new file mode 100644 index 00000000000..4e9591dd426 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/utils/CastIsolatedKotlinPluginClassLoaderAwareTest.kt @@ -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() + assertSame(b, a) + } + + @Test + fun `test impossible cast`() { + val a = object : A {} + assertNull( + a.castIsolatedKotlinPluginClassLoaderAware(), + "Expected impossible cast to return null since the type parameter is marked nullable" + ) + + val exception = assertThrows( + "Expected impossible cast to throw 'ClassCastException' because type parameter is not marked nullable" + ) { a.castIsolatedKotlinPluginClassLoaderAware() } + + 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( + "Expected 'ClassCastException' because of classloader isolation" + ) { isolatedBInstance.castIsolatedKotlinPluginClassLoaderAware() } + + assertTrue( + MULTIPLE_KOTLIN_PLUGINS_LOADED_WARNING in exception.message.orEmpty(), + "Expected classpath warning in the error message" + ) + } + + run { + val exception = assertThrows { + isolatedBInstance.castIsolatedKotlinPluginClassLoaderAware() + } + + 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( + "Expected 'ClassCastException' because of classloader isolation" + ) { isolatedAInstance.castIsolatedKotlinPluginClassLoaderAware() } + + assertTrue( + MULTIPLE_KOTLIN_PLUGINS_LOADED_WARNING in exception.message.orEmpty(), + "Expected classpath warning in the error message" + ) + } + + assertNull( + isolatedAInstance.castIsolatedKotlinPluginClassLoaderAware(), + "Expected 'null', since the type parameter was marked optional and the " + + "isolated classpath was able to detect the impossible cast" + ) + } +}