diff --git a/libraries/tools/kotlin-gradle-plugin/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin/build.gradle.kts index cf82719c354..8cb53d5825a 100644 --- a/libraries/tools/kotlin-gradle-plugin/build.gradle.kts +++ b/libraries/tools/kotlin-gradle-plugin/build.gradle.kts @@ -44,6 +44,7 @@ dependencies { implementation(project(":kotlin-util-klib")) implementation(project(":native:kotlin-klib-commonizer-api")) implementation(project(":kotlin-tooling-metadata")) + implementation(project(":kotlin-project-model")) compileOnly(project(":native:kotlin-native-utils")) compileOnly(project(":kotlin-reflect-api")) compileOnly(project(":kotlin-android-extensions")) diff --git a/libraries/tools/kotlin-project-model/build.gradle.kts b/libraries/tools/kotlin-project-model/build.gradle.kts new file mode 100644 index 00000000000..2b6a7d5a956 --- /dev/null +++ b/libraries/tools/kotlin-project-model/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + kotlin("jvm") + id("jps-compatible") +} + +publish() + +standardPublicJars() + +dependencies { + implementation(kotlinStdlib()) + + testImplementation(kotlin("test-junit")) +} + +pill { + variant = org.jetbrains.kotlin.pill.PillExtension.Variant.FULL +} + +kotlin.target.compilations.all { + kotlinOptions.languageVersion = "1.3" + kotlinOptions.apiVersion = "1.3" + kotlinOptions.freeCompilerArgs += listOf("-Xskip-prerelease-check") +} + +tasks.named("jar") { + callGroovy("manifestAttributes", manifest, project) +} diff --git a/libraries/tools/kotlin-project-model/src/main/kotlin/InternalDependencyExpansion.kt b/libraries/tools/kotlin-project-model/src/main/kotlin/InternalDependencyExpansion.kt new file mode 100644 index 00000000000..4a6d856d1bb --- /dev/null +++ b/libraries/tools/kotlin-project-model/src/main/kotlin/InternalDependencyExpansion.kt @@ -0,0 +1,118 @@ +/* + * Copyright 2010-2020 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.project.model + +import org.jetbrains.kotlin.project.model.utils.variantsContainingFragment +import java.util.ArrayDeque + +interface InternalDependencyExpansion { + fun expandInternalFragmentDependencies(consumingFragment: KotlinModuleFragment): InternalDependencyExpansionResult +} + +class InternalDependencyExpansionResult( + val entries: Iterable +) { + sealed class ExpansionOutcome(val variantMatchingResults: Iterable) { + class VisibleFragments( + val fragments: Iterable, + variantMatchingResults: Iterable + ) : ExpansionOutcome( + variantMatchingResults + ) + + class Failure(variantMatchingResults: Iterable) : ExpansionOutcome(variantMatchingResults) + } + + class Entry( + val dependingFragment: KotlinModuleFragment, + val dependencyFragment: KotlinModuleFragment, + val outcome: ExpansionOutcome + ) +} + +fun InternalDependencyExpansionResult.visibleFragments(): List = + entries.flatMap { entry -> entry.outcome.visibleFragments() } + +fun InternalDependencyExpansionResult.ExpansionOutcome.visibleFragments(): Iterable = when (this) { + is InternalDependencyExpansionResult.ExpansionOutcome.VisibleFragments -> fragments + else -> emptyList() +} + +class DefaultInternalDependencyExpansion( + private val variantResolver: ContainingModuleVariantResolver +) : InternalDependencyExpansion { + interface ContainingModuleVariantResolver { + fun getChosenVariant( + dependingVariant: KotlinModuleVariant, + candidateVariants: Iterable + ): KotlinVariantMatchingResult + } + + override fun expandInternalFragmentDependencies(consumingFragment: KotlinModuleFragment): InternalDependencyExpansionResult { + val visited = mutableSetOf(consumingFragment) + val answer = mutableListOf() + + /** + * For each fragment **t** that we consider a source of declared dependencies that we should expand for [consumingFragment]: + * - also consider the refines-closure of **t** as sources of declared dependencies + * - expand the declared dependencies of **t** to a set of fragments **F** + * - add the expansion result to the answer + * - also consider all fragments in **F** as sources of declared dependencies + */ + val declaredDependencySourceQueue = ArrayDeque().apply { add(consumingFragment) } + while (declaredDependencySourceQueue.isNotEmpty()) { + val declaredDependencySource = declaredDependencySourceQueue.removeFirst() + declaredDependencySourceQueue.addAll(declaredDependencySource.refinesClosure.filter(visited::add)) + val declaredDependenciesToExpand = declaredDependencySource.declaredContainingModuleFragmentDependencies + declaredDependenciesToExpand.forEach { declaredDependency -> + val answerEntry = expandSingleDeclaredDependency(consumingFragment, declaredDependencySource, declaredDependency) + answer += answerEntry + declaredDependencySourceQueue.addAll(answerEntry.outcome.visibleFragments()) + } + } + + return InternalDependencyExpansionResult(answer) + } + + private fun expandSingleDeclaredDependency( + consumingFragment: KotlinModuleFragment, + declaredDependencySource: KotlinModuleFragment, + declaredDependency: KotlinModuleFragment + ): InternalDependencyExpansionResult.Entry { + /** + * Go over the variants containing the fragment that we infer the expansion for (note: it's the original consuming fragment, which + * may not necessarily be the same as the fragment holding the declared dependency if the latter comes from the closure). + * + * For each such containing variant v_i, let w_i be the variant containing the declared dependency fragment. Failure to choose + * prevents us from inferring the dependency expansion and is reported as a failure to expand the dependency. + * + * Then intersect the refined fragments of each w_i and use the intersection as the result. + */ + + val (consumingVariants, producingVariants) = + listOf(consumingFragment, declaredDependency).map { it.containingModule.variantsContainingFragment(it).toSet() } + + val chosenVariants = consumingVariants.map { consumingVariant -> + if (consumingVariant in producingVariants) + VariantMatch(consumingVariant, consumingVariant.containingModule, consumingVariant) + else + variantResolver.getChosenVariant(consumingVariant, producingVariants) + } + + val mismatchedConsumingVariants = chosenVariants.filter { it !is VariantMatch } + + val outcome = + if (mismatchedConsumingVariants.isNotEmpty()) + InternalDependencyExpansionResult.ExpansionOutcome.Failure(chosenVariants) + else + InternalDependencyExpansionResult.ExpansionOutcome.VisibleFragments( + chosenVariants.map { (it as VariantMatch).chosenVariant.refinesClosure }.reduce { acc, it -> acc.intersect(it) }, + chosenVariants + ) + + return InternalDependencyExpansionResult.Entry(declaredDependencySource, declaredDependency, outcome) + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-project-model/src/main/kotlin/KotlinAttributes.kt b/libraries/tools/kotlin-project-model/src/main/kotlin/KotlinAttributes.kt new file mode 100644 index 00000000000..271ce96e373 --- /dev/null +++ b/libraries/tools/kotlin-project-model/src/main/kotlin/KotlinAttributes.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2010-2020 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.project.model + +open class KotlinAttributeKey( + val uniqueName: String +) { + override fun equals(other: Any?): Boolean = + other is KotlinAttributeKey && uniqueName == other.uniqueName + + override fun hashCode(): Int = + uniqueName.hashCode() +} + +object KotlinPlatformTypeAttribute : KotlinAttributeKey("org.jetbrains.kotlin.platform.type") { + const val JVM = "jvm" + const val JS = "js" + const val NATIVE = "native" +} + +object KotlinNativeTargetAttribute : KotlinAttributeKey("org.jetbrains.kotlin.native.target") \ No newline at end of file diff --git a/libraries/tools/kotlin-project-model/src/main/kotlin/KotlinModule.kt b/libraries/tools/kotlin-project-model/src/main/kotlin/KotlinModule.kt new file mode 100644 index 00000000000..8a75f2b6897 --- /dev/null +++ b/libraries/tools/kotlin-project-model/src/main/kotlin/KotlinModule.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2010-2020 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.project.model + +sealed class ModuleSource { + class LocalBuild(val buildId: String) : ModuleSource() + class ExternalDependency(val dependencyId: String) : ModuleSource() +} + +interface KotlinModule { + val moduleName: String + val moduleSource: ModuleSource + + val fragments: Iterable + + val variants: Iterable + get() = fragments.filterIsInstance() +} + +class BasicKotlinModule( + override val moduleName: String, + override val moduleSource: ModuleSource +) : KotlinModule { + override val fragments = mutableListOf() + + override fun toString(): String = "module '$moduleName'" +} \ No newline at end of file diff --git a/libraries/tools/kotlin-project-model/src/main/kotlin/KotlinModuleFragment.kt b/libraries/tools/kotlin-project-model/src/main/kotlin/KotlinModuleFragment.kt new file mode 100644 index 00000000000..62234d670fc --- /dev/null +++ b/libraries/tools/kotlin-project-model/src/main/kotlin/KotlinModuleFragment.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2010-2020 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.project.model + +import org.jetbrains.kotlin.project.model.utils.variantsContainingFragment +import java.io.File + +interface KotlinModuleFragment { + val containingModule: KotlinModule + + val fragmentName: String + val directRefinesDependencies: Iterable + + val declaredContainingModuleFragmentDependencies: Iterable + + val kotlinSourceRoots: Iterable +} + +interface KotlinModuleVariant : KotlinModuleFragment { + val variantAttributes: Map + + var isExported: Boolean +} + +val KotlinModuleFragment.fragmentAttributeSets: Map> + get() = mutableMapOf>().apply { + containingModule.variantsContainingFragment(this@fragmentAttributeSets).forEach { variant -> + variant.variantAttributes.forEach { attribute, value -> + getOrPut(attribute) { mutableSetOf() }.add(value) + } + } + } + +val KotlinModuleFragment.refinesClosure: Set + get() = mutableSetOf().apply { + fun visit(moduleFragment: KotlinModuleFragment) { + if (add(moduleFragment)) + moduleFragment.directRefinesDependencies.forEach(::visit) + } + visit(this@refinesClosure) + } + + +open class BasicKotlinModuleFragment( + override val containingModule: KotlinModule, + override val fragmentName: String +) : KotlinModuleFragment { + + override val directRefinesDependencies: MutableList = mutableListOf() + + override val declaredContainingModuleFragmentDependencies: MutableList = mutableListOf() + override val kotlinSourceRoots: Iterable = emptyList() + override fun toString(): String = "fragment $fragmentName" +} + +class BasicKotlinVariant( + containingModule: KotlinModule, + fragmentName: String +) : BasicKotlinModuleFragment ( + containingModule, + fragmentName +), KotlinModuleVariant { + override var isExported: Boolean = true + override val variantAttributes: MutableMap = mutableMapOf() + override fun toString(): String = "variant $fragmentName" +} \ No newline at end of file diff --git a/libraries/tools/kotlin-project-model/src/main/kotlin/KotlinModuleFragmentResolver.kt b/libraries/tools/kotlin-project-model/src/main/kotlin/KotlinModuleFragmentResolver.kt new file mode 100644 index 00000000000..344b95d981c --- /dev/null +++ b/libraries/tools/kotlin-project-model/src/main/kotlin/KotlinModuleFragmentResolver.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2010-2020 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.project.model + +import org.jetbrains.kotlin.project.model.utils.variantsContainingFragment + +interface KotlinModuleFragmentResolver { + fun getChosenFragments(dependingFragment: KotlinModuleFragment, dependencyModule: KotlinModule): KotlinChosenFragments +} + +class KotlinChosenFragments( + val module: KotlinModule, + val chosenFragments: Iterable, + val variantMatchingResults: Iterable +) + +class DefaultKotlinModuleFragmentResolver( + private val variantResolver: KotlinModuleVariantResolver +) : KotlinModuleFragmentResolver { + override fun getChosenFragments(dependingFragment: KotlinModuleFragment, dependencyModule: KotlinModule): KotlinChosenFragments { + val dependingModule = dependingFragment.containingModule + val containingVariants = dependingModule.variantsContainingFragment(dependingFragment) + + val chosenVariants = containingVariants.map { variantResolver.getChosenVariant(it, dependencyModule) } + + val chosenFragments = chosenVariants.map { variantResolution -> + when (variantResolution) { + is VariantMatch -> variantResolution.chosenVariant.refinesClosure + else -> emptySet() + } + } + + val result = if (chosenFragments.isEmpty()) + emptyList() + else chosenFragments + // Note this emulates the existing behavior that is lenient wrt to unresolved modules, but gives imprecise results. TODO revisit + .filter { it.isNotEmpty() } + .reduce { acc, it -> acc.intersect(it) } + + return KotlinChosenFragments(dependencyModule, result, chosenVariants) + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-project-model/src/main/kotlin/KotlinModuleVariantResolver.kt b/libraries/tools/kotlin-project-model/src/main/kotlin/KotlinModuleVariantResolver.kt new file mode 100644 index 00000000000..f4d149028e4 --- /dev/null +++ b/libraries/tools/kotlin-project-model/src/main/kotlin/KotlinModuleVariantResolver.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2010-2020 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.project.model + +/* + * Copyright 2010-2020 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. + */ + +interface KotlinModuleVariantResolver { + fun getChosenVariant(dependingVariant: KotlinModuleVariant, dependencyModule: KotlinModule): KotlinVariantMatchingResult +} + +sealed class KotlinVariantMatchingResult( + val requestingVariant: KotlinModuleVariant, + val dependencyModule: KotlinModule +) { + companion object { + fun fromMatchingVariants( + requestingVariant: KotlinModuleVariant, + dependencyModule: KotlinModule, + matchingVariants: Collection + ) = when (matchingVariants.size) { + 0 -> NoVariantMatch(requestingVariant, dependencyModule) + 1 -> VariantMatch(requestingVariant, dependencyModule, matchingVariants.single()) + else -> AmbiguousVariants(requestingVariant, dependencyModule, matchingVariants) + } + } +} + +class VariantMatch( + requestingVariant: KotlinModuleVariant, + dependencyModule: KotlinModule, + val chosenVariant: KotlinModuleVariant +) : KotlinVariantMatchingResult(requestingVariant, dependencyModule) + +class NoVariantMatch( + requestingVariant: KotlinModuleVariant, + dependencyModule: KotlinModule +) : KotlinVariantMatchingResult(requestingVariant, dependencyModule) + +class AmbiguousVariants( + requestingVariant: KotlinModuleVariant, + dependencyModule: KotlinModule, + val matchingVariants: Iterable +) : KotlinVariantMatchingResult(requestingVariant, dependencyModule) diff --git a/libraries/tools/kotlin-project-model/src/main/kotlin/utils/KotlinModuleUtils.kt b/libraries/tools/kotlin-project-model/src/main/kotlin/utils/KotlinModuleUtils.kt new file mode 100644 index 00000000000..99df8cb8a3b --- /dev/null +++ b/libraries/tools/kotlin-project-model/src/main/kotlin/utils/KotlinModuleUtils.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2010-2020 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.project.model.utils + +import org.jetbrains.kotlin.project.model.KotlinModule +import org.jetbrains.kotlin.project.model.KotlinModuleFragment +import org.jetbrains.kotlin.project.model.KotlinModuleVariant +import org.jetbrains.kotlin.project.model.refinesClosure + +fun KotlinModule.variantsContainingFragment(fragment: KotlinModuleFragment): Iterable = + variants.filter { fragment in it.refinesClosure } + +fun KotlinModule.findRefiningFragments(fragment: KotlinModuleFragment): Iterable { + val refining = mutableSetOf() + val notRefining = mutableSetOf() + + fun isRefining(other: KotlinModuleFragment): Boolean = when { + other in refining -> true + other in notRefining -> false + fragment in other.directRefinesDependencies -> true.also { refining.add(other) } + fragment.directRefinesDependencies.any { isRefining(it) } -> true.also { refining.add(other) } + else -> false.also { notRefining.add(other) } + } + + return fragments.filter(::isRefining) +} \ No newline at end of file diff --git a/libraries/tools/kotlin-project-model/src/test/kotlin/org/jetbrains/kotlin/project/model/DefaultInternalDependencyExpansionTest.kt b/libraries/tools/kotlin-project-model/src/test/kotlin/org/jetbrains/kotlin/project/model/DefaultInternalDependencyExpansionTest.kt new file mode 100644 index 00000000000..0e6ee4bfd0b --- /dev/null +++ b/libraries/tools/kotlin-project-model/src/test/kotlin/org/jetbrains/kotlin/project/model/DefaultInternalDependencyExpansionTest.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2010-2020 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.project.model + +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class DefaultInternalDependencyExpansionTest { + /** Create a module without internal depends edges, those need to be added manually afterwards */ + fun createTemplateModule(vararg purposes: String) = module("foo").apply { + listOf(*purposes).forEach { purpose -> + val common = fragment("common", purpose) + + val (jvm, js, linux) = listOf("jvm", "js", "linux").map { platform -> + variant(platform, purpose).apply { + variantAttributes[KotlinPlatformTypeAttribute] = when (platform) { + "jvm" -> KotlinPlatformTypeAttribute.JVM + "js" -> KotlinPlatformTypeAttribute.JS + else -> { + variantAttributes[KotlinNativeTargetAttribute] = platform + KotlinPlatformTypeAttribute.NATIVE + } + } + } + } + + val jvmAndJs = fragment("jvmAndJs", purpose).apply { + refines(common) + refinedBy(jvm) + refinedBy(js) + } + val jsAndLinux = fragment("jsAndLinux", purpose).apply { + refines(common) + refinedBy(js) + refinedBy(linux) + } + } + } + + val expansion = DefaultInternalDependencyExpansion(AssociateVariants()) + + @Test + fun testSimpleCommonTestToCommonMainDependency() { + val module = createTemplateModule("main", "test").apply { + // Manually associate the variants: + listOf("jvm", "js", "linux").forEach { + variant(it, "test").depends(variant(it, "main")) + } + // Add just a single dependency from commonTest to commonMain + fragment("commonTest").depends(fragment("commonMain")) + } + val expectedFragments = mapOf( + "commonTest" to setOf("commonMain"), + "jvmAndJsTest" to setOf("jvmAndJsMain", "commonMain"), + "jsAndLinuxTest" to setOf("jsAndLinuxMain", "commonMain"), + "jvmTest" to setOf("jvmMain", "jvmAndJsMain", "commonMain"), + "jsTest" to setOf("jsMain", "jsAndLinuxMain", "jvmAndJsMain", "commonMain"), + "linuxTest" to setOf("linuxMain", "jsAndLinuxMain", "commonMain") + ) + module.fragments.forEach { fragment -> + val result = + expansion.expandInternalFragmentDependencies(fragment).visibleFragments().map { it.fragmentName }.toSet() + val expected = expectedFragments[fragment.fragmentName].orEmpty() + assertEquals(expected, result) + } + } + + @Test + fun testDeclaredDependenciesFromExpansion() { + val module = createTemplateModule("main", "test", "integration").apply { + /** Manually associate the variants: */ + listOf("jvm", "js", "linux").forEach { + variant(it, "test").depends(variant(it, "main")) + variant(it, "integration").depends(variant(it, "test")) + variant(it, "integration").depends(variant(it, "main")) + } + + /** + * Add a dependency from `commonIntegration` to `commonTest` and from (a more specific fragment) + * `jvmAndJsTest` to `jvmAndJsMain`. + */ + fragment("commonIntegration").depends(fragment("commonTest")) + fragment("jvmAndJsTest").depends(fragment("jvmAndJsMain")) + + /** + * Expect that the fragment `jvmAndJsIntegration` receives the dependency on `jvmAndJsMain` through + * the declared dependency added to the fragment `jvmAndJsTest`, which is only visible in expansion. + */ + val result = expansion.expandInternalFragmentDependencies(fragment("jvmAndJsIntegration")) + val expected = setOf("commonMain", "jvmAndJsMain", "commonTest", "jvmAndJsTest") + assertEquals(expected, result.visibleFragments().map { it.fragmentName }.toSet()) + assertTrue { + result.entries.any { + it.dependingFragment == fragment("jvmAndJsTest") && + it.dependencyFragment == fragment("jvmAndJsMain") && + it.outcome.visibleFragments().map { it.fragmentName }.toSet() == setOf("commonMain", "jvmAndJsMain") + } + } + } + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-project-model/src/test/kotlin/org/jetbrains/kotlin/project/model/DefaultKotlinModuleFragmentResolverTest.kt b/libraries/tools/kotlin-project-model/src/test/kotlin/org/jetbrains/kotlin/project/model/DefaultKotlinModuleFragmentResolverTest.kt new file mode 100644 index 00000000000..f8aba0b2244 --- /dev/null +++ b/libraries/tools/kotlin-project-model/src/test/kotlin/org/jetbrains/kotlin/project/model/DefaultKotlinModuleFragmentResolverTest.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2010-2020 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.project.model + +import org.junit.Assume +import org.junit.Assume.assumeTrue +import org.junit.Test +import kotlin.test.assertEquals + +class DefaultKotlinModuleFragmentResolverTest { + val moduleFoo = simpleModule("foo") + val moduleBar = simpleModule("bar") + + val fragmentResolver = DefaultKotlinModuleFragmentResolver(MatchVariantsByExactAttributes()) + + @Test + fun testFragmentVisibility() { + val expectedVisibleFragments = mapOf( + "common" to setOf("commonMain"), + "jvmAndJs" to setOf("commonMain", "jvmAndJsMain"), + "jsAndLinux" to setOf("commonMain", "jsAndLinuxMain"), + "jvm" to setOf("commonMain", "jvmAndJsMain", "jvmMain"), + "js" to setOf("commonMain", "jvmAndJsMain", "jsAndLinuxMain", "jsMain"), + "linux" to setOf("commonMain", "jsAndLinuxMain", "linuxMain") + ) + + moduleBar.fragments.forEach { consumingFragment -> + val result = fragmentResolver.getChosenFragments(consumingFragment, moduleFoo) + val expected = expectedVisibleFragments.getValue(consumingFragment.fragmentName.removeSuffix("Main").removeSuffix("Test")) + assertEquals(expected, result.chosenFragments.map { it.fragmentName }.toSet()) + } + } + + @Test + fun testVisibilityWithMismatchedVariant() { + // TODO this behavior replicates 1.3.x MPP where a mismatched variant gets ignored and only matched variants are intersected. + // This helps with non-published local native targets. + // Consider making it more strict when we have a solution to the original problem. + val dependingModule = simpleModule("baz").apply { + variant("linuxMain").variantAttributes.replace(KotlinNativeTargetAttribute, "notLinux") + } + assumeTrue(MatchVariantsByExactAttributes().getChosenVariant(dependingModule.variant("linuxMain"), moduleFoo) is NoVariantMatch) + + val (commonMainResult, jsAndLinuxResult) = listOf("commonMain", "jsAndLinuxMain").map { + fragmentResolver.getChosenFragments(dependingModule.fragment(it), moduleFoo).chosenFragments.map { it.fragmentName }.toSet() + } + + assertEquals(setOf("commonMain", "jvmAndJsMain"), commonMainResult) + assertEquals(setOf("commonMain", "jvmAndJsMain", "jsAndLinuxMain", "jsMain"), jsAndLinuxResult) + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-project-model/src/test/kotlin/org/jetbrains/kotlin/project/model/DummyVariantMatchingImplementations.kt b/libraries/tools/kotlin-project-model/src/test/kotlin/org/jetbrains/kotlin/project/model/DummyVariantMatchingImplementations.kt new file mode 100644 index 00000000000..94be14b6e0e --- /dev/null +++ b/libraries/tools/kotlin-project-model/src/test/kotlin/org/jetbrains/kotlin/project/model/DummyVariantMatchingImplementations.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2010-2020 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.project.model + +class MatchVariantsByExactAttributes : KotlinModuleVariantResolver { + override fun getChosenVariant(dependingVariant: KotlinModuleVariant, dependencyModule: KotlinModule): KotlinVariantMatchingResult { + val candidates = dependencyModule.variants + return candidates.filter { candidate -> + candidate.isExported && candidate.variantAttributes.all { (attributeKey, candidateValue) -> + attributeKey !in dependingVariant.variantAttributes.keys || + candidateValue == dependingVariant.variantAttributes.getValue(attributeKey) + } + }.let { KotlinVariantMatchingResult.fromMatchingVariants(dependingVariant, dependencyModule, it) } + } +} + +class AssociateVariants : DefaultInternalDependencyExpansion.ContainingModuleVariantResolver { + override fun getChosenVariant( + dependingVariant: KotlinModuleVariant, + candidateVariants: Iterable + ): KotlinVariantMatchingResult { + val result = candidateVariants.filter { it in dependingVariant.declaredContainingModuleFragmentDependencies } + return KotlinVariantMatchingResult.fromMatchingVariants(dependingVariant, dependingVariant.containingModule, result) + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-project-model/src/test/kotlin/org/jetbrains/kotlin/project/model/SimpleModel.kt b/libraries/tools/kotlin-project-model/src/test/kotlin/org/jetbrains/kotlin/project/model/SimpleModel.kt new file mode 100644 index 00000000000..bc775be42a1 --- /dev/null +++ b/libraries/tools/kotlin-project-model/src/test/kotlin/org/jetbrains/kotlin/project/model/SimpleModel.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2010-2020 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.project.model + +fun module(name: String) = BasicKotlinModule(name, ModuleSource.LocalBuild("current")) + +fun BasicKotlinModule.fragment(vararg nameParts: String): BasicKotlinModuleFragment = + fragment(nameParts.drop(1).joinToString("", nameParts.first()) { it.capitalize() }) + +fun BasicKotlinModule.fragment(name: String): BasicKotlinModuleFragment = + fragments.firstOrNull { it.fragmentName == name } ?: BasicKotlinModuleFragment(this, name).also { fragments.add(it) } + +fun BasicKotlinModule.variant(vararg nameParts: String): BasicKotlinVariant = + variant(nameParts.drop(1).joinToString("", nameParts.first()) { it.capitalize() }) + +fun BasicKotlinModule.variant(name: String): BasicKotlinVariant = + fragments.firstOrNull { it.fragmentName == name } + ?.let { it as? BasicKotlinVariant ?: error("$name is not a variant") } + ?: BasicKotlinVariant(this, name).also { fragments.add(it) } + + +fun BasicKotlinModuleFragment.depends(fragment: BasicKotlinModuleFragment) { + require(fragment.containingModule == containingModule) + declaredContainingModuleFragmentDependencies.add(fragment) +} + +fun BasicKotlinModuleFragment.refinedBy(fragment: BasicKotlinModuleFragment) { + fragment.refines(this) +} + +fun BasicKotlinModuleFragment.refines(fragment: BasicKotlinModuleFragment) { + require(fragment.containingModule == containingModule) + directRefinesDependencies.add(fragment) +} + +// --- + +fun simpleModule(name: String) = module(name).apply { + listOf("main", "test").forEach { purpose -> + val common = fragment("common", purpose) + if (purpose == "test") + common.depends(fragment("common", "main")) + + val (jvm, js, linux) = listOf("jvm", "js", "linux").map { platform -> + variant(platform, purpose).apply { + variantAttributes[KotlinPlatformTypeAttribute] = when (platform) { + "jvm" -> KotlinPlatformTypeAttribute.JVM + "js" -> KotlinPlatformTypeAttribute.JS + else -> { + variantAttributes[KotlinNativeTargetAttribute] = platform + KotlinPlatformTypeAttribute.NATIVE + } + } + if (purpose == "test") { + isExported = false + depends(variant(platform, "main")) + } + } + } + + val jvmAndJs = fragment("jvmAndJs", purpose).apply { + refines(common) + refinedBy(jvm) + refinedBy(js) + } + val jsAndLinux = fragment("jsAndLinux", purpose).apply { + refines(common) + refinedBy(js) + refinedBy(linux) + } + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 33ff0624b18..692be47d2bb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -243,6 +243,7 @@ include ":benchmarks", ":generators", ":generators:test-generator", ":tools:kotlinp", + ":kotlin-project-model", ":kotlin-gradle-plugin-api", ":kotlin-gradle-plugin-dsl-codegen", ":kotlin-gradle-plugin-npm-versions-codegen", @@ -510,6 +511,7 @@ project(':noarg-ide-plugin').projectDir = "$rootDir/plugins/noarg/noarg-ide" as project(':kotlin-sam-with-receiver-compiler-plugin').projectDir = "$rootDir/plugins/sam-with-receiver/sam-with-receiver-cli" as File project(':sam-with-receiver-ide-plugin').projectDir = "$rootDir/plugins/sam-with-receiver/sam-with-receiver-ide" as File project(':tools:kotlinp').projectDir = "$rootDir/libraries/tools/kotlinp" as File +project(':kotlin-project-model').projectDir = "$rootDir/libraries/tools/kotlin-project-model" as File project(':kotlin-gradle-plugin-api').projectDir = "$rootDir/libraries/tools/kotlin-gradle-plugin-api" as File project(':kotlin-gradle-plugin-dsl-codegen').projectDir = "$rootDir/libraries/tools/kotlin-gradle-plugin-dsl-codegen" as File project(':kotlin-gradle-plugin-npm-versions-codegen').projectDir = "$rootDir/libraries/tools/kotlin-gradle-plugin-npm-versions-codegen" as File