[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
This commit is contained in:
committed by
Space Team
parent
ded5cf2caa
commit
a65c735feb
@@ -286,6 +286,7 @@
|
||||
/libraries/tools/atomicfu/ "Kotlin Libraries"
|
||||
/libraries/tools/binary-compatibility-validator/ "Kotlin Libraries"
|
||||
/libraries/tools/dukat/ "Kotlin JS"
|
||||
/libraries/tools/ide-plugin-dependencies-validator "Kotlin IDE Analysis Core"
|
||||
/libraries/kotlin-dom-api-compat/ "Kotlin JS"
|
||||
/libraries/tools/kotlin-build-tools-enum-compat/ "Kotlin Build Tools"
|
||||
/libraries/tools/gradle/ "Kotlin Build Tools"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import org.gradle.crypto.checksum.Checksum
|
||||
import org.gradle.plugins.ide.idea.model.IdeaModel
|
||||
import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockMismatchReport
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
|
||||
|
||||
buildscript {
|
||||
// a workaround for kotlin compiler classpath in kotlin project: sometimes gradle substitutes
|
||||
@@ -68,6 +70,7 @@ val kotlinVersion by extra(
|
||||
)
|
||||
|
||||
val kotlinLanguageVersion: String by extra
|
||||
val kotlinApiVersionForModulesUsedInIDE: String by extra
|
||||
|
||||
extra["kotlin_root"] = rootDir
|
||||
|
||||
@@ -245,6 +248,98 @@ extra["compilerModules"] =
|
||||
commonCompilerModules +
|
||||
firAllCompilerModules
|
||||
|
||||
/**
|
||||
* An array of projects used in the IntelliJ Kotlin Plugin.
|
||||
*
|
||||
* Experimental declarations from Kotlin stdlib cannot be used in those projects to avoid stdlib binary compatibility problems.
|
||||
* See KT-62510 for details.
|
||||
*/
|
||||
val projectsUsedInIntelliJKotlinPlugin =
|
||||
fe10CompilerModules +
|
||||
commonCompilerModules +
|
||||
firCompilerCoreModules +
|
||||
irCompilerModulesForIDE +
|
||||
arrayOf(
|
||||
":analysis:analysis-api",
|
||||
":analysis:analysis-api-fe10",
|
||||
":analysis:analysis-api-fir",
|
||||
":analysis:analysis-api-impl-barebone",
|
||||
":analysis:analysis-api-impl-base",
|
||||
":analysis:analysis-api-providers",
|
||||
":analysis:analysis-api-standalone:analysis-api-standalone-base",
|
||||
":analysis:analysis-api-standalone:analysis-api-fir-standalone-base",
|
||||
":analysis:analysis-api-standalone",
|
||||
":analysis:analysis-internal-utils",
|
||||
":analysis:analysis-test-framework",
|
||||
":analysis:decompiled",
|
||||
":analysis:kt-references",
|
||||
":analysis:kt-references:kt-references-fe10",
|
||||
":analysis:light-classes-base",
|
||||
":analysis:low-level-api-fir",
|
||||
":analysis:project-structure",
|
||||
":analysis:symbol-light-classes",
|
||||
) +
|
||||
arrayOf(
|
||||
":kotlin-allopen-compiler-plugin.cli",
|
||||
":kotlin-allopen-compiler-plugin.common",
|
||||
":kotlin-allopen-compiler-plugin.k1",
|
||||
":kotlin-allopen-compiler-plugin.k2",
|
||||
|
||||
":kotlin-assignment-compiler-plugin.cli",
|
||||
":kotlin-assignment-compiler-plugin.common",
|
||||
":kotlin-assignment-compiler-plugin.k1",
|
||||
":kotlin-assignment-compiler-plugin.k2",
|
||||
|
||||
":plugins:parcelize:parcelize-compiler:parcelize.backend",
|
||||
":plugins:parcelize:parcelize-compiler:parcelize.cli",
|
||||
":plugins:parcelize:parcelize-compiler:parcelize.common",
|
||||
":plugins:parcelize:parcelize-compiler:parcelize.k1",
|
||||
":plugins:parcelize:parcelize-compiler:parcelize.k2",
|
||||
":plugins:parcelize:parcelize-runtime",
|
||||
|
||||
":kotlin-sam-with-receiver-compiler-plugin.cli",
|
||||
":kotlin-sam-with-receiver-compiler-plugin.common",
|
||||
":kotlin-sam-with-receiver-compiler-plugin.k1",
|
||||
":kotlin-sam-with-receiver-compiler-plugin.k2",
|
||||
|
||||
":kotlinx-serialization-compiler-plugin.cli",
|
||||
":kotlinx-serialization-compiler-plugin.common",
|
||||
":kotlinx-serialization-compiler-plugin.k1",
|
||||
":kotlinx-serialization-compiler-plugin.k2",
|
||||
":kotlinx-serialization-compiler-plugin.backend",
|
||||
|
||||
":kotlin-lombok-compiler-plugin.cli",
|
||||
":kotlin-lombok-compiler-plugin.common",
|
||||
":kotlin-lombok-compiler-plugin.k1",
|
||||
":kotlin-lombok-compiler-plugin.k2",
|
||||
|
||||
":kotlin-noarg-compiler-plugin.cli",
|
||||
":kotlin-noarg-compiler-plugin.common",
|
||||
":kotlin-noarg-compiler-plugin.k1",
|
||||
":kotlin-noarg-compiler-plugin.k2",
|
||||
":kotlin-noarg-compiler-plugin.backend",
|
||||
|
||||
":plugins:android-extensions-compiler",
|
||||
|
||||
":kotlin-sam-with-receiver-compiler-plugin.cli",
|
||||
":kotlin-sam-with-receiver-compiler-plugin.common",
|
||||
":kotlin-sam-with-receiver-compiler-plugin.k1",
|
||||
":kotlin-sam-with-receiver-compiler-plugin.k2",
|
||||
|
||||
|
||||
":kotlin-compiler-runner-unshaded",
|
||||
":kotlin-preloader",
|
||||
":daemon-common",
|
||||
":kotlin-daemon-client",
|
||||
|
||||
":kotlin-scripting-compiler",
|
||||
":kotlin-gradle-statistics",
|
||||
":jps:jps-common",
|
||||
) +
|
||||
if (kotlinBuildProperties.isKotlinNativeEnabled) arrayOf(":kotlin-native:backend.native") else emptyArray()
|
||||
|
||||
extra["projectsUsedInIntelliJKotlinPlugin"] = projectsUsedInIntelliJKotlinPlugin
|
||||
|
||||
// They are embedded just because we don't publish those dependencies as separate Maven artifacts (yet)
|
||||
extra["kotlinJpsPluginEmbeddedDependencies"] = listOf(
|
||||
":compiler:cli-common",
|
||||
@@ -786,6 +881,7 @@ tasks {
|
||||
dependsOn(":kotlin-tooling-core:check")
|
||||
dependsOn(":kotlin-tooling-metadata:check")
|
||||
dependsOn(":compiler:build-tools:kotlin-build-tools-api:check")
|
||||
dependsOn(":tools:ide-plugin-dependencies-validator:test")
|
||||
}
|
||||
|
||||
register("examplesTest") {
|
||||
|
||||
@@ -29,6 +29,10 @@ cacheRedirectorEnabled=true
|
||||
defaultSnapshotVersion=2.0.255-SNAPSHOT
|
||||
kotlinLanguageVersion=2.0
|
||||
|
||||
# Should be less or equal to the Kotlin stdlib version used inside IntelliJ IDEA repository, see KT-62510.
|
||||
# IntelliJ IDEA Kotlin stdlib version can be found at https://github.com/JetBrains/intellij-community/blob/master/.idea/libraries/kotlin_stdlib.xml
|
||||
kotlinApiVersionForProjectsUsedInIntelliJKotlinPlugin=1.9
|
||||
|
||||
kotlin.build.gradlePlugin.version=0.0.40
|
||||
|
||||
#maven.repository.mirror=http://repository.jetbrains.com/remote-repos/
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# The list of experimental Kotlin stdlib annotations.
|
||||
# Declarations from kotlin stdlib marked with those annotations cannot be used in the projects
|
||||
# that are published for the IJ Kotlin plugin to avoid stdlib binary compatibility problems.
|
||||
# See KT-62510 for details.
|
||||
# The list should be updated as new experimental annotations are added or removed from the stdlib.
|
||||
kotlin.ExperimentalStdlibApi
|
||||
kotlin.io.path.ExperimentalPathApi
|
||||
kotlin.io.encoding.ExperimentalEncodingApi
|
||||
kotlin.time.ExperimentalTime
|
||||
kotlin.ExperimentalUnsignedTypes
|
||||
@@ -0,0 +1,42 @@
|
||||
# IDE Plugin Dependencies Validator
|
||||
|
||||
Some projects inside the Kotlin repository are used inside the IntelliJ Kotlin plugin. Those projects have special restrictions forbidding
|
||||
experimental Kotlin stdlib API use in them. The `:tools:ide-plugin-dependencies-validator` project is a tool to check that those
|
||||
restrictions are not violated.
|
||||
|
||||
See [KTIJ-20529](https://youtrack.jetbrains.com/issue/KTIJ-20529).
|
||||
|
||||
## Details
|
||||
|
||||
IntelliJ IDEA has its own bundled Kotlin stdlib, which is used across the IntelliJ repository. This stdlib is usually the latest available
|
||||
stable Kotlin stdlib. Kotlin repository is compiled against a snapshot Kotlin stdlib. So, the projects from the Kotlin repository,
|
||||
which bundle to IntelliJ, are compiled against snapshot stdlib, but on the runtime in IntelliJ, there is a fixed and stable version of
|
||||
Kotlin stdlib. To avoid binary compatibility problems that may arise at runtime in IntelliJ, experimental stdlib declarations for which
|
||||
binary compatibility is not guaranteed are not allowed to be used inside Kotlin projects used in IntelliJ.
|
||||
|
||||
The list of such projects used inside IntelliJ Kotlin Plugin can be found at `projectsUsedInIntelliJKotlinPlugin` property
|
||||
inside [the root build.gradle.kts](build.gradle.kts)
|
||||
|
||||
## Checks
|
||||
|
||||
The tool checks on all projects defined in `projectsUsedInIntelliJKotlinPlugin`.
|
||||
|
||||
* No opt-ins for experimental stdlib annotations are used inside the source code. The check works by syntax only, and no code resolution is
|
||||
performed. This is not a hundred percent reliable, but it usually works unless there is a name conflict with experimental opt-in
|
||||
annotations
|
||||
from stdlib. The code for those checks can be found at `org.jetbrains.Kotlin.ide.plugin.dependencies.validator` package.
|
||||
* The Kotlin API version used is the same as the version of Kotlin stdlib used inside the IntelliJ Platform. This is defined
|
||||
by `kotlinApiVersionForProjectsUsedInIntelliJKotlinPlugin` property.
|
||||
* No opt-ins for experimental stdlib annotations are used inside the build definition files. The check works by
|
||||
checking that no `-opt-in` arguments with experimental stdlib annotations are defined
|
||||
in `KotlinJvmCompile.KotlinOptions.freeCompilerArgs`.
|
||||
|
||||
## Running Checks
|
||||
|
||||
`gradle :tools:ide-plugin-dependencies-validator:checkIdeDependenciesConfiguration` task runs the check. It consists of two subtasks:
|
||||
|
||||
* `gradle :tools:ide-plugin-dependencies-validator:checkIdeDependenciesConfiguration` task checks the build configuration of projects.
|
||||
* `gradle :tools:ide-plugin-dependencies-validator:run` task checks Kotlin source files inside the project for experimental annotations
|
||||
usages.
|
||||
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
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() }
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2010-2023 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.ide.plugin.dependencies.validator
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
data class ExperimentalAnnotationUsage(
|
||||
val file: Path,
|
||||
val lineNumber: Int,
|
||||
val usedExperimentalAnnotation: String,
|
||||
) {
|
||||
override fun hashCode(): Int {
|
||||
var result = super.hashCode()
|
||||
result = 31 * result + file.toAbsolutePath().hashCode()
|
||||
result = 31 * result + lineNumber
|
||||
result = 31 * result + usedExperimentalAnnotation.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as ExperimentalAnnotationUsage
|
||||
|
||||
if (file.toAbsolutePath() != other.file.toAbsolutePath()) return false
|
||||
if (lineNumber != other.lineNumber) return false
|
||||
if (usedExperimentalAnnotation != other.usedExperimentalAnnotation) return false
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2010-2023 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.ide.plugin.dependencies.validator
|
||||
|
||||
import java.nio.file.Paths
|
||||
import kotlin.io.path.readLines
|
||||
|
||||
object ExperimentalAnnotations {
|
||||
val experimentalAnnotations: Set<String> by lazy {
|
||||
Paths.get(EXPERIMENTAL_ANNOTATIONS_PATH)
|
||||
.readLines()
|
||||
.map { it.trim() }
|
||||
.filterNot { it.startsWith("#") || it.isBlank() }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
|
||||
val experimentalAnnotationShortNames: Set<String> by lazy {
|
||||
experimentalAnnotations.mapTo(mutableSetOf()) { it.substringAfterLast('.') }
|
||||
}
|
||||
|
||||
private const val EXPERIMENTAL_ANNOTATIONS_PATH =
|
||||
"libraries/tools/ide-plugin-dependencies-validator/ExperimentalAnnotations.txt"
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2010-2023 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.ide.plugin.dependencies.validator
|
||||
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.openapi.util.text.StringUtil
|
||||
import com.intellij.psi.impl.PsiFileFactoryImpl
|
||||
import org.jetbrains.kotlin.psi.KtAnnotationEntry
|
||||
import org.jetbrains.kotlin.psi.KtClassLiteralExpression
|
||||
import org.jetbrains.kotlin.psi.KtFile
|
||||
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
|
||||
import org.jetbrains.kotlin.psi.KtValueArgument
|
||||
import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType
|
||||
import org.jetbrains.kotlin.psi.psiUtil.startOffset
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.extension
|
||||
import kotlin.streams.asSequence
|
||||
import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
|
||||
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
|
||||
import org.jetbrains.kotlin.config.CompilerConfiguration
|
||||
import org.jetbrains.kotlin.idea.KotlinLanguage
|
||||
import kotlin.io.path.name
|
||||
import kotlin.io.path.readText
|
||||
|
||||
object ExperimentalOptInUsageInSourceChecker {
|
||||
fun checkExperimentalOptInUsage(srcRoots: List<Path>): List<ExperimentalAnnotationUsage> {
|
||||
val project = createProjectForParsing()
|
||||
try {
|
||||
return srcRoots.filter { it.exists() }
|
||||
.flatMap { srcRoot ->
|
||||
Files.walk(srcRoot)
|
||||
.asSequence()
|
||||
.filter { it.extension == "kt" }
|
||||
.flatMap { file ->
|
||||
val ktFile = file.parseAsKtFile(project)
|
||||
checkExperimentalOptInUsage(ktFile, file)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Disposer.dispose(project)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkExperimentalOptInUsage(ktFile: KtFile, file: Path): List<ExperimentalAnnotationUsage> {
|
||||
return ktFile
|
||||
.collectDescendantsOfType<KtAnnotationEntry>()
|
||||
.flatMap { annotationEntry ->
|
||||
val annotationShortName = annotationEntry.shortName?.asString() ?: return@flatMap emptyList()
|
||||
val experimentalAnnotations = when (annotationShortName) {
|
||||
OPT_IN_ANNOTATION -> {
|
||||
annotationEntry.valueArguments.mapNotNull mapArguments@{ argument ->
|
||||
if (argument !is KtValueArgument) return@mapArguments null
|
||||
val expression = argument.getArgumentExpression() as? KtClassLiteralExpression ?: return@mapArguments null
|
||||
val classReference = expression.receiverExpression as? KtNameReferenceExpression ?: return@mapArguments null
|
||||
classReference.getReferencedName().takeIf { it in ExperimentalAnnotations.experimentalAnnotationShortNames }
|
||||
}
|
||||
}
|
||||
in ExperimentalAnnotations.experimentalAnnotationShortNames -> {
|
||||
listOf(annotationShortName)
|
||||
}
|
||||
else -> return@flatMap emptyList()
|
||||
}
|
||||
if (experimentalAnnotations.isEmpty()) return@flatMap emptyList()
|
||||
|
||||
/* offsetToLineNumber's indexing starts from 0*/
|
||||
val lineNumber = StringUtil.offsetToLineNumber(ktFile.text, annotationEntry.startOffset) + 1
|
||||
|
||||
experimentalAnnotations.map { annotation ->
|
||||
ExperimentalAnnotationUsage(file, lineNumber, annotation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Path.parseAsKtFile(project: Project): KtFile {
|
||||
return PsiFileFactoryImpl(project).createFileFromText(name, KotlinLanguage.INSTANCE, readText()) as KtFile
|
||||
}
|
||||
|
||||
private fun createProjectForParsing(): Project {
|
||||
return KotlinCoreEnvironment.createForProduction(
|
||||
Disposer.newDisposable(),
|
||||
CompilerConfiguration(),
|
||||
EnvironmentConfigFiles.JVM_CONFIG_FILES
|
||||
).project
|
||||
}
|
||||
|
||||
private const val OPT_IN_ANNOTATION = "OptIn"
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2010-2023 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.ide.plugin.dependencies.validator
|
||||
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import kotlin.io.path.absolute
|
||||
import kotlin.io.path.relativeTo
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
check(args.isNotEmpty())
|
||||
|
||||
val experimentalAnnotationUsages =
|
||||
ExperimentalOptInUsageInSourceChecker.checkExperimentalOptInUsage(args.map { Paths.get(it) })
|
||||
.filterNot { usage -> allowedUsages.any { it.isAllowed(usage) } }
|
||||
|
||||
if (experimentalAnnotationUsages.isNotEmpty()) {
|
||||
val errorMessage = buildString {
|
||||
appendLine(
|
||||
"""
|
||||
Experimental Kotlin StdLib API cannot be used in the modules that are used in the IDE.
|
||||
See KT-62510 for more details.
|
||||
The following files use deprecated APIs:
|
||||
""".trimIndent()
|
||||
)
|
||||
for (annotationUsage in experimentalAnnotationUsages) {
|
||||
appendLine(
|
||||
" - " + annotationUsage.file.relativeTo(Paths.get(".").toAbsolutePath()).toString() +
|
||||
":" + annotationUsage.lineNumber +
|
||||
" has an opt-in for experimental declaration: @OptIn(${annotationUsage.usedExperimentalAnnotation}::class)"
|
||||
)
|
||||
}
|
||||
}
|
||||
error(errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Please do not add new usages here, it may break IntelliJ Kotlin Plugin. See KT-62510 for more details
|
||||
private val allowedUsages = listOf(
|
||||
// TODO should be removed as a part of KTIJ-27368
|
||||
AllowedUsage(
|
||||
Paths.get("compiler/ir/serialization.common/src/org/jetbrains/kotlin/backend/common/serialization/CityHash.kt"),
|
||||
"ExperimentalUnsignedTypes"
|
||||
)
|
||||
)
|
||||
|
||||
private data class AllowedUsage(
|
||||
val file: Path,
|
||||
val usedExperimentalAnnotation: String,
|
||||
) {
|
||||
fun isAllowed(usage: ExperimentalAnnotationUsage) =
|
||||
usage.file.absolute() == file.absolute()
|
||||
&& usedExperimentalAnnotation == usage.usedExperimentalAnnotation
|
||||
}
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
class Usage {
|
||||
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class, OtherOptIn::class)
|
||||
class WithOtherArgument {
|
||||
|
||||
}
|
||||
|
||||
fun x() {
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
call()
|
||||
}
|
||||
|
||||
@OptIn()
|
||||
fun empty() {
|
||||
|
||||
}
|
||||
|
||||
@ExperimentalStdlibApi
|
||||
fun direct() {
|
||||
|
||||
}
|
||||
|
||||
@kotlin.io.path.ExperimentalPathApi
|
||||
fun fqName() {
|
||||
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
|
||||
@OptIn(ExperimentalPathApi::class, ExperimentalStdlibApi::class)
|
||||
class Usage {
|
||||
|
||||
}
|
||||
|
||||
@ExperimentalPathApi
|
||||
@ExperimentalStdlibApi
|
||||
fun direct() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@kotlin.io.path.ExperimentalPathApi
|
||||
@kotlin.io.path.ExperimentalPathApi
|
||||
fun fqName() {
|
||||
|
||||
}
|
||||
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
@OptIn(ExperimentalPathApi::class)
|
||||
class Usage {
|
||||
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPathApi::class, OtherOptIn::class)
|
||||
class WithOtherArgument {
|
||||
|
||||
}
|
||||
|
||||
fun x() {
|
||||
@OptIn(ExperimentalPathApi::class)
|
||||
call()
|
||||
}
|
||||
|
||||
@OptIn()
|
||||
fun y() {
|
||||
|
||||
}
|
||||
|
||||
@ExperimentalPathApi
|
||||
fun direct() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@kotlin.io.path.ExperimentalPathApi
|
||||
fun fqName() {
|
||||
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2010-2023 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.ide.plugin.dependencies.validator
|
||||
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.nio.file.Paths
|
||||
import kotlin.io.path.absolute
|
||||
|
||||
class ExperimentalOptInUsageInSourceCheckerTest {
|
||||
@Test
|
||||
fun test() {
|
||||
println(Paths.get(".").toAbsolutePath())
|
||||
val sourcePath = basePath.resolve("testData/source")
|
||||
val usages = ExperimentalOptInUsageInSourceChecker.checkExperimentalOptInUsage(listOf(sourcePath))
|
||||
Assertions.assertEquals(
|
||||
listOf(
|
||||
ExperimentalAnnotationUsage(Paths.get("pckg/experimentalPathApi.kt"), lineNumber = 1, "ExperimentalPathApi"),
|
||||
ExperimentalAnnotationUsage(Paths.get("pckg/experimentalPathApi.kt"), lineNumber = 6, "ExperimentalPathApi"),
|
||||
ExperimentalAnnotationUsage(Paths.get("pckg/experimentalPathApi.kt"), lineNumber = 12, "ExperimentalPathApi"),
|
||||
ExperimentalAnnotationUsage(Paths.get("pckg/experimentalPathApi.kt"), lineNumber = 21, "ExperimentalPathApi"),
|
||||
ExperimentalAnnotationUsage(Paths.get("pckg/experimentalPathApi.kt"), lineNumber = 27, "ExperimentalPathApi"),
|
||||
ExperimentalAnnotationUsage(Paths.get("experimentalStdlibApi.kt"), lineNumber = 1, "ExperimentalStdlibApi"),
|
||||
ExperimentalAnnotationUsage(Paths.get("experimentalStdlibApi.kt"), lineNumber = 6, "ExperimentalStdlibApi"),
|
||||
ExperimentalAnnotationUsage(Paths.get("experimentalStdlibApi.kt"), lineNumber = 12, "ExperimentalStdlibApi"),
|
||||
ExperimentalAnnotationUsage(Paths.get("experimentalStdlibApi.kt"), lineNumber = 21, "ExperimentalStdlibApi"),
|
||||
ExperimentalAnnotationUsage(Paths.get("experimentalStdlibApi.kt"), lineNumber = 26, "ExperimentalPathApi"),
|
||||
ExperimentalAnnotationUsage(Paths.get("multiple.kt"), lineNumber = 2, "ExperimentalPathApi"),
|
||||
ExperimentalAnnotationUsage(Paths.get("multiple.kt"), lineNumber = 2, "ExperimentalStdlibApi"),
|
||||
ExperimentalAnnotationUsage(Paths.get("multiple.kt"), lineNumber = 7, "ExperimentalPathApi"),
|
||||
ExperimentalAnnotationUsage(Paths.get("multiple.kt"), lineNumber = 8, "ExperimentalStdlibApi"),
|
||||
ExperimentalAnnotationUsage(Paths.get("multiple.kt"), lineNumber = 14, "ExperimentalPathApi"),
|
||||
ExperimentalAnnotationUsage(Paths.get("multiple.kt"), lineNumber = 15, "ExperimentalPathApi")
|
||||
).map { it.copy(file = sourcePath.resolve(it.file)) }.sortedWith(experimentalAnnotationUsageComparator),
|
||||
usages.sortedWith(experimentalAnnotationUsageComparator),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val experimentalAnnotationUsageComparator =
|
||||
compareBy<ExperimentalAnnotationUsage>(
|
||||
{ it.file.absolute().toString() },
|
||||
{ it.lineNumber },
|
||||
{ it.usedExperimentalAnnotation },
|
||||
)
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* Copyright 2010-2023 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.ide.plugin.dependencies.validator
|
||||
|
||||
import java.nio.file.Paths
|
||||
|
||||
val basePath = Paths.get("libraries/tools/ide-plugin-dependencies-validator")
|
||||
+6
@@ -114,6 +114,9 @@ fun Project.configureJavaBasePlugin() {
|
||||
}
|
||||
}
|
||||
|
||||
val projectsUsedInIntelliJKotlinPlugin: Array<String> by rootProject.extra
|
||||
val kotlinApiVersionForProjectsUsedInIntelliJKotlinPlugin: String by rootProject.extra
|
||||
|
||||
fun Project.configureKotlinCompilationOptions() {
|
||||
plugins.withType<KotlinBasePluginWrapper> {
|
||||
val commonCompilerArgs = listOfNotNull(
|
||||
@@ -152,6 +155,9 @@ fun Project.configureKotlinCompilationOptions() {
|
||||
apiVersion = kotlinLanguageVersion
|
||||
freeCompilerArgs += "-Xskip-prerelease-check"
|
||||
}
|
||||
if (project.path in projectsUsedInIntelliJKotlinPlugin) {
|
||||
apiVersion = kotlinApiVersionForProjectsUsedInIntelliJKotlinPlugin
|
||||
}
|
||||
if (KotlinVersion.DEFAULT >= KotlinVersion.KOTLIN_2_0 && forced19) {
|
||||
options.progressiveMode.set(false)
|
||||
}
|
||||
|
||||
@@ -300,6 +300,14 @@ fun Project.idePluginDependency(block: () -> Unit) {
|
||||
}
|
||||
|
||||
fun Project.publishJarsForIde(projects: List<String>, libraryDependencies: List<String> = emptyList()) {
|
||||
val projectsUsedInIntelliJKotlinPlugin: Array<String> by rootProject.extra
|
||||
|
||||
for (projectName in projects) {
|
||||
check(projectName in projectsUsedInIntelliJKotlinPlugin) {
|
||||
"`$projectName` is used in IntelliJ Kotlin Plugin, it should be added to `extra[\"projectsUsedInIntelliJKotlinPlugin\"]`"
|
||||
}
|
||||
}
|
||||
|
||||
idePluginDependency {
|
||||
publishProjectJars(projects, libraryDependencies)
|
||||
}
|
||||
|
||||
+3
-1
@@ -575,7 +575,8 @@ include ":generators:analysis-api-generator",
|
||||
":analysis:decompiled:native",
|
||||
":analysis:decompiled:light-classes-for-decompiled",
|
||||
":analysis:decompiled:light-classes-for-decompiled-fe10",
|
||||
":prepare:analysis-api-test-framework"
|
||||
":prepare:analysis-api-test-framework",
|
||||
":tools:ide-plugin-dependencies-validator"
|
||||
|
||||
|
||||
if (buildProperties.inJpsBuildIdeaSync) {
|
||||
@@ -615,6 +616,7 @@ if (buildProperties.inJpsBuildIdeaSync) {
|
||||
project(':tools:binary-compatibility-validator').projectDir = "$rootDir/libraries/tools/binary-compatibility-validator" as File
|
||||
project(':tools:jdk-api-validator').projectDir = "$rootDir/libraries/tools/jdk-api-validator" as File
|
||||
project(':tools:kotlin-stdlib-gen').projectDir = "$rootDir/libraries/tools/kotlin-stdlib-gen" as File
|
||||
project(':tools:ide-plugin-dependencies-validator').projectDir = "$rootDir/libraries/tools/ide-plugin-dependencies-validator" as File
|
||||
|
||||
project(':kotlin-test').projectDir = "$rootDir/libraries/kotlin.test" as File
|
||||
project(':kotlin-test:kotlin-test-js-ir').projectDir = "$rootDir/libraries/kotlin.test/js-ir" as File
|
||||
|
||||
Reference in New Issue
Block a user