Files
kotlin-fork/libraries/tools/ide-plugin-dependencies-validator/build.gradle.kts
T
Ilya Kirillov a65c735feb [build] add checks to ensure that no modules which are part of the IDE plugin do not use experimental stdlib API
to ensure binary compatibility with stdlib inside IntelliJ.

This includes using the latest stable kotlin API version and
forbidding using experimental declarations from stdlib.

^KT-62510
2023-10-25 08:38:07 +00:00

150 lines
5.3 KiB
Kotlin

import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
import java.nio.file.Paths
import kotlin.io.path.readLines
plugins {
application
kotlin("jvm")
id("jps-compatible")
}
dependencies {
implementation(project(":compiler:psi"))
implementation(project(":compiler:cli"))
implementation(intellijCore())
implementation(kotlinStdlib())
// runtime dependencies for IJ
runtimeOnly(commonDependency("org.jetbrains.intellij.deps.fastutil:intellij-deps-fastutil"))
runtimeOnly(commonDependency("org.codehaus.woodstox:stax2-api"))
runtimeOnly(commonDependency("com.fasterxml:aalto-xml"))
runtimeOnly(commonDependency("org.jetbrains.intellij.deps:trove4j"))
// test dependencies
testImplementation(platform(libs.junit.bom))
testImplementation(libs.junit.jupiter.api)
testRuntimeOnly(libs.junit.jupiter.engine)
}
projectTest(jUnitMode = JUnitMode.JUnit5) {
workingDir = rootDir
useJUnitPlatform()
}
application {
mainClass.set("org.jetbrains.kotlin.ide.plugin.dependencies.validator.MainKt")
}
val projectsUsedInIntelliJKotlinPlugin: Array<String> by rootProject.extra
val kotlinApiVersionForProjectsUsedInIntelliJKotlinPlugin: String by rootProject.extra
tasks.withType<JavaExec> {
workingDir = rootProject.projectDir
doFirst {
args = projectsUsedInIntelliJKotlinPlugin.flatMap {
project(it).extensions
.findByType(JavaPluginExtension::class.java)
?.sourceSets?.flatMap { sourceSet ->
sourceSet.allSource.srcDirs.map { it.path }
}.orEmpty()
}
}
}
tasks.register("checkIdeDependenciesConfiguration") {
doFirst {
for (projectName in projectsUsedInIntelliJKotlinPlugin) {
project(projectName).checkIdeDependencyConfiguration()
}
}
}
fun Project.checkIdeDependencyConfiguration() {
val expectedApiVersion = KotlinVersion.fromVersion(kotlinApiVersionForProjectsUsedInIntelliJKotlinPlugin)
for (compileTask in tasks.withType<KotlinJvmCompile>()) {
val projectApiVersion = compileTask.compilerOptions.apiVersion.get()
check(projectApiVersion <= expectedApiVersion) {
"Expected the API Version to be less or equal to `$kotlinApiVersionForProjectsUsedInIntelliJKotlinPlugin`" +
" for the project `$name`, " +
"but `$projectApiVersion` found. The project is used in the IntelliJ, so it should use the same API version" +
"for binary compatibility with Kotlin stdlib . " +
"See KT-62510 for details."
}
val enabledExperimentalAnnotations =
ExperimentalAnnotationsCollector().getUsedExperimentalAnnotations(compileTask.kotlinOptions.freeCompilerArgs)
check(enabledExperimentalAnnotations.isEmpty()) {
"`$name` allows using experimental kotlin stdlib API marked with ${enabledExperimentalAnnotations.joinToString()}. " +
"The project is used in the IntelliJ Kotlin Plugin, so it cannot use experimental Kotlin stdlib API " +
"for binary compatibility with Kotlin stdlib . " +
"See KT-62510 for details."
}
}
}
tasks.register("checkIdeDependencies") {
dependsOn("checkIdeDependenciesConfiguration")
dependsOn("run")
}
val validatorProject: Project get() = project
private class ExperimentalAnnotationsCollector() {
val experimentalAnnotations: Set<String> by lazy {
validatorProject.projectDir.toPath().resolve(EXPERIMENTAL_ANNOTATIONS_FILE)
.readLines()
.map { it.trim() }
.filterNot { it.startsWith("#") || it.isBlank() }
.toSet()
}
fun getUsedExperimentalAnnotations(arguments: List<String>): List<String> {
return buildList {
addAll(getOptInAnnotationsByMultipleArguments(arguments))
arguments.flatMapTo(this) { getOptInAnnotationsBySingleArgument(it) }
removeAll { it !in experimentalAnnotations }
}
}
/**
* Returns a list of experimental annotation used in an argument list of kind `["-opt-in", "kotlin.ExperimentalStdlibApi,kotlin.time.ExperimentalTime"]`
*/
private fun getOptInAnnotationsByMultipleArguments(arguments: List<String>): List<String> {
return arguments.windowed(2).flatMap { (argumentName, value) ->
if (argumentName == "-Xopt-in" || argumentName == "-opt-in") value.split(",").map { it.trim() }
else emptyList()
}
}
private fun getOptInAnnotationsBySingleArgument(argument: String): List<String> {
@Suppress("NAME_SHADOWING")
var argument = argument.trim()
argument = when {
argument.startsWith("-opt-in=") -> {
argument.removePrefix("-opt-in=")
}
argument.startsWith("-Xopt-in=") -> {
argument.removePrefix("-Xopt-in=")
}
else -> {
return emptyList()
}
}
return argument.split(",").map { it.trim() }
}
companion object {
private const val EXPERIMENTAL_ANNOTATIONS_FILE = "ExperimentalAnnotations.txt"
}
}
sourceSets {
"main" { projectDefault() }
"test" { projectDefault() }
}