From a65c735febff3acdbfa47ff9d0872ad6e7f50c4e Mon Sep 17 00:00:00 2001 From: Ilya Kirillov Date: Wed, 11 Oct 2023 19:27:29 +0200 Subject: [PATCH] [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 --- .space/CODEOWNERS | 1 + build.gradle.kts | 96 +++++++++++ gradle.properties | 4 + .../ExperimentalAnnotations.txt | 10 ++ .../README.md | 42 +++++ .../build.gradle.kts | 149 ++++++++++++++++++ .../validator/ExperimentalAnnotationUsage.kt | 35 ++++ .../validator/ExperimentalAnnotations.kt | 27 ++++ .../ExperimentalOptInUsageInSourceChecker.kt | 93 +++++++++++ .../ide/plugin/dependencies/validator/main.kt | 59 +++++++ .../testData/source/experimentalStdlibApi.kt | 29 ++++ .../testData/source/multiple.kt | 18 +++ .../source/pckg/experimentalPathApi.kt | 30 ++++ ...perimentalOptInUsageInSourceCheckerTest.kt | 50 ++++++ .../dependencies/validator/testUtils.kt | 10 ++ .../kotlin/common-configuration.gradle.kts | 6 + .../src/main/kotlin/repoArtifacts.kt | 8 + settings.gradle | 4 +- 18 files changed, 670 insertions(+), 1 deletion(-) create mode 100644 libraries/tools/ide-plugin-dependencies-validator/ExperimentalAnnotations.txt create mode 100644 libraries/tools/ide-plugin-dependencies-validator/README.md create mode 100644 libraries/tools/ide-plugin-dependencies-validator/build.gradle.kts create mode 100644 libraries/tools/ide-plugin-dependencies-validator/src/org/jetbrains/kotlin/ide/plugin/dependencies/validator/ExperimentalAnnotationUsage.kt create mode 100644 libraries/tools/ide-plugin-dependencies-validator/src/org/jetbrains/kotlin/ide/plugin/dependencies/validator/ExperimentalAnnotations.kt create mode 100644 libraries/tools/ide-plugin-dependencies-validator/src/org/jetbrains/kotlin/ide/plugin/dependencies/validator/ExperimentalOptInUsageInSourceChecker.kt create mode 100644 libraries/tools/ide-plugin-dependencies-validator/src/org/jetbrains/kotlin/ide/plugin/dependencies/validator/main.kt create mode 100644 libraries/tools/ide-plugin-dependencies-validator/testData/source/experimentalStdlibApi.kt create mode 100644 libraries/tools/ide-plugin-dependencies-validator/testData/source/multiple.kt create mode 100644 libraries/tools/ide-plugin-dependencies-validator/testData/source/pckg/experimentalPathApi.kt create mode 100644 libraries/tools/ide-plugin-dependencies-validator/tests/org/jetbrains/kotlin/ide/plugin/dependencies/validator/ExperimentalOptInUsageInSourceCheckerTest.kt create mode 100644 libraries/tools/ide-plugin-dependencies-validator/tests/org/jetbrains/kotlin/ide/plugin/dependencies/validator/testUtils.kt 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