[AA] Implement disposable KtResolveExtensions

- This commit adds the `Disposable` interface to `KtResolveExtension`.
  Resolve extensions are disposed after their associated session has
  been invalidated, or reclaimed by the GC.
- Example use case: Opening a message bus connection with the lifetime
  of the resolve extension.

^KT-61222 fixed
This commit is contained in:
Marco Pennekamp
2023-08-18 19:11:41 +02:00
committed by Space Team
parent efb56ed30a
commit 7a5a1833e7
3 changed files with 43 additions and 9 deletions
@@ -5,23 +5,43 @@
package org.jetbrains.kotlin.analysis.api.resolve.extensions
import com.intellij.openapi.Disposable
import com.intellij.psi.search.GlobalSearchScope
import org.jetbrains.kotlin.name.FqName
/**
* Provides a list of Kotlin files which provides additional, generated declarations for resolution.
* Provides a list of Kotlin files which contain additional generated declarations for resolution.
*
* Provided by the [KtResolveExtensionProvider].
* It is created by [KtResolveExtensionProvider].
*
* All member implementations should:
* - consider caching the results for subsequent invocations.
* - be lightweight and not build the whole file structure inside.
* - not use the Kotlin resolve inside, as this function is called during session initialization, so Analysis API access is forbidden.
* - not use Kotlin resolve inside, as these functions are called during session initialization, so Analysis API access is forbidden.
*
* #### Lifecycle Management
*
* A [KtResolveExtension] is tied to the lifetime of its module's analysis session. It is created by [KtResolveExtensionProvider] during
* creation of an analysis session, and disposed after the analysis session has been invalidated.
*
* [KtResolveExtension] implements the [Disposable] interface. The resolve extension can then act as a parent disposable, e.g. for a message
* bus connection.
*
* You *must not* implement [KtResolveExtension]s as module-level services, due to the following reasons:
*
* 1. The IntelliJ platform SDK [discourages](https://plugins.jetbrains.com/docs/intellij/plugin-services.html#types) the use of
* module-level services due to memory consumption. In particular, resolve extensions implemented as module-level services live longer
* than their corresponding analysis session, so the resolve extension would not be garbage collected after its corresponding analysis
* session has been invalidated.
* 2. The module-level service living longer than the analysis session increases the risk of caching invalidated entities in a resolve
* extension.
* 3. Because the [KtResolveExtension] is a [Disposable], if implemented as a module-level service, the service would be disposed too early
* during invalidation of the corresponding analysis session.
*
* @see KtResolveExtensionFile
* @see KtResolveExtensionProvider
*/
public abstract class KtResolveExtension {
public abstract class KtResolveExtension : Disposable {
/**
* Get the list of files that should be generated for the module. Returned files should contain valid Kotlin code.
*
@@ -64,4 +84,7 @@ public abstract class KtResolveExtension {
* (potentially stale) externally generated sources.
*/
public open fun getShadowedScope(): GlobalSearchScope = GlobalSearchScope.EMPTY_SCOPE
}
override fun dispose() {
}
}
@@ -6,6 +6,7 @@
package org.jetbrains.kotlin.analysis.low.level.api.fir.project.structure
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import org.jetbrains.kotlin.analysis.api.resolve.extensions.KtResolveExtensionProvider
import org.jetbrains.kotlin.analysis.low.level.api.fir.api.services.createSealedInheritorsProvider
import org.jetbrains.kotlin.analysis.low.level.api.fir.compile.CodeFragmentScopeProvider
@@ -39,9 +40,17 @@ internal fun LLFirSession.registerIdeComponents(project: Project) {
register(SealedClassInheritorsProvider::class, createSealedInheritorsProvider(project))
register(FirExceptionHandler::class, LLFirExceptionHandler)
register(CodeFragmentScopeProvider::class, CodeFragmentScopeProvider(this))
createResolveExtensionTool()?.let {
register(LLFirResolveExtensionTool::class, it)
}
registerResolveExtensionTool()
}
@SessionConfiguration
private fun LLFirSession.registerResolveExtensionTool() {
val resolveExtensionTool = createResolveExtensionTool() ?: return
// `KtResolveExtension`s are disposables meant to be tied to the lifetime of the `LLFirSession`.
resolveExtensionTool.extensions.forEach { Disposer.register(requestDisposable(), it) }
register(LLFirResolveExtensionTool::class, resolveExtensionTool)
}
private fun LLFirSession.createResolveExtensionTool(): LLFirResolveExtensionTool? {
@@ -37,6 +37,8 @@ import java.util.concurrent.ConcurrentHashMap
* for the [org.jetbrains.kotlin.fir.resolve.providers.FirSymbolProvider].
*/
abstract class LLFirResolveExtensionTool : FirSessionComponent {
internal abstract val extensions: List<KtResolveExtension>
abstract val declarationProvider: LLFirResolveExtensionToolDeclarationProvider
abstract val packageProvider: KotlinPackageProvider
abstract val packageFilter: LLFirResolveExtensionToolPackageFilter
@@ -48,7 +50,7 @@ val FirSession.llResolveExtensionTool: LLFirResolveExtensionTool? by FirSession.
internal class LLFirNonEmptyResolveExtensionTool(
session: LLFirSession,
extensions: List<KtResolveExtension>,
override val extensions: List<KtResolveExtension>,
) : LLFirResolveExtensionTool() {
init {
require(extensions.isNotEmpty())