diff --git a/.space/CODEOWNERS b/.space/CODEOWNERS index 59abfcafc89..7ef8fd11df9 100644 --- a/.space/CODEOWNERS +++ b/.space/CODEOWNERS @@ -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" diff --git a/build.gradle.kts b/build.gradle.kts index 314bc758f8c..1443982a65f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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") { diff --git a/gradle.properties b/gradle.properties index b18aee9fea6..4a616e45bd4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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/ diff --git a/libraries/tools/ide-plugin-dependencies-validator/ExperimentalAnnotations.txt b/libraries/tools/ide-plugin-dependencies-validator/ExperimentalAnnotations.txt new file mode 100644 index 00000000000..7af2bde08af --- /dev/null +++ b/libraries/tools/ide-plugin-dependencies-validator/ExperimentalAnnotations.txt @@ -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 diff --git a/libraries/tools/ide-plugin-dependencies-validator/README.md b/libraries/tools/ide-plugin-dependencies-validator/README.md new file mode 100644 index 00000000000..37f789a293b --- /dev/null +++ b/libraries/tools/ide-plugin-dependencies-validator/README.md @@ -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. + + diff --git a/libraries/tools/ide-plugin-dependencies-validator/build.gradle.kts b/libraries/tools/ide-plugin-dependencies-validator/build.gradle.kts new file mode 100644 index 00000000000..1e93ceb7995 --- /dev/null +++ b/libraries/tools/ide-plugin-dependencies-validator/build.gradle.kts @@ -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 by rootProject.extra +val kotlinApiVersionForProjectsUsedInIntelliJKotlinPlugin: String by rootProject.extra + +tasks.withType { + 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()) { + 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 by lazy { + validatorProject.projectDir.toPath().resolve(EXPERIMENTAL_ANNOTATIONS_FILE) + .readLines() + .map { it.trim() } + .filterNot { it.startsWith("#") || it.isBlank() } + .toSet() + } + + fun getUsedExperimentalAnnotations(arguments: List): List { + 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): List { + 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 { + @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() } +} diff --git a/libraries/tools/ide-plugin-dependencies-validator/src/org/jetbrains/kotlin/ide/plugin/dependencies/validator/ExperimentalAnnotationUsage.kt b/libraries/tools/ide-plugin-dependencies-validator/src/org/jetbrains/kotlin/ide/plugin/dependencies/validator/ExperimentalAnnotationUsage.kt new file mode 100644 index 00000000000..8c5eb85ba5f --- /dev/null +++ b/libraries/tools/ide-plugin-dependencies-validator/src/org/jetbrains/kotlin/ide/plugin/dependencies/validator/ExperimentalAnnotationUsage.kt @@ -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 + } +} diff --git a/libraries/tools/ide-plugin-dependencies-validator/src/org/jetbrains/kotlin/ide/plugin/dependencies/validator/ExperimentalAnnotations.kt b/libraries/tools/ide-plugin-dependencies-validator/src/org/jetbrains/kotlin/ide/plugin/dependencies/validator/ExperimentalAnnotations.kt new file mode 100644 index 00000000000..7a5bda8f6b0 --- /dev/null +++ b/libraries/tools/ide-plugin-dependencies-validator/src/org/jetbrains/kotlin/ide/plugin/dependencies/validator/ExperimentalAnnotations.kt @@ -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 by lazy { + Paths.get(EXPERIMENTAL_ANNOTATIONS_PATH) + .readLines() + .map { it.trim() } + .filterNot { it.startsWith("#") || it.isBlank() } + .toSet() + } + + + val experimentalAnnotationShortNames: Set by lazy { + experimentalAnnotations.mapTo(mutableSetOf()) { it.substringAfterLast('.') } + } + + private const val EXPERIMENTAL_ANNOTATIONS_PATH = + "libraries/tools/ide-plugin-dependencies-validator/ExperimentalAnnotations.txt" +} \ No newline at end of file diff --git a/libraries/tools/ide-plugin-dependencies-validator/src/org/jetbrains/kotlin/ide/plugin/dependencies/validator/ExperimentalOptInUsageInSourceChecker.kt b/libraries/tools/ide-plugin-dependencies-validator/src/org/jetbrains/kotlin/ide/plugin/dependencies/validator/ExperimentalOptInUsageInSourceChecker.kt new file mode 100644 index 00000000000..ec45d4748aa --- /dev/null +++ b/libraries/tools/ide-plugin-dependencies-validator/src/org/jetbrains/kotlin/ide/plugin/dependencies/validator/ExperimentalOptInUsageInSourceChecker.kt @@ -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): List { + 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 { + return ktFile + .collectDescendantsOfType() + .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" +} \ No newline at end of file diff --git a/libraries/tools/ide-plugin-dependencies-validator/src/org/jetbrains/kotlin/ide/plugin/dependencies/validator/main.kt b/libraries/tools/ide-plugin-dependencies-validator/src/org/jetbrains/kotlin/ide/plugin/dependencies/validator/main.kt new file mode 100644 index 00000000000..6fec1b23510 --- /dev/null +++ b/libraries/tools/ide-plugin-dependencies-validator/src/org/jetbrains/kotlin/ide/plugin/dependencies/validator/main.kt @@ -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) { + 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 +} + diff --git a/libraries/tools/ide-plugin-dependencies-validator/testData/source/experimentalStdlibApi.kt b/libraries/tools/ide-plugin-dependencies-validator/testData/source/experimentalStdlibApi.kt new file mode 100644 index 00000000000..7eac5bc769c --- /dev/null +++ b/libraries/tools/ide-plugin-dependencies-validator/testData/source/experimentalStdlibApi.kt @@ -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() { + +} diff --git a/libraries/tools/ide-plugin-dependencies-validator/testData/source/multiple.kt b/libraries/tools/ide-plugin-dependencies-validator/testData/source/multiple.kt new file mode 100644 index 00000000000..7b71071f14c --- /dev/null +++ b/libraries/tools/ide-plugin-dependencies-validator/testData/source/multiple.kt @@ -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() { + +} diff --git a/libraries/tools/ide-plugin-dependencies-validator/testData/source/pckg/experimentalPathApi.kt b/libraries/tools/ide-plugin-dependencies-validator/testData/source/pckg/experimentalPathApi.kt new file mode 100644 index 00000000000..845f6deb3f9 --- /dev/null +++ b/libraries/tools/ide-plugin-dependencies-validator/testData/source/pckg/experimentalPathApi.kt @@ -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() { + +} \ No newline at end of file diff --git a/libraries/tools/ide-plugin-dependencies-validator/tests/org/jetbrains/kotlin/ide/plugin/dependencies/validator/ExperimentalOptInUsageInSourceCheckerTest.kt b/libraries/tools/ide-plugin-dependencies-validator/tests/org/jetbrains/kotlin/ide/plugin/dependencies/validator/ExperimentalOptInUsageInSourceCheckerTest.kt new file mode 100644 index 00000000000..f512dd72352 --- /dev/null +++ b/libraries/tools/ide-plugin-dependencies-validator/tests/org/jetbrains/kotlin/ide/plugin/dependencies/validator/ExperimentalOptInUsageInSourceCheckerTest.kt @@ -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( + { it.file.absolute().toString() }, + { it.lineNumber }, + { it.usedExperimentalAnnotation }, + ) + } +} \ No newline at end of file diff --git a/libraries/tools/ide-plugin-dependencies-validator/tests/org/jetbrains/kotlin/ide/plugin/dependencies/validator/testUtils.kt b/libraries/tools/ide-plugin-dependencies-validator/tests/org/jetbrains/kotlin/ide/plugin/dependencies/validator/testUtils.kt new file mode 100644 index 00000000000..a7bc41c9c3d --- /dev/null +++ b/libraries/tools/ide-plugin-dependencies-validator/tests/org/jetbrains/kotlin/ide/plugin/dependencies/validator/testUtils.kt @@ -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") \ No newline at end of file diff --git a/repo/gradle-build-conventions/buildsrc-compat/src/main/kotlin/common-configuration.gradle.kts b/repo/gradle-build-conventions/buildsrc-compat/src/main/kotlin/common-configuration.gradle.kts index 8d922d106c9..5dad83f0f73 100644 --- a/repo/gradle-build-conventions/buildsrc-compat/src/main/kotlin/common-configuration.gradle.kts +++ b/repo/gradle-build-conventions/buildsrc-compat/src/main/kotlin/common-configuration.gradle.kts @@ -114,6 +114,9 @@ fun Project.configureJavaBasePlugin() { } } +val projectsUsedInIntelliJKotlinPlugin: Array by rootProject.extra +val kotlinApiVersionForProjectsUsedInIntelliJKotlinPlugin: String by rootProject.extra + fun Project.configureKotlinCompilationOptions() { plugins.withType { 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) } diff --git a/repo/gradle-build-conventions/buildsrc-compat/src/main/kotlin/repoArtifacts.kt b/repo/gradle-build-conventions/buildsrc-compat/src/main/kotlin/repoArtifacts.kt index 43b2703ba22..ab7cd6de938 100644 --- a/repo/gradle-build-conventions/buildsrc-compat/src/main/kotlin/repoArtifacts.kt +++ b/repo/gradle-build-conventions/buildsrc-compat/src/main/kotlin/repoArtifacts.kt @@ -300,6 +300,14 @@ fun Project.idePluginDependency(block: () -> Unit) { } fun Project.publishJarsForIde(projects: List, libraryDependencies: List = emptyList()) { + val projectsUsedInIntelliJKotlinPlugin: Array 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) } diff --git a/settings.gradle b/settings.gradle index 99d31d4510a..51d361d3d12 100644 --- a/settings.gradle +++ b/settings.gradle @@ -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