diff --git a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/KtFirAnalysisSessionProvider.kt b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/KtFirAnalysisSessionProvider.kt index fc0e3ef9826..30e7d297e8d 100644 --- a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/KtFirAnalysisSessionProvider.kt +++ b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/KtFirAnalysisSessionProvider.kt @@ -20,8 +20,10 @@ import org.jetbrains.kotlin.analysis.api.session.KtAnalysisSessionProvider import org.jetbrains.kotlin.analysis.low.level.api.fir.LLFirInternals import org.jetbrains.kotlin.analysis.low.level.api.fir.api.getFirResolveSession import org.jetbrains.kotlin.analysis.low.level.api.fir.file.structure.LLFirDeclarationModificationService +import org.jetbrains.kotlin.analysis.project.structure.KtDanglingFileModule import org.jetbrains.kotlin.analysis.project.structure.KtModule import org.jetbrains.kotlin.analysis.project.structure.ProjectStructureProvider +import org.jetbrains.kotlin.analysis.project.structure.isStable import org.jetbrains.kotlin.analysis.providers.createProjectWideOutOfBlockModificationTracker import org.jetbrains.kotlin.psi.KtElement import java.util.concurrent.ConcurrentHashMap @@ -42,6 +44,12 @@ class KtFirAnalysisSessionProvider(project: Project) : KtAnalysisSessionProvider } override fun getAnalysisSessionByUseSiteKtModule(useSiteKtModule: KtModule): KtAnalysisSession { + if (useSiteKtModule is KtDanglingFileModule && !useSiteKtModule.isStable) { + val firResolveSession = useSiteKtModule.getFirResolveSession(project) + val validityToken = tokenFactory.create(project) + return KtFirAnalysisSession.createAnalysisSessionByFirResolveSession(firResolveSession, validityToken) + } + val identifier = tokenFactory.identifier identifier.flushPendingChanges(project) diff --git a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/sessions/LLFirAbstractSessionFactory.kt b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/sessions/LLFirAbstractSessionFactory.kt index e90a9e74702..f8e547e1874 100644 --- a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/sessions/LLFirAbstractSessionFactory.kt +++ b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/sessions/LLFirAbstractSessionFactory.kt @@ -50,6 +50,8 @@ import org.jetbrains.kotlin.resolve.jvm.modules.JavaModuleResolver import org.jetbrains.kotlin.scripting.compiler.plugin.FirScriptingSamWithReceiverExtensionRegistrar import org.jetbrains.kotlin.scripting.definitions.findScriptDefinition import org.jetbrains.kotlin.utils.exceptions.errorWithAttachment +import org.jetbrains.kotlin.utils.exceptions.requireWithAttachment +import org.jetbrains.kotlin.utils.exceptions.withPsiEntry import org.jetbrains.kotlin.utils.exceptions.withVirtualFileEntry import kotlin.script.experimental.host.ScriptingHostConfiguration import kotlin.script.experimental.jvm.defaultJvmScriptingHostConfiguration @@ -466,14 +468,16 @@ internal abstract class LLFirAbstractSessionFactory(protected val project: Proje contextSession: LLFirSession, additionalSessionConfiguration: context(DanglingFileSessionCreationContext) LLFirDanglingFileSession.() -> Unit, ): LLFirSession { + val danglingFile = module.file val platform = module.platform + val builtinsSession = LLFirBuiltinsSessionFactory.getInstance(project).getBuiltinsSession(platform) val languageVersionSettings = wrapLanguageVersionSettings(contextSession.languageVersionSettings) val scopeProvider = FirKotlinScopeProvider(::wrapScopeWithJvmMapped) val components = LLFirModuleResolveComponents(module, globalResolveComponents, scopeProvider) - val session = LLFirDanglingFileSession(module, components, builtinsSession.builtinTypes) + val session = LLFirDanglingFileSession(module, components, builtinsSession.builtinTypes, danglingFile.modificationStamp) components.session = session val moduleData = createModuleData(session) @@ -490,7 +494,7 @@ internal abstract class LLFirAbstractSessionFactory(protected val project: Proje this, components, canContainKotlinPackage = true, - declarationProviderFactory = { scope -> scope.createScopedDeclarationProviderForFile(module.file) } + declarationProviderFactory = { scope -> scope.createScopedDeclarationProviderForFile(danglingFile) } ) register(FirProvider::class, firProvider) @@ -575,8 +579,19 @@ internal abstract class LLFirAbstractSessionFactory(protected val project: Proje fun getOrCreateSessionForDependency(dependency: KtModule): LLFirSession? = when (dependency) { is KtBuiltinsModule -> null // Built-ins are already added + is KtBinaryModule -> llFirSessionCache.getSession(dependency, preferBinary = true) - is KtSourceModule, is KtDanglingFileModule -> llFirSessionCache.getSession(dependency) + + is KtSourceModule -> llFirSessionCache.getSession(dependency) + + is KtDanglingFileModule -> { + requireWithAttachment(dependency.isStable, message = { "Unstable dangling modules cannot be used as a dependency" }) { + withKtModuleEntry("module", module) + withKtModuleEntry("dependency", dependency) + withPsiEntry("dependencyFile", dependency.file) + } + llFirSessionCache.getSession(dependency) + } is KtScriptModule, is KtScriptDependencyModule, diff --git a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/sessions/LLFirDanglingFileSession.kt b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/sessions/LLFirDanglingFileSession.kt index 172d189115e..1f0dab0b67c 100644 --- a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/sessions/LLFirDanglingFileSession.kt +++ b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/sessions/LLFirDanglingFileSession.kt @@ -14,4 +14,5 @@ internal class LLFirDanglingFileSession @PrivateSessionConstructor constructor( ktModule: KtDanglingFileModule, override val moduleComponents: LLFirModuleResolveComponents, builtinTypes: BuiltinTypes, + val modificationStamp: Long ) : LLFirResolvableModuleSession(ktModule, builtinTypes) 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 f8fc6089e88..ccb9542f0ba 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 @@ -36,6 +36,7 @@ class LLFirSessionCache(private val project: Project) { private val sourceCache: SessionStorage = CollectionFactory.createConcurrentSoftValueMap() private val binaryCache: SessionStorage = CollectionFactory.createConcurrentSoftValueMap() private val danglingFileSessionCache: SessionStorage = CollectionFactory.createConcurrentSoftValueMap() + private val unstableDanglingFileSessionCache: SessionStorage = CollectionFactory.createConcurrentSoftValueMap() /** * Returns the existing session if found, or creates a new session and caches it. @@ -48,12 +49,11 @@ class LLFirSessionCache(private val project: Project) { } } - val targetCache = when (module) { - is KtDanglingFileModule -> danglingFileSessionCache - else -> sourceCache + if (module is KtDanglingFileModule) { + return getDanglingFileCachedSession(module) } - return getCachedSession(module, targetCache, ::createSession) + return getCachedSession(module, sourceCache, factory = ::createSession) } /** @@ -64,16 +64,38 @@ class LLFirSessionCache(private val project: Project) { return createSession(module) } - private fun getCachedSession( - module: T, - storage: SessionStorage, - factory: (T) -> LLFirSession - ): LLFirSession { + private fun getDanglingFileCachedSession(module: KtDanglingFileModule): LLFirSession { + if (module.isStable) { + return getCachedSession(module, danglingFileSessionCache, ::createSession) + } + checkCanceled() - return storage.computeIfAbsent(module) { factory(module) }.also { session -> - require(session.isValid) { "A session acquired via `getSession` should always be valid. Module: $module" } + val session = unstableDanglingFileSessionCache.compute(module) { _, existingSession -> + if (existingSession is LLFirDanglingFileSession && existingSession.modificationStamp == module.file.modificationStamp) { + existingSession + } else { + createSession(module) + } } + + requireNotNull(session) + checkSessionValidity(session) + + return session + } + + private fun getCachedSession(module: T, storage: SessionStorage, factory: (T) -> LLFirSession): LLFirSession { + checkCanceled() + + val session = storage.computeIfAbsent(module) { factory(module) } + checkSessionValidity(session) + + return session + } + + private fun checkSessionValidity(session: LLFirSession) { + require(session.isValid) { "A session acquired via `getSession` should always be valid. Module: ${session.ktModule}" } } /** @@ -87,8 +109,9 @@ class LLFirSessionCache(private val project: Project) { val didSourceSessionExist = removeSessionFrom(module, sourceCache) val didBinarySessionExist = module is KtBinaryModule && removeSessionFrom(module, binaryCache) val didDanglingFileSessionExist = module is KtDanglingFileModule && removeSessionFrom(module, danglingFileSessionCache) + val didUnstableDanglingFileSessionExist = module is KtDanglingFileModule && removeSessionFrom(module, unstableDanglingFileSessionCache) - return didSourceSessionExist || didBinarySessionExist || didDanglingFileSessionExist + return didSourceSessionExist || didBinarySessionExist || didDanglingFileSessionExist || didUnstableDanglingFileSessionExist } private fun removeSessionFrom(module: KtModule, storage: SessionStorage): Boolean { @@ -117,7 +140,13 @@ class LLFirSessionCache(private val project: Project) { removeAllDanglingFileSessions() } + fun removeUnstableDanglingFileSessions() { + removeAllSessionsFrom(unstableDanglingFileSessionCache) + } + fun removeContextualDanglingFileSessions(contextModule: KtModule) { + removeUnstableDanglingFileSessions() + if (contextModule is KtDanglingFileModule) { removeAllMatchingSessionsFrom(danglingFileSessionCache) { it is KtDanglingFileModule && hasContextModule(it, contextModule) } } else { @@ -136,6 +165,7 @@ class LLFirSessionCache(private val project: Project) { fun removeAllDanglingFileSessions() { removeAllSessionsFrom(danglingFileSessionCache) + removeAllSessionsFrom(unstableDanglingFileSessionCache) } // Removing script sessions is only needed temporarily until KTIJ-25620 has been implemented. 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 6763ce47aca..7dcd7f1e438 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 @@ -8,6 +8,7 @@ package org.jetbrains.kotlin.analysis.low.level.api.fir.sessions import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.project.Project +import com.intellij.psi.util.PsiModificationTracker import org.jetbrains.kotlin.analysis.project.structure.* import org.jetbrains.kotlin.analysis.providers.KotlinAnchorModuleProvider import org.jetbrains.kotlin.analysis.providers.analysisMessageBus @@ -19,6 +20,10 @@ import org.jetbrains.kotlin.analysis.providers.topics.* * sessions in [LLFirSessionCache] and the cache has to be kept consistent. */ class LLFirSessionInvalidationService(private val project: Project) : Disposable { + private val sessionCache: LLFirSessionCache by lazy { + LLFirSessionCache.getInstance(project) + } + /** * Subscribes to all [modification events][KotlinTopics] via the [analysisMessageBus]. * @@ -53,7 +58,11 @@ class LLFirSessionInvalidationService(private val project: Project) : Disposable ) busConnection.subscribe( KotlinTopics.CODE_FRAGMENT_CONTEXT_MODIFICATION, - KotlinCodeFragmentContextModificationListener { module -> invalidateCodeFragments(module) } + KotlinCodeFragmentContextModificationListener { module -> invalidateContextualDanglingFileSessions(module) } + ) + busConnection.subscribe( + PsiModificationTracker.TOPIC, + PsiModificationTracker.Listener { invalidateUnstableDanglingFileSessions() } ) } @@ -65,7 +74,6 @@ class LLFirSessionInvalidationService(private val project: Project) : Disposable fun invalidate(module: KtModule) { ApplicationManager.getApplication().assertWriteAccessAllowed() - val sessionCache = LLFirSessionCache.getInstance(project) val didSessionExist = sessionCache.removeSession(module) // We don't have to invalidate dependent sessions if the root session does not exist in the cache. It is true that sessions can be @@ -105,11 +113,15 @@ class LLFirSessionInvalidationService(private val project: Project) : Disposable anchorModules?.forEach(::invalidate) } - LLFirSessionCache.getInstance(project).removeAllSessions(includeLibraryModules) + sessionCache.removeAllSessions(includeLibraryModules) } - private fun invalidateCodeFragments(contextModule: KtModule) { - LLFirSessionCache.getInstance(project).removeContextualDanglingFileSessions(contextModule) + private fun invalidateContextualDanglingFileSessions(contextModule: KtModule) { + sessionCache.removeContextualDanglingFileSessions(contextModule) + } + + private fun invalidateUnstableDanglingFileSessions() { + sessionCache.removeUnstableDanglingFileSessions() } override fun dispose() { diff --git a/analysis/project-structure/src/org/jetbrains/kotlin/analysis/project/structure/KtModule.kt b/analysis/project-structure/src/org/jetbrains/kotlin/analysis/project/structure/KtModule.kt index 97aaef3bfe3..47641e183a7 100644 --- a/analysis/project-structure/src/org/jetbrains/kotlin/analysis/project/structure/KtModule.kt +++ b/analysis/project-structure/src/org/jetbrains/kotlin/analysis/project/structure/KtModule.kt @@ -240,6 +240,13 @@ public interface KtDanglingFileModule : KtModule { get() = "Temporary file" } +/** + * True if the dangling file module supports partial invalidation on PSI modifications. + * Sessions for such modules can be cached for longer time. + */ +public val KtDanglingFileModule.isStable: Boolean + get() = file.isPhysical && file.viewProvider.isEventSystemEnabled + /** * A set of sources which live outside the project content root. E.g, testdata files or source files of some other project. */