From 18c4ae1b201e1c7d5f3944619368a402864cf6b7 Mon Sep 17 00:00:00 2001 From: Justin Paupore Date: Tue, 12 Aug 2025 16:50:58 -0700 Subject: [PATCH] [AA REx] Require a parent Disposable when creating resolve extensions. Per the documented API for Resolve Extensions, the `KaResolveExtension` instance is `Disposable`, and will be cleaned up by the creator when it is no longer necessary. This was handled by `LLFirSession` for the extensions created for analysis, but the extensions created by `KaResolveExtensionToContentScopeRefinerBridge` did not dispose their created extensions, causing resource leaks in REx clients. To fix this, and to enforce proper handling of the `Disposable` lifecycle for `KaResolveExtension` users, require passing a `Disposable` factory into `provideExtensionsFor`, which will be used as a parent disposable for any created extensions. For efficiency, the factory will not be called if no extensions are created. --- ...lveExtensionToContentScopeRefinerBridge.kt | 36 ++++++++++++++++--- .../api/analysis-api.undocumented | 1 - .../extensions/KaResolveExtensionProvider.kt | 22 ++++++++++-- .../projectStructure/sessionFactoryHelpers.kt | 6 +--- 4 files changed, 52 insertions(+), 13 deletions(-) diff --git a/analysis/analysis-api-impl-base/src/org/jetbrains/kotlin/analysis/api/impl/base/projectStructure/KaResolveExtensionToContentScopeRefinerBridge.kt b/analysis/analysis-api-impl-base/src/org/jetbrains/kotlin/analysis/api/impl/base/projectStructure/KaResolveExtensionToContentScopeRefinerBridge.kt index 3a08876bc7ad0..f274534d575d0 100644 --- a/analysis/analysis-api-impl-base/src/org/jetbrains/kotlin/analysis/api/impl/base/projectStructure/KaResolveExtensionToContentScopeRefinerBridge.kt +++ b/analysis/analysis-api-impl-base/src/org/jetbrains/kotlin/analysis/api/impl/base/projectStructure/KaResolveExtensionToContentScopeRefinerBridge.kt @@ -5,23 +5,49 @@ package org.jetbrains.kotlin.analysis.api.impl.base.projectStructure +import com.intellij.openapi.Disposable +import com.intellij.openapi.util.Disposer import com.intellij.psi.search.GlobalSearchScope import org.jetbrains.kotlin.analysis.api.KaImplementationDetail import org.jetbrains.kotlin.analysis.api.platform.projectStructure.KotlinContentScopeRefiner import org.jetbrains.kotlin.analysis.api.projectStructure.KaModule +import org.jetbrains.kotlin.analysis.api.resolve.extensions.KaResolveExtension import org.jetbrains.kotlin.analysis.api.resolve.extensions.KaResolveExtensionProvider @KaImplementationDetail class KaResolveExtensionToContentScopeRefinerBridge : KotlinContentScopeRefiner { override fun getEnlargementScopes(module: KaModule): List = - buildList { - if (KaResolveExtensionProvider.provideExtensionsFor(module).isNotEmpty()) { - add(KaBaseResolveExtensionGeneratedFilesScope(listOf(module))) + withResolveExtensionsFor(module) { extensions -> + if (extensions.isNotEmpty()) { + listOf(KaBaseResolveExtensionGeneratedFilesScope(listOf(module))) + } else { + listOf() } } override fun getRestrictionScopes(module: KaModule): List = - KaResolveExtensionProvider.provideExtensionsFor(module).map { resolveExtension -> - GlobalSearchScope.notScope(resolveExtension.getShadowedScope()) + withResolveExtensionsFor(module) { extensions -> + extensions.map { GlobalSearchScope.notScope(it.getShadowedScope()) } } + + companion object { + private inline fun withResolveExtensionsFor( + module: KaModule, + block: (List) -> T, + ): T { + var disposable: Disposable? = null + try { + val extensions = + KaResolveExtensionProvider.provideExtensionsFor(module) { + Disposer.newDisposable("KaResolveExtensionToContentScopeRefinerBridge") + .also { disposable = it } + } + return block(extensions) + } finally { + if (disposable != null) { + Disposer.dispose(disposable) + } + } + } + } } diff --git a/analysis/analysis-api/api/analysis-api.undocumented b/analysis/analysis-api/api/analysis-api.undocumented index fea00d0cdf55a..da57afebbd14d 100644 --- a/analysis/analysis-api/api/analysis-api.undocumented +++ b/analysis/analysis-api/api/analysis-api.undocumented @@ -569,7 +569,6 @@ org.jetbrains.kotlin.analysis.api.resolution.KaSymbolBasedReference org.jetbrains.kotlin.analysis.api.resolution.KaSymbolBasedReference.resolveToSymbols() org.jetbrains.kotlin.analysis.api.resolve.extensions.KaResolveExtensionNavigationTargetsProvider org.jetbrains.kotlin.analysis.api.resolve.extensions.KaResolveExtensionProvider.Companion.EP_NAME -org.jetbrains.kotlin.analysis.api.resolve.extensions.KaResolveExtensionProvider.Companion.provideExtensionsFor(KaModule) org.jetbrains.kotlin.analysis.api.scopes.KaScopeLike org.jetbrains.kotlin.analysis.api.session.KaSessionProvider.Companion.getInstance(Project) org.jetbrains.kotlin.analysis.api.session.KaSessionProvider.analyze(KaModule, KaSession.() -> R) diff --git a/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/resolve/extensions/KaResolveExtensionProvider.kt b/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/resolve/extensions/KaResolveExtensionProvider.kt index 9bc2fe2b29e12..9f1af56f5786d 100644 --- a/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/resolve/extensions/KaResolveExtensionProvider.kt +++ b/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/resolve/extensions/KaResolveExtensionProvider.kt @@ -5,7 +5,9 @@ package org.jetbrains.kotlin.analysis.api.resolve.extensions +import com.intellij.openapi.Disposable import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.util.Disposer import org.jetbrains.kotlin.analysis.api.KaExperimentalApi import org.jetbrains.kotlin.analysis.api.projectStructure.KaModule @@ -37,8 +39,24 @@ public abstract class KaResolveExtensionProvider { public val EP_NAME: ExtensionPointName = ExtensionPointName("org.jetbrains.kotlin.kaResolveExtensionProvider") - public fun provideExtensionsFor(module: KaModule): List { - return EP_NAME.getExtensionList(module.project).flatMap { it.provideExtensionsFor(module) } + /** + * Creates [resolve extensions][KaResolveExtension] for the provided [KaModule]. + * The [Disposable] provided by the factory will be used as a parent for all returned extensions. + * + * @param module The [KaModule] for which to create extensions. + * @param parentDisposableFactory A factory method to retrieve a parent [Disposable] for the returned + * extensions. This factory will only be invoked if at least one extension is created. + */ + public fun provideExtensionsFor( + module: KaModule, + parentDisposableFactory: () -> Disposable, + ): List { + val extensions = EP_NAME.getExtensionList(module.project).flatMap { it.provideExtensionsFor(module) } + if (extensions.isEmpty()) return emptyList() + + val parentDisposable = parentDisposableFactory() + extensions.forEach { Disposer.register(parentDisposable, it) } + return extensions } } } diff --git a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/projectStructure/sessionFactoryHelpers.kt b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/projectStructure/sessionFactoryHelpers.kt index eaab7aec379eb..ad5a6bd17a35d 100644 --- a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/projectStructure/sessionFactoryHelpers.kt +++ b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/projectStructure/sessionFactoryHelpers.kt @@ -6,7 +6,6 @@ package org.jetbrains.kotlin.analysis.low.level.api.fir.projectStructure import com.intellij.openapi.project.Project -import com.intellij.openapi.util.Disposer import org.jetbrains.kotlin.analysis.api.platform.declarations.createAnnotationResolver import org.jetbrains.kotlin.analysis.api.platform.projectStructure.KaResolutionScopeProvider import org.jetbrains.kotlin.analysis.api.platform.projectStructure.KotlinCompilerPluginsProvider @@ -62,14 +61,11 @@ internal fun LLFirSession.registerIdeComponents(project: Project, languageVersio private fun LLFirSession.registerResolveExtensionTool() { val resolveExtensionTool = createResolveExtensionTool() ?: return - // `KaResolveExtension`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? { - val extensions = KaResolveExtensionProvider.provideExtensionsFor(ktModule) + val extensions = KaResolveExtensionProvider.provideExtensionsFor(ktModule) { requestDisposable() } if (extensions.isEmpty()) return null return LLFirNonEmptyResolveExtensionTool(this, extensions) }