diff --git a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/project/IdeaModuleInfos.kt b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/project/IdeaModuleInfos.kt index 35c143ec517..17f245ed3bd 100644 --- a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/project/IdeaModuleInfos.kt +++ b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/project/IdeaModuleInfos.kt @@ -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 = 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 { diff --git a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/project/ResolutionAnchorAwareLibraryModificationTracker.kt b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/project/ResolutionAnchorAwareLibraryModificationTracker.kt new file mode 100644 index 00000000000..21c6c0add9b --- /dev/null +++ b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/project/ResolutionAnchorAwareLibraryModificationTracker.kt @@ -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 + } +} diff --git a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/project/ResolutionAnchorModuleDependencyProviderExtension.kt b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/project/ResolutionAnchorModuleDependencyProviderExtension.kt new file mode 100644 index 00000000000..3cad4070db4 --- /dev/null +++ b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/project/ResolutionAnchorModuleDependencyProviderExtension.kt @@ -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 { + if (!project.libraryToSourceAnalysisEnabled) return emptySet() + + val resolutionAnchorDependencies = HashSet() + ModuleRootManager.getInstance(module).orderEntries().recursively().forEachLibrary { library -> + getIdeaModelInfosCache(project).getLibraryInfosForLibrary(library).flatMapTo(resolutionAnchorDependencies) { libraryInfo -> + ResolutionAnchorCacheService.getInstance(project).getDependencyResolutionAnchors(libraryInfo) + } + true + } + + val additionalModules = HashSet() + for (anchorModule in resolutionAnchorDependencies) { + ModuleRootManager.getInstance(anchorModule.module).orderEntries().recursively() + .forEachModule(CommonProcessors.CollectProcessor(additionalModules)) + } + return additionalModules + } +} diff --git a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/KotlinIdeResolutionAnchorService.kt b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/KotlinIdeResolutionAnchorService.kt index a3f359c5f96..5cefae95819 100644 --- a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/KotlinIdeResolutionAnchorService.kt +++ b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/KotlinIdeResolutionAnchorService.kt @@ -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 { - - data class State( - var moduleNameToAnchorName: Map = emptyMap() - ) - - private val logger = logger() - - @JvmField - @Volatile - var myState: State = State() - - private fun buildMapping(): Map { - 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 - 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 diff --git a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/ResolutionAnchorCacheService.kt b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/ResolutionAnchorCacheService.kt new file mode 100644 index 00000000000..3c09cdb023a --- /dev/null +++ b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/ResolutionAnchorCacheService.kt @@ -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 + + fun getDependencyResolutionAnchors(libraryInfo: LibraryInfo): Set + + companion object { + val Default = object : ResolutionAnchorCacheService { + override val resolutionAnchorsForLibraries: Map + get() = emptyMap() + + override fun getDependencyResolutionAnchors(libraryInfo: LibraryInfo): Set = 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 { + + data class State( + var moduleNameToAnchorName: Map = emptyMap() + ) + + private val logger = logger() + + @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 by lazy { + project.cacheByClassInvalidatingOnRootModifications(ResolutionAnchorMappingCacheKey::class.java) { + mapResolutionAnchorForLibraries() + } + } + + private val resolutionAnchorDependenciesCache: MutableMap> = + project.cacheByClassInvalidatingOnRootModifications(ResolutionAnchorDependenciesCacheKey::class.java) { + ContainerUtil.createConcurrentWeakMap() + } + + override fun getDependencyResolutionAnchors(libraryInfo: LibraryInfo): Set { + 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 { + 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 { + val modulesByNames: Map = 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() + } +} diff --git a/idea/idea-frontend-independent/src/org/jetbrains/kotlin/idea/caches/trackers/KotlinCodeBlockModificationListenerCompat.kt b/idea/idea-frontend-independent/src/org/jetbrains/kotlin/idea/caches/trackers/KotlinCodeBlockModificationListenerCompat.kt index 9c6521beb74..364aadf02a7 100644 --- a/idea/idea-frontend-independent/src/org/jetbrains/kotlin/idea/caches/trackers/KotlinCodeBlockModificationListenerCompat.kt +++ b/idea/idea-frontend-independent/src/org/jetbrains/kotlin/idea/caches/trackers/KotlinCodeBlockModificationListenerCompat.kt @@ -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, diff --git a/idea/idea-frontend-independent/src/org/jetbrains/kotlin/idea/caches/trackers/KotlinModuleOutOfCodeBlockModificationTracker.kt b/idea/idea-frontend-independent/src/org/jetbrains/kotlin/idea/caches/trackers/KotlinModuleOutOfCodeBlockModificationTracker.kt index 44d4b6e0e21..2e5d56055e4 100644 --- a/idea/idea-frontend-independent/src/org/jetbrains/kotlin/idea/caches/trackers/KotlinModuleOutOfCodeBlockModificationTracker.kt +++ b/idea/idea-frontend-independent/src/org/jetbrains/kotlin/idea/caches/trackers/KotlinModuleOutOfCodeBlockModificationTracker.kt @@ -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 } diff --git a/idea/idea-frontend-independent/src/org/jetbrains/kotlin/idea/caches/trackers/ModuleDependencyProviderExtension.kt b/idea/idea-frontend-independent/src/org/jetbrains/kotlin/idea/caches/trackers/ModuleDependencyProviderExtension.kt new file mode 100644 index 00000000000..4ebd52593c6 --- /dev/null +++ b/idea/idea-frontend-independent/src/org/jetbrains/kotlin/idea/caches/trackers/ModuleDependencyProviderExtension.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.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 + + companion object { + val Default = object : ModuleDependencyProviderExtension { + override fun getAdditionalDependencyModules(module: Module): Collection = emptySet() + } + + fun getInstance(project: Project): ModuleDependencyProviderExtension { + return ServiceManager.getService(project, ModuleDependencyProviderExtension::class.java) ?: Default + } + } +} \ No newline at end of file diff --git a/idea/resources/META-INF/plugin-common.xml b/idea/resources/META-INF/plugin-common.xml index 5177fed0af4..e9c33025965 100644 --- a/idea/resources/META-INF/plugin-common.xml +++ b/idea/resources/META-INF/plugin-common.xml @@ -176,9 +176,15 @@ + + + +