diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/ResolveKotlinSourceSetsTest.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/ResolveKotlinSourceSetsTest.kt new file mode 100644 index 00000000000..7873d479b7c --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/ResolveKotlinSourceSetsTest.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2010-2021 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. + */ + +/* Associate compilations are not yet supported by the IDE. KT-34102 */ +@file:Suppress("invisible_reference", "invisible_member", "FunctionName", "DuplicatedCode") + +package org.jetbrains.kotlin.gradle + +import org.gradle.api.Project +import org.gradle.testfixtures.ProjectBuilder +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.sources.resolveAllDependsOnSourceSets +import org.jetbrains.kotlin.gradle.plugin.sources.resolveAllSourceSetsDependingOn +import org.junit.Test +import kotlin.test.BeforeTest +import kotlin.test.assertEquals + +class ResolveKotlinSourceSetsTest { + private lateinit var project: Project + private lateinit var kotlin: KotlinMultiplatformExtension + + @BeforeTest + fun setup() { + project = ProjectBuilder.builder().build() + project.plugins.apply("kotlin-multiplatform") + kotlin = project.extensions.getByName("kotlin") as KotlinMultiplatformExtension + + } + + @Test + fun resolveAllSourceSetsDependingOn() { + val commonMain = kotlin.sourceSets.getByName("commonMain") + val nativeMain = kotlin.sourceSets.create("nativeMain") + val linuxMain = kotlin.sourceSets.create("linuxMain") + val macosMain = kotlin.sourceSets.create("macosMain") + val jvmMain = kotlin.sourceSets.create("jvmMain") + + jvmMain.dependsOn(commonMain) + nativeMain.dependsOn(commonMain) + linuxMain.dependsOn(nativeMain) + macosMain.dependsOn(nativeMain) + + assertEquals( + setOf(nativeMain, linuxMain, macosMain, jvmMain), + kotlin.resolveAllSourceSetsDependingOn(commonMain) + ) + + assertEquals( + setOf(linuxMain, macosMain), + kotlin.resolveAllSourceSetsDependingOn(nativeMain) + ) + + assertEquals(emptySet(), kotlin.resolveAllSourceSetsDependingOn(linuxMain)) + assertEquals(emptySet(), kotlin.resolveAllSourceSetsDependingOn(macosMain)) + assertEquals(emptySet(), kotlin.resolveAllSourceSetsDependingOn(jvmMain)) + } + + @Test + fun resolveAllDependsOnSourceSets() { + val commonMain = kotlin.sourceSets.getByName("commonMain") + val nativeMain = kotlin.sourceSets.create("nativeMain") + val linuxMain = kotlin.sourceSets.create("linuxMain") + val macosMain = kotlin.sourceSets.create("macosMain") + val jvmMain = kotlin.sourceSets.create("jvmMain") + + jvmMain.dependsOn(commonMain) + nativeMain.dependsOn(commonMain) + linuxMain.dependsOn(nativeMain) + macosMain.dependsOn(nativeMain) + + assertEquals( + setOf(nativeMain, commonMain), linuxMain.resolveAllDependsOnSourceSets() + ) + + assertEquals( + setOf(commonMain), nativeMain.resolveAllDependsOnSourceSets() + ) + + assertEquals( + emptySet(), commonMain.resolveAllDependsOnSourceSets() + ) + + assertEquals( + setOf(commonMain), jvmMain.resolveAllDependsOnSourceSets() + ) + } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/DefaultKotlinSourceSet.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/DefaultKotlinSourceSet.kt index 49eb0d94765..09e62d976e5 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/DefaultKotlinSourceSet.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/DefaultKotlinSourceSet.kt @@ -11,16 +11,14 @@ import org.gradle.api.Project import org.gradle.api.file.SourceDirectorySet import org.gradle.util.ConfigureUtil import org.jetbrains.kotlin.build.DEFAULT_KOTLIN_SOURCE_FILES_EXTENSIONS +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet import org.jetbrains.kotlin.gradle.plugin.LanguageSettingsBuilder import org.jetbrains.kotlin.gradle.plugin.mpp.* -import org.jetbrains.kotlin.gradle.plugin.mpp.GranularMetadataTransformation -import org.jetbrains.kotlin.gradle.plugin.mpp.MetadataDependencyResolution import org.jetbrains.kotlin.gradle.plugin.whenEvaluated import org.jetbrains.kotlin.gradle.utils.* import java.io.File -import java.lang.reflect.Constructor import java.util.* const val METADATA_CONFIGURATION_NAME_SUFFIX = "DependenciesMetadata" @@ -72,7 +70,7 @@ class DefaultKotlinSourceSet( } override fun languageSettings(configure: LanguageSettingsBuilder.() -> Unit): LanguageSettingsBuilder = - languageSettings.apply { configure(this) } + languageSettings.apply { configure(this) } override fun getName(): String = displayName @@ -232,6 +230,12 @@ internal fun KotlinSourceSet.disambiguateName(simpleName: String): String { private fun createDefaultSourceDirectorySet(project: Project, name: String?): SourceDirectorySet = project.objects.sourceDirectorySet(name, name) + +@Deprecated( + "Use 'getAllDependsOnSourceSets' instead", + level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith("getAllDependsOnSourceSets") +) internal fun KotlinSourceSet.getSourceSetHierarchy(): Set { val result = mutableSetOf() @@ -245,10 +249,14 @@ internal fun KotlinSourceSet.getSourceSetHierarchy(): Set { return result } +internal fun KotlinSourceSet.resolveAllDependsOnSourceSets(): Set { + return transitiveClosure { dependsOn } +} -private fun Class.constructorOrNull(vararg parameterTypes: Class<*>): Constructor? = - try { - getConstructor(*parameterTypes) - } catch (e: NoSuchMethodException) { - null - } +internal fun Iterable.resolveAllDependsOnSourceSets(): Set { + return flatMapTo(mutableSetOf()) { it.resolveAllDependsOnSourceSets() } +} + +internal fun KotlinMultiplatformExtension.resolveAllSourceSetsDependingOn(sourceSet: KotlinSourceSet): Set { + return sourceSet.transitiveClosure { sourceSets.filter { otherSourceSet -> this in otherSourceSet.dependsOn } } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/utils/transitiveClosure.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/utils/transitiveClosure.kt new file mode 100644 index 00000000000..eb33189186c --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/utils/transitiveClosure.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2010-2021 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.utils + +/** + * General purpose implementation of a transitive closure + * - Recursion free + * - Predictable amount of allocations + * - Handles loops and self references gracefully + * @param edges: Producer function from one node to all its children. This implementation can handle loops and self references gracefully. + * @return Note: No guarantees given about the order ot this [Set] + */ +internal inline fun T.transitiveClosure(edges: T.() -> Iterable): Set { + // Fast path when initial edges are empty + val initialEdges = edges() + if (initialEdges is Collection && initialEdges.isEmpty()) return emptySet() + + val queue = deque() + val results = mutableSetOf() + queue.addAll(initialEdges) + while (queue.isNotEmpty()) { + // ArrayDeque implementation will optimize this call to 'removeFirst' + val resolved = queue.removeAt(0) + if (resolved != this && results.add(resolved)) { + queue.addAll(resolved.edges()) + } + } + + return results.toSet() +} + +@OptIn(ExperimentalStdlibApi::class) +private inline fun deque(): MutableList { + return if (KotlinVersion.CURRENT.isAtLeast(1, 4)) ArrayDeque() + else mutableListOf() +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/utils/TransitiveClosureTest.kt b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/utils/TransitiveClosureTest.kt new file mode 100644 index 00000000000..0e8011ba1b2 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/utils/TransitiveClosureTest.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2010-2021 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.utils + +import kotlin.test.Test +import kotlin.test.assertEquals + +class TransitiveClosureTest { + private class Node(val value: String, val children: MutableList = mutableListOf()) { + + override fun toString(): String { + return value + } + + override fun equals(other: Any?): Boolean { + if (other !is Node) return false + return other.value == value + } + + override fun hashCode(): Int { + return value.hashCode() + } + } + + private fun Node.transitiveClosure() = transitiveClosure { children } + + @Test + fun `transitiveClosure does not include root node`() { + val closure = Node("a", mutableListOf(Node("b"), Node("c"))).transitiveClosure() + assertEquals(setOf(Node("b"), Node("c")), closure, "Expected transitiveClosure to not include root node") + } + + @Test + fun `transitiveClosure handles loop and self references`() { + val nodeA = Node("a") + val nodeB = Node("b") + val nodeC = Node("c") + val nodeD = Node("d") + + // a -> b -> c -> d + nodeA.children.add(nodeB) + nodeB.children.add(nodeC) + nodeC.children.add(nodeD) + + // add self reference to b + nodeB.children.add(nodeB) + + // add loop from c -> a + nodeC.children.add(nodeA) + + val closure = nodeA.transitiveClosure() + assertEquals( + setOf(nodeB, nodeC, nodeD), closure, + "Expected transitiveClosure to be robust against loops and self references" + ) + } +}