[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:
committed by
Space Team
parent
05e22e56d3
commit
681305c8e9
+2
-1
@@ -106,7 +106,8 @@ internal interface KotlinGradleProjectChecker {
|
||||
DisabledNativeTargetsChecker,
|
||||
JsEnvironmentChecker,
|
||||
PreHmppDependenciesUsageChecker,
|
||||
ExperimentalK2UsageChecker
|
||||
ExperimentalK2UsageChecker,
|
||||
KotlinSourceSetTreeDependsOnMismatchChecker,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+16
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+90
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+93
@@ -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",
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user