Support modification tracking for source-dependent LibraryInfo

#KT-39734
This commit is contained in:
Pavel Kirpichenkov
2020-07-24 15:53:15 +03:00
parent 6abd708d42
commit 9d6bdc6fc4
9 changed files with 239 additions and 55 deletions
@@ -40,10 +40,7 @@ import org.jetbrains.kotlin.idea.core.isInTestSourceContentKotlinAware
import org.jetbrains.kotlin.idea.framework.effectiveKind
import org.jetbrains.kotlin.idea.framework.platform
import org.jetbrains.kotlin.idea.klib.AbstractKlibLibraryInfo
import org.jetbrains.kotlin.idea.project.TargetPlatformDetector
import org.jetbrains.kotlin.idea.project.findAnalyzerServices
import org.jetbrains.kotlin.idea.project.getStableName
import org.jetbrains.kotlin.idea.project.isHMPPEnabled
import org.jetbrains.kotlin.idea.project.*
import org.jetbrains.kotlin.idea.stubindex.KotlinSourceFilterScope
import org.jetbrains.kotlin.idea.util.isInSourceContentWithoutInjected
import org.jetbrains.kotlin.idea.util.rootManager
@@ -336,7 +333,9 @@ private class ModuleTestSourceScope(module: Module) : ModuleSourceScope(module)
override fun toString() = "ModuleTestSourceScope($module)"
}
abstract class LibraryInfo(override val project: Project, val library: Library) : IdeaModuleInfo, LibraryModuleInfo, BinaryModuleInfo {
abstract class LibraryInfo(override val project: Project, val library: Library) :
IdeaModuleInfo, LibraryModuleInfo, BinaryModuleInfo, TrackableModuleInfo {
override val moduleOrigin: ModuleOrigin
get() = ModuleOrigin.LIBRARY
@@ -370,6 +369,13 @@ abstract class LibraryInfo(override val project: Project, val library: Library)
override fun getLibraryRoots(): Collection<String> =
library.getFiles(OrderRootType.CLASSES).mapNotNull(PathUtil::getLocalPath)
override fun createModificationTracker(): ModificationTracker {
if (!project.libraryToSourceAnalysisEnabled)
return ModificationTracker.NEVER_CHANGED
return ResolutionAnchorAwareLibraryModificationTracker(this)
}
override fun toString() = "${this::class.simpleName}(libraryName=${library.name}, libraryRoots=${getLibraryRoots()})"
override fun equals(other: Any?): Boolean {
@@ -0,0 +1,20 @@
/*
* 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.idea.caches.project
import com.intellij.openapi.util.ModificationTracker
import org.jetbrains.kotlin.idea.caches.resolve.ResolutionAnchorCacheService
import org.jetbrains.kotlin.idea.caches.trackers.KotlinCodeBlockModificationListener
class ResolutionAnchorAwareLibraryModificationTracker(
private val libraryInfo: LibraryInfo,
) : ModificationTracker {
override fun getModificationCount(): Long {
val anchorDependencies = ResolutionAnchorCacheService.getInstance(libraryInfo.project).getDependencyResolutionAnchors(libraryInfo)
val updaterForModules = KotlinCodeBlockModificationListener.getInstance(libraryInfo.project).perModuleOutOfCodeBlockTrackerUpdater
return anchorDependencies.maxOfOrNull { updaterForModules.getModificationCount(it.module) } ?: 0
}
}
@@ -0,0 +1,47 @@
/*
* 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.idea.caches.project
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.util.CommonProcessors
import org.jetbrains.kotlin.idea.caches.resolve.ResolutionAnchorCacheService
import org.jetbrains.kotlin.idea.caches.trackers.ModuleDependencyProviderExtension
import org.jetbrains.kotlin.idea.project.libraryToSourceAnalysisEnabled
class ResolutionAnchorModuleDependencyProviderExtension(private val project: Project) : ModuleDependencyProviderExtension {
/**
* Consider modules M1, M2, M3, library L1 resolving via Resolution anchor M2, other libraries L2, L3 with the following dependencies:
* M2 depends on M1
* L1 depends on anchor M2
* L2 depends on L1
* L3 depends on L2
* M3 depends on L3
* Then modification of M1 should lead to complete invalidation of all modules and libraries in this example.
*
* Updates for libraries aren't managed here, corresponding ModificationTracker is responsible for that.
* This extension provides missing dependencies from source-dependent library dependencies only to source modules.
*/
override fun getAdditionalDependencyModules(module: Module): Collection<Module> {
if (!project.libraryToSourceAnalysisEnabled) return emptySet()
val resolutionAnchorDependencies = HashSet<ModuleSourceInfo>()
ModuleRootManager.getInstance(module).orderEntries().recursively().forEachLibrary { library ->
getIdeaModelInfosCache(project).getLibraryInfosForLibrary(library).flatMapTo(resolutionAnchorDependencies) { libraryInfo ->
ResolutionAnchorCacheService.getInstance(project).getDependencyResolutionAnchors(libraryInfo)
}
true
}
val additionalModules = HashSet<Module>()
for (anchorModule in resolutionAnchorDependencies) {
ModuleRootManager.getInstance(anchorModule.module).orderEntries().recursively()
.forEachModule(CommonProcessors.CollectProcessor(additionalModules))
}
return additionalModules
}
}
@@ -28,58 +28,13 @@ import org.jetbrains.kotlin.resolve.ResolutionAnchorProvider
* manually for the libraries in project via resolution anchors. Anchor by itself is a source module which is mapped
* to a library and used during resolution as a fallback.
*/
@State(name = "KotlinIdeAnchorService", storages = [Storage("anchors.xml")])
class KotlinIdeResolutionAnchorService(
val project: Project
) : ResolutionAnchorProvider,
PersistentStateComponent<KotlinIdeResolutionAnchorService.State> {
data class State(
var moduleNameToAnchorName: Map<String, String> = emptyMap()
)
private val logger = logger<KotlinIdeResolutionAnchorService>()
@JvmField
@Volatile
var myState: State = State()
private fun buildMapping(): Map<ModuleInfo, ModuleInfo> {
val modulesByNames = getModuleInfosFromIdeaModel(project).associateBy { moduleInfo ->
ProgressIndicatorAndCompilationCanceledStatus.checkCanceled()
when (moduleInfo) {
is LibraryInfo -> moduleInfo.library.name
is ModuleSourceInfo -> moduleInfo.module.name
else -> moduleInfo.name.asString()
}
}
return myState.moduleNameToAnchorName.entries.mapNotNull { (moduleName, anchorName) ->
val module = modulesByNames[moduleName] ?: return@mapNotNull notFoundModule(moduleName)
val anchor = modulesByNames[anchorName] ?: return@mapNotNull notFoundModule(moduleName)
module to anchor
}.toMap()
}
private fun notFoundModule(moduleName: String): Nothing? {
logger.warn("Module <${moduleName}> not found in project model")
return null
}
private val moduleToAnchor: Map<ModuleInfo, ModuleInfo>
get() = project.cacheInvalidatingOnRootModifications {
buildMapping()
}
override fun getState(): State = myState
override fun loadState(state: State) {
XmlSerializerUtil.copyBean(state, myState)
}
) : ResolutionAnchorProvider {
override fun getResolutionAnchor(moduleDescriptor: ModuleDescriptor): ModuleDescriptor? {
if (!project.libraryToSourceAnalysisEnabled) return null
val moduleToAnchor = ResolutionAnchorCacheService.getInstance(project).resolutionAnchorsForLibraries
val moduleInfo = moduleDescriptor.moduleInfo ?: return null
val keyModuleInfo = if (moduleInfo is SourceForBinaryModuleInfo) moduleInfo.binariesModuleInfo else moduleInfo
val mapped = moduleToAnchor[keyModuleInfo] ?: return null
@@ -0,0 +1,120 @@
/*
* 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.idea.caches.resolve
import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
import com.intellij.util.containers.ContainerUtil
import com.intellij.util.xmlb.XmlSerializerUtil
import org.jetbrains.kotlin.analyzer.ModuleInfo
import org.jetbrains.kotlin.caches.project.cacheByClassInvalidatingOnRootModifications
import org.jetbrains.kotlin.idea.caches.project.LibraryDependenciesCache
import org.jetbrains.kotlin.idea.caches.project.LibraryInfo
import org.jetbrains.kotlin.idea.caches.project.ModuleSourceInfo
import org.jetbrains.kotlin.idea.caches.project.getModuleInfosFromIdeaModel
import org.jetbrains.kotlin.progress.ProgressIndicatorAndCompilationCanceledStatus
import org.jetbrains.kotlin.types.typeUtil.closure
import org.jetbrains.kotlin.utils.addToStdlib.cast
interface ResolutionAnchorCacheService {
val resolutionAnchorsForLibraries: Map<LibraryInfo, ModuleSourceInfo>
fun getDependencyResolutionAnchors(libraryInfo: LibraryInfo): Set<ModuleSourceInfo>
companion object {
val Default = object : ResolutionAnchorCacheService {
override val resolutionAnchorsForLibraries: Map<LibraryInfo, ModuleSourceInfo>
get() = emptyMap()
override fun getDependencyResolutionAnchors(libraryInfo: LibraryInfo): Set<ModuleSourceInfo> = emptySet()
}
fun getInstance(project: Project): ResolutionAnchorCacheService =
ServiceManager.getService(project, ResolutionAnchorCacheService::class.java) ?: Default
}
}
@State(name = "KotlinIdeAnchorService", storages = [Storage("anchors.xml")])
class ResolutionAnchorCacheServiceImpl(val project: Project) :
ResolutionAnchorCacheService,
PersistentStateComponent<ResolutionAnchorCacheServiceImpl.State> {
data class State(
var moduleNameToAnchorName: Map<String, String> = emptyMap()
)
private val logger = logger<ResolutionAnchorCacheServiceImpl>()
@JvmField
@Volatile
var myState: State = State()
override fun getState(): State = myState
override fun loadState(state: State) {
XmlSerializerUtil.copyBean(state, myState)
}
object ResolutionAnchorMappingCacheKey
object ResolutionAnchorDependenciesCacheKey
override val resolutionAnchorsForLibraries: Map<LibraryInfo, ModuleSourceInfo> by lazy {
project.cacheByClassInvalidatingOnRootModifications(ResolutionAnchorMappingCacheKey::class.java) {
mapResolutionAnchorForLibraries()
}
}
private val resolutionAnchorDependenciesCache: MutableMap<LibraryInfo, Set<ModuleSourceInfo>> =
project.cacheByClassInvalidatingOnRootModifications(ResolutionAnchorDependenciesCacheKey::class.java) {
ContainerUtil.createConcurrentWeakMap()
}
override fun getDependencyResolutionAnchors(libraryInfo: LibraryInfo): Set<ModuleSourceInfo> {
return resolutionAnchorDependenciesCache.getOrPut(libraryInfo) {
val allTransitiveLibraryDependencies = with(LibraryDependenciesCache.getInstance(project)) {
val (directDependenciesOnLibraries, _) = getLibrariesAndSdksUsedWith(libraryInfo)
directDependenciesOnLibraries.closure { libraryDependency ->
getLibrariesAndSdksUsedWith(libraryDependency).first
}
}
allTransitiveLibraryDependencies.mapNotNull { resolutionAnchorsForLibraries[it] }.toSet()
}
}
private fun associateModulesByNames(): Map<String, ModuleInfo> {
return getModuleInfosFromIdeaModel(project).associateBy { moduleInfo ->
ProgressIndicatorAndCompilationCanceledStatus.checkCanceled()
when (moduleInfo) {
is LibraryInfo -> moduleInfo.library.name ?: "" // TODO: when does library have no name?
is ModuleSourceInfo -> moduleInfo.module.name
else -> moduleInfo.name.asString()
}
}
}
private fun mapResolutionAnchorForLibraries(): Map<LibraryInfo, ModuleSourceInfo> {
val modulesByNames: Map<String, ModuleInfo> = associateModulesByNames()
return myState.moduleNameToAnchorName.entries.mapNotNull { (libraryName, anchorName) ->
val library: LibraryInfo = modulesByNames[libraryName]?.takeIf { it is LibraryInfo }?.cast()
?: run {
logger.warn("Resolution anchor mapping key doesn't point to a known library: $libraryName. Skipping this anchor")
return@mapNotNull null
}
val anchor: ModuleSourceInfo = modulesByNames[anchorName]?.takeIf { it is ModuleSourceInfo }?.cast()
?: run {
logger.warn("Resolution anchor mapping value doesn't point to a source module: $anchorName. Skipping this anchor")
return@mapNotNull null
}
library to anchor
}.toMap()
}
}
@@ -50,7 +50,8 @@ abstract class KotlinCodeBlockModificationListenerCompat(protected val project:
lateinit var kotlinOutOfCodeBlockTracker: ModificationTracker
internal val perModuleOutOfCodeBlockTrackerUpdater = KotlinModuleOutOfCodeBlockModificationTracker.Updater(project)
// TODO: close back exposed API
val perModuleOutOfCodeBlockTrackerUpdater = KotlinModuleOutOfCodeBlockModificationTracker.Updater(project)
protected fun init(
treeAspect: TreeAspect,
@@ -32,6 +32,9 @@ class KotlinModuleOutOfCodeBlockModificationTracker private constructor(private
ModuleRootManager.getInstance(module).orderEntries().recursively().forEachModule(
CommonProcessors.CollectProcessor(resultModuleSet)
)
resultModuleSet.addAll(
ModuleDependencyProviderExtension.getInstance(module.project).getAdditionalDependencyModules(module)
)
}
}
}
@@ -65,7 +68,8 @@ class KotlinModuleOutOfCodeBlockModificationTracker private constructor(private
}
}
internal class Updater(project: Project) {
// TODO: close back exposed API
class Updater(project: Project) {
private val kotlinOfOfCodeBlockTracker by lazy {
KotlinCodeBlockModificationListener.getInstance(project).kotlinOutOfCodeBlockTracker
}
@@ -80,7 +84,8 @@ class KotlinModuleOutOfCodeBlockModificationTracker private constructor(private
// perModuleModCount map
private var perModuleChangesHighWatermark: Long? = null
internal fun getModificationCount(module: Module): Long {
// TODO: close back exposed API
fun getModificationCount(module: Module): Long {
return perModuleModCount[module] ?: perModuleChangesHighWatermark ?: kotlinOfOfCodeBlockTracker.modificationCount
}
@@ -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.idea.caches.trackers
import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
interface ModuleDependencyProviderExtension {
fun getAdditionalDependencyModules(module: Module): Collection<Module>
companion object {
val Default = object : ModuleDependencyProviderExtension {
override fun getAdditionalDependencyModules(module: Module): Collection<Module> = emptySet()
}
fun getInstance(project: Project): ModuleDependencyProviderExtension {
return ServiceManager.getService(project, ModuleDependencyProviderExtension::class.java) ?: Default
}
}
}
@@ -176,9 +176,15 @@
<projectService serviceInterface="org.jetbrains.kotlin.idea.util.ImportInsertHelper"
serviceImplementation="org.jetbrains.kotlin.idea.util.ImportInsertHelperImpl"/>
<projectService serviceInterface="org.jetbrains.kotlin.idea.caches.resolve.ResolutionAnchorCacheService"
serviceImplementation="org.jetbrains.kotlin.idea.caches.resolve.ResolutionAnchorCacheServiceImpl"/>
<projectService serviceInterface="org.jetbrains.kotlin.resolve.ResolutionAnchorProvider"
serviceImplementation="org.jetbrains.kotlin.idea.caches.resolve.KotlinIdeResolutionAnchorService"/>
<projectService serviceInterface="org.jetbrains.kotlin.idea.caches.trackers.ModuleDependencyProviderExtension"
serviceImplementation="org.jetbrains.kotlin.idea.caches.project.ResolutionAnchorModuleDependencyProviderExtension"/>
<projectService serviceImplementation="org.jetbrains.kotlin.idea.project.KotlinLibraryToSourceAnalysisStateComponent"/>
<projectService serviceInterface="org.jetbrains.kotlin.load.kotlin.MetadataFinderFactory"