Validate Java and Kotlin target compatibility have same value.

This will ensure compiled java sources and kotlin sources class files
has the same bytecode version.

^KT-45611 Fixed
^KT-43095 Fixed
This commit is contained in:
Yahor Berdnikau
2021-06-22 13:20:50 +02:00
committed by Space
parent 6d929a8c3b
commit 5486fec0f9
10 changed files with 349 additions and 23 deletions
@@ -34,28 +34,6 @@ class KotlinJavaToolchainTest : KGPBaseTest() {
}
}
@GradleTestVersions(minVersion = "6.7.1")
@GradleTest
@DisplayName("Default Kotlin toolchain should still allow to set Java source and target compatibility")
internal fun shouldNotFailWithDefaultJdkAndCompatibility(gradleVersion: GradleVersion) {
project(
projectName = "simple".fullProjectName,
gradleVersion = gradleVersion
) {
//language=Groovy
rootBuildGradle.append(
"""
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
""".trimIndent()
)
build("assemble")
}
}
@GradleTestVersions(maxVersion = "6.6.1")
@GradleTest
@DisplayName("Should use provided jdk location to compile Kotlin sources")
internal fun customJdkHomeLocation(gradleVersion: GradleVersion) {
@@ -63,6 +41,8 @@ class KotlinJavaToolchainTest : KGPBaseTest() {
projectName = "simple".fullProjectName,
gradleVersion = gradleVersion,
) {
setJavaCompilationCompatibility(JavaVersion.VERSION_1_9)
useJdkToCompile(
getJdk9Path(),
JavaVersion.VERSION_1_9
@@ -88,6 +68,7 @@ class KotlinJavaToolchainTest : KGPBaseTest() {
if (shouldUseToolchain(gradleVersion)) {
useToolchainExtension(11)
} else {
setJavaCompilationCompatibility(JavaVersion.VERSION_1_9)
useJdkToCompile(
getJdk9Path(),
JavaVersion.VERSION_1_9
@@ -107,6 +88,7 @@ class KotlinJavaToolchainTest : KGPBaseTest() {
if (shouldUseToolchain(gradleVersion)) {
useToolchainExtension(11)
} else {
setJavaCompilationCompatibility(JavaVersion.VERSION_1_9)
useJdkToCompile(
getJdk9Path(),
JavaVersion.VERSION_1_9
@@ -134,6 +116,7 @@ class KotlinJavaToolchainTest : KGPBaseTest() {
if (shouldUseToolchain(gradleVersion)) {
useToolchainExtension(11)
} else {
setJavaCompilationCompatibility(JavaVersion.VERSION_1_9)
useJdkToCompile(
getJdk9Path(),
JavaVersion.VERSION_1_9
@@ -440,6 +423,173 @@ class KotlinJavaToolchainTest : KGPBaseTest() {
}
}
@DisplayName("Should produce warning if java and kotlin jvm targets are different")
@GradleTestVersions(minVersion = "6.7.1")
@GradleTest
internal fun shouldWarnIfJavaAndKotlinJvmTargetsAreDifferent(gradleVersion: GradleVersion) {
project(
projectName = "kotlinJavaProject".fullProjectName,
gradleVersion = gradleVersion,
buildOptions = defaultBuildOptions.copy(logLevel = LogLevel.WARN)
) {
setJavaCompilationCompatibility(JavaVersion.VERSION_1_8)
useToolchainToCompile(11)
build("assemble") {
assertOutputContains(
"'compileJava' task (current target is 1.8) and 'compileKotlin' task (current target is 11) jvm target compatibility " +
"should be set to the same Java version."
)
}
}
}
@DisplayName("Should fail the build if verification mode is 'error' and kotlin and java targets are different")
@GradleTestVersions(minVersion = "6.7.1")
@GradleTest
internal fun shouldFailBuildIfJavaAndKotlinJvmTargetsAreDifferent(gradleVersion: GradleVersion) {
project(
projectName = "kotlinJavaProject".fullProjectName,
gradleVersion = gradleVersion
) {
setJavaCompilationCompatibility(JavaVersion.VERSION_1_8)
useToolchainToCompile(11)
//language=properties
gradleProperties.append(
"""
kotlin.jvm.target.validation.mode = error
""".trimIndent()
)
buildAndFail("assemble") {
assertOutputContains(
"'compileJava' task (current target is 1.8) and 'compileKotlin' task (current target is 11) jvm target compatibility " +
"should be set to the same Java version."
)
}
}
}
@DisplayName("Should ignore if verification mode is 'ignore' and kotlin and java targets are different")
@GradleTestVersions(minVersion = "6.7.1")
@GradleTest
internal fun shouldNotPrintAnythingIfJavaAndKotlinJvmTargetsAreDifferent(
gradleVersion: GradleVersion
) {
project(
projectName = "kotlinJavaProject".fullProjectName,
gradleVersion = gradleVersion
) {
setJavaCompilationCompatibility(JavaVersion.VERSION_1_8)
useToolchainToCompile(11)
//language=properties
gradleProperties.append(
"""
kotlin.jvm.target.validation.mode = ignore
""".trimIndent()
)
build("assemble") {
assertOutputDoesNotContain(
"'compileJava' task (current target is 1.8) and 'compileKotlin' task (current target is 11) jvm target compatibility " +
"should be set to the same Java version."
)
}
}
}
@DisplayName("Should not produce warning when java and kotlin jvm targets are the same")
@GradleTestVersions(minVersion = "6.7.1")
@GradleTest
internal fun shouldNotWarnOnJavaAndKotlinSameJvmTargets(gradleVersion: GradleVersion) {
project(
projectName = "kotlinJavaProject".fullProjectName,
gradleVersion = gradleVersion
) {
useToolchainToCompile(11)
build("build") {
assertOutputDoesNotContain(
"'compileJava' task (current target is 1.8) and 'compileKotlin' task (current target is 11) jvm target compatibility " +
"should be set to the same Java version."
)
}
}
}
@DisplayName("Should produce Java-Kotlin jvm target incompatibility warning only for related tasks")
@GradleTestVersions(minVersion = "6.7.1")
@GradleTest
internal fun shouldProduceJavaKotlinJvmTargetDifferenceWarningOnlyForRelatedTasks(
gradleVersion: GradleVersion
) {
project(
projectName = "kotlinJavaProject".fullProjectName,
gradleVersion = gradleVersion
) {
useToolchainToCompile(11)
JavaVersion.VERSION_1_8
//language=Groovy
rootBuildGradle.append(
"""
tasks
.matching {
it instanceof JavaCompile && it.name == "compileTestJava"
}
.configureEach {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
""".trimIndent()
)
build("build") {
assertOutputContains(
"'compileTestJava' task (current target is 1.8) and 'compileTestKotlin' task (current target is 11) jvm target " +
"compatibility should be set to the same Java version."
)
assertOutputDoesNotContain(
"'compileJava' task (current target is 1.8) and 'compileKotlin' task (current target is 11) jvm target compatibility " +
"should be set to the same Java version."
)
}
}
}
@DisplayName("Setting toolchain via java extension should also affect Kotlin compilations")
@GradleTestVersions(minVersion = "6.7.1")
@GradleTest
internal fun settingToolchainViaJavaShouldAlsoWork(gradleVersion: GradleVersion) {
project(
projectName = "kotlinJavaProject".fullProjectName,
gradleVersion = gradleVersion
) {
//language=groovy
rootBuildGradle.append(
"""
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(11))
}
}
""".trimIndent()
)
//language=properties
gradleProperties.append(
"""
kotlin.jvm.target.validation.mode = error
""".trimIndent()
)
build("build")
}
}
private fun BuildResult.assertJdkHomeIsUsingJdk(
javaexecPath: String
) = assertOutputContains("[KOTLIN] Kotlin compilation 'jdkHome' argument: $javaexecPath")
@@ -475,6 +625,22 @@ class KotlinJavaToolchainTest : KGPBaseTest() {
)
}
private fun TestProject.setJavaCompilationCompatibility(
target: JavaVersion
) {
//language=Groovy
rootBuildGradle.append(
"""
tasks.withType(JavaCompile.class).configureEach {
sourceCompatibility = JavaVersion.${target.name}
targetCompatibility = JavaVersion.${target.name}
}
""".trimIndent()
)
}
private fun TestProject.useJdkToCompile(
jdkPath: String,
jdkVersion: JavaVersion
@@ -0,0 +1,19 @@
plugins {
id "org.jetbrains.kotlin.jvm"
id "java"
}
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
implementation 'com.google.guava:guava:12.0'
testImplementation 'org.testng:testng:6.8'
}
test {
useTestNG()
}
@@ -0,0 +1,17 @@
package demo;
/**
* Created by Nikita.Skvortsov
* Date: 3/1/13, 10:53 AM
*/
public class Greeter {
private final String myGreeting;
public Greeter(String greeting) {
myGreeting = greeting;
}
public String getGreeting() {
return myGreeting;
}
}
@@ -0,0 +1,18 @@
package demo;
/**
* Created by Nikita.Skvortsov
* Date: 3/1/13, 10:50 AM
*/
public class HelloWorld {
public static void main(String[] args) {
final KotlinGreetingJoiner example = new KotlinGreetingJoiner(new Greeter("Hi"));
example.addName("Harry");
example.addName("Ron");
example.addName(null);
example.addName("Hermione");
System.out.println(example.getJoinedGreeting());
}
}
@@ -0,0 +1,20 @@
package demo
import com.google.common.primitives.Ints
import com.google.common.base.Joiner
import java.util.ArrayList
class KotlinGreetingJoiner(val greeter : Greeter) {
val names = ArrayList<String?>()
fun addName(name : String?): Unit{
names.add(name)
}
fun getJoinedGreeting() : String? {
val joiner = Joiner.on(" and ").skipNulls();
return "${greeter.getGreeting()} ${joiner.join(names)}"
}
}
@@ -0,0 +1,21 @@
package demo
import com.google.common.primitives.Ints
import com.google.common.base.Joiner
import org.testng.Assert.*
import org.testng.annotations.AfterMethod
import org.testng.annotations.BeforeMethod
import org.testng.annotations.Test as test
class TestSource() {
@test fun f() {
val example : KotlinGreetingJoiner = KotlinGreetingJoiner(Greeter("Hi"))
example.addName("Harry")
example.addName("Ron")
example.addName(null)
example.addName("Hermione")
assertEquals(example.getJoinedGreeting(), "Hi Harry and Ron and Hermione")
}
}
@@ -42,6 +42,7 @@ internal fun PropertiesProvider.mapKotlinTaskProperties(task: AbstractKotlinComp
task.kotlinOptions.useFir = true
}
}
task.jvmTargetValidationMode.set(jvmTargetValidationMode)
}
if (task is Kotlin2JsCompile) {
@@ -340,6 +341,13 @@ internal class PropertiesProvider private constructor(private val project: Proje
val kotlinTestInferJvmVariant: Boolean
get() = booleanProperty("kotlin.test.infer.jvm.variant") ?: true
enum class JvmTargetValidationMode {
IGNORE, WARNING, ERROR
}
val jvmTargetValidationMode: JvmTargetValidationMode
get() = enumProperty("kotlin.jvm.target.validation.mode", JvmTargetValidationMode.WARNING)
private fun propertyWithDeprecatedVariant(propName: String, deprecatedPropName: String): String? {
val deprecatedProperty = property(deprecatedPropName)
if (deprecatedProperty != null) {
@@ -351,6 +359,11 @@ internal class PropertiesProvider private constructor(private val project: Proje
private fun booleanProperty(propName: String): Boolean? =
property(propName)?.toBoolean()
private inline fun <reified T : Enum<T>> enumProperty(
propName: String,
defaultValue: T
): T = property(propName)?.let { enumValueOf<T>(it.toUpperCase()) } ?: defaultValue
private fun property(propName: String): String? =
if (project.hasProperty(propName)) {
project.property(propName) as? String
@@ -119,7 +119,6 @@ internal abstract class DefaultKotlinJavaToolchain @Inject constructor(
get() = defaultJavaToolchainSetter
?: throw GradleException("Toolchain support is available from $TOOLCHAIN_SUPPORTED_VERSION")
private abstract class JvmTargetUpdater(
private val kotlinCompileTaskProvider: () -> KotlinCompileTask<KotlinJvmOptionsBase>?
) {
@@ -5,6 +5,7 @@
package org.jetbrains.kotlin.gradle.tasks
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.file.*
@@ -46,6 +47,7 @@ import org.jetbrains.kotlin.gradle.logging.GradleKotlinLogger
import org.jetbrains.kotlin.gradle.logging.GradlePrintingMessageCollector
import org.jetbrains.kotlin.gradle.logging.kotlinDebug
import org.jetbrains.kotlin.gradle.plugin.*
import org.jetbrains.kotlin.gradle.plugin.mpp.*
import org.jetbrains.kotlin.gradle.plugin.mpp.associateWithTransitiveClosure
import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.KotlinCompilationData
import org.jetbrains.kotlin.gradle.report.ReportingSettings
@@ -428,6 +430,25 @@ abstract class KotlinCompile @Inject constructor(
UsesKotlinJavaToolchain {
class Configurator(kotlinCompilation: KotlinCompilationData<*>) : AbstractKotlinCompile.Configurator<KotlinCompile>(kotlinCompilation) {
override fun configure(task: KotlinCompile) {
super.configure(task)
val compileJavaTaskProvider = when (compilation) {
is KotlinJvmCompilation -> compilation.compileJavaTaskProvider
is KotlinJvmAndroidCompilation -> compilation.compileJavaTaskProvider
is KotlinWithJavaCompilation -> compilation.compileJavaTaskProvider
else -> null
}
if (compileJavaTaskProvider != null) {
task.associatedJavaCompileTaskTargetCompatibility.set(
compileJavaTaskProvider.map { it.targetCompatibility }
)
task.associatedJavaCompileTaskName.set(
compileJavaTaskProvider.map { it.name }
)
}
}
}
@get:Internal
@@ -482,6 +503,15 @@ abstract class KotlinCompile @Inject constructor(
}
)
@get:Internal
internal abstract val associatedJavaCompileTaskTargetCompatibility: Property<String>
@get:Internal
internal abstract val associatedJavaCompileTaskName: Property<String>
@get:Internal
internal abstract val jvmTargetValidationMode: Property<PropertiesProvider.JvmTargetValidationMode>
init {
incremental = true
}
@@ -511,6 +541,8 @@ abstract class KotlinCompile @Inject constructor(
override fun callCompilerAsync(args: K2JVMCompilerArguments, sourceRoots: SourceRoots, changedFiles: ChangedFiles) {
sourceRoots as SourceRoots.ForJvm
validateKotlinAndJavaHasSameTargetCompatibility(args)
val messageCollector = GradlePrintingMessageCollector(logger, args.allWarningsAsErrors)
val outputItemCollector = OutputItemsCollectorImpl()
val compilerRunner = compilerRunner.get()
@@ -544,6 +576,23 @@ abstract class KotlinCompile @Inject constructor(
)
}
private fun validateKotlinAndJavaHasSameTargetCompatibility(args: K2JVMCompilerArguments) {
associatedJavaCompileTaskTargetCompatibility.orNull?.let { targetCompatibility ->
val normalizedJavaVersion = if (targetCompatibility == "1.9") "9" else targetCompatibility
if (normalizedJavaVersion != args.jvmTarget) {
val javaTaskName = associatedJavaCompileTaskName.get()
val errorMessage = "'$javaTaskName' task (current target is $targetCompatibility) and " +
"'$name' task (current target is ${args.jvmTarget}) " +
"jvm target compatibility should be set to the same Java version."
when (jvmTargetValidationMode.get()) {
PropertiesProvider.JvmTargetValidationMode.ERROR -> throw GradleException(errorMessage)
PropertiesProvider.JvmTargetValidationMode.WARNING -> logger.warn(errorMessage)
else -> Unit
}
}
}
}
@get:Input
val disableMultiModuleIC: Boolean by lazy {
if (!isIncrementalCompilationEnabled() || !javaOutputDir.isPresent) {