[LL FIR] KT-55329 Support transitive dependsOn dependencies in LL FIR

- In contrast to other kinds of dependencies, `dependsOn` dependencies
  must be followed transitively.
- Add `transitiveDependsOnDependencies` to `KtModule`. These
  dependencies are calculated lazily with a topological sort. They are
  added to the dependency provider when it's built in
  `LLFirSessionFactory`.

^KT-55329 fixed
This commit is contained in:
Marco Pennekamp
2022-12-20 18:41:23 +01:00
committed by teamcity
parent 1803bd36cc
commit d6cb75ca91
15 changed files with 126 additions and 36 deletions
@@ -17,7 +17,6 @@ import org.jetbrains.kotlin.analysis.api.lifetime.KtLifetimeToken
import org.jetbrains.kotlin.analysis.api.symbols.KtBackingFieldSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KtDeclarationSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KtSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KtValueParameterSymbol
import org.jetbrains.kotlin.analysis.api.symbols.markers.KtSymbolKind
import org.jetbrains.kotlin.analysis.api.symbols.markers.KtSymbolWithKind
import org.jetbrains.kotlin.analysis.project.structure.KtLibraryModule
@@ -72,6 +71,7 @@ internal class KtFe10SymbolContainingDeclarationProvider(
override fun getBinaryRoots(): Collection<Path> = listOf(libraryPath)
override val directRegularDependencies: List<KtModule> = emptyList()
override val directDependsOnDependencies: List<KtModule> = emptyList()
override val transitiveDependsOnDependencies: List<KtModule> = emptyList()
override val directFriendDependencies: List<KtModule> = emptyList()
override val contentScope: GlobalSearchScope = ProjectScope.getLibrariesScope(project)
override val platform: TargetPlatform
@@ -10,6 +10,7 @@ import com.intellij.psi.search.GlobalSearchScope
import org.jetbrains.kotlin.analysis.project.structure.KtLibraryModule
import org.jetbrains.kotlin.analysis.project.structure.KtLibrarySourceModule
import org.jetbrains.kotlin.analysis.project.structure.KtModule
import org.jetbrains.kotlin.analysis.project.structure.computeTransitiveDependsOnDependencies
import org.jetbrains.kotlin.platform.TargetPlatform
import org.jetbrains.kotlin.resolve.PlatformDependentAnalyzerServices
import java.nio.file.Path
@@ -25,6 +26,7 @@ internal class KtLibraryModuleImpl(
override val libraryName: String,
override val librarySources: KtLibrarySourceModule?,
) : KtLibraryModule, KtModuleWithPlatform {
override val transitiveDependsOnDependencies: List<KtModule> by lazy { computeTransitiveDependsOnDependencies(directDependsOnDependencies) }
override val analyzerServices: PlatformDependentAnalyzerServices = super.analyzerServices
override fun getBinaryRoots(): Collection<Path> = binaryRoots
@@ -10,6 +10,7 @@ import com.intellij.psi.search.GlobalSearchScope
import org.jetbrains.kotlin.analysis.project.structure.KtLibraryModule
import org.jetbrains.kotlin.analysis.project.structure.KtLibrarySourceModule
import org.jetbrains.kotlin.analysis.project.structure.KtModule
import org.jetbrains.kotlin.analysis.project.structure.computeTransitiveDependsOnDependencies
import org.jetbrains.kotlin.platform.TargetPlatform
import org.jetbrains.kotlin.resolve.PlatformDependentAnalyzerServices
@@ -23,5 +24,6 @@ internal class KtLibrarySourceModuleImpl(
override val libraryName: String,
override val binaryLibrary: KtLibraryModule,
) : KtLibrarySourceModule, KtModuleWithPlatform {
override val transitiveDependsOnDependencies: List<KtModule> by lazy { computeTransitiveDependsOnDependencies(directDependsOnDependencies) }
override val analyzerServices: PlatformDependentAnalyzerServices = super.analyzerServices
}
@@ -10,6 +10,7 @@ import com.intellij.psi.PsiFile
import com.intellij.psi.search.GlobalSearchScope
import org.jetbrains.kotlin.analysis.project.structure.KtModule
import org.jetbrains.kotlin.analysis.project.structure.KtNotUnderContentRootModule
import org.jetbrains.kotlin.analysis.project.structure.computeTransitiveDependsOnDependencies
import org.jetbrains.kotlin.platform.TargetPlatform
import org.jetbrains.kotlin.platform.jvm.JvmPlatforms
import org.jetbrains.kotlin.resolve.PlatformDependentAnalyzerServices
@@ -24,6 +25,7 @@ internal class KtNotUnderContentRootModuleImpl(
override val moduleDescription: String,
override val project: Project,
) : KtNotUnderContentRootModule, KtModuleWithPlatform {
override val transitiveDependsOnDependencies: List<KtModule> by lazy { computeTransitiveDependsOnDependencies(directDependsOnDependencies) }
override val analyzerServices: PlatformDependentAnalyzerServices = super.analyzerServices
override val contentScope: GlobalSearchScope =
@@ -9,6 +9,7 @@ import com.intellij.openapi.project.Project
import com.intellij.psi.search.GlobalSearchScope
import org.jetbrains.kotlin.analysis.project.structure.KtModule
import org.jetbrains.kotlin.analysis.project.structure.KtSdkModule
import org.jetbrains.kotlin.analysis.project.structure.computeTransitiveDependsOnDependencies
import org.jetbrains.kotlin.platform.TargetPlatform
import org.jetbrains.kotlin.resolve.PlatformDependentAnalyzerServices
import java.nio.file.Path
@@ -23,6 +24,7 @@ internal class KtSdkModuleImpl(
private val binaryRoots: Collection<Path>,
override val sdkName: String,
) : KtSdkModule, KtModuleWithPlatform {
override val transitiveDependsOnDependencies: List<KtModule> by lazy { computeTransitiveDependsOnDependencies(directDependsOnDependencies) }
override val analyzerServices: PlatformDependentAnalyzerServices = super.analyzerServices
override fun getBinaryRoots(): Collection<Path> = binaryRoots
@@ -10,6 +10,7 @@ import com.intellij.psi.PsiFileSystemItem
import com.intellij.psi.search.GlobalSearchScope
import org.jetbrains.kotlin.analysis.project.structure.KtModule
import org.jetbrains.kotlin.analysis.project.structure.KtSourceModule
import org.jetbrains.kotlin.analysis.project.structure.computeTransitiveDependsOnDependencies
import org.jetbrains.kotlin.config.LanguageVersionSettings
import org.jetbrains.kotlin.platform.TargetPlatform
import org.jetbrains.kotlin.resolve.PlatformDependentAnalyzerServices
@@ -25,5 +26,6 @@ internal class KtSourceModuleImpl(
override val languageVersionSettings: LanguageVersionSettings,
internal val sourceRoots: List<PsiFileSystemItem>,
) : KtSourceModule, KtModuleWithPlatform {
override val transitiveDependsOnDependencies: List<KtModule> by lazy { computeTransitiveDependsOnDependencies(directDependsOnDependencies) }
override val analyzerServices: PlatformDependentAnalyzerServices = super.analyzerServices
}
@@ -9,10 +9,7 @@ import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.ProjectScope
import org.jetbrains.kotlin.analysis.project.structure.KtLibraryModule
import org.jetbrains.kotlin.analysis.project.structure.KtLibrarySourceModule
import org.jetbrains.kotlin.analysis.project.structure.KtModule
import org.jetbrains.kotlin.analysis.project.structure.KtSourceModule
import org.jetbrains.kotlin.analysis.project.structure.*
import org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM
import org.jetbrains.kotlin.cli.jvm.config.jvmClasspathRoots
import org.jetbrains.kotlin.cli.jvm.config.jvmModularRoots
@@ -60,6 +57,8 @@ abstract class KtModuleByCompilerConfiguration(
.map { moduleProvider.getModule(it.moduleName) }
}
val transitiveDependsOnDependencies: List<KtModule> by lazy { computeTransitiveDependsOnDependencies(directDependsOnDependencies) }
val directFriendDependencies: List<KtModule> by lazy(LazyThreadSafetyMode.PUBLICATION) {
buildList {
testModule.friendDependencies.mapTo(this) { moduleProvider.getModule(it.moduleName) }
@@ -140,6 +139,7 @@ private class LibraryByRoots(
override val libraryName: String get() = "Test Library"
override val directRegularDependencies: List<KtModule> get() = emptyList()
override val directDependsOnDependencies: List<KtModule> get() = emptyList()
override val transitiveDependsOnDependencies: List<KtModule> get() = emptyList()
override val directFriendDependencies: List<KtModule> get() = emptyList()
override val contentScope: GlobalSearchScope get() = ProjectScope.getLibrariesScope(project)
override val platform: TargetPlatform get() = module.platform
@@ -14,10 +14,17 @@ import org.jetbrains.kotlin.resolve.PlatformDependentAnalyzerServices
import org.jetbrains.kotlin.test.getAnalyzerServices
import java.nio.file.Path
interface KtModuleWithModifiableDependencies {
val directRegularDependencies: MutableList<KtModule>
val directDependsOnDependencies: MutableList<KtModule>
val directFriendDependencies: MutableList<KtModule>
abstract class KtModuleWithModifiableDependencies {
abstract val directRegularDependencies: MutableList<KtModule>
abstract val directDependsOnDependencies: MutableList<KtModule>
abstract val directFriendDependencies: MutableList<KtModule>
/**
* When dependencies are modifiable, transitive `dependsOn` dependencies must be recomputed each time as [directDependsOnDependencies]
* may have been mutated.
*/
val transitiveDependsOnDependencies: List<KtModule>
get() = computeTransitiveDependsOnDependencies(directDependsOnDependencies)
}
class KtSourceModuleImpl(
@@ -26,7 +33,7 @@ class KtSourceModuleImpl(
override val languageVersionSettings: LanguageVersionSettings,
override val project: Project,
override val contentScope: GlobalSearchScope,
) : KtSourceModule, KtModuleWithModifiableDependencies {
) : KtModuleWithModifiableDependencies(), KtSourceModule {
override val analyzerServices: PlatformDependentAnalyzerServices get() = platform.getAnalyzerServices()
override val directRegularDependencies: MutableList<KtModule> = mutableListOf()
@@ -40,7 +47,7 @@ class KtJdkModuleImpl(
override val contentScope: GlobalSearchScope,
override val project: Project,
private val binaryRoots: Collection<Path>,
) : KtSdkModule, KtModuleWithModifiableDependencies {
) : KtModuleWithModifiableDependencies(), KtSdkModule {
override val analyzerServices: PlatformDependentAnalyzerServices
get() = platform.getAnalyzerServices()
@@ -58,7 +65,7 @@ class KtLibraryModuleImpl(
override val project: Project,
private val binaryRoots: Collection<Path>,
override var librarySources: KtLibrarySourceModule?,
) : KtLibraryModule, KtModuleWithModifiableDependencies {
) : KtModuleWithModifiableDependencies(), KtLibraryModule {
override val analyzerServices: PlatformDependentAnalyzerServices get() = platform.getAnalyzerServices()
override fun getBinaryRoots(): Collection<Path> = binaryRoots
@@ -73,7 +80,7 @@ class KtLibrarySourceModuleImpl(
override val contentScope: GlobalSearchScope,
override val project: Project,
override val binaryLibrary: KtLibraryModule,
) : KtLibrarySourceModule, KtModuleWithModifiableDependencies {
) : KtModuleWithModifiableDependencies(), KtLibrarySourceModule {
override val analyzerServices: PlatformDependentAnalyzerServices get() = platform.getAnalyzerServices()
override val directRegularDependencies: MutableList<KtModule> = mutableListOf()
@@ -113,26 +113,36 @@ internal object LLFirSessionFactory {
register(FirSwitchableExtensionDeclarationsSymbolProvider::class, it)
}
fun getOrCreateSessionForDependency(dependency: KtModule): LLFirSession? = when (dependency) {
is KtBuiltinsModule -> null // build in is already added
is KtBinaryModule -> LLFirLibrarySessionFactory.getInstance(project).getLibrarySession(dependency, sessionsCache)
is KtSourceModule -> {
createSourcesSession(
project,
dependency,
globalResolveComponents,
sessionInvalidator,
sessionsCache,
librariesSessionFactory = librariesSessionFactory,
configureSession = configureSession,
)
}
is KtNotUnderContentRootModule -> error("Module $module cannot depend on ${dependency::class}: $dependency")
is KtLibrarySourceModule -> error("Module $module cannot depend on ${dependency::class}: $dependency")
}
val dependencyProvider = LLFirDependentModuleProvidersBySessions(this) {
module.directRegularDependencies.mapNotNullTo(this) { dependency ->
when (dependency) {
is KtBuiltinsModule -> null // build in is already added
is KtBinaryModule -> LLFirLibrarySessionFactory.getInstance(project).getLibrarySession(dependency, sessionsCache)
is KtSourceModule -> {
createSourcesSession(
project,
dependency,
globalResolveComponents,
sessionInvalidator,
sessionsCache,
librariesSessionFactory = librariesSessionFactory,
configureSession = configureSession,
)
}
is KtNotUnderContentRootModule -> error("Module $module cannot depend on ${dependency::class}: $dependency")
is KtLibrarySourceModule -> error("Module $module cannot depend on ${dependency::class}: $dependency")
module.directRegularDependencies.mapNotNullTo(this, ::getOrCreateSessionForDependency)
// The dependency provider needs to have access to all direct and indirect `dependsOn` dependencies, as `dependsOn`
// dependencies are transitive.
val directRegularDependenciesSet = module.directRegularDependencies.toSet()
module.transitiveDependsOnDependencies.forEach { dependency ->
if (dependency !in directRegularDependenciesSet) {
getOrCreateSessionForDependency(dependency)?.let(::add)
}
}
add(builtinsSession)
}
@@ -79,6 +79,9 @@ private class KtNotUnderContentRootModuleForTest(
override val directDependsOnDependencies: List<KtModule>
get() = emptyList()
override val transitiveDependsOnDependencies: List<KtModule>
get() = emptyList()
override val directFriendDependencies: List<KtModule>
get() = emptyList()
@@ -25,7 +25,7 @@ public sealed interface KtModule {
* A list of Regular dependencies. Regular dependency allows the current module to see symbols from the dependent module. In the case
* of a source set, it can be either the source set it depends on, a library, or an SDK.
*
* The dependencies list is non-transitive and should not include the current module.
* The dependencies list is non-transitive and does not include the current module.
*/
public val directRegularDependencies: List<KtModule>
@@ -35,14 +35,22 @@ public sealed interface KtModule {
* A `dependsOn` dependency expresses that the current module can provide `actual` declarations for `expect` declarations from the
* dependent module, as well as see internal symbols of the dependent module.
*
* `dependsOn` dependencies are transitive, but the list is not a transitive closure. The list should not include the current module.
* `dependsOn` dependencies are transitive, but the list is not a transitive closure. The list does not include the current module.
*/
public val directDependsOnDependencies: List<KtModule>
/**
* A list of [directDependsOnDependencies] and all of their parents (directly and indirectly), sorted topologically with the nearest
* dependencies first in the list. The list does not include the current module.
*
* @see computeTransitiveDependsOnDependencies
*/
public val transitiveDependsOnDependencies: List<KtModule>
/**
* A list of Friend dependencies. Friend dependencies express that the current module may see internal symbols of the dependent module.
*
* The dependencies list is non-transitive and should not include the current module.
* The dependencies list is non-transitive and does not include the current module.
*/
public val directFriendDependencies: List<KtModule>
@@ -159,6 +167,7 @@ public class KtBuiltinsModule(
) : KtBinaryModule {
override val directRegularDependencies: List<KtModule> get() = emptyList()
override val directDependsOnDependencies: List<KtModule> get() = emptyList()
override val transitiveDependsOnDependencies: List<KtModule> get() = emptyList()
override val directFriendDependencies: List<KtModule> get() = emptyList()
override val contentScope: GlobalSearchScope get() = GlobalSearchScope.EMPTY_SCOPE
override fun getBinaryRoots(): Collection<Path> = emptyList()
@@ -5,6 +5,8 @@
package org.jetbrains.kotlin.analysis.project.structure
import org.jetbrains.kotlin.utils.topologicalSort
/**
* A list of all modules that the current module can depend on with regular dependency.
*
@@ -51,4 +53,14 @@ public fun KtModule.allDirectDependencies(): Sequence<KtModule> =
* @see KtModule.directFriendDependencies
*/
public inline fun <reified M : KtModule> KtModule.allDirectDependenciesOfType(): Sequence<M> =
allDirectDependencies().filterIsInstance<M>()
allDirectDependencies().filterIsInstance<M>()
/**
* Computes the transitive `dependsOn` dependencies of [directDependsOnDependencies]. [computeTransitiveDependsOnDependencies] is the
* default computation strategy to provide [KtModule.transitiveDependsOnDependencies].
*
* The algorithm is a depth-first search-based topological sort. `dependsOn` dependencies cannot be cyclical and thus form a DAG, which
* allows the application of a topological sort.
*/
public fun computeTransitiveDependsOnDependencies(directDependsOnDependencies: List<KtModule>): List<KtModule> =
topologicalSort(directDependsOnDependencies) { this.directDependsOnDependencies }
@@ -1,5 +1,4 @@
// FIR_IDENTICAL
// FIR_IDE_IGNORE
// MODULE: common
// TARGET_PLATFORM: Common
@@ -1,5 +1,4 @@
// FIR_IDENTICAL
// FIR_IDE_IGNORE
// !LANGUAGE: +MultiPlatformProjects
// TARGET_BACKEND: JVM
@@ -0,0 +1,41 @@
/*
* Copyright 2010-2022 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.utils
/**
* Sorts [nodes] topologically collecting direct edges via [dependencies]. [nodes] and [dependencies] must form a directed, acyclic graph.
* [topologicalSort] will throw an [IllegalStateException] if it encounters a cycle.
*
* The algorithm is based on depth-first search, starting in order from each node in [nodes]. Kahn's algorithm is harder to apply to the
* ad-hoc dependency structure because it's not easily apparent whether a node has no other incoming edges.
*
* For example, consider the following structure: `C -> A, C -> B, B -> A`. The resulting order should be `[C, B, A]`. However, `A` is
* first in the list of dependencies of `C`. Without a way to find the incoming edge from `B` to `A` while processing `C -> A`, a naive
* implementation of Kahn's algorithm might order `A` before `B`.
*/
fun <A> topologicalSort(nodes: Iterable<A>, dependencies: A.() -> Iterable<A>): List<A> {
val visiting = mutableSetOf<A>()
val visited = mutableSetOf<A>()
fun visit(node: A) {
if (node in visited) return
if (node in visiting) throw IllegalStateException("Cannot compute a topological sort: The node $node is in a cycle.")
// Keeping track of the nodes that are being visited allows the algorithm to throw an exception in case of a cycle. The input should
// never be cyclic, but this approach gives some additional safety in case of bugs.
visiting.add(node)
node.dependencies().forEach(::visit)
visiting.remove(node)
visited.add(node)
}
nodes.forEach(::visit)
// The paper algorithm inserts nodes at the head of the result list. Because our `visited` set remembers elements in their order of
// insertion, the result needs to be reversed.
return visited.toMutableList().apply { reverse() }
}