diff --git a/libraries/tools/kotlin-gradle-plugin/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin/build.gradle.kts index 1e1880535e2..e41d77d5fa5 100644 --- a/libraries/tools/kotlin-gradle-plugin/build.gradle.kts +++ b/libraries/tools/kotlin-gradle-plugin/build.gradle.kts @@ -99,6 +99,7 @@ dependencies { functionalTestImplementation("com.github.gundy:semver4j:0.16.4:nodeps") { exclude(group = "*") } + functionalTestImplementation("org.reflections:reflections:0.10.2") } testCompileOnly(project(":compiler")) diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/AbstractKotlinSourceSet.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/AbstractKotlinSourceSet.kt new file mode 100644 index 00000000000..65b75fba0be --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/AbstractKotlinSourceSet.kt @@ -0,0 +1,50 @@ +/* + * 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("LeakingThis") + +package org.jetbrains.kotlin.gradle.plugin.sources + +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import org.jetbrains.kotlin.gradle.utils.MutableObservableSet +import org.jetbrains.kotlin.gradle.utils.ObservableSet + +abstract class AbstractKotlinSourceSet : InternalKotlinSourceSet { + private val dependsOnImpl = MutableObservableSet() + private val dependsOnClosureImpl = MutableObservableSet() + private val withDependsOnClosureImpl = MutableObservableSet(this) + + final override val dependsOn: ObservableSet + get() = dependsOnImpl + + final override val dependsOnClosure: ObservableSet + get() = dependsOnClosureImpl + + final override val withDependsOnClosure: ObservableSet + get() = withDependsOnClosureImpl + + final override fun dependsOn(other: KotlinSourceSet) { + if (other == this) return + /* + Circular dependsOn hierarchies are not allowed: + Throw if this SourceSet is already present in the dependsOnClosure of 'other' + */ + checkForCircularDependsOnEdges(other) + + /* Nothing to-do, if already added as dependency */ + if (!dependsOnImpl.add(other)) return + + /* Maintain dependsOn closure sets */ + other.internal.withDependsOnClosure.forAll { inDependsOnClosure -> + this.dependsOnClosureImpl.add(inDependsOnClosure) + this.withDependsOnClosureImpl.add(inDependsOnClosure) + } + + afterDependsOnAdded(other) + } + + protected open fun afterDependsOnAdded(other: KotlinSourceSet) = Unit +} + diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/CircularDependsOnChecker.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/CircularDependsOnChecker.kt new file mode 100644 index 00000000000..ba0330df40e --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/CircularDependsOnChecker.kt @@ -0,0 +1,28 @@ +/* + * 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.gradle.plugin.sources + +import org.gradle.api.InvalidUserCodeException +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet + +internal fun KotlinSourceSet.checkForCircularDependsOnEdges(other: KotlinSourceSet): Nothing? { + val stack = mutableListOf(this) + val visited = hashSetOf() + + fun checkReachableRecursively(from: KotlinSourceSet) { + if (!visited.add(from)) return + stack += from + if (this == from) throw InvalidUserCodeException( + "Circular dependsOn hierarchy found in the Kotlin source sets: ${(stack.toList()).joinToString(" -> ") { it.name }}" + ) + from.dependsOn.forEach { next -> checkReachableRecursively(next) } + stack -= from + } + + checkReachableRecursively(other) + return null +} + diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/DefaultKotlinSourceSet.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/DefaultKotlinSourceSet.kt index e0699a2fd5d..a5ce18fb6b7 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/DefaultKotlinSourceSet.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/DefaultKotlinSourceSet.kt @@ -3,6 +3,8 @@ * 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("DeprecatedCallableAddReplaceWith") + package org.jetbrains.kotlin.gradle.plugin.sources import org.gradle.api.Action @@ -27,7 +29,7 @@ const val METADATA_CONFIGURATION_NAME_SUFFIX = "DependenciesMetadata" abstract class DefaultKotlinSourceSet @Inject constructor( private val project: Project, val displayName: String -) : KotlinSourceSet { +) : AbstractKotlinSourceSet() { override val apiConfigurationName: String get() = disambiguateName(API) @@ -87,22 +89,12 @@ abstract class DefaultKotlinSourceSet @Inject constructor( override fun dependencies(configure: Action) = dependencies { configure.execute(this) } - override fun dependsOn(other: KotlinSourceSet) { - dependsOnSourceSetsImpl.add(other) - - // Fail-fast approach: check on each new added edge and report a circular dependency at once when the edge is added. - checkForCircularDependencies() - + override fun afterDependsOnAdded(other: KotlinSourceSet) { project.runProjectConfigurationHealthCheckWhenEvaluated { defaultSourceSetLanguageSettingsChecker.runAllChecks(this@DefaultKotlinSourceSet, other) } } - private val dependsOnSourceSetsImpl = mutableSetOf() - - override val dependsOn: Set - get() = dependsOnSourceSetsImpl - override fun toString(): String = "source set $name" private val explicitlyAddedCustomSourceFilesExtensions = ArrayList() @@ -221,32 +213,6 @@ internal val defaultSourceSetLanguageSettingsChecker = ).allChecks ) -private fun KotlinSourceSet.checkForCircularDependencies() { - // If adding an edge creates a cycle, than the source node of the edge belongs to the cycle, so run DFS from that node - // to check whether it became reachable from itself - val visited = hashSetOf() - val stack = LinkedHashSet() // Store the stack explicitly to pretty-print the cycle - - fun checkReachableRecursively(from: KotlinSourceSet) { - stack += from - visited += from - - for (to in from.dependsOn) { - if (to == this@checkForCircularDependencies) - throw InvalidUserCodeException( - "Circular dependsOn hierarchy found in the Kotlin source sets: " + - (stack.toList() + to).joinToString(" -> ") { it.name } - ) - - if (to !in visited) { - checkReachableRecursively(to) - } - } - stack -= from - } - - checkReachableRecursively(this@checkForCircularDependencies) -} internal fun KotlinSourceSet.disambiguateName(simpleName: String): String { val nameParts = listOfNotNull(this.name.takeIf { it != "main" }, simpleName) @@ -256,15 +222,18 @@ internal fun KotlinSourceSet.disambiguateName(simpleName: String): String { internal fun createDefaultSourceDirectorySet(project: Project, name: String?): SourceDirectorySet = project.objects.sourceDirectorySet(name!!, name) +@Deprecated("Use InternalKotlinSourceSet.dependsOnClosure instead") +val KotlinSourceSet.dependsOnClosure: Set get() = this.internal.dependsOnClosure -val KotlinSourceSet.dependsOnClosure get() = closure { it.dependsOn } +@Deprecated("Use InternalKotlinSourceSet.withDependsOnClosure instead") +val KotlinSourceSet.withDependsOnClosure: Set get() = this.internal.withDependsOnClosure -val KotlinSourceSet.withDependsOnClosure get() = withClosure { it.dependsOn } +val Iterable.dependsOnClosure: Set + get() = flatMap { it.internal.dependsOnClosure }.toSet() - this.toSet() -val Iterable.dependsOnClosure get() = closure { it.dependsOn } +val Iterable.withDependsOnClosure: Set + get() = flatMap { it.internal.withDependsOnClosure }.toSet() -val Iterable.withDependsOnClosure get() = withClosure { it.dependsOn } - -internal fun KotlinMultiplatformExtension.findSourceSetsDependingOn(sourceSet: KotlinSourceSet): Set { +fun KotlinMultiplatformExtension.findSourceSetsDependingOn(sourceSet: KotlinSourceSet): Set { return sourceSet.closure { seedSourceSet -> sourceSets.filter { otherSourceSet -> seedSourceSet in otherSourceSet.dependsOn } } } diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/InternalKotlinSourceSet.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/InternalKotlinSourceSet.kt new file mode 100644 index 00000000000..69323db22c2 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/InternalKotlinSourceSet.kt @@ -0,0 +1,20 @@ +/* + * 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.gradle.plugin.sources + +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import org.jetbrains.kotlin.gradle.utils.ObservableSet + +internal val KotlinSourceSet.internal: InternalKotlinSourceSet + get() = (this as? InternalKotlinSourceSet) ?: throw IllegalArgumentException( + "KotlinSourceSet $name does not implement ${InternalKotlinSourceSet::class.simpleName}" + ) + +internal interface InternalKotlinSourceSet : KotlinSourceSet { + override val dependsOn: ObservableSet + val dependsOnClosure: ObservableSet + val withDependsOnClosure: ObservableSet +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/kpm/FragmentMappedKotlinSourceSet.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/kpm/FragmentMappedKotlinSourceSet.kt index 857b6151a05..ac8a4d7618a 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/kpm/FragmentMappedKotlinSourceSet.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/kpm/FragmentMappedKotlinSourceSet.kt @@ -15,6 +15,7 @@ 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.pm20.* +import org.jetbrains.kotlin.gradle.plugin.sources.AbstractKotlinSourceSet import org.jetbrains.kotlin.gradle.plugin.sources.createDefaultSourceDirectorySet import javax.inject.Inject @@ -22,7 +23,7 @@ abstract class FragmentMappedKotlinSourceSet @Inject constructor( private val sourceSetName: String, private val project: Project, internal val underlyingFragment: GradleKpmFragment -) : KotlinSourceSet { +) : AbstractKotlinSourceSet() { val displayName: String get() = sourceSetName @@ -69,7 +70,7 @@ abstract class FragmentMappedKotlinSourceSet @Inject constructor( override fun dependencies(configure: Action) = underlyingFragment.dependencies(configure) - override fun dependsOn(other: KotlinSourceSet) { + override fun afterDependsOnAdded(other: KotlinSourceSet) { if (other !is FragmentMappedKotlinSourceSet) { throw InvalidUserDataException("Could set up dependsOn relationship with an unknown source set $other") } @@ -77,11 +78,6 @@ abstract class FragmentMappedKotlinSourceSet @Inject constructor( underlyingFragment.refines(otherFragment) } - override val dependsOn: Set - get() = project.kotlinExtension.sourceSets.filter { - it is FragmentMappedKotlinSourceSet && it.underlyingFragment in underlyingFragment.declaredRefinesDependencies - }.toSet() - override fun toString(): String = "source set $name" override val requiresVisibilityOf: Set diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/ObservableSet.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/ObservableSet.kt new file mode 100644 index 00000000000..7865c71cff9 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/ObservableSet.kt @@ -0,0 +1,82 @@ +/* + * 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.gradle.utils + +interface ObservableSet : Set { + fun whenObjectAdded(action: (T) -> Unit) + fun forAll(action: (T) -> Unit) +} + +internal class MutableObservableSet(vararg elements: T) : ObservableSet, MutableSet { + private val underlying = mutableSetOf(*elements) + private val whenObjectAddedActions = mutableListOf<(T) -> Unit>() + private val forAllActions = mutableListOf<(T) -> Unit>() + + override fun whenObjectAdded(action: (T) -> Unit) { + whenObjectAddedActions.add(action) + } + + override fun forAll(action: (T) -> Unit) { + forAllActions.add(action) + underlying.toList().forEach(action) + } + + override val size: Int + get() = underlying.size + + override fun clear() { + underlying.clear() + } + + override fun addAll(elements: Collection): Boolean { + val toAdd = elements.toSet() - underlying + return underlying.addAll(toAdd).also { + toAdd.forEach { added -> + whenObjectAddedActions.toList().forEach { action -> action(added) } + forAllActions.toList().forEach { action -> action(added) } + } + } + } + + override fun add(element: T): Boolean { + return underlying.add(element).also { + whenObjectAddedActions.toList().forEach { action -> action(element) } + forAllActions.toList().forEach { action -> action(element) } + } + } + + override fun isEmpty(): Boolean { + return underlying.isEmpty() + } + + override fun iterator(): MutableIterator { + return underlying.iterator() + } + + override fun retainAll(elements: Collection): Boolean { + return underlying.retainAll(elements) + } + + override fun removeAll(elements: Collection): Boolean { + return underlying.removeAll(elements) + } + + override fun remove(element: T): Boolean { + return underlying.remove(element) + } + + override fun containsAll(elements: Collection): Boolean { + return underlying.containsAll(elements) + } + + override fun contains(element: T): Boolean { + return underlying.contains(element) + } + + init { + underlying.addAll(elements) + } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/ObservableSetTest.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/ObservableSetTest.kt new file mode 100644 index 00000000000..3e69d182a48 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/ObservableSetTest.kt @@ -0,0 +1,60 @@ +/* + * 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.gradle + +import org.jetbrains.kotlin.gradle.utils.MutableObservableSet +import org.junit.Test +import kotlin.test.assertEquals + +class ObservableSetTest { + + class TestListener : (Int) -> Unit { + val invocations = mutableListOf() + override fun invoke(p1: Int) { + invocations.add(p1) + } + } + + @Test + fun `test - forAll`() { + val set = MutableObservableSet(1) + val testListener1 = TestListener() + val testListener2 = TestListener() + + set.forAll(testListener1) + assertEquals(listOf(1), testListener1.invocations) + + set.add(2) + assertEquals(listOf(1, 2), testListener1.invocations) + + set.addAll(listOf(3, 4)) + assertEquals(listOf(1, 2, 3, 4), testListener1.invocations) + + set.forAll(testListener2) + assertEquals(listOf(1, 2, 3, 4), testListener2.invocations) + } + + @Test + fun `test - whenObjectAdded`() { + val set = MutableObservableSet(1) + val testListener1 = TestListener() + val testListener2 = TestListener() + + set.whenObjectAdded(testListener1) + assertEquals(emptyList(), testListener1.invocations) + + set.add(2) + assertEquals(listOf(2), testListener1.invocations) + + set.addAll(listOf(3, 4)) + assertEquals(listOf(2, 3, 4), testListener1.invocations) + + set.whenObjectAdded(testListener2) + assertEquals(emptyList(), testListener2.invocations) + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/reflectionUtils.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/reflectionUtils.kt new file mode 100644 index 00000000000..45c7274d91d --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/reflectionUtils.kt @@ -0,0 +1,10 @@ +/* + * 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.gradle + +import org.reflections.Reflections + +val reflections = Reflections("org.jetbrains") \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/sources/InternalKotlinSourceSetTest.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/sources/InternalKotlinSourceSetTest.kt new file mode 100644 index 00000000000..12b91fcc132 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/sources/InternalKotlinSourceSetTest.kt @@ -0,0 +1,29 @@ +/* + * 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.gradle.sources + +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import org.jetbrains.kotlin.gradle.plugin.sources.InternalKotlinSourceSet +import org.jetbrains.kotlin.gradle.reflections +import kotlin.reflect.full.isSubclassOf +import kotlin.test.Test +import kotlin.test.fail + +class InternalKotlinSourceSetTest { + @Test + fun `test - all implementations of KotlinSourceSet - implement InternalKotlinSourceSet`() { + val subtypesOfKotlinSourceSet = reflections.getSubTypesOf(KotlinSourceSet::class.java) + subtypesOfKotlinSourceSet + .filter { subtype -> !subtype.isInterface } + .forEach { implementation -> + if (!implementation.kotlin.isSubclassOf(InternalKotlinSourceSet::class)) { + fail("$implementation does not implement ${InternalKotlinSourceSet::class}") + } + } + } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/SourceSetVisibilityInferenceTest.kt b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/SourceSetVisibilityInferenceTest.kt index c49c3d47ba8..4ab5fb8e7ab 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/SourceSetVisibilityInferenceTest.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/test/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/SourceSetVisibilityInferenceTest.kt @@ -16,6 +16,8 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinCompile import org.jetbrains.kotlin.gradle.plugin.* import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.KotlinCompilationData import org.jetbrains.kotlin.project.model.LanguageSettings +import org.jetbrains.kotlin.tooling.core.closure +import org.jetbrains.kotlin.tooling.core.withClosure import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -234,17 +236,11 @@ class SourceSetCompilationDsl { } } -class MockKotlinSourceSet(private val name: String) : KotlinSourceSet { +class MockKotlinSourceSet(private val name: String) : AbstractKotlinSourceSet() { override fun getName(): String = name - override val dependsOn: MutableSet = mutableSetOf() - override val requiresVisibilityOf: MutableSet = mutableSetOf() - override fun dependsOn(other: KotlinSourceSet) { - dependsOn += other - } - override fun requiresVisibilityOf(other: KotlinSourceSet) { requiresVisibilityOf += other }