diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/util/reportSourceSetCommonizerDependencies.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/util/reportSourceSetCommonizerDependencies.kt index cb1517c2dd5..7563b919f83 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/util/reportSourceSetCommonizerDependencies.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/util/reportSourceSetCommonizerDependencies.kt @@ -10,12 +10,12 @@ import org.intellij.lang.annotations.RegExp import org.jetbrains.kotlin.commonizer.CommonizerTarget import org.jetbrains.kotlin.commonizer.SharedCommonizerTarget import org.jetbrains.kotlin.commonizer.parseCommonizerTarget -import org.jetbrains.kotlin.commonizer.util.transitiveClosure import org.jetbrains.kotlin.gradle.BaseGradleIT import org.jetbrains.kotlin.gradle.BaseGradleIT.CompiledProject import org.jetbrains.kotlin.library.ToolingSingleFileKlibResolveStrategy import org.jetbrains.kotlin.library.commonizerTarget import org.jetbrains.kotlin.library.resolveSingleFileKlib +import org.jetbrains.kotlin.tooling.core.singleClosure import java.io.File import javax.annotation.RegEx import kotlin.test.fail @@ -50,7 +50,7 @@ data class SourceSetCommonizerDependencies( if (konanDataDir != null) { return file.startsWith(konanDataDir) } - return file.allParents.any { parentFile -> parentFile.name == ".konan" } + return file.parentsClosure.any { parentFile -> parentFile.name == ".konan" } } fun assertTargetOnAllDependencies(target: CommonizerTarget) = apply { @@ -165,7 +165,7 @@ private fun inferCommonizerTargetOrNull(libraryFile: File): CommonizerTarget? = strategy = ToolingSingleFileKlibResolveStrategy ).commonizerTarget?.let(::parseCommonizerTarget) -private val File.allParents: Set get() = transitiveClosure(this) { listOfNotNull(parentFile) } +private val File.parentsClosure: Set get() = this.singleClosure { parentFile } private const val dollar = "\$" @@ -186,4 +186,4 @@ tasks.register("reportCommonizerSourceSetDependencies") { } } } -""".trimIndent() \ No newline at end of file +""".trimIndent() diff --git a/libraries/tools/kotlin-tooling-core/src/main/kotlin/org/jetbrains/kotlin/tooling/core/closure.kt b/libraries/tools/kotlin-tooling-core/src/main/kotlin/org/jetbrains/kotlin/tooling/core/closure.kt new file mode 100644 index 00000000000..9e68814e7aa --- /dev/null +++ b/libraries/tools/kotlin-tooling-core/src/main/kotlin/org/jetbrains/kotlin/tooling/core/closure.kt @@ -0,0 +1,138 @@ +/* + * Copyright 2010-2022 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:Suppress("FunctionName") + +package org.jetbrains.kotlin.tooling.core + +/** + * 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: The order of the set is guaranteed to be bfs + */ +inline fun T.closure(edges: (T) -> Iterable): Set = + closureTo( + destination = createResultSet(), + exclude = emptySet(), + dequeue = createDequeue(), + seed = this, + enqueueNextElements = { element -> addAll(edges(element)) } + ) + +/** + * Similar to [closure], but will also include the receiver(seed) of this function into the final set + * @see closure + */ +inline fun T.withClosure(edges: (T) -> Iterable): Set = + closureTo( + destination = createResultSet(this), + exclude = emptySet(), + dequeue = createDequeue(), + seed = this, + enqueueNextElements = { element -> addAll(edges(element)) } + ) + +/** + * @see closure + * @receiver: Will not be included in the return set + */ +inline fun Iterable.closure(edges: (T) -> Iterable): Set { + val thisSet = this.toSet() + return closureTo( + destination = createResultSet(thisSet.size * 2), + exclude = thisSet, + dequeue = createDequeue(thisSet.size * 2), edges = edges + ) +} + +/** + * @see closure + * @receiver: Will be included in the return set + */ +inline fun Iterable.withClosure(edges: (T) -> Iterable): Set { + val size = this.count() + return closureTo( + destination = createResultSet(this, size * 2), + exclude = emptySet(), + dequeue = createDequeue(size * 2), + edges = edges + ) +} + +/** + * @see closure + * @receiver is not included in the return set + */ +inline fun T.singleClosure(next: (T) -> T?): Set = + closureTo(createResultSet(), emptySet(), createDequeue(), this) { element -> next(element)?.let { add(it) } } + +/** + * @see closure + * @receiver is included in the return set + */ +inline fun T.withSingleClosure(next: (T) -> T?): Set = + closureTo(createResultSet(this), emptySet(), createDequeue(), this) { element -> next(element)?.let { add(it) } } + +/* Implementation */ + +private typealias Results = MutableSet +private typealias Dequeue = MutableList + +@PublishedApi +internal inline fun Iterable.closureTo( + destination: Results, exclude: Set, dequeue: Dequeue, edges: (T) -> Iterable +): Set { + forEach { seed -> + closureTo( + destination = destination, + exclude = exclude, + dequeue = dequeue, + seed = seed, + enqueueNextElements = { element -> addAll(edges(element)) }) + } + return destination +} + +@PublishedApi +internal inline fun closureTo( + destination: Results, + exclude: Set, + dequeue: Dequeue, + seed: T, + enqueueNextElements: Dequeue.(value: T) -> Unit +): Set { + dequeue.enqueueNextElements(seed) + while (dequeue.isNotEmpty()) { + val element = dequeue.removeAt(0) + if (element != seed && element !in exclude && destination.add(element)) { + dequeue.enqueueNextElements(element) + } + } + return destination +} + +@PublishedApi +internal inline fun createDequeue(initialSize: Int = 16): MutableList { + return if (KotlinVersion.CURRENT.isAtLeast(1, 4)) ArrayDeque(initialSize) + else ArrayList(initialSize) +} + +@PublishedApi +internal fun createResultSet(initialSize: Int = 16): MutableSet { + return LinkedHashSet(initialSize) +} + +@PublishedApi +internal fun createResultSet(withValue: T, initialSize: Int = 16): MutableSet { + return LinkedHashSet(initialSize).also { it.add(withValue) } +} + +@PublishedApi +internal fun createResultSet(withValues: Iterable, initialSize: Int = 16): MutableSet { + return LinkedHashSet(initialSize).also { it.addAll(withValues) } +} diff --git a/libraries/tools/kotlin-tooling-core/src/test/kotlin/org/jetbrains/kotlin/tooling/core/ClosureTest.kt b/libraries/tools/kotlin-tooling-core/src/test/kotlin/org/jetbrains/kotlin/tooling/core/ClosureTest.kt new file mode 100644 index 00000000000..de811eeb4fb --- /dev/null +++ b/libraries/tools/kotlin-tooling-core/src/test/kotlin/org/jetbrains/kotlin/tooling/core/ClosureTest.kt @@ -0,0 +1,164 @@ +/* + * Copyright 2010-2022 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.tooling.core + +import org.junit.Test +import kotlin.test.assertEquals + +class ClosureTest { + + private class Node(val value: String, var parent: Node? = null, val children: MutableList = mutableListOf()) { + override fun toString(): String = value + } + + @Test + fun `closure does not include root node`() { + val closure = Node("a", children = mutableListOf(Node("b"), Node("c"))).closure { it.children } + assertEquals( + listOf("b", "c"), closure.map { it.value }, + "Expected closure to not include root node" + ) + } + + @Test + fun `withClosure does include root node`() { + val closure = Node("a", children = mutableListOf(Node("b"), Node("c"))).withClosure { it.children } + assertEquals( + listOf("a", "b", "c"), closure.map { it.value }, + "Expected 'withClosure' to include root node" + ) + } + + @Test + fun `closure 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.closure { it.children } + assertEquals( + setOf(nodeB, nodeC, nodeD), closure, + "Expected transitiveClosure to be robust against loops and self references" + ) + } + + @Test + fun `withClosure 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.withClosure { it.children } + assertEquals( + setOf(nodeA, nodeB, nodeC, nodeD), closure, + "Expected transitiveClosure to be robust against loops and self references" + ) + } + + @Test + fun `closure on List`() { + val a = Node("a") + val b = Node("b") + val c = Node("c") + val d = Node("d") + val e = Node("e") + val f = Node("f") + val g = Node("g") + + // a -> b, c -> d + a.children.add(b) + a.children.add(c) + c.children.add(d) + + // e -> f, g, a + e.children.add(f) + e.children.add(g) + e.children.add(a) // <- cycle back to a! + + assertEquals( + listOf("b", "c", "d", "f", "g"), // <- a *is not* listed in closure!!! + listOf(a, e).closure { it.children }.map { it.value } + ) + } + + @Test + fun `withClosure on List`() { + val a = Node("a") + val b = Node("b") + val c = Node("c") + val d = Node("d") + val e = Node("e") + val f = Node("f") + val g = Node("g") + + // a -> b, c -> d + a.children.add(b) + a.children.add(c) + c.children.add(d) + + // e -> f, g, a + e.children.add(f) + e.children.add(g) + e.children.add(a) // <- cycle back to a! + + assertEquals( + listOf("a", "e", "b", "c", "d", "f", "g"), + listOf(a, e).withClosure { it.children }.map { it.value } + ) + } + + @Test + fun singleClosure() { + val a = Node("a") + val b = Node("b") + val c = Node("c") + + c.parent = b + b.parent = a + + assertEquals( + listOf("b", "a"), c.singleClosure { it.parent }.map { it.value }, + ) + } + + @Test + fun withSingleClosure() { + val a = Node("a") + val b = Node("b") + val c = Node("c") + + c.parent = b + b.parent = a + + assertEquals( + listOf("c", "b", "a"), c.withSingleClosure { it.parent }.map { it.value }, + ) + } +} diff --git a/native/commonizer-api/src/org/jetbrains/kotlin/commonizer/util/transitiveClosure.kt b/native/commonizer-api/src/org/jetbrains/kotlin/commonizer/util/transitiveClosure.kt index 241be05429d..134c74046a0 100644 --- a/native/commonizer-api/src/org/jetbrains/kotlin/commonizer/util/transitiveClosure.kt +++ b/native/commonizer-api/src/org/jetbrains/kotlin/commonizer/util/transitiveClosure.kt @@ -5,14 +5,7 @@ package org.jetbrains.kotlin.commonizer.util -/** - * 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] - */ + public inline fun transitiveClosure(seed: T, edges: T.() -> Iterable): Set { // Fast path when initial edges are empty val initialEdges = seed.edges() @@ -32,7 +25,6 @@ public inline fun transitiveClosure(seed: T, edges: T.() -> Iterable return results.toSet() } -@OptIn(ExperimentalStdlibApi::class) @PublishedApi internal inline fun deque(initialSize: Int): MutableList { return if (KotlinVersion.CURRENT.isAtLeast(1, 4)) ArrayDeque(initialSize)