diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/KotlinMPPGradleProjectResolver.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/KotlinMPPGradleProjectResolver.kt index 5ade030d1d5..13ac254d1a3 100644 --- a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/KotlinMPPGradleProjectResolver.kt +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/KotlinMPPGradleProjectResolver.kt @@ -7,6 +7,7 @@ package org.jetbrains.kotlin.idea.configuration import com.google.common.graph.GraphBuilder import com.google.common.graph.Graphs +import com.intellij.build.events.MessageEvent import com.intellij.ide.plugins.PluginManager import com.intellij.openapi.extensions.PluginId import com.intellij.openapi.externalSystem.model.DataNode @@ -39,10 +40,17 @@ import org.jetbrains.kotlin.config.ExternalSystemRunTask import org.jetbrains.kotlin.config.ExternalSystemTestRunTask import org.jetbrains.kotlin.config.LanguageFeature import org.jetbrains.kotlin.gradle.* +import org.jetbrains.kotlin.idea.PlatformVersion import org.jetbrains.kotlin.idea.configuration.GradlePropertiesFileFacade.Companion.KOTLIN_NOT_IMPORTED_COMMON_SOURCE_SETS_SETTING import org.jetbrains.kotlin.idea.configuration.klib.KotlinNativeLibrariesDependencySubstitutor import org.jetbrains.kotlin.idea.configuration.klib.KotlinNativeLibrariesFixer import org.jetbrains.kotlin.idea.configuration.klib.KotlinNativeLibraryNameUtil.KOTLIN_NATIVE_LIBRARY_PREFIX_PLUS_SPACE +import org.jetbrains.kotlin.idea.configuration.mpp.createPopulateModuleDependenciesContext +import org.jetbrains.kotlin.idea.configuration.mpp.getCompilations +import org.jetbrains.kotlin.idea.configuration.mpp.populateModuleDependenciesByCompilations +import org.jetbrains.kotlin.idea.configuration.mpp.populateModuleDependenciesBySourceSetVisibilityGraph +import org.jetbrains.kotlin.idea.configuration.utils.UnsafeTestSourceSetHeuristicApi +import org.jetbrains.kotlin.idea.configuration.utils.predictedProductionSourceSetName import org.jetbrains.kotlin.idea.platform.IdePlatformKindTooling import org.jetbrains.kotlin.idea.util.NotNullableCopyableDataNodeUserDataProperty import org.jetbrains.kotlin.util.removeSuffixIfPresent @@ -94,7 +102,7 @@ open class KotlinMPPGradleProjectResolver : AbstractProjectResolverExtensionComp } override fun populateModuleContentRoots(gradleModule: IdeaModule, ideModule: DataNode) { - val mppModel = resolverCtx.getExtraProject(gradleModule, KotlinMPPGradleModel::class.java) + val mppModel = resolverCtx.getMppModel(gradleModule) if (mppModel == null) { return super.populateModuleContentRoots(gradleModule, ideModule) } else { @@ -117,7 +125,9 @@ open class KotlinMPPGradleProjectResolver : AbstractProjectResolverExtensionComp val projectDataNode = ideModule.getDataNode(ProjectKeys.PROJECT)!! val moduleOutputsMap = projectDataNode.getUserData(MODULES_OUTPUTS)!! val outputDirs = HashSet() - processCompilations(gradleModule, mppModel, ideModule, resolverCtx) { dataNode, compilation -> + getCompilations(gradleModule, mppModel, ideModule, resolverCtx) + .filterNot { (_, compilation) -> delegateToAndroidPlugin(compilation) } + .forEach { (dataNode, compilation) -> var gradleOutputMap = dataNode.getUserData(GradleProjectResolver.GRADLE_OUTPUTS) if (gradleOutputMap == null) { gradleOutputMap = MultiMap.create() @@ -228,7 +238,7 @@ open class KotlinMPPGradleProjectResolver : AbstractProjectResolverExtensionComp } } - private fun Collection.modifyDependenciesOnMppModules( + internal fun Collection.modifyDependenciesOnMppModules( ideProject: DataNode, resolverCtx: ProjectResolverContext ) { @@ -289,8 +299,8 @@ open class KotlinMPPGradleProjectResolver : AbstractProjectResolverExtensionComp val path = toCanonicalPath(target.jar!!.archiveFile!!.absolutePath) val currentModules = userData[path] ?: ArrayList().apply { userData[path] = this } // Test modules should not be added. Otherwise we could get dependnecy of java.mail on jvmTest - val allSourceSets = target.compilations.filter { !it.isTestModule }.flatMap { it.allSourceSets }.toSet() - val availableViaDependsOn = allSourceSets.flatMap { it.dependsOnSourceSets }.mapNotNull { mppModel.sourceSets[it] } + val allSourceSets = target.compilations.filter { !it.isTestModule }.flatMap { it.declaredSourceSets }.toSet() + val availableViaDependsOn = allSourceSets.flatMap { it.allDependsOnSourceSets }.mapNotNull { mppModel.sourceSetsByName[it] } allSourceSets.union(availableViaDependsOn).forEach { sourceSet -> currentModules.add(getKotlinModuleId(gradleModule, sourceSet, resolverCtx)) } @@ -362,7 +372,7 @@ open class KotlinMPPGradleProjectResolver : AbstractProjectResolverExtensionComp resolverCtx ) ?: continue kotlinSourceSet.externalSystemRunTasks = - compilation.allSourceSets.firstNotNullResult { sourceSetToRunTasks[it] } ?: emptyList() + compilation.declaredSourceSets.firstNotNullResult { sourceSetToRunTasks[it] } ?: emptyList() if (compilation.platform == KotlinPlatform.JVM || compilation.platform == KotlinPlatform.ANDROID) { compilationData.targetCompatibility = (kotlinSourceSet.compilerArguments as? K2JVMCompilerArguments)?.jvmTarget @@ -374,9 +384,9 @@ open class KotlinMPPGradleProjectResolver : AbstractProjectResolverExtensionComp } } - for (sourceSet in compilation.allSourceSets) { + for (sourceSet in compilation.declaredSourceSets) { sourceSetToCompilationData.getOrPut(sourceSet.name) { LinkedHashSet() } += compilationData - for (dependentSourceSetName in sourceSet.dependsOnSourceSets) { + for (dependentSourceSetName in sourceSet.allDependsOnSourceSets) { sourceSetToCompilationData.getOrPut(dependentSourceSetName) { LinkedHashSet() } += compilationData } } @@ -394,9 +404,9 @@ open class KotlinMPPGradleProjectResolver : AbstractProjectResolverExtensionComp } val ignoreCommonSourceSets by lazy { externalProject.notImportedCommonSourceSets() } - for (sourceSet in mppModel.sourceSets.values) { + for (sourceSet in mppModel.sourceSetsByName.values) { if (delegateToAndroidPlugin(sourceSet)) continue - if (sourceSet.actualPlatforms.supports(KotlinPlatform.COMMON) && ignoreCommonSourceSets) continue + if (sourceSet.actualPlatforms.platforms.singleOrNull() == KotlinPlatform.COMMON && ignoreCommonSourceSets) continue val moduleId = getKotlinModuleId(gradleModule, sourceSet, resolverCtx) val existingSourceSetDataNode = sourceSetMap[moduleId]?.first if (existingSourceSetDataNode?.kotlinSourceSet != null) continue @@ -410,15 +420,14 @@ open class KotlinMPPGradleProjectResolver : AbstractProjectResolverExtensionComp it.group = externalProject.group it.version = externalProject.version - val name = sourceSet.name - val baseName = name.removeSuffix("Test") - if (baseName != name) { + if (sourceSet.isTestModule) { it.productionModuleId = getInternalModuleName( gradleModule, externalProject, sourceSet, resolverCtx, - baseName + "Main" + @OptIn(UnsafeTestSourceSetHeuristicApi::class) + predictedProductionSourceSetName(sourceSet.name) ) } @@ -430,7 +439,7 @@ open class KotlinMPPGradleProjectResolver : AbstractProjectResolverExtensionComp .mapNotNull { compilationData -> compilationData.targetCompatibility } .minWithOrNull(VersionComparatorUtil.COMPARATOR) - if (sourceSet.actualPlatforms.getSinglePlatform() == KotlinPlatform.NATIVE) { + if (sourceSet.actualPlatforms.singleOrNull() == KotlinPlatform.NATIVE) { it.konanTargets = compilationDataRecords .flatMap { compilationData -> compilationData.konanTargets } .toSet() @@ -438,7 +447,7 @@ open class KotlinMPPGradleProjectResolver : AbstractProjectResolverExtensionComp } } - val kotlinSourceSet = createSourceSetInfo(sourceSet, gradleModule, resolverCtx) ?: continue + val kotlinSourceSet = createSourceSetInfo(mppModel, sourceSet, gradleModule, resolverCtx) ?: continue kotlinSourceSet.externalSystemRunTasks = sourceSetToRunTasks[sourceSet] ?: emptyList() val sourceSetDataNode = @@ -495,15 +504,15 @@ open class KotlinMPPGradleProjectResolver : AbstractProjectResolverExtensionComp ) } val allRunTasks = testRunTasks + nativeMainRunTasks - compilation.allSourceSets.forEach { sourceSet -> + compilation.declaredSourceSets.forEach { sourceSet -> sourceSetToRunTasks.getOrPut(sourceSet) { LinkedHashSet() } += allRunTasks - sourceSet.dependsOnSourceSets.forEach { dependentModule -> - dependsOnReverseGraph.getOrPut(dependentModule) { LinkedHashSet() } += sourceSet + mppModel.resolveAllDependsOnSourceSets(sourceSet).forEach { dependentModule -> + dependsOnReverseGraph.getOrPut(dependentModule.name) { LinkedHashSet() } += sourceSet } } } } - mppModel.sourceSets.forEach { (sourceSetName, sourceSet) -> + mppModel.sourceSetsByName.forEach { (sourceSetName, sourceSet) -> dependsOnReverseGraph[sourceSetName]?.forEach { dependingSourceSet -> sourceSetToRunTasks.getOrPut(sourceSet) { LinkedHashSet() } += sourceSetToRunTasks[dependingSourceSet] ?: emptyList() } @@ -519,7 +528,7 @@ open class KotlinMPPGradleProjectResolver : AbstractProjectResolverExtensionComp val mppModel = resolverCtx.getMppModel(gradleModule) ?: return val sourceSetToPackagePrefix = mppModel.targets.flatMap { it.compilations } .flatMap { compilation -> - compilation.allSourceSets.map { sourceSet -> sourceSet.name to compilation.kotlinTaskProperties.packagePrefix } + compilation.declaredSourceSets.map { sourceSet -> sourceSet.name to compilation.kotlinTaskProperties.packagePrefix } } .toMap() if (resolverCtx.getExtraProject(gradleModule, ExternalProject::class.java) == null) return @@ -563,7 +572,7 @@ open class KotlinMPPGradleProjectResolver : AbstractProjectResolverExtensionComp mppModelPureKotlinSourceFolders + ideModule.pureKotlinSourceFolders } - private data class CompilationWithDependencies( + internal data class CompilationWithDependencies( val compilation: KotlinCompilation, val substitutedDependencies: List ) { @@ -581,275 +590,17 @@ open class KotlinMPPGradleProjectResolver : AbstractProjectResolverExtensionComp ideModule: DataNode, resolverCtx: ProjectResolverContext ) { - val mppModel = resolverCtx.getMppModel(gradleModule) ?: return - mppModel.dependencyMap.values.modifyDependenciesOnMppModules(ideProject, resolverCtx) - val sourceSetMap = ideProject.getUserData(GradleProjectResolver.RESOLVED_SOURCE_SETS) ?: return - val artifactsMap = ideProject.getUserData(CONFIGURATION_ARTIFACTS) ?: return - val substitutor = KotlinNativeLibrariesDependencySubstitutor(mppModel, gradleModule, resolverCtx) - val sourceSetToCompilations = mutableMapOf>() - val processedModuleIds = HashSet() - processCompilations(gradleModule, mppModel, ideModule, resolverCtx) { dataNode, compilation -> - if (processedModuleIds.add(getKotlinModuleId(gradleModule, compilation, resolverCtx))) { - val substitutedDependencies = - substitutor.substituteDependencies(compilation.dependencies.mapNotNull { mppModel.dependencyMap[it] }) - buildDependencies( - resolverCtx, - sourceSetMap, - artifactsMap, - dataNode, - preprocessDependencies(substitutedDependencies), - ideProject - ) - KotlinNativeLibrariesFixer.applyTo(dataNode, ideProject) - for (sourceSet in compilation.allSourceSets) { - (sourceSet.dependsOnSourceSets + sourceSet.name).forEach { - sourceSetToCompilations - .getOrPut(it) { mutableListOf() } - .add(CompilationWithDependencies(compilation, substitutedDependencies)) - } - if (sourceSet.fullName() == compilation.fullName()) continue - val targetDataNode = getSiblingKotlinModuleData(sourceSet, gradleModule, ideModule, resolverCtx) ?: continue - addDependency(dataNode, targetDataNode, sourceSet.isTestModule) - } - } - } - val sourceSetGraph = GraphBuilder.directed().build() - processSourceSets(gradleModule, mppModel, ideModule, resolverCtx) { dataNode, sourceSet -> - sourceSetGraph.addNode(sourceSet) - val productionSourceSet = dataNode - ?.data - ?.productionModuleId - ?.let { ideModule.findChildModuleByInternalName(it) } - ?.kotlinSourceSet - ?.kotlinModule - ?.toSourceSet(mppModel) - if (productionSourceSet != null) { - sourceSetGraph.putEdge(sourceSet, productionSourceSet) - } - for (targetSourceSetName in sourceSet.dependsOnSourceSets) { - val targetSourceSet = mppModel.sourceSets[targetSourceSetName] ?: continue - sourceSetGraph.putEdge(sourceSet, targetSourceSet) - } - // Workaround: Non-android source sets have commonMain/commonTest in their dependsOn - // Remove when the same is implemented for Android modules as well - if (sourceSet.actualPlatforms.supports(KotlinPlatform.ANDROID)) { - val commonSourceSetName = if (sourceSet.isTestModule) { - KotlinSourceSet.COMMON_TEST_SOURCE_SET_NAME - } else { - KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME - } - val commonSourceSet = mppModel.sourceSets[commonSourceSetName] - if (commonSourceSet != null && commonSourceSet != sourceSet) { - sourceSetGraph.putEdge(sourceSet, commonSourceSet) - } - } - } - val closedSourceSetGraph = Graphs.transitiveClosure(sourceSetGraph) - for (sourceSet in closedSourceSetGraph.nodes()) { - val isAndroid = delegateToAndroidPlugin(sourceSet) - val fromDataNode = if (isAndroid) { - ideModule - } else { - getSiblingKotlinModuleData(sourceSet, gradleModule, ideModule, resolverCtx) - } ?: continue - val dependeeSourceSets = closedSourceSetGraph.successors(sourceSet) - val sourceSetInfos = if (isAndroid) { - ideModule.kotlinAndroidSourceSets?.filter { - (it.kotlinModule as? KotlinCompilation)?.allSourceSets?.contains(sourceSet) ?: false - } ?: emptyList() - } else { - listOfNotNull(fromDataNode.kotlinSourceSet) - } - for (sourceSetInfo in sourceSetInfos) { - if (sourceSetInfo.kotlinModule is KotlinCompilation) { - val selfName = sourceSetInfo.kotlinModule.fullName() - sourceSetInfo.addSourceSets(dependeeSourceSets, selfName, gradleModule, resolverCtx) - } - } - if (delegateToAndroidPlugin(sourceSet)) continue - for (dependeeSourceSet in dependeeSourceSets) { - val toDataNode = getSiblingKotlinModuleData(dependeeSourceSet, gradleModule, ideModule, resolverCtx) ?: continue - addDependency(fromDataNode, toDataNode, dependeeSourceSet.isTestModule) - } - if (processedModuleIds.add(getKotlinModuleId(gradleModule, sourceSet, resolverCtx))) { - val mergedSubstitutedDependencies = LinkedHashSet().apply { - val forceNativeDependencyPropagation: Boolean - val excludeInheritedNativeDependencies: Boolean - if (mppModel.extraFeatures.isHMPPEnabled && sourceSet.actualPlatforms.getSinglePlatform() == KotlinPlatform.NATIVE) { - forceNativeDependencyPropagation = mppModel.extraFeatures.isNativeDependencyPropagationEnabled - excludeInheritedNativeDependencies = !forceNativeDependencyPropagation - } else { - forceNativeDependencyPropagation = false - excludeInheritedNativeDependencies = false - } - addAll(substitutor.substituteDependencies(sourceSet)) - - dependeeSourceSets.flatMapTo(this) { dependeeSourceSet -> - substitutor.substituteDependencies(dependeeSourceSet).run { - if (excludeInheritedNativeDependencies) - filter { !it.name.startsWith(KOTLIN_NATIVE_LIBRARY_PREFIX_PLUS_SPACE) } - else this - } - } - - if (forceNativeDependencyPropagation) { - sourceSetToCompilations[sourceSet.name]?.let { compilations -> - addAll(propagatedNativeDependencies(compilations)) - } - } - - if (mppModel.extraFeatures.isHMPPEnabled) { - addAll(propagatedPlatformDependencies(mppModel, sourceSet)) - } - } - - buildDependencies( - resolverCtx, - sourceSetMap, - artifactsMap, - fromDataNode, - preprocessDependencies(mergedSubstitutedDependencies), - ideProject - ) - - @Suppress("UNCHECKED_CAST") - KotlinNativeLibrariesFixer.applyTo(fromDataNode as DataNode, ideProject) - } - } + val context = createPopulateModuleDependenciesContext( + gradleModule = gradleModule, + ideProject = ideProject, + ideModule = ideModule, + resolverCtx = resolverCtx + ) ?: return + populateModuleDependenciesByCompilations(context) + populateModuleDependenciesBySourceSetVisibilityGraph(context) } - private fun KotlinNativeLibrariesDependencySubstitutor.substituteDependencies(kotlinModule: KotlinModule): List = - substituteDependencies(kotlinModule.dependencies.mapNotNull { mppModel.dependencyMap[it] }) - - // We can't really commonize native platform libraries yet. - // But APIs for different targets may be very similar. - // E.g. ios_arm64 and ios_x64 have almost identical platform libraries. - // We handle these special cases and resolve common sources for such - // targets against libraries of one of them. E.g. common sources for - // ios_x64 and ios_arm64 will be resolved against ios_arm64 libraries. - // - // Currently such special casing is available for Apple platforms - // (iOS, watchOS and tvOS) and native Android (ARM, X86). - // TODO: Do we need to support user's interop libraries too? - private fun propagatedNativeDependencies(compilations: List): List { - if (compilations.size <= 1) { - return emptyList() - } - - val copyFrom = when { - compilations.all { it.isAppleCompilation } -> - compilations.selectFirstAvailableTarget( - "watchos_arm64", "watchos_arm32", "watchos_x64", "watchos_x86", - "ios_arm64", "ios_arm32", "ios_x64", - "tvos_arm64", "tvos_x64" - ) - compilations.all { it.konanTarget?.startsWith("android") == true } -> - compilations.selectFirstAvailableTarget( - "android_arm64", "android_arm32", "android_x64", "android_x86" - ) - else -> return emptyList() - } - - return copyFrom.dependencyNames.mapNotNull { (name, dependency) -> - when { - !name.startsWith(KOTLIN_NATIVE_LIBRARY_PREFIX_PLUS_SPACE) -> null // Support only default platform libs for now. - compilations.all { it.dependencyNames.containsKey(name) } -> dependency - else -> null - } - } - } - - - /** - * Source sets sharing code between JVM and Android are the only intermediate source sets that - * can effectively consume a dependency's platform artifact. - * When a library only offers a JVM variant, then Android and JVM consume this variant of the library. - * - * This will be replaced later on by [KT-43450](https://youtrack.jetbrains.com/issue/KT-43450) - * - * @return all dependencies being present across given JVM and Android compilations that this [sourceSet] can also participates in. - */ - private fun propagatedPlatformDependencies( - mppModel: KotlinMPPGradleModel, - sourceSet: KotlinSourceSet, - ): Set { - if ( - sourceSet.actualPlatforms.platforms.sorted() == listOf(KotlinPlatform.JVM, KotlinPlatform.ANDROID).sorted() - ) { - return mppModel.targets - .filter { target -> target.platform == KotlinPlatform.JVM || target.platform == KotlinPlatform.ANDROID } - .flatMap { target -> target.compilations } - .filter { compilation -> compilation.dependsOnSourceSet(mppModel, sourceSet) } - .map { targetCompilations -> targetCompilations.dependencies.mapNotNull(mppModel.dependencyMap::get).toSet() } - .reduceOrNull { acc, dependencies -> acc.intersect(dependencies) }.orEmpty() - - } - - return emptySet() - } - - private fun KotlinCompilation.dependsOnSourceSet(mppModel: KotlinMPPGradleModel, sourceSet: KotlinSourceSet): Boolean { - return allSourceSets.any { containedSourceSet -> sourceSet.isOrDependsOnSourceSet(mppModel, containedSourceSet) } - } - - private fun KotlinSourceSet.isOrDependsOnSourceSet(mppModel: KotlinMPPGradleModel, sourceSet: KotlinSourceSet): Boolean { - if (this == sourceSet) return true - return this.dependsOnSourceSets - .map { dependencySourceSetName -> mppModel.sourceSets.getValue(dependencySourceSetName) } - .any { dependencySourceSet -> dependencySourceSet.isOrDependsOnSourceSet(mppModel, sourceSet) } - } - - private val CompilationWithDependencies.isAppleCompilation: Boolean - get() = konanTarget?.let { - it.startsWith("ios") || it.startsWith("watchos") || it.startsWith("tvos") - } ?: false - - private fun Iterable.selectFirstAvailableTarget(@NonNls vararg targetsByPriority: String): CompilationWithDependencies { - for (target in targetsByPriority) { - val result = firstOrNull { it.konanTarget == target } - if (result != null) { - return result - } - } - return first() - } - - private fun KotlinModule.toSourceSet(mppModel: KotlinMPPGradleModel) = when (this) { - is KotlinSourceSet -> this - is KotlinCompilation -> mppModel.sourceSets[fullName()] - else -> null - } - - private fun preprocessDependencies(dependencies: Collection): List { - return dependencies - .groupBy { it.id } - .mapValues { it.value.firstOrNull { it.scope == "COMPILE" } ?: it.value.lastOrNull() } - .values - .filterNotNull() - } - - private fun addDependency(fromModule: DataNode<*>, toModule: DataNode<*>, dependOnTestModule: Boolean) { - if (fromModule.data == toModule.data) return - val fromData = fromModule.data as? ModuleData ?: return - val toData = toModule.data as? ModuleData ?: return - val existing = fromModule.children.mapNotNull { it.data as? ModuleDependencyData } - .filter { it.target.id == (toModule.data as? ModuleData)?.id } - val nodeToModify = - existing.singleOrNull() ?: existing.firstOrNull { it.scope == DependencyScope.COMPILE } ?: existing.firstOrNull() - if (nodeToModify != null) { - nodeToModify.scope = DependencyScope.COMPILE - nodeToModify.isProductionOnTestDependency = nodeToModify.isProductionOnTestDependency || dependOnTestModule - return - } - val moduleDependencyData = ModuleDependencyData(fromData, toData).also { - it.scope = DependencyScope.COMPILE - it.isExported = false - it.isProductionOnTestDependency = dependOnTestModule - } - fromModule.createChild(ProjectKeys.MODULE_DEPENDENCY, moduleDependencyData) - } - - private fun getSiblingKotlinModuleData( + internal fun getSiblingKotlinModuleData( kotlinModule: KotlinModule, gradleModule: IdeaModule, ideModule: DataNode, @@ -885,36 +636,13 @@ open class KotlinMPPGradleProjectResolver : AbstractProjectResolverExtensionComp sourceSetsMap[dataNode.data.id] = dataNode } } - for (sourceSet in mppModel.sourceSets.values) { + for (sourceSet in mppModel.sourceSetsByName.values) { val moduleId = getKotlinModuleId(gradleModule, sourceSet, resolverCtx) val moduleDataNode = sourceSetsMap[moduleId] processor(moduleDataNode, sourceSet) } } - private fun processCompilations( - gradleModule: IdeaModule, - mppModel: KotlinMPPGradleModel, - ideModule: DataNode, - resolverCtx: ProjectResolverContext, - processor: (DataNode, KotlinCompilation) -> Unit - ) { - val sourceSetsMap = HashMap>() - for (dataNode in ExternalSystemApiUtil.findAll(ideModule, GradleSourceSetData.KEY)) { - if (dataNode.kotlinSourceSet != null) { - sourceSetsMap[dataNode.data.id] = dataNode - } - } - for (target in mppModel.targets) { - if (delegateToAndroidPlugin(target)) continue - for (compilation in target.compilations) { - val moduleId = getKotlinModuleId(gradleModule, compilation, resolverCtx) - val moduleDataNode = sourceSetsMap[moduleId] ?: continue - processor(moduleDataNode, compilation) - } - } - } - private val IdeaModule.jdkNameIfAny get() = try { jdkName @@ -988,7 +716,7 @@ open class KotlinMPPGradleProjectResolver : AbstractProjectResolverExtensionComp if (effectiveClassesDir != null) { sourcesWithTypes += compilation.sourceType to DefaultExternalSourceDirectorySet().also { dirSet -> dirSet.outputDir = effectiveClassesDir - dirSet.srcDirs = compilation.allSourceSets.flatMapTo(LinkedHashSet()) { it.sourceDirs } + dirSet.srcDirs = compilation.declaredSourceSets.flatMapTo(LinkedHashSet()) { it.sourceDirs } dirSet.gradleOutputDirs += compilation.output.classesDirs dirSet.setInheritedCompilerOutput(false) } @@ -996,7 +724,7 @@ open class KotlinMPPGradleProjectResolver : AbstractProjectResolverExtensionComp if (resourcesDir != null) { sourcesWithTypes += compilation.resourceType to DefaultExternalSourceDirectorySet().also { dirSet -> dirSet.outputDir = resourcesDir - dirSet.srcDirs = compilation.allSourceSets.flatMapTo(LinkedHashSet()) { it.resourceDirs } + dirSet.srcDirs = compilation.declaredSourceSets.flatMapTo(LinkedHashSet()) { it.resourceDirs } dirSet.gradleOutputDirs += resourcesDir dirSet.setInheritedCompilerOutput(false) } @@ -1034,7 +762,9 @@ open class KotlinMPPGradleProjectResolver : AbstractProjectResolverExtensionComp private val KotlinModule.resourceType get() = if (isTestModule) ExternalSystemSourceType.TEST_RESOURCE else ExternalSystemSourceType.RESOURCE + @OptIn(ExperimentalGradleToolingApi::class) private fun createSourceSetInfo( + mppModel: KotlinMPPGradleModel, sourceSet: KotlinSourceSet, gradleModule: IdeaModule, resolverCtx: ProjectResolverContext @@ -1044,13 +774,15 @@ open class KotlinMPPGradleProjectResolver : AbstractProjectResolverExtensionComp val languageSettings = sourceSet.languageSettings info.moduleId = getKotlinModuleId(gradleModule, sourceSet, resolverCtx) info.gradleModuleId = getModuleId(resolverCtx, gradleModule) - info.actualPlatforms.addSimplePlatforms(sourceSet.actualPlatforms.platforms) + info.actualPlatforms.pushPlatforms(sourceSet.actualPlatforms) info.isTestModule = sourceSet.isTestModule - info.dependsOn = sourceSet.dependsOnSourceSets.toList().map { - getGradleModuleQualifiedName(resolverCtx, gradleModule, it) + info.dependsOn = mppModel.resolveAllDependsOnSourceSets(sourceSet).map { dependsOnSourceSet -> + getGradleModuleQualifiedName(resolverCtx, gradleModule, dependsOnSourceSet.name) } //TODO(auskov): target flours are lost here - info.compilerArguments = createCompilerArguments(emptyList(), sourceSet.actualPlatforms.getSinglePlatform()).also { + info.compilerArguments = createCompilerArguments( + emptyList(), sourceSet.actualPlatforms.singleOrNull() ?: KotlinPlatform.COMMON + ).also { it.multiPlatform = true it.languageVersion = languageSettings.languageVersion it.apiVersion = languageSettings.apiVersion @@ -1086,9 +818,9 @@ open class KotlinMPPGradleProjectResolver : AbstractProjectResolverExtensionComp return KotlinSourceSetInfo(compilation).also { sourceSetInfo -> sourceSetInfo.moduleId = getKotlinModuleId(gradleModule, compilation, resolverCtx) sourceSetInfo.gradleModuleId = getModuleId(resolverCtx, gradleModule) - sourceSetInfo.actualPlatforms.addSimplePlatforms(listOf(compilation.platform)) + sourceSetInfo.actualPlatforms.pushPlatforms(listOf(compilation.platform)) sourceSetInfo.isTestModule = compilation.isTestModule - sourceSetInfo.dependsOn = compilation.allSourceSets.flatMap { it.dependsOnSourceSets }.map { + sourceSetInfo.dependsOn = compilation.declaredSourceSets.flatMap { it.allDependsOnSourceSets }.map { getGradleModuleQualifiedName(resolverCtx, gradleModule, it) }.distinct().toList() sourceSetInfo.compilerArguments = @@ -1105,7 +837,7 @@ open class KotlinMPPGradleProjectResolver : AbstractProjectResolverExtensionComp /** Checks if our IDE doesn't support such platform */ private fun KotlinPlatform.isNotSupported() = IdePlatformKindTooling.getToolingIfAny(this) == null - private fun KotlinSourceSetInfo.addSourceSets( + internal fun KotlinSourceSetInfo.addSourceSets( sourceSets: Collection, selfName: String, gradleModule: IdeaModule, @@ -1131,7 +863,7 @@ open class KotlinMPPGradleProjectResolver : AbstractProjectResolverExtensionComp private fun getKotlinModuleId(gradleModule: IdeaModule, kotlinModule: KotlinModule, resolverCtx: ProjectResolverContext) = getGradleModuleQualifiedName(resolverCtx, gradleModule, kotlinModule.fullName()) - private fun getGradleModuleQualifiedName( + internal fun getGradleModuleQualifiedName( resolverCtx: ProjectResolverContext, gradleModule: IdeaModule, simpleName: String @@ -1144,10 +876,13 @@ open class KotlinMPPGradleProjectResolver : AbstractProjectResolverExtensionComp ignoreCase = true ) ?: false - private fun delegateToAndroidPlugin(kotlinTarget: KotlinTarget): Boolean = + internal fun delegateToAndroidPlugin(compilation: KotlinCompilation): Boolean = + androidPluginPresent && compilation.platform == KotlinPlatform.ANDROID + + internal fun delegateToAndroidPlugin(kotlinTarget: KotlinTarget): Boolean = androidPluginPresent && kotlinTarget.platform == KotlinPlatform.ANDROID - private fun delegateToAndroidPlugin(kotlinSourceSet: KotlinSourceSet): Boolean = + internal fun delegateToAndroidPlugin(kotlinSourceSet: KotlinSourceSet): Boolean = androidPluginPresent && kotlinSourceSet.actualPlatforms.platforms.singleOrNull() == KotlinPlatform.ANDROID } } diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/klib/KotlinNativeLibrariesDependencySubstitutor.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/klib/KotlinNativeLibrariesDependencySubstitutor.kt index 61d72143695..dc550d4219e 100644 --- a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/klib/KotlinNativeLibrariesDependencySubstitutor.kt +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/klib/KotlinNativeLibrariesDependencySubstitutor.kt @@ -9,9 +9,11 @@ import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.roots.DependencyScope import com.intellij.openapi.util.Key import org.gradle.tooling.model.idea.IdeaModule +import org.jetbrains.kotlin.gradle.KotlinDependency import org.jetbrains.kotlin.gradle.KotlinMPPGradleModel import org.jetbrains.kotlin.idea.configuration.buildClasspathData import org.jetbrains.kotlin.idea.configuration.klib.KotlinNativeLibraryNameUtil.buildIDELibraryName +import org.jetbrains.kotlin.idea.configuration.mpp.KotlinDependenciesPreprocessor import org.jetbrains.kotlin.idea.inspections.gradle.findKotlinPluginVersion import org.jetbrains.kotlin.konan.library.KONAN_STDLIB_NAME import org.jetbrains.plugins.gradle.ExternalDependencyId @@ -21,10 +23,15 @@ import java.io.File // KT-29613, KT-29783 internal class KotlinNativeLibrariesDependencySubstitutor( - val mppModel: KotlinMPPGradleModel, + private val mppModel: KotlinMPPGradleModel, private val gradleModule: IdeaModule, private val resolverCtx: ProjectResolverContext -) { +) : KotlinDependenciesPreprocessor { + + override fun invoke(dependencies: Iterable): List { + return substituteDependencies(dependencies.toList()) + } + // Substitutes `ExternalDependency` entries that represent KLIBs with new dependency entries with proper type and name: // - every `FileCollectionDependency` is checked whether it points to an existing KLIB, and substituted if it is // - similarly for every `ExternalLibraryDependency` with `groupId == "Kotlin/Native"` (legacy KLIB provided by Gradle plugin <= 1.3.20) diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/mpp/DistinctIdKotlinDependenciesPreprocessor.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/mpp/DistinctIdKotlinDependenciesPreprocessor.kt new file mode 100644 index 00000000000..ebedbd944c7 --- /dev/null +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/mpp/DistinctIdKotlinDependenciesPreprocessor.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2010-2021 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.idea.configuration.mpp + +import org.jetbrains.kotlin.gradle.KotlinDependency + +internal object DistinctIdKotlinDependenciesPreprocessor : KotlinDependenciesPreprocessor { + override fun invoke(dependencies: Iterable): List { + return dependencies + .groupBy { dependency -> dependency.id } + .mapValues { (_, dependenciesWithSameId) -> + dependenciesWithSameId.firstOrNull { it.scope == "COMPILE" } ?: dependenciesWithSameId.lastOrNull() + } + .values + .filterNotNull() + } + +} \ No newline at end of file diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/mpp/KotlinDependenciesPreprocessor.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/mpp/KotlinDependenciesPreprocessor.kt new file mode 100644 index 00000000000..7305ecff177 --- /dev/null +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/mpp/KotlinDependenciesPreprocessor.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2010-2021 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.idea.configuration.mpp + +import org.jetbrains.kotlin.gradle.KotlinDependency + +internal interface KotlinDependenciesPreprocessor { + operator fun invoke(dependencies: Iterable): List +} + +internal operator fun KotlinDependenciesPreprocessor.plus( + other: KotlinDependenciesPreprocessor +): KotlinDependenciesPreprocessor { + return SequentialKotlinDependenciesPreprocessor(this, other) +} + +private class SequentialKotlinDependenciesPreprocessor( + private val first: KotlinDependenciesPreprocessor, + private val second: KotlinDependenciesPreprocessor +) : KotlinDependenciesPreprocessor { + override fun invoke(dependencies: Iterable): List { + return second(first(dependencies)) + } +} \ No newline at end of file diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/mpp/PopulateModuleDependenciesContext.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/mpp/PopulateModuleDependenciesContext.kt new file mode 100644 index 00000000000..69386fa8d51 --- /dev/null +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/mpp/PopulateModuleDependenciesContext.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2010-2021 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.idea.configuration.mpp + +import com.intellij.openapi.externalSystem.model.DataNode +import com.intellij.openapi.externalSystem.model.project.ModuleData +import com.intellij.openapi.externalSystem.model.project.ProjectData +import org.gradle.tooling.model.idea.IdeaModule +import org.jetbrains.kotlin.gradle.* +import org.jetbrains.kotlin.idea.configuration.KotlinMPPGradleProjectResolver +import org.jetbrains.kotlin.idea.configuration.KotlinMPPGradleProjectResolver.Companion.CompilationWithDependencies +import org.jetbrains.kotlin.idea.configuration.getMppModel +import org.jetbrains.kotlin.idea.configuration.klib.KotlinNativeLibrariesDependencySubstitutor +import org.jetbrains.plugins.gradle.model.ExternalSourceSet +import org.jetbrains.plugins.gradle.model.data.GradleSourceSetData +import org.jetbrains.plugins.gradle.service.project.GradleProjectResolver +import org.jetbrains.plugins.gradle.service.project.ProjectResolverContext +import com.intellij.openapi.util.Pair as IntelliJPair + + +internal typealias ModuleId = String +internal typealias ArtifactPath = String + +internal data class PopulateModuleDependenciesContext( + val resolverCtx: ProjectResolverContext, + val mppModel: KotlinMPPGradleModel, + val gradleModule: IdeaModule, + val ideProject: DataNode, + val ideModule: DataNode, + val dependenciesPreprocessor: KotlinDependenciesPreprocessor, + val sourceSetMap: Map, ExternalSourceSet>>, + val artifactsMap: Map, + val processedModuleIds: MutableSet = mutableSetOf() +) + +internal fun KotlinMPPGradleProjectResolver.Companion.createPopulateModuleDependenciesContext( + gradleModule: IdeaModule, + ideProject: DataNode, + ideModule: DataNode, + resolverCtx: ProjectResolverContext +): PopulateModuleDependenciesContext? { + val mppModel = resolverCtx.getMppModel(gradleModule) ?: return null + mppModel.dependencyMap.values.modifyDependenciesOnMppModules(ideProject, resolverCtx) + + val sourceSetMap = ideProject.getUserData(GradleProjectResolver.RESOLVED_SOURCE_SETS) ?: return null + val artifactsMap = ideProject.getUserData(GradleProjectResolver.CONFIGURATION_ARTIFACTS) ?: return null + val dependenciesPreprocessor = KotlinNativeLibrariesDependencySubstitutor(mppModel, gradleModule, resolverCtx) + .plus(DistinctIdKotlinDependenciesPreprocessor) + + return PopulateModuleDependenciesContext( + resolverCtx = resolverCtx, + mppModel = mppModel, + gradleModule = gradleModule, + ideProject = ideProject, + ideModule = ideModule, + dependenciesPreprocessor = dependenciesPreprocessor, + sourceSetMap = sourceSetMap, + artifactsMap = artifactsMap, + ) +} + +internal fun PopulateModuleDependenciesContext.getDependencies(module: KotlinModule): List { + return dependenciesPreprocessor(module.dependencies.mapNotNull { id -> mppModel.dependencyMap[id] }) +} + +internal fun PopulateModuleDependenciesContext.getCompilationsWithDependencies( + sourceSet: KotlinSourceSet +): List { + return mppModel.getCompilations(sourceSet).map { compilation -> CompilationWithDependencies(compilation, getDependencies(compilation)) } +} + +internal fun KotlinCompilation.dependsOnSourceSet(mppModel: KotlinMPPGradleModel, sourceSet: KotlinSourceSet): Boolean { + return declaredSourceSets.any { containedSourceSet -> sourceSet.isOrDependsOnSourceSet(mppModel, containedSourceSet) } +} + +internal fun KotlinSourceSet.isOrDependsOnSourceSet(mppModel: KotlinMPPGradleModel, sourceSet: KotlinSourceSet): Boolean { + if (this == sourceSet) return true + return this.declaredDependsOnSourceSets + .map { dependencySourceSetName -> mppModel.sourceSetsByName.getValue(dependencySourceSetName) } + .any { dependencySourceSet -> dependencySourceSet.isOrDependsOnSourceSet(mppModel, sourceSet) } +} + + diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/mpp/addDependency.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/mpp/addDependency.kt new file mode 100644 index 00000000000..44e512a3bbb --- /dev/null +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/mpp/addDependency.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2010-2021 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.idea.configuration.mpp + +import com.intellij.openapi.externalSystem.model.DataNode +import com.intellij.openapi.externalSystem.model.ProjectKeys +import com.intellij.openapi.externalSystem.model.project.ModuleData +import com.intellij.openapi.externalSystem.model.project.ModuleDependencyData +import com.intellij.openapi.roots.DependencyScope +import org.jetbrains.kotlin.idea.configuration.KotlinMPPGradleProjectResolver + +internal fun KotlinMPPGradleProjectResolver.Companion.addDependency( + fromModule: DataNode<*>, toModule: DataNode<*>, dependOnTestModule: Boolean +) { + if (fromModule.data == toModule.data) return + val fromData = fromModule.data as? ModuleData ?: return + val toData = toModule.data as? ModuleData ?: return + + val existing = fromModule.children.mapNotNull { it.data as? ModuleDependencyData } + .filter { it.target.id == (toModule.data as? ModuleData)?.id } + + val nodeToModify = existing.singleOrNull() ?: existing.firstOrNull { it.scope == DependencyScope.COMPILE } ?: existing.firstOrNull() + if (nodeToModify != null) { + nodeToModify.scope = DependencyScope.COMPILE + nodeToModify.isProductionOnTestDependency = nodeToModify.isProductionOnTestDependency || dependOnTestModule + return + } + + val moduleDependencyData = ModuleDependencyData(fromData, toData).also { + it.scope = DependencyScope.COMPILE + it.isExported = false + it.isProductionOnTestDependency = dependOnTestModule + } + + fromModule.createChild(ProjectKeys.MODULE_DEPENDENCY, moduleDependencyData) +} \ No newline at end of file diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/mpp/buildDependencies.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/mpp/buildDependencies.kt new file mode 100644 index 00000000000..b4d7049604d --- /dev/null +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/mpp/buildDependencies.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2010-2021 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.idea.configuration.mpp + +import com.intellij.openapi.externalSystem.model.DataNode +import com.intellij.openapi.externalSystem.model.project.ProjectData +import com.intellij.openapi.util.Pair +import org.jetbrains.kotlin.gradle.KotlinDependency +import org.jetbrains.kotlin.idea.configuration.KotlinMPPGradleProjectResolver +import org.jetbrains.kotlin.idea.configuration.klib.KotlinNativeLibrariesFixer +import org.jetbrains.plugins.gradle.model.ExternalSourceSet +import org.jetbrains.plugins.gradle.model.data.GradleSourceSetData +import org.jetbrains.plugins.gradle.service.project.GradleProjectResolverUtil +import org.jetbrains.plugins.gradle.service.project.ProjectResolverContext + +@Throws(IllegalStateException::class) +internal fun KotlinMPPGradleProjectResolver.Companion.buildDependencies( + resolverCtx: ProjectResolverContext, + sourceSetMap: Map, ExternalSourceSet>>, + artifactsMap: Map, + ownerDataNode: DataNode, + dependencies: Collection, + ideProject: DataNode +) { + GradleProjectResolverUtil.buildDependencies( + resolverCtx, sourceSetMap, artifactsMap, ownerDataNode, dependencies, ideProject + ) + KotlinNativeLibrariesFixer.applyTo(ownerDataNode, ideProject) +} \ No newline at end of file diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/mpp/getCompilations.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/mpp/getCompilations.kt new file mode 100644 index 00000000000..1b007bced28 --- /dev/null +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/mpp/getCompilations.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2010-2021 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.idea.configuration.mpp + +import com.intellij.openapi.externalSystem.model.DataNode +import com.intellij.openapi.externalSystem.model.project.ModuleData +import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil +import org.gradle.tooling.model.idea.IdeaModule +import org.jetbrains.kotlin.gradle.KotlinCompilation +import org.jetbrains.kotlin.gradle.KotlinMPPGradleModel +import org.jetbrains.kotlin.idea.configuration.KotlinMPPGradleProjectResolver +import org.jetbrains.kotlin.idea.configuration.kotlinSourceSet +import org.jetbrains.kotlin.idea.configuration.utils.getKotlinModuleId +import org.jetbrains.plugins.gradle.model.data.GradleSourceSetData +import org.jetbrains.plugins.gradle.service.project.ProjectResolverContext + +internal fun KotlinMPPGradleProjectResolver.Companion.getCompilations( + gradleModule: IdeaModule, + mppModel: KotlinMPPGradleModel, + ideModule: DataNode, + resolverCtx: ProjectResolverContext +): Sequence, KotlinCompilation>> { + + val sourceSetsMap = HashMap>() + for (dataNode in ExternalSystemApiUtil.findAll(ideModule, GradleSourceSetData.KEY)) { + if (dataNode.kotlinSourceSet != null) { + sourceSetsMap[dataNode.data.id] = dataNode + } + } + + return sequence { + for (target in mppModel.targets) { + for (compilation in target.compilations) { + val moduleId = getKotlinModuleId(gradleModule, compilation, resolverCtx) + val moduleDataNode = sourceSetsMap[moduleId] ?: continue + yield(moduleDataNode to compilation) + } + } + } +} + + diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/mpp/populateModuleDependenciesByCompilations.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/mpp/populateModuleDependenciesByCompilations.kt new file mode 100644 index 00000000000..98770f6b8cb --- /dev/null +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/mpp/populateModuleDependenciesByCompilations.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2010-2021 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.idea.configuration.mpp + +import org.jetbrains.kotlin.idea.configuration.KotlinMPPGradleProjectResolver +import org.jetbrains.kotlin.idea.configuration.utils.fullName +import org.jetbrains.kotlin.idea.configuration.utils.getKotlinModuleId + +internal fun KotlinMPPGradleProjectResolver.Companion.populateModuleDependenciesByCompilations( + context: PopulateModuleDependenciesContext +): Unit = with(context) { + getCompilations(gradleModule, mppModel, ideModule, resolverCtx) + .filterNot { (_, compilation) -> delegateToAndroidPlugin(compilation) } + .filter { (_, compilation) -> processedModuleIds.add(getKotlinModuleId(gradleModule, compilation, resolverCtx)) } + .forEach { (dataNode, compilation) -> + buildDependencies( + resolverCtx, sourceSetMap, artifactsMap, dataNode, getDependencies(compilation), ideProject + ) + for (sourceSet in compilation.declaredSourceSets) { + if (sourceSet.fullName() == compilation.fullName()) continue + val targetDataNode = getSiblingKotlinModuleData(sourceSet, gradleModule, ideModule, resolverCtx) ?: continue + addDependency(dataNode, targetDataNode, sourceSet.isTestModule) + } + } + +} + diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/mpp/populateModuleDependenciesBySourceSetVisibilityGraph.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/mpp/populateModuleDependenciesBySourceSetVisibilityGraph.kt new file mode 100644 index 00000000000..cbe61aa9399 --- /dev/null +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/mpp/populateModuleDependenciesBySourceSetVisibilityGraph.kt @@ -0,0 +1,234 @@ +/* + * Copyright 2010-2021 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.idea.configuration.mpp + +import com.google.common.graph.Graph +import com.intellij.openapi.externalSystem.model.DataNode +import org.jetbrains.annotations.NonNls +import org.jetbrains.kotlin.gradle.* +import org.jetbrains.kotlin.idea.configuration.KotlinMPPGradleProjectResolver +import org.jetbrains.kotlin.idea.configuration.KotlinMPPGradleProjectResolver.Companion.CompilationWithDependencies +import org.jetbrains.kotlin.idea.configuration.klib.KotlinNativeLibraryNameUtil +import org.jetbrains.kotlin.idea.configuration.kotlinAndroidSourceSets +import org.jetbrains.kotlin.idea.configuration.kotlinSourceSet +import org.jetbrains.kotlin.idea.configuration.utils.createSourceSetVisibilityGraph +import org.jetbrains.kotlin.idea.configuration.utils.fullName +import org.jetbrains.kotlin.idea.configuration.utils.getKotlinModuleId +import org.jetbrains.kotlin.idea.configuration.utils.transitiveClosure +import org.jetbrains.plugins.gradle.model.ExternalDependency +import org.jetbrains.plugins.gradle.model.data.GradleSourceSetData + +internal fun KotlinMPPGradleProjectResolver.Companion.populateModuleDependenciesBySourceSetVisibilityGraph( + context: PopulateModuleDependenciesContext +): Unit = with(context) { + val sourceSetVisibilityGraph = createSourceSetVisibilityGraph(mppModel).transitiveClosure + for (sourceSet in sourceSetVisibilityGraph.nodes()) { + populateSourceSetInfos(context, sourceSetVisibilityGraph, sourceSet) + if (delegateToAndroidPlugin(sourceSet)) continue + + + val visibleSourceSets = sourceSetVisibilityGraph.successors(sourceSet) + val fromDataNode = getSiblingKotlinModuleData(sourceSet, gradleModule, ideModule, resolverCtx)?.cast() + ?: return + + + /* Add dependencies from current sourceSet to all visible source sets (dependsOn, test to production, ...)*/ + for (visibleSourceSet in visibleSourceSets) { + val toDataNode = getSiblingKotlinModuleData(visibleSourceSet, gradleModule, ideModule, resolverCtx) ?: continue + addDependency(fromDataNode, toDataNode, visibleSourceSet.isTestModule) + } + + if (!processedModuleIds.add(getKotlinModuleId(gradleModule, sourceSet, resolverCtx))) continue + val settings = dependencyPopulationSettings(mppModel, sourceSet) + val directDependencies = getDependencies(sourceSet).toSet() + val dependenciesFromVisibleSourceSets = getDependenciesFromVisibleSourceSets(settings, visibleSourceSets) + val dependenciesFromNativePropagation = getPropagatedNativeDependencies(settings, sourceSet) + val dependenciesFromPlatformPropagation = getPropagatedPlatformDependencies(sourceSet) + + val dependencies = dependenciesPreprocessor( + directDependencies + dependenciesFromVisibleSourceSets + + dependenciesFromNativePropagation + dependenciesFromPlatformPropagation + ) + + buildDependencies(resolverCtx, sourceSetMap, artifactsMap, fromDataNode, dependencies, ideProject) + } +} + +private data class DependencyPopulationSettings( + val forceNativeDependencyPropagation: Boolean, + val excludeInheritedNativeDependencies: Boolean +) + +private fun dependencyPopulationSettings(mppModel: KotlinMPPGradleModel, sourceSet: KotlinSourceSet): DependencyPopulationSettings { + val forceNativeDependencyPropagation: Boolean + val excludeInheritedNativeDependencies: Boolean + if (mppModel.extraFeatures.isHMPPEnabled && sourceSet.actualPlatforms.singleOrNull() == KotlinPlatform.NATIVE) { + forceNativeDependencyPropagation = mppModel.extraFeatures.isNativeDependencyPropagationEnabled + excludeInheritedNativeDependencies = !forceNativeDependencyPropagation + } else { + forceNativeDependencyPropagation = false + excludeInheritedNativeDependencies = false + } + return DependencyPopulationSettings( + forceNativeDependencyPropagation = forceNativeDependencyPropagation, + excludeInheritedNativeDependencies = excludeInheritedNativeDependencies + ) +} + +private fun PopulateModuleDependenciesContext.getDependenciesFromVisibleSourceSets( + settings: DependencyPopulationSettings, + visibleSourceSets: Set +): Set { + val inheritedDependencies = visibleSourceSets + .flatMap { visibleSourceSet -> getDependencies(visibleSourceSet) } + + return if (settings.excludeInheritedNativeDependencies) { + inheritedDependencies.filter { !it.name.startsWith(KotlinNativeLibraryNameUtil.KOTLIN_NATIVE_LIBRARY_PREFIX_PLUS_SPACE) } + } else { + inheritedDependencies + }.toSet() +} + +private fun PopulateModuleDependenciesContext.getPropagatedNativeDependencies( + settings: DependencyPopulationSettings, + sourceSet: KotlinSourceSet +): Set { + if (!settings.forceNativeDependencyPropagation) { + return emptySet() + } + + return getPropagatedNativeDependencies(getCompilationsWithDependencies(sourceSet)).toSet() +} + +/** + * We can't really commonize native platform libraries yet. + * But APIs for different targets may be very similar. + * E.g. ios_arm64 and ios_x64 have almost identical platform libraries + * We handle these special cases and resolve common sources for such + * targets against libraries of one of them. E.g. common sources for + * ios_x64 and ios_arm64 will be resolved against ios_arm64 libraries. + * Currently such special casing is available for Apple platforms + * (iOS, watchOS and tvOS) and native Android (ARM, X86). + * TODO: Do we need to support user's interop libraries too? + */ +private fun getPropagatedNativeDependencies(compilations: List): List { + if (compilations.size <= 1) { + return emptyList() + } + + val copyFrom = when { + compilations.all { it.isAppleCompilation } -> + compilations.selectFirstAvailableTarget( + "watchos_arm64", "watchos_arm32", "watchos_x86", + "ios_arm64", "ios_arm32", "ios_x64", + "tvos_arm64", "tvos_x64" + ) + compilations.all { it.konanTarget?.startsWith("android") == true } -> + compilations.selectFirstAvailableTarget( + "android_arm64", "android_arm32", "android_x64", "android_x86" + ) + else -> return emptyList() + } + + return copyFrom.dependencyNames.mapNotNull { (name, dependency) -> + when { + !name.startsWith(KotlinNativeLibraryNameUtil.KOTLIN_NATIVE_LIBRARY_PREFIX_PLUS_SPACE) -> null // Support only default platform libs for now. + compilations.all { it.dependencyNames.containsKey(name) } -> dependency + else -> null + } + } +} + +private val CompilationWithDependencies.isAppleCompilation: Boolean + get() = konanTarget?.let { + it.startsWith("ios") || it.startsWith("watchos") || it.startsWith("tvos") + } ?: false + +private fun Iterable.selectFirstAvailableTarget( + @NonNls vararg targetsByPriority: String +): CompilationWithDependencies { + for (target in targetsByPriority) { + val result = firstOrNull { it.konanTarget == target } + if (result != null) { + return result + } + } + return first() +} + + +private fun PopulateModuleDependenciesContext.getPropagatedPlatformDependencies( + sourceSet: KotlinSourceSet +): Set { + if (!mppModel.extraFeatures.isHMPPEnabled) { + return emptySet() + } + + return getPropagatedPlatformDependencies(mppModel, sourceSet) +} + + +/** + * Source sets sharing code between JVM and Android are the only intermediate source sets that + * can effectively consume a dependency's platform artifact. + * When a library only offers a JVM variant, then Android and JVM consume this variant of the library. + * + * This will be replaced later on by [KT-43450](https://youtrack.jetbrains.com/issue/KT-43450) + * + * @return all dependencies being present across given JVM and Android compilations that this [sourceSet] can also participates in. + */ +private fun getPropagatedPlatformDependencies( + mppModel: KotlinMPPGradleModel, + sourceSet: KotlinSourceSet, +): Set { + if ( + sourceSet.actualPlatforms.platforms.sorted() == listOf(KotlinPlatform.JVM, KotlinPlatform.ANDROID).sorted() + ) { + return mppModel.targets + .filter { target -> target.platform == KotlinPlatform.JVM || target.platform == KotlinPlatform.ANDROID } + .flatMap { target -> target.compilations } + .filter { compilation -> compilation.dependsOnSourceSet(mppModel, sourceSet) } + .map { targetCompilations -> targetCompilations.dependencies.mapNotNull(mppModel.dependencyMap::get).toSet() } + .reduceOrNull { acc, dependencies -> acc.intersect(dependencies) }.orEmpty() + } + + return emptySet() +} + + +// TODO: Move this maybe to another semantic part of KotlinMPPGradleProjectResolver? +private fun KotlinMPPGradleProjectResolver.Companion.populateSourceSetInfos( + context: PopulateModuleDependenciesContext, + closedSourceSetGraph: Graph, + sourceSet: KotlinSourceSet +) = with(context) { + val isAndroid = delegateToAndroidPlugin(sourceSet) + val fromDataNode = if (isAndroid) ideModule + else getSiblingKotlinModuleData(sourceSet, gradleModule, ideModule, resolverCtx) ?: return + + val dependeeSourceSets = closedSourceSetGraph.successors(sourceSet) + val sourceSetInfos = if (isAndroid) { + ideModule.kotlinAndroidSourceSets?.filter { + (it.kotlinModule as? KotlinCompilation)?.declaredSourceSets?.contains(sourceSet) ?: false + } ?: emptyList() + } else { + listOfNotNull(fromDataNode.kotlinSourceSet) + } + for (sourceSetInfo in sourceSetInfos) { + if (sourceSetInfo.kotlinModule is KotlinCompilation) { + val selfName = sourceSetInfo.kotlinModule.fullName() + sourceSetInfo.addSourceSets(dependeeSourceSets, selfName, gradleModule, resolverCtx) + } + } +} + +inline fun DataNode<*>.cast(): DataNode { + if (data !is T) { + throw ClassCastException("DataNode<${data.javaClass.canonicalName}> cannot be cast to DataNode<${T::class.java.canonicalName}>") + } + @Suppress("UNCHECKED_CAST") + return this as DataNode +} \ No newline at end of file diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/utils/KotlinModuleUtils.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/utils/KotlinModuleUtils.kt new file mode 100644 index 00000000000..8e134595d64 --- /dev/null +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/utils/KotlinModuleUtils.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2010-2021 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.idea.configuration.utils + +import org.gradle.tooling.model.idea.IdeaModule +import org.jetbrains.kotlin.gradle.KotlinCompilation +import org.jetbrains.kotlin.gradle.KotlinModule +import org.jetbrains.kotlin.gradle.compilationFullName +import org.jetbrains.kotlin.idea.configuration.KotlinMPPGradleProjectResolver +import org.jetbrains.plugins.gradle.service.project.ProjectResolverContext + +internal fun KotlinModule.fullName(simpleName: String = name) = when (this) { + is KotlinCompilation -> compilationFullName(simpleName, disambiguationClassifier) + else -> simpleName +} + +internal fun KotlinMPPGradleProjectResolver.Companion.getKotlinModuleId( + gradleModule: IdeaModule, kotlinModule: KotlinModule, resolverCtx: ProjectResolverContext +) = getGradleModuleQualifiedName(resolverCtx, gradleModule, kotlinModule.fullName()) \ No newline at end of file diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/utils/KotlinSourceSetGraphUtil.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/utils/KotlinSourceSetGraphUtil.kt new file mode 100644 index 00000000000..d6048a76f39 --- /dev/null +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/utils/KotlinSourceSetGraphUtil.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2010-2021 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("UnstableApiUsage") + +package org.jetbrains.kotlin.idea.configuration.utils + +import com.google.common.graph.* +import org.jetbrains.kotlin.gradle.KotlinMPPGradleModel +import org.jetbrains.kotlin.gradle.KotlinPlatform +import org.jetbrains.kotlin.gradle.KotlinSourceSet + +internal fun createSourceSetVisibilityGraph(model: KotlinMPPGradleModel): ImmutableGraph { + val graph = createSourceSetDependsOnGraph(model) + graph.putInferredTestToProductionEdges() + return graph.immutable +} + +internal fun createSourceSetDependsOnGraph(model: KotlinMPPGradleModel): MutableGraph { + return createSourceSetDependsOnGraph(model.sourceSetsByName) +} + +internal fun createSourceSetDependsOnGraph( + sourceSetsByName: Map +): MutableGraph { + val graph = GraphBuilder.directed().build() + val sourceSets = sourceSetsByName.values.toSet() + + for (sourceSet in sourceSets) { + graph.addNode(sourceSet) + val dependsOnSourceSets = getFixedDependsOnSourceSets(sourceSetsByName, sourceSet) + for (dependsOnSourceSet in dependsOnSourceSets) { + graph.addNode(dependsOnSourceSet) + graph.putEdge(sourceSet, dependsOnSourceSet) + } + } + + return graph +} + +internal fun MutableGraph.putInferredTestToProductionEdges() { + val sourceSets = this.nodes() + for (sourceSet in sourceSets) { + if (sourceSet.isTestModule) { + @OptIn(UnsafeTestSourceSetHeuristicApi::class) + val predictedMainSourceSetName = predictedProductionSourceSetName(sourceSet.name) + val predictedMainSourceSet = sourceSets.firstOrNull { it.name == predictedMainSourceSetName } ?: continue + putEdge(sourceSet, predictedMainSourceSet) + } + } +} + +private fun getFixedDependsOnSourceSets( + sourceSetsByName: Map, sourceSet: KotlinSourceSet +): Set { + /* + Workaround for older Kotlin Gradle Plugin versions that did not explicitly declare a dependsOn relation + from a Kotlin source set to "commonMain" + (Can probably be dropped in Kotlin 1.5) + */ + val implicitDependsOnEdgeForAndroid = if ( + sourceSet.actualPlatforms.contains(KotlinPlatform.ANDROID) && sourceSet.declaredDependsOnSourceSets.isEmpty() + ) { + val commonSourceSetName = if (sourceSet.isTestModule) KotlinSourceSet.COMMON_TEST_SOURCE_SET_NAME + else KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME + listOfNotNull(sourceSetsByName[commonSourceSetName]) + } else emptyList() + + return sourceSet.declaredDependsOnSourceSets.map(sourceSetsByName::getValue) + .plus(implicitDependsOnEdgeForAndroid) + /* + Gracefully filter out source sets that declare a dependency on themselves. + This also fixes KTIJ-1025 + */ + .filter { dependencySourceSet -> dependencySourceSet != sourceSet } + .toSet() +} + +/** + * @see Graphs.transitiveClosure + */ +internal val Graph.transitiveClosure: Graph get() = Graphs.transitiveClosure(this) + +/** + * @see ImmutableGraph.copyOf + */ +internal val Graph.immutable: ImmutableGraph get() = ImmutableGraph.copyOf(this) \ No newline at end of file diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/utils/TestSourceSetUtils.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/utils/TestSourceSetUtils.kt new file mode 100644 index 00000000000..ea178a06145 --- /dev/null +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/utils/TestSourceSetUtils.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2010-2021 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.idea.configuration.utils + +// TODO: Replace heuristic with proper import! +@RequiresOptIn(level = RequiresOptIn.Level.ERROR) +annotation class UnsafeTestSourceSetHeuristicApi + + +@UnsafeTestSourceSetHeuristicApi +internal fun isProbablyTestSourceSet(name: String): Boolean { + return name.endsWith("Test") || name == "test" +} + +@UnsafeTestSourceSetHeuristicApi +internal fun isProbablyProductionSourceSet(name: String): Boolean { + return !isProbablyTestSourceSet(name) +} + +@UnsafeTestSourceSetHeuristicApi +internal fun predictedProductionSourceSetName(testSourceSetName: String): String { + /* + This implementation uses a lot of implicit knowledge and should be replaced. + This implicit knowledge is/was used at other places as well that should also + use an explicit API once available. + + See: + https://github.com/JetBrains/intellij-community/blob/c05e8d6de8d1c690eb99c2fff8629c0de373c48f/plugins/gradle/src/org/jetbrains/plugins/gradle/service/project/CommonGradleProjectResolverExtension.java#L153 + https://github.com/JetBrains/intellij-kotlin/blob/fe6ee9dfb42ad1cdd9e55449e2481125f1631133/gradle/gradle-idea/src/org/jetbrains/kotlin/idea/configuration/KotlinMPPGradleProjectResolver.kt#L424 + https://github.com/JetBrains/intellij-kotlin/blob/fe6ee9dfb42ad1cdd9e55449e2481125f1631133/gradle/gradle-idea/src/org/jetbrains/kotlin/idea/configuration/KotlinMPPGradleProjectResolver.kt#L627 + */ + if (testSourceSetName.endsWith("Test")) { + return testSourceSetName.removeSuffix("Test") + "Main" + } + if (testSourceSetName == "test") { + return "main" + } + return testSourceSetName + "Main" +} \ No newline at end of file diff --git a/idea/idea-gradle/tests/org/jetbrains/kotlin/idea/configuration/utils/KotlinSourceSetGraphUtilTest.kt b/idea/idea-gradle/tests/org/jetbrains/kotlin/idea/configuration/utils/KotlinSourceSetGraphUtilTest.kt new file mode 100644 index 00000000000..6bc17e29086 --- /dev/null +++ b/idea/idea-gradle/tests/org/jetbrains/kotlin/idea/configuration/utils/KotlinSourceSetGraphUtilTest.kt @@ -0,0 +1,167 @@ +/* + * Copyright 2010-2021 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.idea.configuration.utils + +import com.google.common.graph.EndpointPair +import com.google.common.graph.EndpointPair.ordered +import com.google.common.graph.Graph +import com.google.common.graph.GraphBuilder +import org.jetbrains.kotlin.gradle.* +import org.jetbrains.kotlin.gradle.KotlinSourceSet.Companion.COMMON_MAIN_SOURCE_SET_NAME +import org.jetbrains.kotlin.gradle.KotlinSourceSet.Companion.COMMON_TEST_SOURCE_SET_NAME +import org.junit.Test +import kotlin.test.assertEquals + +class KotlinSourceSetGraphUtilTest { + + @Test + fun `simple layout`() { + val sourceSetsByName = mapOf( + createKotlinSourceSetPair(COMMON_MAIN_SOURCE_SET_NAME), + createKotlinSourceSetPair(COMMON_TEST_SOURCE_SET_NAME), + createKotlinSourceSetPair("jvmMain", setOf(COMMON_MAIN_SOURCE_SET_NAME)), + createKotlinSourceSetPair("jvmTest", setOf(COMMON_TEST_SOURCE_SET_NAME)) + ) + val dependsOnGraph = createSourceSetDependsOnGraph(sourceSetsByName) + + assertEquals( + listOf( + ordered("jvmMain", COMMON_MAIN_SOURCE_SET_NAME), + ordered("jvmTest", COMMON_TEST_SOURCE_SET_NAME) + ).sorted(), + dependsOnGraph.nameEdges().sorted() + ) + } + + @Test + fun `hierarchical layout`() { + val sourceSetsByName = mapOf( + createKotlinSourceSetPair(COMMON_MAIN_SOURCE_SET_NAME), + createKotlinSourceSetPair(COMMON_TEST_SOURCE_SET_NAME), + + createKotlinSourceSetPair("jvmAndJsMain", setOf(COMMON_MAIN_SOURCE_SET_NAME)), + createKotlinSourceSetPair("jvmAndJsTest", setOf(COMMON_TEST_SOURCE_SET_NAME)), + + createKotlinSourceSetPair("jvmMain", setOf("jvmAndJsMain")), + createKotlinSourceSetPair("jvmTest", setOf("jvmAndJsTest")), + + createKotlinSourceSetPair("jsMain", setOf("jvmAndJsMain")), + createKotlinSourceSetPair("jsTest", setOf("jvmAndJsTest")), + ) + + val dependsOnGraph = createSourceSetDependsOnGraph(sourceSetsByName) + + assertEquals( + listOf( + ordered("jvmMain", "jvmAndJsMain"), + ordered("jvmTest", "jvmAndJsTest"), + ordered("jsMain", "jvmAndJsMain"), + ordered("jsTest", "jvmAndJsTest"), + ordered("jvmAndJsMain", COMMON_MAIN_SOURCE_SET_NAME), + ordered("jvmAndJsTest", COMMON_TEST_SOURCE_SET_NAME) + ).sorted(), + dependsOnGraph.nameEdges().sorted() + ) + } + + @Test + fun `source set depending on multiple other source sets`() { + val sourceSetsByName = mapOf( + createKotlinSourceSetPair("common1"), + createKotlinSourceSetPair("common2"), + createKotlinSourceSetPair("consumerA", setOf("common1", "common2")), + createKotlinSourceSetPair("consumerB", setOf("common1", "consumerA")) + ) + + val dependsOnGraph = createSourceSetDependsOnGraph(sourceSetsByName) + + + assertEquals( + listOf( + ordered("consumerA", "common1"), + ordered("consumerA", "common2"), + ordered("consumerB", "common1"), + ordered("consumerB", "consumerA") + ).sorted(), + dependsOnGraph.nameEdges().sorted() + ) + } + + @Test + fun `infer default test to production edges`() { + val sourceSetsByName = mapOf( + createKotlinSourceSetPair(COMMON_MAIN_SOURCE_SET_NAME), + createKotlinSourceSetPair(COMMON_TEST_SOURCE_SET_NAME, isTestModule = true), + createKotlinSourceSetPair("main"), + createKotlinSourceSetPair("test", isTestModule = true), + createKotlinSourceSetPair("abcMain"), + createKotlinSourceSetPair("abcTest", isTestModule = true) + ) + + val graph = GraphBuilder.directed().build() + sourceSetsByName.values.forEach { sourceSet -> graph.addNode(sourceSet) } + graph.putInferredTestToProductionEdges() + + assertEquals( + listOf( + ordered(COMMON_TEST_SOURCE_SET_NAME, COMMON_MAIN_SOURCE_SET_NAME), + ordered("test", "main"), + ordered("abcTest", "abcMain") + ).sorted(), + graph.nameEdges().sorted() + ) + } + + @Test + fun `depends on graph will automatically infer missing dependsOn edges for Android source sets`() { + val sourceSetsByName = mapOf( + createKotlinSourceSetPair(COMMON_MAIN_SOURCE_SET_NAME), + createKotlinSourceSetPair(COMMON_TEST_SOURCE_SET_NAME, isTestModule = true), + createKotlinSourceSetPair("androidMain", platforms = setOf(KotlinPlatform.ANDROID)), + createKotlinSourceSetPair("androidTest", platforms = setOf(KotlinPlatform.ANDROID), isTestModule = true) + ) + + assertEquals( + listOf( + ordered("androidMain", COMMON_MAIN_SOURCE_SET_NAME), + ordered("androidTest", COMMON_TEST_SOURCE_SET_NAME) + ).sorted(), + createSourceSetDependsOnGraph(sourceSetsByName).nameEdges().sorted() + ) + } +} + +private fun createKotlinSourceSetPair( + name: String, + declaredDependsOnSourceSets: Set = emptySet(), + allDependsOnSourceSets: Set = declaredDependsOnSourceSets, + platforms: Set = emptySet(), + isTestModule: Boolean = false, +): Pair = name to KotlinSourceSetImpl( + name = name, + languageSettings = KotlinLanguageSettingsImpl( + languageVersion = null, + apiVersion = null, + isProgressiveMode = false, + enabledLanguageFeatures = emptySet(), + experimentalAnnotationsInUse = emptySet(), + compilerPluginArguments = emptyArray(), + compilerPluginClasspath = emptySet(), + freeCompilerArgs = emptyArray() + ), + sourceDirs = emptySet(), + resourceDirs = emptySet(), + dependencies = emptyArray(), + declaredDependsOnSourceSets = declaredDependsOnSourceSets, + allDependsOnSourceSets = allDependsOnSourceSets, + defaultActualPlatforms = KotlinPlatformContainerImpl().apply { pushPlatforms(platforms) }, + defaultIsTestModule = isTestModule +) + +private fun Graph.nameEdges() = edges() + .map { edge -> ordered(edge.source().name, edge.target().name) } + +private fun Iterable>.sorted() = this.sortedBy { it.source() + " -> " + it.target() } \ No newline at end of file diff --git a/idea/idea-gradle/tests/org/jetbrains/kotlin/idea/configuration/utils/TestSourceSetUtilsTest.kt b/idea/idea-gradle/tests/org/jetbrains/kotlin/idea/configuration/utils/TestSourceSetUtilsTest.kt new file mode 100644 index 00000000000..702db57b55b --- /dev/null +++ b/idea/idea-gradle/tests/org/jetbrains/kotlin/idea/configuration/utils/TestSourceSetUtilsTest.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2010-2021 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.idea.configuration.utils + +import org.junit.Test +import kotlin.test.assertEquals + +@OptIn(UnsafeTestSourceSetHeuristicApi::class) +class TestSourceSetUtilsTest { + + @Test + fun `test predicted production source set name for various examples`() { + assertEquals("commonMain", predictedProductionSourceSetName("commonTest")) + assertEquals("main", predictedProductionSourceSetName("test")) + assertEquals("abcMain", predictedProductionSourceSetName("abc")) + assertEquals("abctestMain", predictedProductionSourceSetName("abctest")) + } +} \ No newline at end of file diff --git a/idea/kotlin-gradle-tooling/build.gradle.kts b/idea/kotlin-gradle-tooling/build.gradle.kts index 06764e4de8f..1e96bc73c5b 100644 --- a/idea/kotlin-gradle-tooling/build.gradle.kts +++ b/idea/kotlin-gradle-tooling/build.gradle.kts @@ -12,11 +12,17 @@ dependencies { compileOnly(intellijPluginDep("gradle")) compileOnly(intellijDep()) { includeJars("slf4j-api-1.7.25") } + + testImplementation(commonDep("junit:junit")) + testImplementation(projectTests(":compiler:tests-common")) + testCompileOnly(projectTests(":idea:idea-test-framework")) + testCompileOnly(intellijDep()) + } sourceSets { "main" { projectDefault() } - "test" {} + "test" { projectDefault() } } tasks.withType> { diff --git a/idea/kotlin-gradle-tooling/src/KotlinMPPGradleModelExtensions.kt b/idea/kotlin-gradle-tooling/src/KotlinMPPGradleModelExtensions.kt new file mode 100644 index 00000000000..a48a627de16 --- /dev/null +++ b/idea/kotlin-gradle-tooling/src/KotlinMPPGradleModelExtensions.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2010-2021 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 + +@ExperimentalGradleToolingApi +fun KotlinMPPGradleModel.getCompilations(sourceSet: KotlinSourceSet): Set { + return targets.flatMap { target -> target.compilations } + .filter { compilation -> compilationDependsOnSourceSet(compilation, sourceSet) } + .toSet() +} + +@ExperimentalGradleToolingApi +fun KotlinMPPGradleModel.compilationDependsOnSourceSet( + compilation: KotlinCompilation, sourceSet: KotlinSourceSet +): Boolean { + return compilation.declaredSourceSets.any { sourceSetInCompilation -> + sourceSetInCompilation == sourceSet || sourceSetInCompilation.isDependsOn(this, sourceSet) + } +} diff --git a/idea/kotlin-gradle-tooling/src/KotlinMPPGradleModelImpl.kt b/idea/kotlin-gradle-tooling/src/KotlinMPPGradleModelImpl.kt index d235d54e3a8..a9cedb9a2d3 100644 --- a/idea/kotlin-gradle-tooling/src/KotlinMPPGradleModelImpl.kt +++ b/idea/kotlin-gradle-tooling/src/KotlinMPPGradleModelImpl.kt @@ -16,30 +16,25 @@ class KotlinSourceSetProto( private val dependencies: () -> Array, val dependsOnSourceSets: Set ) { - fun buildKotlinSourceSetImpl( doBuildDependencies: Boolean, allSourceSetsProtosByNames: Map, - dependsOnCache: HashMap> ) = KotlinSourceSetImpl( - name, - languageSettings, - sourceDirs, - resourceDirs, - if (doBuildDependencies) dependencies.invoke() else emptyArray(), - calculateDependsOnClosure(this, allSourceSetsProtosByNames, dependsOnCache) + name = name, + languageSettings = languageSettings, + sourceDirs = sourceDirs, + resourceDirs = resourceDirs, + dependencies = if (doBuildDependencies) dependencies.invoke() else emptyArray(), + declaredDependsOnSourceSets = dependsOnSourceSets, + allDependsOnSourceSets = allDependsOnSourceSets(allSourceSetsProtosByNames) ) +} - private fun calculateDependsOnClosure( - currentSourceSetProto: KotlinSourceSetProto, - sourceSetsMap: Map, - cache: MutableMap> - ): Set { - return cache.computeIfAbsent(currentSourceSetProto.name) { - currentSourceSetProto.dependsOnSourceSets.flatMap { name -> - val nextSourceSet = sourceSetsMap[name] ?: error("Source set $name is not found in map $sourceSetsMap") - calculateDependsOnClosure(nextSourceSet, sourceSetsMap, cache).union(setOf(name)) - }.toSet() +fun KotlinSourceSetProto.allDependsOnSourceSets(sourceSetsByName: Map): Set { + return mutableSetOf().apply { + addAll(dependsOnSourceSets) + dependsOnSourceSets.map(sourceSetsByName::getValue).forEach { dependsOnSourceSet -> + addAll(dependsOnSourceSet.allDependsOnSourceSets(sourceSetsByName)) } } } @@ -50,29 +45,41 @@ class KotlinSourceSetImpl( override val sourceDirs: Set, override val resourceDirs: Set, override val dependencies: Array, - override val dependsOnSourceSets: Set, - val defaultPlatform: KotlinPlatformContainerImpl = KotlinPlatformContainerImpl(), + override val declaredDependsOnSourceSets: Set, + @Suppress("OverridingDeprecatedMember") + override val allDependsOnSourceSets: Set, + defaultActualPlatforms: KotlinPlatformContainerImpl = KotlinPlatformContainerImpl(), + defaultIsTestModule: Boolean = false ) : KotlinSourceSet { - constructor(kotlinSourceSet: KotlinSourceSet, cloningCache: MutableMap) : this( - kotlinSourceSet.name, - KotlinLanguageSettingsImpl(kotlinSourceSet.languageSettings), - HashSet(kotlinSourceSet.sourceDirs), - HashSet(kotlinSourceSet.resourceDirs), - kotlinSourceSet.dependencies, - HashSet(kotlinSourceSet.dependsOnSourceSets), - KotlinPlatformContainerImpl(kotlinSourceSet.actualPlatforms) + @Suppress("DEPRECATION") + constructor(kotlinSourceSet: KotlinSourceSet) : this( + name = kotlinSourceSet.name, + languageSettings = KotlinLanguageSettingsImpl(kotlinSourceSet.languageSettings), + sourceDirs = HashSet(kotlinSourceSet.sourceDirs), + resourceDirs = HashSet(kotlinSourceSet.resourceDirs), + dependencies = kotlinSourceSet.dependencies.clone(), + declaredDependsOnSourceSets = HashSet(kotlinSourceSet.declaredDependsOnSourceSets), + allDependsOnSourceSets = HashSet(kotlinSourceSet.allDependsOnSourceSets), + defaultActualPlatforms = KotlinPlatformContainerImpl(kotlinSourceSet.actualPlatforms) ) { this.isTestModule = kotlinSourceSet.isTestModule } - override var actualPlatforms: KotlinPlatformContainer = defaultPlatform + override var actualPlatforms: KotlinPlatformContainer = defaultActualPlatforms internal set - override var isTestModule: Boolean = false + override var isTestModule: Boolean = defaultIsTestModule internal set override fun toString() = name + + init { + @Suppress("DEPRECATION") + require(allDependsOnSourceSets.containsAll(declaredDependsOnSourceSets)) { + "Inconsistent source set dependencies: 'allDependsOnSourceSets' is expected to contain all 'declaredDependsOnSourceSets'" + } + } } data class KotlinLanguageSettingsImpl( @@ -128,7 +135,7 @@ data class KotlinNativeCompilationExtensionsImpl( data class KotlinCompilationImpl( override val name: String, override val allSourceSets: Collection, - override val defaultSourceSets: Collection, + override val declaredSourceSets: Collection, override val dependencies: Array, override val output: KotlinCompilationOutput, override val arguments: KotlinCompilationArguments, @@ -139,15 +146,15 @@ data class KotlinCompilationImpl( // create deep copy constructor(kotlinCompilation: KotlinCompilation, cloningCache: MutableMap) : this( - kotlinCompilation.name, - cloneSourceSetsWithCaching(kotlinCompilation.allSourceSets, cloningCache), - cloneSourceSetsWithCaching(kotlinCompilation.defaultSourceSets, cloningCache), - kotlinCompilation.dependencies, - KotlinCompilationOutputImpl(kotlinCompilation.output), - KotlinCompilationArgumentsImpl(kotlinCompilation.arguments), - kotlinCompilation.dependencyClasspath, - KotlinTaskPropertiesImpl(kotlinCompilation.kotlinTaskProperties), - kotlinCompilation.nativeExtensions?.let(::KotlinNativeCompilationExtensionsImpl) + name = kotlinCompilation.name, + declaredSourceSets = cloneSourceSetsWithCaching(kotlinCompilation.declaredSourceSets, cloningCache), + allSourceSets = cloneSourceSetsWithCaching(kotlinCompilation.allSourceSets, cloningCache), + dependencies = kotlinCompilation.dependencies, + output = KotlinCompilationOutputImpl(kotlinCompilation.output), + arguments = KotlinCompilationArgumentsImpl(kotlinCompilation.arguments), + dependencyClasspath = kotlinCompilation.dependencyClasspath, + kotlinTaskProperties = KotlinTaskPropertiesImpl(kotlinCompilation.kotlinTaskProperties), + nativeExtensions = kotlinCompilation.nativeExtensions?.let(::KotlinNativeCompilationExtensionsImpl) ) { disambiguationClassifier = kotlinCompilation.disambiguationClassifier platform = kotlinCompilation.platform @@ -158,7 +165,7 @@ data class KotlinCompilationImpl( override lateinit var platform: KotlinPlatform internal set - + // TODO: Logic like this is duplicated *and different* override val isTestModule: Boolean get() = name == KotlinCompilation.TEST_COMPILATION_NAME || platform == KotlinPlatform.ANDROID && name.contains("Test") @@ -171,7 +178,7 @@ data class KotlinCompilationImpl( cloningCache: MutableMap ): List = sourceSets.map { initialSourceSet -> - (cloningCache[initialSourceSet] as? KotlinSourceSet) ?: KotlinSourceSetImpl(initialSourceSet, cloningCache).also { + (cloningCache[initialSourceSet] as? KotlinSourceSet) ?: KotlinSourceSetImpl(initialSourceSet).also { cloningCache[initialSourceSet] = it } } @@ -250,7 +257,7 @@ data class ExtraFeaturesImpl( ) : ExtraFeatures data class KotlinMPPGradleModelImpl( - override val sourceSets: Map, + override val sourceSetsByName: Map, override val targets: Collection, override val extraFeatures: ExtraFeatures, override val kotlinNativeHome: String, @@ -259,11 +266,9 @@ data class KotlinMPPGradleModelImpl( ) : KotlinMPPGradleModel { constructor(mppModel: KotlinMPPGradleModel, cloningCache: MutableMap) : this( - mppModel.sourceSets.mapValues { initialSourceSet -> - (cloningCache[initialSourceSet] as? KotlinSourceSet) ?: KotlinSourceSetImpl( - initialSourceSet.value, - cloningCache - ).also { cloningCache[initialSourceSet] = it } + mppModel.sourceSetsByName.mapValues { initialSourceSet -> + (cloningCache[initialSourceSet] as? KotlinSourceSet) ?: KotlinSourceSetImpl(initialSourceSet.value) + .also { cloningCache[initialSourceSet] = it } }, mppModel.targets.map { initialTarget -> (cloningCache[initialTarget] as? KotlinTarget) ?: KotlinTargetImpl(initialTarget, cloningCache).also { @@ -289,20 +294,20 @@ class KotlinPlatformContainerImpl() : KotlinPlatformContainer { get() = myPlatforms != null constructor(platform: KotlinPlatformContainer) : this() { - myPlatforms = HashSet(platform.platforms) + myPlatforms = HashSet(platform.platforms) } - override val platforms: Collection + override val platforms: Set get() = myPlatforms ?: defaultCommonPlatform override fun supports(simplePlatform: KotlinPlatform): Boolean = platforms.contains(simplePlatform) - override fun addSimplePlatforms(platforms: Collection) { - (myPlatforms ?: HashSet().apply { myPlatforms = this }).let { - it.addAll(platforms) - if (it.contains(KotlinPlatform.COMMON)) { - it.clear() - it.addAll(defaultCommonPlatform) + override fun pushPlatforms(platforms: Iterable) { + myPlatforms = (myPlatforms ?: LinkedHashSet()).apply { + addAll(platforms) + if (contains(KotlinPlatform.COMMON)) { + clear() + addAll(defaultCommonPlatform) } } } diff --git a/idea/kotlin-gradle-tooling/test/KotlinMppGradleModelExtensionsTest.kt b/idea/kotlin-gradle-tooling/test/KotlinMppGradleModelExtensionsTest.kt new file mode 100644 index 00000000000..ba243641d5f --- /dev/null +++ b/idea/kotlin-gradle-tooling/test/KotlinMppGradleModelExtensionsTest.kt @@ -0,0 +1,307 @@ +/* + * Copyright 2010-2021 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 + +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +@OptIn(ExperimentalGradleToolingApi::class) +class KotlinMppGradleModelExtensionsTest { + + @Test + fun resolveDeclaredDependsOnSourceSets() { + val commonMain = createKotlinSourceSet("commonMain") + val appleMain = createKotlinSourceSet("appleMain", declaredDependsOnSourceSets = setOf("commonMain")) + val macosMain = createKotlinSourceSet("macosMain", declaredDependsOnSourceSets = setOf("appleMain")) + val iosMain = createKotlinSourceSet("iosMain", declaredDependsOnSourceSets = setOf("appleMain", "commonMain")) + + val model = createKotlinMPPGradleModel( + sourceSets = setOf(commonMain, appleMain, macosMain, iosMain) + ) + + assertEquals( + emptySet(), model.resolveDeclaredDependsOnSourceSets(commonMain), + "Expected no declared dependency source sets for commonMain" + ) + + assertEquals( + setOf(commonMain), model.resolveDeclaredDependsOnSourceSets(appleMain), + "Expected only declared dependency for 'appleMain'" + ) + + assertEquals( + setOf(appleMain, commonMain), model.resolveDeclaredDependsOnSourceSets(iosMain), + "Expected only declared dependency for 'iosMain'" + ) + } + + @Test + fun `resolveDeclaredDependsOnSourceSets is graceful for missing source sets`() { + val commonMain = createKotlinSourceSet("commonMain") + val macosMain = createKotlinSourceSet("macosMain", declaredDependsOnSourceSets = setOf("commonMain", "missing")) + val model = createKotlinMPPGradleModel(sourceSets = setOf(commonMain, macosMain)) + + assertEquals( + setOf(commonMain), model.resolveDeclaredDependsOnSourceSets(macosMain), + "Expected declaredDependencySourceSets to ignore missing dependency source set" + ) + } + + @Test + fun resolveAllDependsOnSourceSets() { + val commonMain = createKotlinSourceSet("commonMain") + val appleMain = createKotlinSourceSet("appleMain", declaredDependsOnSourceSets = setOf("commonMain")) + val x64Main = createKotlinSourceSet("x64Main", declaredDependsOnSourceSets = setOf("commonMain")) + val macosMain = createKotlinSourceSet("macosMain", declaredDependsOnSourceSets = setOf("appleMain")) + val macosX64Main = createKotlinSourceSet("macosX64Main", declaredDependsOnSourceSets = setOf("appleMain", "x64Main")) + val macosArm64Main = createKotlinSourceSet("macosArm64Main", declaredDependsOnSourceSets = setOf("appleMain")) + + val model = createKotlinMPPGradleModel(sourceSets = setOf(commonMain, appleMain, x64Main, macosMain, macosX64Main, macosArm64Main)) + + assertEquals( + setOf(appleMain, x64Main, commonMain), + model.resolveAllDependsOnSourceSets(macosX64Main), + ) + + assertEquals( + setOf(appleMain, x64Main, commonMain).sortedBy { it.name }, + model.resolveAllDependsOnSourceSets(macosX64Main).sortedBy { it.name }.toList(), + ) + + assertEquals( + setOf(appleMain, commonMain), + model.resolveAllDependsOnSourceSets(macosArm64Main).toSet(), + ) + + assertEquals( + setOf(commonMain), + model.resolveAllDependsOnSourceSets(appleMain).toSet(), + "Expected only 'commonMain' for 'appleMain'" + ) + + assertEquals( + emptySet(), model.resolveAllDependsOnSourceSets(commonMain).toSet(), + "Expected empty set for 'commonMain'" + ) + } + + @Test + fun `resolving source sets with self references or loops`() { + val a = createKotlinSourceSet("a", declaredDependsOnSourceSets = setOf("")) + val b = createKotlinSourceSet("b", declaredDependsOnSourceSets = setOf("a", "c")) // loop d -> c -> b -> c + val c = createKotlinSourceSet("c", declaredDependsOnSourceSets = setOf("b", "c")) // self reference c -> c + val d = createKotlinSourceSet("d", declaredDependsOnSourceSets = setOf("c")) + + val model = createKotlinMPPGradleModel(sourceSets = setOf(a, b, c, d)) + + assertEquals( + setOf(a, b, c).sortedBy { it.name }, + model.resolveAllDependsOnSourceSets(d).sortedBy { it.name }, + "Expected dependency loop to be handled gracefully" + ) + + assertEquals( + setOf(a, b, c).sortedBy { it.name }, + model.resolveAllDependsOnSourceSets(c).sortedBy { it.name }, + "Expected self dependency to be resolved for 'resolveAllDependsOnSourceSets'" + ) + + assertEquals( + setOf(b, c).sortedBy { it.name }, + model.resolveDeclaredDependsOnSourceSets(c).sortedBy { it.name }, + "Expected self dependency to be resolved by 'resolveDeclaredDependsOnSourceSets'" + ) + } + + @Test + fun isDependsOn() { + val commonMain = createKotlinSourceSet("commonMain") + val appleMain = createKotlinSourceSet("appleMain", declaredDependsOnSourceSets = setOf("commonMain")) + val x64Main = createKotlinSourceSet("x64Main", declaredDependsOnSourceSets = setOf("commonMain")) + val macosX64Main = createKotlinSourceSet("macosX64Main", declaredDependsOnSourceSets = setOf("appleMain", "x64Main")) + val macosArm64Main = createKotlinSourceSet("macosArm64Main", declaredDependsOnSourceSets = setOf("appleMain")) + + + val model = createKotlinMPPGradleModel(sourceSets = setOf(commonMain, appleMain, x64Main, macosX64Main)) + + assertTrue( + model.isDependsOn(from = appleMain, to = commonMain), + "Expected isDependsOn from appleMain to commonMain" + ) + + assertTrue( + model.isDependsOn(from = x64Main, to = commonMain), + "Expected isDependsOn from x64Main to commonMain" + ) + + assertTrue( + model.isDependsOn(from = macosX64Main, to = commonMain), + "Expected isDependsOn from macosX64Main to commonMain" + ) + + assertTrue( + model.isDependsOn(from = macosX64Main, to = appleMain), + "Expected isDependsOn from macosX64Main to appleMain" + ) + + assertTrue( + model.isDependsOn(from = macosX64Main, to = commonMain), + "Expected isDependsOn from macosX64Main to commonMain" + ) + + assertTrue( + model.isDependsOn(from = macosX64Main, to = appleMain), + "Expected isDependsOn from macosX64Main to appleMain" + ) + + assertTrue( + model.isDependsOn(from = macosX64Main, to = x64Main), + "Expected isDependsOn from macosX64Main to x64Main" + ) + + assertTrue( + model.isDependsOn(from = macosArm64Main, to = commonMain), + "Expected isDependsOn from macosArm64Main to commonMain" + ) + + assertTrue( + model.isDependsOn(from = macosArm64Main, to = appleMain), + "Expected isDependsOn from macosArm64Main to appleMain" + ) + + assertFalse( + model.isDependsOn(from = macosArm64Main, to = x64Main), + "Expected false isDependsOn from macosArm64 to x64Main" + ) + + for (sourceSet in model.sourceSetsByName.values) { + assertFalse( + model.isDependsOn(from = commonMain, to = sourceSet), + "Expected false isDependsOn from commonMain to ${sourceSet.name}" + ) + + assertFalse( + model.isDependsOn(from = sourceSet, to = sourceSet), + "Expected false isDependsOn for same source set" + ) + + assertFalse( + sourceSet.isDependsOn(model, sourceSet), + "Expected false isDependsOn for same source set" + ) + } + } + + @Test + fun compilationDependsOnSourceSet() { + val commonMain = createKotlinSourceSet("commonMain") + val appleMain = createKotlinSourceSet("appleMain", declaredDependsOnSourceSets = setOf("commonMain")) + val macosMain = createKotlinSourceSet("macosMain", declaredDependsOnSourceSets = setOf("appleMain")) + val iosMain = createKotlinSourceSet("iosMain", declaredDependsOnSourceSets = setOf("appleMain")) + + val metadataCompilation = createKotlinCompilation(defaultSourceSets = setOf(commonMain)) + val macosMainCompilation = createKotlinCompilation(defaultSourceSets = setOf(macosMain)) + val iosMainCompilation = createKotlinCompilation(defaultSourceSets = setOf(iosMain)) + + val model = createKotlinMPPGradleModel(sourceSets = setOf(commonMain, appleMain, macosMain, iosMain)) + + assertTrue( + model.compilationDependsOnSourceSet(iosMainCompilation, iosMain), + "Expected iosMainCompilation depending on iosMain" + ) + + assertTrue( + model.compilationDependsOnSourceSet(iosMainCompilation, appleMain), + "Expected iosMainCompilation depending on appleMain" + ) + + assertTrue( + model.compilationDependsOnSourceSet(iosMainCompilation, commonMain), + "Expected iosMainCompilation depending on commonMain" + ) + + assertFalse( + model.compilationDependsOnSourceSet(iosMainCompilation, macosMain), + "Expected iosMainCompilation *not* depending on macosMain" + ) + + assertTrue( + model.compilationDependsOnSourceSet(macosMainCompilation, macosMain), + "Expected macosMainCompilation depending on macosMain" + ) + + assertTrue( + model.compilationDependsOnSourceSet(macosMainCompilation, appleMain), + "Expected macosMainCompilation depending on appleMain" + ) + + assertTrue( + model.compilationDependsOnSourceSet(macosMainCompilation, commonMain), + "Expected macosMainCompilation depending on commonMain" + ) + + assertFalse( + model.compilationDependsOnSourceSet(macosMainCompilation, iosMain), + "Expected macosMainCompilation *not* depending on iosMain" + ) + + assertTrue( + model.compilationDependsOnSourceSet(metadataCompilation, commonMain), + "Expected metadataCompilation depending on commonMain" + ) + + assertFalse( + model.compilationDependsOnSourceSet(metadataCompilation, appleMain), + "Expected metadataCompilation *not* depending on appleMain" + ) + + assertFalse( + model.compilationDependsOnSourceSet(metadataCompilation, macosMain), + "Expected metadataCompilation *not* depending on macosMain" + ) + + assertFalse( + model.compilationDependsOnSourceSet(metadataCompilation, iosMain), + "Expected metadataCompilation *not* depending on iosMain" + ) + } + + @Test + fun getCompilations() { + val commonMain = createKotlinSourceSet("commonMain") + val appleMain = createKotlinSourceSet("appleMain", declaredDependsOnSourceSets = setOf("commonMain")) + val macosMain = createKotlinSourceSet("macosMain", declaredDependsOnSourceSets = setOf("appleMain")) + + val metadataCompilation = createKotlinCompilation(defaultSourceSets = setOf(commonMain)) + val macosMainCompilation = createKotlinCompilation(defaultSourceSets = setOf(macosMain)) + + val metadataTarget = createKotlinTarget("metadata", compilations = setOf(metadataCompilation)) + val macosTarget = createKotlinTarget("macos", compilations = setOf(macosMainCompilation)) + + val model = createKotlinMPPGradleModel( + sourceSets = setOf(commonMain, appleMain, macosMain), + targets = setOf(metadataTarget, macosTarget) + ) + + assertEquals( + setOf(metadataCompilation, macosMainCompilation), + model.getCompilations(commonMain), + "Expected correct compilations for commonMain" + ) + + assertEquals( + setOf(macosMainCompilation), model.getCompilations(appleMain), + "Expected correct compilations for appleMain" + ) + + assertEquals( + setOf(macosMainCompilation), model.getCompilations(macosMain), + "Expected correct compilations for macosMain" + ) + } +} diff --git a/idea/kotlin-gradle-tooling/test/createKotlinMPPGradleModel.kt b/idea/kotlin-gradle-tooling/test/createKotlinMPPGradleModel.kt new file mode 100644 index 00000000000..ff4b74f8848 --- /dev/null +++ b/idea/kotlin-gradle-tooling/test/createKotlinMPPGradleModel.kt @@ -0,0 +1,124 @@ +/* + * Copyright 2010-2021 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 + +internal fun createKotlinMPPGradleModel( + dependencyMap: Map = emptyMap(), + sourceSets: Set = emptySet(), + targets: Iterable = emptyList(), + extraFeatures: ExtraFeatures = createExtraFeatures(), + kotlinNativeHome: String = "" + +): KotlinMPPGradleModelImpl { + return KotlinMPPGradleModelImpl( + dependencyMap = dependencyMap, + sourceSetsByName = sourceSets.associateBy { it.name }, + targets = targets.toList(), + extraFeatures = extraFeatures, + kotlinNativeHome = kotlinNativeHome + ) +} + +internal fun createExtraFeatures( + coroutinesState: String? = null, + isHmppEnabled: Boolean = false, + isNativeDependencyPropagationEnabled: Boolean = false +): ExtraFeaturesImpl { + return ExtraFeaturesImpl( + coroutinesState = coroutinesState, + isHMPPEnabled = isHmppEnabled, + isNativeDependencyPropagationEnabled = isNativeDependencyPropagationEnabled + ) +} + +internal fun createKotlinSourceSet( + name: String, + declaredDependsOnSourceSets: Set = emptySet(), + allDependsOnSourceSets: Set = declaredDependsOnSourceSets, + platforms: Set = emptySet(), +): KotlinSourceSetImpl = KotlinSourceSetImpl( + name = name, + languageSettings = KotlinLanguageSettingsImpl( + languageVersion = null, + apiVersion = null, + isProgressiveMode = false, + enabledLanguageFeatures = emptySet(), + experimentalAnnotationsInUse = emptySet(), + compilerPluginArguments = emptyArray(), + compilerPluginClasspath = emptySet(), + freeCompilerArgs = emptyArray() + ), + sourceDirs = emptySet(), + resourceDirs = emptySet(), + dependencies = emptyArray(), + declaredDependsOnSourceSets = declaredDependsOnSourceSets, + allDependsOnSourceSets = allDependsOnSourceSets, + defaultActualPlatforms = KotlinPlatformContainerImpl().apply { pushPlatforms(platforms) }, +) + +internal fun createKotlinCompilation( + name: String = "main", + defaultSourceSets: Set = emptySet(), + allSourceSets: Set = emptySet(), + dependencies: Iterable = emptyList(), + output: KotlinCompilationOutput = createKotlinCompilationOutput(), + arguments: KotlinCompilationArguments = createKotlinCompilationArguments(), + dependencyClasspath: Iterable = emptyList(), + kotlinTaskProperties: KotlinTaskProperties = createKotlinTaskProperties(), + nativeExtensions: KotlinNativeCompilationExtensions? = null + +): KotlinCompilationImpl { + return KotlinCompilationImpl( + name = name, + declaredSourceSets = defaultSourceSets, + allSourceSets = allSourceSets, + dependencies = dependencies.toList().toTypedArray(), + output = output, + arguments = arguments, + dependencyClasspath = dependencyClasspath.toList().toTypedArray(), + kotlinTaskProperties = kotlinTaskProperties, + nativeExtensions = nativeExtensions + ) +} + +internal fun createKotlinCompilationOutput(): KotlinCompilationOutputImpl { + return KotlinCompilationOutputImpl( + classesDirs = emptySet(), + effectiveClassesDir = null, + resourcesDir = null + ) +} + +internal fun createKotlinCompilationArguments(): KotlinCompilationArgumentsImpl { + return KotlinCompilationArgumentsImpl( + defaultArguments = emptyArray(), + currentArguments = emptyArray() + ) +} + +internal fun createKotlinTaskProperties(): KotlinTaskPropertiesImpl { + return KotlinTaskPropertiesImpl( + null, null, null, null + ) +} + +internal fun createKotlinTarget( + name: String, + platform: KotlinPlatform = KotlinPlatform.COMMON, + compilations: Iterable = emptyList() +): KotlinTargetImpl { + return KotlinTargetImpl( + name = name, + presetName = null, + disambiguationClassifier = null, + platform = platform, + compilations = compilations.toList(), + testRunTasks = emptyList(), + nativeMainRunTasks = emptyList(), + jar = null, + konanArtifacts = emptyList() + ) +} \ No newline at end of file