[Gradle] Add KotlinSourceSetTreeDependsOnMismatch diagnostic

In Kotlin Multiplatform it is not possible to have
source set dependencies between two different SourceSet Trees.

For example commonTest can't dependOn commonMain as SourceSet dependency
instead binaries of commonMain should be included as dependency to
commonTest. Which is implemented through different mechanisms

^KT-47144 Verification Pending
This commit is contained in:
Anton Lakotka
2023-06-02 12:22:26 +02:00
committed by Space Team
parent 05e22e56d3
commit 681305c8e9
4 changed files with 201 additions and 1 deletions
@@ -106,7 +106,8 @@ internal interface KotlinGradleProjectChecker {
DisabledNativeTargetsChecker,
JsEnvironmentChecker,
PreHmppDependenciesUsageChecker,
ExperimentalK2UsageChecker
ExperimentalK2UsageChecker,
KotlinSourceSetTreeDependsOnMismatchChecker,
)
}
}
@@ -330,4 +330,20 @@ object KotlinToolingDiagnostics {
""".trimIndent()
)
}
object KotlinSourceSetTreeDependsOnMismatch : ToolingDiagnosticFactory(ERROR) {
operator fun invoke(dependeeName: String, dependencyName: String) = build(
"""
KotlinSourceSets '${dependeeName}' can't depend on '${dependencyName}'
as they belong to different KotlinSourceSet trees. Please remove this dependency.
""".trimIndent()
)
operator fun invoke(dependeeAName: String, dependeeBName: String, dependencyName: String) = build(
"""
KotlinSourceSets '${dependeeAName}' and '${dependeeBName}' can't both depend on '${dependencyName}'
as they belong to different KotlinSourceSet trees. Please remove one of the dependencies.
""".trimIndent()
)
}
}
@@ -0,0 +1,90 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle.plugin.diagnostics.checkers
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
import org.jetbrains.kotlin.gradle.plugin.KotlinTargetHierarchy.SourceSetTree
import org.jetbrains.kotlin.gradle.plugin.diagnostics.KotlinGradleProjectChecker
import org.jetbrains.kotlin.gradle.plugin.diagnostics.KotlinGradleProjectCheckerContext
import org.jetbrains.kotlin.gradle.plugin.diagnostics.KotlinToolingDiagnostics.KotlinSourceSetTreeDependsOnMismatch
import org.jetbrains.kotlin.gradle.plugin.diagnostics.KotlinToolingDiagnosticsCollector
import org.jetbrains.kotlin.gradle.plugin.mpp.targetHierarchy.orNull
import org.jetbrains.kotlin.gradle.plugin.sources.android.androidSourceSetInfoOrNull
import org.jetbrains.kotlin.gradle.plugin.sources.awaitPlatformCompilations
import org.jetbrains.kotlin.gradle.plugin.sources.internal
internal object KotlinSourceSetTreeDependsOnMismatchChecker : KotlinGradleProjectChecker {
override suspend fun KotlinGradleProjectCheckerContext.runChecks(collector: KotlinToolingDiagnosticsCollector) {
val sourceSets = this.multiplatformExtension
?.awaitSourceSets()
// Ignoring Android source sets
?.filter { it.androidSourceSetInfoOrNull == null }
?: return
// A "good" source set is part of only single Source Set Tree
val goodSourceSets = mutableMapOf<KotlinSourceSet, SourceSetTree?>()
// A "bad" source set is part of >=2 Source Set Trees
val badSourceSets = mutableMapOf<KotlinSourceSet, List<SourceSetTree?>>()
val reverseSourceSetDependencies = mutableMapOf<KotlinSourceSet, MutableSet<KotlinSourceSet>>()
fun KotlinSourceSet.addReverseDependencyTo(that: KotlinSourceSet) =
reverseSourceSetDependencies.getOrPut(this) { mutableSetOf() }.add(that)
for (sourceSet in sourceSets) {
sourceSet.dependsOn.forEach { it.addReverseDependencyTo(sourceSet) }
val platformCompilations = sourceSet.internal.awaitPlatformCompilations()
val distinctSourceSetTrees = platformCompilations.map { SourceSetTree.orNull(it) }.distinct()
val totalDistinctSourceSetTrees = distinctSourceSetTrees.size
@Suppress("KotlinConstantConditions")
when {
totalDistinctSourceSetTrees > 1 -> badSourceSets[sourceSet] = distinctSourceSetTrees
totalDistinctSourceSetTrees == 1 -> goodSourceSets[sourceSet] = distinctSourceSetTrees.single()
// case when source set has no platform compilation and thus its totalDistinctSourceSetTrees == 0
// is covered by [UnusedSourceSetsChecker]
totalDistinctSourceSetTrees == 0 -> continue
}
}
for ((badSourceSet, sourceSetTrees) in badSourceSets) {
val dependents = reverseSourceSetDependencies[badSourceSet].orEmpty()
// check if any of its dependents is also a bad source set then skip it
// For example if nativeTest depends on nativeMain,
// then transitively commonMain would have incorrect source set tree as well, but it is not a root cause.
// NB: user can still add commonTest to commonMain dependency directly but this case will not be reported
// until "dependent source sets relations is fixed
if (dependents.any { it in badSourceSets }) continue
// Heuristic: pick two source sets with different SourceSetTree
// Theoretically there could be a lot of invalid pairs,
// but it should be enough to report only 1 of them as it should cover the majority of cases.
// NB: it is safe to do destruction on two elements as per definition of "bad" source set.
val (sourceSetTreeA, sourceSetTreeB) = sourceSetTrees
val sourceSetA = dependents.firstOrNull { it in goodSourceSets && goodSourceSets[it] == sourceSetTreeA }
val sourceSetB = dependents.firstOrNull { it in goodSourceSets && goodSourceSets[it] == sourceSetTreeB }
if (sourceSetA == null || sourceSetB == null) {
val goodSourceSet = sourceSetA ?: sourceSetB
if (goodSourceSet == null) {
project.logger.warn("Can't identify why kotlin source set '${badSourceSet.name}' has incorrect dependsOn relation")
} else {
collector.report(
project,
KotlinSourceSetTreeDependsOnMismatch(goodSourceSet.name, badSourceSet.name)
)
}
} else {
collector.report(
project,
KotlinSourceSetTreeDependsOnMismatch(sourceSetA.name, sourceSetB.name, badSourceSet.name)
)
}
}
}
}
@@ -0,0 +1,93 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
@file:Suppress("FunctionName")
package org.jetbrains.kotlin.gradle.unitTests.diagnosticsTests
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.diagnostics.KotlinToolingDiagnostics.KotlinSourceSetTreeDependsOnMismatch
import org.jetbrains.kotlin.gradle.plugin.diagnostics.ToolingDiagnostic
import org.jetbrains.kotlin.gradle.plugin.diagnostics.kotlinToolingDiagnosticsCollector
import org.jetbrains.kotlin.gradle.util.*
import org.jetbrains.kotlin.gradle.util.assertContainsDiagnostic
import kotlin.test.*
class KotlinSourceSetTreeDependsOnMismatchTest {
private fun checkDiagnostics(configure: KotlinMultiplatformExtension.() -> Unit): List<ToolingDiagnostic> {
val project = buildProjectWithMPP {
kotlin {
targetHierarchy.default()
project.androidApplication { compileSdk = 32 }
iosX64(); iosArm64(); iosSimulatorArm64()
watchosArm64(); watchosX64(); watchosSimulatorArm64()
macosX64(); macosArm64()
linuxX64(); linuxArm64()
configure()
}
}
project.evaluate()
return project.kotlinToolingDiagnosticsCollector.getDiagnosticsForProject(project).toList()
}
private fun checkBadSourceSetDependency(
correctDependent: String,
incorrectDependent: String,
dependency: String
) = checkDiagnostics {
sourceSets.getByName(incorrectDependent).dependsOn(sourceSets.getByName(dependency))
}.assertContainsDiagnostic(
KotlinSourceSetTreeDependsOnMismatch(correctDependent, incorrectDependent, dependency)
)
private fun checkBadSourceSetDependency(
incorrectDependent: String,
dependency: String
) = checkDiagnostics {
sourceSets.getByName(incorrectDependent).dependsOn(sourceSets.getByName(dependency))
}.assertContainsDiagnostic(
KotlinSourceSetTreeDependsOnMismatch(incorrectDependent, dependency)
)
@Test
fun `no diagnostics should be reported for correctly configured project`() {
checkDiagnostics {
// no extra configurations
}.assertNoDiagnostics(KotlinSourceSetTreeDependsOnMismatch.id)
}
@Test
fun `commonTest cant depend on commonMain`() = checkBadSourceSetDependency(
correctDependent = "nativeMain",
incorrectDependent = "commonTest",
dependency = "commonMain"
)
@Test
fun `commonMain cant depend on commonTest`() = checkBadSourceSetDependency(
correctDependent = "nativeTest",
incorrectDependent = "commonMain",
dependency = "commonTest"
)
@Test
fun `appleTest cant depend on appleMain`() = checkBadSourceSetDependency(
correctDependent = "iosMain",
incorrectDependent = "appleTest",
dependency = "appleMain"
)
@Test
fun `iosX64Test cant depend on iosX64Main`() = checkBadSourceSetDependency(
incorrectDependent = "iosX64Test",
dependency = "iosX64Main",
)
@Test
fun `iosX64Test cant depend on commonMain`() = checkBadSourceSetDependency(
correctDependent = "nativeMain",
incorrectDependent = "iosX64Test",
dependency = "commonMain",
)
}