[AA] forbid analyze from write action

Analysis is not supposed to be called from write action.
Such actions can lead to IDE freezes and incorrect behavior

^KT-60586 Fixed
This commit is contained in:
Dmitrii Gridin
2023-07-20 15:12:12 +02:00
committed by Space Team
parent c624bba8cd
commit 88da053408
5 changed files with 63 additions and 13 deletions
@@ -1,5 +1,5 @@
/* /*
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors. * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/ */
@@ -11,3 +11,6 @@ public annotation class KtAnalysisApiInternals
@RequiresOptIn("Analysis should not be allowed to be ran from EDT thread, otherwise it may cause IDE freezes") @RequiresOptIn("Analysis should not be allowed to be ran from EDT thread, otherwise it may cause IDE freezes")
public annotation class KtAllowAnalysisOnEdt public annotation class KtAllowAnalysisOnEdt
@RequiresOptIn("Analysis should not be allowed to be ran from write action, otherwise it may cause IDE freezes and incorrect behavior in some cases")
public annotation class KtAllowAnalysisFromWriteAction
@@ -1,9 +1,9 @@
/* /*
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors. * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/ */
@file:OptIn(KtAnalysisApiInternals::class) @file:OptIn(KtAnalysisApiInternals::class, KtAllowProhibitedAnalyzeFromWriteAction::class)
package org.jetbrains.kotlin.analysis.api.lifetime package org.jetbrains.kotlin.analysis.api.lifetime
@@ -31,6 +31,7 @@ public class KtReadActionConfinementLifetimeToken(project: Project) : KtLifetime
override fun isAccessible(): Boolean { override fun isAccessible(): Boolean {
val application = ApplicationManager.getApplication() val application = ApplicationManager.getApplication()
if (application.isDispatchThread && !allowOnEdt.get()) return false if (application.isDispatchThread && !allowOnEdt.get()) return false
if (application.isWriteAccessAllowed && !allowFromWriteAction.get()) return false
if (KtAnalysisAllowanceManager.resolveIsForbiddenInActionWithName.get() != null) return false if (KtAnalysisAllowanceManager.resolveIsForbiddenInActionWithName.get() != null) return false
if (!application.isReadAccessAllowed) return false if (!application.isReadAccessAllowed) return false
if (!KtReadActionConfinementLifetimeTokenFactory.isInsideAnalysisContext()) return false if (!KtReadActionConfinementLifetimeTokenFactory.isInsideAnalysisContext()) return false
@@ -41,6 +42,7 @@ public class KtReadActionConfinementLifetimeToken(project: Project) : KtLifetime
override fun getInaccessibilityReason(): String { override fun getInaccessibilityReason(): String {
val application = ApplicationManager.getApplication() val application = ApplicationManager.getApplication()
if (application.isDispatchThread && !allowOnEdt.get()) return "Called in EDT thread" if (application.isDispatchThread && !allowOnEdt.get()) return "Called in EDT thread"
if (application.isWriteAccessAllowed && !allowFromWriteAction.get()) return "Called from write action"
if (!application.isReadAccessAllowed) return "Called outside read action" if (!application.isReadAccessAllowed) return "Called outside read action"
KtAnalysisAllowanceManager.resolveIsForbiddenInActionWithName.get()?.let { actionName -> KtAnalysisAllowanceManager.resolveIsForbiddenInActionWithName.get()?.let { actionName ->
return "Resolve is forbidden in $actionName" return "Resolve is forbidden in $actionName"
@@ -55,6 +57,10 @@ public class KtReadActionConfinementLifetimeToken(project: Project) : KtLifetime
public companion object { public companion object {
@KtAnalysisApiInternals @KtAnalysisApiInternals
public val allowOnEdt: ThreadLocal<Boolean> = ThreadLocal.withInitial { false } public val allowOnEdt: ThreadLocal<Boolean> = ThreadLocal.withInitial { false }
@KtAnalysisApiInternals
@KtAllowProhibitedAnalyzeFromWriteAction
public val allowFromWriteAction: ThreadLocal<Boolean> = ThreadLocal.withInitial { false }
} }
public override val factory: KtLifetimeTokenFactory = KtReadActionConfinementLifetimeTokenFactory public override val factory: KtLifetimeTokenFactory = KtReadActionConfinementLifetimeTokenFactory
@@ -78,13 +84,15 @@ public object KtReadActionConfinementLifetimeTokenFactory : KtLifetimeTokenFacto
private val lifetimeOwnersStack = ThreadLocal.withInitial<PersistentList<KtLifetimeToken>> { persistentListOf() } private val lifetimeOwnersStack = ThreadLocal.withInitial<PersistentList<KtLifetimeToken>> { persistentListOf() }
internal fun isInsideAnalysisContext() = lifetimeOwnersStack.get().size > 0 internal fun isInsideAnalysisContext() = lifetimeOwnersStack.get().isNotEmpty()
internal fun currentToken() = lifetimeOwnersStack.get().last() internal fun currentToken() = lifetimeOwnersStack.get().last()
} }
@RequiresOptIn("Analysis should be prohibited to be ran from write action, otherwise it may cause IDE freezes and incorrect behavior in some cases")
private annotation class KtAllowProhibitedAnalyzeFromWriteAction
/** /**
*
* @see KtAnalysisSession * @see KtAnalysisSession
* @see KtReadActionConfinementLifetimeToken * @see KtReadActionConfinementLifetimeToken
*/ */
@@ -98,3 +106,22 @@ public inline fun <T> allowAnalysisOnEdt(action: () -> T): T {
KtReadActionConfinementLifetimeToken.allowOnEdt.set(false) KtReadActionConfinementLifetimeToken.allowOnEdt.set(false)
} }
} }
/**
* Analysis is not supposed to be called from write action.
* Such actions can lead to IDE freezes and incorrect behavior in some cases.
*
* @see KtAnalysisSession
* @see KtReadActionConfinementLifetimeToken
*/
@KtAllowAnalysisFromWriteAction
@KtAllowProhibitedAnalyzeFromWriteAction
public inline fun <T> allowAnalysisFromWriteAction(action: () -> T): T {
if (KtReadActionConfinementLifetimeToken.allowFromWriteAction.get()) return action()
KtReadActionConfinementLifetimeToken.allowFromWriteAction.set(true)
try {
return action()
} finally {
KtReadActionConfinementLifetimeToken.allowFromWriteAction.set(false)
}
}
@@ -39,6 +39,7 @@ projectTest(jUnitMode = JUnitMode.JUnit5) {
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions.freeCompilerArgs += "-Xcontext-receivers" kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
kotlinOptions.freeCompilerArgs += "-opt-in=org.jetbrains.kotlin.analysis.api.lifetime.KtAllowProhibitedAnalyzeFromWriteAction"
} }
@@ -8,9 +8,11 @@ package org.jetbrains.kotlin.light.classes.symbol
import com.intellij.psi.JavaPsiFacade import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiElement import com.intellij.psi.PsiElement
import com.intellij.psi.PsiType import com.intellij.psi.PsiType
import org.jetbrains.kotlin.analysis.api.KtAllowAnalysisFromWriteAction
import org.jetbrains.kotlin.analysis.api.KtAllowAnalysisOnEdt import org.jetbrains.kotlin.analysis.api.KtAllowAnalysisOnEdt
import org.jetbrains.kotlin.analysis.api.KtAnalysisSession import org.jetbrains.kotlin.analysis.api.KtAnalysisSession
import org.jetbrains.kotlin.analysis.api.analyze import org.jetbrains.kotlin.analysis.api.analyze
import org.jetbrains.kotlin.analysis.api.lifetime.allowAnalysisFromWriteAction
import org.jetbrains.kotlin.analysis.api.lifetime.allowAnalysisOnEdt import org.jetbrains.kotlin.analysis.api.lifetime.allowAnalysisOnEdt
import org.jetbrains.kotlin.analysis.project.structure.KtModule import org.jetbrains.kotlin.analysis.project.structure.KtModule
import org.jetbrains.kotlin.builtins.StandardNames import org.jetbrains.kotlin.builtins.StandardNames
@@ -19,8 +21,10 @@ import org.jetbrains.kotlin.psi.KtElement
internal fun PsiElement.nonExistentType(): PsiType = internal fun PsiElement.nonExistentType(): PsiType =
JavaPsiFacade.getElementFactory(project).createTypeFromText(StandardNames.NON_EXISTENT_CLASS.asString(), this) JavaPsiFacade.getElementFactory(project).createTypeFromText(StandardNames.NON_EXISTENT_CLASS.asString(), this)
@OptIn(KtAllowAnalysisOnEdt::class) @OptIn(KtAllowAnalysisOnEdt::class, KtAllowAnalysisFromWriteAction::class)
private inline fun <E> allowLightClassesOnEdt(crossinline action: () -> E): E = allowAnalysisOnEdt(action) private inline fun <E> allowLightClassesOnEdt(crossinline action: () -> E): E = allowAnalysisFromWriteAction {
allowAnalysisOnEdt(action)
}
internal inline fun <R> analyzeForLightClasses(context: KtElement, crossinline action: KtAnalysisSession.() -> R): R = internal inline fun <R> analyzeForLightClasses(context: KtElement, crossinline action: KtAnalysisSession.() -> R): R =
allowLightClassesOnEdt { allowLightClassesOnEdt {
@@ -1,24 +1,22 @@
/* /*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/ */
package org.jetbrains.kotlin.cli.jvm.compiler package org.jetbrains.kotlin.cli.jvm.compiler
import com.intellij.DynamicBundle import com.intellij.DynamicBundle
import com.intellij.codeInsight.ContainerProvider import com.intellij.codeInsight.ContainerProvider
import com.intellij.codeInsight.folding.JavaCodeFoldingSettings
import com.intellij.codeInsight.folding.impl.JavaCodeFoldingSettingsBase
import com.intellij.codeInsight.runner.JavaMainMethodProvider import com.intellij.codeInsight.runner.JavaMainMethodProvider
import com.intellij.core.JavaCoreApplicationEnvironment import com.intellij.core.JavaCoreApplicationEnvironment
import com.intellij.ide.highlighter.JavaClassFileType import com.intellij.ide.highlighter.JavaClassFileType
import com.intellij.lang.MetaLanguage import com.intellij.lang.MetaLanguage
import com.intellij.mock.MockApplication
import com.intellij.openapi.Disposable import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.Disposer
import com.intellij.openapi.vfs.VirtualFileSystem import com.intellij.openapi.vfs.VirtualFileSystem
import com.intellij.psi.FileContextProvider import com.intellij.psi.FileContextProvider
import com.intellij.psi.augment.PsiAugmentProvider import com.intellij.psi.augment.PsiAugmentProvider
import com.intellij.psi.codeStyle.JavaFileCodeStyleFacade
import com.intellij.psi.codeStyle.JavaFileCodeStyleFacadeFactory import com.intellij.psi.codeStyle.JavaFileCodeStyleFacadeFactory
import com.intellij.psi.impl.smartPointers.SmartPointerAnchorProvider import com.intellij.psi.impl.smartPointers.SmartPointerAnchorProvider
import com.intellij.psi.meta.MetaDataContributor import com.intellij.psi.meta.MetaDataContributor
@@ -28,8 +26,7 @@ import org.jetbrains.kotlin.cli.jvm.modules.CoreJrtFileSystem
class KotlinCoreApplicationEnvironment private constructor( class KotlinCoreApplicationEnvironment private constructor(
parentDisposable: Disposable, unitTestMode: Boolean parentDisposable: Disposable, unitTestMode: Boolean
) : ) : JavaCoreApplicationEnvironment(parentDisposable, unitTestMode) {
JavaCoreApplicationEnvironment(parentDisposable, unitTestMode) {
init { init {
registerApplicationService(JavaFileCodeStyleFacadeFactory::class.java, DummyJavaFileCodeStyleFacadeFactory()) registerApplicationService(JavaFileCodeStyleFacadeFactory::class.java, DummyJavaFileCodeStyleFacadeFactory())
@@ -40,6 +37,24 @@ class KotlinCoreApplicationEnvironment private constructor(
return CoreJrtFileSystem() return CoreJrtFileSystem()
} }
override fun createApplication(parentDisposable: Disposable): MockApplication {
val mock = super.createApplication(parentDisposable)
/**
* We can't use just [unitTestMode] from constructor, because at this moment
* the corresponding property is not yet initialized, so [unitTestMode] is effectively always false,
* because this function called from super class constructor
*/
return if (mock.isUnitTestMode) {
object : MockApplication(parentDisposable) {
override fun isUnitTestMode(): Boolean = true
override fun isWriteAccessAllowed(): Boolean = false
}
} else {
mock
}
}
private var fastJarFileSystemField: FastJarFileSystem? = null private var fastJarFileSystemField: FastJarFileSystem? = null
private var fastJarFileSystemFieldInitialized = false private var fastJarFileSystemFieldInitialized = false