diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/KotlinGradleProjectResolverExtension.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/KotlinGradleProjectResolverExtension.kt index b5dbac9a30c..042a25f22d9 100644 --- a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/KotlinGradleProjectResolverExtension.kt +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/KotlinGradleProjectResolverExtension.kt @@ -24,16 +24,20 @@ import com.intellij.openapi.externalSystem.model.project.ProjectData import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil import com.intellij.openapi.roots.DependencyScope import com.intellij.openapi.util.Key +import com.intellij.openapi.util.io.FileUtil import org.gradle.tooling.model.idea.IdeaModule +import org.gradle.tooling.model.idea.IdeaModuleDependency import org.jetbrains.kotlin.gradle.CompilerArgumentsBySourceSet import org.jetbrains.kotlin.gradle.KotlinGradleModel import org.jetbrains.kotlin.gradle.KotlinGradleModelBuilder import org.jetbrains.kotlin.psi.NotNullableUserDataProperty import org.jetbrains.kotlin.psi.UserDataProperty -import org.jetbrains.plugins.gradle.model.ExternalProject +import org.jetbrains.plugins.gradle.model.ExternalProjectDependency +import org.jetbrains.plugins.gradle.model.FileCollectionDependency import org.jetbrains.plugins.gradle.model.data.GradleSourceSetData import org.jetbrains.plugins.gradle.service.project.AbstractProjectResolverExtension -import org.jetbrains.plugins.gradle.service.project.GradleProjectResolverUtil.getModuleId +import org.jetbrains.plugins.gradle.service.project.GradleProjectResolver +import java.util.* var DataNode.isResolved by NotNullableUserDataProperty(Key.create("IS_RESOLVED"), false) @@ -60,10 +64,103 @@ class KotlinGradleProjectResolverExtension : AbstractProjectResolverExtension() override fun populateModuleDependencies(gradleModule: IdeaModule, ideModule: DataNode, ideProject: DataNode) { + val outputToSourceSet = ideProject.getUserData(GradleProjectResolver.MODULES_OUTPUTS) + val sourceSetByName = ideProject.getUserData(GradleProjectResolver.RESOLVED_SOURCE_SETS) + val gradleModel = resolverCtx.getExtraProject(gradleModule, KotlinGradleModel::class.java) ?: return super.populateModuleDependencies(gradleModule, ideModule, ideProject) - importTransitiveCommonDependencies(gradleModel, ideProject, gradleModule, ideModule) + val gradleIdeaProject = gradleModule.project + + fun DataNode.getGradleModule() = + if (this != ideModule) gradleIdeaProject.modules.firstOrNull { it.gradleProject.path == data.id } else gradleModule + + fun DataNode.getDependencies(): Set> { + if (resolverCtx.isResolveModulePerSourceSet) { + if (sourceSetByName == null) return emptySet() + val externalSourceSet = sourceSetByName[data.id]?.second ?: return emptySet() + return externalSourceSet.dependencies.mapNotNullTo(LinkedHashSet()) { dependency -> + when (dependency) { + is ExternalProjectDependency -> { + val targetModuleNode = ExternalSystemApiUtil.findFirstRecursively(ideProject) { + (it.data as? ModuleData)?.id == dependency.projectPath + } as DataNode? ?: return@mapNotNullTo null + ExternalSystemApiUtil.findAll(targetModuleNode, GradleSourceSetData.KEY).firstOrNull { it.sourceSetName == "main" } + } + is FileCollectionDependency -> { + dependency.files + .mapTo(HashSet()) { + val path = FileUtil.toSystemIndependentName(it.path) + val targetSourceSetId = outputToSourceSet?.get(path)?.first ?: return@mapTo null + sourceSetByName[targetSourceSetId]?.first + } + .singleOrNull() + } + else -> null + } + } + } + + return getGradleModule()?.dependencies?.mapNotNullTo(LinkedHashSet()) { + val targetModuleName = (it as? IdeaModuleDependency)?.targetModuleName ?: return@mapNotNullTo null + val targetGradleModule = gradleIdeaProject.modules.firstOrNull { it.name == targetModuleName } ?: return@mapNotNullTo null + ExternalSystemApiUtil.findFirstRecursively(ideProject) { + (it.data as? ModuleData)?.id == targetGradleModule.gradleProject.path + } as DataNode? + } ?: emptySet() + } + + fun addTransitiveDependenciesOnImplementedModules() { + val moduleNodesToProcess = if (resolverCtx.isResolveModulePerSourceSet) { + ExternalSystemApiUtil.findAll(ideModule, GradleSourceSetData.KEY) + } + else listOf(ideModule) + + for (currentModuleNode in moduleNodesToProcess) { + val toProcess = LinkedList>() + val processed = HashSet>() + toProcess.add(currentModuleNode) + + while (toProcess.isNotEmpty()) { + val moduleNode = toProcess.pollFirst() + processed.add(moduleNode) + + val moduleNodeForGradleModel = if (resolverCtx.isResolveModulePerSourceSet) { + ExternalSystemApiUtil.findParent(moduleNode, ProjectKeys.MODULE) + } + else moduleNode + val ideaModule = if (moduleNodeForGradleModel != ideModule) { + gradleIdeaProject.modules.firstOrNull { it.gradleProject.path == moduleNodeForGradleModel?.data?.id } + } + else gradleModule + + val implementsInfo = resolverCtx.getExtraProject(ideaModule, KotlinGradleModel::class.java)?.implements + val targetModule = implementsInfo?.let { findModule(ideProject, it) } + if (targetModule != null) { + if (resolverCtx.isResolveModulePerSourceSet) { + val targetSourceSetsByName = ExternalSystemApiUtil + .findAll(targetModule, GradleSourceSetData.KEY) + .associateBy { it.sourceSetName } + val targetMainSourceSet = targetSourceSetsByName["main"] ?: targetModule + val targetSourceSet = targetSourceSetsByName[currentModuleNode.sourceSetName] + if (targetSourceSet != null) { + addDependency(currentModuleNode, targetSourceSet) + } + if (currentModuleNode.sourceSetName == "test" && targetMainSourceSet != targetSourceSet) { + addDependency(currentModuleNode, targetMainSourceSet) + } + } + else { + addDependency(currentModuleNode, targetModule) + } + } + + moduleNode.getDependencies().filterTo(toProcess) { it !in processed } + } + } + } + + addTransitiveDependenciesOnImplementedModules() ideModule.isResolved = true ideModule.hasKotlinPlugin = gradleModel.hasKotlinPlugin @@ -75,17 +172,8 @@ class KotlinGradleProjectResolverExtension : AbstractProjectResolverExtension() super.populateModuleDependencies(gradleModule, ideModule, ideProject) } - private fun importTransitiveCommonDependencies(gradleModel: KotlinGradleModel, ideProject: DataNode, gradleModule: IdeaModule, ideModule: DataNode) { - gradleModel.transitiveCommonDependencies.forEach { implementsModuleId -> - val targetModule = findModule(ideProject, implementsModuleId) ?: return - if (resolverCtx.isResolveModulePerSourceSet) { - populateSourceSetDependencies(gradleModule, ideModule, targetModule) - } - else { - addDependency(ideModule, targetModule) - } - } - } + private val DataNode.sourceSetName + get() = (data as? GradleSourceSetData)?.id?.substringAfterLast(':') private fun addDependency(ideModule: DataNode, targetModule: DataNode) { val moduleDependencyData = ModuleDependencyData(ideModule.data, targetModule.data) @@ -97,23 +185,4 @@ class KotlinGradleProjectResolverExtension : AbstractProjectResolverExtension() private fun findModule(ideProject: DataNode, moduleId: String): DataNode? { return ideProject.children.find { (it.data as? ModuleData)?.id == moduleId } as DataNode? } - - private fun populateSourceSetDependencies(gradleModule: IdeaModule, - fromModule: DataNode, - targetModule: DataNode) { - val fromSourceSets = ExternalSystemApiUtil.findAll(fromModule, GradleSourceSetData.KEY) - .associateBy { it.data.id } - val targetSourceSets = ExternalSystemApiUtil.findAll(targetModule, GradleSourceSetData.KEY) - .associateBy { it.data.id.substringAfterLast(':') } - - val externalProject = resolverCtx.getExtraProject(gradleModule, ExternalProject::class.java) ?: return - for (sourceSet in externalProject.sourceSets.values) { - if (sourceSet == null || sourceSet.sources.isEmpty()) continue - - val moduleId = getModuleId(externalProject, sourceSet) - val fromModuleDataNode = (if (fromSourceSets.isEmpty()) fromModule else fromSourceSets[moduleId]) ?: continue - val targetModuleDataNode = (if (targetSourceSets.isEmpty()) targetModule else targetSourceSets[sourceSet.name]) ?: continue - addDependency(fromModuleDataNode, targetModuleDataNode) - } - } } diff --git a/idea/idea-gradle/tests/org/jetbrains/kotlin/gradle/MultiplatformProjectImportingTest.kt b/idea/idea-gradle/tests/org/jetbrains/kotlin/gradle/MultiplatformProjectImportingTest.kt index 1a84b62d6a9..13f6658d3aa 100644 --- a/idea/idea-gradle/tests/org/jetbrains/kotlin/gradle/MultiplatformProjectImportingTest.kt +++ b/idea/idea-gradle/tests/org/jetbrains/kotlin/gradle/MultiplatformProjectImportingTest.kt @@ -250,4 +250,93 @@ class MultiplatformProjectImportingTest : GradleImportingTestCase() { assertModuleModuleDepScope("jvm-app_test", "common-lib1_test", DependencyScope.COMPILE) assertModuleModuleDepScope("jvm-app_test", "common-lib2_test", DependencyScope.COMPILE) } + + @Test + fun testTransitiveImplement() { + createProjectSubFile( + "settings.gradle", + "include ':project1', ':project2', ':project3'" + ) + + val kotlinVersion = "1.1.51" + + createProjectSubFile("build.gradle", """ + buildscript { + repositories { + mavenCentral() + } + + dependencies { + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") + } + } + + project('project1') { + apply plugin: 'kotlin-platform-common' + + sourceSets { + custom + } + } + + project('project2') { + repositories { + mavenCentral() + } + + apply plugin: 'kotlin-platform-jvm' + + sourceSets { + custom + } + + dependencies { + implement project(':project1') + } + } + + project('project3') { + repositories { + mavenCentral() + } + + apply plugin: 'kotlin-platform-jvm' + apply plugin: 'kotlin' + + sourceSets { + custom + } + + dependencies { + compile project(':project2') + customCompile project(':project2') + testCompile(project(':project2').sourceSets.test.output) + } + } + """) + + importProject() + + assertModuleModuleDepScope("project1_test", "project1_main", DependencyScope.COMPILE) + + assertModuleModuleDepScope("project2_main", "project1_main", DependencyScope.COMPILE) + + assertModuleModuleDepScope("project2_test", "project2_main", DependencyScope.COMPILE) + assertModuleModuleDepScope("project2_test", "project1_test", DependencyScope.COMPILE) + assertModuleModuleDepScope("project2_test", "project1_main", DependencyScope.COMPILE) + + assertModuleModuleDepScope("project2_custom", "project1_custom", DependencyScope.COMPILE) + + assertModuleModuleDepScope("project3_main", "project2_main", DependencyScope.COMPILE) + assertModuleModuleDepScope("project3_main", "project1_main", DependencyScope.COMPILE) + + assertModuleModuleDepScope("project3_test", "project3_main", DependencyScope.COMPILE) + assertModuleModuleDepScope("project3_test", "project2_test", DependencyScope.COMPILE) + assertModuleModuleDepScope("project3_test", "project2_main", DependencyScope.COMPILE) + assertModuleModuleDepScope("project3_test", "project1_test", DependencyScope.COMPILE) + assertModuleModuleDepScope("project3_test", "project1_main", DependencyScope.COMPILE) + + assertModuleModuleDepScope("project3_custom", "project1_custom", DependencyScope.COMPILE) + assertModuleModuleDepScope("project3_custom", "project2_main", DependencyScope.COMPILE) + } } \ No newline at end of file diff --git a/idea/kotlin-gradle-tooling/src/KotlinGradleModelBuilder.kt b/idea/kotlin-gradle-tooling/src/KotlinGradleModelBuilder.kt index 2435adb3de9..e168d10e127 100644 --- a/idea/kotlin-gradle-tooling/src/KotlinGradleModelBuilder.kt +++ b/idea/kotlin-gradle-tooling/src/KotlinGradleModelBuilder.kt @@ -26,7 +26,6 @@ import java.io.Serializable import java.lang.Exception import java.lang.reflect.InvocationTargetException import java.util.* -import kotlin.collections.HashSet interface ArgsInfo : Serializable { val currentArguments: List @@ -48,7 +47,6 @@ interface KotlinGradleModel : Serializable { val coroutines: String? val platformPluginId: String? val implements: String? - val transitiveCommonDependencies: Set } class KotlinGradleModelImpl( @@ -56,8 +54,7 @@ class KotlinGradleModelImpl( override val compilerArgumentsBySourceSet: CompilerArgumentsBySourceSet, override val coroutines: String?, override val platformPluginId: String?, - override val implements: String?, - override val transitiveCommonDependencies: Set + override val implements: String? ) : KotlinGradleModel abstract class AbstractKotlinGradleModelBuilder : ModelBuilderService { @@ -70,7 +67,6 @@ abstract class AbstractKotlinGradleModelBuilder : ModelBuilderService { "kotlin2js" to "kotlin-platform-js" ) val kotlinPluginIds = listOf("kotlin", "kotlin2js", "kotlin-android") - val kotlinPlatformCommonPluginId = "kotlin-platform-common" val ABSTRACT_KOTLIN_COMPILE_CLASS = "org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile" fun Task.getSourceSetName(): String { @@ -91,52 +87,11 @@ class KotlinGradleModelBuilder : AbstractKotlinGradleModelBuilder() { override fun canBuild(modelName: String?): Boolean = modelName == KotlinGradleModel::class.java.name private fun getImplements(project: Project): Project? { - val implementsConfiguration = project.configurations.run { findByName("expectedBy") ?: findByName("implement") } ?: return null + val implementsConfiguration = project.configurations.run { findByName("implement") } ?: return null val implementsProjectDependency = implementsConfiguration.dependencies.filterIsInstance().firstOrNull() return implementsProjectDependency?.dependencyProject } - private val Project.isCommon get() = plugins.hasPlugin(kotlinPlatformCommonPluginId) - - private fun transitiveCommonDependencies(startingProject: Project): Set { - val toProcess = LinkedList>() - toProcess.add(startingProject to false) - val processed = HashSet() - val result = HashSet() - - fun enqueueDependencies(project: Project, isReachableViaImpl: Boolean, collectImpls: Boolean) { - val isCommonProject = project.isCommon - if (isCommonProject && collectImpls) return - - val configName = if (collectImpls) "implement" else "compile" - val compileConfiguration = project.configurations.findByName(configName) ?: return - val dependencies = compileConfiguration - .dependencies - .filterIsInstance() - .map { it.dependencyProject } - - for (dep in dependencies) { - if (dep.path in processed) continue - if (!dep.isCommon && (collectImpls || isCommonProject)) continue - toProcess.add(dep to (isReachableViaImpl || collectImpls)) - } - } - - while (toProcess.isNotEmpty()) { - val (project, isReachableViaImpl) = toProcess.pollFirst() - processed.add(project.path) - - if (isReachableViaImpl) { - result.add(project.pathOrName()) - } - - enqueueDependencies(project, isReachableViaImpl, false) - enqueueDependencies(project, isReachableViaImpl, true) - } - - return result - } - // see GradleProjectResolverUtil.getModuleId() in IDEA codebase private fun Project.pathOrName() = if (path == ":") name else path @@ -206,15 +161,13 @@ class KotlinGradleModelBuilder : AbstractKotlinGradleModelBuilder() { val platform = platformPluginId ?: pluginToPlatform.entries.singleOrNull { project.plugins.findPlugin(it.key) != null }?.value val implementedProject = getImplements(project) - val transitiveCommon = transitiveCommonDependencies(project) return KotlinGradleModelImpl( kotlinPluginId != null || platformPluginId != null, compilerArgumentsBySourceSet, getCoroutines(project), platform, - implementedProject?.pathOrName(), - transitiveCommon + implementedProject?.pathOrName() ) } }