[Gradle] Report warning when there is multiple source roots in compilation

This issue should warn users about possible problems in K2 compiler.
Because in this setup there will be no symbols visibility between
those source set roots as it used to be in K1.
There is an assumption that this case usually appears in the code
generation setup. And users might not experience any problems before.

^KT-64913 Verification Pending
This commit is contained in:
Anton Lakotka
2024-02-23 13:15:28 +01:00
committed by Space Team
parent b5eccd63b1
commit 3ff2bc403c
10 changed files with 476 additions and 3 deletions
@@ -12,6 +12,7 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinSourceSetConvention.isRegisteredByK
import org.jetbrains.kotlin.gradle.dsl.NativeTargetShortcutTrace
import org.jetbrains.kotlin.gradle.internal.KOTLIN_BUILD_TOOLS_API_IMPL
import org.jetbrains.kotlin.gradle.internal.KOTLIN_MODULE_GROUP
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider
@@ -98,6 +99,66 @@ object KotlinToolingDiagnostics {
}
}
object MultipleSourceSetRootsInCompilation : ToolingDiagnosticFactory(WARNING) {
operator fun invoke(
kotlinCompilation: KotlinCompilation<*>,
unexpectedSourceSetRoot: String,
expectedRoot: String,
): ToolingDiagnostic = build(
"""
Kotlin Source Set '$unexpectedSourceSetRoot' is included to '${kotlinCompilation.name}' compilation of '${kotlinCompilation.target.name}' target,
but it doesn't depend on '$expectedRoot'.
Please remove '$unexpectedSourceSetRoot' and include its sources to the compilation's default source set:
kotlin.sourceSets["${kotlinCompilation.defaultSourceSet.name}"].kotlin.srcDir() // <-- pass sources directory of '$unexpectedSourceSetRoot'
Or provide explicit dependency if the solution above is not applicable
kotlin.sourceSets["$unexpectedSourceSetRoot"].dependsOn($expectedRoot)
See https://kotl.in/connecting-source-sets for more details.
""".trimIndent()
)
operator fun invoke(
targetNames: Collection<String>,
unexpectedSourceSetRoot: String,
expectedRoot: String,
): ToolingDiagnostic = build(
"""
Kotlin Source Set '$unexpectedSourceSetRoot' is included in compilations of Kotlin Targets: ${targetNames.joinToString(", ") { "'$it'" }}
but it doesn't depend on '$expectedRoot'
Please remove '$unexpectedSourceSetRoot' and include its sources to one of the default source set: https://kotl.in/hierarchy-template
For example:
kotlin.sourceSets.commonMain.kotlin.srcDir() // <-- pass here sources directory
Or add explicit dependency if the solution above is not applicable:
kotlin.sourceSets["$unexpectedSourceSetRoot"].dependsOn($expectedRoot)
See https://kotl.in/connecting-source-sets for more details.
""".trimIndent()
)
operator fun invoke(kotlinCompilation: KotlinCompilation<*>, sourceSetRoots: Collection<String>) = build(
"""
Kotlin Source Sets: ${sourceSetRoots.joinToString(", ") { "'$it'" }}
are included to '${kotlinCompilation.name}' compilation of '${kotlinCompilation.target.name}' target.
However, they have no common source set root between them.
Please remove these kotlin source sets and include their source directories to the compilation's default source set.
kotlin.sourceSets["${kotlinCompilation.defaultSourceSet.name}"].kotlin.srcDir() // <-- pass sources directories here
Or, if the solution above is not applicable, specify `dependsOn` edges between these source sets so that there are no multiple roots.
See https://kotl.in/connecting-source-sets for more details.
""".trimIndent()
)
}
object AndroidSourceSetLayoutV1Deprecation : ToolingDiagnosticFactory(ERROR) {
operator fun invoke() = build(
"""
@@ -0,0 +1,103 @@
/*
* Copyright 2010-2024 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.*
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.Stage.ReadyForExecution
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet.Companion.COMMON_MAIN_SOURCE_SET_NAME
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet.Companion.COMMON_TEST_SOURCE_SET_NAME
import org.jetbrains.kotlin.gradle.plugin.await
import org.jetbrains.kotlin.gradle.plugin.diagnostics.KotlinGradleProjectChecker
import org.jetbrains.kotlin.gradle.plugin.diagnostics.KotlinGradleProjectCheckerContext
import org.jetbrains.kotlin.gradle.plugin.diagnostics.KotlinToolingDiagnostics.MultipleSourceSetRootsInCompilation
import org.jetbrains.kotlin.gradle.plugin.diagnostics.KotlinToolingDiagnosticsCollector
import org.jetbrains.kotlin.gradle.plugin.mpp.isMain
import org.jetbrains.kotlin.gradle.plugin.mpp.isTest
import org.jetbrains.kotlin.gradle.plugin.sources.internal
import org.jetbrains.kotlin.gradle.plugin.sources.withDependsOnClosure
internal object MultipleSourceSetRootsInCompilationChecker : KotlinGradleProjectChecker {
private fun KotlinCompilation<*>.sourceSetRoots() = kotlinSourceSets.withDependsOnClosure.filter { it.dependsOn.isEmpty() }
override suspend fun KotlinGradleProjectCheckerContext.runChecks(collector: KotlinToolingDiagnosticsCollector) {
// Await for the last Stage and perform check to ensure that the final state is correct.
ReadyForExecution.await()
val targets = multiplatformExtension?.awaitTargets() ?: return
val (allDefaultCompilationsWithMultipleRoots, allNonDefaultCompilationsWithMultipleRoots) = targets
// Exclude metadata target because users don't declare it explicitly, and we don't want to ask them to configure it.
// If some metadata compilation has multiple source set roots,
// then underlying platform compilations should report the same.
.filter { it.platformType != KotlinPlatformType.common }
.flatMap { it.compilations }
.filter { it.sourceSetRoots().size > 1 }
.partition { it.isMain() || it.isTest() }
collector.reportForDefaultPlatformCompilations(allDefaultCompilationsWithMultipleRoots)
collector.reportForNonDefaultCompilations(allNonDefaultCompilationsWithMultipleRoots)
}
/**
* Report for 'main' and 'test' compilations.
* These are special because we know that all source sets should depend on `commonMain` or `commonTest` accordingly.
*/
private fun KotlinToolingDiagnosticsCollector.reportForDefaultPlatformCompilations(compilations: Collection<KotlinCompilation<*>>) {
// Some source sets can be included in multiple compilations we don't want to report a diagnostic for each case.
val alreadyReportedSourceSet = mutableSetOf<KotlinSourceSet>()
for (compilation in compilations) {
val expectedSourceSetRoot = when {
compilation.isMain() -> COMMON_MAIN_SOURCE_SET_NAME
compilation.isTest() -> COMMON_TEST_SOURCE_SET_NAME
else -> continue
}
val unexpectedSourceSetRoots = compilation.sourceSetRoots().filter { it.name != expectedSourceSetRoot }
// In most cases, I expect to have only 1 unexpected source set root.
// So it is ok to report diagnostic for each unexpectedSourceSetRoot.
unexpectedSourceSetRoots.forEach { unexpectedSourceSetRoot ->
if (!alreadyReportedSourceSet.add(unexpectedSourceSetRoot)) return@forEach
val includedIntoCompilations = unexpectedSourceSetRoot.internal
.compilations
.filter { it.platformType != KotlinPlatformType.common }
if (includedIntoCompilations.isEmpty()) return@forEach // this case is handled by a different diagnostic
val singleCompilation = includedIntoCompilations.singleOrNull()
val diagnostic = if (singleCompilation != null) {
MultipleSourceSetRootsInCompilation(
singleCompilation,
unexpectedSourceSetRoot.name,
expectedSourceSetRoot
)
} else {
MultipleSourceSetRootsInCompilation(
targetNames = includedIntoCompilations.map { it.target.name },
unexpectedSourceSetRoot.name,
expectedSourceSetRoot
)
}
report(compilation.project, diagnostic)
}
}
}
/**
* For non-default compilations, we don't know which of the multiple source set roots should win so report diagnostic differently
*/
private fun KotlinToolingDiagnosticsCollector.reportForNonDefaultCompilations(compilations: Collection<KotlinCompilation<*>>) {
for (compilation in compilations) {
// Exclude android compilations as they might incorrectly report due to connection to android source sets
// where no dependsOn relations don't have to be declared.
if (compilation.target.platformType == KotlinPlatformType.androidJvm) continue
val diagnostic = MultipleSourceSetRootsInCompilation(compilation, compilation.sourceSetRoots().map { it.name })
report(compilation.project, diagnostic)
}
}
}
@@ -141,6 +141,7 @@ internal fun Project.registerKotlinPluginExtensions() {
if (isMultiplatform) {
register(project, KotlinMultiplatformAndroidGradlePluginCompatibilityChecker)
register(project, MultipleSourceSetRootsInCompilationChecker)
}
}
}
@@ -0,0 +1,110 @@
/*
* Copyright 2010-2024 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.gradle.api.Project
import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.disambiguateName
import org.jetbrains.kotlin.gradle.util.*
import org.jetbrains.kotlin.gradle.util.checkDiagnosticsWithMppProject
import kotlin.test.Test
class MultipleSourceSetRootsInCompilationTest {
private fun checkDiagnostics(name: String, projectConfiguration: Project.() -> Unit) =
checkDiagnosticsWithMppProject("MultipleSourceSetRootsInCompilationTest/${name}", projectConfiguration)
@Test
fun extraSourceSetInEachTarget() {
// It is expected to see diagnostic reported for each extra source set
checkDiagnostics("extraSourceSetInEachTarget") {
kotlin {
fun KotlinTarget.addExtraSourceSets() {
val extraMain = project.multiplatformExtension.sourceSets.create(disambiguateName("extraMain"))
val extraTest = project.multiplatformExtension.sourceSets.create(disambiguateName("extraTest"))
compilations.getByName("main").defaultSourceSet.dependsOn(extraMain)
compilations.getByName("test").defaultSourceSet.dependsOn(extraTest)
}
jvm {
addExtraSourceSets()
}
js {
nodejs()
addExtraSourceSets()
}
linuxX64 {
addExtraSourceSets()
}
}
}
}
@Test
fun secondCommonMain() {
// commonMain2 is included to multiple targets, but it is expected to see only 1 diagnostic reported
checkDiagnostics("secondCommonMain") {
kotlin {
val commonMain2 = sourceSets.create("commonMain2")
listOf(jvm(), js { nodejs() }, linuxX64()).forEach {
it.compilations.getByName("main").defaultSourceSet.dependsOn(commonMain2)
}
}
}
}
@Test
fun customCompilations() {
checkDiagnostics("customCompilations") {
// Diagnostic should be reported only for JVM target.
// Because of ambiguity between commonIntegrationTest and jvmIntegrationTest2.
kotlin {
val commonIntegrationTest = sourceSets.create("commonIntegrationTest")
val jvmIntegrationTest2 = sourceSets.create("jvmIntegrationTest2")
jvm {
val integrationTest = compilations.create("integrationTest")
integrationTest.defaultSourceSet.dependsOn(commonIntegrationTest)
integrationTest.defaultSourceSet.dependsOn(jvmIntegrationTest2)
}
linuxX64 {
compilations.create("integrationTest").defaultSourceSet.dependsOn(commonIntegrationTest)
}
js { nodejs() }
}
}
}
@Test
fun androidProjectWithMultipleVariants() {
val project = buildProjectWithMPP {
androidApplication {
compileSdk = 33
flavorDimensions.add("country")
productFlavors {
create("arstozka") { it.dimension = "country" }
create("kolechia") { it.dimension = "country" }
}
}
kotlin {
linuxX64()
androidTarget {
}
}
}
project.evaluate()
project.assertNoDiagnostics()
}
}
@@ -15,12 +15,13 @@ import java.nio.file.Path
import kotlin.test.assertTrue
import kotlin.test.fail
internal fun checkDiagnosticsWithMppProject(projectName: String, projectConfiguration: Project.() -> Unit) {
internal fun checkDiagnosticsWithMppProject(expectedDiagnosticsFile: String, projectConfiguration: Project.() -> Unit) {
val projectName = File(expectedDiagnosticsFile).name
val project = buildProjectWithMPP(projectBuilder = { withName(projectName) })
project.setMultiplatformAndroidSourceSetLayoutVersion(2)
project.projectConfiguration()
project.evaluate()
project.checkDiagnostics(projectName)
project.checkDiagnostics(expectedDiagnosticsFile)
}
internal fun ToolingDiagnostic.equals(that: ToolingDiagnostic, ignoreThrowable: Boolean) = if (ignoreThrowable) {
@@ -0,0 +1,20 @@
[KotlinDefaultHierarchyFallbackDependsOnUsageDetected | WARNING] The Default Kotlin Hierarchy Template was not applied to 'root project 'customCompilations'':
Explicit .dependsOn() edges were configured for the following source sets:
[jvmIntegrationTest, linuxX64IntegrationTest]
Consider removing dependsOn-calls or disabling the default template by adding
'kotlin.mpp.applyDefaultHierarchyTemplate=false'
to your gradle.properties
Learn more about hierarchy templates: https://kotl.in/hierarchy-template
----
[MultipleSourceSetRootsInCompilation | WARNING] Kotlin Source Sets: 'commonIntegrationTest', 'jvmIntegrationTest2'
are included to 'integrationTest' compilation of 'jvm' target.
However, they have no common source set root between them.
Please remove these kotlin source sets and include their source directories to the compilation's default source set.
kotlin.sourceSets["jvmIntegrationTest"].kotlin.srcDir() // <-- pass sources directories here
Or, if the solution above is not applicable, specify `dependsOn` edges between these source sets so that there are no multiple roots.
See https://kotl.in/connecting-source-sets for more details.
@@ -0,0 +1,87 @@
[KotlinDefaultHierarchyFallbackDependsOnUsageDetected | WARNING] The Default Kotlin Hierarchy Template was not applied to 'root project 'extraSourceSetInEachTarget'':
Explicit .dependsOn() edges were configured for the following source sets:
[jsMain, jsTest, jvmMain, jvmTest, linuxX64Main, linuxX64Test]
Consider removing dependsOn-calls or disabling the default template by adding
'kotlin.mpp.applyDefaultHierarchyTemplate=false'
to your gradle.properties
Learn more about hierarchy templates: https://kotl.in/hierarchy-template
----
[MultipleSourceSetRootsInCompilation | WARNING] Kotlin Source Set 'jsExtraMain' is included to 'main' compilation of 'js' target,
but it doesn't depend on 'commonMain'.
Please remove 'jsExtraMain' and include its sources to the compilation's default source set:
kotlin.sourceSets["jsMain"].kotlin.srcDir() // <-- pass sources directory of 'jsExtraMain'
Or provide explicit dependency if the solution above is not applicable
kotlin.sourceSets["jsExtraMain"].dependsOn(commonMain)
See https://kotl.in/connecting-source-sets for more details.
----
[MultipleSourceSetRootsInCompilation | WARNING] Kotlin Source Set 'jsExtraTest' is included to 'test' compilation of 'js' target,
but it doesn't depend on 'commonTest'.
Please remove 'jsExtraTest' and include its sources to the compilation's default source set:
kotlin.sourceSets["jsTest"].kotlin.srcDir() // <-- pass sources directory of 'jsExtraTest'
Or provide explicit dependency if the solution above is not applicable
kotlin.sourceSets["jsExtraTest"].dependsOn(commonTest)
See https://kotl.in/connecting-source-sets for more details.
----
[MultipleSourceSetRootsInCompilation | WARNING] Kotlin Source Set 'jvmExtraMain' is included to 'main' compilation of 'jvm' target,
but it doesn't depend on 'commonMain'.
Please remove 'jvmExtraMain' and include its sources to the compilation's default source set:
kotlin.sourceSets["jvmMain"].kotlin.srcDir() // <-- pass sources directory of 'jvmExtraMain'
Or provide explicit dependency if the solution above is not applicable
kotlin.sourceSets["jvmExtraMain"].dependsOn(commonMain)
See https://kotl.in/connecting-source-sets for more details.
----
[MultipleSourceSetRootsInCompilation | WARNING] Kotlin Source Set 'jvmExtraTest' is included to 'test' compilation of 'jvm' target,
but it doesn't depend on 'commonTest'.
Please remove 'jvmExtraTest' and include its sources to the compilation's default source set:
kotlin.sourceSets["jvmTest"].kotlin.srcDir() // <-- pass sources directory of 'jvmExtraTest'
Or provide explicit dependency if the solution above is not applicable
kotlin.sourceSets["jvmExtraTest"].dependsOn(commonTest)
See https://kotl.in/connecting-source-sets for more details.
----
[MultipleSourceSetRootsInCompilation | WARNING] Kotlin Source Set 'linuxX64ExtraMain' is included to 'main' compilation of 'linuxX64' target,
but it doesn't depend on 'commonMain'.
Please remove 'linuxX64ExtraMain' and include its sources to the compilation's default source set:
kotlin.sourceSets["linuxX64Main"].kotlin.srcDir() // <-- pass sources directory of 'linuxX64ExtraMain'
Or provide explicit dependency if the solution above is not applicable
kotlin.sourceSets["linuxX64ExtraMain"].dependsOn(commonMain)
See https://kotl.in/connecting-source-sets for more details.
----
[MultipleSourceSetRootsInCompilation | WARNING] Kotlin Source Set 'linuxX64ExtraTest' is included to 'test' compilation of 'linuxX64' target,
but it doesn't depend on 'commonTest'.
Please remove 'linuxX64ExtraTest' and include its sources to the compilation's default source set:
kotlin.sourceSets["linuxX64Test"].kotlin.srcDir() // <-- pass sources directory of 'linuxX64ExtraTest'
Or provide explicit dependency if the solution above is not applicable
kotlin.sourceSets["linuxX64ExtraTest"].dependsOn(commonTest)
See https://kotl.in/connecting-source-sets for more details.
@@ -0,0 +1,23 @@
[KotlinDefaultHierarchyFallbackDependsOnUsageDetected | WARNING] The Default Kotlin Hierarchy Template was not applied to 'root project 'secondCommonMain'':
Explicit .dependsOn() edges were configured for the following source sets:
[jsMain, jvmMain, linuxX64Main]
Consider removing dependsOn-calls or disabling the default template by adding
'kotlin.mpp.applyDefaultHierarchyTemplate=false'
to your gradle.properties
Learn more about hierarchy templates: https://kotl.in/hierarchy-template
----
[MultipleSourceSetRootsInCompilation | WARNING] Kotlin Source Set 'commonMain2' is included in compilations of Kotlin Targets: 'jvm', 'js', 'linuxX64'
but it doesn't depend on 'commonMain'
Please remove 'commonMain2' and include its sources to one of the default source set: https://kotl.in/hierarchy-template
For example:
kotlin.sourceSets.commonMain.kotlin.srcDir() // <-- pass here sources directory
Or add explicit dependency if the solution above is not applicable:
kotlin.sourceSets["commonMain2"].dependsOn(commonMain)
See https://kotl.in/connecting-source-sets for more details.
@@ -1,3 +1,59 @@
[CommonMainOrTestWithDependsOnDiagnostic | ERROR] commonMain can't declare dependsOn on other source sets
----
[CommonMainOrTestWithDependsOnDiagnostic | ERROR] commonTest can't declare dependsOn on other source sets
----
[MultipleSourceSetRootsInCompilation | WARNING] Kotlin Source Set 'myCustomCommonMain' is included in compilations of Kotlin Targets: 'jvm', 'linuxX64'
but it doesn't depend on 'commonMain'
Please remove 'myCustomCommonMain' and include its sources to one of the default source set: https://kotl.in/hierarchy-template
For example:
kotlin.sourceSets.commonMain.kotlin.srcDir() // <-- pass here sources directory
Or add explicit dependency if the solution above is not applicable:
kotlin.sourceSets["myCustomCommonMain"].dependsOn(commonMain)
See https://kotl.in/connecting-source-sets for more details.
----
[MultipleSourceSetRootsInCompilation | WARNING] Kotlin Source Set 'myCustomCommonMain2' is included in compilations of Kotlin Targets: 'jvm', 'linuxX64'
but it doesn't depend on 'commonMain'
Please remove 'myCustomCommonMain2' and include its sources to one of the default source set: https://kotl.in/hierarchy-template
For example:
kotlin.sourceSets.commonMain.kotlin.srcDir() // <-- pass here sources directory
Or add explicit dependency if the solution above is not applicable:
kotlin.sourceSets["myCustomCommonMain2"].dependsOn(commonMain)
See https://kotl.in/connecting-source-sets for more details.
----
[MultipleSourceSetRootsInCompilation | WARNING] Kotlin Source Set 'myCustomCommonTest' is included in compilations of Kotlin Targets: 'jvm', 'linuxX64'
but it doesn't depend on 'commonTest'
Please remove 'myCustomCommonTest' and include its sources to one of the default source set: https://kotl.in/hierarchy-template
For example:
kotlin.sourceSets.commonMain.kotlin.srcDir() // <-- pass here sources directory
Or add explicit dependency if the solution above is not applicable:
kotlin.sourceSets["myCustomCommonTest"].dependsOn(commonTest)
See https://kotl.in/connecting-source-sets for more details.
----
[MultipleSourceSetRootsInCompilation | WARNING] Kotlin Source Set 'myCustomCommonTest2' is included in compilations of Kotlin Targets: 'jvm', 'linuxX64'
but it doesn't depend on 'commonTest'
Please remove 'myCustomCommonTest2' and include its sources to one of the default source set: https://kotl.in/hierarchy-template
For example:
kotlin.sourceSets.commonMain.kotlin.srcDir() // <-- pass here sources directory
Or add explicit dependency if the solution above is not applicable:
kotlin.sourceSets["myCustomCommonTest2"].dependsOn(commonTest)
See https://kotl.in/connecting-source-sets for more details.
@@ -1,4 +1,15 @@
[KotlinCompilationSourceDeprecation | WARNING] `KotlinCompilation.source(KotlinSourceSet)` method is deprecated
and will be removed in upcoming Kotlin releases.
See https://kotl.in/compilation-source-deprecation for details.
See https://kotl.in/compilation-source-deprecation for details.
----
[MultipleSourceSetRootsInCompilation | WARNING] Kotlin Source Sets: 'jvmCustom', 'customMain'
are included to 'custom' compilation of 'jvm' target.
However, they have no common source set root between them.
Please remove these kotlin source sets and include their source directories to the compilation's default source set.
kotlin.sourceSets["jvmCustom"].kotlin.srcDir() // <-- pass sources directories here
Or, if the solution above is not applicable, specify `dependsOn` edges between these source sets so that there are no multiple roots.
See https://kotl.in/connecting-source-sets for more details.