Fix setting production-on-test flags in MPP projects

#KT-29854 Fixed
This commit is contained in:
Andrey Uskov
2019-04-11 16:09:44 +03:00
parent 1a0b7902c2
commit 364274ef90
12 changed files with 200 additions and 45 deletions
@@ -38,6 +38,7 @@ import org.jetbrains.kotlin.utils.addIfNotNull
import org.jetbrains.plugins.gradle.model.data.GradleSourceSetData
import java.io.File
import java.io.IOException
import org.jetbrains.kotlin.idea.configuration.kotlinSourceSet
class KotlinAndroidGradleMPPModuleDataService : AbstractProjectDataService<ModuleData, Void>() {
override fun getTargetDataKey() = ProjectKeys.MODULE
@@ -95,11 +96,17 @@ class KotlinAndroidGradleMPPModuleDataService : AbstractProjectDataService<Modul
for (sourceSet in activeCompilation.sourceSets) {
if (sourceSet.platform != KotlinPlatform.ANDROID) {
val sourceSetId = activeSourceSetInfo.sourceSetIdsByName[sourceSet.name] ?: continue
val sourceSetData = ExternalSystemApiUtil.findFirstRecursively(projectNode) {
val sourceSetNode = ExternalSystemApiUtil.findFirstRecursively(projectNode) {
(it.data as? ModuleData)?.id == sourceSetId
}?.data as? ModuleData ?: continue
} as? DataNode<out ModuleData>? ?: continue
val sourceSetData = sourceSetNode.data as? ModuleData ?: continue
val sourceSetModule = modelsProvider.findIdeModule(sourceSetData) ?: continue
addModuleDependencyIfNeeded(rootModel, sourceSetModule, activeSourceSetInfo.isTestModule)
addModuleDependencyIfNeeded(
rootModel,
sourceSetModule,
activeSourceSetInfo.isTestModule,
sourceSetNode.kotlinSourceSet?.isTestModule ?: false
)
}
}
}
@@ -169,7 +176,8 @@ class KotlinAndroidGradleMPPModuleDataService : AbstractProjectDataService<Modul
}
val commonSourceSetName = KotlinSourceSet.commonName(testScope)
val isAndroidModule = getAndroidModuleModel(moduleNode) != null
SmartList<DataNode<GradleSourceSetData>>()
val gradleSourceSetDataNodes = SmartList<DataNode<GradleSourceSetData>>()
.apply {
addIfNotNull(
(if (isAndroidModule) relevantNodes.firstByPlatformOrNull(KotlinPlatform.ANDROID) else null)
@@ -181,14 +189,19 @@ class KotlinAndroidGradleMPPModuleDataService : AbstractProjectDataService<Modul
}
)
}
.mapNotNull { modelsProvider.findIdeModule(it.data) }
.forEach {
addModuleDependencyIfNeeded(rootModel, it, testScope)
val dependeeRootModel = modelsProvider.getModifiableRootModel(it)
dependeeRootModel.getModuleDependencies(testScope).forEach { transitiveDependee ->
addModuleDependencyIfNeeded(rootModel, transitiveDependee, testScope)
}
val testKotlinModules =
gradleSourceSetDataNodes.filter { it.kotlinSourceSet?.isTestModule ?: false }.mapNotNull { modelsProvider.findIdeModule(it.data) }
.toSet()
gradleSourceSetDataNodes.forEach { node ->
val module = modelsProvider.findIdeModule(node.data) ?: return
addModuleDependencyIfNeeded(rootModel, module, testScope, node.kotlinSourceSet?.isTestModule ?: false)
val dependeeRootModel = modelsProvider.getModifiableRootModel(module)
dependeeRootModel.getModuleDependencies(testScope).forEach { transitiveDependee ->
addModuleDependencyIfNeeded(rootModel, transitiveDependee, testScope, testKotlinModules.contains(transitiveDependee))
}
}
}
private fun getAndroidModuleModel(moduleNode: DataNode<ModuleData>) =
@@ -38,6 +38,7 @@ import org.jetbrains.kotlin.utils.addIfNotNull
import org.jetbrains.plugins.gradle.model.data.GradleSourceSetData
import java.io.File
import java.io.IOException
import org.jetbrains.kotlin.idea.configuration.kotlinSourceSet
class KotlinAndroidGradleMPPModuleDataService : AbstractProjectDataService<ModuleData, Void>() {
override fun getTargetDataKey() = ProjectKeys.MODULE
@@ -95,11 +96,17 @@ class KotlinAndroidGradleMPPModuleDataService : AbstractProjectDataService<Modul
for (sourceSet in activeCompilation.sourceSets) {
if (sourceSet.platform != KotlinPlatform.ANDROID) {
val sourceSetId = activeSourceSetInfo.sourceSetIdsByName[sourceSet.name] ?: continue
val sourceSetData = ExternalSystemApiUtil.findFirstRecursively(projectNode) {
val sourceSetNode = ExternalSystemApiUtil.findFirstRecursively(projectNode) {
(it.data as? ModuleData)?.id == sourceSetId
}?.data as? ModuleData ?: continue
} as? DataNode<out ModuleData>? ?: continue
val sourceSetData = sourceSetNode.data as? ModuleData ?: continue
val sourceSetModule = modelsProvider.findIdeModule(sourceSetData) ?: continue
addModuleDependencyIfNeeded(rootModel, sourceSetModule, activeSourceSetInfo.isTestModule)
addModuleDependencyIfNeeded(
rootModel,
sourceSetModule,
activeSourceSetInfo.isTestModule,
sourceSetNode.kotlinSourceSet?.isTestModule ?: false
)
}
}
}
@@ -169,7 +176,8 @@ class KotlinAndroidGradleMPPModuleDataService : AbstractProjectDataService<Modul
}
val commonSourceSetName = KotlinSourceSet.commonName(testScope)
val isAndroidModule = getAndroidModuleModel(moduleNode) != null
SmartList<DataNode<GradleSourceSetData>>()
val gradleSourceSetDataNodes = SmartList<DataNode<GradleSourceSetData>>()
.apply {
addIfNotNull(
(if (isAndroidModule) relevantNodes.firstByPlatformOrNull(KotlinPlatform.ANDROID) else null)
@@ -181,14 +189,19 @@ class KotlinAndroidGradleMPPModuleDataService : AbstractProjectDataService<Modul
}
)
}
.mapNotNull { modelsProvider.findIdeModule(it.data) }
.forEach {
addModuleDependencyIfNeeded(rootModel, it, testScope)
val dependeeRootModel = modelsProvider.getModifiableRootModel(it)
dependeeRootModel.getModuleDependencies(testScope).forEach { transitiveDependee ->
addModuleDependencyIfNeeded(rootModel, transitiveDependee, testScope)
}
val testKotlinModules =
gradleSourceSetDataNodes.filter { it.kotlinSourceSet?.isTestModule ?: false }.mapNotNull { modelsProvider.findIdeModule(it.data) }
.toSet()
gradleSourceSetDataNodes.forEach { node ->
val module = modelsProvider.findIdeModule(node.data) ?: return
addModuleDependencyIfNeeded(rootModel, module, testScope, node.kotlinSourceSet?.isTestModule ?: false)
val dependeeRootModel = modelsProvider.getModifiableRootModel(module)
dependeeRootModel.getModuleDependencies(testScope).forEach { transitiveDependee ->
addModuleDependencyIfNeeded(rootModel, transitiveDependee, testScope, testKotlinModules.contains(transitiveDependee))
}
}
}
private fun getAndroidModuleModel(moduleNode: DataNode<ModuleData>) =
@@ -38,6 +38,7 @@ import org.jetbrains.kotlin.utils.addIfNotNull
import org.jetbrains.plugins.gradle.model.data.GradleSourceSetData
import java.io.File
import java.io.IOException
import org.jetbrains.kotlin.idea.configuration.kotlinSourceSet
class KotlinAndroidGradleMPPModuleDataService : AbstractProjectDataService<ModuleData, Void>() {
override fun getTargetDataKey() = ProjectKeys.MODULE
@@ -95,11 +96,17 @@ class KotlinAndroidGradleMPPModuleDataService : AbstractProjectDataService<Modul
for (sourceSet in activeCompilation.sourceSets) {
if (sourceSet.platform != KotlinPlatform.ANDROID) {
val sourceSetId = activeSourceSetInfo.sourceSetIdsByName[sourceSet.name] ?: continue
val sourceSetData = ExternalSystemApiUtil.findFirstRecursively(projectNode) {
val sourceSetNode = ExternalSystemApiUtil.findFirstRecursively(projectNode) {
(it.data as? ModuleData)?.id == sourceSetId
}?.data as? ModuleData ?: continue
} as? DataNode<out ModuleData>? ?: continue
val sourceSetData = sourceSetNode.data as? ModuleData ?: continue
val sourceSetModule = modelsProvider.findIdeModule(sourceSetData) ?: continue
addModuleDependencyIfNeeded(rootModel, sourceSetModule, activeSourceSetInfo.isTestModule)
addModuleDependencyIfNeeded(
rootModel,
sourceSetModule,
activeSourceSetInfo.isTestModule,
sourceSetNode.kotlinSourceSet?.isTestModule ?: false
)
}
}
}
@@ -169,7 +176,8 @@ class KotlinAndroidGradleMPPModuleDataService : AbstractProjectDataService<Modul
}
val commonSourceSetName = KotlinSourceSet.commonName(testScope)
val isAndroidModule = getAndroidModuleModel(moduleNode) != null
SmartList<DataNode<GradleSourceSetData>>()
val gradleSourceSetDataNodes = SmartList<DataNode<GradleSourceSetData>>()
.apply {
addIfNotNull(
(if (isAndroidModule) relevantNodes.firstByPlatformOrNull(KotlinPlatform.ANDROID) else null)
@@ -181,14 +189,19 @@ class KotlinAndroidGradleMPPModuleDataService : AbstractProjectDataService<Modul
}
)
}
.mapNotNull { modelsProvider.findIdeModule(it.data) }
.forEach {
addModuleDependencyIfNeeded(rootModel, it, testScope)
val dependeeRootModel = modelsProvider.getModifiableRootModel(it)
dependeeRootModel.getModuleDependencies(testScope).forEach { transitiveDependee ->
addModuleDependencyIfNeeded(rootModel, transitiveDependee, testScope)
}
val testKotlinModules =
gradleSourceSetDataNodes.filter { it.kotlinSourceSet?.isTestModule ?: false }.mapNotNull { modelsProvider.findIdeModule(it.data) }
.toSet()
gradleSourceSetDataNodes.forEach { node ->
val module = modelsProvider.findIdeModule(node.data) ?: return
addModuleDependencyIfNeeded(rootModel, module, testScope, node.kotlinSourceSet?.isTestModule ?: false)
val dependeeRootModel = modelsProvider.getModifiableRootModel(module)
dependeeRootModel.getModuleDependencies(testScope).forEach { transitiveDependee ->
addModuleDependencyIfNeeded(rootModel, transitiveDependee, testScope, testKotlinModules.contains(transitiveDependee))
}
}
}
private fun getAndroidModuleModel(moduleNode: DataNode<ModuleData>) =
@@ -28,6 +28,8 @@ class KotlinJavaMPPSourceSetDataService : AbstractProjectDataService<GradleSourc
project: Project,
modelsProvider: IdeModifiableModelsProvider
) {
val testKotlinModules =
toImport.filter { it.kotlinSourceSet?.isTestModule ?: false }.map { modelsProvider.findIdeModule(it.data) }.toSet()
val projectNode = toImport.firstOrNull()?.let { ExternalSystemApiUtil.findParent(it, ProjectKeys.PROJECT) } ?: return
val targetsByUrl = ExternalSystemApiUtil
.findAllRecursively(projectNode, KotlinTargetData.KEY)
@@ -53,9 +55,19 @@ class KotlinJavaMPPSourceSetDataService : AbstractProjectDataService<GradleSourc
val compilationInfo = compilationNode.kotlinSourceSet ?: continue
if (!isTestSourceSet && compilationInfo.isTestModule) continue
val compilationRootModel = modelsProvider.getModifiableRootModel(compilationModule)
addModuleDependencyIfNeeded(rootModel, compilationModule, isTestSourceSet)
addModuleDependencyIfNeeded(
rootModel,
compilationModule,
isTestSourceSet,
compilationNode.kotlinSourceSet?.isTestModule ?: false
)
compilationRootModel.getModuleDependencies(isTestSourceSet).forEach { transitiveDependee ->
addModuleDependencyIfNeeded(rootModel, transitiveDependee, isTestSourceSet)
addModuleDependencyIfNeeded(
rootModel,
transitiveDependee,
isTestSourceSet,
testKotlinModules.contains(transitiveDependee)
)
}
}
rootModel.removeOrderEntry(libraryEntry)
@@ -31,7 +31,6 @@ import org.jetbrains.kotlin.cli.common.arguments.parseCommandLineArguments
import org.jetbrains.kotlin.config.LanguageFeature
import org.jetbrains.kotlin.gradle.*
import org.jetbrains.kotlin.idea.configuration.GradlePropertiesFileFacade.Companion.KOTLIN_NOT_IMPORTED_COMMON_SOURCE_SETS_SETTING
import org.jetbrains.kotlin.idea.configuration.KotlinMPPGradleProjectResolver.Companion.isNotSupported
import org.jetbrains.kotlin.idea.platform.IdePlatformKindTooling
import org.jetbrains.plugins.gradle.model.*
import org.jetbrains.plugins.gradle.model.data.BuildScriptClasspathData
@@ -361,7 +360,7 @@ open class KotlinMPPGradleProjectResolver : AbstractProjectResolverExtension() {
for (sourceSet in compilation.sourceSets) {
if (sourceSet.fullName() == compilation.fullName()) continue
val targetDataNode = getSiblingKotlinModuleData(sourceSet, gradleModule, ideModule, resolverCtx) ?: continue
addDependency(dataNode, targetDataNode)
addDependency(dataNode, targetDataNode, sourceSet.isTestModule)
}
}
}
@@ -421,7 +420,7 @@ open class KotlinMPPGradleProjectResolver : AbstractProjectResolverExtension() {
if (sourceSet.platform == KotlinPlatform.ANDROID) continue
for (dependeeSourceSet in dependeeSourceSets) {
val toDataNode = getSiblingKotlinModuleData(dependeeSourceSet, gradleModule, ideModule, resolverCtx) ?: continue
addDependency(fromDataNode, toDataNode)
addDependency(fromDataNode, toDataNode, dependeeSourceSet.isTestModule)
}
if (processedModuleIds.add(getKotlinModuleId(gradleModule, sourceSet, resolverCtx))) {
val mergedDependencies = LinkedHashSet<KotlinDependency>().apply {
@@ -454,12 +453,13 @@ open class KotlinMPPGradleProjectResolver : AbstractProjectResolverExtension() {
.filterNotNull()
}
private fun addDependency(fromModule: DataNode<*>, toModule: DataNode<*>) {
private fun addDependency(fromModule: DataNode<*>, toModule: DataNode<*>, dependOnTestModule: Boolean) {
val fromData = fromModule.data as? ModuleData ?: return
val toData = toModule.data as? ModuleData ?: return
val moduleDependencyData = ModuleDependencyData(fromData, toData).also {
it.scope = DependencyScope.COMPILE
it.isExported = false
it.isProductionOnTestDependency = dependOnTestModule
}
fromModule.createChild(ProjectKeys.MODULE_DEPENDENCY, moduleDependencyData)
}
@@ -567,6 +567,9 @@ open class KotlinMPPGradleProjectResolver : AbstractProjectResolverExtension() {
sourceSet.name = compilation.fullName()
sourceSet.targetCompatibility = compilationData.targetCompatibility
sourceSet.dependencies += compilation.dependencies
//TODO after applying patch to IDEA core uncomment the following line:
// sourceSet.isTest = compilation.sourceSets.filter { isTestModule }.isNotEmpty()
// It will allow to get rid of hacks with guessing module type in DataServices and obtain properly set productionOnTest flags
val sourcesWithTypes = SmartList<kotlin.Pair<IExternalSystemSourceType, ExternalSourceDirectorySet>>()
if (effectiveClassesDir != null) {
sourcesWithTypes += compilation.sourceType to DefaultExternalSourceDirectorySet().also { dirSet ->
@@ -8,20 +8,58 @@ package org.jetbrains.kotlin.idea
import com.intellij.openapi.module.Module
import com.intellij.openapi.roots.DependencyScope
import com.intellij.openapi.roots.ModifiableRootModel
import com.intellij.openapi.roots.impl.ModuleOrderEntryImpl
import com.intellij.openapi.diagnostic.Logger
/**
* Returns the dependency scope which has flags specific to the both provided scopes
*/
fun getScopeContainingBoth(scope1: DependencyScope, scope2: DependencyScope?): DependencyScope {
if (scope2 == null) {
return scope1
}
val compile = scope1.isForProductionCompile || scope2.isForProductionCompile
val runtime = scope1.isForProductionRuntime || scope2.isForProductionRuntime
val testCompile = scope1.isForTestCompile || scope2.isForTestCompile
val testRuntime = scope1.isForTestRuntime || scope2.isForTestRuntime
//we suppose that scope1 or scope 2 is at least Test
val result = if (compile || runtime) DependencyScope.COMPILE else DependencyScope.TEST
// validate that result is valid. We could not express several cases (e.g. RUNTIME + TEST), we will log this case
if (result.isForProductionCompile != compile ||
result.isForProductionRuntime != runtime ||
result.isForTestCompile != testCompile ||
result.isForTestRuntime != testRuntime
) {
Logger.getInstance(KotlinJavaMPPSourceSetDataService::class.java)
.warn("Could not express cross-module dependency with flags Compile=$compile Runtime=$runtime TestCompile=$testCompile TestRuntime=$testRuntime")
}
return result
}
fun addModuleDependencyIfNeeded(
rootModel: ModifiableRootModel,
dependeeModule: Module,
testScope: Boolean
testScope: Boolean,
dependOnTest: Boolean
) {
val dependencyScope = if (testScope) DependencyScope.TEST else DependencyScope.COMPILE
val existingEntry = rootModel.findModuleOrderEntry(dependeeModule)
val existingDependOnTest = (existingEntry as? ModuleOrderEntryImpl)?.isProductionOnTestDependency ?: false
val requiredScope = getScopeContainingBoth(if (testScope) DependencyScope.TEST else DependencyScope.COMPILE, existingEntry?.scope)
if (existingEntry != null) {
val existingScope = existingEntry.scope
if (existingScope == DependencyScope.COMPILE || existingScope == dependencyScope) return
if (dependencyScope == DependencyScope.COMPILE) {
if (requiredScope != existingScope || (dependOnTest && !existingDependOnTest)) {
rootModel.removeOrderEntry(existingEntry)
} else {
return
}
}
rootModel.addModuleOrderEntry(dependeeModule).also { it.scope = dependencyScope }
rootModel.addModuleOrderEntry(dependeeModule).also {
it.scope = requiredScope
(it as? ModuleOrderEntryImpl)?.isProductionOnTestDependency = dependOnTest || existingDependOnTest
}
}
@@ -536,6 +536,19 @@ class NewMultiplatformProjectImportingTest : MultiplePluginVersionGradleImportin
}
}
@Test
fun testProductionOnTestFlag() {
configureByFiles()
importProject(true)
checkProjectStructure(false, false, false ) {
module("project.javaModule.test") {
moduleDependency("project.mppModule.jvmTest", DependencyScope.TEST, true)
moduleDependency("project.mppModule.jvmMain", DependencyScope.TEST, false)
}
}
}
private fun checkProjectStructure(
exhaustiveModuleList: Boolean = true,
exhaustiveSourceSourceRootList: Boolean = true,
@@ -10,6 +10,7 @@ import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.rootManager
import com.intellij.openapi.roots.*
import com.intellij.openapi.roots.impl.ModuleOrderEntryImpl
import com.intellij.openapi.util.io.FileUtil
import org.jetbrains.jps.model.module.JpsModuleSourceRootType
import org.jetbrains.jps.util.JpsPathUtil
@@ -147,7 +148,7 @@ class ModuleInfo(
expectedDependencyNames += libraryEntry.presentableName
}
fun moduleDependency(moduleName: String, scope: DependencyScope) {
fun moduleDependency(moduleName: String, scope: DependencyScope, productionOnTest: Boolean? = null) {
val moduleEntry = rootModel.orderEntries.filterIsInstance<ModuleOrderEntry>().singleOrNull { it.moduleName == moduleName }
if (moduleEntry == null) {
val allModules = rootModel.orderEntries.filterIsInstance<ModuleOrderEntry>().map { it.moduleName }.joinToString(", ")
@@ -155,6 +156,7 @@ class ModuleInfo(
return
}
checkDependencyScope(moduleEntry, scope)
checkProductionOnTest(moduleEntry, productionOnTest)
expectedDependencyNames += moduleEntry.presentableName
}
@@ -241,6 +243,23 @@ class ModuleInfo(
)
}
}
private fun checkProductionOnTest(library: ExportableOrderEntry, productionOnTest: Boolean?) {
if (productionOnTest == null) return
val actualFlag = (library as? ModuleOrderEntryImpl)?.isProductionOnTestDependency
if (actualFlag == null) {
projectInfo.messageCollector.report(
"Module '${module.name}': Dependency '${library.presentableName}' has no productionOnTest property"
)
} else {
if (actualFlag != productionOnTest) {
projectInfo.messageCollector.report(
"Module '${module.name}': Dependency '${library.presentableName}': expected productionOnTest '$productionOnTest' but found '$actualFlag'"
)
}
}
}
}
fun checkProjectStructure(
@@ -0,0 +1,16 @@
subprojects {
buildscript {
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.21"
}
}
repositories {
mavenLocal()
mavenCentral()
}
}
@@ -0,0 +1,6 @@
apply plugin: 'kotlin'
dependencies {
testCompile project(':mppModule')
}
@@ -0,0 +1,7 @@
apply plugin: 'kotlin-multiplatform'
kotlin {
jvm()
}
println("mppModule")
@@ -0,0 +1,2 @@
include 'javaModule'
include 'mppModule'