Implemented preview of the prototype of the Kotlin Coverage plugin
diff --git a/libraries/tools/kotlin-coverage/kotlin-coverage-compiler-subplugin/src/build.gradle.kts b/libraries/tools/kotlin-coverage/kotlin-coverage-compiler-subplugin/src/build.gradle.kts new file mode 100644 index 0000000..eedb904 --- /dev/null +++ b/libraries/tools/kotlin-coverage/kotlin-coverage-compiler-subplugin/src/build.gradle.kts
@@ -0,0 +1,18 @@ +plugins { + id("gradle-plugin-common-configuration") +} + +dependencies { + commonApi(platform(project(":kotlin-gradle-plugins-bom"))) +} + +gradlePlugin { + plugins { + create("kotlinCoverage") { + id = "org.jetbrains.kotlin.plugin.coverage" + displayName = "Kotlin compiler plugin for collecting test coverage" + description = displayName + implementationClass = "org.jetbrains.kotlin.coverage.CoverageCompilerSubplugin" + } + } +}
diff --git a/libraries/tools/kotlin-coverage/kotlin-coverage-compiler-subplugin/src/common/kotlin/org/jetbrains/kotlin/coverage/CoverageCompilerSubplugin.kt b/libraries/tools/kotlin-coverage/kotlin-coverage-compiler-subplugin/src/common/kotlin/org/jetbrains/kotlin/coverage/CoverageCompilerSubplugin.kt new file mode 100644 index 0000000..ed337b0 --- /dev/null +++ b/libraries/tools/kotlin-coverage/kotlin-coverage-compiler-subplugin/src/common/kotlin/org/jetbrains/kotlin/coverage/CoverageCompilerSubplugin.kt
@@ -0,0 +1,34 @@ +/* + * Copyright 2010-2025 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.coverage + +import org.gradle.api.Project +import org.gradle.api.provider.Provider +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilerPluginSupportPlugin +import org.jetbrains.kotlin.gradle.plugin.SubpluginArtifact +import org.jetbrains.kotlin.gradle.plugin.SubpluginOption + +class CoverageCompilerSubplugin : KotlinCompilerPluginSupportPlugin { + override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean = true + + override fun applyToCompilation(kotlinCompilation: KotlinCompilation<*>): Provider<List<SubpluginOption>> { + return kotlinCompilation.target.project.provider { emptyList() } + } + + override fun apply(target: Project) { + super.apply(target) + } + + override fun getCompilerPluginId(): String = "org.jetbrains.kotlin.coverage" + + override fun getPluginArtifact(): SubpluginArtifact = SubpluginArtifact(GROUP_NAME, ARTIFACT_NAME) + + companion object { + const val GROUP_NAME = "org.jetbrains.kotlin" + const val ARTIFACT_NAME = "coverage-compiler-plugin-embeddable" + } +} \ No newline at end of file
diff --git a/libraries/tools/kotlin-coverage/kotlin-coverage-compiler-subplugin/src/common/resources/META-INF/services/org.jetbrains.kotlin.gradle.plugin.KotlinGradleSubplugin b/libraries/tools/kotlin-coverage/kotlin-coverage-compiler-subplugin/src/common/resources/META-INF/services/org.jetbrains.kotlin.gradle.plugin.KotlinGradleSubplugin new file mode 100644 index 0000000..d9e4d09 --- /dev/null +++ b/libraries/tools/kotlin-coverage/kotlin-coverage-compiler-subplugin/src/common/resources/META-INF/services/org.jetbrains.kotlin.gradle.plugin.KotlinGradleSubplugin
@@ -0,0 +1 @@ +org.jetbrains.kotlin.coverage.CoverageCompilerSubplugin \ No newline at end of file
diff --git a/libraries/tools/kotlin-coverage/kotlin-coverage-runtime/build.gradle.kts b/libraries/tools/kotlin-coverage/kotlin-coverage-runtime/build.gradle.kts new file mode 100644 index 0000000..d0eba2d --- /dev/null +++ b/libraries/tools/kotlin-coverage/kotlin-coverage-runtime/build.gradle.kts
@@ -0,0 +1,19 @@ +plugins { + kotlin("jvm") +} + +kotlin { + explicitApi() +} + +configureKotlinCompileTasksGradleCompatibility() + +publish() + +standardPublicJars() + +dependencies { + // remove stdlib dependency from api artifact in order not to affect the dependencies of the user project + val coreDepsVersion = libs.versions.kotlin.`for`.gradle.plugins.compilation.get() + compileOnly(kotlin("stdlib", coreDepsVersion)) +}
diff --git a/libraries/tools/kotlin-coverage/kotlin-coverage-runtime/src/main/kotlin/org/jetbrains/kotlin/coverage/runtime/FileRoutines.kt b/libraries/tools/kotlin-coverage/kotlin-coverage-runtime/src/main/kotlin/org/jetbrains/kotlin/coverage/runtime/FileRoutines.kt new file mode 100644 index 0000000..319d794 --- /dev/null +++ b/libraries/tools/kotlin-coverage/kotlin-coverage-runtime/src/main/kotlin/org/jetbrains/kotlin/coverage/runtime/FileRoutines.kt
@@ -0,0 +1,53 @@ +/* + * Copyright 2010-2025 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.coverage.runtime + +import java.io.File +import java.io.OutputStream +import kotlin.experimental.or + +// expect-actual +public object FileRoutines { + public fun writeToFile(filePath: String, block: Writer.() -> Unit) { + val file = File(filePath) + file.parentFile.mkdirs() + file.outputStream().buffered().use { + WriterActual(it).block() + } + } + + +} + +// expect +public interface Writer { + public fun writeInt(value: Int) + public fun writeBooleanArray(array: BooleanArray) +} + +// actual +private class WriterActual(val output: OutputStream) : Writer { + override fun writeInt(value: Int) { + output.write(value and 0xFF000000.toInt() shr 24) + output.write(value and 0xFF0000 shr 16) + output.write(value and 0xFF00 shr 8) + output.write(value and 0xFF) + } + + override fun writeBooleanArray(array: BooleanArray) { + val bytesSize = array.size / 8 + if (array.size % 8 > 0) 1 else 0 + val result = ByteArray(bytesSize) { 0 } + array.forEachIndexed { index, value -> + val byteIndex = index / 8 + val bitIndex = index % 8 + if (value) { + result[byteIndex] = result[byteIndex] or (1 shl bitIndex).toByte() + } + } + output.write(result) + } +} +
diff --git a/libraries/tools/kotlin-coverage/kotlin-coverage-runtime/src/main/kotlin/org/jetbrains/kotlin/coverage/runtime/HitStorage.kt b/libraries/tools/kotlin-coverage/kotlin-coverage-runtime/src/main/kotlin/org/jetbrains/kotlin/coverage/runtime/HitStorage.kt new file mode 100644 index 0000000..ca7f20e --- /dev/null +++ b/libraries/tools/kotlin-coverage/kotlin-coverage-runtime/src/main/kotlin/org/jetbrains/kotlin/coverage/runtime/HitStorage.kt
@@ -0,0 +1,50 @@ +/* + * Copyright 2010-2025 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.coverage.runtime + +import kotlin.random.Random + +// expect-actual +public object BooleanHitStorage { + private val segments: MutableMap<Long, BooleanArray> = mutableMapOf() + + public fun reset() { + synchronized(this) { + segments.clear() + } + + } + + public fun getOrCreateSegment(moduleId: Int, segmentNumber: Int, size: Int): BooleanArray { + val id = (moduleId.toLong() shl 32) or segmentNumber.toLong() + + synchronized(this) { + if (segments.isEmpty()) { + initShutdownHook() + } + + segments[id]?.let { return it } + + val array = BooleanArray(size) + segments[id] = array + return array + } + } + + public fun saveHits() { + val dir = (System.getProperties()["kotlin.coverage.executions.path"] as? String) ?: "kover" + val id = Random.nextLong() + HitWriter.writeBoolean("$dir/coverage-$id.kex", segments) + } + + private fun initShutdownHook() { + Runtime.getRuntime().addShutdownHook(Thread { + saveHits() + }) + } +} + +
diff --git a/libraries/tools/kotlin-coverage/kotlin-coverage-runtime/src/main/kotlin/org/jetbrains/kotlin/coverage/runtime/HitWriter.kt b/libraries/tools/kotlin-coverage/kotlin-coverage-runtime/src/main/kotlin/org/jetbrains/kotlin/coverage/runtime/HitWriter.kt new file mode 100644 index 0000000..259b90e --- /dev/null +++ b/libraries/tools/kotlin-coverage/kotlin-coverage-runtime/src/main/kotlin/org/jetbrains/kotlin/coverage/runtime/HitWriter.kt
@@ -0,0 +1,23 @@ +/* + * Copyright 2010-2025 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.coverage.runtime + +public object HitWriter { + public fun writeBoolean(filePath: String, segments: Map<Long, BooleanArray>) { + FileRoutines.writeToFile(filePath) { + writeInt(100500) + writeInt(segments.size) + for ((id, array) in segments) { + val moduleId = (id shr 32).toInt() + val segmentNumber = id.toInt() + writeInt(moduleId) + writeInt(segmentNumber) + writeInt(array.size) + writeBooleanArray(array) + } + } + } +} \ No newline at end of file
diff --git a/libraries/tools/kotlin-gradle-plugin-api/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/kotlinTopLevelExtensionConfig.kt b/libraries/tools/kotlin-gradle-plugin-api/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/kotlinTopLevelExtensionConfig.kt index f262c32..f7a5e64 100644 --- a/libraries/tools/kotlin-gradle-plugin-api/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/kotlinTopLevelExtensionConfig.kt +++ b/libraries/tools/kotlin-gradle-plugin-api/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/kotlinTopLevelExtensionConfig.kt
@@ -5,6 +5,8 @@ package org.jetbrains.kotlin.gradle.dsl +import org.gradle.api.provider.Property + /** * A plugin DSL extension for configuring common options for the entire project. * @@ -54,6 +56,17 @@ * Sets [explicitApi] option to report issues as warnings. */ fun explicitApiWarning() + + fun coverage(block: CoverageConfig.() -> Unit) { + this.coverage.apply(block) + } + + val coverage: CoverageConfig + get() = error("Not implemented yet") +} + +interface CoverageConfig { + val enabled: Property<Boolean> } /**
diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinMultiplatformExtension.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinMultiplatformExtension.kt index 199260a..896474c 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinMultiplatformExtension.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinMultiplatformExtension.kt
@@ -275,6 +275,8 @@ .also { syncCommonMultiplatformOptions(it) } + + override val coverage: CoverageConfig = project.objects.newInstance(CoverageConfig::class.java).also { it.enabled.convention(false) } } private const val targetsExtensionDeprecationMessage =
diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinProjectExtension.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinProjectExtension.kt index 84b73e3..a1b11e4 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinProjectExtension.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinProjectExtension.kt
@@ -206,6 +206,8 @@ } override val publishing: KotlinPublishing = KotlinJvmPublishingDsl(project) + + override val coverage: CoverageConfig = project.objects.newInstance(CoverageConfig::class.java).also { it.enabled.convention(false) } } private class KotlinJvmPublishingDsl(private val project: Project) : KotlinPublishing {
diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/coverage/KotlinCoverageAction.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/coverage/KotlinCoverageAction.kt new file mode 100644 index 0000000..a09c548 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/coverage/KotlinCoverageAction.kt
@@ -0,0 +1,89 @@ +/* + * Copyright 2010-2025 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.gradle.plugin.coverage + +import org.gradle.api.tasks.testing.Test +import org.jetbrains.kotlin.gradle.dsl.kotlinJvmExtensionOrNull +import org.jetbrains.kotlin.gradle.dsl.multiplatformExtensionOrNull +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType +import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle +import org.jetbrains.kotlin.gradle.plugin.KotlinProjectSetupCoroutine +import org.jetbrains.kotlin.gradle.plugin.await +import org.jetbrains.kotlin.gradle.plugin.getKotlinPluginVersion +import org.jetbrains.kotlin.gradle.plugin.mpp.internal +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jetbrains.kotlin.gradle.utils.withType + +internal val KotlinCoverageAction = KotlinProjectSetupCoroutine { + KotlinPluginLifecycle.Stage.AfterEvaluateBuildscript.await() + + val jvmExtension = kotlinJvmExtensionOrNull + val multiplatformExtension = multiplatformExtensionOrNull + if (jvmExtension == null && multiplatformExtension == null) return@KotlinProjectSetupCoroutine + + val kgpVersion = getKotlinPluginVersion() + + jvmExtension?.target?.compilations?.all { compilation -> + val dependencyProvider = provider { + if (jvmExtension.coverage.enabled.get()) { + listOf(dependencies.create("org.jetbrains.kotlin:coverage-compiler-plugin-embeddable:$kgpVersion")) + } else { + emptyList() + } + } + compilation.internal.configurations.pluginConfiguration.dependencies.addAllLater(dependencyProvider) + } + + multiplatformExtension?.targets + ?.filter { it.platformType == KotlinPlatformType.jvm || it.platformType == KotlinPlatformType.androidJvm } + ?.forEach { target -> + target.compilations.all { compilation -> + if (compilation.name.contains("test", ignoreCase = true)) return@all + + val dependencyProvider = provider { + if (multiplatformExtension.coverage.enabled.get()) { + listOf(dependencies.create("org.jetbrains.kotlin:coverage-compiler-plugin-embeddable:$kgpVersion")) + } else { + emptyList() + } + } + compilation.internal.configurations.pluginConfiguration.dependencies.addAllLater(dependencyProvider) + } + } + + tasks.withType<KotlinCompile>().configureEach { compileTask -> + compileTask.pluginClasspath.from() + } + + KotlinPluginLifecycle.Stage.AfterEvaluateBuildscript.await() + + + if (jvmExtension != null) { + dependencies.add("implementation", "org.jetbrains.kotlin:coverage-runtime:$kgpVersion") + } else if (multiplatformExtension != null) { + dependencies.add("jvmMainImplementation", "org.jetbrains.kotlin:coverage-runtime:$kgpVersion") + } else { + return@KotlinProjectSetupCoroutine + } + + tasks.withType<Test>().configureEach { testTask -> + val reportPath = layout.buildDirectory.dir("kover/exec/${testTask.name}").get().asFile.absolutePath + testTask.systemProperty("kotlin.coverage.executions.path", reportPath) + testTask.doFirst { + file(reportPath).deleteRecursively() + } + } + tasks.withType<KotlinCompile>().configureEach { compileTask -> + compileTask.pluginClasspath.from() + + val moduleName = compileTask.compilerOptions.moduleName.get() + val metadataFilePath = layout.buildDirectory.file("kover/metadata/${moduleName}.kim").get().asFile.absolutePath + compileTask.compilerOptions.freeCompilerArgs.addAll( + "-P", "plugin:org.jetbrains.kotlin.coverage:modulePath=${projectDir.absolutePath}", + "-P", "plugin:org.jetbrains.kotlin.coverage:metadataFilePath=$metadataFilePath" + ) + } +} \ No newline at end of file
diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/registerKotlinPluginExtensions.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/registerKotlinPluginExtensions.kt index f1634e3..dd98c3e 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/registerKotlinPluginExtensions.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/registerKotlinPluginExtensions.kt
@@ -11,6 +11,7 @@ import org.jetbrains.kotlin.gradle.internal.CustomizeKotlinDependenciesSetupAction import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.Companion.kotlinPropertiesProvider import org.jetbrains.kotlin.gradle.plugin.abi.AbiValidationSetupAction +import org.jetbrains.kotlin.gradle.plugin.coverage.KotlinCoverageAction import org.jetbrains.kotlin.gradle.plugin.diagnostics.KotlinGradleProjectChecker import org.jetbrains.kotlin.gradle.plugin.diagnostics.KotlinToolingDiagnosticsSetupAction import org.jetbrains.kotlin.gradle.plugin.diagnostics.checkers.* @@ -71,6 +72,7 @@ if (isAbiValidationEnabled) { register(project, AbiValidationSetupAction) } + register(project, KotlinCoverageAction) if (isJvm || isMultiplatform) { register(project, ScriptingGradleSubpluginSetupAction)
diff --git a/plugins/coverage/coverage-compiler-embeddable/build.gradle.kts b/plugins/coverage/coverage-compiler-embeddable/build.gradle.kts new file mode 100644 index 0000000..9e473d1 --- /dev/null +++ b/plugins/coverage/coverage-compiler-embeddable/build.gradle.kts
@@ -0,0 +1,17 @@ +plugins { + kotlin("jvm") +} + +dependencies { + embedded(project(":coverage-compiler-plugin")) { isTransitive = false } +} + +publish() + +runtimeJar(rewriteDefaultJarDepsToShadedCompiler()) +sourcesJarWithSourcesFromEmbedded( + project(":coverage-compiler-plugin").tasks.named<Jar>("sourcesJar") +) +javadocJarWithJavadocFromEmbedded( + project(":coverage-compiler-plugin").tasks.named<Jar>("javadocJar") +)
diff --git a/plugins/coverage/coverage-compiler-plugin/build.gradle.kts b/plugins/coverage/coverage-compiler-plugin/build.gradle.kts new file mode 100644 index 0000000..260e9f9 --- /dev/null +++ b/plugins/coverage/coverage-compiler-plugin/build.gradle.kts
@@ -0,0 +1,29 @@ +description = "Kotlin Serialization Compiler Plugin (CLI)" + +plugins { + kotlin("jvm") + id("jps-compatible") +} + +dependencies { + compileOnly(project(":compiler:util")) + compileOnly(project(":compiler:cli")) + compileOnly(project(":compiler:ir.backend.common")) + compileOnly(project(":compiler:plugin-api")) + compileOnly(project(":compiler:ir.tree")) + compileOnly(project(":compiler:fir:entrypoint")) + compileOnly(project(":kotlin-util-klib-metadata")) + + compileOnly(intellijCore()) +} + +optInToExperimentalCompilerApi() + +sourceSets { + "main" { projectDefault() } + "test" { none() } +} + +runtimeJar() +sourcesJar() +javadocJar()
diff --git a/plugins/coverage/coverage-compiler-plugin/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor b/plugins/coverage/coverage-compiler-plugin/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor new file mode 100644 index 0000000..1e529d1 --- /dev/null +++ b/plugins/coverage/coverage-compiler-plugin/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor
@@ -0,0 +1 @@ +org.jetbrains.kotlin.coverage.compiler.extensions.KotlinCoveragePluginOptions \ No newline at end of file
diff --git a/plugins/coverage/coverage-compiler-plugin/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar b/plugins/coverage/coverage-compiler-plugin/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar new file mode 100644 index 0000000..6459782 --- /dev/null +++ b/plugins/coverage/coverage-compiler-plugin/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar
@@ -0,0 +1,17 @@ +# +# Copyright 2010-2017 JetBrains s.r.o. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +org.jetbrains.kotlin.coverage.compiler.extensions.KotlinCoveragePluginRegistrar \ No newline at end of file
diff --git a/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/common/Context.kt b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/common/Context.kt new file mode 100644 index 0000000..5dd10f8 --- /dev/null +++ b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/common/Context.kt
@@ -0,0 +1,223 @@ +/* + * Copyright 2010-2025 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. + */ + +@file:OptIn(InternalSymbolFinderAPI::class, UnsafeDuringIrConstructionAPI::class) + +package org.jetbrains.kotlin.coverage.compiler.common + +import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.coverage.compiler.common.Constants.BOOLEAN_STORAGE_NAME +import org.jetbrains.kotlin.coverage.compiler.common.Constants.CREATE_BOOLEAN_SEGMENT_NAME +import org.jetbrains.kotlin.coverage.compiler.common.Constants.KOTLIN_COVERAGE_DECLARATION_ORIGIN +import org.jetbrains.kotlin.coverage.compiler.common.Constants.KOTLIN_COVERAGE_STATEMENT_ORIGIN +import org.jetbrains.kotlin.coverage.compiler.common.Constants.LET_NAME +import org.jetbrains.kotlin.descriptors.DescriptorVisibilities +import org.jetbrains.kotlin.descriptors.Modality +import org.jetbrains.kotlin.ir.InternalSymbolFinderAPI +import org.jetbrains.kotlin.ir.IrStatement +import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET +import org.jetbrains.kotlin.ir.declarations.* +import org.jetbrains.kotlin.ir.declarations.impl.IrVariableImpl +import org.jetbrains.kotlin.ir.expressions.* +import org.jetbrains.kotlin.ir.expressions.impl.* +import org.jetbrains.kotlin.ir.symbols.IrClassSymbol +import org.jetbrains.kotlin.ir.symbols.IrReturnTargetSymbol +import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol +import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI +import org.jetbrains.kotlin.ir.symbols.impl.IrSimpleFunctionSymbolImpl +import org.jetbrains.kotlin.ir.symbols.impl.IrVariableSymbolImpl +import org.jetbrains.kotlin.ir.types.IrType +import org.jetbrains.kotlin.ir.types.defaultType +import org.jetbrains.kotlin.ir.util.defaultType +import org.jetbrains.kotlin.ir.util.functions +import org.jetbrains.kotlin.ir.util.substitute +import org.jetbrains.kotlin.name.CallableId +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name + +internal class KotlinCoverageInstrumentationContext(pluginContext: IrPluginContext) { + val irFactory = pluginContext.irFactory + + val declarationOrigin = KOTLIN_COVERAGE_DECLARATION_ORIGIN + val statementOrigin = KOTLIN_COVERAGE_STATEMENT_ORIGIN + + val builtIns = KotlinBuiltIns(pluginContext) + val runtime = CoverageRuntime(pluginContext) + val factory = Factory(builtIns, irFactory) +} + +class KotlinBuiltIns(private val pluginContext: IrPluginContext) { + val unitType = pluginContext.irBuiltIns.unitType + val unitClass = pluginContext.irBuiltIns.unitClass + val nothingType = pluginContext.irBuiltIns.nothingType + val stringType = pluginContext.irBuiltIns.stringType + val intType = pluginContext.irBuiltIns.intType + val booleanType = pluginContext.irBuiltIns.booleanType + val arrayOfPrimitiveBooleansClass = pluginContext.irBuiltIns.primitiveArrayForType.getValue(pluginContext.irBuiltIns.booleanType) + val primitiveArrayOfBooleanType = arrayOfPrimitiveBooleansClass.defaultType + + val function0 = pluginContext.irBuiltIns.functionN(0) + val function0InvokeFun = function0.functions.firstOrNull { it.name.asString() == "invoke" }?.symbol + ?: throw IllegalStateException("Can't find function 'invoke' in the class 'kotlin.Function0'") + + val letFun = pluginContext.referenceFunctions(LET_NAME).firstOrNull() + ?: throw IllegalStateException("Can't find built-in function '${LET_NAME.asSingleFqName().asString()}'") + + val primitiveArrayOfBooleansSetter = arrayOfPrimitiveBooleansClass.functions.firstOrNull { it.owner.name.asString() == "set" } + ?: throw IllegalStateException("Can't find function set for primitive byte array") + + fun functionNType(returnType: IrType, types: List<IrType>): IrType { + val irClass = pluginContext.irBuiltIns.functionN(types.size) + + val typeParams = irClass.typeParameters + return irClass.defaultType.substitute(typeParams, listOf(returnType) + types) + } + + +} + +class CoverageRuntime(pluginContext: IrPluginContext) { + val booleanStorageClass = pluginContext.referenceClass(BOOLEAN_STORAGE_NAME) + ?: throw IllegalStateException("Can't find class '${BOOLEAN_STORAGE_NAME.asString()}'") + val getOrCreateBooleanSegmentFun = pluginContext.irBuiltIns.symbolFinder.findFunctions(CREATE_BOOLEAN_SEGMENT_NAME).firstOrNull() + ?: throw IllegalStateException("Can't find function '${CREATE_BOOLEAN_SEGMENT_NAME.asSingleFqName().asString()}'") +} + +class Factory(private val builtIns: KotlinBuiltIns, private val irFactory: IrFactory) { + fun call(function: IrSimpleFunctionSymbol, returnType: IrType, block: IrCall.() -> Unit = {}): IrCall { + return IrCallImpl( + UNDEFINED_OFFSET, + UNDEFINED_OFFSET, + returnType, + function, + origin = KOTLIN_COVERAGE_STATEMENT_ORIGIN + ).also { call -> + call.block() + } + } + + fun `val`(name: Name, type: IrType, parent: IrDeclarationParent, initializer: IrExpression? = null): IrVariable { + return IrVariableImpl( + UNDEFINED_OFFSET, + UNDEFINED_OFFSET, + KOTLIN_COVERAGE_DECLARATION_ORIGIN, + IrVariableSymbolImpl(), + name, + type, + isVar = false, + isConst = false, + isLateinit = false, + ).also { variable -> + variable.initializer = initializer + variable.parent = parent + } + } + + fun intConst(value: Int): IrConst { + return IrConstImpl( + UNDEFINED_OFFSET, + UNDEFINED_OFFSET, + builtIns.intType, + IrConstKind.Int, + value + ) + } + + fun booleanConst(value: Boolean): IrConst { + return IrConstImpl( + UNDEFINED_OFFSET, + UNDEFINED_OFFSET, + builtIns.booleanType, + IrConstKind.Boolean, + value + ) + } + + fun getObjectValue(classSymbol: IrClassSymbol): IrGetObjectValue { + return IrGetObjectValueImpl( + UNDEFINED_OFFSET, + UNDEFINED_OFFSET, + classSymbol.defaultType, + classSymbol + ) + } + + fun lambda( + returnType: IrType, + argumentTypes: List<IrType>, + parent: IrDeclarationParent, + builder: MutableList<IrStatement>.() -> Unit, + ): IrFunctionExpression { + val function = + irFactory.createSimpleFunction( + UNDEFINED_OFFSET, + UNDEFINED_OFFSET, + KOTLIN_COVERAGE_DECLARATION_ORIGIN, + name = Name.special("<anonymous>"), + isExternal = false, + visibility = DescriptorVisibilities.LOCAL, + containerSource = null, + isSuspend = false, + isInline = false, + isExpect = false, + modality = Modality.FINAL, + isFakeOverride = false, + symbol = IrSimpleFunctionSymbolImpl(), + isTailrec = false, + isOperator = false, + isInfix = false, + returnType = returnType, + ) + function.body = block(builder) + function.parent = parent + + + return IrFunctionExpressionImpl( + UNDEFINED_OFFSET, + UNDEFINED_OFFSET, + builtIns.functionNType(returnType, argumentTypes), + function, + KOTLIN_COVERAGE_STATEMENT_ORIGIN + ) + } + + fun getValue(variable: IrVariable, irType: IrType = variable.type): IrGetValue { + return IrGetValueImpl( + UNDEFINED_OFFSET, + UNDEFINED_OFFSET, + irType, + variable.symbol, + KOTLIN_COVERAGE_STATEMENT_ORIGIN + ) + } + + fun returnValue(returnTarget: IrReturnTargetSymbol, value: IrExpression): IrReturn { + return IrReturnImpl( + UNDEFINED_OFFSET, + UNDEFINED_OFFSET, + builtIns.nothingType, + returnTarget, + value + ) + } + + fun block(builder: MutableList<IrStatement>.() -> Unit): IrBlockBody { + return irFactory.createBlockBody(UNDEFINED_OFFSET, UNDEFINED_OFFSET) { + statements.builder() + } + } +} + +private object Constants { + val KOTLIN_COVERAGE_STATEMENT_ORIGIN = IrStatementOriginImpl("KOTLIN_COVERAGE_ORIGIN") + val KOTLIN_COVERAGE_DECLARATION_ORIGIN = IrDeclarationOriginImpl("KOTLIN_COVERAGE_ORIGIN", true) + + val PRINTLN_NAME = CallableId(FqName("kotlin.io"), Name.identifier("println")) + val LET_NAME = CallableId(FqName("kotlin"), Name.identifier("let")) + val COVERAGE_RUNTIME_PACKAGE = FqName("org.jetbrains.kotlin.coverage.runtime") + val BOOLEAN_STORAGE_NAME = ClassId(COVERAGE_RUNTIME_PACKAGE, Name.identifier("BooleanHitStorage")) + val CREATE_BOOLEAN_SEGMENT_NAME = CallableId(BOOLEAN_STORAGE_NAME, Name.identifier("getOrCreateSegment")) +}
diff --git a/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/extensions/CoverageLoweringExtension.kt b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/extensions/CoverageLoweringExtension.kt new file mode 100644 index 0000000..e9d1aad --- /dev/null +++ b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/extensions/CoverageLoweringExtension.kt
@@ -0,0 +1,56 @@ +/* + * Copyright 2010-2025 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. + */ + +@file:OptIn(UnsafeDuringIrConstructionAPI::class) + +package org.jetbrains.kotlin.coverage.compiler.extensions + +import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension +import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.coverage.compiler.instrumentation.IrFileVisitor +import org.jetbrains.kotlin.coverage.compiler.common.KotlinCoverageInstrumentationContext +import org.jetbrains.kotlin.coverage.compiler.hit.HitRegistrarFactory +import org.jetbrains.kotlin.coverage.compiler.instrumentation.LineBranchInstrumenter +import org.jetbrains.kotlin.coverage.compiler.metadata.ModuleIM +import org.jetbrains.kotlin.coverage.compiler.metadata.writeToFile +import org.jetbrains.kotlin.ir.declarations.IrModuleFragment +import org.jetbrains.kotlin.ir.declarations.path +import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI +import org.jetbrains.kotlin.platform.jvm.isJvm +import java.io.File +import kotlin.random.Random + +class CoverageLoweringExtension(val modulePath: String, val metadataFilePath: String) : IrGenerationExtension { + override fun generate( + moduleFragment: IrModuleFragment, + pluginContext: IrPluginContext, + ) { + if (!pluginContext.platform.isJvm()) { + // only JVM platform supported for now + return + } + + val context = KotlinCoverageInstrumentationContext(pluginContext) + + val hitRegistrarFactory = HitRegistrarFactory() + val instrumenter = LineBranchInstrumenter() + + val moduleDir = File(modulePath) + + val moduleId = moduleFragment.name.asString().hashCode() and Random.nextInt() + + val moduleIM = ModuleIM() + + moduleFragment.files.forEach { file -> + val relativePath = File(file.path).toRelativeString(moduleDir) + val fileIM = moduleIM.addFile(relativePath, file.packageFqName.asString()) + + val segmentGenerator = hitRegistrarFactory.create(moduleId, fileIM.number, context) + IrFileVisitor(file, fileIM, instrumenter, segmentGenerator, context).process() + } + + moduleIM.writeToFile(File(metadataFilePath)) + } +}
diff --git a/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/extensions/KotlinCoveragePluginRegistrar.kt b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/extensions/KotlinCoveragePluginRegistrar.kt new file mode 100644 index 0000000..e292354 --- /dev/null +++ b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/extensions/KotlinCoveragePluginRegistrar.kt
@@ -0,0 +1,62 @@ +/* + * Copyright 2010-2025 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.coverage.compiler.extensions + +import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension +import org.jetbrains.kotlin.compiler.plugin.AbstractCliOption +import org.jetbrains.kotlin.compiler.plugin.CliOption +import org.jetbrains.kotlin.compiler.plugin.CliOptionProcessingException +import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor +import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar +import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.config.CompilerConfigurationKey + +private const val PLUGIN_ID = "org.jetbrains.kotlin.coverage" + +private val MODULE_PATH: CompilerConfigurationKey<String> = + CompilerConfigurationKey.create("Path to the root of the current module.") + +private val METADATA_PATH: CompilerConfigurationKey<String> = + CompilerConfigurationKey.create("Path to the kotlin coverage metadata file.") + +class KotlinCoveragePluginRegistrar : CompilerPluginRegistrar() { + override val pluginId: String = PLUGIN_ID + + override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) { + val modulePath = configuration.get(MODULE_PATH) + ?: throw CliOptionProcessingException("${KotlinCoveragePluginOptions.MODULE_PATH_OPTION.optionName} option not specified") + val metadataFilePath = configuration.get(METADATA_PATH) + ?: throw CliOptionProcessingException("${KotlinCoveragePluginOptions.METADATA_PATH_OPTION.optionName} option not specified") + + IrGenerationExtension.registerExtension(CoverageLoweringExtension(modulePath, metadataFilePath)) + } + + override val supportsK2: Boolean = true +} + +class KotlinCoveragePluginOptions : CommandLineProcessor { + companion object { + val MODULE_PATH_OPTION = CliOption( + "modulePath", "Module directory path", + "Path to the root of the current module", + required = true, allowMultipleOccurrences = false + ) + val METADATA_PATH_OPTION = CliOption( + "metadataFilePath", "Coverage metadata file path", + "Path to the file with coverage metadata", + required = true, allowMultipleOccurrences = false + ) + } + + override val pluginId get() = PLUGIN_ID + override val pluginOptions = listOf(MODULE_PATH_OPTION, METADATA_PATH_OPTION) + + override fun processOption(option: AbstractCliOption, value: String, configuration: CompilerConfiguration) = when (option) { + MODULE_PATH_OPTION -> configuration.put(MODULE_PATH, value) + METADATA_PATH_OPTION -> configuration.put(METADATA_PATH, value) + else -> throw CliOptionProcessingException("Unknown option: ${option.optionName}") + } +}
diff --git a/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/hit/BooleanHitRegistrar.kt b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/hit/BooleanHitRegistrar.kt new file mode 100644 index 0000000..da06dc9 --- /dev/null +++ b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/hit/BooleanHitRegistrar.kt
@@ -0,0 +1,111 @@ +/* + * Copyright 2010-2025 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.coverage.compiler.hit + +import org.jetbrains.kotlin.coverage.compiler.common.KotlinCoverageInstrumentationContext +import org.jetbrains.kotlin.descriptors.DescriptorVisibilities +import org.jetbrains.kotlin.descriptors.Modality +import org.jetbrains.kotlin.ir.IrStatement +import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET +import org.jetbrains.kotlin.ir.declarations.* +import org.jetbrains.kotlin.ir.symbols.impl.IrPropertySymbolImpl +import org.jetbrains.kotlin.ir.symbols.impl.IrSimpleFunctionSymbolImpl +import org.jetbrains.kotlin.name.Name + +internal class BooleanHitRegistrar(val moduleId: Int, val segmentNumber: Int, val context: KotlinCoverageInstrumentationContext) : + HitRegistrar { + private val segmentPropertyName = Name.identifier($$"$kover_segment_") + private val segmentGetterName = Name.identifier($$"$kover_get_segment_") + + private val segmentProperty: IrProperty = context.irFactory.createProperty( + UNDEFINED_OFFSET, + UNDEFINED_OFFSET, + context.declarationOrigin, + segmentPropertyName, + DescriptorVisibilities.PRIVATE, + Modality.FINAL, + IrPropertySymbolImpl(), + isVar = true, + isConst = false, + isLateinit = false, + isDelegated = false, + ) + + private val segmentGetter: IrSimpleFunction = context.irFactory.createSimpleFunction( + UNDEFINED_OFFSET, + UNDEFINED_OFFSET, + context.declarationOrigin, + segmentGetterName, + DescriptorVisibilities.PRIVATE, + isInline = false, + isExpect = false, + context.builtIns.primitiveArrayOfBooleanType, + Modality.FINAL, + IrSimpleFunctionSymbolImpl(), + isTailrec = false, + isSuspend = false, + isOperator = false, + isInfix = false + ).also { function -> function.returnType = context.builtIns.primitiveArrayOfBooleanType} + + val pointsCounter = Counter() + + override val extraDeclarations: List<IrDeclaration> = listOf(segmentProperty, segmentGetter) + + override fun body(irFunction: IrFunction): BlockWithExecutionPoints { + return BooleanBlockWithExecutionPoints(segmentGetter, irFunction, pointsCounter, context) + } + + override fun finalize() { + val myCall = context.factory.call(context.runtime.getOrCreateBooleanSegmentFun, context.builtIns.primitiveArrayOfBooleanType) { + arguments[0] = context.factory.getObjectValue(context.runtime.booleanStorageClass) + arguments[1] = context.factory.intConst(moduleId) + arguments[2] = context.factory.intConst(segmentNumber) + arguments[3] = context.factory.intConst(pointsCounter.count) + } + + val returnCall = context.factory.returnValue(segmentGetter.symbol, myCall) + + segmentGetter.body = context.factory.block { + add(returnCall) + } + } +} + +private class BooleanBlockWithExecutionPoints( + private val segmentGetter: IrSimpleFunction, + private val thisFunction: IrFunction, + private val counter: Counter, + val context: KotlinCoverageInstrumentationContext, +) : BlockWithExecutionPoints { + override val pointsCount: Int + get() = counter.count + + private val variableName = Name.identifier($$"$coverage_segment") + + private val variable: IrVariable = context.factory.`val`( + variableName, + context.builtIns.primitiveArrayOfBooleanType, + thisFunction, + context.factory.call(segmentGetter.symbol, context.builtIns.primitiveArrayOfBooleanType) + ) + + override val firstStatement: IrStatement = variable + + override fun registerPoint(): ExecutionPoint { + val id = counter.count++ + + val statement = context.factory.call(context.builtIns.primitiveArrayOfBooleansSetter, context.builtIns.unitType) { + arguments[0] = context.factory.getValue(variable) + arguments[1] = context.factory.intConst(id) + arguments[2] = context.factory.booleanConst(true) + } + + return ExecutionPoint(id, statement) + } +} + +internal class Counter(var count: Int = 0) \ No newline at end of file
diff --git a/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/hit/HitRegistrar.kt b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/hit/HitRegistrar.kt new file mode 100644 index 0000000..e6c2640 --- /dev/null +++ b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/hit/HitRegistrar.kt
@@ -0,0 +1,40 @@ +/* + * Copyright 2010-2025 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. + */ + +@file:OptIn(UnsafeDuringIrConstructionAPI::class) + +package org.jetbrains.kotlin.coverage.compiler.hit + +import org.jetbrains.kotlin.coverage.compiler.common.KotlinCoverageInstrumentationContext +import org.jetbrains.kotlin.ir.IrStatement +import org.jetbrains.kotlin.ir.declarations.IrDeclaration +import org.jetbrains.kotlin.ir.declarations.IrFunction +import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI + +internal class HitRegistrarFactory { + fun create(moduleId: Int, segmentNumber: Int, context: KotlinCoverageInstrumentationContext): HitRegistrar { + // now we support only boolean hits + return BooleanHitRegistrar(moduleId, segmentNumber, context) + } +} + +internal interface HitRegistrar { + val extraDeclarations: List<IrDeclaration> + fun body(irFunction: IrFunction): BlockWithExecutionPoints + fun finalize() +} + +internal interface BlockWithExecutionPoints { + val firstStatement: IrStatement + val pointsCount: Int + fun registerPoint(): ExecutionPoint +} + +internal class ExecutionPoint( + val id: Int, + val hitStatement: IrStatement, +) + +
diff --git a/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/instrumentation/Instrumenter.kt b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/instrumentation/Instrumenter.kt new file mode 100644 index 0000000..e71c82e --- /dev/null +++ b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/instrumentation/Instrumenter.kt
@@ -0,0 +1,23 @@ +/* + * Copyright 2010-2025 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.coverage.compiler.instrumentation + +import org.jetbrains.kotlin.coverage.compiler.common.KotlinCoverageInstrumentationContext +import org.jetbrains.kotlin.coverage.compiler.hit.HitRegistrar +import org.jetbrains.kotlin.coverage.compiler.metadata.FunctionIM +import org.jetbrains.kotlin.ir.declarations.IrFile +import org.jetbrains.kotlin.ir.declarations.IrFunction + +internal sealed interface Instrumenter { + fun instrument( + irFunction: IrFunction, + functionIM: FunctionIM, + irFile: IrFile, + hitRegistrar: HitRegistrar, + context: KotlinCoverageInstrumentationContext, + ) +} +
diff --git a/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/instrumentation/IrFileVisitor.kt b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/instrumentation/IrFileVisitor.kt new file mode 100644 index 0000000..61ad196 --- /dev/null +++ b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/instrumentation/IrFileVisitor.kt
@@ -0,0 +1,80 @@ +/* + * Copyright 2010-2025 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. + */ + +@file:OptIn(UnsafeDuringIrConstructionAPI::class) + +package org.jetbrains.kotlin.coverage.compiler.instrumentation + +import org.jetbrains.kotlin.coverage.compiler.common.KotlinCoverageInstrumentationContext +import org.jetbrains.kotlin.coverage.compiler.hit.HitRegistrar +import org.jetbrains.kotlin.coverage.compiler.metadata.DeclarationContainerIM +import org.jetbrains.kotlin.coverage.compiler.metadata.FileIM +import org.jetbrains.kotlin.coverage.compiler.metadata.positionRange +import org.jetbrains.kotlin.ir.declarations.* +import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI + +/** + * Class to walk over an IR declarations tree, add required declarations and instrument functions. + */ +internal class IrFileVisitor( + val irFile: IrFile, + val fileIM: FileIM, + val instrumenter: Instrumenter, + val hitRegistrar: HitRegistrar, + val context: KotlinCoverageInstrumentationContext, +) { + internal fun process() { + // TODO can we use file to collect coverage or we should place only in classes/objects/companions/top level files + + // register declarations + hitRegistrar.extraDeclarations.forEach { irDeclaration -> + irDeclaration.parent = irFile + irFile.declarations.add(irDeclaration) + } + + fileIM.processTopLevelContainer(irFile) + + hitRegistrar.finalize() + } + + private fun DeclarationContainerIM.processTopLevelContainer(container: IrDeclarationContainer) { + container.declarations.forEach { irDeclaration -> + if (irDeclaration.origin != IrDeclarationOrigin.DEFINED) return@forEach + + when (irDeclaration) { + is IrProperty -> processProperty(irDeclaration) + is IrClass -> processClass(irDeclaration) + is IrFunction -> processFunction(irDeclaration) + } + } + } + + private fun DeclarationContainerIM.processClass(irClass: IrClass) { + // TODO is init{} block always injected to <init>? + val classIM = addClass(irClass.name.asString(), irClass.isCompanion, irFile.positionRange(irClass)) + classIM.processTopLevelContainer(irClass) + } + + private fun DeclarationContainerIM.processFunction(irFunction: IrFunction) { + // TODO (irClass.parent as IrFunction).body?.statements?.filterIsInstance<IrClass>() + + val range = irFile.positionRange(irFunction) + val functionIM = addFunction(irFunction.name.asString(), listOf(), "", range) + + instrumenter.instrument(irFunction, functionIM, irFile, hitRegistrar, context) + } + + private fun DeclarationContainerIM.processProperty(irProperty: IrProperty) { + if (irProperty.getter == null && irProperty.setter == null) return + + irProperty.backingField?.initializer + + val range = irFile.positionRange(irProperty) + val propertyIM = addProperty(irProperty.name.asString(), irProperty.isVar, irProperty.isConst, range) + + // TODO irProperty.backingField?.initializer + } + +} \ No newline at end of file
diff --git a/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/instrumentation/LineBranchInstrumenter.kt b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/instrumentation/LineBranchInstrumenter.kt new file mode 100644 index 0000000..922728d --- /dev/null +++ b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/instrumentation/LineBranchInstrumenter.kt
@@ -0,0 +1,310 @@ +/* + * Copyright 2010-2025 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. + */ + +@file:OptIn(UnsafeDuringIrConstructionAPI::class) + +package org.jetbrains.kotlin.coverage.compiler.instrumentation + +import org.jetbrains.kotlin.coverage.compiler.common.KotlinCoverageInstrumentationContext +import org.jetbrains.kotlin.coverage.compiler.hit.BlockWithExecutionPoints +import org.jetbrains.kotlin.coverage.compiler.hit.HitRegistrar +import org.jetbrains.kotlin.coverage.compiler.metadata.FunctionIM +import org.jetbrains.kotlin.coverage.compiler.metadata.LineBranchBodyIM +import org.jetbrains.kotlin.coverage.compiler.metadata.LineBranchBodyIM.LineInfo +import org.jetbrains.kotlin.coverage.compiler.metadata.Position +import org.jetbrains.kotlin.coverage.compiler.metadata.position +import org.jetbrains.kotlin.ir.IrStatement +import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin +import org.jetbrains.kotlin.ir.declarations.IrDeclarationParent +import org.jetbrains.kotlin.ir.declarations.IrFile +import org.jetbrains.kotlin.ir.declarations.IrFunction +import org.jetbrains.kotlin.ir.declarations.IrParameterKind +import org.jetbrains.kotlin.ir.declarations.IrVariable +import org.jetbrains.kotlin.ir.expressions.IrBlockBody +import org.jetbrains.kotlin.ir.expressions.IrCall +import org.jetbrains.kotlin.ir.expressions.IrConst +import org.jetbrains.kotlin.ir.expressions.IrConstructorCall +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.expressions.IrExpressionBody +import org.jetbrains.kotlin.ir.expressions.IrGetValue +import org.jetbrains.kotlin.ir.expressions.IrReturn +import org.jetbrains.kotlin.ir.expressions.IrSetValue +import org.jetbrains.kotlin.ir.expressions.IrStatementOrigin +import org.jetbrains.kotlin.ir.expressions.IrTypeOperatorCall +import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI +import org.jetbrains.kotlin.name.Name + +internal class LineBranchInstrumenter() : Instrumenter { + override fun instrument( + irFunction: IrFunction, + functionIM: FunctionIM, + irFile: IrFile, + hitRegistrar: HitRegistrar, + context: KotlinCoverageInstrumentationContext, + ) { + val body = irFunction.body ?: return + + val bodyPointsRegistry = hitRegistrar.body(irFunction) + + when (body) { + is IrBlockBody -> instrumentBlock(body, irFunction, functionIM, bodyPointsRegistry, irFile, context) + is IrExpressionBody -> {} + else -> {}// do nothing + } + } + + + private fun instrumentBlock( + block: IrBlockBody, + irFunction: IrFunction, + functionIM: FunctionIM, + pointsRegistry: BlockWithExecutionPoints, + irFile: IrFile, + context: KotlinCoverageInstrumentationContext, + ) { + val bodyInstrumentation = BodyInstrumentation(pointsRegistry, irFile, irFunction, context) + val instrumented = bodyInstrumentation.instrument(block.statements) + + if (bodyInstrumentation.finishedLines.isNotEmpty()) { + block.statements.clear() + block.statements.addAll(instrumented) + functionIM.body = LineBranchBodyIM(bodyInstrumentation.finishedLines) + } + } +} + + +private class BodyInstrumentation( + val pointsRegistry: BlockWithExecutionPoints, + val irFile: IrFile, + val parentFunction: IrDeclarationParent, + val context: KotlinCoverageInstrumentationContext, +) { + var currentLine: LineInfo? = null + var tmpVariableCount = 0 + + val finishedLines: MutableList<LineInfo> = mutableListOf() + val currentBranches: MutableList<LineBranchBodyIM.BranchInfo> = mutableListOf() + + fun instrument(statements: List<IrStatement>): List<IrStatement> { + val instrumentedStatements = mutableListOf<IrStatement>() + + instrumentedStatements.add(pointsRegistry.firstStatement) + statements.forEach { statement -> + instrumentedStatements.addAll(instrumentStatement(statement).allStatements) + } + + if (currentLine != null) { + finishedLines.add(currentLine!!) + } + + return instrumentedStatements + } + + + private fun instrumentStatement(statement: IrStatement): InstrumentationResult { + return when (statement) { + is IrReturn -> instrument(statement) + is IrConst -> instrument(statement) + is IrVariable -> instrument(statement) + is IrSetValue -> instrument(statement) +// is IrGetValue -> instrument(statement) + is IrCall -> instrument(statement) + is IrConstructorCall -> instrument(statement) + is IrTypeOperatorCall -> instrument(statement) + else -> { + InstrumentationResult.fromStatement(statement) + } + } + } + + private fun instrument(returnStatement: IrReturn): InstrumentationResult { + val hitPointStatement = registerLinePoint(returnStatement) + returnStatement.value = instrumentStatement(returnStatement.value).wrap() + return InstrumentationResult.fromPointed(hitPointStatement, returnStatement) + } + + private fun instrument(constStatement: IrConst): InstrumentationResult { + val hitPointStatement = registerLinePoint(constStatement) + return InstrumentationResult.fromPointed(hitPointStatement, constStatement) + } + + private fun instrument(variable: IrVariable): InstrumentationResult { + if (variable.origin != IrDeclarationOrigin.DEFINED) { + // process only user-defined variables + return InstrumentationResult.fromStatement(variable) + } + + val hitPointStatement = registerLinePoint(variable) + variable.initializer = variable.initializer?.let { instrumentStatement(it).wrap() } + return InstrumentationResult.fromPointed(hitPointStatement, variable) + } + + private fun instrument(setValue: IrSetValue): InstrumentationResult { + if (setValue.origin != IrStatementOrigin.EQ) { + // process only user-defined variables + return InstrumentationResult.fromStatement(setValue) + } + + val hitPointStatement = registerLinePoint(setValue) + setValue.value = instrumentStatement(setValue.value).wrap() + return InstrumentationResult.fromPointed(hitPointStatement, setValue) + } + + private fun instrument(irCall: IrCall): InstrumentationResult { + val leadingStatements = mutableListOf<IrStatement>() + // process receivers + irCall.symbol.owner.parameters.forEachIndexed { index, parameter -> + if (parameter.kind != IrParameterKind.DispatchReceiver && parameter.kind != IrParameterKind.ExtensionReceiver) { + return@forEachIndexed + } + // skip null arguments (default values in most cases) + val arg = irCall.arguments[index] ?: return@forEachIndexed + + val instrumentedReceiver = instrumentReceiver(arg) + + irCall.arguments[index] = instrumentedReceiver.asExpression + // add initialization of receivers in separate variables + leadingStatements.addAll(instrumentedReceiver.leadingStatements) + } + + val hitPointStatement = registerLinePoint(irCall) + + // process arguments + irCall.symbol.owner.parameters.forEachIndexed { index, parameter -> + if (parameter.kind != IrParameterKind.Regular) { + return@forEachIndexed + } + // skip null arguments - in [actualArgs] it's already null + val arg = irCall.arguments[index] ?: return@forEachIndexed + irCall.arguments[index] = instrumentStatement(arg).wrap() + } + + return InstrumentationResult.fromPointed(leadingStatements, hitPointStatement, irCall) + } + + private fun instrument(irConstructorCall: IrConstructorCall): InstrumentationResult { + val hitPointStatement = registerLinePoint(irConstructorCall) + + // process arguments + irConstructorCall.symbol.owner.parameters.forEachIndexed { index, parameter -> + if (parameter.kind != IrParameterKind.Regular) { + return@forEachIndexed + } + // skip null arguments - in [actualArgs] it's already null + val arg = irConstructorCall.arguments[index] ?: return@forEachIndexed + irConstructorCall.arguments[index] = instrumentStatement(arg).wrap() + } + return InstrumentationResult.fromPointed(hitPointStatement, irConstructorCall) + } + + private fun instrument(irTypeOperatorCall: IrTypeOperatorCall): InstrumentationResult { + val instrumented = instrumentStatement(irTypeOperatorCall.argument).wrap() + irTypeOperatorCall.argument = instrumented + // don't change the operator itself + return InstrumentationResult.fromStatement(irTypeOperatorCall) + } + + private fun instrumentReceiver(irExpression: IrExpression): InstrumentationResult { + val name = Name.identifier($$"$tmp" + tmpVariableCount++) + val instrumented = instrumentStatement(irExpression) + return when { + instrumented.hasHitPoint && instrumented.leadingStatements.isEmpty() -> + InstrumentationResult.fromPointed(listOf(instrumented.hitPoint), null, instrumented.statement) + + instrumented.hasHitPoint && instrumented.leadingStatements.isNotEmpty() -> { + val variable = context.factory.`val`(name, irExpression.type, parentFunction, instrumented.asExpression) + val get = context.factory.getValue(variable) + InstrumentationResult.fromPointed(instrumented.leadingStatements + variable + instrumented.hitPoint, null, get) + } + + else -> return instrumented + } + } + + /** + * Add execution point before [irStatement] if needed. + */ + private fun registerLinePoint(irStatement: IrStatement): IrStatement? { + val position = irFile.position(irStatement.startOffset) + return if (isNextLine(position)) { + val point = pointsRegistry.registerPoint() + startNewLine(position, point.id) + point.hitStatement + } else { + null + } + } + + fun isNextLine(position: Position): Boolean { + val currentLineStart = currentLine?.lineNumber + return if (currentLineStart == null) { + true + } else { + currentLineStart != position.line + } + } + + fun startNewLine(position: Position, pointId: Int) { + val current = currentLine + if (current != null) { + finishedLines += current + } + + currentLine = LineInfo(position.line, position.column, pointId, mutableListOf()) + currentBranches.clear() + } + + private fun InstrumentationResult.wrap(): IrExpression { + val expression = + asExpression ?: throw IllegalStateException("Instrumentation result can't be wrapped to expression; $leadingStatements") + if (hitPoint == null && leadingStatements.isEmpty()) { + return expression + } + return context.factory.call(context.builtIns.function0InvokeFun, expression.type) { + arguments[0] = context.factory.lambda(expression.type, emptyList(), parentFunction) { + addAll(allStatements) + } + } + } + + private class InstrumentationResult private constructor( + val leadingStatements: List<IrStatement>, + val hitPoint: IrStatement?, + val statement: IrStatement, + ) { + val asExpression: IrExpression + get() { + return statement as? IrExpression ?: error("Instrumented statement is not an expression, actual $statement") + } + val allStatements: List<IrStatement> + get() { + return if (hitPoint == null) { + leadingStatements + statement + } else { + leadingStatements + hitPoint + statement + } + } + val hasHitPoint: Boolean = hitPoint != null + + companion object { + fun fromPointed(hitPointStatement: IrStatement?, instrumentedStatement: IrStatement): InstrumentationResult { + return InstrumentationResult(emptyList(), hitPointStatement, instrumentedStatement) + } + + fun fromPointed( + leadingStatements: List<IrStatement?>, + hitPointStatement: IrStatement?, + statement: IrStatement, + ): InstrumentationResult { + return InstrumentationResult(leadingStatements.filterNotNull(), hitPointStatement, statement) + } + + fun fromStatement(instrumentedStatement: IrStatement): InstrumentationResult { + return InstrumentationResult(emptyList(), null, instrumentedStatement) + } + } + } +}
diff --git a/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/metadata/InstrumentationMetadata.kt b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/metadata/InstrumentationMetadata.kt new file mode 100644 index 0000000..70040cf --- /dev/null +++ b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/metadata/InstrumentationMetadata.kt
@@ -0,0 +1,140 @@ +/* + * Copyright 2010-2025 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.coverage.compiler.metadata + +import org.jetbrains.kotlin.ir.IrElement +import org.jetbrains.kotlin.ir.declarations.IrFile + + +internal class ModuleIM { + private val _files = mutableListOf<FileIM>() + val files: List<FileIM> = _files + + fun addFile(path: String, packageName: String): FileIM { + val file = FileIM(_files.size, path, packageName) + _files.add(file) + return file + } +} + +internal class FileIM( + val number: Int, + val path: String, + val packageName: String, +) : DeclarationContainerIM() { + override val parent: DeclarationContainerIM? = null +} + +internal sealed class DeclarationContainerIM { + abstract val parent: DeclarationContainerIM? + + private val _declarations = mutableListOf<DeclarationIM>() + val declarations: List<DeclarationIM> = _declarations + + open fun addFunction( + name: String, + params: List<String>, + returnType: String, + range: PositionRange, + ): FunctionIM { + return FunctionIM(name, params, returnType, range, this).also { functionIM -> _declarations.add(functionIM) } + } + + open fun addClass( + name: String, + isCompanion: Boolean, + range: PositionRange, + ): ClassIM { + return ClassIM(name, isCompanion, range, this).also { _declarations.add(it) } + } + + open fun addProperty( + name: String, + isVar: Boolean, + isConst: Boolean, + range: PositionRange, + ): PropertyIM { + return PropertyIM(name, isVar, isConst, range, this).also { _declarations.add(it) } + } +} + +internal sealed class DeclarationIM : DeclarationContainerIM() { + abstract val range: PositionRange +} + +internal class ClassIM( + val name: String, + val isCompanion: Boolean, + override val range: PositionRange, + override val parent: DeclarationContainerIM, +) : DeclarationIM() { + val isLocal: Boolean get() = parent is FunctionIM +} + +internal class PropertyIM( + val name: String, + val isVar: Boolean, + val isConst: Boolean, + override val range: PositionRange, + override val parent: DeclarationContainerIM, +) : DeclarationIM() { + + // TODO add initializer, getter and setter + + override fun addProperty( + name: String, + isVar: Boolean, + isConst: Boolean, + range: PositionRange, + ): PropertyIM { + throw UnsupportedOperationException("Property cannot have classes") + } + + override fun addClass( + name: String, + isCompanion: Boolean, + range: PositionRange, + ): ClassIM { + throw UnsupportedOperationException("Property cannot have properties") + } +} + +internal class FunctionIM( + val name: String, + val params: List<String>, + val returnType: String, + override val range: PositionRange, + override val parent: DeclarationContainerIM, +) : DeclarationIM() { + val isLocal: Boolean get() = parent is FunctionIM + var body: BodyIM? = null +} + +internal sealed interface BodyIM + + +internal class Position(val line: Int, val column: Int) { + override fun toString(): String { + return "$line:$column" + } +} + +internal class PositionRange(val start: Position, val end: Position) { + override fun toString(): String { + return "${start.line}:${start.column}-${end.line}:${end.column}" + } +} + +internal fun IrFile.positionRange(element: IrElement): PositionRange { + return PositionRange(position(element.startOffset), position(element.endOffset)) +} + +internal fun IrFile.position(offset: Int): Position { + return Position( + fileEntry.getLineNumber(offset), + fileEntry.getColumnNumber(offset) + ) +} \ No newline at end of file
diff --git a/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/metadata/InstrumentationMetadataWriter.kt b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/metadata/InstrumentationMetadataWriter.kt new file mode 100644 index 0000000..33b4fcb --- /dev/null +++ b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/metadata/InstrumentationMetadataWriter.kt
@@ -0,0 +1,81 @@ +/* + * Copyright 2010-2025 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. + */ + +@file:OptIn(UnsafeDuringIrConstructionAPI::class) + +package org.jetbrains.kotlin.coverage.compiler.metadata + +import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI +import java.io.File + +internal fun ModuleIM.writeToFile(file: File) { + file.parentFile.mkdirs() + file.bufferedWriter().use { out -> + out.writeModule(this) + } +} + +private fun Appendable.writeModule(moduleIM: ModuleIM) { + moduleIM.files.forEach { fileIM -> + appendLine(fileIM.path) + appendLine(fileIM.packageName) + writeDeclarations(SINGLE_INDENT, fileIM) + } +} + +private fun Appendable.writeDeclarations(indent: String, containerIM: DeclarationContainerIM) { + containerIM.declarations.forEach { declarationIM -> + when (declarationIM) { + is ClassIM -> writeClass(indent, declarationIM) + is FunctionIM -> writeFunction(indent, declarationIM) + is PropertyIM -> writeProperty(indent, declarationIM) + else -> error("Unknown declaration type: ${declarationIM::class.simpleName}") + } + } +} + +private fun Appendable.writeProperty(indent: String, propertyIM: PropertyIM) { + append(indent) + if (propertyIM.isConst) append("const ") + if (propertyIM.isVar) append("var ") else append("val ") + appendLine(propertyIM.name) + appendLine("$indent[${propertyIM.range}]") + writeDeclarations("$indent ", propertyIM) +} + +private fun Appendable.writeClass(indent: String, classIM: ClassIM) { + append(indent) + if (classIM.isCompanion) append("companion object ") else append("class ") + appendLine(classIM.name) + appendLine("$indent[${classIM.range}]") + writeDeclarations("$indent ", classIM) +} + +private fun Appendable.writeFunction(indent: String, functionIM: FunctionIM) { + appendLine("${indent}fun ${functionIM.name}(${functionIM.params}): ${functionIM.returnType}") + val nextIndent = indent + SINGLE_INDENT + appendLine("${nextIndent}range: ${functionIM.range}") + writeBody(nextIndent, functionIM.body) + writeDeclarations(nextIndent, functionIM) +} + + +private fun Appendable.writeBody(indent: String, body: BodyIM?) { + append("${indent}body: ") + if (body != null && body is LineBranchBodyIM) { + body.lines.forEach { line -> + append("${line.pointId}=${line.lineNumber}:${line.columnStart}[") + + append("];") + } + } else { + append("null") + } + + appendLine() +} + + +const val SINGLE_INDENT = " "
diff --git a/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/metadata/LineBranchInstrumentationMetadata.kt b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/metadata/LineBranchInstrumentationMetadata.kt new file mode 100644 index 0000000..a93e434 --- /dev/null +++ b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/metadata/LineBranchInstrumentationMetadata.kt
@@ -0,0 +1,12 @@ +/* + * Copyright 2010-2025 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.coverage.compiler.metadata + +internal class LineBranchBodyIM(val lines: MutableList<LineInfo>) : BodyIM { + internal class LineInfo(val lineNumber: Int, val columnStart: Int, val pointId: Int, val branches: MutableList<BranchInfo>) + + internal class BranchInfo(val id: Int, val columnStart: Int, val columnEnd: Int, val pointId: Int) +}
diff --git a/settings.gradle b/settings.gradle index f2c53dc..1a1e785 100644 --- a/settings.gradle +++ b/settings.gradle
@@ -482,6 +482,11 @@ ":kotlin-serialization", ":kotlin-serialization-unshaded" +include ":coverage-compiler-plugin-embeddable", + ":coverage-compiler-plugin", + ":coverage-compiler-gradle", + ":coverage-runtime" + include ":kotlin-dataframe-compiler-plugin", ":kotlin-dataframe-compiler-plugin.embeddable", ":kotlin-dataframe-compiler-plugin.common", @@ -999,6 +1004,11 @@ project(':kotlin-serialization').projectDir = file("$rootDir/libraries/tools/kotlin-serialization") project(':kotlin-serialization-unshaded').projectDir = file("$rootDir/libraries/tools/kotlin-serialization-unshaded") +project(':coverage-compiler-plugin-embeddable').projectDir = "$rootDir/plugins/coverage/coverage-compiler-embeddable" as File +project(':coverage-compiler-plugin').projectDir = "$rootDir/plugins/coverage/coverage-compiler-plugin" as File +project(':coverage-compiler-gradle').projectDir = "$rootDir/libraries/tools/kotlin-coverage/kotlin-coverage-compiler-subplugin" as File +project(':coverage-runtime').projectDir = "$rootDir/libraries/tools/kotlin-coverage/kotlin-coverage-runtime" as File + project(':kotlin-atomicfu-compiler-plugin').projectDir = file("$rootDir/plugins/atomicfu/atomicfu-compiler") project(':kotlin-atomicfu-compiler-plugin-embeddable').projectDir = file("$rootDir/plugins/atomicfu/atomicfu-compiler-embeddable") project(':kotlinx-atomicfu-runtime').projectDir = file("$rootDir/plugins/atomicfu/atomicfu-runtime")