From 3b0c8ef38e489374cc1ae41506769957d57fe191 Mon Sep 17 00:00:00 2001 From: Vitaly Legchilkin Date: Thu, 12 Feb 2026 21:50:58 +0100 Subject: [PATCH] PY-87576 Fix read access violation in InstallAndImportPackageQuickFix onSuccess() was called from a coroutine on Dispatchers.Default but accessed PSI elements without a read action. Use suspend writeCommandAction to properly dispatch to EDT with write access. Add InstallPackageQuickFixTest to verify that onSuccess correctly adds import statements to the file. Make InstallPackageQuickFix and InstallAndImportPackageQuickFix non-internal (annotated with @ApiStatus.Internal instead) so they are accessible from the test module. GitOrigin-RevId: 4b8ef9194803cdd12192351ef357b18321ba482a --- .../InstallAndImportPackageQuickFix.kt | 28 ++++----- .../quickfix/InstallPackageQuickFix.kt | 8 ++- .../importPandas.py | 1 + .../importPandasAsPd.py | 1 + .../unit/InstallPackageQuickFixTest.kt | 57 +++++++++++++++++++ 5 files changed, 74 insertions(+), 21 deletions(-) create mode 100644 python/testData/quickFixes/InstallPackageQuickFixTest/importPandas.py create mode 100644 python/testData/quickFixes/InstallPackageQuickFixTest/importPandasAsPd.py create mode 100644 python/testSrc/com/intellij/python/junit5Tests/unit/InstallPackageQuickFixTest.kt diff --git a/python/src/com/jetbrains/python/inspections/quickfix/InstallAndImportPackageQuickFix.kt b/python/src/com/jetbrains/python/inspections/quickfix/InstallAndImportPackageQuickFix.kt index e274d348b979..a18a8fc68b7a 100644 --- a/python/src/com/jetbrains/python/inspections/quickfix/InstallAndImportPackageQuickFix.kt +++ b/python/src/com/jetbrains/python/inspections/quickfix/InstallAndImportPackageQuickFix.kt @@ -3,14 +3,17 @@ package com.jetbrains.python.inspections.quickfix import com.intellij.codeInspection.ProblemDescriptor -import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.command.writeCommandAction +import com.intellij.openapi.project.Project import com.intellij.psi.PsiElement import com.jetbrains.python.PyPsiBundle import com.jetbrains.python.codeInsight.imports.AddImportHelper +import org.jetbrains.annotations.ApiStatus import org.jetbrains.annotations.Nls -internal class InstallAndImportPackageQuickFix( - override val packageName: String, +@ApiStatus.Internal +class InstallAndImportPackageQuickFix( + packageName: String, private val importAlias: String?, ) : InstallPackageQuickFix(packageName) { @@ -18,17 +21,10 @@ internal class InstallAndImportPackageQuickFix( override fun getFamilyName(): @Nls String = PyPsiBundle.message("QFIX.install.and.import.package") - override fun onSuccess(descriptor: ProblemDescriptor?) { - executeWriteCommandToAddImport(descriptor?.psiElement ?: return) - } - - private fun executeWriteCommandToAddImport(psiElement: PsiElement) { - WriteCommandAction.writeCommandAction(psiElement.project) - .withName(PyPsiBundle.message("INSP.package.requirements.add.import")) - .withGroupId(GROUP_ID) - .run { - addImportToFile(psiElement) - } + override suspend fun onSuccess(project: Project, descriptor: ProblemDescriptor) { + writeCommandAction(project, PyPsiBundle.message("INSP.package.requirements.add.import")) { + addImportToFile(descriptor.psiElement ?: return@writeCommandAction ) + } } private fun addImportToFile(element: PsiElement) = @@ -39,8 +35,4 @@ internal class InstallAndImportPackageQuickFix( AddImportHelper.ImportPriority.THIRD_PARTY, element ) - - companion object { - private const val GROUP_ID = "Add import" - } } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/inspections/quickfix/InstallPackageQuickFix.kt b/python/src/com/jetbrains/python/inspections/quickfix/InstallPackageQuickFix.kt index 264babdb545c..dd9910dcb590 100644 --- a/python/src/com/jetbrains/python/inspections/quickfix/InstallPackageQuickFix.kt +++ b/python/src/com/jetbrains/python/inspections/quickfix/InstallPackageQuickFix.kt @@ -13,10 +13,12 @@ import com.jetbrains.python.packaging.management.ui.PythonPackageManagerUI import com.jetbrains.python.packaging.utils.PyPackageCoroutine import com.jetbrains.python.sdk.legacy.PythonSdkUtil import com.jetbrains.python.statistics.PyPackagesUsageCollector +import org.jetbrains.annotations.ApiStatus import org.jetbrains.annotations.Nls // this is HighPriorityAction because we always want our fix to appear above lsp tools because our fix is much better -internal open class InstallPackageQuickFix(open val packageName: String) : LocalQuickFix, HighPriorityAction { +@ApiStatus.Internal +open class InstallPackageQuickFix(val packageName: String) : LocalQuickFix, HighPriorityAction { override fun getFamilyName(): @Nls String = PyBundle.message("python.unresolved.reference.inspection.install.package", packageName) override fun applyFix(project: Project, descriptor: ProblemDescriptor) { @@ -25,7 +27,7 @@ internal open class InstallPackageQuickFix(open val packageName: String) : Local ?: return PyPackageCoroutine.launch(project) { PythonPackageManagerUI.forSdk(project, sdk).installWithConfirmation(listOf(packageName)) ?: return@launch - onSuccess(descriptor) + onSuccess(project, descriptor) PyPackagesUsageCollector.installSingleEvent.log() } } @@ -34,7 +36,7 @@ internal open class InstallPackageQuickFix(open val packageName: String) : Local override fun availableInBatchMode(): Boolean = false - open fun onSuccess(descriptor: ProblemDescriptor?) {} + open suspend fun onSuccess(project: Project, descriptor: ProblemDescriptor) {} override fun generatePreview(project: Project, previewDescriptor: ProblemDescriptor): IntentionPreviewInfo = IntentionPreviewInfo.EMPTY } \ No newline at end of file diff --git a/python/testData/quickFixes/InstallPackageQuickFixTest/importPandas.py b/python/testData/quickFixes/InstallPackageQuickFixTest/importPandas.py new file mode 100644 index 000000000000..8d4cd312c083 --- /dev/null +++ b/python/testData/quickFixes/InstallPackageQuickFixTest/importPandas.py @@ -0,0 +1 @@ +pandas.array() diff --git a/python/testData/quickFixes/InstallPackageQuickFixTest/importPandasAsPd.py b/python/testData/quickFixes/InstallPackageQuickFixTest/importPandasAsPd.py new file mode 100644 index 000000000000..0788b100ebbe --- /dev/null +++ b/python/testData/quickFixes/InstallPackageQuickFixTest/importPandasAsPd.py @@ -0,0 +1 @@ +pd.array() diff --git a/python/testSrc/com/intellij/python/junit5Tests/unit/InstallPackageQuickFixTest.kt b/python/testSrc/com/intellij/python/junit5Tests/unit/InstallPackageQuickFixTest.kt new file mode 100644 index 000000000000..359a80ac058c --- /dev/null +++ b/python/testSrc/com/intellij/python/junit5Tests/unit/InstallPackageQuickFixTest.kt @@ -0,0 +1,57 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.python.junit5Tests.unit + +import com.intellij.codeInspection.InspectionManager +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.openapi.application.readAction +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiFile +import com.intellij.python.junit5Tests.framework.PyDefaultTestApplication +import com.intellij.python.junit5Tests.framework.metaInfo.TestClassInfo +import com.intellij.testFramework.TestDataPath +import com.intellij.testFramework.common.timeoutRunBlocking +import com.jetbrains.python.inspections.quickfix.InstallAndImportPackageQuickFix +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +@PyDefaultTestApplication +@TestClassInfo +@TestDataPath($$"$CONTENT_ROOT/../testData/quickFixes/InstallPackageQuickFixTest") +internal class InstallPackageQuickFixTest(val project: Project) { + + @Test + fun testImportPandas(psiFile: PsiFile) = timeoutRunBlocking { + applyInstallAndImportQuickFix(psiFile, packageName = "pandas", importAlias = null) + assertEquals("import pandas\n\npandas.array()\n", psiFile.text) + } + + @Test + fun testImportPandasAsPd(psiFile: PsiFile) = timeoutRunBlocking { + applyInstallAndImportQuickFix(psiFile, packageName = "pandas", importAlias = "pd") + assertEquals("import pandas as pd\n\npd.array()\n", psiFile.text) + } + + /** + * The test currently calls onSuccess directly (bypassing applyFix / real package install), + * because I didn't find an easy way to run the complete inspection. + * The [com.jetbrains.python.packaging.pip.PypiPackageCache] cache is not initialized in tests + * also [com.jetbrains.python.inspections.quickfix.InstallPackageQuickFix] is not available in batch mode and the inspection filters it out + */ + private suspend fun applyInstallAndImportQuickFix(psiFile: PsiFile, packageName: String, importAlias: String?) { + val descriptor = readAction { + val element = psiFile.findElementAt(0)!! + InspectionManager.getInstance(project) + .createProblemDescriptor( + element, + "", + null as LocalQuickFix?, + ProblemHighlightType.GENERIC_ERROR_OR_WARNING, + true + ) + } + + val quickFix = InstallAndImportPackageQuickFix(packageName, importAlias) + quickFix.onSuccess(project, descriptor) + } +}