From 7a5a1833e7c1734fa7b46a161734c7bd83ce2191 Mon Sep 17 00:00:00 2001 From: Marco Pennekamp Date: Fri, 18 Aug 2023 19:11:41 +0200 Subject: [PATCH] [AA] Implement disposable `KtResolveExtension`s - 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 --- .../resolve/extensions/KtResolveExtension.kt | 33 ++++++++++++++++--- .../structure/sessionFactoryHelpers.kt | 15 +++++++-- .../extensions/LLFirResolveExtensionTool.kt | 4 ++- 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/resolve/extensions/KtResolveExtension.kt b/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/resolve/extensions/KtResolveExtension.kt index 2ab3db43052..a611e422e45 100644 --- a/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/resolve/extensions/KtResolveExtension.kt +++ b/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/resolve/extensions/KtResolveExtension.kt @@ -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 -} \ No newline at end of file + + override fun dispose() { + } +} diff --git a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/project/structure/sessionFactoryHelpers.kt b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/project/structure/sessionFactoryHelpers.kt index 1c41c37d574..0d6e284372a 100644 --- a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/project/structure/sessionFactoryHelpers.kt +++ b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/project/structure/sessionFactoryHelpers.kt @@ -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? { diff --git a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/resolve/extensions/LLFirResolveExtensionTool.kt b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/resolve/extensions/LLFirResolveExtensionTool.kt index 2a4c3439e06..6cea0df24c1 100644 --- a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/resolve/extensions/LLFirResolveExtensionTool.kt +++ b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/resolve/extensions/LLFirResolveExtensionTool.kt @@ -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 + 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, + override val extensions: List, ) : LLFirResolveExtensionTool() { init { require(extensions.isNotEmpty())