diff --git a/analysis/analysis-api-providers/src/org/jetbrains/kotlin/analysis/providers/topics/KotlinGlobalModuleStateModificationListener.kt b/analysis/analysis-api-providers/src/org/jetbrains/kotlin/analysis/providers/topics/KotlinGlobalModuleStateModificationListener.kt index 410799c3a52..f42f98fe7f3 100644 --- a/analysis/analysis-api-providers/src/org/jetbrains/kotlin/analysis/providers/topics/KotlinGlobalModuleStateModificationListener.kt +++ b/analysis/analysis-api-providers/src/org/jetbrains/kotlin/analysis/providers/topics/KotlinGlobalModuleStateModificationListener.kt @@ -9,9 +9,13 @@ import org.jetbrains.kotlin.analysis.project.structure.KtModule public fun interface KotlinGlobalModuleStateModificationListener { /** - * [onModification] is invoked in a write action before or after global modification of the module state of all [KtModule]s. + * [onModification] is invoked in a write action before or after global module state modification. * - * This event is published after SDK removal and to invalidate caches during/between tests. + * The module structure, source code, and binary content of all [KtModule]s in the project should be considered modified when this event + * is received. This includes source files being moved or removed, binary content being added, removed, or changed, and modules possibly + * being removed. Thus, all caches related to module structure, source code, and binaries should be invalidated. + * + * @see KotlinTopics */ public fun onModification() } diff --git a/analysis/analysis-api-providers/src/org/jetbrains/kotlin/analysis/providers/topics/KotlinGlobalSourceModuleStateModificationListener.kt b/analysis/analysis-api-providers/src/org/jetbrains/kotlin/analysis/providers/topics/KotlinGlobalSourceModuleStateModificationListener.kt index be7b112c605..c22b6c7ac7c 100644 --- a/analysis/analysis-api-providers/src/org/jetbrains/kotlin/analysis/providers/topics/KotlinGlobalSourceModuleStateModificationListener.kt +++ b/analysis/analysis-api-providers/src/org/jetbrains/kotlin/analysis/providers/topics/KotlinGlobalSourceModuleStateModificationListener.kt @@ -9,10 +9,16 @@ import org.jetbrains.kotlin.analysis.project.structure.KtModule public fun interface KotlinGlobalSourceModuleStateModificationListener { /** - * [onModification] is invoked in a write action before or after global modification of the module state of all source [KtModule]s, - * excluding [stable][org.jetbrains.kotlin.analysis.project.structure.isStableModule] modules. + * [onModification] is invoked in a write action before or after global source module state modification. * - * This event is published to invalidate caches during/between tests. + * The module structure and source code of all source [KtModule]s in the project should be considered modified when this event is + * received. This includes source files being moved or removed, and source modules possibly being removed. Thus, all caches related to + * source module structure and source code should be invalidated. + * + * Library modules (including library sources) do not need to be considered modified, so any caches related to library modules and their + * contents may be kept. + * + * @see KotlinTopics */ public fun onModification() } diff --git a/analysis/analysis-api-providers/src/org/jetbrains/kotlin/analysis/providers/topics/KotlinGlobalSourceOutOfBlockModificationListener.kt b/analysis/analysis-api-providers/src/org/jetbrains/kotlin/analysis/providers/topics/KotlinGlobalSourceOutOfBlockModificationListener.kt index 25765a594d1..ba657e75917 100644 --- a/analysis/analysis-api-providers/src/org/jetbrains/kotlin/analysis/providers/topics/KotlinGlobalSourceOutOfBlockModificationListener.kt +++ b/analysis/analysis-api-providers/src/org/jetbrains/kotlin/analysis/providers/topics/KotlinGlobalSourceOutOfBlockModificationListener.kt @@ -9,10 +9,15 @@ import org.jetbrains.kotlin.analysis.project.structure.KtModule public fun interface KotlinGlobalSourceOutOfBlockModificationListener { /** - * [onModification] is invoked in a write action before or after global out-of-block modification of all source [KtModule]s, excluding - * [stable][org.jetbrains.kotlin.analysis.project.structure.isStableModule] modules. + * [onModification] is invoked in a write action before or after global out-of-block modification of all sources. * - * This event is published on global PSI changes. + * The source code of all source [KtModule]s in the project should be considered modified when this event is received. This includes + * source files being moved or removed. Thus, all caches related to source code and source files should be invalidated. + * + * Library modules (including library sources) do not need to be considered modified, so any caches related to library modules and their + * contents may be kept. + * + * @see KotlinTopics */ public fun onModification() } diff --git a/analysis/analysis-api-providers/src/org/jetbrains/kotlin/analysis/providers/topics/KotlinModuleOutOfBlockModificationListener.kt b/analysis/analysis-api-providers/src/org/jetbrains/kotlin/analysis/providers/topics/KotlinModuleOutOfBlockModificationListener.kt index 6b09916e7ed..c7b1b429531 100644 --- a/analysis/analysis-api-providers/src/org/jetbrains/kotlin/analysis/providers/topics/KotlinModuleOutOfBlockModificationListener.kt +++ b/analysis/analysis-api-providers/src/org/jetbrains/kotlin/analysis/providers/topics/KotlinModuleOutOfBlockModificationListener.kt @@ -17,6 +17,8 @@ public fun interface KotlinModuleOutOfBlockModificationListener { * * This event may be published for any and all source code changes, not just out-of-block modifications, to simplify the implementation * of modification detection. + * + * @see KotlinTopics */ public fun onModification(module: KtModule) } diff --git a/analysis/analysis-api-providers/src/org/jetbrains/kotlin/analysis/providers/topics/KotlinModuleStateModificationListener.kt b/analysis/analysis-api-providers/src/org/jetbrains/kotlin/analysis/providers/topics/KotlinModuleStateModificationListener.kt index 9ec91bab451..25452d55a04 100644 --- a/analysis/analysis-api-providers/src/org/jetbrains/kotlin/analysis/providers/topics/KotlinModuleStateModificationListener.kt +++ b/analysis/analysis-api-providers/src/org/jetbrains/kotlin/analysis/providers/topics/KotlinModuleStateModificationListener.kt @@ -9,8 +9,29 @@ import org.jetbrains.kotlin.analysis.project.structure.KtModule public fun interface KotlinModuleStateModificationListener { /** - * [onModification] is invoked in a write action *before* the [module] is moved, removed, or its structure is changed. [isRemoval] - * signals if the event is a removal. + * [onModification] is invoked in a write action *before* the [module] is updated or removed (see [modificationKind] for specifics). + * + * @see KotlinTopics */ - public fun onModification(module: KtModule, isRemoval: Boolean) + public fun onModification(module: KtModule, modificationKind: KotlinModuleStateModificationKind) +} + +public enum class KotlinModuleStateModificationKind { + /** + * The [KtModule]'s properties or references to other modules are being changed. + * + * #### Examples + * + * - The name of the module is being changed. + * - The module's content roots are being changed, such as adding another source folder to a source module. + * - If module A depends on module B and module B is being removed, in addition to the removal event for module B, module A also + * receives an update event. + */ + UPDATE, + + /** + * The [KtModule] is being removed. Because this event is published before the removal, the [KtModule] can still be accessed to clear + * caches. It should be removed from any caches managed by the subscriber to avoid stale or broken keys/values. + */ + REMOVAL, } diff --git a/analysis/analysis-api-providers/src/org/jetbrains/kotlin/analysis/providers/topics/KotlinTopics.kt b/analysis/analysis-api-providers/src/org/jetbrains/kotlin/analysis/providers/topics/KotlinTopics.kt index cd68d5c6e03..2ff2b5e0cef 100644 --- a/analysis/analysis-api-providers/src/org/jetbrains/kotlin/analysis/providers/topics/KotlinTopics.kt +++ b/analysis/analysis-api-providers/src/org/jetbrains/kotlin/analysis/providers/topics/KotlinTopics.kt @@ -20,15 +20,24 @@ import org.jetbrains.kotlin.analysis.providers.analysisMessageBus * - [KotlinGlobalSourceModuleStateModificationListener] * - [KotlinGlobalSourceOutOfBlockModificationListener] * - * Modification events may be published before or after a modification, so subscribers should not assume that the modification has or hasn't - * happened yet. The reason for this design decision is that the underlying events (such as PSI tree changes or workspace model events) are - * sometimes published before and sometimes after a change, or even both. Modification events published before the modification should - * however be published close to the modification. - * * Care needs to be taken with the lack of interplay between different types of topics: Publishing a global modification event, for example, * does not imply the corresponding module-level event. Similarly, publishing a module state modification event does not imply out-of-block * modification. * + * Global modification events are published when it's not feasible or desired to publish events for a single module, or a limited set of + * modules. For example, a change in the environment such as removing an SDK might affect all modules, so a global event is more + * appropriate. + * + * #### Timing Guarantees + * + * Most modification events may be published before or after a modification, so subscribers should not assume that the modification has or + * hasn't happened yet. The reason for this design decision is that some of the underlying events (such as PSI tree changes) may be + * published before and after a change, or even both. Modification events published before the modification should however be published + * close to the modification. + * + * Only [module state modification events][KotlinModuleStateModificationListener] guarantee that the event is published before the module is + * affected. This allows subscribers to access the module's properties and dependencies to invalidate or update caches. + * * #### Implementation Notes * * Analysis API implementations need to take care of publishing to these topics via the [analysisMessageBus]. In general, if your tool works diff --git a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/sessions/LLFirSessionCache.kt b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/sessions/LLFirSessionCache.kt index b49a771cb8d..2141f863852 100644 --- a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/sessions/LLFirSessionCache.kt +++ b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/sessions/LLFirSessionCache.kt @@ -91,19 +91,20 @@ class LLFirSessionCache(private val project: Project) { } /** - * Removes all sessions after global invalidation. If [includeStableModules] is `false`, sessions of stable modules will not be removed. + * Removes all sessions after global invalidation. If [includeLibraryModules] is `false`, sessions of library modules will not be + * removed. * * [removeAllSessions] must be called in a write action. */ - fun removeAllSessions(includeStableModules: Boolean) { + fun removeAllSessions(includeLibraryModules: Boolean) { ApplicationManager.getApplication().assertWriteAccessAllowed() - if (includeStableModules) { + if (includeLibraryModules) { removeAllSessionsFrom(sourceCache) removeAllSessionsFrom(binaryCache) } else { - // `binaryCache` can only contain binary and thus stable modules, so we only need to remove sessions from `sourceCache`. - removeAllMatchingSessionsFrom(sourceCache) { !it.isStableModule } + // `binaryCache` can only contain library modules, so we only need to remove sessions from `sourceCache`. + removeAllMatchingSessionsFrom(sourceCache) { it !is KtBinaryModule && it !is KtLibrarySourceModule } } } diff --git a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/sessions/LLFirSessionInvalidationService.kt b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/sessions/LLFirSessionInvalidationService.kt index ba7b7ff016b..c873cc50403 100644 --- a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/sessions/LLFirSessionInvalidationService.kt +++ b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/sessions/LLFirSessionInvalidationService.kt @@ -41,15 +41,15 @@ class LLFirSessionInvalidationService(private val project: Project) : Disposable ) busConnection.subscribe( KotlinTopics.GLOBAL_MODULE_STATE_MODIFICATION, - KotlinGlobalModuleStateModificationListener { invalidateAll(includeStableModules = true) } + KotlinGlobalModuleStateModificationListener { invalidateAll(includeLibraryModules = true) } ) busConnection.subscribe( KotlinTopics.GLOBAL_SOURCE_MODULE_STATE_MODIFICATION, - KotlinGlobalSourceModuleStateModificationListener { invalidateAll(includeStableModules = false) }, + KotlinGlobalSourceModuleStateModificationListener { invalidateAll(includeLibraryModules = false) }, ) busConnection.subscribe( KotlinTopics.GLOBAL_SOURCE_OUT_OF_BLOCK_MODIFICATION, - KotlinGlobalSourceOutOfBlockModificationListener { invalidateAll(includeStableModules = false) }, + KotlinGlobalSourceOutOfBlockModificationListener { invalidateAll(includeLibraryModules = false) }, ) } @@ -81,21 +81,21 @@ class LLFirSessionInvalidationService(private val project: Project) : Disposable } } - private fun invalidateAll(includeStableModules: Boolean) { + private fun invalidateAll(includeLibraryModules: Boolean) { ApplicationManager.getApplication().assertWriteAccessAllowed() - // When anchor modules are configured and `includeStableModules` is `false`, we get a situation where the anchor module session will - // be invalidated (because it is a source session), while its library dependents won't be invalidated (because they are sessions of - // stable modules). But such library sessions also need to be invalidated because they depend on the anchor module. + // When anchor modules are configured and `includeLibraryModules` is `false`, we get a situation where the anchor module session + // will be invalidated (because it is a source session), while its library dependents won't be invalidated. But such library + // sessions also need to be invalidated because they depend on the anchor module. // // Invalidating anchor modules before all source sessions has the advantage that `invalidate`'s session existence check will work, // so we do not have to invalidate dependent sessions if the anchor module does not exist in the first place. - if (!includeStableModules) { + if (!includeLibraryModules) { val anchorModules = KotlinAnchorModuleProvider.getInstance(project)?.getAllAnchorModules() anchorModules?.forEach(::invalidate) } - LLFirSessionCache.getInstance(project).removeAllSessions(includeStableModules) + LLFirSessionCache.getInstance(project).removeAllSessions(includeLibraryModules) } override fun dispose() { diff --git a/analysis/project-structure/src/org/jetbrains/kotlin/analysis/project/structure/ktModuleUtils.kt b/analysis/project-structure/src/org/jetbrains/kotlin/analysis/project/structure/ktModuleUtils.kt index a9d90521ce9..ea85e161a75 100644 --- a/analysis/project-structure/src/org/jetbrains/kotlin/analysis/project/structure/ktModuleUtils.kt +++ b/analysis/project-structure/src/org/jetbrains/kotlin/analysis/project/structure/ktModuleUtils.kt @@ -64,11 +64,3 @@ public inline fun KtModule.allDirectDependenciesOfType(): */ public fun computeTransitiveDependsOnDependencies(directDependsOnDependencies: List): List = topologicalSort(directDependsOnDependencies) { this.directDependsOnDependencies } - -/** - * A stable [KtModule] does not contain conventionally *editable* sources and cannot cause or be affected by out-of-block modification. - * - * Stable modules may still be subject to modification (e.g. when a JAR is added to a library), but in such cases, module state modification - * events will be raised. - */ -public val KtModule.isStableModule: Boolean get() = this is KtBinaryModule || this is KtLibrarySourceModule