[AA] KT-58257 Improve modification event API and documentation

- Module state modification events now have a modification kind, which
  allows adding additional kinds of modification in the future (e.g.
  separating module property and content root updates, if the workspace
  model ever supports it).
  - Splitting off modification kinds was also a good opportunity to
    better document when module state modification occurs.
- Changed the documentation of modification events to be (1) less
  reliant on the IDE implementation and (2) more detailed in the
  intended effect of each event.
- Removed the notion of "stable" modules again and replaced it with
  "libraries". Even though an SDK is technically not a library, the
  term "library modules" should be more friendly to API consumers.
This commit is contained in:
Marco Pennekamp
2023-07-18 16:43:12 +02:00
committed by Space Team
parent d2b05b8f4f
commit 03c8654fe3
9 changed files with 78 additions and 38 deletions
@@ -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()
}
@@ -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()
}
@@ -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()
}
@@ -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)
}
@@ -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,
}
@@ -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
@@ -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 }
}
}
@@ -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() {
@@ -64,11 +64,3 @@ public inline fun <reified M : KtModule> KtModule.allDirectDependenciesOfType():
*/
public fun computeTransitiveDependsOnDependencies(directDependsOnDependencies: List<KtModule>): List<KtModule> =
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