From a355fefd05c4b2bea26d274dd595864a890aaec2 Mon Sep 17 00:00:00 2001 From: "sebastian.sellmair" Date: Thu, 30 Dec 2021 10:22:13 +0100 Subject: [PATCH] [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 --- .../gradle/dsl/KotlinProjectExtension.kt | 17 +-- .../kotlin/gradle/utils/reflectionUtils.kt | 51 +++++++++ ...solatedKotlinPluginClassLoaderAwareTest.kt | 105 ++++++++++++++++++ 3 files changed, 165 insertions(+), 8 deletions(-) create mode 100644 libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/utils/reflectionUtils.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/utils/CastIsolatedKotlinPluginClassLoaderAwareTest.kt 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" + ) + } +}