diff --git a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/ResolveInWriteActionManager.kt b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/ResolveInWriteActionManager.kt index 821ce0fc480..6429dd1b754 100644 --- a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/ResolveInWriteActionManager.kt +++ b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/ResolveInWriteActionManager.kt @@ -8,6 +8,7 @@ package org.jetbrains.kotlin.idea.caches.resolve import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.util.registry.Registry +import org.jetbrains.annotations.TestOnly /** * Temporary allow resolve in write action. @@ -19,10 +20,24 @@ fun allowResolveInWriteAction(runnable: () -> T): T { return ResolveInWriteActionManager.runWithResolveAllowedInWriteAction(runnable) } +/** + * Force resolve check in tests. + */ +@TestOnly +fun forceResolveInWriteActionCheckInTests(errorHandler: (() -> Unit)? = null, runnable: () -> T): T { + return ResolveInWriteActionManager.runWithForceResolveAllowedCheckInTests(errorHandler, runnable) +} + +private const val RESOLVE_IN_WRITE_ACTION_ERROR_MESSAGE = "Resolve is not allowed under the write action!" + +class ResolveInWriteActionException(message: String? = null) : IllegalThreadStateException(message ?: RESOLVE_IN_WRITE_ACTION_ERROR_MESSAGE) + internal object ResolveInWriteActionManager { private val LOG = Logger.getInstance(ResolveInWriteActionManager::class.java) private val isResolveInWriteActionAllowed: ThreadLocal = ThreadLocal.withInitial { false } + private val isForceCheckInTests: ThreadLocal = ThreadLocal.withInitial { false } + private val errorHandler: ThreadLocal<(() -> Unit)?> = ThreadLocal.withInitial { null } fun assertNoResolveUnderWriteAction() { if (isResolveInWriteActionAllowed.get()) return @@ -31,15 +46,25 @@ internal object ResolveInWriteActionManager { if (!application.isWriteAccessAllowed) return - if (application.isUnitTestMode) return + if (application.isUnitTestMode) { + if (!isForceCheckInTests.get()) return - @Suppress("InvalidBundleOrProperty") - if (!Registry.`is`("kotlin.write.resolve.check", false)) return + val handler = errorHandler.get() + if (handler != null) { + handler() + return + } - LOG.error("Resolve is not allowed under the write action!") + throw ResolveInWriteActionException() + } else { + @Suppress("InvalidBundleOrProperty") + if (!Registry.`is`("kotlin.write.resolve.check", false)) return + + LOG.error(RESOLVE_IN_WRITE_ACTION_ERROR_MESSAGE) + } } - inline fun runWithResolveAllowedInWriteAction(runnable: () -> T): T { + internal inline fun runWithResolveAllowedInWriteAction(runnable: () -> T): T { val wasSet = if (ApplicationManager.getApplication()?.isWriteAccessAllowed == true && isResolveInWriteActionAllowed.get() == false) { isResolveInWriteActionAllowed.set(true) @@ -56,4 +81,23 @@ internal object ResolveInWriteActionManager { } } } + + internal fun runWithForceResolveAllowedCheckInTests(errorHandler: (() -> Unit)?, runnable: () -> T): T { + val wasSet = if (isForceCheckInTests.get() == false) { + isForceCheckInTests.set(true) + this.errorHandler.set(errorHandler) + true + } else { + false + } + + try { + return runnable() + } finally { + if (wasSet) { + isForceCheckInTests.set(false) + this.errorHandler.set(null) + } + } + } } \ No newline at end of file diff --git a/idea/testData/quickfix/allowResolveInWriteAction.txt b/idea/testData/quickfix/allowResolveInWriteAction.txt new file mode 100644 index 00000000000..d8599da6518 --- /dev/null +++ b/idea/testData/quickfix/allowResolveInWriteAction.txt @@ -0,0 +1,68 @@ +# Actions that are allowed to resolve in write action. Normally this set shouldn't be extended and eventually should be dropped. +# Please consider rewriting a quick-fix and remove resolve from it before adding a new entry to this list. + +org.jetbrains.kotlin.idea.core.overrideImplement.ImplementMembersHandler +org.jetbrains.kotlin.idea.inspections.IntentionBasedInspection$HighPriorityIntentionBasedQuickFix +org.jetbrains.kotlin.idea.inspections.KotlinUnusedImportInspection$OptimizeImportsQuickFix +org.jetbrains.kotlin.idea.inspections.SafeDeleteFix +org.jetbrains.kotlin.idea.inspections.SuspiciousCollectionReassignmentInspection$ChangeTypeToMutableFix +org.jetbrains.kotlin.idea.inspections.SuspiciousCollectionReassignmentInspection$JoinWithInitializerFix +org.jetbrains.kotlin.idea.inspections.UnusedReceiverParameterInspection$RemoveReceiverFix +org.jetbrains.kotlin.idea.inspections.migration.ObsoleteCoroutineUsageFix +org.jetbrains.kotlin.idea.intentions.AddPropertyAccessorsIntention +org.jetbrains.kotlin.idea.intentions.AddPropertyGetterIntention +org.jetbrains.kotlin.idea.intentions.AddPropertySetterIntention +org.jetbrains.kotlin.idea.intentions.ConvertPropertyInitializerToGetterIntention +org.jetbrains.kotlin.idea.intentions.CreateKotlinSubClassIntention +org.jetbrains.kotlin.idea.intentions.MovePropertyToConstructorIntention +org.jetbrains.kotlin.idea.intentions.SpecifyTypeExplicitlyIntention +org.jetbrains.kotlin.idea.quickfix.AddAnnotationTargetFix +org.jetbrains.kotlin.idea.quickfix.AddExclExclCallFix +org.jetbrains.kotlin.idea.quickfix.AddFunctionToSupertypeFix +org.jetbrains.kotlin.idea.quickfix.AddModifierFix +org.jetbrains.kotlin.idea.quickfix.AddNameToArgumentFix +org.jetbrains.kotlin.idea.quickfix.AddPropertyToSupertypeFix +org.jetbrains.kotlin.idea.quickfix.AddStartProjectionsForInnerClass +org.jetbrains.kotlin.idea.quickfix.AddWhenRemainingBranchesFix +org.jetbrains.kotlin.idea.quickfix.AssignToPropertyFix +org.jetbrains.kotlin.idea.quickfix.ChangeAccessorTypeFix +org.jetbrains.kotlin.idea.quickfix.ChangeCallableReturnTypeFix$ForEnclosing +org.jetbrains.kotlin.idea.quickfix.ChangeParameterTypeFix +org.jetbrains.kotlin.idea.quickfix.ChangeToMutableCollectionFix +org.jetbrains.kotlin.idea.quickfix.ChangeVisibilityFix$ChangeToInternalFix +org.jetbrains.kotlin.idea.quickfix.ChangeVisibilityFix$ChangeToPrivateFix +org.jetbrains.kotlin.idea.quickfix.ChangeVisibilityFix$ChangeToProtectedFix +org.jetbrains.kotlin.idea.quickfix.ChangeVisibilityFix$ChangeToPublicFix +org.jetbrains.kotlin.idea.quickfix.ConvertExtensionPropertyInitializerToGetterFix +org.jetbrains.kotlin.idea.quickfix.ConvertMemberToExtensionFix +org.jetbrains.kotlin.idea.quickfix.ConvertToAnonymousObjectFix +org.jetbrains.kotlin.idea.quickfix.ImportFix +org.jetbrains.kotlin.idea.quickfix.InitializePropertyQuickFixFactory$AddInitializerFix +org.jetbrains.kotlin.idea.quickfix.InitializePropertyQuickFixFactory$InitializeWithConstructorParameter +org.jetbrains.kotlin.idea.quickfix.InitializePropertyQuickFixFactory$MoveToConstructorParameters +org.jetbrains.kotlin.idea.quickfix.InlineTypeParameterFix +org.jetbrains.kotlin.idea.quickfix.InsertDelegationCallQuickfix +org.jetbrains.kotlin.idea.quickfix.KotlinSuppressIntentionAction +org.jetbrains.kotlin.idea.quickfix.LetImplementInterfaceFix +org.jetbrains.kotlin.idea.quickfix.LowPriorityQuickFixWithDelegateFactory +org.jetbrains.kotlin.idea.quickfix.QuickFixWithDelegateFactory +org.jetbrains.kotlin.idea.quickfix.RemoveNameFromFunctionExpressionFix +org.jetbrains.kotlin.idea.quickfix.RemovePartsFromPropertyFix +org.jetbrains.kotlin.idea.quickfix.RemoveRedundantInitializerFix +org.jetbrains.kotlin.idea.quickfix.RemoveUnusedFunctionParameterFix +org.jetbrains.kotlin.idea.quickfix.RemoveUnusedValueFix +org.jetbrains.kotlin.idea.quickfix.RenameModToRemFix +org.jetbrains.kotlin.idea.quickfix.RenameParameterToMatchOverriddenMethodFix +org.jetbrains.kotlin.idea.quickfix.RenameUnresolvedReferenceFix +org.jetbrains.kotlin.idea.quickfix.ReplaceInfixOrOperatorCallFix +org.jetbrains.kotlin.idea.quickfix.RestrictedRetentionForExpressionAnnotationFactory$AddSourceRetentionFix +org.jetbrains.kotlin.idea.quickfix.SimplifyComparisonFix +org.jetbrains.kotlin.idea.quickfix.SmartCastImpossibleInIfThenFactory$createQuickFix$1 +org.jetbrains.kotlin.idea.quickfix.SpecifyOverrideExplicitlyFix +org.jetbrains.kotlin.idea.quickfix.SpecifyTypeExplicitlyFix +org.jetbrains.kotlin.idea.quickfix.SuperClassNotInitialized$AddParenthesisFix +org.jetbrains.kotlin.idea.quickfix.WrapWithSafeLetCallFix +org.jetbrains.kotlin.idea.quickfix.createFromUsage.createVariable.CreateLocalVariableActionFactory$createAction$2 +org.jetbrains.kotlin.idea.quickfix.migration.MigrateExternalExtensionFix +org.jetbrains.kotlin.idea.quickfix.replaceWith.DeprecatedSymbolUsageFix +org.jetbrains.kotlin.idea.quickfix.replaceWith.DeprecatedSymbolUsageInWholeProjectFix \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/quickfix/AbstractQuickFixTest.kt b/idea/tests/org/jetbrains/kotlin/idea/quickfix/AbstractQuickFixTest.kt index dedffaf7879..7dd454e2dc4 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/quickfix/AbstractQuickFixTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/quickfix/AbstractQuickFixTest.kt @@ -7,7 +7,10 @@ package org.jetbrains.kotlin.idea.quickfix import com.intellij.codeInsight.daemon.quickFix.ActionHint import com.intellij.codeInsight.intention.IntentionAction +import com.intellij.codeInsight.intention.IntentionActionDelegate +import com.intellij.codeInsight.intention.impl.config.IntentionActionWrapper import com.intellij.codeInspection.SuppressableProblemGroup +import com.intellij.codeInspection.ex.QuickFixWrapper import com.intellij.openapi.command.CommandProcessor import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.util.text.StringUtil @@ -18,6 +21,8 @@ import com.intellij.testFramework.LightProjectDescriptor import com.intellij.testFramework.UsefulTestCase import com.intellij.util.ui.UIUtil import junit.framework.TestCase +import org.jetbrains.kotlin.idea.caches.resolve.ResolveInWriteActionException +import org.jetbrains.kotlin.idea.caches.resolve.forceResolveInWriteActionCheckInTests import org.jetbrains.kotlin.idea.facet.KotlinFacet import org.jetbrains.kotlin.idea.test.* import org.jetbrains.kotlin.psi.KtFile @@ -26,8 +31,28 @@ import org.junit.Assert import org.junit.ComparisonFailure import java.io.File import java.io.IOException +import kotlin.collections.HashSet abstract class AbstractQuickFixTest : KotlinLightCodeInsightFixtureTestCase(), QuickFixTest { + companion object { + private val quickFixesAllowedToResolveInWriteAction = AllowedToResolveUnderWriteActionData( + "idea/testData/quickfix/allowResolveInWriteAction.txt", + """ + # Actions that are allowed to resolve in write action. Normally this list shouldn't be extended and eventually should + # be dropped. Please consider rewriting a quick-fix and remove resolve from it before adding a new entry to this list. + """.trimIndent() + ) + + private fun unwrapIntention(action: Any): Any { + return when (action) { + is IntentionActionDelegate -> unwrapIntention(action.delegate) + is IntentionActionWrapper -> unwrapIntention(action.delegate) + is QuickFixWrapper -> unwrapIntention(action.fix) + else -> action + } + } + } + @Throws(Exception::class) protected fun doTest(beforeFileName: String) { val beforeFileText = FileUtil.loadFile(File(beforeFileName)) @@ -144,12 +169,25 @@ abstract class AbstractQuickFixTest : KotlinLightCodeInsightFixtureTestCase(), Q val intention = findActionWithText(actionHint.expectedText) if (actionHint.shouldPresent()) { if (intention == null) { - fail("Action with text '" + actionHint.expectedText + "' not found\nAvailable actions: " + - myFixture.availableIntentions.joinToString(prefix = "[", postfix = "]") { it.text }) + fail( + "Action with text '" + actionHint.expectedText + "' not found\nAvailable actions: " + + myFixture.availableIntentions.joinToString(prefix = "[", postfix = "]") { it.text }) + return + } + + val writeActionResolveHandler: () -> Unit = { + val unwrappedIntention = unwrapIntention(intention) + + val intentionClassName = unwrappedIntention.javaClass.name + if (!quickFixesAllowedToResolveInWriteAction.isWriteActionAllowed(intentionClassName)) { + throw ResolveInWriteActionException() + } } val stubComparisonFailure: ComparisonFailure? = try { - myFixture.launchAction(intention!!) + forceResolveInWriteActionCheckInTests(writeActionResolveHandler) { + myFixture.launchAction(intention) + } null } catch (comparisonFailure: ComparisonFailure) { comparisonFailure diff --git a/idea/tests/org/jetbrains/kotlin/idea/quickfix/AllowedToResolveUnderWriteActionData.kt b/idea/tests/org/jetbrains/kotlin/idea/quickfix/AllowedToResolveUnderWriteActionData.kt new file mode 100644 index 00000000000..dfd83a171a0 --- /dev/null +++ b/idea/tests/org/jetbrains/kotlin/idea/quickfix/AllowedToResolveUnderWriteActionData.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. 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.idea.quickfix + +import java.io.File +import java.util.* + +class AllowedToResolveUnderWriteActionData(path: String, private val comment: String) { + private val file = File(path) + + private val allowedActions = if (file.exists()) { + file.readLines().filter { + val trimmed = it.trim() + !(trimmed.isEmpty() || trimmed.startsWith("#")) + }.toSet() + } else { + setOf() + } + + // Set to true to update test data. + private val updateTestData = false + private val updateTestDataSet: MutableSet = TreeSet(allowedActions) + + fun isWriteActionAllowed(action: String): Boolean { + if (action in allowedActions) { + return true + } + + @Suppress("ConstantConditionIf") + if (updateTestData && updateTestDataSet.add(action)) { + file.writeText( + "$comment\n\n${updateTestDataSet.joinToString("\n")}" + ) + } + + return false + } +} \ No newline at end of file